@ztd-me/eslint 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/app-router.d.ts +4 -0
- package/dist/app-router.js +113 -0
- package/dist/index.js +31 -6
- package/dist/options.d.ts +4 -0
- package/docs/framework-compatibility.md +23 -0
- package/docs/pnpm-policy.md +29 -0
- package/docs/publishing.md +9 -7
- package/docs/rule-coverage.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,15 +45,24 @@ Type-aware checking is on by default, using `tsconfig.json`. Supply an existing
|
|
|
45
45
|
|
|
46
46
|
The parser uses the explicit TS project, with `.vue` as an extra file extension and `vue-eslint-parser` as the SFC outer parser. Typed rules cover Vue scripts as well as TS and TSX. Vue template expressions use Vue rules and the custom array visitor; TypeScript rules do not type-check template expressions.
|
|
47
47
|
|
|
48
|
+
Since 0.1.2, the explicit project settings apply only to actual type-aware source files. Markdown TS/TSX
|
|
49
|
+
code fences and Astro's virtual TS snippets use the existing upstream syntax scope, without asking the
|
|
50
|
+
application TS config to include generated files such as `README.md/0_0.tsx`. Markdown markup,
|
|
51
|
+
TypeScript syntax, React semantics, array formatting and other applicable checks remain enabled.
|
|
52
|
+
Actual application files still require the selected strict project; files outside it fail rather than
|
|
53
|
+
receiving a syntax-only fallback. The same virtual-file boundary applies to typed Vitest rules.
|
|
54
|
+
|
|
48
55
|
For a JavaScript-only scope, use `typescript: false`. This turns off TypeScript parsing and rules; it does not lower JS complexity, length, or correctness standards. There is no syntax-only TypeScript or `strictTypes: false` mode. Run `tsc --noEmit` as a separate compiler gate; `exactOptionalPropertyTypes`, `noPropertyAccessFromIndexSignature`, and `useUnknownInCatchVariables` are also recommended compiler settings.
|
|
49
56
|
|
|
50
57
|
## Framework options
|
|
51
58
|
|
|
52
59
|
| Option | Default | Behavior |
|
|
53
60
|
| --- | --- | --- |
|
|
54
|
-
| `react` | `false` | `true`, or `{ files, compiler, experimental }`; includes hooks in `.ts` as well as JSX |
|
|
61
|
+
| `react` | `false` | `true`, or `{ files, compiler, experimental, framework, appDir }`; includes hooks in `.ts` as well as JSX |
|
|
55
62
|
| `react.compiler` | `false` | Adds four official React Compiler checks |
|
|
56
63
|
| `react.experimental` | `false` | Adds experimental fetch cleanup checking |
|
|
64
|
+
| `react.framework` | unset | Explicit `'next'` or `'vinext'` App Router server export integration |
|
|
65
|
+
| `react.appDir` | `app`, `src/app` | One literal relative app directory, replacing the defaults; requires `framework` |
|
|
57
66
|
| `vue` | `false` | `true`, or `{ files }`; Vue 3 correctness and accessibility are strict |
|
|
58
67
|
| `test` | `false` | Explicitly enable the researched Vitest profile; do not enable for Node test or Jest |
|
|
59
68
|
| `rules` | `{}` | Deliberate final overrides; subsequent flat configs run last |
|
|
@@ -62,6 +71,26 @@ JSX accessibility is disabled and `eslint-plugin-jsx-a11y` is not installed: its
|
|
|
62
71
|
|
|
63
72
|
The selected React Hooks checks use one owner per behavior: `react/*` for hooks and effects, `react-hooks/*` only for globals, immutability, refs, and optional Compiler checks. The experimental fetch rule is off until explicitly selected. React version migration opinions are inherited from antfu rather than strengthened as correctness rules.
|
|
64
73
|
|
|
74
|
+
### Next.js and vinext App Router
|
|
75
|
+
|
|
76
|
+
Since 0.1.1, explicitly select the framework to allow its server route exports:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import ztd from '@ztd-me/eslint'
|
|
80
|
+
|
|
81
|
+
export default ztd({
|
|
82
|
+
react: { framework: 'vinext' },
|
|
83
|
+
// For Next.js use framework: 'next'.
|
|
84
|
+
// For a monorepo add appDir: 'apps/web/app'.
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`ztd/app-router-exports` adapts the pinned upstream Fast Refresh export rule only for `page` and `layout` files with `.js`, `.jsx`, `.ts`, or `.tsx` extensions under the selected app directory. Nested route groups, dynamic segments and parallel slots are included; private `_` directories are excluded. `react.files` further restricts that scope. Framework selection is explicit and independent of installed packages.
|
|
89
|
+
|
|
90
|
+
Server route files may export `metadata`, `generateMetadata`, `viewport`, `generateViewport`, `generateStaticParams`, `dynamic`, `dynamicParams`, `revalidate`, `fetchCache`, `runtime`, `preferredRegion`, and `maxDuration`. Next.js also recognizes `instant` and `prefetch`; vinext does not inherit these Next-specific exports. A file with a `'use client'` directive receives no such exemptions. Other named helpers, arbitrary constants, wildcard exports, ordinary components and non-route files retain Fast Refresh errors. Primitive constants are also checked, independent of installed Vite/Next.js packages.
|
|
91
|
+
|
|
92
|
+
This rule validates the component export boundary, not framework export values or every framework restriction. The framework build must still validate metadata types, mutually exclusive `metadata`/`generateMetadata`, route configuration and cache-mode restrictions. Pages Router and metadata image/sitemap files do not receive this App Router page/layout profile. [Compatibility evidence and upstream sources](docs/framework-compatibility.md) document the scope and tested versions.
|
|
93
|
+
|
|
65
94
|
## Limits and arrays
|
|
66
95
|
|
|
67
96
|
| Measure | Inclusive maximum / threshold |
|
|
@@ -112,6 +141,6 @@ pnpm run check
|
|
|
112
141
|
|
|
113
142
|
The checks build JS/declarations, type-check the public API, self-lint source/tests/scripts, exercise real TS/React/Vue fixtures, check all numeric boundaries and all selected catalog options, and install/import/type-check an independently packed artifact with pnpm. Negative fixtures are checked by the test harness, not included in ordinary self-lint. The test modules have reasoned local exceptions for Node's asynchronous ESM setup and imports of the distribution being tested; safety and size limits remain active.
|
|
114
143
|
|
|
115
|
-
[Publishing instructions](docs/publishing.md) describe the stage-only GitHub workflow and the user-controlled promotion. Staging is not a public release. After promotion, run `node scripts/registry-smoke.mjs 0.1.
|
|
144
|
+
[Publishing instructions](docs/publishing.md) describe the stage-only GitHub workflow and the user-controlled promotion. Staging is not a public release. After promotion, run `node scripts/registry-smoke.mjs 0.1.2` to install the exact public version in a fresh pnpm project and verify imports, declarations and lint behavior. Registry installation follows pnpm's supply-chain policies; a policy rejection is a blocker, not permission to disable the policy.
|
|
116
145
|
|
|
117
146
|
The repository has not granted an open-source license; package metadata is `UNLICENSED` pending that separate decision.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import refresh from 'eslint-plugin-react-refresh';
|
|
2
|
+
import { isRecord } from './array-simple.js';
|
|
3
|
+
function isRuleModule(value) {
|
|
4
|
+
return isRecord(value) && typeof value.create === 'function' && isRecord(value.meta);
|
|
5
|
+
}
|
|
6
|
+
function isRuleContext(value) {
|
|
7
|
+
return isRecord(value) && typeof value.report === 'function' && typeof value.filename === 'string'
|
|
8
|
+
&& isRecord(value.sourceCode) && Array.isArray(value.options);
|
|
9
|
+
}
|
|
10
|
+
function resolveFramework(value) {
|
|
11
|
+
if (value === undefined || value === 'next' || value === 'vinext') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
throw new Error('@ztd-me/eslint: unsupported react.framework');
|
|
15
|
+
}
|
|
16
|
+
function loadRefreshRule() {
|
|
17
|
+
const candidate = refresh.rules['only-export-components'];
|
|
18
|
+
if (!isRuleModule(candidate)) {
|
|
19
|
+
throw new Error('@ztd-me/eslint: incompatible react-refresh rule implementation');
|
|
20
|
+
}
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
const refreshRule = loadRefreshRule();
|
|
24
|
+
const serverExports = [
|
|
25
|
+
'metadata',
|
|
26
|
+
'generateMetadata',
|
|
27
|
+
'viewport',
|
|
28
|
+
'generateViewport',
|
|
29
|
+
'generateStaticParams',
|
|
30
|
+
'dynamic',
|
|
31
|
+
'dynamicParams',
|
|
32
|
+
'revalidate',
|
|
33
|
+
'fetchCache',
|
|
34
|
+
'runtime',
|
|
35
|
+
'preferredRegion',
|
|
36
|
+
'maxDuration',
|
|
37
|
+
];
|
|
38
|
+
export const appRouterExports = {
|
|
39
|
+
meta: {
|
|
40
|
+
...refreshRule.meta,
|
|
41
|
+
schema: [
|
|
42
|
+
{
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: { framework: { enum: ['next', 'vinext'] } },
|
|
45
|
+
required: ['framework'],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
create(context) {
|
|
51
|
+
const isClient = context.sourceCode.ast.body.some(statement => (statement.type === 'ExpressionStatement' && 'directive' in statement && statement.directive === 'use client'));
|
|
52
|
+
const option = context.options[0];
|
|
53
|
+
const framework = resolveFramework(isRecord(option) ? option.framework : undefined);
|
|
54
|
+
const allowExportNames = isClient
|
|
55
|
+
? []
|
|
56
|
+
: [
|
|
57
|
+
...serverExports,
|
|
58
|
+
...(framework === 'next'
|
|
59
|
+
? ['instant', 'prefetch']
|
|
60
|
+
: []),
|
|
61
|
+
];
|
|
62
|
+
const adapted = Object.create(context, {
|
|
63
|
+
options: { value: [
|
|
64
|
+
{ allowExportNames, allowConstantExport: false },
|
|
65
|
+
] },
|
|
66
|
+
filename: { value: context.filename.replace(/\.[jt]s$/u, extension => `${extension}x`) },
|
|
67
|
+
});
|
|
68
|
+
if (!isRuleContext(adapted)) {
|
|
69
|
+
throw new Error('@ztd-me/eslint: incompatible ESLint rule context');
|
|
70
|
+
}
|
|
71
|
+
return refreshRule.create(adapted);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
function appDirectories(options) {
|
|
75
|
+
if (options.appDir === undefined) {
|
|
76
|
+
return ['app', 'src/app'];
|
|
77
|
+
}
|
|
78
|
+
if (typeof options.appDir !== 'string') {
|
|
79
|
+
throw new TypeError('@ztd-me/eslint: react.appDir must be a literal relative directory');
|
|
80
|
+
}
|
|
81
|
+
const directory = options.appDir.replaceAll('\\', '/').replace(/\/$/u, '');
|
|
82
|
+
const hasTraversal = directory.split('/').some(part => part === '' || part === '.' || part === '..');
|
|
83
|
+
if (/[:*?{}[\]()!]/u.test(directory) || hasTraversal) {
|
|
84
|
+
throw new Error('@ztd-me/eslint: react.appDir must be a literal relative directory without globs or traversal');
|
|
85
|
+
}
|
|
86
|
+
return [directory];
|
|
87
|
+
}
|
|
88
|
+
export function appRouterConfigs(options) {
|
|
89
|
+
const framework = resolveFramework(options.framework);
|
|
90
|
+
if (framework === undefined) {
|
|
91
|
+
if (options.appDir !== undefined) {
|
|
92
|
+
throw new Error('@ztd-me/eslint: react.appDir requires an explicit react.framework');
|
|
93
|
+
}
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const directories = appDirectories(options);
|
|
97
|
+
const files = directories.map(directory => `${directory}/**/{page,layout}.{js,jsx,ts,tsx}`);
|
|
98
|
+
const rules = {
|
|
99
|
+
'react-refresh/only-export-components': 'off',
|
|
100
|
+
'ztd/app-router-exports': [
|
|
101
|
+
'error',
|
|
102
|
+
{ framework },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
name: 'ztd/react-app-router',
|
|
108
|
+
files: options.files === undefined ? files : options.files.flatMap(scope => files.map(file => [scope, file])),
|
|
109
|
+
ignores: directories.map(directory => `${directory}/**/_*/**`),
|
|
110
|
+
rules,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import antfu, { GLOB_SRC, GLOB_TESTS, GLOB_TS, GLOB_TSX } from '@antfu/eslint-config';
|
|
2
2
|
import hooks from 'eslint-plugin-react-hooks';
|
|
3
3
|
import sonar from 'eslint-plugin-sonarjs';
|
|
4
|
+
import { appRouterConfigs, appRouterExports } from './app-router.js';
|
|
4
5
|
import { arrayLayout } from './array-layout.js';
|
|
5
6
|
import { isRecord } from './array-simple.js';
|
|
6
7
|
import { reactRules } from './rules/react.js';
|
|
@@ -11,6 +12,7 @@ import { typedRules } from './rules/typed.js';
|
|
|
11
12
|
import { vueA11yRules } from './rules/vue-a11y.js';
|
|
12
13
|
import { vueRules } from './rules/vue.js';
|
|
13
14
|
import { resolveTypeScript } from './typescript.js';
|
|
15
|
+
const typeAwareIgnores = ['**/*.md/**', '**/*.astro/*.ts'];
|
|
14
16
|
function shared(sfcFiles) {
|
|
15
17
|
return {
|
|
16
18
|
name: 'ztd/shared',
|
|
@@ -18,7 +20,10 @@ function shared(sfcFiles) {
|
|
|
18
20
|
GLOB_SRC,
|
|
19
21
|
...sfcFiles,
|
|
20
22
|
],
|
|
21
|
-
plugins: { sonarjs: sonar, ztd: { rules: {
|
|
23
|
+
plugins: { sonarjs: sonar, ztd: { rules: {
|
|
24
|
+
'array-layout': arrayLayout,
|
|
25
|
+
'app-router-exports': appRouterExports,
|
|
26
|
+
} } },
|
|
22
27
|
linterOptions: { reportUnusedDisableDirectives: 'error' },
|
|
23
28
|
rules: {
|
|
24
29
|
...sharedRules,
|
|
@@ -36,7 +41,7 @@ function shared(sfcFiles) {
|
|
|
36
41
|
},
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
|
-
function typed(files) {
|
|
44
|
+
function typed(files, parserOptions) {
|
|
40
45
|
return [
|
|
41
46
|
{
|
|
42
47
|
name: 'ztd/typescript-syntax',
|
|
@@ -55,7 +60,9 @@ function typed(files) {
|
|
|
55
60
|
{
|
|
56
61
|
name: 'ztd/typescript-typed',
|
|
57
62
|
files,
|
|
58
|
-
ignores:
|
|
63
|
+
ignores: typeAwareIgnores,
|
|
64
|
+
// antfu applies its general parserOptions to both parsers; keep project settings in this typed scope.
|
|
65
|
+
languageOptions: { parserOptions },
|
|
59
66
|
rules: {
|
|
60
67
|
...typedRules,
|
|
61
68
|
'require-await': 'off',
|
|
@@ -91,6 +98,7 @@ function react(options) {
|
|
|
91
98
|
])),
|
|
92
99
|
},
|
|
93
100
|
},
|
|
101
|
+
...appRouterConfigs(settings),
|
|
94
102
|
];
|
|
95
103
|
}
|
|
96
104
|
function vue(options) {
|
|
@@ -128,6 +136,7 @@ function vitest(enabled, tsFiles) {
|
|
|
128
136
|
configs.push({
|
|
129
137
|
name: 'ztd/vitest-typed',
|
|
130
138
|
files: tsFiles.flatMap(file => GLOB_TESTS.map(testFile => [file, testFile])),
|
|
139
|
+
ignores: typeAwareIgnores,
|
|
131
140
|
rules: { 'ts/unbound-method': 'off', 'test/unbound-method': [
|
|
132
141
|
'error',
|
|
133
142
|
{ ignoreStatic: false },
|
|
@@ -147,7 +156,17 @@ function antfuReact(options, tsFiles) {
|
|
|
147
156
|
return false;
|
|
148
157
|
}
|
|
149
158
|
const files = typeof options === 'object' ? options.files ?? [GLOB_SRC] : [GLOB_SRC];
|
|
150
|
-
|
|
159
|
+
const overrides = {
|
|
160
|
+
'react-refresh/only-export-components': [
|
|
161
|
+
'error',
|
|
162
|
+
{ allowConstantExport: false, allowExportNames: [] },
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
files,
|
|
167
|
+
filesTypeAware: files.flatMap(file => tsFiles.map(tsFile => [file, tsFile])),
|
|
168
|
+
overrides,
|
|
169
|
+
};
|
|
151
170
|
}
|
|
152
171
|
function antfuVue(files) {
|
|
153
172
|
return files.length === 0 ? false : { a11y: true, vueVersion: 3, files };
|
|
@@ -183,12 +202,18 @@ export async function createConfig(options = {}, ...localConfigs) {
|
|
|
183
202
|
shared(sfcFiles),
|
|
184
203
|
...react(enableReact),
|
|
185
204
|
...vue(enableVue),
|
|
186
|
-
...(
|
|
205
|
+
...(tsOptions === false ? [] : typed(tsFiles, tsOptions.parserOptions)),
|
|
187
206
|
...vitest(test, typescript === false ? [] : tsFiles),
|
|
188
207
|
];
|
|
189
208
|
return await antfu({
|
|
190
209
|
...base,
|
|
191
|
-
typescript: tsOptions === false
|
|
210
|
+
typescript: tsOptions === false
|
|
211
|
+
? false
|
|
212
|
+
: {
|
|
213
|
+
tsconfigPath: tsOptions.tsconfigPath,
|
|
214
|
+
filesTypeAware: tsFiles,
|
|
215
|
+
ignoresTypeAware: typeAwareIgnores,
|
|
216
|
+
},
|
|
192
217
|
react: antfuReact(enableReact, tsFiles),
|
|
193
218
|
vue: antfuVue(sfcFiles),
|
|
194
219
|
test,
|
package/dist/options.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface ReactOptions {
|
|
|
13
13
|
compiler?: boolean;
|
|
14
14
|
/** Opt in to the experimental fetch cleanup check. */
|
|
15
15
|
experimental?: boolean;
|
|
16
|
+
/** Explicit App Router integration. Defaults to framework-neutral React. */
|
|
17
|
+
framework?: 'next' | 'vinext';
|
|
18
|
+
/** Literal app directory relative to the config root. Default: app and src/app. Requires framework. */
|
|
19
|
+
appDir?: string;
|
|
16
20
|
}
|
|
17
21
|
export interface VueOptions {
|
|
18
22
|
/** Vue 3 SFCs. Default: **\/*.vue. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# App Router export compatibility
|
|
2
|
+
|
|
3
|
+
Version 0.1.0 reports `react-refresh/only-export-components` on an otherwise valid vinext server layout exporting `metadata` beside its default component. This was reproduced from the public npm installation with ESLint 10.11.0 and TypeScript 6.0.3; the TSX file was included in the strict project and produced a real rule error rather than an ignored-file diagnostic.
|
|
4
|
+
|
|
5
|
+
Antfu 9.5.1 detects the package named `next`, but not `vinext`. Its Next export allowances apply to all React files, and installed Vite enables primitive constant exports. Version 0.1.1 makes the ordinary React export rule deterministic and introduces explicit `react.framework: 'next' | 'vinext'`, with an optional literal relative `appDir`. The selected research rules and numerical limits remain unchanged.
|
|
6
|
+
|
|
7
|
+
## Framework contract
|
|
8
|
+
|
|
9
|
+
[Next.js metadata documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) restricts metadata exports to server page/layout modules. [Route segment configuration](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) supplies the other recognized route exports. Older dynamic/revalidation/cache options remain framework-mode dependent; the Next.js compiler owns those restrictions. The current Next.js documentation also defines the `instant` and `prefetch` route exports; they are specific to the Next profile.
|
|
10
|
+
|
|
11
|
+
[Next.js Fast Refresh documentation](https://nextjs.org/docs/architecture/fast-refresh) explains the behavior of ordinary modules exporting both components and other values. The [upstream React Refresh rule](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) offers framework export allowances, but a global allowance does not prove that metadata belongs in arbitrary components or client modules.
|
|
12
|
+
|
|
13
|
+
Vinext was inspected at upstream commit `e5ada16269f18317aad9c9070d9ff64f62699e88`, package version 1.0.1. Its [metadata implementation](https://github.com/cloudflare/vinext/blob/e5ada16269f18317aad9c9070d9ff64f62699e88/packages/vinext/src/shims/metadata.tsx) handles layout/page metadata in server rendering. Its [development route handling](https://github.com/cloudflare/vinext/blob/e5ada16269f18317aad9c9070d9ff64f62699e88/packages/vinext/src/server/dev-route-files.ts) recognizes app page/layout files and excludes private directories. Vinext's segment configuration and prefetch policy do not establish support for Next's per-segment `prefetch` export.
|
|
14
|
+
|
|
15
|
+
## Validation
|
|
16
|
+
|
|
17
|
+
Package tests exercise both framework profiles with real typed TSX fixtures; root and `src/app` paths; route groups, dynamic segments and parallel slots; explicit monorepo directories; React file-scope intersections; plain `.js` routes; and client directives parsed by both JS and TypeScript parsers. Negative cases retain errors for ordinary components, private/non-route files, client metadata/constants, arbitrary helper exports and unsupported framework names/scopes. The packed consumer checks the new API declarations and server/ordinary/client behavior independently of repository imports.
|
|
18
|
+
|
|
19
|
+
The integration harness repeatedly lints the same TS files through different ESLint instances, so it explicitly requests the parser's watch-program mode. A fresh child process separately validates the immutable single-run CI mode. This follows the parser's [program-management distinction](https://typescript-eslint.io/packages/parser/#disallowautomaticsingleruninference); no type-aware rule or consumer parser setting is disabled.
|
|
20
|
+
|
|
21
|
+
An isolated project installed the actual published vinext 1.0.1, Vite 8.3.2, `@vitejs/plugin-react` 6.1.1, `@vitejs/plugin-rsc` 0.5.35 and React 19.2.6 using pnpm 11.19.0. A server layout's metadata appeared in the SSR title, and editing the metadata changed the rendered title without restarting the dev server. This verifies server metadata handling and development invalidation; it does not claim a browser client-state preservation test or a complete Next.js/vinext application build.
|
|
22
|
+
|
|
23
|
+
The wrapper delegates component/export analysis to the pinned React Refresh rule 0.5.7. It recognizes a `'use client'` directive before selecting any server allowances and scans the selected `.js`/`.ts` route modules even when they contain no JSX. Framework builds and TypeScript remain required to validate export values, metadata exclusivity and supported configuration combinations.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Inherited pnpm policy
|
|
2
|
+
|
|
3
|
+
Antfu 9.5.1 enables `pnpm/yaml-enforce-settings` at error severity and requires these workspace settings:
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
minimumReleaseAgeExcludePrune: true
|
|
7
|
+
shellEmulator: true
|
|
8
|
+
trustPolicy: no-downgrade
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
These inherited checks are separate from the 198-entry research catalog. They are intentional upstream defaults; this package does not silently disable them when a consumer fails lint. Changing a consumer's dependency policy requires that project's approval and native installation/build checks.
|
|
12
|
+
|
|
13
|
+
The user now approves tools verification and the three consumer projects to use `minimumReleaseAgeExclude: ['@ztd-me/*']` for frequent scope releases. Add this age-only selector to the existing list without removing unrelated gates or approved entries. `trustPolicy: no-downgrade`, package integrity and dependency-build restrictions remain active; there is no scope trust exclusion. The existing exact `semver@6.3.1` trust exception remains separate.
|
|
14
|
+
|
|
15
|
+
[`minimumReleaseAgeExcludePrune`](https://pnpm.io/settings/dependency-resolution#minimumreleaseageexcludeprune) was added in pnpm 11.22.0. It removes release-age exception entries that no longer match packages/versions in the lockfile. It retains an active package-wide exception, including `@ztd-me/eslint`, while that package remains installed; this does not automatically expire the exception after the age cutoff. Wildcard entries are retained. Older pnpm versions do not provide the pruning behavior merely because the YAML key is present.
|
|
16
|
+
|
|
17
|
+
[`trustPolicy: no-downgrade`](https://pnpm.io/settings/dependency-resolution#trustpolicy) was added in pnpm 10.21.0 and rejects a package release whose publishing trust drops below earlier releases. Trusted publishing ranks above provenance-only publishing, which ranks above publication without either. Release-age exceptions do not bypass this independent policy. Installation must check the actual dependency graph; switching a package from token publication to trusted publication can make a later token-only release a downgrade.
|
|
18
|
+
|
|
19
|
+
[`shellEmulator: true`](https://pnpm.io/settings/other#shellemulator) makes pnpm execute scripts with its portable JavaScript bash-like shell. This changes script semantics for portability and does not grant additional operating-system privileges. Verify scripts that depend on shell-specific behavior using the target pnpm version.
|
|
20
|
+
|
|
21
|
+
Consumers using pnpm 10 must review a pnpm upgrade before relying on the pruning requirement. A pnpm 11 migration must preserve explicitly allowed dependency builds when converting `onlyBuiltDependencies` to `allowBuilds`; it must not broaden build permissions. Do not disable release-age/trust checks or add broad trust exclusions to get an installation through. A lint pass alone does not prove that the pinned package manager implements every setting.
|
|
22
|
+
|
|
23
|
+
## Existing exact semver trust exception
|
|
24
|
+
|
|
25
|
+
On 2026-10-02, enabling `no-downgrade` on the selected package graph rejected `semver@6.3.1`. Both 0.1.0 and the 0.1.1 framework patch depend on stable `eslint-plugin-react-hooks@7.1.1`, which requires `@babel/core:^7.24.4`. Resolved Babel core 7.29.7 and its compilation-target helper require `semver:^6.3.1`. Registry inspection found 27 eligible stable Babel 7 versions, all requiring that same semver range; 6.3.1 remains the latest 6.x version.
|
|
26
|
+
|
|
27
|
+
Semver 6.3.1 was published on 2023-07-10 without provenance, after provenance-bearing 7.5.4 on 2023-07-07. This meets pnpm's publication-date policy even though the major versions differ; the age exception for this ESLint package does not fix it. A policy failure is not proof of a compromised release, but it remains an installation blocker.
|
|
28
|
+
|
|
29
|
+
Babel 8 is outside the Hooks plugin's supported dependency range. Forcing Babel 8 or semver 7 through a cross-major override is not a supported package upgrade. The user-approved integration retains only `semver@6.3.1` as an exact trust exception. A coherent upstream remedy is a compatible Babel 7 release using an accepted semver dependency, or stable React Hooks support for Babel 8, followed by a tested ESLint package release. The `@ztd-me/*` age selector does not expand that trust exception.
|
package/docs/publishing.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
`.github/workflows/publish-eslint.yml` verifies and stages `@ztd-me/eslint`. Every upload uses `pnpm stage publish`; the workflow never directly publishes or approves a stage. `NPM_TOKEN` has Read and write (stage only) rights, with Bypass 2FA disabled. The token is bound to `https://registry.npmjs.org` in the stage step only, kept in memory, and never printed. Installation/build/test/pack steps have no token.
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The only trigger is a push to `main` affecting `packages/eslint-config/**`, `.github/workflows/publish-eslint.yml` or the shared `scripts/npm-staging.mjs` guard. There is no manual trigger, temporary release branch, or authentication selector. Package versions are independent of the Go CLI's `v*` tags: review and merge an ESLint package version bump to request a release. Source changes without a version bump still run verification, but the existing version is skipped.
|
|
6
6
|
|
|
7
|
-
The verification jobs use a frozen pnpm lockfile on Node 22 and 24. The Node 24 job packs the tested package.
|
|
7
|
+
The verification jobs use a frozen pnpm lockfile on Node 22 and 24. The Node 24 job packs the tested package. A single staging job consumes that exact artifact and saves the npm stage response as a workflow artifact. Runs share one concurrency group without cancellation. Before upload, the script checks public registry metadata and the authenticated pending-stage list. Published or already-staged versions produce a skip receipt, preserving an existing stage ID. Registry/authentication errors and malformed responses fail before upload. The Actions run, source SHA, tarball checksum and stage ID are review evidence. Do not resubmit after an ambiguous timeout; inspect the existing stage before retrying.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Maintainer approval
|
|
10
10
|
|
|
11
|
-
The
|
|
11
|
+
The user-provided stage-only repository `NPM_TOKEN` is the currently verified authentication route. Staging does not make a version installable, and creating a repository secret does not establish an npm Trusted Publisher. The first release, `0.1.0`, was staged and owner-approved separately; normal workflow runs must skip it rather than recreate its stage.
|
|
12
12
|
|
|
13
13
|
After the stage succeeds, the package owner must review and promote the stage using npm's website and their 2FA, or run an authenticated `pnpm stage approve <stage-id>` and complete the required proof of presence. Keep the OTP and credential out of chat. This user-controlled promotion is required before stage 1 can be called complete.
|
|
14
14
|
|
|
@@ -24,7 +24,9 @@ The package owner configures the trust grant in npm's package settings after npm
|
|
|
24
24
|
| Environment name | Leave empty; this workflow uses no GitHub environment |
|
|
25
25
|
| Allowed actions | Stage publishing only; do not enable direct publish or dist-tag mutation for this workflow |
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
These are the exact settings for a future OIDC migration, not evidence that the grant exists. After the owner confirms a stage-only grant, update the existing staging job to job-scoped `id-token: write`, remove `NPM_TOKEN` and its in-memory token binding, and use pnpm's native OIDC authentication on the same GitHub-hosted runner. Keep the single automatic trigger and the workflow filename. Do not add a parallel authentication job or a manual selector. The assistant does not create the trust grant or change package security settings.
|
|
28
|
+
|
|
29
|
+
The migration must also replace the authenticated pending-stage lookup: npm's OIDC identity supports `stage publish`, but not `stage list`, `stage view`, or approval. Removing the token without redesigning this guard would fail before upload. Preserve duplicate prevention using durable stage receipts and explicit reconciliation of ambiguous outcomes, and test the resulting stage-only path before retiring the token route. Maintainer review and proof of presence still control promotion.
|
|
28
30
|
|
|
29
31
|
References: [pnpm stage](https://pnpm.io/cli/stage), [pnpm token authentication](https://pnpm.io/blog/releases/11.10), [npm staged publishing](https://docs.npmjs.com/staged-publishing/), [npm Trusted Publishers](https://docs.npmjs.com/trusted-publishers/).
|
|
30
32
|
|
|
@@ -33,7 +35,7 @@ References: [pnpm stage](https://pnpm.io/cli/stage), [pnpm token authentication]
|
|
|
33
35
|
After promotion, inspect the exact public registry version, then run:
|
|
34
36
|
|
|
35
37
|
```sh
|
|
36
|
-
node scripts/registry-smoke.mjs
|
|
38
|
+
node scripts/registry-smoke.mjs <promoted-version>
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
This creates a fresh consumer, installs the exact registry version using pnpm, checks default/named ESM exports and declarations, and runs JS/TS/framework smoke checks.
|
|
41
|
+
This creates a fresh consumer, installs the exact registry version using pnpm 11.22.0, checks default/named ESM exports and declarations, and runs JS/TS/framework smoke checks. The temporary store/cache settings are inherited by pnpm's pre-run install checks, including in cloud sandboxes without a writable home directory. The user-authorized age selector is `@ztd-me/*`; other packages keep a strict 1440-minute age gate. No-downgrade and the existing exact `semver@6.3.1` trust exception remain active, with no scope trust exclusion. A stage response, registry metadata alone, or a local tarball install is insufficient proof of public installation.
|
package/docs/rule-coverage.md
CHANGED
|
@@ -21,6 +21,8 @@ Core/TS extensions, unused-variable owners, React Hooks owners, SFC line-width o
|
|
|
21
21
|
|
|
22
22
|
The implementation does not claim a Vue index-key check: current stable Vue rules ensure a key exists but cannot ensure it is not a loop index. Vue template typing and dynamic component inference also remain outside this package’s static proof boundary.
|
|
23
23
|
|
|
24
|
+
Version 0.1.1 adds a scoped App Router adapter for the inherited Fast Refresh export rule without changing the 198-entry catalog. [Framework compatibility](framework-compatibility.md) explains the explicit API, scope, upstream contracts and runtime evidence. Antfu also inherits package-manager policy checks outside the research catalog; [pnpm policy](pnpm-policy.md) describes their effects and version requirements.
|
|
25
|
+
|
|
24
26
|
## shared
|
|
25
27
|
|
|
26
28
|
| Rule | Exact researched value | Disposition |
|