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 +});