From 75eaa1abf567d7057fafd8d89e8dddd442cd64e3 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 30 Sep 2022 10:58:31 +0800 Subject: [PATCH 1/3] feat: create `defineRule` utility function --- packages/eslint-plugin-next/src/utils/define-rule.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/eslint-plugin-next/src/utils/define-rule.ts diff --git a/packages/eslint-plugin-next/src/utils/define-rule.ts b/packages/eslint-plugin-next/src/utils/define-rule.ts new file mode 100644 index 0000000000000..22220254a7ee5 --- /dev/null +++ b/packages/eslint-plugin-next/src/utils/define-rule.ts @@ -0,0 +1,3 @@ +import type { Rule } from 'eslint' + +export const defineRule = (rule: Rule.RuleModule) => rule From 3ac2fbb55cf46e354a40b6f71f02277b4085501d Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 30 Sep 2022 11:14:44 +0800 Subject: [PATCH 2/3] refactor: migrate all existing eslint util to ts --- packages/eslint-plugin-next/package.json | 1 + .../{get-root-dirs.js => get-root-dirs.ts} | 18 ++--- .../src/utils/node-attributes.js | 54 ------------- .../src/utils/node-attributes.ts | 74 ++++++++++++++++++ .../src/utils/{url.js => url.ts} | 78 +++++++++---------- pnpm-lock.yaml | 8 +- 6 files changed, 120 insertions(+), 113 deletions(-) rename packages/eslint-plugin-next/src/utils/{get-root-dirs.js => get-root-dirs.ts} (59%) delete mode 100644 packages/eslint-plugin-next/src/utils/node-attributes.js create mode 100644 packages/eslint-plugin-next/src/utils/node-attributes.ts rename packages/eslint-plugin-next/src/utils/{url.js => url.ts} (81%) diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 151c2001186bf..75da43f6c1d13 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -15,6 +15,7 @@ "glob": "7.1.7" }, "devDependencies": { + "@types/glob": "7.1.1", "eslint": "7.24.0" }, "scripts": { diff --git a/packages/eslint-plugin-next/src/utils/get-root-dirs.js b/packages/eslint-plugin-next/src/utils/get-root-dirs.ts similarity index 59% rename from packages/eslint-plugin-next/src/utils/get-root-dirs.js rename to packages/eslint-plugin-next/src/utils/get-root-dirs.ts index 9a65aa99201b8..7a7d2c40c1e42 100644 --- a/packages/eslint-plugin-next/src/utils/get-root-dirs.js +++ b/packages/eslint-plugin-next/src/utils/get-root-dirs.ts @@ -1,25 +1,19 @@ -// @ts-check -const glob = require('glob') +import * as glob from 'glob' +import type { Rule } from 'eslint' /** * Process a Next.js root directory glob. - * - * @param {string} rootDir - A Next.js root directory glob. - * @returns {string[]} - An array of Root directories. */ -const processRootDir = (rootDir) => { +const processRootDir = (rootDir: string): string[] => { // Ensures we only match folders. if (!rootDir.endsWith('/')) rootDir += '/' return glob.sync(rootDir) } /** - * Gets one or more Root - * - * @param {import('eslint').Rule.RuleContext} context - ESLint rule context - * @returns An array of root directories. + * Gets one or more Root, returns an array of root directories. */ -const getRootDirs = (context) => { +export const getRootDirs = (context: Rule.RuleContext) => { let rootDirs = [context.getCwd()] /** @type {{rootDir?:string|string[]}|undefined} */ @@ -36,5 +30,3 @@ const getRootDirs = (context) => { return rootDirs } - -module.exports = getRootDirs diff --git a/packages/eslint-plugin-next/src/utils/node-attributes.js b/packages/eslint-plugin-next/src/utils/node-attributes.js deleted file mode 100644 index 2e1e5792c5c92..0000000000000 --- a/packages/eslint-plugin-next/src/utils/node-attributes.js +++ /dev/null @@ -1,54 +0,0 @@ -// Return attributes and values of a node in a convenient way: -/* example: - - { attr1: { - hasValue: true, - value: 15 - }, - attr2: { - hasValue: false - } -Inclusion of hasValue is in case an eslint rule cares about boolean values -explicitly assigned to attribute vs the attribute being used as a flag -*/ -class NodeAttributes { - constructor(ASTnode) { - this.attributes = {} - ASTnode.attributes.forEach((attribute) => { - if (!attribute.type || attribute.type !== 'JSXAttribute') { - return - } - this.attributes[attribute.name.name] = { - hasValue: !!attribute.value, - } - if (attribute.value) { - if (attribute.value.value) { - this.attributes[attribute.name.name].value = attribute.value.value - } else if (attribute.value.expression) { - this.attributes[attribute.name.name].value = - typeof attribute.value.expression.value !== 'undefined' - ? attribute.value.expression.value - : attribute.value.expression.properties - } - } - }) - } - hasAny() { - return !!Object.keys(this.attributes).length - } - has(attrName) { - return !!this.attributes[attrName] - } - hasValue(attrName) { - return !!this.attributes[attrName].hasValue - } - value(attrName) { - if (!this.attributes[attrName]) { - return true - } - - return this.attributes[attrName].value - } -} - -module.exports = NodeAttributes diff --git a/packages/eslint-plugin-next/src/utils/node-attributes.ts b/packages/eslint-plugin-next/src/utils/node-attributes.ts new file mode 100644 index 0000000000000..4ad4667fa8b74 --- /dev/null +++ b/packages/eslint-plugin-next/src/utils/node-attributes.ts @@ -0,0 +1,74 @@ +// Return attributes and values of a node in a convenient way: +/* example: + + { attr1: { + hasValue: true, + value: 15 + }, + attr2: { + hasValue: false + } +Inclusion of hasValue is in case an eslint rule cares about boolean values +explicitly assigned to attribute vs the attribute being used as a flag +*/ +export default class NodeAttributes { + attributes: Record< + string, + | { + hasValue?: false + } + | { + hasValue: true + value: any + } + > + + constructor(ASTnode) { + this.attributes = {} + ASTnode.attributes.forEach((attribute) => { + if (!attribute.type || attribute.type !== 'JSXAttribute') { + return + } + + if (!!attribute.value) { + // hasValue + const value = attribute.value.value + ? attribute.value.value + : typeof attribute.value.expression.value !== 'undefined' + ? attribute.value.expression.value + : attribute.value.expression.properties + + this.attributes[attribute.name.name] = { + hasValue: true, + value, + } + } else { + this.attributes[attribute.name.name] = { + hasValue: false, + } + } + }) + } + hasAny() { + return !!Object.keys(this.attributes).length + } + has(attrName: string) { + return !!this.attributes[attrName] + } + hasValue(attrName: string) { + return !!this.attributes[attrName].hasValue + } + value(attrName: string) { + const attr = this.attributes[attrName] + + if (!attr) { + return true + } + + if (attr.hasValue) { + return attr.value + } + + return undefined + } +} diff --git a/packages/eslint-plugin-next/src/utils/url.js b/packages/eslint-plugin-next/src/utils/url.ts similarity index 81% rename from packages/eslint-plugin-next/src/utils/url.js rename to packages/eslint-plugin-next/src/utils/url.ts index 7f4823ac325bb..a49f00471040d 100644 --- a/packages/eslint-plugin-next/src/utils/url.js +++ b/packages/eslint-plugin-next/src/utils/url.ts @@ -1,5 +1,5 @@ -const fs = require('fs') -const path = require('path') +import * as path from 'path' +import * as fs from 'fs' // Cache for fs.lstatSync lookup. // Prevent multiple blocking IO requests that have already been calculated. @@ -11,53 +11,26 @@ const fsLstatSync = (source) => { /** * Checks if the source is a directory. - * @param {string} source */ -function isDirectory(source) { +function isDirectory(source: string) { return fsLstatSync(source).isDirectory() } /** * Checks if the source is a directory. - * @param {string} source */ -function isSymlink(source) { +function isSymlink(source: string) { return fsLstatSync(source).isSymbolicLink() } -/** - * Gets the possible URLs from a directory. - * @param {string} urlprefix - * @param {string[]} directories - */ -function getUrlFromPagesDirectories(urlPrefix, directories) { - return Array.from( - // De-duplicate similar pages across multiple directories. - new Set( - directories - .map((directory) => parseUrlForPages(urlPrefix, directory)) - .flat() - .map( - // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly. - (url) => `^${normalizeURL(url)}$` - ) - ) - ).map((urlReg) => { - urlReg = urlReg.replace(/\[.*\]/g, '((?!.+?\\..+?).*?)') - return new RegExp(urlReg) - }) -} - // Cache for fs.readdirSync lookup. // Prevent multiple blocking IO requests that have already been calculated. const fsReadDirSyncCache = {} /** * Recursively parse directory for page URLs. - * @param {string} urlprefix - * @param {string} directory */ -function parseUrlForPages(urlprefix, directory) { +function parseUrlForPages(urlprefix: string, directory: string) { fsReadDirSyncCache[directory] = fsReadDirSyncCache[directory] || fs.readdirSync(directory) const res = [] @@ -84,9 +57,8 @@ function parseUrlForPages(urlprefix, directory) { * - Replaces `index.html` with `/` * - Makes sure all URLs are have a trailing `/` * - Removes query string - * @param {string} url */ -function normalizeURL(url) { +export function normalizeURL(url: string) { if (!url) { return } @@ -101,11 +73,37 @@ function normalizeURL(url) { return url } -function execOnce(fn) { +/** + * Gets the possible URLs from a directory. + */ +export function getUrlFromPagesDirectories( + urlPrefix: string, + directories: string[] +) { + return Array.from( + // De-duplicate similar pages across multiple directories. + new Set( + directories + .map((directory) => parseUrlForPages(urlPrefix, directory)) + .flat() + .map( + // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly. + (url) => `^${normalizeURL(url)}$` + ) + ) + ).map((urlReg) => { + urlReg = urlReg.replace(/\[.*\]/g, '((?!.+?\\..+?).*?)') + return new RegExp(urlReg) + }) +} + +export function execOnce( + fn: (...args: TArgs) => TResult +): (...args: TArgs) => TResult { let used = false - let result + let result: TResult - return (...args) => { + return (...args: TArgs) => { if (!used) { used = true result = fn(...args) @@ -113,9 +111,3 @@ function execOnce(fn) { return result } } - -module.exports = { - getUrlFromPagesDirectories, - normalizeURL, - execOnce, -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a3bb5809f4bb..d8a2c15109ebd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -419,11 +419,13 @@ importers: packages/eslint-plugin-next: specifiers: + '@types/glob': 7.1.1 eslint: 7.24.0 glob: 7.1.7 dependencies: glob: 7.1.7 devDependencies: + '@types/glob': 7.1.1 eslint: 7.24.0 packages/font: @@ -594,8 +596,8 @@ importers: source-map: 0.6.1 stream-browserify: 3.0.0 stream-http: 3.1.1 - string_decoder: 1.3.0 string-hash: 1.1.3 + string_decoder: 1.3.0 strip-ansi: 6.0.0 styled-jsx: 5.0.7 tar: 6.1.11 @@ -784,8 +786,8 @@ importers: source-map: 0.6.1 stream-browserify: 3.0.0 stream-http: 3.1.1 - string_decoder: 1.3.0 string-hash: 1.1.3 + string_decoder: 1.3.0 strip-ansi: 6.0.0 tar: 6.1.11 taskr: 1.1.0 @@ -11618,8 +11620,8 @@ packages: engines: { node: '>=10' } hasBin: true dependencies: - is-text-path: 1.0.1 JSONStream: 1.3.5 + is-text-path: 1.0.1 lodash: 4.17.21 meow: 8.1.2 split2: 2.2.0 From 05562ed09ce651cc417a011801cad3358b0393af Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 30 Sep 2022 11:46:37 +0800 Subject: [PATCH 3/3] refactor: migrate all rules to ts --- ...font-display.js => google-font-display.ts} | 11 ++-- ...reconnect.js => google-font-preconnect.ts} | 9 +-- ...nline-script-id.js => inline-script-id.ts} | 8 ++- ...script-for-ga.js => next-script-for-ga.ts} | 9 +-- ...riable.js => no-assign-module-variable.ts} | 16 +++-- ...re-interactive-script-outside-document.ts} | 9 +-- .../rules/{no-css-tags.js => no-css-tags.ts} | 7 ++- ...-page.js => no-document-import-in-page.ts} | 9 +-- ...duplicate-head.js => no-duplicate-head.ts} | 16 +++-- ...{no-head-element.js => no-head-element.ts} | 8 ++- ...ument.js => no-head-import-in-document.ts} | 9 +-- ...for-pages.js => no-html-link-for-pages.ts} | 27 ++++---- .../src/rules/no-img-element.ts | 61 ++++++++++--------- ...-custom-font.js => no-page-custom-font.ts} | 47 ++++++++------ ...head.js => no-script-component-in-head.ts} | 7 ++- ...cument.js => no-styled-jsx-in-document.ts} | 9 +-- ...{no-sync-scripts.js => no-sync-scripts.ts} | 8 ++- ...t-head.js => no-title-in-document-head.ts} | 7 ++- .../src/rules/{no-typos.js => no-typos.ts} | 9 +-- ...olyfillio.js => no-unwanted-polyfillio.ts} | 8 ++- .../src/utils/get-root-dirs.ts | 4 +- 21 files changed, 168 insertions(+), 130 deletions(-) rename packages/eslint-plugin-next/src/rules/{google-font-display.js => google-font-display.ts} (88%) rename packages/eslint-plugin-next/src/rules/{google-font-preconnect.js => google-font-preconnect.ts} (87%) rename packages/eslint-plugin-next/src/rules/{inline-script-id.js => inline-script-id.ts} (95%) rename packages/eslint-plugin-next/src/rules/{next-script-for-ga.js => next-script-for-ga.ts} (94%) rename packages/eslint-plugin-next/src/rules/{no-assign-module-variable.js => no-assign-module-variable.ts} (67%) rename packages/eslint-plugin-next/src/rules/{no-before-interactive-script-outside-document.js => no-before-interactive-script-outside-document.ts} (91%) rename packages/eslint-plugin-next/src/rules/{no-css-tags.js => no-css-tags.ts} (91%) rename packages/eslint-plugin-next/src/rules/{no-document-import-in-page.js => no-document-import-in-page.ts} (87%) rename packages/eslint-plugin-next/src/rules/{no-duplicate-head.js => no-duplicate-head.ts} (77%) rename packages/eslint-plugin-next/src/rules/{no-head-element.js => no-head-element.ts} (88%) rename packages/eslint-plugin-next/src/rules/{no-head-import-in-document.js => no-head-import-in-document.ts} (87%) rename packages/eslint-plugin-next/src/rules/{no-html-link-for-pages.js => no-html-link-for-pages.ts} (86%) rename packages/eslint-plugin-next/src/rules/{no-page-custom-font.js => no-page-custom-font.ts} (82%) rename packages/eslint-plugin-next/src/rules/{no-script-component-in-head.js => no-script-component-in-head.ts} (92%) rename packages/eslint-plugin-next/src/rules/{no-styled-jsx-in-document.js => no-styled-jsx-in-document.ts} (89%) rename packages/eslint-plugin-next/src/rules/{no-sync-scripts.js => no-sync-scripts.ts} (90%) rename packages/eslint-plugin-next/src/rules/{no-title-in-document-head.js => no-title-in-document-head.ts} (93%) rename packages/eslint-plugin-next/src/rules/{no-typos.js => no-typos.ts} (95%) rename packages/eslint-plugin-next/src/rules/{no-unwanted-polyfillio.js => no-unwanted-polyfillio.ts} (97%) diff --git a/packages/eslint-plugin-next/src/rules/google-font-display.js b/packages/eslint-plugin-next/src/rules/google-font-display.ts similarity index 88% rename from packages/eslint-plugin-next/src/rules/google-font-display.js rename to packages/eslint-plugin-next/src/rules/google-font-display.ts index 19a7096eca907..5913d4a2025ac 100644 --- a/packages/eslint-plugin-next/src/rules/google-font-display.js +++ b/packages/eslint-plugin-next/src/rules/google-font-display.ts @@ -1,8 +1,9 @@ -const NodeAttributes = require('../utils/node-attributes.js') +import { defineRule } from '../utils/define-rule' +import NodeAttributes from '../utils/node-attributes' const url = 'https://nextjs.org/docs/messages/google-font-display' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Enforce font-display behavior with Google Fonts.', @@ -12,10 +13,10 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { - let message + let message: string | undefined if (node.name.name !== 'link') { return @@ -58,4 +59,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/google-font-preconnect.js b/packages/eslint-plugin-next/src/rules/google-font-preconnect.ts similarity index 87% rename from packages/eslint-plugin-next/src/rules/google-font-preconnect.js rename to packages/eslint-plugin-next/src/rules/google-font-preconnect.ts index ae5491f964c69..05da834b22b32 100644 --- a/packages/eslint-plugin-next/src/rules/google-font-preconnect.js +++ b/packages/eslint-plugin-next/src/rules/google-font-preconnect.ts @@ -1,8 +1,9 @@ -const NodeAttributes = require('../utils/node-attributes.js') +import { defineRule } from '../utils/define-rule' +import NodeAttributes from '../utils/node-attributes' const url = 'https://nextjs.org/docs/messages/google-font-preconnect' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Ensure `preconnect` is used with Google Fonts.', @@ -12,7 +13,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { if (node.name.name !== 'link') { @@ -43,4 +44,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/inline-script-id.js b/packages/eslint-plugin-next/src/rules/inline-script-id.ts similarity index 95% rename from packages/eslint-plugin-next/src/rules/inline-script-id.js rename to packages/eslint-plugin-next/src/rules/inline-script-id.ts index e7cd994bd1899..e73a38dd836a4 100644 --- a/packages/eslint-plugin-next/src/rules/inline-script-id.js +++ b/packages/eslint-plugin-next/src/rules/inline-script-id.ts @@ -1,6 +1,8 @@ +import { defineRule } from '../utils/define-rule' + const url = 'https://nextjs.org/docs/messages/inline-script-id' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -11,7 +13,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { let nextScriptImportName = null return { @@ -70,4 +72,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/next-script-for-ga.js b/packages/eslint-plugin-next/src/rules/next-script-for-ga.ts similarity index 94% rename from packages/eslint-plugin-next/src/rules/next-script-for-ga.js rename to packages/eslint-plugin-next/src/rules/next-script-for-ga.ts index 4dfd6bea6889b..e928e4219a1b0 100644 --- a/packages/eslint-plugin-next/src/rules/next-script-for-ga.js +++ b/packages/eslint-plugin-next/src/rules/next-script-for-ga.ts @@ -1,4 +1,5 @@ -const NodeAttributes = require('../utils/node-attributes.js') +import { defineRule } from '../utils/define-rule' +import NodeAttributes from '../utils/node-attributes' const SUPPORTED_SRCS = [ 'www.google-analytics.com/analytics.js', @@ -18,7 +19,7 @@ const containsStr = (str, strList) => { return strList.some((s) => str.includes(s)) } -module.exports = { +export = defineRule({ meta: { docs: { description, @@ -28,7 +29,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { if (node.name.name !== 'script') { @@ -76,4 +77,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-assign-module-variable.js b/packages/eslint-plugin-next/src/rules/no-assign-module-variable.ts similarity index 67% rename from packages/eslint-plugin-next/src/rules/no-assign-module-variable.js rename to packages/eslint-plugin-next/src/rules/no-assign-module-variable.ts index 459546741ca85..3fa7690810123 100644 --- a/packages/eslint-plugin-next/src/rules/no-assign-module-variable.js +++ b/packages/eslint-plugin-next/src/rules/no-assign-module-variable.ts @@ -1,6 +1,7 @@ +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-assign-module-variable' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent assignment to the `module` variable.', @@ -11,13 +12,16 @@ module.exports = { schema: [], }, - create: function (context) { + create(context) { return { VariableDeclaration(node) { // Checks node.declarations array for variable with id.name of `module` - const moduleVariableFound = node.declarations.some( - (declaration) => declaration.id.name === 'module' - ) + const moduleVariableFound = node.declarations.some((declaration) => { + if ('name' in declaration.id) { + return declaration.id.name === 'module' + } + return false + }) // Return early if no `module` variable is found if (!moduleVariableFound) { @@ -31,4 +35,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.js b/packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.ts similarity index 91% rename from packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.js rename to packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.ts index 9125fa0b2491d..26b81c626cf15 100644 --- a/packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.js +++ b/packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.ts @@ -1,9 +1,10 @@ -const path = require('path') +import { defineRule } from '../utils/define-rule' +import * as path from 'path' const url = 'https://nextjs.org/docs/messages/no-before-interactive-script-outside-document' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -14,7 +15,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { let scriptImportName = null return { @@ -56,4 +57,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-css-tags.js b/packages/eslint-plugin-next/src/rules/no-css-tags.ts similarity index 91% rename from packages/eslint-plugin-next/src/rules/no-css-tags.js rename to packages/eslint-plugin-next/src/rules/no-css-tags.ts index 520ae3a5f99d4..a206d28c75875 100644 --- a/packages/eslint-plugin-next/src/rules/no-css-tags.js +++ b/packages/eslint-plugin-next/src/rules/no-css-tags.ts @@ -1,6 +1,7 @@ +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-css-tags' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent manual stylesheet tags.', @@ -10,7 +11,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { if (node.name.name !== 'link') { @@ -43,4 +44,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-document-import-in-page.js b/packages/eslint-plugin-next/src/rules/no-document-import-in-page.ts similarity index 87% rename from packages/eslint-plugin-next/src/rules/no-document-import-in-page.js rename to packages/eslint-plugin-next/src/rules/no-document-import-in-page.ts index b801650e34e04..c5317f9960d94 100644 --- a/packages/eslint-plugin-next/src/rules/no-document-import-in-page.js +++ b/packages/eslint-plugin-next/src/rules/no-document-import-in-page.ts @@ -1,8 +1,9 @@ -const path = require('path') +import { defineRule } from '../utils/define-rule' +import * as path from 'path' const url = 'https://nextjs.org/docs/messages/no-document-import-in-page' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -13,7 +14,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { ImportDeclaration(node) { if (node.source.value !== 'next/document') { @@ -38,4 +39,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-duplicate-head.js b/packages/eslint-plugin-next/src/rules/no-duplicate-head.ts similarity index 77% rename from packages/eslint-plugin-next/src/rules/no-duplicate-head.js rename to packages/eslint-plugin-next/src/rules/no-duplicate-head.ts index 8b574fd73492b..0dcfdbe4140fa 100644 --- a/packages/eslint-plugin-next/src/rules/no-duplicate-head.js +++ b/packages/eslint-plugin-next/src/rules/no-duplicate-head.ts @@ -1,6 +1,7 @@ +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-duplicate-head' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -11,7 +12,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { let documentImportName return { ImportDeclaration(node) { @@ -30,6 +31,7 @@ module.exports = { (ancestorNode) => ancestorNode.type === 'ClassDeclaration' && ancestorNode.superClass && + 'name' in ancestorNode.superClass && ancestorNode.superClass.name === documentImportName ) @@ -37,7 +39,13 @@ module.exports = { return } - if (node.argument && node.argument.children) { + // @ts-expect-error - `node.argument` could be a `JSXElement` which has property `children` + if ( + node.argument && + 'children' in node.argument && + node.argument.children + ) { + // @ts-expect-error - `node.argument` could be a `JSXElement` which has property `children` const headComponents = node.argument.children.filter( (childrenNode) => childrenNode.openingElement && @@ -57,4 +65,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-head-element.js b/packages/eslint-plugin-next/src/rules/no-head-element.ts similarity index 88% rename from packages/eslint-plugin-next/src/rules/no-head-element.js rename to packages/eslint-plugin-next/src/rules/no-head-element.ts index c1668b2cf6fca..1bcd4874cf4c5 100644 --- a/packages/eslint-plugin-next/src/rules/no-head-element.js +++ b/packages/eslint-plugin-next/src/rules/no-head-element.ts @@ -1,6 +1,8 @@ +import { defineRule } from '../utils/define-rule' + const url = 'https://nextjs.org/docs/messages/no-head-element' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent usage of `` element.', @@ -11,7 +13,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { const paths = context.getFilename() @@ -29,4 +31,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-head-import-in-document.js b/packages/eslint-plugin-next/src/rules/no-head-import-in-document.ts similarity index 87% rename from packages/eslint-plugin-next/src/rules/no-head-import-in-document.js rename to packages/eslint-plugin-next/src/rules/no-head-import-in-document.ts index 9a852f40558ae..2d480c7c6648b 100644 --- a/packages/eslint-plugin-next/src/rules/no-head-import-in-document.js +++ b/packages/eslint-plugin-next/src/rules/no-head-import-in-document.ts @@ -1,8 +1,9 @@ -const path = require('path') +import { defineRule } from '../utils/define-rule' +import * as path from 'path' const url = 'https://nextjs.org/docs/messages/no-head-import-in-document' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent usage of `next/head` in `pages/_document.js`.', @@ -12,7 +13,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { ImportDeclaration(node) { if (node.source.value !== 'next/head') { @@ -38,4 +39,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-html-link-for-pages.js b/packages/eslint-plugin-next/src/rules/no-html-link-for-pages.ts similarity index 86% rename from packages/eslint-plugin-next/src/rules/no-html-link-for-pages.js rename to packages/eslint-plugin-next/src/rules/no-html-link-for-pages.ts index df4dfc58c0b7b..b5b06727f53c5 100644 --- a/packages/eslint-plugin-next/src/rules/no-html-link-for-pages.js +++ b/packages/eslint-plugin-next/src/rules/no-html-link-for-pages.ts @@ -1,12 +1,13 @@ -// @ts-check -const path = require('path') -const fs = require('fs') -const getRootDir = require('../utils/get-root-dirs') -const { +import { defineRule } from '../utils/define-rule' +import * as path from 'path' +import * as fs from 'fs' +import { getRootDirs } from '../utils/get-root-dirs' + +import { getUrlFromPagesDirectories, normalizeURL, execOnce, -} = require('../utils/url') +} from '../utils/url' const pagesDirWarning = execOnce((pagesDirs) => { console.warn( @@ -21,7 +22,7 @@ const fsExistsSyncCache = {} const url = 'https://nextjs.org/docs/messages/no-html-link-for-pages' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -51,16 +52,12 @@ module.exports = { /** * Creates an ESLint rule listener. - * - * @param {import('eslint').Rule.RuleContext} context - ESLint rule context - * @returns {import('eslint').Rule.RuleListener} An ESLint rule listener */ - create: function (context) { - /** @type {(string|string[])[]} */ - const ruleOptions = context.options + create(context) { + const ruleOptions: (string | string[])[] = context.options const [customPagesDirectory] = ruleOptions - const rootDirs = getRootDir(context) + const rootDirs = getRootDirs(context) const pagesDirs = ( customPagesDirectory @@ -135,4 +132,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-img-element.ts b/packages/eslint-plugin-next/src/rules/no-img-element.ts index baea84b669b93..0a5e5e4dcd32d 100644 --- a/packages/eslint-plugin-next/src/rules/no-img-element.ts +++ b/packages/eslint-plugin-next/src/rules/no-img-element.ts @@ -1,37 +1,38 @@ -import type { Rule } from 'eslint' +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-img-element' -export const meta: Rule.RuleMetaData = { - docs: { - description: 'Prevent usage of `` element to prevent layout shift.', - category: 'HTML', - recommended: true, - url, +export = defineRule({ + meta: { + docs: { + description: 'Prevent usage of `` element to prevent layout shift.', + category: 'HTML', + recommended: true, + url, + }, + type: 'problem', + schema: [], }, - type: 'problem', - schema: [], -} - -export const create: Rule.RuleModule['create'] = (context) => { - return { - JSXOpeningElement(node) { - if (node.name.name !== 'img') { - return - } + create(context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'img') { + return + } - if (node.attributes.length === 0) { - return - } + if (node.attributes.length === 0) { + return + } - if (node.parent?.parent?.openingElement?.name?.name === 'picture') { - return - } + if (node.parent?.parent?.openingElement?.name?.name === 'picture') { + return + } - context.report({ - node, - message: `Do not use \`\` element. Use \`\` from \`next/image\` instead. See: ${url}`, - }) - }, - } -} + context.report({ + node, + message: `Do not use \`\` element. Use \`\` from \`next/image\` instead. See: ${url}`, + }) + }, + } + }, +}) diff --git a/packages/eslint-plugin-next/src/rules/no-page-custom-font.js b/packages/eslint-plugin-next/src/rules/no-page-custom-font.ts similarity index 82% rename from packages/eslint-plugin-next/src/rules/no-page-custom-font.js rename to packages/eslint-plugin-next/src/rules/no-page-custom-font.ts index 6bd5e7872aba2..6cbe4f7243a62 100644 --- a/packages/eslint-plugin-next/src/rules/no-page-custom-font.js +++ b/packages/eslint-plugin-next/src/rules/no-page-custom-font.ts @@ -1,9 +1,15 @@ -const NodeAttributes = require('../utils/node-attributes.js') -const { sep, posix } = require('path') +import { defineRule } from '../utils/define-rule' +import NodeAttributes from '../utils/node-attributes' +import { sep, posix } from 'path' +import type { AST } from 'eslint' const url = 'https://nextjs.org/docs/messages/no-page-custom-font' -module.exports = { +function isIdentifierMatch(id1, id2) { + return (id1 === null && id2 === null) || (id1 && id2 && id1.name === id2.name) +} + +export = defineRule({ meta: { docs: { description: 'Prevent page-only custom fonts.', @@ -13,7 +19,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { const paths = context.getFilename().split('pages') const page = paths[paths.length - 1] @@ -53,6 +59,7 @@ module.exports = { if ( node.declaration.type === 'ClassDeclaration' && node.declaration.superClass && + 'name' in node.declaration.superClass && node.declaration.superClass.name === documentImportName ) { localDefaultExportId = node.declaration.id @@ -73,7 +80,7 @@ module.exports = { // find the top level of the module const program = ancestors.find( (ancestor) => ancestor.type === 'Program' - ) + ) as AST.Program // go over each token to find the combination of `export default ` for (let i = 0; i <= program.tokens.length - 1; i++) { @@ -106,22 +113,28 @@ module.exports = { if (exportDeclarationType === 'ClassDeclaration') { return ( ancestor.type === exportDeclarationType && + 'superClass' in ancestor && ancestor.superClass && + 'name' in ancestor.superClass && ancestor.superClass.name === documentImportName ) } - // export default function ... - if (exportDeclarationType === 'FunctionDeclaration') { - return ( - ancestor.type === exportDeclarationType && - isIdentifierMatch(ancestor.id, localDefaultExportId) - ) + if ('id' in ancestor) { + // export default function ... + if (exportDeclarationType === 'FunctionDeclaration') { + return ( + ancestor.type === exportDeclarationType && + isIdentifierMatch(ancestor.id, localDefaultExportId) + ) + } + + // function ...() {} export default ... + // class ... extends ...; export default ... + return isIdentifierMatch(ancestor.id, localDefaultExportId) } - // function ...() {} export default ... - // class ... extends ...; export default ... - return isIdentifierMatch(ancestor.id, localDefaultExportId) + return false }) // file starts with _document and this is within the default export @@ -154,8 +167,4 @@ module.exports = { }, } }, -} - -function isIdentifierMatch(id1, id2) { - return (id1 === null && id2 === null) || (id1 && id2 && id1.name === id2.name) -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-script-component-in-head.js b/packages/eslint-plugin-next/src/rules/no-script-component-in-head.ts similarity index 92% rename from packages/eslint-plugin-next/src/rules/no-script-component-in-head.js rename to packages/eslint-plugin-next/src/rules/no-script-component-in-head.ts index 9e342e738f42f..68b063ef8abc5 100644 --- a/packages/eslint-plugin-next/src/rules/no-script-component-in-head.js +++ b/packages/eslint-plugin-next/src/rules/no-script-component-in-head.ts @@ -1,6 +1,7 @@ +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-script-component-in-head' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent usage of `next/script` in `next/head` component.', @@ -10,7 +11,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { let isNextHead = null return { @@ -52,4 +53,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.js b/packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.ts similarity index 89% rename from packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.js rename to packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.ts index 342f93eb1a066..42f4a4b899bb9 100644 --- a/packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.js +++ b/packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.ts @@ -1,8 +1,9 @@ -const path = require('path') +import { defineRule } from '../utils/define-rule' +import * as path from 'path' const url = 'https://nextjs.org/docs/messages/no-styled-jsx-in-document' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent usage of `styled-jsx` in `pages/_document.js`.', @@ -12,7 +13,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { const document = context.getFilename().split('pages')[1] @@ -44,4 +45,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-sync-scripts.js b/packages/eslint-plugin-next/src/rules/no-sync-scripts.ts similarity index 90% rename from packages/eslint-plugin-next/src/rules/no-sync-scripts.js rename to packages/eslint-plugin-next/src/rules/no-sync-scripts.ts index 335c429fa5632..e786d2a52d5d1 100644 --- a/packages/eslint-plugin-next/src/rules/no-sync-scripts.js +++ b/packages/eslint-plugin-next/src/rules/no-sync-scripts.ts @@ -1,6 +1,8 @@ +import { defineRule } from '../utils/define-rule' + const url = 'https://nextjs.org/docs/messages/no-sync-scripts' -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent synchronous scripts.', @@ -10,7 +12,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { return { JSXOpeningElement(node) { if (node.name.name !== 'script') { @@ -35,4 +37,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-title-in-document-head.js b/packages/eslint-plugin-next/src/rules/no-title-in-document-head.ts similarity index 93% rename from packages/eslint-plugin-next/src/rules/no-title-in-document-head.js rename to packages/eslint-plugin-next/src/rules/no-title-in-document-head.ts index 5c6346ee84cc8..f164498e0e00d 100644 --- a/packages/eslint-plugin-next/src/rules/no-title-in-document-head.js +++ b/packages/eslint-plugin-next/src/rules/no-title-in-document-head.ts @@ -1,6 +1,7 @@ +import { defineRule } from '../utils/define-rule' const url = 'https://nextjs.org/docs/messages/no-title-in-document-head' -module.exports = { +export = defineRule({ meta: { docs: { description: @@ -11,7 +12,7 @@ module.exports = { type: 'problem', schema: [], }, - create: function (context) { + create(context) { let headFromNextDocument = false return { ImportDeclaration(node) { @@ -51,4 +52,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-typos.js b/packages/eslint-plugin-next/src/rules/no-typos.ts similarity index 95% rename from packages/eslint-plugin-next/src/rules/no-typos.js rename to packages/eslint-plugin-next/src/rules/no-typos.ts index 09ca6f3128998..9882746d0b601 100644 --- a/packages/eslint-plugin-next/src/rules/no-typos.js +++ b/packages/eslint-plugin-next/src/rules/no-typos.ts @@ -1,4 +1,5 @@ -const path = require('path') +import { defineRule } from '../utils/define-rule' +import * as path from 'path' const NEXT_EXPORT_FUNCTIONS = [ 'getStaticProps', @@ -40,7 +41,7 @@ function minDistance(a, b) { } /* eslint-disable eslint-plugin/require-meta-docs-url */ -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent common typos in Next.js data fetching functions.', @@ -50,7 +51,7 @@ module.exports = { schema: [], }, - create: function (context) { + create(context) { function checkTypos(node, name) { if (NEXT_EXPORT_FUNCTIONS.includes(name)) { return @@ -105,4 +106,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.js b/packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts similarity index 97% rename from packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.js rename to packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts index 7fc24d756e927..b1ac5f2780d45 100644 --- a/packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.js +++ b/packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts @@ -1,3 +1,5 @@ +import { defineRule } from '../utils/define-rule' + // Keep in sync with next.js polyfills file : https://github.com/vercel/next.js/blob/master/packages/next-polyfill-nomodule/src/index.js const NEXT_POLYFILLED_FEATURES = [ 'Array.prototype.@@iterator', @@ -69,7 +71,7 @@ const url = 'https://nextjs.org/docs/messages/no-unwanted-polyfillio' //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = { +export = defineRule({ meta: { docs: { description: 'Prevent duplicate polyfills from Polyfill.io.', @@ -81,7 +83,7 @@ module.exports = { schema: [], }, - create: function (context) { + create(context) { let scriptImport = null return { @@ -132,4 +134,4 @@ module.exports = { }, } }, -} +}) diff --git a/packages/eslint-plugin-next/src/utils/get-root-dirs.ts b/packages/eslint-plugin-next/src/utils/get-root-dirs.ts index 7a7d2c40c1e42..db8b6b712d5eb 100644 --- a/packages/eslint-plugin-next/src/utils/get-root-dirs.ts +++ b/packages/eslint-plugin-next/src/utils/get-root-dirs.ts @@ -16,8 +16,8 @@ const processRootDir = (rootDir: string): string[] => { export const getRootDirs = (context: Rule.RuleContext) => { let rootDirs = [context.getCwd()] - /** @type {{rootDir?:string|string[]}|undefined} */ - const nextSettings = context.settings.next || {} + const nextSettings: { rootDir?: string | string[] } = + context.settings.next || {} let rootDir = nextSettings.rootDir if (typeof rootDir === 'string') {