Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
102 changes: 5 additions & 97 deletions apps/activitypub/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,6 @@
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
});
2 changes: 1 addition & 1 deletion apps/activitypub/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tryghost/activitypub",
"type": "module",
"version": "3.1.46",
"version": "3.1.50",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
39 changes: 13 additions & 26 deletions apps/admin-toolbar/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +17 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

strictLinterOptions currently disables the strict behavior.

Line 18 applies strictLinterOptions, but the shared preset currently sets reportUnusedDisableDirectives to 'off', so stale eslint-disable directives will not fail CI as intended.

🔧 Proposed fix (in eslint.shared.mjs)
 export const strictLinterOptions = {
     linterOptions: {
-        reportUnusedDisableDirectives: 'off'
+        reportUnusedDisableDirectives: 'error'
     }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin-toolbar/eslint.config.js` around lines 17 - 18, The
`strictLinterOptions` being applied in eslint.config.js is not enforcing strict
behavior because in the `eslint.shared.mjs` file, the
`reportUnusedDisableDirectives` setting is configured as `'off'`. To fix this,
locate the `reportUnusedDisableDirectives` property in `eslint.shared.mjs` and
change its value from `'off'` to `'error'` (or `'warn'`) so that stale
eslint-disable directives are properly caught and fail CI as intended.

},
{
files: ['src/**/*.js'],
...js.configs.recommended,
Expand All @@ -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
}
},
{
Expand All @@ -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
}
}
];
101 changes: 6 additions & 95 deletions apps/admin-x-design-system/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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'
});
91 changes: 8 additions & 83 deletions apps/admin-x-framework/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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'
}
);
});
Loading
Loading