diff --git a/AGENTS.md b/AGENTS.md index 7a57746576d..6196bf5aed9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -254,6 +254,25 @@ Public-facing apps (`comments-ui`, `signup-form`, `sodo-search`, `portal`, `anno ### Commit Messages When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`. +### ESLint Config +Source of truth: [eslint.shared.mjs](eslint.shared.mjs) at the repo root. Two factories cover most workspaces — `reactAppConfig` (every `apps/*` workspace) and `nodeLibConfig` (Node libs in `ghost/`). Each factory has full JSDoc with `@example`s; hover the call site in your editor. + +Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`): + +```js +import {reactAppConfig} from '../../eslint.shared.mjs'; +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + shadeRestricted: true +}); +``` + +Conventions: +- **Rules are `'error'` or `'off'` — never `'warn'`.** Warnings get ignored and pollute output. Applies to every workspace covered by the factories above + the standalones; `e2e/` has its own setup (see [e2e/CLAUDE.md](e2e/CLAUDE.md)) and currently still uses warn-level Playwright rules — a separate cleanup. +- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped. +- **Standalone configs** (`ghost/core`, `ghost/admin`, `apps/admin`, `apps/admin-toolbar`) exist because their rule sets genuinely don't fit a factory — read the file directly. They import shared atoms (`correctnessRules`, `nodeLibRules`, `localFilenamesPlugin`, `strictLinterOptions`) where applicable. +- **Plugin deps**: workspaces that use Tailwind must list `tailwindcss` as a (dev)Dependency themselves; other eslint plugins are root devDeps because the factory imports them dynamically. + ### When Working on Admin UI - **New features:** Build in React (`apps/admin-x-*` or `apps/posts`) - **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.) diff --git a/apps/activitypub/eslint.config.js b/apps/activitypub/eslint.config.js index 25ee44be864..a16bd303581 100644 --- a/apps/activitypub/eslint.config.js +++ b/apps/activitypub/eslint.config.js @@ -1,98 +1,7 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - shadeLayeredImportsRule, - sortImportsRule, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...sortImportsRule, - ...shadeLayeredImportsRule, - ...tailwindRulesV4, - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - 'no-shadow': 'off', - '@typescript-eslint/no-shadow': 'error', - 'react-refresh/only-export-components': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off' - } - }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - 'no-undef': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + shadeRestricted: true, + sortImports: true +}); diff --git a/apps/activitypub/package.json b/apps/activitypub/package.json index 969dafcfb7a..b37856a1f53 100644 --- a/apps/activitypub/package.json +++ b/apps/activitypub/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/activitypub", "type": "module", - "version": "3.1.46", + "version": "3.1.51", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-toolbar/eslint.config.js b/apps/admin-toolbar/eslint.config.js index 28591ab5095..a54593dbe90 100644 --- a/apps/admin-toolbar/eslint.config.js +++ b/apps/admin-toolbar/eslint.config.js @@ -2,10 +2,21 @@ import js from '@eslint/js'; import globals from 'globals'; import ghostPlugin from 'eslint-plugin-ghost'; +import {correctnessRules, strictLinterOptions} from '../../eslint.shared.mjs'; + +// Standalone (not factory-based) because admin-toolbar is vanilla JS + jQuery, +// no React. Adding it to reactAppConfig with `typescript: false, reactRefresh: +// false` would still load eslint-plugin-react unnecessarily. The base rules +// come from the shared correctnessRules atom. + export default [ { ignores: ['umd/**/*.js'] }, + { + files: ['**/*'], + ...strictLinterOptions + }, { files: ['src/**/*.js'], ...js.configs.recommended, @@ -21,19 +32,7 @@ export default [ ghost: ghostPlugin }, rules: { - curly: 'error', - camelcase: ['error', {properties: 'never'}], - 'dot-notation': 'error', - eqeqeq: ['error', 'always'], - 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], - 'no-eval': 'error', - 'no-useless-call': 'error', - 'no-console': 'error', - 'no-shadow': 'error', - 'array-callback-return': 'error', - 'no-constructor-return': 'error', - 'no-promise-executor-return': 'error', - 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + ...correctnessRules } }, { @@ -53,19 +52,7 @@ export default [ ghost: ghostPlugin }, rules: { - curly: 'error', - camelcase: ['error', {properties: 'never'}], - 'dot-notation': 'error', - eqeqeq: ['error', 'always'], - 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], - 'no-eval': 'error', - 'no-useless-call': 'error', - 'no-console': 'error', - 'no-shadow': 'error', - 'array-callback-return': 'error', - 'no-constructor-return': 'error', - 'no-promise-executor-return': 'error', - 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + ...correctnessRules } } ]; diff --git a/apps/admin-toolbar/package.json b/apps/admin-toolbar/package.json index 16922c6b576..8991eaeb9d7 100644 --- a/apps/admin-toolbar/package.json +++ b/apps/admin-toolbar/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/admin-toolbar", "type": "module", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/admin-x-design-system/eslint.config.js b/apps/admin-x-design-system/eslint.config.js index 231cb97b2ce..581e2f8b3f2 100644 --- a/apps/admin-x-design-system/eslint.config.js +++ b/apps/admin-x-design-system/eslint.config.js @@ -1,96 +1,7 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*', 'storybook-static/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...tailwindRulesV4, - // TS handles these — disable the base ESLint variants - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - }, - // Storybook story files — render() functions intentionally use hooks - { - files: ['**/*.stories.{ts,tsx,js,jsx}'], - rules: { - 'react-hooks/rules-of-hooks': 'off' - } - }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + ignores: ['dist/**/*', 'storybook-static/**/*'], + storybook: 'storiesBlock' +}); diff --git a/apps/admin-x-framework/eslint.config.js b/apps/admin-x-framework/eslint.config.js index 31cd8c5b14c..fd8849c25a3 100644 --- a/apps/admin-x-framework/eslint.config.js +++ b/apps/admin-x-framework/eslint.config.js @@ -1,85 +1,10 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - shadeLayeredImportsRule, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...shadeLayeredImportsRule, - // TS handles these — disable the base ESLint variants - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-explicit-any': 'off' - } +export default await reactAppConfig({ + shadeRestricted: true, + extraTestRules: { + // TODO: 71 legacy violations in test/ — mostly mock-fixture typing + // shortcuts. Cleanup PR will type them properly and flip back. + '@typescript-eslint/no-explicit-any': 'off' } -); +}); diff --git a/apps/admin-x-settings/eslint.config.js b/apps/admin-x-settings/eslint.config.js index 6e4a8a49cb2..c788dc33d22 100644 --- a/apps/admin-x-settings/eslint.config.js +++ b/apps/admin-x-settings/eslint.config.js @@ -1,107 +1,19 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - shadeLayeredImportsRule, - sortImportsRule, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...sortImportsRule, - ...shadeLayeredImportsRule, - ...tailwindRulesV4, - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - 'prefer-const': 'off', - 'react-refresh/only-export-components': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off' - } - }, - // Final overrides for src files. Kept in a separate block to guarantee - // these win over typescript-eslint's recommended rules. - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - rules: { - '@typescript-eslint/no-explicit-any': 'warn' - } +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + shadeRestricted: true, + sortImports: true, + extraSrcRules: { + // TODO: 43 legacy violations. Remove this override after the cleanup PR + // converts all `let` → `const` where reassignment never happens. + 'prefer-const': 'off', + // TODO: 2 legacy violations. Easy to fix — drop this override and type + // the two remaining `any` usages (in design-and-branding + utils). + '@typescript-eslint/no-explicit-any': 'off' }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - 'no-undef': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-explicit-any': 'off' - } + extraTestRules: { + // TODO: 7 legacy violations in test/ — mock-fixture typing shortcuts. + '@typescript-eslint/no-explicit-any': 'off' } -); +}); diff --git a/apps/admin/eslint.config.js b/apps/admin/eslint.config.js index a25e18f953b..63d1654a858 100644 --- a/apps/admin/eslint.config.js +++ b/apps/admin/eslint.config.js @@ -8,6 +8,8 @@ import { globalIgnores } from 'eslint/config' import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths' import ghostPlugin from 'eslint-plugin-ghost'; +import {shadeLayeredImportsRule, strictLinterOptions} from '../../eslint.shared.mjs'; + const noHardcodedGhostPaths = { meta: { type: 'problem', @@ -43,8 +45,13 @@ const localPlugin = { }; const tailwindCssConfig = `${import.meta.dirname}/src/index.css`; +// TODO: this workspace doesn't yet apply `correctnessRules` from the shared +// module. Doing so would surface 14 violations (4 no-console, 5 curly, 2 +// no-promise-executor-return, etc.) that need source cleanup. Follow-up PR +// will fix the violations + add the spread. export default tseslint.config([ globalIgnores(['dist']), + {files: ['**/*'], ...strictLinterOptions}, { files: ['**/*.{ts,tsx}'], extends: [ @@ -74,14 +81,13 @@ export default tseslint.config([ }, rules: { 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], - 'no-restricted-imports': ['error', { - paths: [{ - name: '@tryghost/shade', - message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).', - }], - }], + ...shadeLayeredImportsRule, 'tailwindcss/classnames-order': 'error', 'tailwindcss/no-contradicting-classname': 'error', + // TODO: leaked warn from reactHooks.configs['recommended-latest']. The + // shared factory drops this to 'off' across the rest of the React apps; + // this workspace isn't on the factory yet, so override explicitly. + 'react-hooks/exhaustive-deps': 'off', }, }, // Apply no-relative-import-paths rule for src files (auto-fix supported) diff --git a/apps/announcement-bar/eslint.config.js b/apps/announcement-bar/eslint.config.js index cf166428870..9bd14c495c4 100644 --- a/apps/announcement-bar/eslint.config.js +++ b/apps/announcement-bar/eslint.config.js @@ -1,55 +1,9 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import {correctnessRules} from '../../eslint.shared.mjs'; - -const baseConfig = { - ...js.configs.recommended, - ...reactPlugin.configs.flat.recommended, - plugins: { - ...reactPlugin.configs.flat.recommended.plugins, - ghost: ghostPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactPlugin.configs.flat.recommended.rules, - ...correctnessRules, - 'react/prop-types': 'off' - } -}; - -export default [ - { - ignores: ['umd/**/*', 'dist/**/*'] - }, - { - ...baseConfig, - files: ['src/**/*.{js,jsx}'], - languageOptions: { - ...reactPlugin.configs.flat.recommended.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: globals.browser - } - }, - { - ...baseConfig, - files: ['test/**/*.{js,jsx}'], - languageOptions: { - ...reactPlugin.configs.flat.recommended.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.vitest, - ...globals.jest, - vi: 'readonly' - } - } - } -]; +// LEGACY: vanilla JS (no TypeScript). Should migrate to TS — small surface +// (~6 components, 1–2 days). +export default await reactAppConfig({ + typescript: false, + reactRefresh: false, // bundled as UMD for theme distribution + ignores: ['umd/**/*', 'dist/**/*'] +}); diff --git a/apps/announcement-bar/package.json b/apps/announcement-bar/package.json index 3e4bef5f6e4..0ffee76f5ae 100644 --- a/apps/announcement-bar/package.json +++ b/apps/announcement-bar/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/announcement-bar", "type": "module", - "version": "1.1.23", + "version": "1.1.24", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/comments-ui/eslint.config.js b/apps/comments-ui/eslint.config.js index 70db0f147f6..3b64592d0fb 100644 --- a/apps/comments-ui/eslint.config.js +++ b/apps/comments-ui/eslint.config.js @@ -1,69 +1,30 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import i18nextPlugin from 'eslint-plugin-i18next'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - reactDefaultsOff, - sortImportsRule, - tailwindRulesWithConfig, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindConfig = `${import.meta.dirname}/tailwind.config.js`; - -const reactFlat = reactPlugin.configs.flat.recommended; -const i18nextFlat = i18nextPlugin.configs['flat/recommended']; - -export default tseslint.config( - { - ignores: ['umd/**/*', 'dist/**/*'] - }, - { - files: ['src/**/*.{js,jsx,ts,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ...i18nextFlat.plugins, - ghost: ghostPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...i18nextFlat.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...sortImportsRule, - ...reactDefaultsOff, - 'react/jsx-sort-props': ['error', { - reservedFirst: true, - callbacksLast: true, - shorthandLast: true, - locale: 'en' - }], - 'react/button-has-type': 'error', - 'react/no-array-index-key': 'error', - ...tailwindRulesWithConfig(tailwindConfig), - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - 'no-undef': 'off' - } +export default await reactAppConfig({ + // UMD bundle (no Vite HMR runtime), so the react-refresh rule is meaningless. + reactRefresh: false, + // LEGACY: Tailwind v3. Migration to v4 is a multi-day class/theme rewrite + + // CDN regression testing. Tracked separately; this override stays until + // the migration lands. + legacyTailwindV3ConfigPath: `${import.meta.dirname}/tailwind.config.js`, + i18next: true, + sortImports: true, + ignores: ['umd/**/*', 'dist/**/*'], + // Matches main's behavior: package.json lints `src` only — test/ is not + // linted in CI. Keep test/ out of srcGlobs so test-only relaxations + // (Playwright fixture destructure pattern, `let` in HSL helper) don't + // bleed into src. + testGlobs: false, + extraSrcRules: { + // TODO: 41 legacy `any` violations. Remove this override after typing + // them properly (mostly external API response shapes — needs careful + // typing, not a 1-line fix per). + '@typescript-eslint/no-explicit-any': 'off', + // TODO: 22 legacy `exhaustive-deps` violations from newly-loading the + // react-hooks plugin in this workspace (it was previously silent because + // the plugin wasn't registered). Each fix is a per-call-site judgment + // (add dep / wrap in useCallback / suppress with reason). Remove this + // override after the cleanup PR. + 'react-hooks/exhaustive-deps': 'off' } -); +}); diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json index e6a778e7425..6e4978cee44 100644 --- a/apps/comments-ui/package.json +++ b/apps/comments-ui/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/comments-ui", "type": "module", - "version": "1.5.14", + "version": "1.5.15", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/portal/eslint.config.js b/apps/portal/eslint.config.js index b3e87ff2927..562a37b98cc 100644 --- a/apps/portal/eslint.config.js +++ b/apps/portal/eslint.config.js @@ -1,89 +1,14 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import i18nextPlugin from 'eslint-plugin-i18next'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - jsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const i18nextFlat = i18nextPlugin.configs['flat/recommended']; -const reactFlat = reactPlugin.configs.flat.recommended; -const reactJsxRuntime = reactPlugin.configs.flat['jsx-runtime']; - -export default tseslint.config( - { - ignores: ['umd/**/*', 'dist/**/*'] - }, - { - files: ['src/**/*.{js,jsx}', 'test/**/*.{js,jsx}'], - ...js.configs.recommended, - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.vitest, - ...globals.jest, - vi: 'readonly', - require: 'readonly' - } - }, - plugins: { - ...reactFlat.plugins, - ...i18nextFlat.plugins, - ghost: ghostPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactJsxRuntime.rules, - ...i18nextFlat.rules, - ...correctnessRules, - ...jsUnusedVarsRule, - 'react/prop-types': 'off' - } - }, - { - files: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - parserOptions: { - ecmaFeatures: {jsx: true}, - project: './tsconfig.json', - tsconfigRootDir: import.meta.dirname - }, - globals: { - ...globals.browser, - ...globals.vitest, - ...globals.jest, - vi: 'readonly' - } - }, - plugins: { - ...reactFlat.plugins, - ...i18nextFlat.plugins, - ghost: ghostPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...reactFlat.rules, - ...reactJsxRuntime.rules, - ...i18nextFlat.rules, - ...correctnessRules, - ...jsUnusedVarsRule, - 'react/prop-types': 'off' - } - } -); +export default await reactAppConfig({ + // LEGACY: Portal is mid-TS-migration. `src/` has both `.js` and `.ts` files + // with different parser requirements. Emit two separate src blocks until + // every `.js` file is converted to `.ts`. Tracked separately — when the + // migration lands, drop this flag and the workspace becomes a vanilla + // reactAppConfig() call. + legacyJsTsSplit: true, + tsconfigRootDir: import.meta.dirname, // workspace tsconfig.json, not the factory's + reactRefresh: false, // portal is bundled as UMD for theme distribution + i18next: true, + ignores: ['umd/**/*', 'dist/**/*'] +}); diff --git a/apps/portal/package.json b/apps/portal/package.json index acd8a1699ea..3e100e973d5 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/portal", "type": "module", - "version": "2.69.9", + "version": "2.69.10", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/portal/src/utils/contrast-color.js b/apps/portal/src/utils/contrast-color.js index b45e48e0f07..74b77560bb4 100644 --- a/apps/portal/src/utils/contrast-color.js +++ b/apps/portal/src/utils/contrast-color.js @@ -1,6 +1,6 @@ function padZero(str, len) { len = len || 2; - var zeros = new Array(len).join('0'); + const zeros = new Array(len).join('0'); return (zeros + str).slice(-len); } diff --git a/apps/posts/eslint.config.js b/apps/posts/eslint.config.js index d81f1452ad0..a16bd303581 100644 --- a/apps/posts/eslint.config.js +++ b/apps/posts/eslint.config.js @@ -1,96 +1,7 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - shadeLayeredImportsRule, - sortImportsRule, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...sortImportsRule, - ...shadeLayeredImportsRule, - ...tailwindRulesV4, - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - 'react-refresh/only-export-components': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off' - } - }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - 'no-undef': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + shadeRestricted: true, + sortImports: true +}); diff --git a/apps/shade/eslint.config.js b/apps/shade/eslint.config.js index 03cc361784e..1884172b230 100644 --- a/apps/shade/eslint.config.js +++ b/apps/shade/eslint.config.js @@ -1,91 +1,9 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import storybookPlugin from 'eslint-plugin-storybook'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; - -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*', 'storybook-static/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}', 'scripts/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...tailwindRulesV4, - // TS handles these — disable the base ESLint variants - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - }, - ...storybookPlugin.configs['flat/recommended'], - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +import {reactAppConfig} from '../../eslint.shared.mjs'; + +// Shade lints `scripts/` alongside `src/` (one-off build/codegen scripts). +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + ignores: ['dist/**/*', 'storybook-static/**/*'], + srcGlobs: ['src/**/*.{js,ts,cjs,tsx}', 'scripts/**/*.{js,ts,cjs,tsx}'], + storybook: 'plugin' +}); diff --git a/apps/signup-form/eslint.config.js b/apps/signup-form/eslint.config.js index 28f74cafdbc..eeb96d362d0 100644 --- a/apps/signup-form/eslint.config.js +++ b/apps/signup-form/eslint.config.js @@ -1,63 +1,13 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; - -import { - correctnessRules, - reactDefaultsOff, - sortImportsRule, - tailwindRulesWithConfig, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindConfig = `${import.meta.dirname}/tailwind.config.cjs`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['umd/**/*', 'dist/**/*', 'storybook-static/**/*'] - }, - { - files: ['src/**/*.{js,jsx,ts,tsx,cjs}', 'test/**/*.{js,jsx,ts,tsx,cjs}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...sortImportsRule, - ...reactDefaultsOff, - 'react/jsx-sort-props': ['error', { - reservedFirst: true, - callbacksLast: true, - shorthandLast: true, - locale: 'en' - }], - 'react/button-has-type': 'error', - 'react/no-array-index-key': 'error', - ...tailwindRulesWithConfig(tailwindConfig), - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +import {reactAppConfig} from '../../eslint.shared.mjs'; + +export default await reactAppConfig({ + // UMD bundle (no Vite HMR runtime), so the react-refresh rule is meaningless. + reactRefresh: false, + // LEGACY: Tailwind v3. Migration to v4 is a multi-day class/theme rewrite + + // CDN regression testing. Tracked separately. + legacyTailwindV3ConfigPath: `${import.meta.dirname}/tailwind.config.cjs`, + sortImports: true, + ignores: ['umd/**/*', 'dist/**/*', 'storybook-static/**/*'], + srcGlobs: ['src/**/*.{js,jsx,ts,tsx,cjs}', 'test/**/*.{js,jsx,ts,tsx,cjs}'], + testGlobs: false // single combined src+test block +}); diff --git a/apps/signup-form/package.json b/apps/signup-form/package.json index 7d5c1e3b468..b59dd55443a 100644 --- a/apps/signup-form/package.json +++ b/apps/signup-form/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/signup-form", "type": "module", - "version": "0.3.29", + "version": "0.3.30", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/sodo-search/eslint.config.js b/apps/sodo-search/eslint.config.js index 47a658897c9..524ca626983 100644 --- a/apps/sodo-search/eslint.config.js +++ b/apps/sodo-search/eslint.config.js @@ -1,59 +1,11 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - sortImportsRule -} from '../../eslint.shared.mjs'; - -const baseConfig = { - ...js.configs.recommended, - ...reactPlugin.configs.flat.recommended, - plugins: { - ...reactPlugin.configs.flat.recommended.plugins, - ghost: ghostPlugin - }, - settings: { - react: {version: 'detect'} - }, - rules: { - ...js.configs.recommended.rules, - ...reactPlugin.configs.flat.recommended.rules, - ...correctnessRules, - ...sortImportsRule, - 'react/prop-types': 'off' - } -}; - -export default [ - { - ignores: ['umd/**/*', 'dist/**/*'] - }, - { - ...baseConfig, - files: ['src/**/*.{js,jsx}'], - languageOptions: { - ...reactPlugin.configs.flat.recommended.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: globals.browser - } - }, - { - ...baseConfig, - files: ['test/**/*.{js,jsx}'], - languageOptions: { - ...reactPlugin.configs.flat.recommended.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.vitest, - ...globals.jest, - vi: 'readonly' - } - } - } -]; +// LEGACY: vanilla JS (no TypeScript). sodo-search should migrate to TS — small +// surface (~6 components) so the migration is 1–2 days. Until then, the +// typescript: false flag is the escape hatch. +export default await reactAppConfig({ + typescript: false, + reactRefresh: false, // bundled as UMD for theme distribution + sortImports: true, + ignores: ['umd/**/*', 'dist/**/*'] +}); diff --git a/apps/sodo-search/package.json b/apps/sodo-search/package.json index 36c66f1136b..91d49e75fd0 100644 --- a/apps/sodo-search/package.json +++ b/apps/sodo-search/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/sodo-search", "type": "module", - "version": "1.8.26", + "version": "1.8.27", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/stats/eslint.config.js b/apps/stats/eslint.config.js index d81f1452ad0..a16bd303581 100644 --- a/apps/stats/eslint.config.js +++ b/apps/stats/eslint.config.js @@ -1,96 +1,7 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import reactPlugin from 'eslint-plugin-react'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import reactRefreshPlugin from 'eslint-plugin-react-refresh'; -import tailwindcssPlugin from 'eslint-plugin-tailwindcss'; -import tseslint from 'typescript-eslint'; +import {reactAppConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - reactDefaultsOff, - reactStrictRules, - shadeLayeredImportsRule, - sortImportsRule, - tailwindRulesV4, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const tailwindCssConfig = `${import.meta.dirname}/../admin/src/index.css`; - -const reactFlat = reactPlugin.configs.flat.recommended; - -export default tseslint.config( - { - ignores: ['dist/**/*'] - }, - { - files: ['src/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ...reactFlat.languageOptions, - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node - } - }, - plugins: { - ...reactFlat.plugins, - ghost: ghostPlugin, - 'react-hooks': reactHooksPlugin, - 'react-refresh': reactRefreshPlugin, - tailwindcss: tailwindcssPlugin - }, - settings: { - react: {version: 'detect'}, - tailwindcss: {config: tailwindCssConfig} - }, - rules: { - ...js.configs.recommended.rules, - ...reactFlat.rules, - ...reactHooksPlugin.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...reactDefaultsOff, - ...reactStrictRules, - ...sortImportsRule, - ...shadeLayeredImportsRule, - ...tailwindRulesV4, - 'no-undef': 'off', - 'no-redeclare': 'off', - 'no-unexpected-multiline': 'off', - 'react-refresh/only-export-components': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off' - } - }, - { - files: ['test/**/*.{js,ts,cjs,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - ...globals.vitest, - vi: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...correctnessRules, - ...tsUnusedVarsRule, - ...mochaRulesOff(ghostPlugin), - 'no-undef': 'off', - '@typescript-eslint/no-inferrable-types': 'off' - } - } -); +export default await reactAppConfig({ + tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + shadeRestricted: true, + sortImports: true +}); diff --git a/eslint.shared.mjs b/eslint.shared.mjs index 5f77dd39bc8..ce49791703f 100644 --- a/eslint.shared.mjs +++ b/eslint.shared.mjs @@ -1,14 +1,38 @@ -// Shared building blocks for workspace ESLint flat configs. Each export is a -// rule object, a helper, or a plugin definition. Workspaces import only the -// pieces they need; this file imports nothing from the eslint plugin ecosystem -// so consumers don't take transitive peer deps via it. +// Shared ESLint config for Ghost workspaces. +// +// ============================================================================ +// Strategy: every rule is 'error' or 'off' — never 'warn'. Warnings get +// ignored by humans and agents and just pollute lint output. +// ============================================================================ +// +// Two factories cover most workspaces. Hover the factory call in your editor +// for full JSDoc on every param. +// +// reactAppConfig({...}) — every frontend React app (apps/*) +// nodeLibConfig({...}) — Node libs (ghost/i18n, ghost/parse-email-address) +// +// Decision tree: +// Frontend React app (apps/*) → reactAppConfig +// Node lib (ghost/i18n, parse-...) → nodeLibConfig +// Everything else → standalone (see "Standalone workspaces" below) +// +// Standalone workspaces (don't use a factory — read the file directly): +// ghost/core — 13 file-glob blocks for migrations, schema, frontend/server seam +// ghost/admin — Ember workspace, 90+ ember-plugin rules, babel parser +// apps/admin — host shell, recommendedTypeChecked posture, custom local plugins +// apps/admin-toolbar — pure jQuery vanilla JS, no React (could compose from atoms but tiny) +// +// `legacy*` params are escape hatches for known migrations that aren't worth +// blocking config on (e.g. Tailwind v3 → v4, JS → TS finish). They're +// intentionally named `legacy` so PRs to remove them are scoped and visible. import path from 'node:path'; -// === Rule sets === +// ============================================================================ +// === Atomic rule objects (composed inside factories; also exported for the +// === standalone workspaces that don't fit a factory) +// ============================================================================ -// Correctness baseline shared by every workspace flat config. Each rule is -// individually overridable in the consumer's `rules` block. export const correctnessRules = { curly: 'error', camelcase: ['error', {properties: 'never'}], @@ -25,8 +49,6 @@ export const correctnessRules = { 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] }; -// TS-aware unused-vars: disables the base rule, enables the TS variant with -// args ignore pattern. Most TS workspaces want this. export const tsUnusedVarsRule = { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { @@ -36,21 +58,18 @@ export const tsUnusedVarsRule = { }] }; -// Vanilla unused-vars with caughtErrors:'none' — ESLint 9 flipped the default -// to 'all'. For non-TS workspaces where the previous behavior is wanted. +// ESLint 9 flipped the no-unused-vars default for caughtErrors from 'none' to +// 'all'. Restore the previous behavior so unused catch bindings stay tolerated. export const jsUnusedVarsRule = { 'no-unused-vars': ['error', {caughtErrors: 'none'}] }; -// Import-sort autofix from eslint-plugin-ghost. export const sortImportsRule = { 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] }] }; -// Block barrel imports of @tryghost/shade; consumers must import from layered -// subpaths. Used by admin-x-* / posts / activitypub / admin host. export const shadeLayeredImportsRule = { 'no-restricted-imports': ['error', { paths: [{ @@ -60,13 +79,11 @@ export const shadeLayeredImportsRule = { }] }; -// React rule defaults turned off across React workspaces. export const reactDefaultsOff = { - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off' + 'react/react-in-jsx-scope': 'off', // not needed with the new JSX transform + 'react/prop-types': 'off' // codebase uses TypeScript for prop typing }; -// React stylistic + correctness preferences applied across the React workspaces. export const reactStrictRules = { 'react/jsx-sort-props': ['error', { reservedFirst: true, @@ -76,27 +93,22 @@ export const reactStrictRules = { }], 'react/button-has-type': 'error', 'react/no-array-index-key': 'error', - 'react/jsx-key': 'off' + 'react/jsx-key': 'off' // TODO: 22 legacy violations across 4 workspaces; flip back to error after cleanup }; -// Tailwind v4 ruleset using settings-based config (consumer sets -// `settings.tailwindcss.config`). For v3 workspaces use tailwindRulesWithConfig(). -// All rules are 'error' or 'off' — Ghost's stance is no warnings (warnings -// get ignored and pollute context). migration-from-tailwind-2 is off since -// Ghost is already on v4; the rule is no longer providing value. +// Tailwind v4 ruleset (settings-based config). export const tailwindRulesV4 = { 'tailwindcss/classnames-order': 'error', 'tailwindcss/enforces-negative-arbitrary-values': 'error', 'tailwindcss/enforces-shorthand': 'error', - 'tailwindcss/migration-from-tailwind-2': 'off', - 'tailwindcss/no-arbitrary-value': 'off', - 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/migration-from-tailwind-2': 'off', // already on v4; rule is a v2 migration helper + 'tailwindcss/no-arbitrary-value': 'off', // intentionally allowed + 'tailwindcss/no-custom-classname': 'off', // codebase relies on custom classnames 'tailwindcss/no-contradicting-classname': 'error' }; -// Tailwind v3 ruleset with config passed per-rule (v3 didn't read `settings` -// the same way). Use the consumer's tailwind config absolute path. Same -// error-or-off stance as tailwindRulesV4. +// Tailwind v3 ruleset (per-rule config). LEGACY — used only by comments-ui +// and signup-form until they migrate to v4. export function tailwindRulesWithConfig(config) { return { 'tailwindcss/classnames-order': ['error', {config}], @@ -109,11 +121,93 @@ export function tailwindRulesWithConfig(config) { }; } -// === Helpers === +// Composite rule sets used by factories. Every rule is 'error' or 'off' — +// never 'warn'. Rules at 'off' have inline TODOs with violation counts so +// re-enabling them is a scoped cleanup PR. -// Build an object that disables every `ghost/mocha/*` rule shipped by -// eslint-plugin-ghost. Used in test blocks so Vitest patterns don't trip -// false-positive mocha warnings. Pass the consumer's ghostPlugin. +export const tsReactAppRules = { + ...correctnessRules, + ...tsUnusedVarsRule, + ...reactDefaultsOff, + ...reactStrictRules, + // Apply react-hooks rules at the rule-set level so they cover BOTH src and + // test blocks. The src block ALSO extends reactHooks.configs.recommended-latest + // which is fine — these match. exhaustive-deps stays off everywhere; the + // workspace-specific overrides for src already handle TODO counts. + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'off', + 'no-var': 'error', + // TS handles these at compile time; turning them off in ESLint avoids + // duplicate/contradictory reports. + 'no-undef': 'off', + 'no-redeclare': 'off', + 'no-unexpected-multiline': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + // Catches real type-safety regressions. Workspaces with legacy violations + // override to 'off' explicitly (admin-x-settings: 2, comments-ui: 41). + '@typescript-eslint/no-explicit-any': 'error', + // TODO: 97 violations across 8 workspaces. Cleanup PR will flip to 'error'. + '@typescript-eslint/no-non-null-assertion': 'off', + // TODO: 121 violations across all 9 TS React workspaces. Cleanup PR will flip to 'error'. + '@typescript-eslint/no-empty-function': 'off' +}; + +// Extras for Vite-based apps with eslint-plugin-react-refresh registered. +// (react-hooks is loaded by every React app now, including UMD — react-refresh +// is the Vite-specific HMR rule.) +export const viteOnlyExtras = { + // LEGACY: 195 violations across 7 Vite apps. The rule fires on any module + // that exports a non-component alongside a component (utils, constants, + // hooks). React/Vite patterns mix these constantly — fixing 195 cases means + // splitting hundreds of files. Practically permanent; the rule's HMR + // benefit doesn't justify the codebase upheaval. + 'react-refresh/only-export-components': 'off' +}; + +export const jsReactAppRules = { + ...correctnessRules, + ...jsUnusedVarsRule, + ...reactDefaultsOff, + 'no-var': 'error' +}; + +export const nodeLibRules = { + ...correctnessRules, + 'no-var': 'error', + 'one-var': ['error', 'never'], + 'ghost/ghost-custom/no-native-error': 'error', + 'ghost/ghost-custom/ghost-error-usage': 'error', + 'ghost/ghost-custom/ghost-tpl-usage': 'error' +}; + +export const noGhostIgnitionRequireRule = { + 'ghost/node/no-restricted-require': ['error', [ + { + name: 'ghost-ignition', + message: '@deprecated, please use @tryghost/errors, @tryghost/logging or @tryghost/debug. Config and Server are coming soon!' + } + ]] +}; + +// Strict linter options. reportUnusedDisableDirectives is 'off' for now. +// TODO: ~50 stale inline `eslint-disable` comments across the codebase (rough +// estimate based on prior autofix runs). Flipping this to 'error' requires +// either deleting each manually (eslint --fix tends to leave whitespace +// residue) or a careful autofix + cleanup pass. Worth doing — once flipped, +// the lint output stays honest about whether each inline disable is suppressing +// an actual violation. +export const strictLinterOptions = { + linterOptions: { + reportUnusedDisableDirectives: 'off' + } +}; + +// ============================================================================ +// === Helpers +// ============================================================================ + +// Disables every `ghost/mocha/*` rule. Used in test blocks so Vitest patterns +// don't trip false-positive mocha-style warnings. export function mochaRulesOff(ghostPlugin) { return Object.fromEntries( Object.keys(ghostPlugin.rules || {}) @@ -122,13 +216,12 @@ export function mochaRulesOff(ghostPlugin) { ); } -// === Plugins === +// ============================================================================ +// === Plugins +// ============================================================================ -// eslint-plugin-filenames-ts@1.3.2's match-regex rule calls context.getScope(), -// which ESLint 9 removed. Minimal replacement: filename check + optional -// ignoreExporting via AST scan (no scope traversal). Use under the -// 'local-filenames' plugin name. Compatible with the three call sites that -// previously defined their own shim (ghost/core, ghost/admin, ghost/i18n). +// ESLint-9-compatible replacement for eslint-plugin-filenames-ts's match-regex +// rule (the upstream calls context.getScope() which ESLint 9 removed). const filenamesMatchRegex = { meta: { type: 'problem', @@ -172,3 +265,594 @@ const filenamesMatchRegex = { export const localFilenamesPlugin = { rules: {'match-regex': filenamesMatchRegex} }; + +// ============================================================================ +// === Factory: reactAppConfig +// ============================================================================ +// +// Single factory for every frontend React app in apps/*. + +/** + * Options for {@link reactAppConfig}. + * + * @typedef {object} ReactAppConfigOptions + * @property {boolean} [typescript=true] + * When false: vanilla JS app, no typescript-eslint extends, no + * @typescript-eslint/* rules. LEGACY for sodo-search and announcement-bar + * (should migrate to TS eventually). + * @property {boolean} [reactRefresh=true] + * When false: skip eslint-plugin-react-refresh. Set false for UMD-bundled + * apps (comments-ui, signup-form) and vanilla JS apps — react-refresh is a + * Vite-HMR rule that's meaningless without Vite's HMR runtime. + * @property {boolean} [i18next=false] + * When true: load eslint-plugin-i18next and apply its flat/recommended preset. + * @property {'plugin' | 'storiesBlock' | null} [storybook=null] + * - `'plugin'`: applies eslint-plugin-storybook's full flat/recommended + * ruleset (story-exports check, prefer-pascal-case, hierarchy-separator, + * no-redundant-story-name, etc.) — this is what you want for any workspace + * with a proper Storybook setup. Used by shade. + * - `'storiesBlock'`: a minimal escape hatch — adds just one rule override + * (`react-hooks/rules-of-hooks: 'off'`) scoped to `**\/*.stories.*` files, + * for workspaces that have Storybook stories but don't want the full + * storybook ruleset. Used by admin-x-design-system. + * - `null` (default): skip Storybook handling entirely. + * @property {string} [tailwindCssPath] + * Absolute path to a Tailwind v4 CSS config. Omit to skip Tailwind. Setting + * both this and `legacyTailwindV3ConfigPath` throws. NOTE: the workspace + * must also have `tailwindcss` as a (dev)Dependency — the + * eslint-plugin-tailwindcss settings-based resolver requires it locally. + * @property {string} [legacyTailwindV3ConfigPath] + * LEGACY escape hatch. Absolute path to a Tailwind v3 JS/CJS config. Used by + * comments-ui and signup-form until they migrate to v4. The migration + * involves theme token rewrites + class rewrites + CDN regression testing + * (multi-day, blocked on no owner) so the rest of the codebase isn't held up. + * @property {boolean} [shadeRestricted=false] + * When true: block barrel imports of `@tryghost/shade` (force layered subpath + * imports). Only relevant for workspaces that import shade. + * @property {boolean} [sortImports=false] + * When true: apply `ghost/sort-imports-es6-autofix/sort-imports-es6`. + * @property {boolean} [legacyJsTsSplit=false] + * LEGACY escape hatch for portal only. Portal is mid-TS-migration with both + * `.js` and `.ts` source files mixed in `src/`. When true, emits two src + * blocks: one for `src/**\/*.{js,jsx}` (vanilla rules) and one for + * `src/**\/*.{ts,tsx}` (TS rules + project: './tsconfig.json'). Remove when + * portal's JS files are fully migrated to TS. When used, pass + * `tsconfigRootDir: import.meta.dirname` so the TS block resolves + * `tsconfig.json` from the workspace, not the factory's directory. + * @property {string} [tsconfigRootDir] + * Workspace directory containing `tsconfig.json` for type-aware lint blocks. + * Only consulted when `legacyJsTsSplit: true`. Defaults to `process.cwd()` + * (works when ESLint is invoked from the workspace directory, which is the + * `pnpm --filter ... exec eslint` pattern). + * @property {string[]} [ignores=['dist/**\/*']] + * Extra ignore globs (replaces the default — pass an array including your + * defaults). + * @property {string[]} [srcGlobs] + * Override src globs. Default depends on typescript flag: TS uses + * `['src/**\/*.{js,ts,cjs,tsx}']`; vanilla JS uses `['src/**\/*.{js,jsx}']`. + * @property {string[]} [testGlobs] + * Override test globs. Default mirrors `srcGlobs` for the `test/` directory. + * Pass `false` to skip the test block entirely (some UMD apps lint a single + * src+test combined block via `srcGlobs`). + * @property {object} [extraSrcRules] + * Per-workspace rule overrides for the src block (highest precedence). + * @property {object} [extraTestRules] + * Per-workspace rule overrides for the test block. + */ + +/** + * Build a flat ESLint config for a frontend React app. + * + * @param {ReactAppConfigOptions} [options] + * @returns {Promise} + * + * @example + * // apps/posts/eslint.config.js — Vite TS React + Tailwind v4 + shade restriction + * import {reactAppConfig} from '../../eslint.shared.mjs'; + * export default await reactAppConfig({ + * tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`, + * shadeRestricted: true + * }); + * + * @example + * // apps/comments-ui/eslint.config.js — UMD TS React + Tailwind v3 + i18next + * import {reactAppConfig} from '../../eslint.shared.mjs'; + * export default await reactAppConfig({ + * reactRefresh: false, + * legacyTailwindV3ConfigPath: `${import.meta.dirname}/tailwind.config.js`, + * i18next: true, + * sortImports: true, + * testGlobs: false, + * srcGlobs: ['src/**\/*.{js,jsx,ts,tsx}'] + * }); + * + * @example + * // apps/announcement-bar/eslint.config.js — vanilla JS React + * import {reactAppConfig} from '../../eslint.shared.mjs'; + * export default await reactAppConfig({ + * typescript: false, + * reactRefresh: false, + * ignores: ['umd/**\/*', 'dist/**\/*'] + * }); + */ +const REACT_APP_PARAMS = new Set([ + 'typescript', 'reactRefresh', 'i18next', 'storybook', 'tailwindCssPath', + 'legacyTailwindV3ConfigPath', 'shadeRestricted', 'sortImports', + 'legacyJsTsSplit', 'tsconfigRootDir', 'ignores', 'srcGlobs', 'testGlobs', + 'extraSrcRules', 'extraTestRules' +]); + +export async function reactAppConfig(options = {}) { + const unknown = Object.keys(options).filter(k => !REACT_APP_PARAMS.has(k)); + if (unknown.length) { + throw new Error(`reactAppConfig: unknown options ${JSON.stringify(unknown)}. Valid keys: ${JSON.stringify([...REACT_APP_PARAMS])}`); + } + const { + typescript = true, + reactRefresh = true, + i18next = false, + storybook = null, + tailwindCssPath, + legacyTailwindV3ConfigPath, + shadeRestricted = false, + sortImports = false, + legacyJsTsSplit = false, + tsconfigRootDir, + ignores = ['dist/**/*'], + srcGlobs, + testGlobs, + extraSrcRules = {}, + extraTestRules = {} + } = options; + if (tailwindCssPath && legacyTailwindV3ConfigPath) { + throw new Error('reactAppConfig: pass either tailwindCssPath (v4) or legacyTailwindV3ConfigPath (v3), not both.'); + } + if (legacyJsTsSplit && !typescript) { + throw new Error('reactAppConfig: legacyJsTsSplit requires typescript: true (the TS block uses tseslint).'); + } + if (legacyJsTsSplit && (srcGlobs || testGlobs !== undefined)) { + // The split branch hardcodes its file globs by extension (.js,.jsx vs + // .ts,.tsx) — if a consumer passes srcGlobs/testGlobs we'd silently + // ignore them. Throw rather than confuse. + throw new Error('reactAppConfig: legacyJsTsSplit does not honor srcGlobs/testGlobs; they would be silently dropped.'); + } + if (shadeRestricted && extraSrcRules['no-restricted-imports']) { + // Setting both would silently replace the shade restriction with the + // user's rule (last-key-wins on spread). If a workspace needs both, + // add the user's paths to shadeLayeredImportsRule via a custom + // `no-restricted-imports` value that includes both shade + the + // workspace's paths. Throwing rather than silently losing the shade + // restriction (which is security-shaped). + throw new Error('reactAppConfig: shadeRestricted + extraSrcRules[\'no-restricted-imports\'] would silently override the shade restriction. Merge them in your workspace config.'); + } + + // Lazy-load plugins so a Node lib calling nodeLibConfig never loads React. + // typescript-eslint is loaded unconditionally because we use its `config()` + // helper to flatten the `extends:` key (vanilla ESLint flat config doesn't + // support `extends:` natively). + const [ + {default: js}, + {default: globals}, + {default: ghostPlugin}, + {default: reactPlugin}, + {default: reactHooksPlugin}, + tseslint + ] = await Promise.all([ + import('@eslint/js'), + import('globals'), + import('eslint-plugin-ghost'), + import('eslint-plugin-react'), + import('eslint-plugin-react-hooks'), + import('typescript-eslint') + ]); + + const reactRefreshPlugin = reactRefresh + ? (await import('eslint-plugin-react-refresh')).default + : null; + const tailwindcssPlugin = (tailwindCssPath || legacyTailwindV3ConfigPath) + ? (await import('eslint-plugin-tailwindcss')).default + : null; + const i18nextPlugin = i18next + ? (await import('eslint-plugin-i18next')).default + : null; + const storybookPlugin = storybook === 'plugin' + ? (await import('eslint-plugin-storybook')).default + : null; + + const reactFlat = reactPlugin.configs.flat.recommended; + const reactJsxRuntime = reactPlugin.configs.flat['jsx-runtime']; + const i18nextFlat = i18nextPlugin?.configs['flat/recommended']; + + const defaultTsSrcGlobs = ['src/**/*.{js,ts,cjs,tsx}']; + const defaultJsSrcGlobs = ['src/**/*.{js,jsx}']; + const defaultTsTestGlobs = ['test/**/*.{js,ts,cjs,tsx}']; + const defaultJsTestGlobs = ['test/**/*.{js,jsx}']; + + const baseLanguageOptions = { + ...reactFlat.languageOptions, + ecmaVersion: 2022, + sourceType: 'module', + globals: {...globals.browser, ...globals.node} + }; + + const basePlugins = { + ...reactFlat.plugins, + ...(i18nextFlat?.plugins ?? {}), + ghost: ghostPlugin, + 'react-hooks': reactHooksPlugin, + ...(reactRefreshPlugin && {'react-refresh': reactRefreshPlugin}), + ...(tailwindcssPlugin && {tailwindcss: tailwindcssPlugin}) + }; + + const baseSettings = { + react: {version: 'detect'}, + ...(tailwindCssPath && {tailwindcss: {config: tailwindCssPath}}) + }; + + const tailwindRules = tailwindCssPath + ? tailwindRulesV4 + : (legacyTailwindV3ConfigPath ? tailwindRulesWithConfig(legacyTailwindV3ConfigPath) : {}); + + // Shared rule layers for any src block (TS or JS variant overlays on top). + const baseSrcRules = { + ...js.configs.recommended.rules, + ...reactFlat.rules, + ...(i18nextFlat?.rules ?? {}), + ...reactHooksPlugin.configs.recommended.rules, + // TODO: ~46 legacy violations (~24 across Vite TS apps + 22 in + // comments-ui surfaced when react-hooks plugin was added there + 1 in + // announcement-bar). Real bug-catcher (missing useEffect/useMemo deps); + // cleanup PR will fix per-call-site and flip this to 'error'. Until + // then 'off' is intentional — the plugin's default is 'warn' which + // Ghost's stance forbids. + 'react-hooks/exhaustive-deps': 'off', + ...(reactRefresh ? viteOnlyExtras : {}), + ...(shadeRestricted ? shadeLayeredImportsRule : {}), + ...(sortImports ? sortImportsRule : {}), + ...tailwindRules + }; + + // Build src block(s). legacyJsTsSplit produces two src blocks + // (one .js, one .ts); everything else produces one. + const srcBlocks = []; + + if (legacyJsTsSplit) { + // Portal — split blocks. The validator above guarantees srcGlobs and + // testGlobs are not set when legacyJsTsSplit is true. + srcBlocks.push({ + files: ['src/**/*.{js,jsx}', 'test/**/*.{js,jsx}'], + ...js.configs.recommended, + languageOptions: { + ...reactFlat.languageOptions, + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.vitest, + ...globals.jest, + vi: 'readonly', + require: 'readonly' + } + }, + plugins: basePlugins, + settings: baseSettings, + rules: { + ...baseSrcRules, + ...reactJsxRuntime.rules, + ...jsReactAppRules, + ...extraSrcRules + } + }); + srcBlocks.push({ + files: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}'], + extends: [...tseslint.configs.recommended], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { + ecmaFeatures: {jsx: true}, + project: './tsconfig.json', + // BUG-FIX: was `import.meta.dirname` which resolves to the + // factory's directory (repo root), not the workspace. + tsconfigRootDir: tsconfigRootDir ?? process.cwd() + }, + globals: { + ...globals.browser, + ...globals.vitest, + ...globals.jest, + vi: 'readonly' + } + }, + plugins: basePlugins, + settings: baseSettings, + rules: { + ...baseSrcRules, // includes js.recommended + reactFlat + i18nextFlat + react-hooks + viteOnlyExtras + tailwindRules + ...reactJsxRuntime.rules, + ...tsReactAppRules, // TS branch needs TS-safe defaults (no-undef: off, no-explicit-any: error, react strict rules) + ...extraSrcRules + } + }); + } else if (typescript) { + srcBlocks.push({ + files: srcGlobs ?? defaultTsSrcGlobs, + extends: [...tseslint.configs.recommended], + languageOptions: baseLanguageOptions, + plugins: basePlugins, + settings: baseSettings, + rules: { + ...baseSrcRules, + ...tsReactAppRules, + ...extraSrcRules + } + }); + } else { + // Vanilla JS (sodo-search, announcement-bar). LEGACY: should migrate to TS. + srcBlocks.push({ + files: srcGlobs ?? defaultJsSrcGlobs, + ...js.configs.recommended, + languageOptions: { + ...reactFlat.languageOptions, + ecmaVersion: 2022, + sourceType: 'module', + globals: globals.browser + }, + plugins: basePlugins, + settings: baseSettings, + rules: { + ...baseSrcRules, + ...jsReactAppRules, + ...extraSrcRules + } + }); + } + + // Test block — skipped if testGlobs === false. + const testBlocks = []; + if (testGlobs !== false && !legacyJsTsSplit) { + const resolvedTestGlobs = testGlobs ?? (typescript ? defaultTsTestGlobs : defaultJsTestGlobs); + const testLanguageOptions = typescript + ? { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + ...globals.vitest, + vi: 'readonly' + } + } + : { + ...reactFlat.languageOptions, + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.vitest, + ...globals.jest, + vi: 'readonly' + } + }; + // Test blocks need the same plugins as src because the spread rules + // (tsReactAppRules / jsReactAppRules) reference react/*, react-hooks/*, + // etc. — ESLint flat config errors if a rule references a plugin not + // registered in the same block. + testBlocks.push({ + files: resolvedTestGlobs, + ...(typescript ? {extends: [...tseslint.configs.recommended]} : js.configs.recommended), + languageOptions: testLanguageOptions, + plugins: basePlugins, + settings: baseSettings, + rules: { + ...(typescript ? tsReactAppRules : {...js.configs.recommended.rules, ...reactFlat.rules, ...jsReactAppRules}), + ...mochaRulesOff(ghostPlugin), + ...extraTestRules + } + }); + } + + // Storybook handling (after src/test so storybook rules win for stories). + const storybookBlocks = []; + if (storybook === 'plugin') { + storybookBlocks.push(...storybookPlugin.configs['flat/recommended']); + // The storybook preset ships 3 warn-level rules. Ghost's stance is + // error-or-off, never warn — normalize them. hierarchy-separator and + // no-redundant-story-name have zero violations across shade so they + // flip to error for free; prefer-pascal-case has 29 violations so + // it's off until cleanup. + storybookBlocks.push({ + files: ['**/*.stories.{ts,tsx,js,jsx,mjs,cjs}', '**/*.story.{ts,tsx,js,jsx,mjs,cjs}'], + rules: { + 'storybook/hierarchy-separator': 'error', + 'storybook/no-redundant-story-name': 'error', + // TODO: 29 legacy violations in shade. Flip to 'error' after the + // cleanup PR renames story exports to PascalCase. + 'storybook/prefer-pascal-case': 'off' + } + }); + } else if (storybook === 'storiesBlock') { + storybookBlocks.push({ + files: ['**/*.stories.{ts,tsx,js,jsx}'], + rules: {'react-hooks/rules-of-hooks': 'off'} + }); + } + + return tseslint.config( + {ignores}, + {files: ['**/*'], ...strictLinterOptions}, + ...srcBlocks, + ...storybookBlocks, + ...testBlocks + ); +} + +// ============================================================================ +// === Factory: nodeLibConfig +// ============================================================================ + +/** + * Options for {@link nodeLibConfig}. + * + * @typedef {object} NodeLibConfigOptions + * @property {boolean} [typescript=true] + * When true: extends tseslint.configs.recommended + tsUnusedVarsRule. + * When false: vanilla JS with jsUnusedVarsRule. + * @property {boolean} [commonjs=false] + * When true: sourceType is 'commonjs' instead of 'module'. + * @property {boolean} [legacyLocalFilenames=false] + * LEGACY escape hatch for ghost/i18n. When true: register the + * localFilenamesPlugin and turn off `ghost/filenames/match-regex` so the + * workspace can use `local-filenames/match-regex` (its workspace-local + * variant) instead. Should be unified with the rest of the codebase + * eventually so we only have one filename-matching rule. + * @property {string[]} [srcGlobs] + * Override src globs. Default depends on typescript flag. + * @property {string[]} [testGlobs] + * Override test globs. Pass `false` to skip the test block. + * @property {string[]} [ignores=['build/**\/*']] + * Replace default ignore globs. + * @property {object} [extraSrcRules] + * Per-workspace src rule overrides. + * @property {object} [extraTestRules] + * Per-workspace test rule overrides. + * @property {Array} [extraBlocks] + * Append extra config blocks (e.g. ghost/i18n's `max-lines` override on + * `lib/index.js`). Each block SHOULD set its own `files:` glob — flat config + * treats a block without `files:` as applying to all files, which is rarely + * what consumers want. + */ + +/** + * Build a flat ESLint config for a Node library. + * + * @param {NodeLibConfigOptions} [options] + * @returns {Promise} + * + * @example + * // ghost/parse-email-address/eslint.config.mjs — TS Node lib + * import {nodeLibConfig} from '../../eslint.shared.mjs'; + * export default await nodeLibConfig(); + * + * @example + * // ghost/i18n/eslint.config.mjs — JS Node lib with local-filenames variant + * import {nodeLibConfig, noGhostIgnitionRequireRule} from '../../eslint.shared.mjs'; + * export default await nodeLibConfig({ + * typescript: false, + * commonjs: true, + * legacyLocalFilenames: true, + * srcGlobs: ['*.js', 'lib/**\/*.js'], + * extraSrcRules: noGhostIgnitionRequireRule, + * extraBlocks: [{ + * files: ['lib/**\/index.js', 'index.js'], + * rules: {'max-lines': ['error', {skipBlankLines: true, skipComments: true, max: 50}]} + * }] + * }); + */ +const NODE_LIB_PARAMS = new Set([ + 'typescript', 'commonjs', 'legacyLocalFilenames', 'srcGlobs', 'testGlobs', + 'ignores', 'extraSrcRules', 'extraTestRules', 'extraBlocks' +]); + +export async function nodeLibConfig(options = {}) { + const unknown = Object.keys(options).filter(k => !NODE_LIB_PARAMS.has(k)); + if (unknown.length) { + throw new Error(`nodeLibConfig: unknown options ${JSON.stringify(unknown)}. Valid keys: ${JSON.stringify([...NODE_LIB_PARAMS])}`); + } + const { + typescript = true, + commonjs = false, + legacyLocalFilenames = false, + srcGlobs, + testGlobs, + ignores = ['build/**/*'], + extraSrcRules = {}, + extraTestRules = {}, + extraBlocks = [] + } = options; + const [ + {default: js}, + {default: globals}, + {default: ghostPlugin}, + tseslint + ] = await Promise.all([ + import('@eslint/js'), + import('globals'), + import('eslint-plugin-ghost'), + import('typescript-eslint') + ]); + + const defaultTsSrcGlobs = ['src/**/*.ts']; + const defaultJsSrcGlobs = ['*.js', 'lib/**/*.js']; + const defaultTsTestGlobs = ['test/**/*.ts']; + const defaultJsTestGlobs = ['test/**/*.js']; + + const sourceType = commonjs ? 'commonjs' : 'module'; + + const unusedVarsRule = typescript ? tsUnusedVarsRule : jsUnusedVarsRule; + const baseRules = { + ...nodeLibRules, + ...unusedVarsRule, + // Turn off the eslint-plugin-ghost filename rule when using the + // local-filenames variant — they're equivalent in intent. + ...(legacyLocalFilenames ? {'ghost/filenames/match-regex': 'off'} : {}) + }; + + const plugins = { + ghost: ghostPlugin, + ...(legacyLocalFilenames && {'local-filenames': localFilenamesPlugin}) + }; + + const srcLanguageOptions = { + ecmaVersion: 2022, + sourceType, + globals: globals.node + }; + + const testLanguageOptions = { + ecmaVersion: 2022, + sourceType, + globals: { + ...globals.node, + ...globals.mocha, + ...globals.vitest, + vi: 'readonly', + beforeAll: 'readonly', + should: 'readonly', + sinon: 'readonly' + } + }; + + const srcBlock = { + files: srcGlobs ?? (typescript ? defaultTsSrcGlobs : defaultJsSrcGlobs), + ...(typescript ? {extends: [...tseslint.configs.recommended]} : js.configs.recommended), + languageOptions: srcLanguageOptions, + plugins, + rules: { + ...js.configs.recommended.rules, + ...baseRules, + ...(typescript ? {'no-undef': 'off'} : {}), + ...extraSrcRules + } + }; + + const testBlock = testGlobs === false ? null : { + files: testGlobs ?? (typescript ? defaultTsTestGlobs : defaultJsTestGlobs), + ...(typescript ? {extends: [...tseslint.configs.recommended]} : js.configs.recommended), + languageOptions: testLanguageOptions, + plugins, + rules: { + ...js.configs.recommended.rules, + ...baseRules, + ...mochaRulesOff(ghostPlugin), + ...(typescript ? {'no-undef': 'off'} : {}), + ...extraTestRules + } + }; + + return tseslint.config( + {ignores}, + {files: ['**/*'], ...strictLinterOptions}, + srcBlock, + ...extraBlocks, + ...(testBlock ? [testBlock] : []) + ); +} diff --git a/ghost/admin/eslint.config.mjs b/ghost/admin/eslint.config.mjs index f6df262e975..7f49ead0adf 100644 --- a/ghost/admin/eslint.config.mjs +++ b/ghost/admin/eslint.config.mjs @@ -5,22 +5,22 @@ import ghostPlugin from 'eslint-plugin-ghost'; import emberPlugin from 'eslint-plugin-ember'; import reactPlugin from 'eslint-plugin-react'; -import {localFilenamesPlugin} from '../../eslint.shared.mjs'; +import { + correctnessRules, + jsUnusedVarsRule, + localFilenamesPlugin, + mochaRulesOff, + strictLinterOptions +} from '../../eslint.shared.mjs'; +// ghost/admin uses local-filenames/match-regex (workspace-scoped, blocks +// below) instead of ghost/filenames/match-regex (from correctnessRules) — turn +// that one off here. no-unused-private-class-members is workspace-specific +// (ESLint 9 added it to recommended; codebase has intentional placeholders). const ghostBaseRules = { - curly: 'error', - camelcase: ['error', {properties: 'never'}], - 'dot-notation': 'error', - eqeqeq: ['error', 'always'], - 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], - 'no-eval': 'error', - 'no-useless-call': 'error', - 'no-console': 'error', - 'no-shadow': 'error', - 'array-callback-return': 'error', - 'no-constructor-return': 'error', - 'no-promise-executor-return': 'error', - 'no-unused-vars': ['error', {caughtErrors: 'none'}], + ...correctnessRules, + ...jsUnusedVarsRule, + 'ghost/filenames/match-regex': 'off', 'no-unused-private-class-members': 'off' }; @@ -105,11 +105,7 @@ const emberRules = { 'ghost/ember/require-valid-css-selector-in-test-helpers': 'error' }; -const mochaRulesOff = Object.fromEntries( - Object.keys(ghostPlugin.rules || {}) - .filter(rule => rule.startsWith('mocha/')) - .map(rule => [`ghost/${rule}`, 'off']) -); +const mochaRulesOffForGhost = mochaRulesOff(ghostPlugin); export default [ { @@ -121,6 +117,10 @@ export default [ 'node_modules/**' ] }, + { + files: ['**/*'], + ...strictLinterOptions + }, { files: ['**/*.js'], ...js.configs.recommended, @@ -189,7 +189,7 @@ export default [ } }, rules: { - ...mochaRulesOff, + ...mochaRulesOffForGhost, 'ghost/ember/no-invalid-debug-function-arguments': 'off', 'ghost/mocha/no-setup-in-describe': 'off' } diff --git a/ghost/core/eslint.config.mjs b/ghost/core/eslint.config.mjs index 1d6e82429fa..73620e9b1ed 100644 --- a/ghost/core/eslint.config.mjs +++ b/ghost/core/eslint.config.mjs @@ -4,39 +4,53 @@ import globals from 'globals'; import ghostPlugin from 'eslint-plugin-ghost'; import tseslint from 'typescript-eslint'; -import {localFilenamesPlugin} from '../../eslint.shared.mjs'; +import { + correctnessRules, + jsUnusedVarsRule, + localFilenamesPlugin, + mochaRulesOff, + nodeLibRules, + strictLinterOptions +} from '../../eslint.shared.mjs'; const __dirname = import.meta.dirname; +// nodeLibRules + jsUnusedVarsRule covers the bulk. Two workspace-specific: +// turn off ghost/filenames/match-regex (we use local-filenames/match-regex in +// scoped blocks below) and no-unused-private-class-members (codebase has +// intentional placeholder private fields; ESLint 9 added this rule to +// recommended without warning). const ghostBaseRules = { - curly: 'error', - camelcase: ['error', {properties: 'never'}], - 'dot-notation': 'error', - eqeqeq: ['error', 'always'], - 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], - 'no-eval': 'error', - 'no-useless-call': 'error', - 'no-console': 'error', - 'no-shadow': 'error', - 'array-callback-return': 'error', - 'no-constructor-return': 'error', - 'no-promise-executor-return': 'error', - 'no-unused-vars': ['error', {caughtErrors: 'none'}], - // ESLint 9 added this to eslint:recommended; the codebase has intentional - // placeholder private fields and was not previously linted for them. - 'no-unused-private-class-members': 'off', - 'no-var': 'error', - 'one-var': ['error', 'never'], - 'ghost/ghost-custom/no-native-error': 'error', - 'ghost/ghost-custom/ghost-error-usage': 'error', - 'ghost/ghost-custom/ghost-tpl-usage': 'error' + ...nodeLibRules, + ...jsUnusedVarsRule, + 'ghost/filenames/match-regex': 'off', + 'no-unused-private-class-members': 'off' }; -const mochaRulesOff = Object.fromEntries( - Object.keys(ghostPlugin.rules || {}) - .filter(rule => rule.startsWith('mocha/')) - .map(rule => [`ghost/${rule}`, 'off']) -); +const mochaRulesOffForGhost = mochaRulesOff(ghostPlugin); + +// LEGACY: typescript-eslint v8's recommended ruleset is stricter than the old +// plugin:ghost/ts posture. These overrides restore the previous behavior so +// the TS migration isn't blocked. Re-enabling each is its own cleanup PR +// (counts are codebase-wide and large — flipping any would block this PR). +// Applied across 4 file-glob blocks in ghost/core to ensure they win over +// every tseslint extends in the chain. +const tsLegacyRelaxations = { + // LEGACY: tseslint v8 default. Switching means cleaning up CommonJS + // `require()` calls across hundreds of .ts files. + '@typescript-eslint/no-require-imports': 'off', + // LEGACY: tseslint v8 default. Codebase uses `expect(x).to.be.true` + // patterns that read as unused expressions. + '@typescript-eslint/no-unused-expressions': 'off', + // LEGACY: tseslint v8 default. `Function` used as a type for callbacks. + '@typescript-eslint/no-unsafe-function-type': 'off', + // LEGACY: tseslint v8 default. `// @ts-ignore` comments still exist. + '@typescript-eslint/ban-ts-comment': 'off', + // LEGACY: previous posture from plugin:ghost/ts allowed inferrable types. + '@typescript-eslint/no-inferrable-types': 'off', + // LEGACY: `let` declarations across the codebase could be `const`. + 'prefer-const': 'off' +}; const migrationLoopRules = ['error', {selector: 'ForStatement', message: 'For statements can perform badly in migrations'}, @@ -62,6 +76,10 @@ export default tseslint.config( '!core/frontend/src/member-attribution/**/*.js' ] }, + { + files: ['**/*'], + ...strictLinterOptions + }, // ============================================================ // Base: server / shared / frontend / root JS files // ============================================================ @@ -71,6 +89,8 @@ export default tseslint.config( 'core/shared/**/*.js', 'core/frontend/**/*.js', 'core/*.js', + 'bin/**/*.js', + 'scripts/**/*.js', '*.js' ], ...js.configs.recommended, @@ -112,33 +132,18 @@ export default tseslint.config( caughtErrors: 'none' }], 'no-undef': 'off', - // typescript-eslint v8's recommended is stricter than what the - // legacy ghost/ts config enforced. Match the previous posture - // until a separate cleanup pass tightens them. - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'prefer-const': 'off' + '@typescript-eslint/no-explicit-any': 'off', + ...tsLegacyRelaxations } }, // ============================================================ - // TypeScript override block: relax tseslint v8 recommended back to - // the posture the legacy plugin:ghost/ts enforced. Must come AFTER the - // tseslint extends to win. + // TypeScript override block: must come AFTER tseslint extends to win. // ============================================================ { files: ['core/**/*.ts', '*.ts'], rules: { - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - 'prefer-const': 'off' + '@typescript-eslint/no-explicit-any': 'off', + ...tsLegacyRelaxations } }, // ============================================================ @@ -350,21 +355,14 @@ export default tseslint.config( rules: { ...js.configs.recommended.rules, // Tests historically extended ghost/test (= base + test-rules). - // Re-applying only the rules that the legacy config actually had: - curly: 'error', - 'dot-notation': 'error', - eqeqeq: ['error', 'always'], - 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], - 'no-eval': 'error', - 'no-useless-call': 'error', - 'no-console': 'error', - 'array-callback-return': 'error', - 'no-constructor-return': 'error', - 'no-promise-executor-return': 'error', + // Base rules come from correctnessRules (minus the ghost-filenames + // rule we replace with local-filenames below); test-rules are the + // workspace-specific extras after. + ...correctnessRules, + 'ghost/filenames/match-regex': 'off', 'no-unused-private-class-members': 'off', - ...mochaRulesOff, + ...mochaRulesOffForGhost, 'ghost/mocha/no-skipped-tests': 'error', - 'no-shadow': 'error', camelcase: 'off', 'no-prototype-builtins': 'off', 'no-unused-vars': ['error', { @@ -392,13 +390,8 @@ export default tseslint.config( files: ['**/*.ts'], extends: [...tseslint.configs.recommended], rules: { - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - 'prefer-const': 'off' + '@typescript-eslint/no-explicit-any': 'off', + ...tsLegacyRelaxations } }, // Test files override — must come AFTER the final relaxation so the @@ -409,13 +402,14 @@ export default tseslint.config( extends: [...tseslint.configs.recommended], rules: { '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - 'prefer-const': 'off' + '@typescript-eslint/no-explicit-any': 'off', + ...tsLegacyRelaxations } + }, + // CLI scripts and build tools intentionally write to console — must come + // after the base block so the no-console override wins. + { + files: ['bin/**/*.js', 'scripts/**/*.js'], + rules: {'no-console': 'off'} } ); diff --git a/ghost/i18n/eslint.config.mjs b/ghost/i18n/eslint.config.mjs index fd7e3888acc..9f9e5f56992 100644 --- a/ghost/i18n/eslint.config.mjs +++ b/ghost/i18n/eslint.config.mjs @@ -1,88 +1,30 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; +import {nodeLibConfig, noGhostIgnitionRequireRule} from '../../eslint.shared.mjs'; -import { - correctnessRules, - jsUnusedVarsRule, - localFilenamesPlugin, - mochaRulesOff -} from '../../eslint.shared.mjs'; - -const ghostI18nExtras = { - 'no-var': 'warn', - 'one-var': ['warn', 'never'], - 'ghost/node/no-restricted-require': ['warn', [ - { - name: 'ghost-ignition', - message: '@deprecated, please use @tryghost/errors, @tryghost/logging or @tryghost/debug. Config and Server are coming soon!' - } - ]], - 'ghost/ghost-custom/no-native-error': 'error', - 'ghost/ghost-custom/ghost-error-usage': 'error', - 'ghost/ghost-custom/ghost-tpl-usage': 'error', - // This workspace uses the local-filenames variant of the rule; turn off - // the eslint-plugin-ghost one that correctnessRules enables. - 'ghost/filenames/match-regex': 'off', - 'local-filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] -}; - -export default [ - { - ignores: ['build/**/*'] +export default await nodeLibConfig({ + // ghost/i18n is JS-only (CommonJS) and uses the local-filenames variant of + // match-regex instead of the ghost-plugin one. legacyLocalFilenames handles both. + typescript: false, + commonjs: true, + legacyLocalFilenames: true, + srcGlobs: ['*.js', 'lib/**/*.js'], + testGlobs: ['test/**/*.js'], + extraSrcRules: { + ...noGhostIgnitionRequireRule, + // Use the local-filenames variant (workspace-local plugin). The shared + // factory's legacyLocalFilenames flag turned off the ghost/filenames one + // for us already. + 'local-filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] }, - { - files: ['*.js', 'lib/**/*.js'], - ...js.configs.recommended, - languageOptions: { - ecmaVersion: 2022, - sourceType: 'commonjs', - globals: globals.node - }, - plugins: { - ghost: ghostPlugin, - 'local-filenames': localFilenamesPlugin - }, - rules: { - ...js.configs.recommended.rules, - ...correctnessRules, - ...jsUnusedVarsRule, - ...ghostI18nExtras - } - }, - { - files: ['lib/**/index.js', 'index.js'], - rules: { - 'max-lines': ['error', {skipBlankLines: true, skipComments: true, max: 50}] - } + extraTestRules: { + ...noGhostIgnitionRequireRule, + 'local-filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + 'ghost/ghost-custom/node-assert-strict': 'error' }, - { - files: ['test/**/*.js'], - ...js.configs.recommended, - languageOptions: { - ecmaVersion: 2022, - sourceType: 'commonjs', - globals: { - ...globals.node, - ...globals.mocha, - ...globals.vitest, - vi: 'readonly', - beforeAll: 'readonly', - should: 'readonly', - sinon: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin, - 'local-filenames': localFilenamesPlugin - }, - rules: { - ...js.configs.recommended.rules, - ...correctnessRules, - ...jsUnusedVarsRule, - ...ghostI18nExtras, - ...mochaRulesOff(ghostPlugin), - 'ghost/ghost-custom/node-assert-strict': 'error' + extraBlocks: [ + { + // Keep the index entry points small — they're public surface. + files: ['lib/**/index.js', 'index.js'], + rules: {'max-lines': ['error', {skipBlankLines: true, skipComments: true, max: 50}]} } - } -]; + ] +}); diff --git a/ghost/parse-email-address/eslint.config.mjs b/ghost/parse-email-address/eslint.config.mjs index 6ac6a86fd2b..d70bbc5ca00 100644 --- a/ghost/parse-email-address/eslint.config.mjs +++ b/ghost/parse-email-address/eslint.config.mjs @@ -1,70 +1,3 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import ghostPlugin from 'eslint-plugin-ghost'; -import tseslint from 'typescript-eslint'; +import {nodeLibConfig} from '../../eslint.shared.mjs'; -import { - correctnessRules, - mochaRulesOff, - tsUnusedVarsRule -} from '../../eslint.shared.mjs'; - -const ghostParseEmailExtras = { - 'no-var': 'warn', - 'one-var': ['warn', 'never'], - 'ghost/ghost-custom/no-native-error': 'error', - 'ghost/ghost-custom/ghost-error-usage': 'error', - 'ghost/ghost-custom/ghost-tpl-usage': 'error' -}; - -export default tseslint.config( - { - ignores: ['build/**/*'] - }, - { - files: ['src/**/*.ts'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: globals.node - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...js.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...ghostParseEmailExtras, - 'no-undef': 'off' - } - }, - { - files: ['test/**/*.ts'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.node, - ...globals.mocha, - ...globals.vitest, - vi: 'readonly', - should: 'readonly', - sinon: 'readonly' - } - }, - plugins: { - ghost: ghostPlugin - }, - rules: { - ...js.configs.recommended.rules, - ...correctnessRules, - ...tsUnusedVarsRule, - ...ghostParseEmailExtras, - ...mochaRulesOff(ghostPlugin), - 'no-undef': 'off' - } - } -); +export default await nodeLibConfig(); diff --git a/package.json b/package.json index 7265da26136..99af1043da0 100644 --- a/package.json +++ b/package.json @@ -78,12 +78,18 @@ "test:release": "node --test 'scripts/test/*.js'" }, "devDependencies": { + "@eslint/js": "catalog:", "@playwright/test": "catalog:", "@secretlint/secretlint-rule-pattern": "13.0.2", "@secretlint/secretlint-rule-preset-recommend": "13.0.2", "eslint": "catalog:", "eslint-plugin-ghost": "catalog:", + "eslint-plugin-i18next": "6.1.4", "eslint-plugin-react": "catalog:", + "eslint-plugin-react-hooks": "catalog:", + "eslint-plugin-react-refresh": "catalog:", + "eslint-plugin-storybook": "10.4.4", + "eslint-plugin-tailwindcss": "catalog:", "globals": "catalog:", "husky": "9.1.7", "jsonc-parser": "catalog:", @@ -94,6 +100,7 @@ "secretlint": "13.0.2", "semver": "7.8.4", "typescript": "catalog:", + "typescript-eslint": "catalog:", "vitest": "catalog:" }, "nx": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 878da68caf7..3c32aa1e828 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,6 +369,9 @@ importers: .: devDependencies: + '@eslint/js': + specifier: 'catalog:' + version: 9.39.4 '@playwright/test': specifier: 'catalog:' version: 1.60.0 @@ -384,9 +387,24 @@ importers: eslint-plugin-ghost: specifier: 'catalog:' version: 3.5.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0) + eslint-plugin-i18next: + specifier: 6.1.4 + version: 6.1.4 eslint-plugin-react: specifier: 'catalog:' version: 7.37.5(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)) + eslint-plugin-react-hooks: + specifier: 'catalog:' + version: 5.2.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)) + eslint-plugin-react-refresh: + specifier: 'catalog:' + version: 0.5.2(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)) + eslint-plugin-storybook: + specifier: 10.4.4 + version: 10.4.4(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + eslint-plugin-tailwindcss: + specifier: 'catalog:' + version: 4.0.0-beta.0(tailwindcss@4.2.2) globals: specifier: 'catalog:' version: 17.6.0 @@ -420,6 +438,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 + typescript-eslint: + specifier: 'catalog:' + version: 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) vitest: specifier: 'catalog:' version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -528,7 +549,7 @@ importers: version: 4.2.2 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -640,7 +661,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -668,7 +689,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-plugin-ghost: specifier: 'catalog:' - version: 3.5.0(eslint@9.39.4(jiti@2.7.0)) + version: 3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) globals: specifier: 'catalog:' version: 17.6.0 @@ -855,7 +876,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) validator: specifier: 'catalog:' version: 13.12.0 @@ -973,7 +994,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1130,7 +1151,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-plugin-ghost: specifier: 'catalog:' - version: 3.5.0(eslint@9.39.4(jiti@2.7.0)) + version: 3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: specifier: 'catalog:' version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -1154,7 +1175,7 @@ importers: version: 4.2.2 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1412,7 +1433,7 @@ importers: version: 17.0.2(react@17.0.2) typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1527,7 +1548,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-plugin-ghost: specifier: 'catalog:' - version: 3.5.0(eslint@9.39.4(jiti@2.7.0)) + version: 3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: specifier: 'catalog:' version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -1554,7 +1575,7 @@ importers: version: 4.2.2 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1744,7 +1765,7 @@ importers: version: 0.5.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-storybook: specifier: 10.4.4 - version: 10.4.4(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@5.5.0)(typescript@5.9.3) + version: 10.4.4(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) eslint-plugin-tailwindcss: specifier: 'catalog:' version: 4.0.0-beta.0(tailwindcss@4.2.2) @@ -1783,7 +1804,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1874,7 +1895,7 @@ importers: version: 3.4.19(tsx@4.22.4)(yaml@2.9.0) typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -2059,7 +2080,7 @@ importers: version: 4.2.2 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: 'catalog:' version: 7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -2128,7 +2149,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) ghost/admin: dependencies: @@ -2648,7 +2669,7 @@ importers: version: 1.0.6 '@tryghost/nodemailer': specifier: 2.3.0 - version: 2.3.0(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) + version: 2.3.0(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) '@tryghost/nql': specifier: 'catalog:' version: 0.13.1(supports-color@5.5.0) @@ -3171,7 +3192,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 8.58.0 - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) validator: specifier: 'catalog:' version: 13.12.0 @@ -3206,7 +3227,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-plugin-ghost: specifier: 3.5.0 - version: 3.5.0(eslint@9.39.4(jiti@2.7.0)) + version: 3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) fs-extra: specifier: 'catalog:' version: 11.3.5 @@ -3246,7 +3267,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-plugin-ghost: specifier: 3.5.0 - version: 3.5.0(eslint@9.39.4(jiti@2.7.0)) + version: 3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) globals: specifier: 17.6.0 version: 17.6.0 @@ -3258,7 +3279,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 8.58.0 - version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vitest: specifier: 'catalog:' version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.21)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.19.21)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -29175,13 +29196,13 @@ snapshots: '@tryghost/mw-vhost@1.0.6': {} - '@tryghost/nodemailer@2.3.0(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8)': + '@tryghost/nodemailer@2.3.0(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8)': dependencies: '@aws-sdk/client-sesv2': 3.1067.0 '@tryghost/errors': 1.3.13 '@tryghost/tpl': 2.3.0 nodemailer: 8.0.11 - nodemailer-mailgun-transport: 2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) + nodemailer-mailgun-transport: 2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) nodemailer-stub-transport: 1.1.0 transitivePeerDependencies: - arc-templates @@ -29991,10 +30012,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/utils': 8.49.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) @@ -30023,10 +30044,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) '@typescript-eslint/utils': 8.49.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) @@ -30039,15 +30060,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 - eslint: 9.39.4(jiti@2.7.0) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 9.39.4(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -30055,15 +30076,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.58.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -30071,12 +30092,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.39.4(jiti@2.7.0) @@ -30147,6 +30168,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.0 @@ -30186,7 +30219,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.60.0(supports-color@5.5.0)(typescript@5.9.3)': + '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) '@typescript-eslint/types': 8.60.0 @@ -30279,7 +30312,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) @@ -30344,9 +30389,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.60.0(supports-color@5.5.0)(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.60.0(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) '@typescript-eslint/types': 8.60.0 '@typescript-eslint/visitor-keys': 8.60.0 @@ -30403,6 +30448,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -30414,12 +30470,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.60.0 '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: @@ -33565,13 +33632,15 @@ snapshots: ora: 3.4.0 through2: 3.0.2 - consolidate@0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8): + consolidate@0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): dependencies: bluebird: 3.7.2 optionalDependencies: babel-core: 6.26.3 handlebars: 4.7.9 lodash: 4.18.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) underscore: 1.13.8 consolidate@1.0.4(@babel/core@7.29.7)(handlebars@4.7.9)(lodash@4.18.1)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): @@ -35470,7 +35539,7 @@ snapshots: - '@glint/template' - supports-color - ember-eslint-parser@0.5.13(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + ember-eslint-parser@0.5.13(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): dependencies: '@babel/core': 7.29.7 '@babel/eslint-parser': 7.28.6(@babel/core@7.29.7)(eslint@9.39.4(jiti@1.21.7)) @@ -35482,7 +35551,7 @@ snapshots: mathml-tag-names: 2.1.3 svg-tags: 1.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint - typescript @@ -35537,22 +35606,6 @@ snapshots: - eslint - typescript - ember-eslint-parser@0.5.13(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): - dependencies: - '@babel/eslint-parser': 7.28.6(@babel/core@7.29.7)(eslint@9.39.4(jiti@2.7.0)) - '@glimmer/syntax': 0.95.0 - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - content-tag: 2.0.3 - eslint-scope: 7.2.2 - html-tags: 3.3.1 - mathml-tag-names: 2.1.3 - svg-tags: 1.0.0 - optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) - transitivePeerDependencies: - - eslint - - typescript - ember-exam@6.0.1(ember-mocha@0.16.2(@babel/core@7.29.7))(supports-color@5.5.0): dependencies: '@embroider/macros': 0.29.0 @@ -36411,11 +36464,11 @@ snapshots: eslint: 9.39.4(jiti@2.7.0) eslint-rule-composer: 0.3.0 - eslint-plugin-ember@12.7.5(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-ember@12.7.5(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): dependencies: '@ember-data/rfc395-data': 0.0.4 css-tree: 3.2.1 - ember-eslint-parser: 0.5.13(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + ember-eslint-parser: 0.5.13(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) ember-rfc176-data: 0.3.18 eslint: 9.39.4(jiti@1.21.7) eslint-utils: 3.0.0(eslint@9.39.4(jiti@1.21.7)) @@ -36425,7 +36478,7 @@ snapshots: requireindex: 1.2.0 snake-case: 3.0.4 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - '@babel/core' - typescript @@ -36487,25 +36540,6 @@ snapshots: - '@babel/core' - typescript - eslint-plugin-ember@12.7.5(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): - dependencies: - '@ember-data/rfc395-data': 0.0.4 - css-tree: 3.2.1 - ember-eslint-parser: 0.5.13(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - ember-rfc176-data: 0.3.18 - eslint: 9.39.4(jiti@2.7.0) - eslint-utils: 3.0.0(eslint@9.39.4(jiti@2.7.0)) - estraverse: 5.3.0 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - requireindex: 1.2.0 - snake-case: 3.0.4 - optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) - transitivePeerDependencies: - - '@babel/core' - - typescript - eslint-plugin-es-x@7.8.0(eslint@9.39.4(jiti@1.21.7)): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) @@ -36554,10 +36588,10 @@ snapshots: eslint-plugin-ghost@3.5.0(@babel/core@7.29.7)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@kapouer/eslint-plugin-no-return-in-loop': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.4(jiti@1.21.7) - eslint-plugin-ember: 12.7.5(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-ember: 12.7.5(@babel/core@7.29.7)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-filenames-ts: 1.3.2(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-mocha: 7.0.1(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-n: 17.24.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) @@ -36602,23 +36636,6 @@ snapshots: - '@babel/core' - supports-color - eslint-plugin-ghost@3.5.0(eslint@9.39.4(jiti@2.7.0)): - dependencies: - '@kapouer/eslint-plugin-no-return-in-loop': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-ember: 12.7.5(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - eslint-plugin-filenames-ts: 1.3.2(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-mocha: 7.0.1(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-n: 17.24.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - eslint-plugin-sort-imports-es6-autofix: 0.6.0(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-unicorn: 42.0.0(eslint@9.39.4(jiti@2.7.0)) - typescript: 5.9.3 - transitivePeerDependencies: - - '@babel/core' - - supports-color - eslint-plugin-i18next@6.1.4: dependencies: lodash: 4.18.1 @@ -36694,10 +36711,18 @@ snapshots: eslint: 9.39.4(jiti@2.7.0) globals: 17.6.0 + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) @@ -36780,9 +36805,18 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-storybook@10.4.4(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@5.5.0)(typescript@5.9.3): + eslint-plugin-storybook@10.4.4(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + storybook: 10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-storybook@10.4.4(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) storybook: 10.4.4(@testing-library/dom@9.3.4)(@types/react@18.3.31)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: @@ -41968,9 +42002,9 @@ snapshots: node@runtime:22.18.0: {} - nodemailer-mailgun-transport@2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8): + nodemailer-mailgun-transport@2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): dependencies: - consolidate: 0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) + consolidate: 0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) form-data: 4.0.6 mailgun.js: 8.2.2 transitivePeerDependencies: @@ -46428,7 +46462,7 @@ snapshots: typescript-eslint@8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) @@ -46437,9 +46471,20 @@ snapshots: transitivePeerDependencies: - supports-color - typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3): + typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.7.0)(supports-color@5.5.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0)(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(supports-color@5.5.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)