diff --git a/.gitignore b/.gitignore
index f05fdc6..cb8d013 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,6 @@ package-lock.json
# Code Coverage
.nyc_output/
coverage/
+
+# Build
+dist/
diff --git a/.gitpod.yml b/.gitpod.yml
index 2b06da2..d4e9861 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -1,2 +1,2 @@
tasks:
- - init: npm install
+ - init: pnpm install
diff --git a/docs/index.html b/docs/index.html
index c84c10e..216b805 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/lib/data/compat-grant.js b/lib/data/compat-grant.ts
similarity index 95%
rename from lib/data/compat-grant.js
rename to lib/data/compat-grant.ts
index 642ebd9..7d92316 100644
--- a/lib/data/compat-grant.js
+++ b/lib/data/compat-grant.ts
@@ -1,8 +1,10 @@
+import type { CompatMap, VersionAssertion } from './version-assertion';
+
// Documentation:
// - Tampermonkey: https://www.tampermonkey.net/documentation.php#_grant
// - Violentmonkey: https://violentmonkey.github.io/api/gm
// - Greasemonkey: https://wiki.greasespot.net/Greasemonkey_Manual:API
-const compatMap = {
+export const compatMap: CompatMap = {
'GM.addElement': [
{ type: 'tampermonkey', versionConstraint: '>=4.11.6113' },
{ type: 'violentmonkey', versionConstraint: '>=2.13.0-beta.3' }
@@ -204,7 +206,20 @@ const compatMap = {
'window.onurlchange': [{ type: 'tampermonkey', versionConstraint: '>=4.11' }]
};
-const gmPolyfillOverride = {
+export const gmPolyfillOverride: {
+ [Key in keyof typeof compatMap]?:
+ | 'ignore'
+ | {
+ deps: (keyof typeof compatMap)[];
+ versions: VersionAssertion[];
+ }
+ | {
+ deps: (keyof typeof compatMap)[];
+ }
+ | {
+ versions: VersionAssertion[];
+ };
+} = {
GM_addStyle: 'ignore',
GM_registerMenuCommand: 'ignore',
GM_getResourceText: {
@@ -251,6 +266,3 @@ const gmPolyfillOverride = {
deps: ['GM_getResourceText']
}
};
-
-module.exports.compatMap = compatMap;
-module.exports.gmPolyfillOverride = gmPolyfillOverride;
diff --git a/lib/data/compat-headers.js b/lib/data/compat-headers.ts
similarity index 97%
rename from lib/data/compat-headers.js
rename to lib/data/compat-headers.ts
index b9e2e3a..f34feca 100644
--- a/lib/data/compat-headers.js
+++ b/lib/data/compat-headers.ts
@@ -1,8 +1,14 @@
+import { CompatMap } from './version-assertion';
+
// Documentation:
// - Tampermonkey: https://www.tampermonkey.net/documentation.php
// - Violentmonkey: https://violentmonkey.github.io/api/metadata-block/
// - Greasemonkey: https://wiki.greasespot.net/Metadata_Block
-const compatMap = {
+export const compatMap: {
+ localized: CompatMap;
+ unlocalized: CompatMap;
+ nonFunctional: CompatMap;
+} = {
localized: {
name: [
{ type: 'tampermonkey', versionConstraint: '>=3.9' },
@@ -186,5 +192,3 @@ const compatMap = {
developer: []
}
};
-
-module.exports = compatMap;
diff --git a/lib/data/version-assertion.ts b/lib/data/version-assertion.ts
new file mode 100644
index 0000000..dd06b69
--- /dev/null
+++ b/lib/data/version-assertion.ts
@@ -0,0 +1,8 @@
+export type VersionAssertion = {
+ type: 'tampermonkey' | 'violentmonkey' | 'greasemonkey';
+ versionConstraint: string;
+};
+
+export type CompatMap = {
+ [Key in string]?: VersionAssertion[];
+};
diff --git a/lib/index.js b/lib/index.js
deleted file mode 100644
index e3baec7..0000000
--- a/lib/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-'use strict';
-
-const requireIndex = require('requireindex');
-
-module.exports.rules = Object.fromEntries(
- Object.entries(requireIndex(__dirname + '/rules')).map(
- ([ruleName, ruleMeta]) => {
- return [
- ruleName,
- {
- ...ruleMeta,
- meta: {
- ...ruleMeta.meta,
- docs: {
- ...ruleMeta.meta.docs,
- url: `https://yash-singh1.github.io/eslint-plugin-userscripts/#/rules/${ruleName}`
- }
- }
- }
- ];
- }
- )
-);
-
-module.exports.configs = {
- recommended: {
- plugins: ['userscripts'],
- rules: {
- 'userscripts/filename-user': ['error', 'always'],
- 'userscripts/no-invalid-metadata': ['error', { top: 'required' }],
- 'userscripts/require-name': ['error', 'required'],
- 'userscripts/require-description': ['error', 'required'],
- 'userscripts/require-version': ['error', 'required'],
- 'userscripts/require-attribute-space-prefix': 'error',
- 'userscripts/use-homepage-and-url': 'error',
- 'userscripts/require-download-url': 'error',
- 'userscripts/align-attributes': ['error', 2],
- 'userscripts/metadata-spacing': ['error', 'always'],
- 'userscripts/no-invalid-headers': 'error',
- 'userscripts/no-invalid-grant': 'error',
- 'userscripts/compat-grant': 'off',
- 'userscripts/compat-headers': 'off',
- 'userscripts/better-use-match': 'warn'
- }
- }
-};
diff --git a/lib/index.ts b/lib/index.ts
new file mode 100644
index 0000000..0bbe5b9
--- /dev/null
+++ b/lib/index.ts
@@ -0,0 +1,77 @@
+'use strict';
+
+import alignAttributes from './rules/align-attributes';
+import betterUseMatch from './rules/better-use-match';
+import compatGrant from './rules/compat-grant';
+import compatHeaders from './rules/compat-headers';
+import filenameUser from './rules/filename-user';
+import metadataSpacing from './rules/metadata-spacing';
+import noInvalidGrant from './rules/no-invalid-grant';
+import noInvalidHeaders from './rules/no-invalid-headers';
+import noInvalidMetadata from './rules/no-invalid-metadata';
+import requireAttributeSpacePrefix from './rules/require-attribute-space-prefix';
+import requireDescription from './rules/require-description';
+import requireDownloadUrl from './rules/require-download-url';
+import requireName from './rules/require-name';
+import requireVersion from './rules/require-version';
+import useHomepageAndUrl from './rules/use-homepage-and-url';
+import type { ESLint } from 'eslint';
+
+const rules = Object.fromEntries(
+ Object.entries({
+ 'align-attributes': alignAttributes,
+ 'better-use-match': betterUseMatch,
+ 'compat-grant': compatGrant,
+ 'compat-headers': compatHeaders,
+ 'filename-user': filenameUser,
+ 'metadata-spacing': metadataSpacing,
+ 'no-invalid-grant': noInvalidGrant,
+ 'no-invalid-headers': noInvalidHeaders,
+ 'no-invalid-metadata': noInvalidMetadata,
+ 'require-attribute-space-prefix': requireAttributeSpacePrefix,
+ 'require-description': requireDescription,
+ 'require-download-url': requireDownloadUrl,
+ 'require-name': requireName,
+ 'require-version': requireVersion,
+ 'use-homepage-and-url': useHomepageAndUrl
+ }).map(([ruleName, ruleMeta]) => {
+ return [
+ ruleName,
+ {
+ ...ruleMeta,
+ meta: {
+ ...ruleMeta.meta,
+ docs: {
+ ...ruleMeta.meta.docs,
+ url: `https://yash-singh1.github.io/eslint-plugin-userscripts/#/rules/${ruleName}`
+ }
+ }
+ }
+ ];
+ })
+) satisfies ESLint.Plugin['rules'];
+
+const configs = {
+ recommended: {
+ plugins: ['userscripts'],
+ rules: {
+ 'userscripts/filename-user': ['error', 'always'],
+ 'userscripts/no-invalid-metadata': ['error', { top: 'required' }],
+ 'userscripts/require-name': ['error', 'required'],
+ 'userscripts/require-description': ['error', 'required'],
+ 'userscripts/require-version': ['error', 'required'],
+ 'userscripts/require-attribute-space-prefix': 'error',
+ 'userscripts/use-homepage-and-url': 'error',
+ 'userscripts/require-download-url': 'error',
+ 'userscripts/align-attributes': ['error', 2],
+ 'userscripts/metadata-spacing': ['error', 'always'],
+ 'userscripts/no-invalid-headers': 'error',
+ 'userscripts/no-invalid-grant': 'error',
+ 'userscripts/compat-grant': 'off',
+ 'userscripts/compat-headers': 'off',
+ 'userscripts/better-use-match': 'warn'
+ }
+ }
+} satisfies ESLint.Plugin['configs'];
+
+export { rules, configs };
diff --git a/lib/rules/align-attributes.js b/lib/rules/align-attributes.ts
similarity index 74%
rename from lib/rules/align-attributes.js
rename to lib/rules/align-attributes.ts
index 7f8ba2d..b25cd21 100644
--- a/lib/rules/align-attributes.js
+++ b/lib/rules/align-attributes.ts
@@ -1,4 +1,8 @@
-module.exports = {
+import type { Rule } from 'eslint';
+import type { Position } from 'estree';
+import type { NonNullishComment } from '../utils/comment';
+
+export default {
meta: {
type: 'suggestion',
docs: {
@@ -20,18 +24,23 @@ module.exports = {
create: (context) => {
const spacing = context.options[0] || 2;
- const sourceCode = context.getSourceCode();
+ const sourceCode = context.sourceCode;
const comments = sourceCode.getAllComments();
let inMetadata = false;
let done = false;
- let metadata = [];
- let start = {};
- let end = {};
+ let metadata: {
+ key: string;
+ space: number;
+ line: number;
+ comment: (typeof comments)[number];
+ }[] = [];
+ let start: Position | null = null;
+ let end: Position | null = null;
for (const comment of comments.filter(
- (comment) => comment.type === 'Line'
- )) {
+ (comment) => comment.type === 'Line' && comment.loc
+ ) as NonNullishComment[]) {
if (done) {
continue;
}
@@ -47,10 +56,10 @@ module.exports = {
inMetadata = true;
} else if (inMetadata && commentValue.startsWith('@')) {
// Get space string between key and value
- const [, spaceString] = /^\S*(\s*)/.exec(commentValue.slice(1));
+ const spaceString = /^\S*(\s*)/.exec(commentValue.slice(1))?.[1];
// Keys w/o value must not be validated
- if (spaceString.length === 0) {
+ if (!spaceString || spaceString.length === 0) {
continue;
}
@@ -63,7 +72,7 @@ module.exports = {
}
}
- if (Object.keys(end).length === 0) {
+ if (!end) {
end = sourceCode.getLocFromIndex(sourceCode.getText().length);
}
@@ -78,10 +87,10 @@ module.exports = {
metadata.map(({ space }) => space).sort()[0] < spacing;
if (
- hasSpaceLessThenSpacing ||
- metadata
- .map(({ key, space }) => key.length + space)
- .some((val) => val !== totalSpacing)
+ start &&
+ end &&
+ (hasSpaceLessThenSpacing ||
+ metadata.some(({ key, space }) => key.length + space !== totalSpacing))
) {
context.report({
loc: {
@@ -99,7 +108,13 @@ module.exports = {
) {
const startColumn = /^(.*?@\S*)/.exec(
sourceCode.getLines()[metadatapoint.line - 1]
- )[1].length;
+ )?.[1].length;
+
+ // istanbul ignore if
+ if (!startColumn) {
+ continue;
+ }
+
fixerRules.push(
fixer.replaceTextRange(
[
@@ -124,4 +139,4 @@ module.exports = {
return {};
}
-};
+} satisfies Rule.RuleModule;
diff --git a/lib/rules/better-use-match.js b/lib/rules/better-use-match.ts
similarity index 82%
rename from lib/rules/better-use-match.js
rename to lib/rules/better-use-match.ts
index e422a6e..8b6a777 100644
--- a/lib/rules/better-use-match.js
+++ b/lib/rules/better-use-match.ts
@@ -1,6 +1,6 @@
-const createValidator = require('../utils/createValidator');
+import { createValidator } from '../utils/createValidator';
-module.exports = createValidator({
+export default createValidator({
name: 'include',
required: false,
validator: ({ attrVal, context }) => {
diff --git a/lib/rules/compat-grant.js b/lib/rules/compat-grant.ts
similarity index 76%
rename from lib/rules/compat-grant.js
rename to lib/rules/compat-grant.ts
index c79cbdb..cb5c3c1 100644
--- a/lib/rules/compat-grant.js
+++ b/lib/rules/compat-grant.ts
@@ -1,9 +1,10 @@
-const createValidator = require('../utils/createValidator');
-const { compatMap, gmPolyfillOverride } = require('../data/compat-grant');
-const { intersects } = require('semver');
-const cleanupRange = require('../utils/cleanupRange');
+import { createValidator } from '../utils/createValidator';
+import { compatMap, gmPolyfillOverride } from '../data/compat-grant';
+import { intersects } from 'semver';
+import { cleanupRange } from '../utils/cleanupRange';
+import type { VersionAssertion } from '../data/version-assertion';
-module.exports = createValidator({
+export default createValidator({
name: 'grant',
required: false,
validator: ({ attrVal, context }) => {
@@ -12,9 +13,9 @@ module.exports = createValidator({
}
const requestedGrant = attrVal.val;
- const allRequired =
+ const allRequired: boolean =
context.options[0] && context.options[0].requireAllCompatible;
- const overrides =
+ const overrides: typeof gmPolyfillOverride =
context.settings.userscriptGrantCompatabilityOverrides || {};
const gmPolyfill = context.options[0] && context.options[0].gmPolyfill;
const gmPolyfillFallback =
@@ -26,46 +27,53 @@ module.exports = createValidator({
return;
}
- const supports = [];
+ const supports: boolean[] = [];
- function doesSupport(givenGrant) {
- let compatValue =
+ function doesSupport(givenGrant: string) {
+ let compatValue:
+ | (typeof gmPolyfillOverride)[keyof typeof gmPolyfillOverride]
+ | VersionAssertion[] =
overrides[givenGrant] ||
(gmPolyfill && gmPolyfillOverride[givenGrant]
? gmPolyfillOverride[givenGrant]
: compatMap[givenGrant]);
+ let compatVersions: VersionAssertion[] = Array.isArray(compatValue)
+ ? compatValue
+ : [];
- if (compatValue === 'ignore') {
+ if (!compatValue || compatValue === 'ignore') {
return;
}
- if (compatValue.deps) {
+
+ if ('deps' in compatValue) {
for (const overrideDep of compatValue.deps) {
doesSupport(overrideDep);
}
- if (compatValue.versions) {
- compatValue = compatValue.versions;
+ if ('versions' in compatValue) {
+ compatVersions = compatValue.versions as VersionAssertion[];
} else {
return;
}
}
if (!Array.isArray(compatValue)) {
- if (compatValue.versions) {
- compatValue = compatValue.versions;
+ if ('versions' in compatValue) {
+ compatVersions = compatValue.versions as VersionAssertion[];
} else {
return;
}
}
for (const versionConstraint in context.settings.userscriptVersions) {
- const foundAssertion = compatValue.find(
+ const foundAssertion = compatVersions.find(
(constraint) => constraint.type === versionConstraint
);
const secondAssertionFound =
compatMap[givenGrant] &&
- compatMap[givenGrant].find(
+ compatMap[givenGrant]!.find(
(constraint) => constraint.type === versionConstraint
);
+
supports.push(
(foundAssertion
? intersects(
diff --git a/lib/rules/compat-headers.js b/lib/rules/compat-headers.ts
similarity index 82%
rename from lib/rules/compat-headers.js
rename to lib/rules/compat-headers.ts
index 484c9dd..4cd4883 100644
--- a/lib/rules/compat-headers.js
+++ b/lib/rules/compat-headers.ts
@@ -1,9 +1,9 @@
-const createValidator = require('../utils/createValidator');
-const compatMap = require('../data/compat-headers');
-const { intersects } = require('semver');
-const cleanupRange = require('../utils/cleanupRange');
+import { createValidator } from '../utils/createValidator';
+import { compatMap } from '../data/compat-headers';
+import { intersects } from 'semver';
+import { cleanupRange } from '../utils/cleanupRange';
-module.exports = createValidator({
+export default createValidator({
name: 'headers',
required: false,
validator: ({ attrVal, context }) => {
@@ -15,15 +15,17 @@ module.exports = createValidator({
const allRequired =
context.options[0] && context.options[0].requireAllCompatible;
- const supports = [];
+ const supports: boolean[] = [];
+ const nonLocaleHeaderName = headerName.split(':')[0];
+
if (
headerName.includes(':') &&
- Object.keys(compatMap.localized).includes(headerName.split(':')[0])
+ nonLocaleHeaderName in compatMap.localized
) {
for (const versionConstraint in context.settings.userscriptVersions) {
- const foundAssertion = compatMap.localized[
- headerName.split(':')[0]
- ].find((constraint) => constraint.type === versionConstraint);
+ const foundAssertion = compatMap.localized[nonLocaleHeaderName]!.find(
+ (constraint) => constraint.type === versionConstraint
+ );
supports.push(
foundAssertion
? intersects(
@@ -35,9 +37,9 @@ module.exports = createValidator({
: false
);
}
- } else if (compatMap.unlocalized[headerName]) {
+ } else if (headerName in compatMap.unlocalized) {
for (const versionConstraint in context.settings.userscriptVersions) {
- const foundAssertion = compatMap.unlocalized[headerName].find(
+ const foundAssertion = compatMap.unlocalized[headerName]!.find(
(constraint) => constraint.type === versionConstraint
);
supports.push(
diff --git a/lib/rules/filename-user.js b/lib/rules/filename-user.ts
similarity index 86%
rename from lib/rules/filename-user.js
rename to lib/rules/filename-user.ts
index 517a5bc..b251976 100644
--- a/lib/rules/filename-user.js
+++ b/lib/rules/filename-user.ts
@@ -1,4 +1,6 @@
-module.exports = {
+import type { Rule } from 'eslint';
+
+export default {
meta: {
type: 'suggestion',
docs: {
@@ -15,7 +17,8 @@ module.exports = {
}
},
create: (context) => {
- const fileName = context.getFilename();
+ // istanbul ignore next
+ const fileName = context.filename ?? context.getFilename();
if (fileName === '' || fileName === '') {
return {};
@@ -43,4 +46,4 @@ module.exports = {
}
};
}
-};
+} satisfies Rule.RuleModule;
diff --git a/lib/rules/metadata-spacing.js b/lib/rules/metadata-spacing.ts
similarity index 85%
rename from lib/rules/metadata-spacing.js
rename to lib/rules/metadata-spacing.ts
index 36f0a61..b43f9d6 100644
--- a/lib/rules/metadata-spacing.js
+++ b/lib/rules/metadata-spacing.ts
@@ -1,7 +1,7 @@
-const parse = require('../utils/parse');
+import type { Rule } from 'eslint';
+import { parse } from '../utils/parse';
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+export default {
meta: {
type: 'suggestion',
docs: {
@@ -12,7 +12,7 @@ module.exports = {
fixable: 'whitespace'
},
create: (context) => {
- const sourceCode = context.getSourceCode();
+ const sourceCode = context.sourceCode;
const result = parse(sourceCode);
const hasMetadata = result.enteredMetadata !== -1 && result.end;
@@ -40,7 +40,7 @@ module.exports = {
const range = [
sourceCode.getIndexFromLoc(metadataLastLineLoc.start),
sourceCode.getIndexFromLoc(metadataLastLineLoc.end)
- ];
+ ] satisfies [number, number];
return fixer.insertTextAfterRange(range, '\n');
}
@@ -49,4 +49,4 @@ module.exports = {
return {};
}
-};
+} satisfies Rule.RuleModule;
diff --git a/lib/rules/no-invalid-grant.js b/lib/rules/no-invalid-grant.ts
similarity index 78%
rename from lib/rules/no-invalid-grant.js
rename to lib/rules/no-invalid-grant.ts
index 528f07e..1727fa3 100644
--- a/lib/rules/no-invalid-grant.js
+++ b/lib/rules/no-invalid-grant.ts
@@ -1,7 +1,7 @@
-const createValidator = require('../utils/createValidator');
-const { compatMap } = require('../data/compat-grant');
+import { createValidator } from '../utils/createValidator';
+import { compatMap } from '../data/compat-grant';
-module.exports = createValidator({
+export default createValidator({
name: 'grant',
required: false,
validator: ({ attrVal, context }) => {
diff --git a/lib/rules/no-invalid-headers.js b/lib/rules/no-invalid-headers.ts
similarity index 91%
rename from lib/rules/no-invalid-headers.js
rename to lib/rules/no-invalid-headers.ts
index b0a4aca..8d00594 100644
--- a/lib/rules/no-invalid-headers.js
+++ b/lib/rules/no-invalid-headers.ts
@@ -1,5 +1,5 @@
-const createValidator = require('../utils/createValidator');
-const compatMap = require('../data/compat-headers');
+import { createValidator } from '../utils/createValidator';
+import { compatMap } from '../data/compat-headers';
// Documentation:
// - Tampermonkey: https://www.tampermonkey.net/documentation.php
@@ -13,7 +13,7 @@ const internationalized = Object.keys(compatMap.localized).map(
(item) => new RegExp(`^${item}(:\\S+)?$`)
);
-module.exports = createValidator({
+export default createValidator({
name: 'headers',
validator: ({ attrVal, context }) => {
const optionsHeaders =
diff --git a/lib/rules/no-invalid-metadata.js b/lib/rules/no-invalid-metadata.js
deleted file mode 100644
index 0a8d443..0000000
--- a/lib/rules/no-invalid-metadata.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const parse = require('../utils/parse');
-
-module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'ensure userscripts have valid metadata',
- category: 'Possible Errors'
- },
- messages: {
- metadataRequired: 'Add metadata to the userscript',
- moveMetadataToTop: 'Move the metadata to the top of the file',
- noClosingMetadata: 'Closing metadata comment not found',
- noCodeBetween: 'Code found between in metadata',
- attributeNotStartsWithAtTheRate: 'Attributes should begin with @'
- },
- schema: [
- {
- type: 'object',
- properties: {
- top: {
- enum: ['required', 'optional'],
- default: 'required'
- }
- },
- additionalProperties: false
- }
- ]
- },
- create: (context) => {
- const sourceCode = context.getSourceCode();
-
- const comments = sourceCode.getAllComments();
-
- const result = parse(sourceCode);
-
- for (const lineLoc of result.lines.filter((line) => line.codeBetween))
- context.report({
- loc: lineLoc,
- messageId: 'noCodeBetween'
- });
-
- for (const lineLoc of result.lines.filter((line) => line.invalid))
- context.report({
- loc: lineLoc,
- messageId: 'attributeNotStartsWithAtTheRate'
- });
-
- if (result.enteredMetadata !== -1 && !result.end) {
- context.report({
- loc: comments.find(
- (comment) =>
- comment.value.trim() === '==UserScript==' && comment.type === 'Line'
- ).loc,
- messageId: 'noClosingMetadata'
- });
- }
-
- return {
- Program(node) {
- if (result.enteredMetadata === -1) {
- context.report({
- node,
- messageId: 'metadataRequired'
- });
- } else if (
- (!context.options[0] ||
- !context.options[0].top ||
- context.options[0].top === 'required') &&
- (result.enteredMetadata !== 0 || comments[0].loc.start.line !== 1)
- ) {
- context.report({
- loc: comments.find(
- (comment) =>
- comment.value.trim() === '==UserScript==' &&
- comment.type === 'Line'
- ).loc,
- messageId: 'moveMetadataToTop'
- });
- }
- }
- };
- }
-};
diff --git a/lib/rules/no-invalid-metadata.ts b/lib/rules/no-invalid-metadata.ts
new file mode 100644
index 0000000..b6ca085
--- /dev/null
+++ b/lib/rules/no-invalid-metadata.ts
@@ -0,0 +1,107 @@
+import { parse } from '../utils/parse';
+import type { Rule } from 'eslint';
+import type { NonNullishComment } from '../utils/comment';
+
+export default {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'ensure userscripts have valid metadata',
+ category: 'Possible Errors'
+ },
+ messages: {
+ metadataRequired: 'Add metadata to the userscript',
+ moveMetadataToTop: 'Move the metadata to the top of the file',
+ noClosingMetadata: 'Closing metadata comment not found',
+ noCodeBetween: 'Code found between in metadata',
+ attributeNotStartsWithAtTheRate: 'Attributes should begin with @'
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ top: {
+ enum: ['required', 'optional'],
+ default: 'required'
+ }
+ },
+ additionalProperties: false
+ }
+ ]
+ },
+ create: (context) => {
+ const sourceCode = context.sourceCode;
+
+ const comments = sourceCode.getAllComments();
+
+ const result = parse(sourceCode);
+
+ for (const { lineLoc } of result.lines.filter((line) => line.codeBetween))
+ context.report({
+ loc: lineLoc,
+ messageId: 'noCodeBetween'
+ });
+
+ for (const { lineLoc } of result.lines.filter((line) => line.invalid))
+ context.report({
+ loc: lineLoc,
+ messageId: 'attributeNotStartsWithAtTheRate'
+ });
+
+ const startComment = comments.find(
+ (comment) =>
+ comment.value.trim() === '==UserScript==' && comment.type === 'Line'
+ );
+ if (
+ startComment &&
+ startComment.loc &&
+ result.enteredMetadata !== -1 &&
+ !result.end
+ ) {
+ context.report({
+ loc: startComment.loc,
+ messageId: 'noClosingMetadata'
+ });
+ }
+
+ const firstComment = comments.find((comment) => {
+ return comment.loc;
+ }) as NonNullishComment | undefined;
+
+ return {
+ Program(node) {
+ if (result.enteredMetadata === -1 || !firstComment) {
+ context.report({
+ node,
+ messageId: 'metadataRequired'
+ });
+ } else if (
+ (!context.options[0] ||
+ !context.options[0].top ||
+ context.options[0].top === 'required') &&
+ (result.enteredMetadata !== 0 || firstComment.loc.start.line !== 1)
+ ) {
+ const firstStartComment = comments.find(
+ (comment) =>
+ comment.value.trim() === '==UserScript==' &&
+ comment.type === 'Line'
+ ) as NonNullishComment | undefined;
+ if (firstStartComment) {
+ context.report({
+ loc: firstStartComment.loc,
+ messageId: 'moveMetadataToTop'
+ });
+ } else {
+ const firstComment = comments.find(
+ (comment) => comment.loc
+ ) as NonNullishComment;
+ context.report({
+ loc: firstComment.loc,
+ messageId: 'moveMetadataToTop'
+ });
+ }
+ }
+ }
+ };
+ }
+} satisfies Rule.RuleModule;
diff --git a/lib/rules/require-attribute-space-prefix.js b/lib/rules/require-attribute-space-prefix.ts
similarity index 84%
rename from lib/rules/require-attribute-space-prefix.js
rename to lib/rules/require-attribute-space-prefix.ts
index 1ed9e4e..b8c7b8b 100644
--- a/lib/rules/require-attribute-space-prefix.js
+++ b/lib/rules/require-attribute-space-prefix.ts
@@ -1,6 +1,7 @@
-const parse = require('../utils/parse');
+import type { Rule } from 'eslint';
+import { parse } from '../utils/parse';
-module.exports = {
+export default {
meta: {
type: 'suggestion',
docs: {
@@ -13,7 +14,7 @@ module.exports = {
schema: []
},
create: (context) => {
- const sourceCode = context.getSourceCode();
+ const sourceCode = context.sourceCode;
const result = parse(sourceCode);
@@ -36,4 +37,4 @@ module.exports = {
return {};
}
-};
+} satisfies Rule.RuleModule;
diff --git a/lib/rules/require-description.js b/lib/rules/require-description.ts
similarity index 80%
rename from lib/rules/require-description.js
rename to lib/rules/require-description.ts
index cad8050..f20f977 100644
--- a/lib/rules/require-description.js
+++ b/lib/rules/require-description.ts
@@ -1,12 +1,12 @@
-const createValidator = require('../utils/createValidator');
+import { createValidator } from '../utils/createValidator';
const descriptionReg = /^description(:\S+)?$/;
-module.exports = createValidator({
+export default createValidator({
name: 'description',
required: true,
validator: ({ attrVal, context }) => {
- let iteratedKeyNames = [];
+ let iteratedKeyNames: string[] = [];
for (let attrValue of attrVal) {
if (iteratedKeyNames.includes(attrValue.key)) {
context.report({
diff --git a/lib/rules/require-download-url.js b/lib/rules/require-download-url.ts
similarity index 63%
rename from lib/rules/require-download-url.js
rename to lib/rules/require-download-url.ts
index f02d858..ffa13ce 100644
--- a/lib/rules/require-download-url.js
+++ b/lib/rules/require-download-url.ts
@@ -1,6 +1,6 @@
-const createValidator = require('../utils/createValidator');
+import { createValidator } from '../utils/createValidator';
-module.exports = createValidator({
+export default createValidator({
name: 'updateURL',
validator: ({ attrVal, metadata, context, keyName }) => {
if (keyName === 'updateURL' && !metadata['downloadURL']) {
@@ -10,12 +10,9 @@ module.exports = createValidator({
fix: function (fixer) {
return fixer.insertTextAfterRange(
attrVal.comment.range,
- `\n${context
- .getSourceCode()
- .lines[attrVal.comment.loc.start.line - 1].replace(
- /^(\s*\/\/\s*@)\S*/,
- '$1downloadURL'
- )}`
+ `\n${context.sourceCode.lines[
+ attrVal.comment.loc.start.line - 1
+ ].replace(/^(\s*\/\/\s*@)\S*/, '$1downloadURL')}`
);
}
});
diff --git a/lib/rules/require-name.js b/lib/rules/require-name.ts
similarity index 51%
rename from lib/rules/require-name.js
rename to lib/rules/require-name.ts
index 9d4cfbc..8767e0a 100644
--- a/lib/rules/require-name.js
+++ b/lib/rules/require-name.ts
@@ -1,12 +1,13 @@
-const createValidator = require('../utils/createValidator');
+import { NonNullishComment } from '../utils/comment';
+import { type Metadata, createValidator } from '../utils/createValidator';
const nameReg = /^name(:\S+)?$/;
-module.exports = createValidator({
+export default createValidator({
name: 'name',
required: true,
validator: ({ attrVal, context, metadata }) => {
- let iteratedKeyNames = [];
+ let iteratedKeyNames: string[] = [];
for (let attrValue of attrVal) {
if (iteratedKeyNames.includes(attrValue.key)) {
context.report({
@@ -20,31 +21,36 @@ module.exports = createValidator({
const metadataValues = Object.values(metadata);
+ const sourceCode = context.sourceCode;
+ const comments = sourceCode.getAllComments();
+
+ const startComment = comments.find(
+ (comment) =>
+ comment.value.trim() === '==UserScript==' && comment.type === 'Line'
+ ) as NonNullishComment | undefined;
+
if (
+ startComment &&
metadataValues.some(
(attrValue, attrValIndex) =>
attrValIndex !== 0 &&
- nameReg.test(attrValue[0] ? attrValue[0].key : attrValue.key) &&
+ nameReg.test(
+ Array.isArray(attrValue) ? attrValue[0].key : attrValue.key
+ ) &&
!nameReg.test(
- metadataValues[attrValIndex - 1][0]
- ? metadataValues[attrValIndex - 1][0].key
- : metadataValues[attrValIndex - 1].key
+ Array.isArray(metadataValues[attrValIndex - 1])
+ ? (metadataValues[attrValIndex - 1] as Metadata[])[0].key
+ : (metadataValues[attrValIndex - 1] as Metadata).key
)
)
) {
- const sourceCode = context.getSourceCode();
- const comments = sourceCode.getAllComments();
const endingMetadataComment = comments.find(
(comment) =>
comment.value.trim() === '==/UserScript==' && comment.type === 'Line'
- );
+ ) as NonNullishComment | undefined;
context.report({
loc: {
- start: comments.find(
- (comment) =>
- comment.value.trim() === '==UserScript==' &&
- comment.type === 'Line'
- ).loc.start,
+ start: startComment.loc.start,
end: endingMetadataComment
? endingMetadataComment.loc.end
: { line: sourceCode.lines.length, column: 0 }
@@ -53,48 +59,34 @@ module.exports = createValidator({
fix: function (fixer) {
let fixerRules = [];
for (let attrValue of attrVal) {
- // istanbul ignore else
- if (!Array.isArray(attrValue)) {
- attrValue = [attrValue];
- }
- for (let deepAttrValue of attrValue) {
- fixerRules.push(
- fixer.removeRange(
- deepAttrValue.comment.range.map((val, index) =>
- index === 0
- ? val -
- context
- .getSourceCode()
- .lines[deepAttrValue.loc.start.line - 1].split(
- '//'
- )[0].length -
- 1
- : val
- )
- )
- );
- }
+ fixerRules.push(
+ fixer.removeRange([
+ attrValue.comment.range[0] -
+ context.sourceCode.lines[attrValue.loc.start.line - 1].split(
+ '//'
+ )[0].length -
+ 1,
+ attrValue.comment.range[1]
+ ])
+ );
}
fixerRules.push(
fixer.insertTextAfterRange(
- context
- .getSourceCode()
- .getAllComments()
- .find((val) => val.value.trim() === '==UserScript==').range,
+ startComment.range,
attrVal
.sort((attrValue1, attrValue2) =>
attrValue1.key === 'name'
? -1
: attrValue2.key === 'name'
- ? 1
- : 0
+ ? 1
+ : 0
)
.map(
(attrValue) =>
`\n${
- context
- .getSourceCode()
- .lines[attrValue.loc.start.line - 1].split('//')[0]
+ context.sourceCode.lines[
+ attrValue.loc.start.line - 1
+ ].split('//')[0]
}//${attrValue.comment.value}`
)
.join('')
diff --git a/lib/rules/require-version.js b/lib/rules/require-version.ts
similarity index 65%
rename from lib/rules/require-version.js
rename to lib/rules/require-version.ts
index 3052e46..713e54c 100644
--- a/lib/rules/require-version.js
+++ b/lib/rules/require-version.ts
@@ -1,9 +1,9 @@
-const createValidator = require('../utils/createValidator');
+import { createValidator } from '../utils/createValidator';
const versionRegex =
/^([\dA-Za-z–-]+)(\.[\dA-Za-z–-]+)*(\+([\dA-Za-z]+)(\.[\dA-Za-z]+)*)?\s*$/;
-module.exports = createValidator({
+export default createValidator({
name: 'version',
required: true,
validator: ({ attrVal, index, context }) => {
@@ -13,15 +13,15 @@ module.exports = createValidator({
messageId: 'multipleVersions'
});
}
- if (!versionRegex.test(attrVal.val)) {
+ const versionWhitespace = /^(\s*\/\/\s*)/.exec(
+ context.sourceCode.lines[attrVal.comment.loc.start.line]
+ )?.[1];
+ if (versionWhitespace && !versionRegex.test(attrVal.val)) {
context.report({
loc: {
start: {
line: attrVal.loc.start.line,
- column:
- /^(\s*\/\/\s*)/.exec(
- context.getSourceCode().lines[attrVal.comment.loc.start.line]
- )[1].length - 1
+ column: versionWhitespace.length - 1
},
end: attrVal.loc.end
},
diff --git a/lib/rules/use-homepage-and-url.js b/lib/rules/use-homepage-and-url.ts
similarity index 65%
rename from lib/rules/use-homepage-and-url.js
rename to lib/rules/use-homepage-and-url.ts
index 6732da2..c06a7d0 100644
--- a/lib/rules/use-homepage-and-url.js
+++ b/lib/rules/use-homepage-and-url.ts
@@ -1,15 +1,15 @@
-const createValidator = require('../utils/createValidator');
+import { createValidator } from '../utils/createValidator';
const homepageAttrs = ['homepage', 'homepageURL'];
-module.exports = createValidator({
+export default createValidator({
name: homepageAttrs,
validator: ({ attrVal, metadata, context, keyName }) => {
const attribute = homepageAttrs.find(
(homepageAttr) => homepageAttr !== keyName
- );
- if (!metadata[attribute]) {
+ ) as string;
+ if (!(attribute in metadata)) {
context.report({
loc: attrVal.loc,
messageId: 'missingAttribute',
@@ -19,17 +19,15 @@ module.exports = createValidator({
fix: function (fixer) {
return fixer.insertTextAfterRange(
attrVal.comment.range,
- `\n${context
- .getSourceCode()
- .lines[attrVal.comment.loc.start.line - 1].replace(
- /^(\s*\/\/\s*@)\S*/,
- '$1' + attribute
- )}`
+ `\n${context.sourceCode.lines[
+ attrVal.comment.loc.start.line - 1
+ ].replace(/^(\s*\/\/\s*@)\S*/, '$1' + attribute)}`
);
}
});
}
},
+
messages: {
missingAttribute: "Didn't find attribute '{{ attribute }}' in the metadata"
},
diff --git a/lib/utils/cleanupRange.js b/lib/utils/cleanupRange.js
deleted file mode 100644
index 5a0beab..0000000
--- a/lib/utils/cleanupRange.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Cleans up a range and makes it semver-compatible
- *
- * @param {string} range The range to cleanup
- * @returns {string} A newer semver-compatible range
- */
-function cleanupRange(range) {
- return range
- .replace(/1(\.\d+){3}/, '1')
- .replace(/(\d+\.\d+\.\d+)\.(\d+)/, '$1-beta.$2');
-}
-
-module.exports = cleanupRange;
diff --git a/lib/utils/cleanupRange.ts b/lib/utils/cleanupRange.ts
new file mode 100644
index 0000000..f46f294
--- /dev/null
+++ b/lib/utils/cleanupRange.ts
@@ -0,0 +1,5 @@
+export function cleanupRange(range: string) {
+ return range
+ .replace(/1(\.\d+){3}/, '1')
+ .replace(/(\d+\.\d+\.\d+)\.(\d+)/, '$1-beta.$2');
+}
diff --git a/lib/utils/comment.ts b/lib/utils/comment.ts
new file mode 100644
index 0000000..e05eed7
--- /dev/null
+++ b/lib/utils/comment.ts
@@ -0,0 +1,13 @@
+import type { Comment } from 'estree';
+
+type NonNullish = T extends null
+ ? never
+ : T extends undefined
+ ? never
+ : T;
+
+type NonNullishProps = {
+ [K in keyof T]-?: NonNullish;
+};
+
+export type NonNullishComment = NonNullishProps;
diff --git a/lib/utils/createValidator.js b/lib/utils/createValidator.ts
similarity index 50%
rename from lib/utils/createValidator.js
rename to lib/utils/createValidator.ts
index c8d1c20..d8090fe 100644
--- a/lib/utils/createValidator.js
+++ b/lib/utils/createValidator.ts
@@ -1,64 +1,62 @@
-const parse = require('./parse');
+import { ParsingResult, parse } from './parse';
+import type { JSONSchema4 } from 'json-schema';
+import type { SourceLocation } from 'acorn';
+import type { Rule } from 'eslint';
-/**
- * The metadata information on an attribute
- *
- * @typedef {Object} Metadata
- * @property {string} val The value extracted from the comment
- * @property {import('acorn')['SourceLocation']} loc The location of the comment
- * @property {{
- * loc: import('acorn')['SourceLocation'];
- * type: 'Line';
- * range: [number, number];
- * value: string;
- * }} comment
- * The comment itself
- * @property {string} key The name of the key of the attribute
- */
+export type Metadata = {
+ comment: {
+ loc: SourceLocation;
+ type: 'Line';
+ range: [number, number];
+ value: string;
+ };
+ loc: SourceLocation;
+ val: string;
+ key: string;
+};
+
+type ValidatorCallback = (validationInfo: {
+ attrVal: UseArray extends true ? Metadata[] : Metadata;
+ index: UseArray extends true ? number[] : number;
+ metadata: Record;
+ context: Rule.RuleContext;
+ keyName: string | string[];
+}) => void;
+
+interface Options {
+ name: string | string[];
+ required?: boolean;
+ messages?: Record;
+ regexMatch?: RegExp;
+ fixable?: boolean;
+ regexMatxch?: RegExp;
+ schema?: JSONSchema4;
+}
+
+interface OptionsRunOnce extends Options {
+ validator?: ValidatorCallback | false;
+ runOnce: true;
+}
-/**
- * The callback for validators on validation rules
- *
- * @callback validatorCallback
- * @param {{
- * attrVal: Metadata | Metadata[];
- * index: number | number[];
- * indexMatch: number | number[];
- * metadata: Object;
- * context: RuleContext;
- * keyName: string | string[];
- * }} validationInfo
- * The information based on which the validator validates the metadata
- */
+interface OptionsRunMultiple extends Options {
+ validator?: ValidatorCallback | false;
+ runOnce?: false;
+}
-/**
- * The main options for the validator creator function
- *
- * @typedef {Object} ValidatorOptions
- * @property {string | string[]} name The name of the attribute(s) that the rule
- * validates
- * @property {boolean} required Whether the attribute(s) are required or not
- * @property {false | validatorCallback} [validator=false] The custom validator
- * function. Default is `false`
- * @property {Object} [messages={}] Messages that are needed
- * when reporting in the validator. Default is `{}`
- * @property {boolean} [fixable=false] Whether the rule is fixable or not.
- * Default is `false`
- * @property {RegExp} [regexMatch] A regular expression to match all keys,
- * defaults to usage of name
- * @property {boolean} [runOnce=false] A boolean representing whether the
- * validator should run once on all matches. Default is `false`
- * @property {Object} [schema] The configuration options
- */
+type FilterTrue = T extends any
+ ? T['metadataInfo'] extends true
+ ? T
+ : never
+ : never;
-/**
- * Function to create a validator rule
- *
- * @param {ValidatorOptions} options The rule options
- * @returns {import('eslint/lib/shared/types.js')['Rule']} The resulting
- * validation rule
- */
-module.exports = function createValidator({
+function isRunOnce(
+ validator: OptionsRunMultiple['validator'] | OptionsRunOnce['validator'],
+ runOnce: boolean
+): validator is OptionsRunOnce['validator'] {
+ return runOnce;
+}
+
+export function createValidator({
name,
required = false,
validator = false,
@@ -69,10 +67,8 @@ module.exports = function createValidator({
),
runOnce = false,
schema
-}) {
- if (typeof name === 'string') {
- name = [name];
- }
+}: OptionsRunMultiple | OptionsRunOnce) {
+ const nameList: string[] = typeof name === 'string' ? [name] : name;
return {
meta: {
@@ -80,7 +76,7 @@ module.exports = function createValidator({
docs: {
description: `${
required ? `require ${validator ? 'and validate ' : ''}` : 'validate '
- }${name.join(' and ')} in the metadata for userscripts`,
+ }${nameList.join(' and ')} in the metadata for userscripts`,
category: 'Best Practices'
},
schema: required
@@ -92,18 +88,21 @@ module.exports = function createValidator({
]
: schema || undefined,
messages: {
- missingAttribute: `Didn't find attribute '${name}' in the metadata`,
+ missingAttribute: `Didn't find attribute '${nameList}' in the metadata`,
...messages
},
fixable: fixable ? 'code' : undefined
},
create: (context) => {
- const sourceCode = context.getSourceCode();
+ const sourceCode = context.sourceCode;
const comments = sourceCode.getAllComments();
const result = parse(sourceCode);
- let metadata = {};
- for (const line of result.lines.filter((line) => line.metadataInfo)) {
+ let metadata: Record = {};
+ const lines = result.lines.filter(
+ (line) => line.metadataInfo
+ ) as FilterTrue[];
+ for (const line of lines) {
const actualValue = line.value.trim().slice(2);
const lengthDiff = line.value.length - actualValue.length - 2;
const newLoc = {
@@ -112,7 +111,7 @@ module.exports = function createValidator({
column: line.lineLoc.start.column + lengthDiff
},
end: line.lineLoc.end
- };
+ } satisfies SourceLocation;
const val = {
val: line.metadataValue.value,
loc: newLoc,
@@ -126,41 +125,47 @@ module.exports = function createValidator({
type: 'Line'
},
key: line.metadataValue.key
- };
+ } satisfies Metadata;
// istanbul ignore else
if (metadata[line.metadataValue.key]) {
- if (!Array.isArray(metadata[line.metadataValue.key]))
+ if (!Array.isArray(metadata[line.metadataValue.key])) {
metadata[line.metadataValue.key] = [
- metadata[line.metadataValue.key]
+ metadata[line.metadataValue.key] as Metadata
];
- metadata[line.metadataValue.key].push(val);
+ }
+ (metadata[line.metadataValue.key] as Metadata[]).push(val);
continue;
}
metadata[line.metadataValue.key] = val;
}
const metadataKeys = Object.keys(metadata);
+ const startComment = comments.find(
+ (comment) =>
+ comment.value.trim() === '==UserScript==' && comment.type === 'Line'
+ );
+
// istanbul ignore else
if (
+ startComment &&
+ startComment.loc &&
required &&
result.enteredMetadata !== -1 &&
(!context.options[0] || context.options[0] === 'required') &&
- !metadataKeys.some((name) => regexMatch.test(name))
+ !metadataKeys.some((metadataKeyName) =>
+ regexMatch.test(metadataKeyName)
+ )
) {
context.report({
- loc: comments.find(
- (comment) =>
- comment.value.trim() === '==UserScript==' &&
- comment.type === 'Line'
- ).loc,
+ loc: startComment.loc,
messageId: 'missingAttribute'
});
} else if (
validator &&
- metadataKeys.some((name) => regexMatch.test(name))
+ metadataKeys.some((metadataKeyName) => regexMatch.test(metadataKeyName))
) {
- if (runOnce) {
- const matchingMetadataKeyIndex = [];
+ if (isRunOnce(validator, runOnce)) {
+ const matchingMetadataKeyIndex: number[] = [];
for (const metadataKeyIndex in metadataKeys) {
if (regexMatch.test(metadataKeys[metadataKeyIndex])) {
matchingMetadataKeyIndex.push(+metadataKeyIndex);
@@ -169,27 +174,15 @@ module.exports = function createValidator({
const attributeValues = matchingMetadataKeyIndex
.map((index) => metadata[metadataKeys[index]])
.reduce(
- (accumalator, metadataPart) =>
+ (accumalator: Metadata[], metadataPart) =>
Array.isArray(metadataPart)
? [...accumalator, ...metadataPart]
: [...accumalator, metadataPart],
- []
+ [] as Metadata[]
);
validator({
attrVal: attributeValues,
index: [...attributeValues.keys()],
- indexMatch: matchingMetadataKeyIndex.reduce(
- (accumalator, metadataKeyIndex) =>
- Array.isArray(metadata[metadataKeys[metadataKeyIndex]])
- ? [
- ...accumalator,
- ...metadata[metadataKeys[metadataKeyIndex]].map(
- () => metadataKeys
- )
- ]
- : [...accumalator, metadataKeyIndex],
- []
- ),
metadata,
context,
keyName: matchingMetadataKeyIndex.map(
@@ -202,13 +195,12 @@ module.exports = function createValidator({
continue;
}
if (Array.isArray(metadata[metadataKeys[metadataKeyIndex]])) {
- for (const [index, attrVal] of metadata[
- metadataKeys[metadataKeyIndex]
- ].entries()) {
+ for (const [index, attrVal] of (
+ metadata[metadataKeys[metadataKeyIndex]] as Metadata[]
+ ).entries()) {
validator({
attrVal,
index,
- indexMatch: metadataKeyIndex,
metadata,
context,
keyName: metadataKeys[metadataKeyIndex]
@@ -216,9 +208,8 @@ module.exports = function createValidator({
}
} else {
validator({
- attrVal: metadata[metadataKeys[metadataKeyIndex]],
+ attrVal: metadata[metadataKeys[metadataKeyIndex]] as Metadata,
index: 0,
- indexMatch: metadataKeyIndex,
metadata,
context,
keyName: metadataKeys[metadataKeyIndex]
@@ -230,5 +221,5 @@ module.exports = function createValidator({
return {};
}
- };
-};
+ } satisfies Rule.RuleModule;
+}
diff --git a/lib/utils/parse.js b/lib/utils/parse.ts
similarity index 71%
rename from lib/utils/parse.js
rename to lib/utils/parse.ts
index ac8958a..102979f 100644
--- a/lib/utils/parse.js
+++ b/lib/utils/parse.ts
@@ -1,40 +1,39 @@
-/**
- * The result of parsing the metadata
- *
- * @typedef {Object} ParsingResult Result of the metadata parser
- * @property {boolean} end Whether the metadata ended or not
- * @property {number} enteredMetadata The index line of where the metadata began
- * (-1 by default)
- * @property {{
- * value: string;
- * lineLoc: import('acorn')['SourceLocation'];
- * codeBetween: boolean;
- * end: boolean;
- * start: boolean;
- * invalid: boolean;
- * metadataInfo: boolean;
- * metadataValue: { key: string; value: string } | undefined;
- * }[]} lines
- * Array of lines in the metadata
- */
+import type { SourceCode } from 'eslint';
+import type { SourceLocation } from 'acorn';
-/**
- * Parses metadata in source code
- *
- * @param {import('eslint')['SourceCode']['prototype']} sourceCode The
- * sourceCode brought from context.getSourceCode()
- * @returns {ParsingResult} The result of parsing the metadata
- */
-module.exports = function parse(sourceCode) {
+type Line = {
+ value: string;
+ lineLoc: SourceLocation;
+ codeBetween: boolean;
+ end: boolean;
+ start: boolean;
+ invalid: boolean;
+};
+
+export type ParsingResult = {
+ end: boolean;
+ enteredMetadata: number;
+ lines: (
+ | (Line & {
+ metadataInfo: true;
+ metadataValue: { key: string; value: string };
+ })
+ | (Line & {
+ metadataInfo: false;
+ })
+ )[];
+};
+
+export function parse(sourceCode: SourceCode) {
const defaultLineInfo = {
codeBetween: false,
end: false,
start: false,
invalid: false,
- metadataInfo: false
+ metadataInfo: false as const
};
- let result = {
+ let result: ParsingResult = {
end: false,
enteredMetadata: -1,
lines: []
@@ -132,4 +131,4 @@ module.exports = function parse(sourceCode) {
}
return result;
-};
+}
diff --git a/package.json b/package.json
index 7360c39..3c3689e 100644
--- a/package.json
+++ b/package.json
@@ -8,12 +8,37 @@
"eslint-plugin"
],
"author": "Yash Singh",
- "main": "./lib/index.js",
+ "main": "./dist/index.js",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "require": "./dist/index.js"
+ },
+ "./package.json": {
+ "import": "./package.json",
+ "require": "./package.json"
+ },
+ "./lib/rules": {
+ "import": "./dist/rules/index.js",
+ "require": "./dist/rules/index.js"
+ },
+ "./lib/utils/createValidator.js": {
+ "import": "./dist/utils/createValidator.js",
+ "require": "./dist/utils/createValidator.js"
+ },
+ "./lib/utils/parse.js": {
+ "import": "./dist/utils/parse.js",
+ "require": "./dist/utils/parse.js"
+ }
+ },
"scripts": {
- "test": "nyc --reporter=lcov --reporter=text mocha tests --recursive",
+ "test": "nyc --reporter=lcov --reporter=text mocha --recursive --file $(find tests -type file -name \"*.ts\") --require ts-node/register",
+ "build": "tsup",
+ "type-check": "tsc --noEmit",
"lint": "eslint . --ignore-path .gitignore && prettier --check . --ignore-path .gitignore && markdownlint . --ignore-path .gitignore",
"lint:fix": "eslint . --ignore-path .gitignore --fix && prettier --write . --ignore-path .gitignore && markdownlint --fix . --ignore-path .gitignore",
- "prepare": "husky install"
+ "prepare": "husky install",
+ "coverage": "live-server coverage/lcov-report"
},
"dependencies": {
"requireindex": "~1.2.0",
@@ -21,25 +46,35 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.19.1",
+ "@types/eslint": "^8.44.8",
+ "@types/estree": "1.0.5",
+ "@types/json-schema": "^7.0.15",
+ "@types/mocha": "^10.0.6",
+ "@types/node": "^20.10.4",
+ "@types/semver": "^7.5.6",
"acorn": "^8.8.2",
- "eslint": "^8.40.0",
+ "eslint": "8.40.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-eslint-plugin": "^5.0.8",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-unicorn": "^47.0.0",
"husky": "^8.0.3",
+ "live-server": "^1.2.2",
"markdownlint-cli": "^0.35.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
- "prettier": "^2.8.4",
- "prettier-plugin-jsdoc": "^0.4.2",
- "should": "^13.2.3"
+ "prettier": "^3.1.1",
+ "prettier-plugin-jsdoc": "^1.3.0",
+ "should": "^13.2.3",
+ "ts-node": "^10.9.2",
+ "tsup": "^8.0.1",
+ "typescript": "^5.3.3"
},
"peerDependencies": {
- "eslint": ">=8.0.0 <9"
+ "eslint": ">=8.40.0 <10"
},
"engines": {
- "node": ">=16.0.0 <21.0.0"
+ "node": ">=16.0.0 <22.0.0"
},
"license": "MIT",
"homepage": "https://github.com/Yash-Singh1/eslint-plugin-userscripts#readme",
diff --git a/tests/docs/README.js b/tests/docs/README.ts
similarity index 74%
rename from tests/docs/README.js
rename to tests/docs/README.ts
index 13751e3..0b9c5b3 100644
--- a/tests/docs/README.js
+++ b/tests/docs/README.ts
@@ -1,12 +1,15 @@
-const fs = require('fs');
+import fs from 'node:fs';
+import type { Rule } from 'markdownlint';
+
+import * as plugin from '../../lib';
+
const recommendedRules = new Set(
- Object.entries(require('../../').configs.recommended.rules)
+ Object.entries(plugin.configs.recommended.rules)
.filter((entry) => entry[1] !== 'off')
.map((entry) => entry[0].split('/')[1])
);
-/** @type {import('markdownlint').Rule} */
-module.exports = {
+export default {
names: ['table-all-rules'],
tags: ['tables'],
description: 'Ensures that all the rules are implemented in the READMEs',
@@ -17,7 +20,7 @@ module.exports = {
const rules = fs
.readdirSync('lib/rules')
.map((ruleJs) => ruleJs.split('.')[0]);
- const doneRules = [];
+ const doneRules: string[] = [];
for (const token of params.tokens.filter(
(token) => token.type === 'td_open'
@@ -59,13 +62,20 @@ module.exports = {
const tableOpen = params.tokens.find(
(token) => token.type === 'table_open'
);
- for (const undocumentedRule of rules.filter(
- (rule) => !doneRules.includes(rule)
- )) {
+ if (tableOpen) {
+ for (const undocumentedRule of rules.filter(
+ (rule) => !doneRules.includes(rule)
+ )) {
+ onError({
+ lineNumber: tableOpen.lineNumber,
+ detail: `Rule ${undocumentedRule} is not documented in the README`
+ });
+ }
+ } else {
onError({
- lineNumber: tableOpen.lineNumber,
- detail: `Rule ${undocumentedRule} is not documented in the README`
+ lineNumber: params.tokens[0].lineNumber,
+ detail: `No table found in the README`
});
}
}
-};
+} satisfies Rule;
diff --git a/tests/docs/_sidebar.js b/tests/docs/_sidebar.ts
similarity index 90%
rename from tests/docs/_sidebar.js
rename to tests/docs/_sidebar.ts
index 1982fca..dc6cde4 100644
--- a/tests/docs/_sidebar.js
+++ b/tests/docs/_sidebar.ts
@@ -1,7 +1,7 @@
-const fs = require('fs');
+import fs from 'node:fs';
+import type { Rule, MarkdownItToken } from 'markdownlint';
-/** @type {import('markdownlint').Rule} */
-module.exports = {
+export default {
names: ['table-all-rules'],
tags: ['tables'],
description: 'Ensures that all the rules are implemented in the READMEs',
@@ -12,7 +12,7 @@ module.exports = {
const rules = fs
.readdirSync('lib/rules')
.map((ruleJs) => ruleJs.split('.')[0]);
- const doneRules = [];
+ const doneRules: string[] = [];
let utilitiesStarted = false;
params.tokens = params.tokens.reduce((allTokens, token) => {
@@ -26,7 +26,7 @@ module.exports = {
allTokens.push(token);
}
return allTokens;
- }, []);
+ }, [] as MarkdownItToken[]);
for (const token of params.tokens.filter(
(token) => token.type === 'inline'
@@ -60,4 +60,4 @@ module.exports = {
});
}
}
-};
+} satisfies Rule;
diff --git a/tests/lib/data/compat-grant.js b/tests/lib/data/compat-grant.ts
similarity index 63%
rename from tests/lib/data/compat-grant.js
rename to tests/lib/data/compat-grant.ts
index 7e9d6ff..b42b190 100644
--- a/tests/lib/data/compat-grant.js
+++ b/tests/lib/data/compat-grant.ts
@@ -1,19 +1,19 @@
-const {
- compatMap,
- gmPolyfillOverride
-} = require('../../../lib/data/compat-grant');
+import { compatMap, gmPolyfillOverride } from '../../../lib/data/compat-grant';
+import type { VersionAssertion } from '../../../lib/data/version-assertion';
-require('should');
+import 'should';
-function validateCompatibilityData(compatabilityData) {
+function validateCompatibilityData(compatabilityData: VersionAssertion[]) {
compatabilityData.should.be.an.Array;
- compatabilityData.should.matchEach((compatabilityAssertion) => {
- compatabilityAssertion.should.have
- .property('type')
- .a.String()
- .equalOneOf(['tampermonkey', 'greasemonkey', 'violentmonkey']);
- compatabilityAssertion.should.have.property('versionConstraint').a.String;
- });
+ compatabilityData.should.matchEach(
+ (compatabilityAssertion: VersionAssertion) => {
+ compatabilityAssertion.should.have
+ .property('type')
+ .a.String()
+ .equalOneOf(['tampermonkey', 'greasemonkey', 'violentmonkey']);
+ compatabilityAssertion.should.have.property('versionConstraint').a.String;
+ }
+ );
}
describe('grant data', () => {
@@ -43,7 +43,7 @@ describe('gm polyfill overrides', () => {
grantFuncOverride.should.be.an.Object;
if (grantFuncOverride.deps) {
grantFuncOverride.deps.should.be.an.Array;
- grantFuncOverride.deps.should.matchEach((grantDep) => {
+ grantFuncOverride.deps.should.matchEach((grantDep: string) => {
grantDep.should.be.an.String;
});
}
diff --git a/tests/lib/data/compat-headers.js b/tests/lib/data/compat-headers.js
deleted file mode 100644
index 5e60a63..0000000
--- a/tests/lib/data/compat-headers.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const compatMap = require('../../../lib/data/compat-headers');
-
-require('should');
-
-describe('headers data', () => {
- it('should be an object', () => {
- compatMap.should.be.an.Object;
- });
-
- it('should have arrays as all the values with a schema', () => {
- Object.values(compatMap).should.matchEach((compatabilityDataCategory) => {
- compatabilityDataCategory.should.be.an.Object;
- Object.values(compatabilityDataCategory).should.matchEach(
- (compatabilityData) => {
- compatabilityData.should.be.an.Array;
- compatabilityData.should.matchEach((compatabilityAssertion) => {
- compatabilityAssertion.should.have
- .property('type')
- .a.String()
- .equalOneOf(['tampermonkey', 'greasemonkey', 'violentmonkey']);
- compatabilityAssertion.should.have.property('versionConstraint').a
- .String;
- });
- }
- );
- });
- });
-});
diff --git a/tests/lib/data/compat-headers.ts b/tests/lib/data/compat-headers.ts
new file mode 100644
index 0000000..f230451
--- /dev/null
+++ b/tests/lib/data/compat-headers.ts
@@ -0,0 +1,40 @@
+import type {
+ CompatMap,
+ VersionAssertion
+} from '../../../lib/data/version-assertion';
+import { compatMap } from '../../../lib/data/compat-headers';
+
+import 'should';
+
+describe('headers data', () => {
+ it('should be an object', () => {
+ compatMap.should.be.an.Object;
+ });
+
+ it('should have arrays as all the values with a schema', () => {
+ Object.values(compatMap).should.matchEach(
+ (compatabilityDataCategory: CompatMap) => {
+ compatabilityDataCategory.should.be.an.Object;
+ Object.values(compatabilityDataCategory).should.matchEach(
+ (compatabilityData: VersionAssertion[]) => {
+ compatabilityData.should.be.an.Array;
+ compatabilityData.should.matchEach(
+ (compatabilityAssertion: VersionAssertion) => {
+ compatabilityAssertion.should.have
+ .property('type')
+ .a.String()
+ .equalOneOf([
+ 'tampermonkey',
+ 'greasemonkey',
+ 'violentmonkey'
+ ]);
+ compatabilityAssertion.should.have.property('versionConstraint')
+ .a.String;
+ }
+ );
+ }
+ );
+ }
+ );
+ });
+});
diff --git a/tests/lib/index.js b/tests/lib/index.js
deleted file mode 100644
index 2e665cb..0000000
--- a/tests/lib/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const requireindex = require('requireindex');
-const fs = require('fs');
-const path = require('path');
-
-require('should');
-
-describe('config', () => {
- it('should have all rules', () => {
- Object.keys(require('../..').configs.recommended.rules)
- .map((ruleOption) => ruleOption.split('/')[1])
- .sort()
- .should.deepEqual(
- fs.readdirSync('lib/rules').map((filename) => filename.split('.js')[0])
- );
- });
-});
-
-describe('rules', () => {
- it('should have meta.docs.url', () => {
- require('../..').rules[Object.keys(require('../..').rules)[0]].meta.docs.url
- .should.be.String;
- });
-});
-
-module.exports = requireindex(
- __dirname.replace(/[/\\]tests([/\\]lib)$/, `$1${path.sep}rules`)
-);
diff --git a/tests/lib/index.ts b/tests/lib/index.ts
new file mode 100644
index 0000000..5de89a2
--- /dev/null
+++ b/tests/lib/index.ts
@@ -0,0 +1,21 @@
+import fs from 'node:fs';
+import 'should';
+
+import * as plugin from '../../lib';
+
+describe('config', () => {
+ it('should have all rules', () => {
+ Object.keys(plugin.configs.recommended.rules)
+ .map((ruleOption) => ruleOption.split('/')[1])
+ .sort()
+ .should.deepEqual(
+ fs.readdirSync('lib/rules').map((filename) => filename.split('.ts')[0])
+ );
+ });
+});
+
+describe('rules', () => {
+ it('should have meta.docs.url', () => {
+ plugin.rules[Object.keys(plugin.rules)[0]].meta.docs.url.should.be.String;
+ });
+});
diff --git a/tests/lib/rules/align-attributes.js b/tests/lib/rules/align-attributes.ts
similarity index 91%
rename from tests/lib/rules/align-attributes.js
rename to tests/lib/rules/align-attributes.ts
index e77eac2..5ab5493 100644
--- a/tests/lib/rules/align-attributes.js
+++ b/tests/lib/rules/align-attributes.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['align-attributes'];
-const RuleTester = require('eslint').RuleTester;
+import alignAttributes from '../../../lib/rules/align-attributes';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('align-attributes', rule, {
+ruleTester.run('align-attributes', alignAttributes, {
valid: [
`// ==UserScript==
// ==/UserScript==`,
diff --git a/tests/lib/rules/better-use-match.js b/tests/lib/rules/better-use-match.ts
similarity index 67%
rename from tests/lib/rules/better-use-match.js
rename to tests/lib/rules/better-use-match.ts
index 82a9b5d..0816adc 100644
--- a/tests/lib/rules/better-use-match.js
+++ b/tests/lib/rules/better-use-match.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['better-use-match'];
-const RuleTester = require('eslint').RuleTester;
+import betterUseMatch from '../../../lib/rules/better-use-match';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('better-use-match', rule, {
+ruleTester.run('better-use-match', betterUseMatch, {
valid: [
`// ==UserScript==
// @description This is my description
diff --git a/tests/lib/rules/compat-grant.js b/tests/lib/rules/compat-grant.ts
similarity index 97%
rename from tests/lib/rules/compat-grant.js
rename to tests/lib/rules/compat-grant.ts
index 85349c7..56c2487 100644
--- a/tests/lib/rules/compat-grant.js
+++ b/tests/lib/rules/compat-grant.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['compat-grant'];
-const RuleTester = require('eslint').RuleTester;
+import compatGrant from '../../../lib/rules/compat-grant';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('compat-grant', rule, {
+ruleTester.run('compat-grant', compatGrant, {
valid: [
`// ==UserScript==
// @grant GM.openInTab
diff --git a/tests/lib/rules/compat-headers.js b/tests/lib/rules/compat-headers.ts
similarity index 95%
rename from tests/lib/rules/compat-headers.js
rename to tests/lib/rules/compat-headers.ts
index 1343764..916d7b5 100644
--- a/tests/lib/rules/compat-headers.js
+++ b/tests/lib/rules/compat-headers.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['compat-headers'];
-const RuleTester = require('eslint').RuleTester;
+import compatHeaders from '../../../lib/rules/compat-headers';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('compat-headers', rule, {
+ruleTester.run('compat-headers', compatHeaders, {
valid: [
`// ==UserScript==
// @grant GM.openInTab
diff --git a/tests/lib/rules/filename-user.js b/tests/lib/rules/filename-user.ts
similarity index 81%
rename from tests/lib/rules/filename-user.js
rename to tests/lib/rules/filename-user.ts
index ffc6291..22c04d8 100644
--- a/tests/lib/rules/filename-user.js
+++ b/tests/lib/rules/filename-user.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['filename-user'];
-const RuleTester = require('eslint').RuleTester;
+import filenameUser from '../../../lib/rules/filename-user';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('filename-user', rule, {
+ruleTester.run('filename-user', filenameUser, {
valid: [
{
filename: 'hello.user.js',
@@ -15,7 +15,7 @@ ruleTester.run('filename-user', rule, {
options: ['always']
},
{
- filename: '/home/john/dir/theirfiledir/heythere.user.js',
+ filename: 'dir/theirfiledir/heythere.user.js',
code: '',
options: ['always']
},
@@ -30,7 +30,7 @@ ruleTester.run('filename-user', rule, {
options: ['never']
},
{
- filename: '/home/person/thing3eee.js',
+ filename: 'thing3eee.js',
code: 'var hey = 2',
options: ['never']
},
@@ -79,15 +79,15 @@ ruleTester.run('filename-user', rule, {
]
},
{
- filename: '/home/john/dir/theirfiledir/heythere.js',
+ filename: 'dir/theirfiledir/heythere.js',
code: '',
options: ['always'],
errors: [
{
messageId: 'filenameExtension',
data: {
- oldFilename: '/home/john/dir/theirfiledir/heythere.js',
- newFilename: '/home/john/dir/theirfiledir/heythere.user.js'
+ oldFilename: 'dir/theirfiledir/heythere.js',
+ newFilename: 'dir/theirfiledir/heythere.user.js'
}
}
]
@@ -121,15 +121,15 @@ ruleTester.run('filename-user', rule, {
]
},
{
- filename: '/home/person/thing3eee.user.js',
+ filename: 'thing3eee.user.js',
code: 'var hey = 2',
options: ['never'],
errors: [
{
messageId: 'filenameExtension',
data: {
- oldFilename: '/home/person/thing3eee.user.js',
- newFilename: '/home/person/thing3eee.js'
+ oldFilename: 'thing3eee.user.js',
+ newFilename: 'thing3eee.js'
}
}
]
diff --git a/tests/lib/rules/metadata-spacing.js b/tests/lib/rules/metadata-spacing.ts
similarity index 96%
rename from tests/lib/rules/metadata-spacing.js
rename to tests/lib/rules/metadata-spacing.ts
index 577b4d6..9721d2f 100644
--- a/tests/lib/rules/metadata-spacing.js
+++ b/tests/lib/rules/metadata-spacing.ts
@@ -1,9 +1,9 @@
-const rule = require('..')['metadata-spacing'];
-const RuleTester = require('eslint').RuleTester;
+import metadataSpacing from '../../../lib/rules/metadata-spacing';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('metadata-spacing', rule, {
+ruleTester.run('metadata-spacing', metadataSpacing, {
valid: [
`// ==UserScript==
// @name Bottom Padding to Swagger UI
diff --git a/tests/lib/rules/no-invalid-grant.js b/tests/lib/rules/no-invalid-grant.ts
similarity index 90%
rename from tests/lib/rules/no-invalid-grant.js
rename to tests/lib/rules/no-invalid-grant.ts
index d86506d..d5bf359 100644
--- a/tests/lib/rules/no-invalid-grant.js
+++ b/tests/lib/rules/no-invalid-grant.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['no-invalid-grant'];
-const RuleTester = require('eslint').RuleTester;
+import noInvalidGrant from '../../../lib/rules/no-invalid-grant';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('no-invalid-grant', rule, {
+ruleTester.run('no-invalid-grant', noInvalidGrant, {
valid: [
// "@grant" should not be detected if it's part of another header
// indent using tabs
diff --git a/tests/lib/rules/no-invalid-headers.js b/tests/lib/rules/no-invalid-headers.ts
similarity index 93%
rename from tests/lib/rules/no-invalid-headers.js
rename to tests/lib/rules/no-invalid-headers.ts
index 060fb49..cdb143c 100644
--- a/tests/lib/rules/no-invalid-headers.js
+++ b/tests/lib/rules/no-invalid-headers.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['no-invalid-headers'];
-const RuleTester = require('eslint').RuleTester;
+import noInvalidHeader from '../../../lib/rules/no-invalid-headers';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('no-invalid-headers', rule, {
+ruleTester.run('no-invalid-headers', noInvalidHeader, {
valid: [
`// ==UserScript==
// @name Bottom Padding to Swagger UI
diff --git a/tests/lib/rules/no-invalid-metadata.js b/tests/lib/rules/no-invalid-metadata.ts
similarity index 60%
rename from tests/lib/rules/no-invalid-metadata.js
rename to tests/lib/rules/no-invalid-metadata.ts
index 96a8a8f..8bb7546 100644
--- a/tests/lib/rules/no-invalid-metadata.js
+++ b/tests/lib/rules/no-invalid-metadata.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['no-invalid-metadata'];
-const RuleTester = require('eslint').RuleTester;
+import noInvalidMetadata from '../../../lib/rules/no-invalid-metadata';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('no-invalid-metadata', rule, {
+ruleTester.run('no-invalid-metadata', noInvalidMetadata, {
valid: [
// handle empty comment lines
`// ==UserScript==
@@ -159,6 +159,55 @@ ruleTester.run('no-invalid-metadata', rule, {
messageId: 'moveMetadataToTop'
}
]
+ },
+ {
+ code: `/*///////
+ // ==UserScript==
+ // @name Bottom Padding to Swagger UI
+ // @namespace https://github.com/Yash-Singh1/UserScripts
+ // @version 1.3
+ // @description Adds bottom padding to the Swagger UI
+ //
+ // @author Yash Singh
+ // @icon https://petstore.swagger.io/favicon-32x32.png
+ // @grant none
+ //
+ // @homepage https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme
+ // @homepageURL https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme
+ // @supportURL https://github.com/Yash-Singh1/UserScripts/issues
+ // @downloadURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js
+ // @updateURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js
+ // ==/UserScript==
+ /*///////`,
+ errors: [
+ {
+ messageId: 'moveMetadataToTop'
+ }
+ ]
+ },
+ {
+ code: `/*///////
+ // @name Bottom Padding to Swagger UI
+ // @namespace https://github.com/Yash-Singh1/UserScripts
+ // @version 1.3
+ // @description Adds bottom padding to the Swagger UI
+ //
+ // @author Yash Singh
+ // @icon https://petstore.swagger.io/favicon-32x32.png
+ // @grant none
+ //
+ // @homepage https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme
+ // @homepageURL https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme
+ // @supportURL https://github.com/Yash-Singh1/UserScripts/issues
+ // @downloadURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js
+ // @updateURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js
+ // ==/UserScript==
+ /*///////`,
+ errors: [
+ {
+ messageId: 'metadataRequired'
+ }
+ ]
}
]
});
diff --git a/tests/lib/rules/require-attribute-space-prefix.js b/tests/lib/rules/require-attribute-space-prefix.ts
similarity index 90%
rename from tests/lib/rules/require-attribute-space-prefix.js
rename to tests/lib/rules/require-attribute-space-prefix.ts
index 88e7aff..c0bd804 100644
--- a/tests/lib/rules/require-attribute-space-prefix.js
+++ b/tests/lib/rules/require-attribute-space-prefix.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['require-attribute-space-prefix'];
-const RuleTester = require('eslint').RuleTester;
+import requireAttributeSpacePrefix from '../../../lib/rules/require-attribute-space-prefix';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('require-attribute-space-prefix', rule, {
+ruleTester.run('require-attribute-space-prefix', requireAttributeSpacePrefix, {
valid: [
// handle empty comment lines
`// ==UserScript==
diff --git a/tests/lib/rules/require-description.js b/tests/lib/rules/require-description.ts
similarity index 89%
rename from tests/lib/rules/require-description.js
rename to tests/lib/rules/require-description.ts
index 7826907..1921dd3 100644
--- a/tests/lib/rules/require-description.js
+++ b/tests/lib/rules/require-description.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['require-description'];
-const RuleTester = require('eslint').RuleTester;
+import requireDescription from '../../../lib/rules/require-description';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('require-description', rule, {
+ruleTester.run('require-description', requireDescription, {
valid: [
`// ==UserScript==
// @description This is my description
diff --git a/tests/lib/rules/require-download-url.js b/tests/lib/rules/require-download-url.ts
similarity index 77%
rename from tests/lib/rules/require-download-url.js
rename to tests/lib/rules/require-download-url.ts
index aae3f24..38a09fa 100644
--- a/tests/lib/rules/require-download-url.js
+++ b/tests/lib/rules/require-download-url.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['require-download-url'];
-const RuleTester = require('eslint').RuleTester;
+import requireDownloadUrl from '../../../lib/rules/require-download-url';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('require-download-url', rule, {
+ruleTester.run('require-download-url', requireDownloadUrl, {
valid: [
`// ==UserScript==
// @downloadURL example.com
diff --git a/tests/lib/rules/require-name.js b/tests/lib/rules/require-name.ts
similarity index 96%
rename from tests/lib/rules/require-name.js
rename to tests/lib/rules/require-name.ts
index f3c8205..6550d75 100644
--- a/tests/lib/rules/require-name.js
+++ b/tests/lib/rules/require-name.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['require-name'];
-const RuleTester = require('eslint').RuleTester;
+import requireName from '../../../lib/rules/require-name';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('require-name', rule, {
+ruleTester.run('require-name', requireName, {
valid: [
`// ==UserScript==
// @name This is my description
diff --git a/tests/lib/rules/require-version.js b/tests/lib/rules/require-version.ts
similarity index 91%
rename from tests/lib/rules/require-version.js
rename to tests/lib/rules/require-version.ts
index bd74239..781ec28 100644
--- a/tests/lib/rules/require-version.js
+++ b/tests/lib/rules/require-version.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['require-version'];
-const RuleTester = require('eslint').RuleTester;
+import requireVersion from '../../../lib/rules/require-version';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('require-version', rule, {
+ruleTester.run('require-version', requireVersion, {
valid: [
'Alpha-v1',
'0.0.0',
diff --git a/tests/lib/rules/use-homepage-and-url.js b/tests/lib/rules/use-homepage-and-url.ts
similarity index 84%
rename from tests/lib/rules/use-homepage-and-url.js
rename to tests/lib/rules/use-homepage-and-url.ts
index eb53667..88bb763 100644
--- a/tests/lib/rules/use-homepage-and-url.js
+++ b/tests/lib/rules/use-homepage-and-url.ts
@@ -1,8 +1,8 @@
-const rule = require('..')['use-homepage-and-url'];
-const RuleTester = require('eslint').RuleTester;
+import useHomepageAndUrl from '../../../lib/rules/use-homepage-and-url';
+import { RuleTester } from 'eslint';
const ruleTester = new RuleTester();
-ruleTester.run('use-homepage-and-url', rule, {
+ruleTester.run('use-homepage-and-url', useHomepageAndUrl, {
valid: [
`// ==UserScript==
// @homepage example.com
diff --git a/tests/lib/utils/createValidator.js b/tests/lib/utils/createValidator.ts
similarity index 74%
rename from tests/lib/utils/createValidator.js
rename to tests/lib/utils/createValidator.ts
index 80ecd6c..b3afcf7 100644
--- a/tests/lib/utils/createValidator.js
+++ b/tests/lib/utils/createValidator.ts
@@ -1,5 +1,5 @@
-const createValidator = require('../../../lib/utils/createValidator.js');
-const assert = require('assert');
+import { createValidator } from '../../../lib/utils/createValidator';
+import assert from 'node:assert';
it('should properly generate description', () => {
assert.strictEqual(
@@ -15,10 +15,7 @@ it('should properly generate description', () => {
createValidator({
name: 'attributeName2',
required: true,
- validator: (
- /* eslint-disable-next-line no-unused-vars */
- arg
- ) => {}
+ validator: () => {}
}).meta.docs.description,
'require and validate attributeName2 in the metadata for userscripts'
);
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..496a393
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Default",
+ "compilerOptions": {
+ "composite": false,
+ "declaration": true,
+ "declarationMap": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSources": false,
+ "isolatedModules": true,
+ "moduleResolution": "node",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "es2020"
+ },
+ "include": ["lib/**/*.ts", "tests/**/*.ts", "should"],
+ "exclude": ["node_modules"]
+}
diff --git a/tsup.config.ts b/tsup.config.ts
new file mode 100644
index 0000000..be07a80
--- /dev/null
+++ b/tsup.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['lib/**/*.ts'],
+ sourcemap: false,
+ clean: true,
+ format: 'cjs',
+ target: 'es2019',
+ minify: false,
+ platform: 'node',
+ outDir: 'dist',
+ bundle: false
+});