diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6d8b03647d5c..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,24 +0,0 @@ -__fixtures__ -__mocks__ -dist -node_modules -.yarn -.history -build -coverage -examples/ - -packages/lqip-loader/lib/ -packages/docusaurus/lib/ -packages/docusaurus-*/lib/* -packages/eslint-plugin/lib/ -packages/stylelint-copyright/lib/ - -packages/create-docusaurus/lib/* -packages/create-docusaurus/templates/facebook - -website/_dogfooding/_swizzle_theme_tests -website/_dogfooding/_asset-tests/badSyntax.js - - -packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 61bde859a1c5..000000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,555 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const OFF = 0; -const WARNING = 1; -const ERROR = 2; - -// Prevent importing lodash, usually for browser bundle size reasons -const LodashImportPatterns = ['lodash', 'lodash.**', 'lodash/**']; - -// Prevent importing content plugins, usually for coupling reasons -const ContentPluginsImportPatterns = [ - '@docusaurus/plugin-content-blog', - '@docusaurus/plugin-content-blog/**', - // TODO fix theme-common => docs dependency issue - // '@docusaurus/plugin-content-docs', - // '@docusaurus/plugin-content-docs/**', - '@docusaurus/plugin-content-pages', - '@docusaurus/plugin-content-pages/**', -]; - -module.exports = { - root: true, - env: { - browser: true, - commonjs: true, - node: true, - }, - parser: '@typescript-eslint/parser', - parserOptions: { - // tsconfigRootDir: __dirname, - // project: ['./tsconfig.base.json', './website/tsconfig.base.json'], - }, - globals: { - JSX: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:react-hooks/recommended', - - 'plugin:@vitest/legacy-recommended', - - 'airbnb', - 'plugin:@typescript-eslint/recommended', - // 'plugin:@typescript-eslint/recommended-requiring-type-checking', - // 'plugin:@typescript-eslint/strict', - 'plugin:regexp/recommended', - 'prettier', - 'plugin:@docusaurus/all', - ], - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - }, - reportUnusedDisableDirectives: true, - plugins: [ - 'react-compiler', - 'react-hooks', - 'header', - '@vitest', - '@typescript-eslint', - 'regexp', - '@docusaurus', - ], - rules: { - 'react-compiler/react-compiler': ERROR, - 'react/jsx-uses-react': OFF, // JSX runtime: automatic - 'react/react-in-jsx-scope': OFF, // JSX runtime: automatic - 'array-callback-return': WARNING, - camelcase: WARNING, - 'class-methods-use-this': OFF, // It's a way of allowing private variables. - curly: [WARNING, 'all'], - 'global-require': WARNING, - 'lines-between-class-members': OFF, - 'max-classes-per-file': OFF, - 'max-len': [ - WARNING, - { - code: Infinity, // Code width is already enforced by Prettier/oxfmt - tabWidth: 2, - comments: 80, - ignoreUrls: true, - ignorePattern: '(eslint-disable|@)', - }, - ], - 'arrow-body-style': OFF, - 'no-await-in-loop': OFF, - 'no-case-declarations': WARNING, - 'no-console': OFF, - 'no-constant-binary-expression': ERROR, - 'no-continue': OFF, - 'no-control-regex': WARNING, - 'no-else-return': OFF, - 'no-empty': [WARNING, {allowEmptyCatch: true}], - 'no-lonely-if': WARNING, - 'no-nested-ternary': WARNING, - 'no-param-reassign': [WARNING, {props: false}], - 'no-prototype-builtins': WARNING, - 'no-restricted-exports': OFF, - 'no-restricted-properties': [ - ERROR, - .../** @type {[string, string][]} */ ([ - // TODO: TS doesn't make Boolean a narrowing function yet, - // so filter(Boolean) is problematic type-wise - // ['compact', 'Array#filter(Boolean)'], - ['concat', 'Array#concat'], - ['drop', 'Array#slice(n)'], - ['dropRight', 'Array#slice(0, -n)'], - ['fill', 'Array#fill'], - ['filter', 'Array#filter'], - ['find', 'Array#find'], - ['findIndex', 'Array#findIndex'], - ['first', 'foo[0]'], - ['flatten', 'Array#flat'], - ['flattenDeep', 'Array#flat(Infinity)'], - ['flatMap', 'Array#flatMap'], - ['fromPairs', 'Object.fromEntries'], - ['head', 'foo[0]'], - ['indexOf', 'Array#indexOf'], - ['initial', 'Array#slice(0, -1)'], - ['join', 'Array#join'], - // Unfortunately there's no great alternative to _.last yet - // Candidates: foo.slice(-1)[0]; foo[foo.length - 1] - // Array#at is ES2022; could replace _.nth as well - // ['last'], - ['map', 'Array#map'], - ['reduce', 'Array#reduce'], - ['reverse', 'Array#reverse'], - ['slice', 'Array#slice'], - ['take', 'Array#slice(0, n)'], - ['takeRight', 'Array#slice(-n)'], - ['tail', 'Array#slice(1)'], - ]).map(([property, alternative]) => ({ - object: '_', - property, - message: `Use ${alternative} instead.`, - })), - ...[ - 'readdirSync', - 'readFileSync', - 'statSync', - 'lstatSync', - 'existsSync', - 'pathExistsSync', - 'realpathSync', - 'mkdirSync', - 'mkdirpSync', - 'mkdirsSync', - 'writeFileSync', - 'writeJsonSync', - 'outputFileSync', - 'outputJsonSync', - 'moveSync', - 'copySync', - 'copyFileSync', - 'ensureFileSync', - 'ensureDirSync', - 'ensureLinkSync', - 'ensureSymlinkSync', - 'unlinkSync', - 'removeSync', - 'emptyDirSync', - ].map((property) => ({ - object: 'fs', - property, - message: 'Do not use sync fs methods.', - })), - ], - 'no-restricted-syntax': [ - WARNING, - // Copied from airbnb, removed for...of statement, added export all - { - selector: 'ForInStatement', - message: - 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', - }, - { - selector: 'LabeledStatement', - message: - 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - }, - { - selector: 'WithStatement', - message: - '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', - }, - { - selector: 'ExportAllDeclaration', - message: - "Export all does't work well if imported in ESM due to how they are transpiled, and they can also lead to unexpected exposure of internal methods.", - }, - // TODO make an internal plugin to ensure this - // { - // selector: - // @ 'ExportDefaultDeclaration > Identifier, ExportNamedDeclaration[source=null] > ExportSpecifier', - // message: 'Export in one statement' - // }, - ...['path', 'fs-extra', 'webpack', 'lodash'].map((m) => ({ - selector: `ImportDeclaration[importKind=value]:has(Literal[value=${m}]) > ImportSpecifier[importKind=value]`, - message: - 'Default-import this, both for readability and interoperability with ESM', - })), - ], - 'no-template-curly-in-string': WARNING, - 'no-unused-expressions': [ - WARNING, - {allowTaggedTemplates: true, allowShortCircuit: true}, - ], - 'no-useless-escape': WARNING, - 'no-void': [ERROR, {allowAsStatement: true}], - 'prefer-destructuring': OFF, - 'prefer-named-capture-group': WARNING, - 'prefer-template': WARNING, - yoda: WARNING, - - 'header/header': [ - ERROR, - 'block', - [ - '*', - ' * Copyright (c) Facebook, Inc. and its affiliates.', - ' *', - ' * This source code is licensed under the MIT license found in the', - ' * LICENSE file in the root directory of this source tree.', - ' ', - ], - ], - - 'import/extensions': OFF, - // This rule doesn't yet support resolving .js imports when the actual file - // is .ts. Plus it's not all that useful when our code is fully TS-covered. - 'import/no-unresolved': [ - OFF, - { - // Ignore certain webpack aliases because they can't be resolved - ignore: [ - '^@theme', - '^@docusaurus', - '^@generated', - '^@site', - '^@testing-utils', - ], - }, - ], - 'import/order': [ - WARNING, - { - groups: [ - 'builtin', - 'external', - 'internal', - ['parent', 'sibling', 'index'], - 'type', - ], - pathGroups: [ - // always put css import to the last, ref: - // https://github.com/import-js/eslint-plugin-import/issues/1239 - { - pattern: '*.+(css|sass|less|scss|pcss|styl)', - group: 'unknown', - patternOptions: {matchBase: true}, - position: 'after', - }, - {pattern: 'vitest', group: 'builtin', position: 'before'}, - {pattern: 'react', group: 'builtin', position: 'before'}, - {pattern: 'react-dom', group: 'builtin', position: 'before'}, - {pattern: 'react-dom/**', group: 'builtin', position: 'before'}, - {pattern: 'stream', group: 'builtin', position: 'before'}, - {pattern: 'fs-extra', group: 'builtin'}, - {pattern: 'lodash', group: 'external', position: 'before'}, - {pattern: 'clsx', group: 'external', position: 'before'}, - // 'Bit weird to not use the `import/internal-regex` option, but this - // way, we can make `import type { Props } from "@theme/*"` appear - // before `import styles from "styles.module.css"`, which is what we - // always did. This should be removable once we stop using ambient - // module declarations for theme aliases. - {pattern: '@theme/**', group: 'internal'}, - {pattern: '@site/**', group: 'internal'}, - {pattern: '@theme-init/**', group: 'internal'}, - {pattern: '@theme-original/**', group: 'internal'}, - ], - pathGroupsExcludedImportTypes: [], - // example: let `import './nprogress.css';` after importing others - // in `packages/docusaurus-theme-classic/src/nprogress.ts` - // see more: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#warnonunassignedimports-truefalse - warnOnUnassignedImports: true, - }, - ], - 'import/prefer-default-export': OFF, - - '@vitest/consistent-test-it': WARNING, - '@vitest/expect-expect': OFF, - '@vitest/no-large-snapshots': [ - WARNING, - {maxSize: Infinity, inlineMaxSize: 50}, - ], - '@vitest/no-test-return-statement': ERROR, - '@vitest/prefer-expect-resolves': WARNING, - '@vitest/prefer-lowercase-title': [WARNING, {ignore: ['describe']}], - '@vitest/prefer-spy-on': WARNING, - '@vitest/prefer-to-be': OFF, - '@vitest/prefer-to-have-length': WARNING, - '@vitest/require-top-level-describe': ERROR, - '@vitest/valid-title': [ - ERROR, - { - mustNotMatch: { - it: [ - '^should|\\.$', - 'Titles should not begin with "should" or end with a full-stop', - ], - }, - }, - ], - - 'jsx-a11y/click-events-have-key-events': WARNING, - 'jsx-a11y/no-noninteractive-element-interactions': WARNING, - 'jsx-a11y/html-has-lang': OFF, - - 'react-hooks/rules-of-hooks': ERROR, - 'react-hooks/exhaustive-deps': ERROR, - - // Sometimes we do need the props as a whole, e.g. when spreading - 'react/destructuring-assignment': OFF, - 'react/function-component-definition': [ - WARNING, - { - namedComponents: 'function-declaration', - unnamedComponents: 'arrow-function', - }, - ], - 'react/jsx-filename-extension': OFF, - 'react/jsx-key': [ERROR, {checkFragmentShorthand: true}], - 'react/jsx-no-useless-fragment': [ERROR, {allowExpressions: true}], - 'react/jsx-props-no-spreading': OFF, - 'react/no-array-index-key': OFF, // We build a static site, and nearly all components don't change. - 'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}], - 'react/prefer-stateless-function': WARNING, - 'react/prop-types': OFF, - 'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}], - - '@typescript-eslint/consistent-type-definitions': OFF, - '@typescript-eslint/require-await': OFF, - - '@typescript-eslint/ban-ts-comment': [ - ERROR, - {'ts-expect-error': 'allow-with-description'}, - ], - '@typescript-eslint/consistent-indexed-object-style': OFF, - '@typescript-eslint/consistent-type-imports': [ - WARNING, - {disallowTypeAnnotations: false}, - ], - '@typescript-eslint/explicit-module-boundary-types': WARNING, - '@typescript-eslint/method-signature-style': ERROR, - '@typescript-eslint/no-empty-function': OFF, - '@typescript-eslint/no-empty-interface': [ - ERROR, - { - allowSingleExtends: true, - }, - ], - '@typescript-eslint/no-inferrable-types': OFF, - '@typescript-eslint/no-namespace': [WARNING, {allowDeclarations: true}], - 'no-use-before-define': OFF, - '@typescript-eslint/no-use-before-define': [ - ERROR, - {functions: false, classes: false, variables: true}, - ], - '@typescript-eslint/no-non-null-assertion': OFF, - 'no-redeclare': OFF, - '@typescript-eslint/no-redeclare': ERROR, - 'no-shadow': OFF, - '@typescript-eslint/no-shadow': ERROR, - 'no-unused-vars': OFF, - // We don't provide any escape hatches for this rule. Rest siblings and - // function placeholder params are always ignored, and any other unused - // locals must be justified with a disable comment. - '@typescript-eslint/no-unused-vars': [ - ERROR, - { - ignoreRestSiblings: true, - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/prefer-optional-chain': ERROR, - '@docusaurus/no-html-links': ERROR, - '@docusaurus/prefer-docusaurus-heading': ERROR, - '@docusaurus/no-untranslated-text': [ - WARNING, - { - ignoredStrings: [ - '·', - '-', - '—', - '×', - '​', // zwj: ​ - '@', - 'WebContainers', - 'Twitter', - 'X', - 'GitHub', - 'Dev.to', - '1.x', - ], - }, - ], - }, - overrides: [ - { - files: ['packages/docusaurus/src/client/**/*.{js,ts,tsx}'], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: [ - ...LodashImportPatterns, - ...ContentPluginsImportPatterns, - // Prevent importing server code in client bundle - '**/../babel/**', - '**/../server/**', - '**/../commands/**', - '**/../webpack/**', - ], - }, - ], - }, - }, - { - files: [ - 'packages/docusaurus-theme-common/src/**/*.{js,ts,tsx}', - 'packages/docusaurus-utils-common/src/**/*.{js,ts,tsx}', - ], - excludedFiles: '*.test.{js,ts,tsx}', - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: [ - ...LodashImportPatterns, - ...ContentPluginsImportPatterns, - ], - }, - ], - }, - }, - { - files: ['packages/docusaurus-*/src/theme/**/*.{js,ts,tsx}'], - excludedFiles: '*.test.{js,ts,tsx}', - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: LodashImportPatterns.concat( - // Prevents relative imports between React theme components - [ - '../**', - './**', - // Allows relative styles module import with consistent filename - '!./styles.module.css', - ], - ), - }, - ], - }, - }, - { - files: [ - 'packages/docusaurus-*/src/theme/**/*.{js,ts,tsx}', - 'packages/docusaurus/src/client/theme-fallback/**/*.{js,ts,tsx}', - ], - rules: { - 'import/no-named-export': ERROR, - }, - }, - { - files: ['packages/create-docusaurus/templates/**/*.{js,ts,tsx}'], - rules: { - 'header/header': OFF, - 'global-require': OFF, - '@typescript-eslint/no-var-requires': OFF, - '@docusaurus/no-untranslated-text': OFF, - }, - }, - { - files: ['*.d.ts'], - rules: { - 'import/no-duplicates': OFF, - }, - }, - { - files: ['*.{ts,tsx}'], - rules: { - 'no-undef': OFF, - 'import/no-import-module-exports': OFF, - }, - }, - { - files: ['*.{js,mjs,cjs}'], - rules: { - // Make JS code directly runnable in Node. - '@typescript-eslint/no-var-requires': OFF, - '@typescript-eslint/explicit-module-boundary-types': OFF, - }, - }, - { - files: [ - '**/__tests__/**', - 'packages/docusaurus-plugin-debug/**', - 'website/_dogfooding/**', - ], - rules: { - '@docusaurus/no-untranslated-text': OFF, - }, - }, - { - // Internal files where extraneous deps don't matter much at long as - // they run - files: [ - '*.test.{js,ts,tsx}', - '**/__tests__/**', - 'admin/**', - 'test/**', - 'website/**', - 'packages/docusaurus-theme-common/removeThemeInternalReexport.mjs', - 'packages/docusaurus-theme-translations/update.mjs', - 'packages/docusaurus-theme-translations/src/utils.ts', - ], - rules: { - 'import/no-extraneous-dependencies': OFF, - }, - }, - { - files: ['packages/eslint-plugin/**/*.{js,ts}'], - extends: ['plugin:eslint-plugin/recommended'], - }, - { - files: [ - 'packages/docusaurus-plugin-debug/**', - 'packages/docusaurus/src/**', - ], - rules: { - '@docusaurus/prefer-docusaurus-heading': OFF, - }, - }, - ], -}; diff --git a/.gitattributes b/.gitattributes index 7f6e48fc5d26..c809e70abfc4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,6 +39,6 @@ test/** linguist-vendored admin/** linguist-documentation website/** linguist-documentation packages/create-docusaurus/templates/** linguist-vendored -.eslintrc.* linguist-vendored +eslint.config.* linguist-vendored vitest.config.* linguist-vendored .stylelintrc.* linguist-vendored diff --git a/.syncpackrc.ts b/.syncpackrc.ts index c80893e549cd..a945b1b4db3d 100644 --- a/.syncpackrc.ts +++ b/.syncpackrc.ts @@ -30,6 +30,12 @@ export default { ], versionGroups: [ + // TODO temporary, need to upgrade jiti deps + { + dependencies: ['jiti'], + isIgnored: true, + }, + { label: 'Ignore * deps in type-alias packages', packages: [ diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 000000000000..4b352d3d8f0d --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import {defineConfig, globalIgnores} from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; +import js from '@eslint/js'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +// @ts-expect-error: no types provided +import header from 'eslint-plugin-header'; +import importPlugin from 'eslint-plugin-import'; +import vitest from '@vitest/eslint-plugin'; +// @ts-expect-error: no types provided +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import docusaurus from '@docusaurus/eslint-plugin'; +import regexp from 'eslint-plugin-regexp'; +import prettier from 'eslint-config-prettier/flat'; + +import rules from './eslint.rules'; + +const plugins = defineConfig([ + js.configs.recommended, + tseslint.configs.recommended, + react.configs.flat.recommended, + reactHooks.configs.flat.recommended, + importPlugin.flatConfigs.recommended, + vitest.configs.recommended, + jsxA11y.flatConfigs.recommended, + regexp.configs['flat/recommended'], + prettier, + docusaurus.configs.flat.all, + + // TODO replace by maintained plugin? + // See https://github.com/facebook/docusaurus/pull/11803 + // This adapts the legacy plugin to flat config + { + plugins: { + header: { + meta: { + name: 'eslint-plugin-header', + version: 'whatever', + namespace: 'header', + }, + rules: header.rules, + }, + }, + }, +]); + +const ignores = globalIgnores([ + '**/.docusaurus/**', + '**/__fixtures__/**', + '__mocks__', + 'dist', + 'node_modules', + '.yarn', + '.history', + 'build', + 'coverage', + 'examples/', + 'packages/lqip-loader/lib/*', + 'packages/docusaurus/lib/*', + 'packages/docusaurus-*/lib/*', + 'packages/eslint-plugin/lib/', + 'packages/stylelint-copyright/lib/', + 'packages/create-docusaurus/lib/*', + 'packages/create-docusaurus/templates/facebook', + 'website/i18n', + 'website/_dogfooding/_swizzle_theme_tests', + 'website/_dogfooding/_asset-tests/badSyntax.js', + 'packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy', +]); + +export default defineConfig(plugins, rules, ignores, { + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + ...globals.commonjs, + JSX: true, + }, + parserOptions: { + // projectService: true, + }, + }, + + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + react: { + version: '19', + }, + }, + + linterOptions: { + reportUnusedDisableDirectives: true, + }, +}); diff --git a/eslint.rules.ts b/eslint.rules.ts new file mode 100644 index 000000000000..900d65821331 --- /dev/null +++ b/eslint.rules.ts @@ -0,0 +1,532 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {defineConfig} from 'eslint/config'; +import eslintPlugin from 'eslint-plugin-eslint-plugin'; + +const OFF = 0; +const WARNING = 1; +const ERROR = 2; + +// Prevent importing lodash, usually for browser bundle size reasons +const LodashImportPatterns = ['lodash', 'lodash.**', 'lodash/**']; + +// Prevent importing content plugins, usually for coupling reasons +const ContentPluginsImportPatterns = [ + '@docusaurus/plugin-content-blog', + '@docusaurus/plugin-content-blog/**', + // TODO fix theme-common => docs dependency issue + // '@docusaurus/plugin-content-docs', + // '@docusaurus/plugin-content-docs/**', + '@docusaurus/plugin-content-pages', + '@docusaurus/plugin-content-pages/**', +]; + +export default defineConfig( + { + rules: { + 'array-callback-return': WARNING, + camelcase: WARNING, + 'class-methods-use-this': OFF, // It's a way of allowing private variables. + curly: [WARNING, 'all'], + 'global-require': OFF, // Deprecated, @typescript-eslint/no-require-import is enough + 'no-alert': WARNING, + 'lines-between-class-members': OFF, + 'max-classes-per-file': OFF, + 'max-len': [ + WARNING, + { + code: Infinity, // Code width is already enforced by Prettier/oxfmt + tabWidth: 2, + comments: 80, + ignoreUrls: true, + ignorePattern: '(eslint-disable|@)', + }, + ], + 'arrow-body-style': OFF, + 'no-await-in-loop': OFF, + 'no-case-declarations': WARNING, + 'no-console': OFF, + 'no-constant-binary-expression': ERROR, + 'no-continue': OFF, + 'no-control-regex': WARNING, + 'no-else-return': OFF, + 'no-empty': [WARNING, {allowEmptyCatch: true}], + 'no-lonely-if': WARNING, + 'no-nested-ternary': WARNING, + 'no-param-reassign': [WARNING, {props: false}], + 'no-prototype-builtins': WARNING, + 'no-restricted-exports': OFF, + 'no-restricted-properties': [ + ERROR, + .../** @type {[string, string][]} */ ([ + // TODO: TS doesn't make Boolean a narrowing function yet, + // so filter(Boolean) is problematic type-wise + // ['compact', 'Array#filter(Boolean)'], + ['concat', 'Array#concat'], + ['drop', 'Array#slice(n)'], + ['dropRight', 'Array#slice(0, -n)'], + ['fill', 'Array#fill'], + ['filter', 'Array#filter'], + ['find', 'Array#find'], + ['findIndex', 'Array#findIndex'], + ['first', 'foo[0]'], + ['flatten', 'Array#flat'], + ['flattenDeep', 'Array#flat(Infinity)'], + ['flatMap', 'Array#flatMap'], + ['fromPairs', 'Object.fromEntries'], + ['head', 'foo[0]'], + ['indexOf', 'Array#indexOf'], + ['initial', 'Array#slice(0, -1)'], + ['join', 'Array#join'], + // Unfortunately there's no great alternative to _.last yet + // Candidates: foo.slice(-1)[0]; foo[foo.length - 1] + // Array#at is ES2022; could replace _.nth as well + // ['last'], + ['map', 'Array#map'], + ['reduce', 'Array#reduce'], + ['reverse', 'Array#reverse'], + ['slice', 'Array#slice'], + ['take', 'Array#slice(0, n)'], + ['takeRight', 'Array#slice(-n)'], + ['tail', 'Array#slice(1)'], + ]).map(([property, alternative]) => ({ + object: '_', + property, + message: `Use ${alternative} instead.`, + })), + ...[ + 'readdirSync', + 'readFileSync', + 'statSync', + 'lstatSync', + 'existsSync', + 'pathExistsSync', + 'realpathSync', + 'mkdirSync', + 'mkdirpSync', + 'mkdirsSync', + 'writeFileSync', + 'writeJsonSync', + 'outputFileSync', + 'outputJsonSync', + 'moveSync', + 'copySync', + 'copyFileSync', + 'ensureFileSync', + 'ensureDirSync', + 'ensureLinkSync', + 'ensureSymlinkSync', + 'unlinkSync', + 'removeSync', + 'emptyDirSync', + ].map((property) => ({ + object: 'fs', + property, + message: 'Do not use sync fs methods.', + })), + ], + 'no-restricted-syntax': [ + WARNING, + // Copied from airbnb, removed for...of statement, added export all + { + selector: 'ForInStatement', + message: + 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + { + selector: 'LabeledStatement', + message: + 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: + '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + { + selector: 'ExportAllDeclaration', + message: + "Export all does't work well if imported in ESM due to how they are transpiled, and they can also lead to unexpected exposure of internal methods.", + }, + // TODO make an internal plugin to ensure this + // { + // selector: + // @ 'ExportDefaultDeclaration > Identifier, ExportNamedDeclaration[source=null] > ExportSpecifier', + // message: 'Export in one statement' + // }, + ...['path', 'fs-extra', 'webpack', 'lodash'].map((m) => ({ + selector: `ImportDeclaration[importKind=value]:has(Literal[value=${m}]) > ImportSpecifier[importKind=value]`, + message: + 'Default-import this, both for readability and interoperability with ESM', + })), + ], + 'no-template-curly-in-string': WARNING, + 'no-unused-expressions': OFF, + 'no-useless-escape': WARNING, + 'no-void': [ERROR, {allowAsStatement: true}], + 'prefer-destructuring': OFF, + 'prefer-named-capture-group': WARNING, + 'prefer-template': WARNING, + yoda: WARNING, + + /* + TODO fix + 'header/header': [ + ERROR, + 'block', + [ + '*', + ' * Copyright (c) Facebook, Inc. and its affiliates.', + ' *', + ' * This source code is licensed under the MIT license found in the', + ' * LICENSE file in the root directory of this source tree.', + ' ', + ], + ], + + */ + + 'import/extensions': OFF, + // This rule doesn't yet support resolving .js imports when the actual file + // is .ts. Plus it's not all that useful when our code is fully TS-covered. + 'import/no-unresolved': [ + OFF, + { + // Ignore certain webpack aliases because they can't be resolved + ignore: [ + '^@theme', + '^@docusaurus', + '^@generated', + '^@site', + '^@testing-utils', + ], + }, + ], + 'import/order': [ + WARNING, + { + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + 'type', + ], + pathGroups: [ + // always put css import to the last, ref: + // https://github.com/import-js/eslint-plugin-import/issues/1239 + { + pattern: '*.+(css|sass|less|scss|pcss|styl)', + group: 'unknown', + patternOptions: {matchBase: true}, + position: 'after', + }, + {pattern: 'vitest', group: 'builtin', position: 'before'}, + {pattern: 'react', group: 'builtin', position: 'before'}, + {pattern: 'react-dom', group: 'builtin', position: 'before'}, + {pattern: 'react-dom/**', group: 'builtin', position: 'before'}, + {pattern: 'stream', group: 'builtin', position: 'before'}, + {pattern: 'fs-extra', group: 'builtin'}, + {pattern: 'lodash', group: 'external', position: 'before'}, + {pattern: 'clsx', group: 'external', position: 'before'}, + // 'Bit weird to not use the `import/internal-regex` option, but this + // way, we can make `import type { Props } from "@theme/*"` appear + // before `import styles from "styles.module.css"`, which is what we + // always did. This should be removable once we stop using ambient + // module declarations for theme aliases. + {pattern: '@theme/**', group: 'internal'}, + {pattern: '@site/**', group: 'internal'}, + {pattern: '@theme-init/**', group: 'internal'}, + {pattern: '@theme-original/**', group: 'internal'}, + ], + pathGroupsExcludedImportTypes: [], + // example: let `import './nprogress.css';` after importing others + // in `packages/docusaurus-theme-classic/src/nprogress.ts` + // see more: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#warnonunassignedimports-truefalse + warnOnUnassignedImports: true, + }, + ], + 'import/prefer-default-export': OFF, + + 'vitest/consistent-test-it': WARNING, + 'vitest/expect-expect': OFF, + 'vitest/no-large-snapshots': [ + WARNING, + {maxSize: Infinity, inlineMaxSize: 50}, + ], + 'vitest/no-test-return-statement': ERROR, + 'vitest/prefer-expect-resolves': WARNING, + 'vitest/prefer-lowercase-title': [WARNING, {ignore: ['describe']}], + 'vitest/prefer-spy-on': WARNING, + 'vitest/prefer-to-be': OFF, + 'vitest/prefer-to-have-length': WARNING, + 'vitest/require-top-level-describe': ERROR, + 'vitest/valid-title': [ + ERROR, + { + mustNotMatch: { + it: [ + '^should|\\.$', + 'Titles should not begin with "should" or end with a full-stop', + ], + }, + }, + ], + + 'jsx-a11y/click-events-have-key-events': WARNING, + 'jsx-a11y/no-noninteractive-element-interactions': WARNING, + 'jsx-a11y/html-has-lang': OFF, + + // Sometimes we do need the props as a whole, e.g. when spreading + 'react/destructuring-assignment': OFF, + 'react/function-component-definition': [ + WARNING, + { + namedComponents: 'function-declaration', + unnamedComponents: 'arrow-function', + }, + ], + 'react/jsx-filename-extension': OFF, + 'react/jsx-key': [ERROR, {checkFragmentShorthand: true}], + 'react/jsx-no-useless-fragment': [ERROR, {allowExpressions: true}], + 'react/jsx-props-no-spreading': OFF, + 'react/no-array-index-key': OFF, // We build a static site, and nearly all components don't change. + 'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}], + 'react/prefer-stateless-function': WARNING, + 'react/prop-types': OFF, + 'react/require-default-props': [ + ERROR, + {ignoreFunctionalComponents: true}, + ], + 'react/jsx-uses-react': OFF, // JSX runtime: automatic + 'react/react-in-jsx-scope': OFF, // JSX runtime: automatic + 'react-hooks/set-state-in-effect': WARNING, // TODO re-enable later? + 'react-hooks/rules-of-hooks': ERROR, + 'react-hooks/exhaustive-deps': ERROR, + + '@typescript-eslint/no-empty-object-type': OFF, + '@typescript-eslint/prefer-optional-chain': OFF, + '@typescript-eslint/consistent-type-definitions': OFF, + '@typescript-eslint/require-await': OFF, + '@typescript-eslint/no-explicit-any': WARNING, + '@typescript-eslint/no-unused-expressions': [ + WARNING, + {allowTaggedTemplates: true, allowShortCircuit: true}, + ], + + '@typescript-eslint/ban-ts-comment': [ + ERROR, + {'ts-expect-error': 'allow-with-description'}, + ], + '@typescript-eslint/consistent-indexed-object-style': OFF, + '@typescript-eslint/consistent-type-imports': [ + WARNING, + {disallowTypeAnnotations: false}, + ], + '@typescript-eslint/explicit-module-boundary-types': WARNING, + '@typescript-eslint/method-signature-style': ERROR, + '@typescript-eslint/no-empty-function': OFF, + '@typescript-eslint/no-empty-interface': [ + ERROR, + { + allowSingleExtends: true, + }, + ], + '@typescript-eslint/no-inferrable-types': OFF, + '@typescript-eslint/no-namespace': [WARNING, {allowDeclarations: true}], + 'no-use-before-define': OFF, + '@typescript-eslint/no-use-before-define': [ + ERROR, + {functions: false, classes: false, variables: true}, + ], + '@typescript-eslint/no-non-null-assertion': OFF, + 'no-redeclare': OFF, + '@typescript-eslint/no-redeclare': ERROR, + 'no-shadow': OFF, + '@typescript-eslint/no-shadow': ERROR, + 'no-unused-vars': OFF, + // We don't provide any escape hatches for this rule. Rest siblings and + // function placeholder params are always ignored, and any other unused + // locals must be justified with a disable comment. + '@typescript-eslint/no-unused-vars': [ + ERROR, + { + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@docusaurus/no-html-links': ERROR, + '@docusaurus/prefer-docusaurus-heading': ERROR, + '@docusaurus/no-untranslated-text': [ + WARNING, + { + ignoredStrings: [ + '·', + '-', + '—', + '×', + '​', // zwj: ​ + '@', + 'WebContainers', + 'Twitter', + 'X', + 'GitHub', + 'Dev.to', + '1.x', + ], + }, + ], + }, + }, + + { + files: ['packages/docusaurus/src/client/**/*.{js,ts,tsx}'], + rules: { + 'no-restricted-imports': [ + ERROR, + { + patterns: [ + ...LodashImportPatterns, + ...ContentPluginsImportPatterns, + // Prevent importing server code in client bundle + '**/../babel/**', + '**/../server/**', + '**/../commands/**', + '**/../webpack/**', + ], + }, + ], + }, + }, + { + files: [ + 'packages/docusaurus-theme-common/src/**/*.{js,ts,tsx}', + 'packages/docusaurus-utils-common/src/**/*.{js,ts,tsx}', + ], + ignores: ['**/*.test.{js,ts,tsx}', '**/__tests__/**'], + rules: { + 'no-restricted-imports': [ + ERROR, + { + patterns: [...LodashImportPatterns, ...ContentPluginsImportPatterns], + }, + ], + }, + }, + { + files: ['packages/docusaurus-*/src/theme/**/*.{js,ts,tsx}'], + ignores: ['**/*.test.{js,ts,tsx}', '**/__tests__/**'], + rules: { + 'no-restricted-imports': [ + ERROR, + { + patterns: LodashImportPatterns.concat( + // Prevents relative imports between React theme components + [ + '../**', + './**', + // Allows relative styles module import with consistent filename + '!./styles.module.css', + ], + ), + }, + ], + }, + }, + { + files: [ + 'packages/docusaurus-*/src/theme/**/*.{js,ts,tsx}', + 'packages/docusaurus/src/client/theme-fallback/**/*.{js,ts,tsx}', + ], + rules: { + 'import/no-named-export': ERROR, + }, + }, + { + files: ['packages/create-docusaurus/templates/**/*.{js,ts,tsx}'], + rules: { + 'header/header': OFF, + 'global-require': OFF, + '@typescript-eslint/no-require-imports': WARNING, + '@typescript-eslint/no-var-requires': OFF, + '@docusaurus/no-untranslated-text': OFF, + }, + }, + { + files: ['*.d.ts'], + rules: { + 'import/no-duplicates': OFF, + }, + }, + { + files: ['*.{ts,tsx}'], + rules: { + 'no-undef': OFF, + 'import/no-import-module-exports': OFF, + }, + }, + { + files: ['*.{js,mjs,cjs}'], + rules: { + // Make JS code directly runnable in Node. + '@typescript-eslint/no-var-requires': OFF, + '@typescript-eslint/explicit-module-boundary-types': OFF, + }, + }, + { + files: [ + '**/__tests__/**', + 'packages/docusaurus-plugin-debug/**', + 'website/_dogfooding/**', + ], + rules: { + '@docusaurus/no-untranslated-text': OFF, + }, + }, + { + // Internal files where extraneous deps don't matter much at long as + // they run + files: [ + '*.test.{js,ts,tsx}', + '**/__tests__/**', + 'admin/**', + 'test/**', + 'website/**', + 'packages/docusaurus-theme-common/removeThemeInternalReexport.mjs', + 'packages/docusaurus-theme-translations/update.mjs', + 'packages/docusaurus-theme-translations/src/utils.ts', + ], + rules: { + 'import/no-extraneous-dependencies': OFF, + }, + }, + + // Website-specific rules + { + files: ['website/**'], + rules: { + '@typescript-eslint/no-require-imports': OFF, + }, + }, + + { + files: ['packages/eslint-plugin/**/*.{js,ts}'], + ...eslintPlugin.configs.recommended, + }, + + { + files: [ + 'packages/docusaurus-plugin-debug/**', + 'packages/docusaurus/src/**', + ], + rules: { + '@docusaurus/prefer-docusaurus-heading': OFF, + }, + }, +); diff --git a/package.json b/package.json index b6a2458bcc8f..d01524cc45f3 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "devDependencies": { "@ai-sdk/react": "^2.0.30", "@crowdin/cli": "^4.14.2", + "@eslint/js": "^10.0.1", "@swc/core": "^1.15.32", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", @@ -90,34 +91,35 @@ "@types/react": "^19.2.14", "@types/semver": "^7.7.1", "@types/shelljs": "^0.8.12", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^5.0.0", + "@vitest/eslint-plugin": "^1.6.17", "cross-env": "^10.1.0", "cspell": "^8.18.1", - "eslint": "^8.45.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.8.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-eslint-plugin": "^7.3.3", "eslint-plugin-header": "^3.1.1", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-compiler": "^19.0.0-beta-40c6c23-20250301", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-regexp": "^1.15.0", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-regexp": "^3.1.0", + "globals": "^17.6.0", "husky": "^9.1.7", "image-size": "^2.0.2", "jest-serializer-ansi-escapes": "^5.0.0", "jest-serializer-react-helmet-async": "^1.0.21", + "jiti": "^2.7.0", "jsdom": "^25.0.1", "lerna": "^7.4.2", "lerna-changelog": "^2.2.0", "lint-staged": "^17.0.2", "lockfile-lint": "^5.0.0", "npm-run-all": "^4.1.5", + "oxfmt": "^0.47.0", "patch-package": "^8.0.1", "pkg-pr-new": "^0.0.68", "postinstall-postinstall": "^2.1.0", - "oxfmt": "^0.47.0", "react": "^19.2.5", "react-dom": "^19.2.5", "rimraf": "^3.0.2", @@ -128,9 +130,8 @@ "stylelint-config-standard": "^29.0.0", "syncpack": "^14.3.1", "typescript": "~6.0.3", - "vitest": "^4.0.0", - "@vitejs/plugin-react": "^5.0.0", - "@vitest/eslint-plugin": "^1.4.0" + "typescript-eslint": "^8.59.3", + "vitest": "^4.0.0" }, "resolutions": { "**/pretty-format/react-is": "^19.2.0", diff --git a/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts b/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts index 7448dc586ce7..030adcb9e261 100644 --- a/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts +++ b/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts @@ -45,13 +45,13 @@ const default => { `, }); - const errorMock = vi.spyOn(console, 'error').mockImplementation(() => {}); + using error = vi.spyOn(console, 'error'); await expect( extractSourceCodeFileTranslations(sourceCodeFilePath, TestBabelOptions), ).rejects.toThrow(); - expect(errorMock).toHaveBeenCalledWith( + expect(error).toHaveBeenCalledWith( expect.stringMatching( /Error while attempting to extract Docusaurus translations from source code file at/, ), diff --git a/packages/docusaurus-babel/src/preset.ts b/packages/docusaurus-babel/src/preset.ts index cbbeb2207423..6a8b6a539373 100644 --- a/packages/docusaurus-babel/src/preset.ts +++ b/packages/docusaurus-babel/src/preset.ts @@ -57,7 +57,7 @@ function getTransformOptions(isServer: boolean): TransformOptions { // By default, it assumes @babel/runtime@7.0.0. Since we use >7.0.0, // better to explicitly specify the version so that it can reuse the // helper better. See https://github.com/babel/babel/issues/10261 - // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require + // eslint-disable-next-line @typescript-eslint/no-require-imports version: (require('@babel/runtime/package.json') as {version: string}) .version, regenerator: true, diff --git a/packages/docusaurus-bundler/src/minifyHtml.ts b/packages/docusaurus-bundler/src/minifyHtml.ts index 8c564986f8aa..226fb08f2ba2 100644 --- a/packages/docusaurus-bundler/src/minifyHtml.ts +++ b/packages/docusaurus-bundler/src/minifyHtml.ts @@ -62,7 +62,7 @@ async function getTerserMinifier(): Promise { return {code, warnings: []}; } catch (err) { throw new Error(`HTML minification failed (Terser)`, { - cause: err as Error, + cause: err, }); } }, @@ -116,7 +116,7 @@ async function getSwcMinifier(): Promise { }; } catch (err) { throw new Error(`HTML minification failed (SWC)`, { - cause: err as Error, + cause: err, }); } }, diff --git a/packages/docusaurus-faster/src/index.ts b/packages/docusaurus-faster/src/index.ts index 27cc387a7ad4..dd3832d29e50 100644 --- a/packages/docusaurus-faster/src/index.ts +++ b/packages/docusaurus-faster/src/index.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import Rspack from '@rspack/core'; +import {rspack as Rspack} from '@rspack/core'; import * as lightningcss from 'lightningcss'; import browserslist from 'browserslist'; import {minify as swcHtmlMinifier} from '@swc/html'; diff --git a/packages/docusaurus-logger/src/__tests__/index.test.ts b/packages/docusaurus-logger/src/__tests__/index.test.ts index 8bb9ec157c82..f1172db4cc83 100644 --- a/packages/docusaurus-logger/src/__tests__/index.test.ts +++ b/packages/docusaurus-logger/src/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {beforeAll, describe, expect, it, vi} from 'vitest'; +import {describe, expect, it, vi} from 'vitest'; import logger from '../index'; // Force chalk to ANSI level 3 in tests, so output is colored even in CI @@ -94,54 +94,53 @@ describe('interpolate', () => { }); describe('info', () => { - const consoleMock = vi.spyOn(console, 'info').mockImplementation(() => {}); it('prints objects', () => { + using info = vi.spyOn(console, 'info'); logger.info({a: 1}); logger.info(undefined); logger.info([1, 2, 3]); logger.info(new Date(2021, 10, 13)); - expect(consoleMock.mock.calls).toMatchSnapshot(); + expect(info.mock.calls).toMatchSnapshot(); }); }); describe('warn', () => { - const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); it('prints objects', () => { + using warn = vi.spyOn(console, 'warn'); logger.warn({a: 1}); logger.warn(undefined); logger.warn([1, 2, 3]); logger.warn(new Date(2021, 10, 13)); - expect(consoleMock.mock.calls).toMatchSnapshot(); + expect(warn.mock.calls).toMatchSnapshot(); }); }); describe('error', () => { - const consoleMock = vi.spyOn(console, 'error').mockImplementation(() => {}); it('prints objects', () => { + using error = vi.spyOn(console, 'error'); logger.error({a: 1}); logger.error(undefined); logger.error([1, 2, 3]); logger.error(new Date(2021, 10, 13)); - expect(consoleMock.mock.calls).toMatchSnapshot(); + expect(error.mock.calls).toMatchSnapshot(); }); }); describe('success', () => { - const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => {}); it('prints objects', () => { + using log = vi.spyOn(console, 'log'); logger.success({a: 1}); logger.success(undefined); logger.success([1, 2, 3]); logger.success(new Date(2021, 10, 13)); - expect(consoleMock.mock.calls).toMatchSnapshot(); + expect(log.mock.calls).toMatchSnapshot(); }); }); describe('report', () => { - beforeAll(() => vi.clearAllMocks()); it('works with all severities', () => { - const consoleLog = vi.spyOn(console, 'info').mockImplementation(() => {}); - const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + using log = vi.spyOn(console, 'info'); + using warn = vi.spyOn(console, 'warn'); logger.report('ignore')('hey'); logger.report('log')('hey'); logger.report('warn')('hey'); @@ -154,12 +153,10 @@ describe('report', () => { ).toThrowErrorMatchingInlineSnapshot( `[Error: Unexpected "reportingSeverity" value: foo.]`, ); - expect(consoleLog).toHaveBeenCalledTimes(1); - expect(consoleLog).toHaveBeenCalledWith( - expect.stringMatching(/.*\[INFO\].* hey/), - ); - expect(consoleWarn).toHaveBeenCalledTimes(1); - expect(consoleWarn).toHaveBeenCalledWith( + expect(log).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith(expect.stringMatching(/.*\[INFO\].* hey/)); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith( expect.stringMatching(/.*\[WARNING\].* hey/), ); }); diff --git a/packages/docusaurus-mdx-loader/src/__tests__/frontMatter.test.ts b/packages/docusaurus-mdx-loader/src/__tests__/frontMatter.test.ts index 6b93b43f4a2b..5ecfe820df96 100644 --- a/packages/docusaurus-mdx-loader/src/__tests__/frontMatter.test.ts +++ b/packages/docusaurus-mdx-loader/src/__tests__/frontMatter.test.ts @@ -25,14 +25,14 @@ function testField(params: { ErrorMessage: string, ][]; }) { - // eslint-disable-next-line @vitest/require-top-level-describe + // eslint-disable-next-line vitest/require-top-level-describe test(`[${params.prefix}] accept valid values`, () => { params.validFrontMatters.forEach((frontMatter) => { expect(validateMDXFrontMatter(frontMatter)).toEqual(frontMatter); }); }); - // eslint-disable-next-line @vitest/require-top-level-describe + // eslint-disable-next-line vitest/require-top-level-describe test(`[${params.prefix}] convert valid values`, () => { params.convertibleFrontMatter?.forEach( ([convertibleFrontMatter, convertedFrontMatter]) => { @@ -43,7 +43,7 @@ function testField(params: { ); }); - // eslint-disable-next-line @vitest/require-top-level-describe + // eslint-disable-next-line vitest/require-top-level-describe test(`[${params.prefix}] throw error for values`, () => { params.invalidFrontMatters?.forEach(([frontMatter, message]) => { try { @@ -56,7 +56,7 @@ function testField(params: { )}`, ); } catch (err) { - // eslint-disable-next-line @vitest/no-conditional-expect + // eslint-disable-next-line vitest/no-conditional-expect expect((err as Error).message).toMatch( new RegExp(escapeStringRegexp(message)), ); diff --git a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts index 3a39816c2872..4ce74dc0b0a2 100644 --- a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {describe, expect, it, vi} from 'vitest'; import * as path from 'path'; import plugin from '..'; import type {PluginOptions} from '../index'; @@ -184,11 +184,6 @@ this is a code block }); describe('onBrokenMarkdownLinks', () => { - const warnMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); - beforeEach(() => { - warnMock.mockClear(); - }); - async function processResolutionErrors( content: string, onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'] = 'throw', @@ -227,6 +222,8 @@ this is a code block describe('warns', () => { it('for unresolvable md and mdx link', async () => { + using warn = vi.spyOn(console, 'warn'); + /* language=markdown */ const content = ` [link1](link1.mdx) @@ -251,8 +248,8 @@ this is a code block " `); - expect(warnMock).toHaveBeenCalledTimes(2); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(2); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown link with URL \`link1.mdx\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (2:1) couldn't be resolved. @@ -267,6 +264,8 @@ this is a code block }); it('for unresolvable md and mdx link - with recovery', async () => { + using warn = vi.spyOn(console, 'warn'); + /* language=markdown */ const content = ` [link1](link1.mdx) @@ -298,8 +297,8 @@ this is a code block " `); - expect(warnMock).toHaveBeenCalledTimes(2); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(2); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownLinks called with", diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts index f9821921782d..4602e27c76d9 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {describe, expect, it, vi} from 'vitest'; import * as path from 'path'; import vfile from 'to-vfile'; import plugin, {type PluginOptions} from '../index'; @@ -68,10 +68,10 @@ describe('transformImage plugin', () => { }); it('does not choke on invalid image', async () => { - const errorMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); + using warn = vi.spyOn(console, 'warn'); const result = await processContent(`![invalid image](/invalid.png)`); expect(result).toMatchSnapshot(); - expect(errorMock).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledTimes(1); }); describe('onBrokenMarkdownImages', () => { @@ -121,19 +121,15 @@ describe('transformImage plugin', () => { return processContent(content, {onBrokenMarkdownImages: 'warn'}); } - const warnMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); - beforeEach(() => { - warnMock.mockClear(); - }); - it('if image absolute path does not exist', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.doesNotExistAbsolute); expect(result).toMatchInlineSnapshot(` "![img](/img/doesNotExist.png) " `); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown image with URL \`/img/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.", @@ -143,13 +139,14 @@ describe('transformImage plugin', () => { }); it('if image relative path does not exist', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.doesNotExistRelative); expect(result).toMatchInlineSnapshot(` "![img](./doesNotExist.png) " `); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown image with URL \`./doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.", @@ -159,13 +156,14 @@ describe('transformImage plugin', () => { }); it('if image @site path does not exist', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.doesNotExistSiteAlias); expect(result).toMatchInlineSnapshot(` "![img](@site/doesNotExist.png) " `); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown image with URL \`@site/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.", @@ -175,13 +173,14 @@ describe('transformImage plugin', () => { }); it('if image url empty', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.urlEmpty); expect(result).toMatchInlineSnapshot(` "![img]() " `); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown image with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1).", @@ -205,19 +204,16 @@ describe('transformImage plugin', () => { }); } - const logMock = vi.spyOn(console, 'log').mockImplementation(() => {}); - beforeEach(() => { - logMock.mockClear(); - }); - it('if image absolute path does not exist', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.doesNotExistAbsolute); expect(result).toMatchInlineSnapshot(` "![new 404 alt](/404.png) " `); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownImages called for ", @@ -249,13 +245,15 @@ describe('transformImage plugin', () => { }); it('if image relative path does not exist', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.doesNotExistRelative); expect(result).toMatchInlineSnapshot(` "![new 404 alt](/404.png) " `); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownImages called for ", @@ -287,13 +285,15 @@ describe('transformImage plugin', () => { }); it('if image @site path does not exist', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.doesNotExistSiteAlias); expect(result).toMatchInlineSnapshot(` "![new 404 alt](/404.png) " `); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownImages called for ", @@ -325,13 +325,15 @@ describe('transformImage plugin', () => { }); it('if image url empty', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.urlEmpty); expect(result).toMatchInlineSnapshot(` "![new 404 alt](/404.png) " `); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownImages called for ", diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/index.test.ts index 6be1e6fe515b..29bcad267a10 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {describe, expect, it, vi} from 'vitest'; import * as path from 'path'; import vfile from 'to-vfile'; import plugin, {type PluginOptions} from '..'; @@ -110,16 +110,12 @@ describe('transformLinks plugin', () => { return processContent(content, {onBrokenMarkdownLinks: 'warn'}); } - const warnMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); - beforeEach(() => { - warnMock.mockClear(); - }); - it('if url is empty', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.urlEmpty); expect(result).toMatchInlineSnapshot(`"[empty]()"`); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown link with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1).", @@ -129,10 +125,11 @@ describe('transformLinks plugin', () => { }); it('if file with site alias does not exist', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processWarn(fixtures.fileDoesNotExistSiteAlias); expect(result).toMatchInlineSnapshot(`"[file](@site/file.zip)"`); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock.mock.calls).toMatchInlineSnapshot(` + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchInlineSnapshot(` [ [ "[WARNING] Markdown link with URL \`@site/file.zip\` in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved. @@ -157,18 +154,15 @@ describe('transformLinks plugin', () => { }); } - const logMock = vi.spyOn(console, 'log').mockImplementation(() => {}); - beforeEach(() => { - logMock.mockClear(); - }); - it('if url is empty', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.urlEmpty); expect(result).toMatchInlineSnapshot( `"[empty](/404 "fixed link title")"`, ); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownLinks called with", @@ -217,12 +211,14 @@ describe('transformLinks plugin', () => { }); it('if file with site alias does not exist', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processWarn(fixtures.fileDoesNotExistSiteAlias); expect(result).toMatchInlineSnapshot( `"[file](/404 "fixed link title")"`, ); - expect(logMock).toHaveBeenCalledTimes(1); - expect(logMock.mock.calls).toMatchInlineSnapshot(` + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "onBrokenMarkdownLinks called with", diff --git a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts index 67cdb3e79efb..e6eb8a8e3c43 100644 --- a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {describe, expect, it, vi} from 'vitest'; import path from 'path'; import remark2rehype from 'remark-rehype'; import stringify from 'rehype-stringify'; @@ -37,55 +37,55 @@ const processFixture = async ( }; describe('directives remark plugin - client compiler', () => { - const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); - beforeEach(() => vi.clearAllMocks()); - const options = {compilerName: 'client'} as const; it('default behavior for container directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('containerDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(1); - expect(consoleMock.mock.calls).toMatchSnapshot('console'); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchSnapshot('console'); }); it('default behavior for leaf directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('leafDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(1); - expect(consoleMock.mock.calls).toMatchSnapshot('console'); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchSnapshot('console'); }); it('default behavior for text directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('textDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(1); - expect(consoleMock.mock.calls).toMatchSnapshot('console'); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls).toMatchSnapshot('console'); }); }); describe('directives remark plugin - server compiler', () => { - const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); - beforeEach(() => vi.clearAllMocks()); - const options = {compilerName: 'server'} as const; it('default behavior for container directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('containerDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); }); it('default behavior for leaf directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('leafDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); }); it('default behavior for text directives', async () => { + using warn = vi.spyOn(console, 'warn'); const result = await processFixture('textDirectives', options); expect(result).toMatchSnapshot('result'); - expect(consoleMock).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/docusaurus-mdx-loader/src/utils.ts b/packages/docusaurus-mdx-loader/src/utils.ts index 30ed0506fe4c..17a8ae9ffe1f 100644 --- a/packages/docusaurus-mdx-loader/src/utils.ts +++ b/packages/docusaurus-mdx-loader/src/utils.ts @@ -128,7 +128,7 @@ export async function compileToJSX({ error.message }\nDetails:\n${errorDetails}`, // TODO error cause doesn't seem to be used by Webpack stats.errors :s - {cause: error}, + {cause: errorUnknown}, ); } } diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 5876e364848d..4ab1e21a54e4 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -260,7 +260,6 @@ declare module '@docusaurus/Translate' { } declare module '@docusaurus/router' { - // eslint-disable-next-line import/no-extraneous-dependencies export {useHistory, useLocation, Redirect, matchPath} from 'react-router-dom'; } @@ -352,7 +351,6 @@ declare module '@docusaurus/Noop' { } declare module '@docusaurus/renderRoutes' { - // eslint-disable-next-line import/no-extraneous-dependencies import {renderRoutes} from 'react-router-config'; export default renderRoutes; diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 697a04eaace0..eca89823a2e7 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -101,7 +101,7 @@ function validateCollectedRedirects( if (to.startsWith('/')) { try { return decodeURI(new URL(to, 'https://example.com').pathname); - } catch (e) {} + } catch {} } return undefined; }) diff --git a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts index 3cdf1b7f6e14..5cd0f935804c 100644 --- a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts +++ b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts @@ -28,7 +28,7 @@ function searchAnchorForwarding(toUrl: string): boolean { const url = new URL(toUrl, 'https://example.com'); const containsSearchOrAnchor = url.search || url.hash; return !containsSearchOrAnchor; - } catch (e) { + } catch { return false; } } diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml index 88f35a40be6e..f2b1c9b5a61e 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml @@ -1,24 +1,24 @@ - + - https://docusaurus.io/blog/ - Docusaurus blog website fixture Blog + https://docusaurus.io/myBaseUrl/blog + Hello Blog 2023-07-23T00:00:00.000Z https://github.com/jpmonette/feed - - Docusaurus blog website fixture Blog - https://docusaurus.io/img/docusaurus.ico + + Hello Blog + https://docusaurus.io/myBaseUrl/image/favicon.ico Copyright <![CDATA[test links]]> - https://docusaurus.io/blog/blog-with-links - + https://docusaurus.io/myBaseUrl/blog/blog-with-links + 2023-07-23T00:00:00.000Z absolute full url

absolute pathname

relative pathname

md link

-

anchor

+

anchor

relative pathname + anchor

@@ -27,8 +27,8 @@
<![CDATA[MDX Blog Sample with require calls]]> - https://docusaurus.io/blog/mdx-require-blog-post - + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + 2021-03-06T00:00:00.000Z Test MDX with require calls

@@ -40,8 +40,8 @@
<![CDATA[Full Blog Sample]]> - https://docusaurus.io/blog/mdx-blog-post - + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + 2021-03-05T00:00:00.000Z HTML Heading 1 @@ -51,10 +51,10 @@

Import DOM

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 5
+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5