@ploaness/config 1.0.0

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/biome-core.json +128 -0
  4. package/biome.json +12 -0
  5. package/dependency-cruiser-core.json +59 -0
  6. package/dependency-cruiser.json +32 -0
  7. package/dist/a11y.d.ts +3 -0
  8. package/dist/a11y.d.ts.map +1 -0
  9. package/dist/a11y.js +9 -0
  10. package/dist/a11y.js.map +1 -0
  11. package/dist/access.d.ts +4 -0
  12. package/dist/access.d.ts.map +1 -0
  13. package/dist/access.js +4 -0
  14. package/dist/access.js.map +1 -0
  15. package/dist/eslint-core.d.ts +167 -0
  16. package/dist/eslint-core.d.ts.map +1 -0
  17. package/dist/eslint-core.js +698 -0
  18. package/dist/eslint-core.js.map +1 -0
  19. package/dist/eslint-library.d.ts +3 -0
  20. package/dist/eslint-library.d.ts.map +1 -0
  21. package/dist/eslint-library.js +53 -0
  22. package/dist/eslint-library.js.map +1 -0
  23. package/dist/eslint.d.ts +3 -0
  24. package/dist/eslint.d.ts.map +1 -0
  25. package/dist/eslint.js +261 -0
  26. package/dist/eslint.js.map +1 -0
  27. package/dist/playwright.d.ts +3 -0
  28. package/dist/playwright.d.ts.map +1 -0
  29. package/dist/playwright.js +108 -0
  30. package/dist/playwright.js.map +1 -0
  31. package/dist/project-settings.d.ts +3 -0
  32. package/dist/project-settings.d.ts.map +1 -0
  33. package/dist/project-settings.js +23 -0
  34. package/dist/project-settings.js.map +1 -0
  35. package/dist/vitest-core.d.ts +90 -0
  36. package/dist/vitest-core.d.ts.map +1 -0
  37. package/dist/vitest-core.js +101 -0
  38. package/dist/vitest-core.js.map +1 -0
  39. package/dist/vitest-library.d.ts +3 -0
  40. package/dist/vitest-library.d.ts.map +1 -0
  41. package/dist/vitest-library.js +37 -0
  42. package/dist/vitest-library.js.map +1 -0
  43. package/dist/vitest-setup.d.ts +2 -0
  44. package/dist/vitest-setup.d.ts.map +1 -0
  45. package/dist/vitest-setup.js +150 -0
  46. package/dist/vitest-setup.js.map +1 -0
  47. package/dist/vitest.d.ts +3 -0
  48. package/dist/vitest.d.ts.map +1 -0
  49. package/dist/vitest.js +117 -0
  50. package/dist/vitest.js.map +1 -0
  51. package/knip.json +5 -0
  52. package/package.json +117 -0
  53. package/pins.json +52 -0
  54. package/stylelint.json +49 -0
  55. package/tsconfig-core.json +34 -0
  56. package/tsconfig.json +11 -0
@@ -0,0 +1,698 @@
1
+ // The framework-neutral half of the ploaness ESLint contract.
2
+ //
3
+ // It was extracted because ploaness never ran ESLint on itself. The rules below carry every size cap,
4
+ // explicitness rule, naming ban, and suppression discipline the governing standard states - and the
5
+ // harness that publishes them was not measured by them. Sharing only the five cap NUMBERS would have
6
+ // left roughly seventy rule declarations to drift; the blocks are shared instead, and the globs are the
7
+ // caller's, because the globs are the only genuinely project-shaped part.
8
+ //
9
+ // `packages/config/eslint.js` composes these with the Payload-specific blocks. The ploaness repository
10
+ // composes them with its own workspace layout. Neither restates a rule.
11
+ import js from '@eslint/js';
12
+ import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
13
+ import { PROJECT_SETUP_FILE } from '@ploaness/governance';
14
+ import vitest from '@vitest/eslint-plugin';
15
+ import { defineConfig } from 'eslint/config';
16
+ import prettier from 'eslint-config-prettier';
17
+ import functional from 'eslint-plugin-functional';
18
+ import jsdoc from 'eslint-plugin-jsdoc';
19
+ import regexp from 'eslint-plugin-regexp';
20
+ import sonarjs from 'eslint-plugin-sonarjs';
21
+ import unicorn from 'eslint-plugin-unicorn';
22
+ import globals from 'globals';
23
+ import tseslint from 'typescript-eslint';
24
+ // Narrowing rather than a property read: `rules` arrives as `unknown` out of the index signature above,
25
+ // and a layer that carries none is passed through untouched.
26
+ const isRuleTable = (value) => typeof value === 'object' && value !== null;
27
+ const COMPLEXITY_MAX = 8;
28
+ const MAX_PARAMS = 4;
29
+ const MAX_DEPTH = 3;
30
+ const MAX_LINES_PER_FILE = 500;
31
+ const MAX_LINES_PER_FUNCTION = 50;
32
+ const MIN_NAME_LENGTH = 2;
33
+ // The governing standard's "short structural-value allowlist".
34
+ const STRUCTURAL_NUMBERS = [-1, 0, 1];
35
+ // No mocks. Tests run against real objects and real services (a real Payload instance, a real
36
+ // database, real in-process servers) - never test doubles. These are the Vitest entry points that
37
+ // create mocks/stubs/spies, plus the third-party mocking libraries, all banned build-wide.
38
+ const NO_MOCKS_MESSAGE = 'No mocks/stubs/spies. Test against real objects and real services instead (see AGENTS.md).';
39
+ const MOCKING_VI_METHODS = [
40
+ 'fn',
41
+ 'mock',
42
+ 'doMock',
43
+ 'unmock',
44
+ 'mocked',
45
+ 'spyOn',
46
+ 'stubGlobal',
47
+ 'stubEnv',
48
+ 'importMock',
49
+ ];
50
+ const MOCKING_PACKAGES = [
51
+ 'sinon',
52
+ 'testdouble',
53
+ 'jest-mock',
54
+ '@jest/globals',
55
+ 'proxyquire',
56
+ 'nock',
57
+ 'msw',
58
+ ];
59
+ // Collection/global/field/block configs are declarative wiring: importing one already satisfies its
60
+ // coverage, so a function inlined there has no unit-test seam and can sit untested. This rule bans
61
+ // inline functions in those files, forcing extraction into the unit-tested src/access and src/lib
62
+ // modules, then reference by name. Identifier references (`read: anyone`) pass; inline functions do not.
63
+ // A flat-config block that sets `no-restricted-syntax` REPLACES the setting rather than adding to it, so
64
+ // a scoped block would silently drop these. Spread into every such array instead of restating them.
65
+ const INHERITANCE_MESSAGE = 'Add behavior by composing objects, not by inheriting. Inherit only from a base the ' +
66
+ 'language or a dependency requires.';
67
+ // The mock ban as ENTRIES rather than as a finished rule setting, for the reason the comment above
68
+ // `NO_INHERITANCE` gives: a scoped block that sets `no-restricted-properties` REPLACES this rather than
69
+ // adding to it. `eslint.js` set that key for `src/**` to the process.env rule alone, which switched the
70
+ // build-wide mock ban off across a project's whole source tree without a word anywhere saying so.
71
+ const NO_MOCK_PROPERTIES = MOCKING_VI_METHODS.map((property) => ({
72
+ object: 'vi',
73
+ property,
74
+ message: NO_MOCKS_MESSAGE,
75
+ }));
76
+ const NO_INHERITANCE = [
77
+ 'error',
78
+ {
79
+ selector: 'ClassDeclaration[superClass]:not([superClass.name="Error"])',
80
+ message: INHERITANCE_MESSAGE,
81
+ },
82
+ {
83
+ selector: 'ClassExpression[superClass]:not([superClass.name="Error"])',
84
+ message: INHERITANCE_MESSAGE,
85
+ },
86
+ ];
87
+ // An assertion whose operands are literals alone cannot fail when the code under test changes, so it
88
+ // reports as passing while judging nothing. This catches the exact form the standard names; the general
89
+ // rule is not statically decidable and is stated in the agent guide instead.
90
+ const LITERAL_ASSERTION_MESSAGE = 'An assertion over a literal cannot fail when the code under test changes. Assert an observable outcome.';
91
+ const NO_LITERAL_ASSERTIONS = [
92
+ {
93
+ selector: "CallExpression[callee.name='expect'] > Literal",
94
+ message: LITERAL_ASSERTION_MESSAGE,
95
+ },
96
+ {
97
+ selector: "CallExpression[callee.name='expect'] > TemplateLiteral[expressions.length=0]",
98
+ message: LITERAL_ASSERTION_MESSAGE,
99
+ },
100
+ {
101
+ selector: "CallExpression[callee.name='expect'] > UnaryExpression[operator='-'] > Literal",
102
+ message: LITERAL_ASSERTION_MESSAGE,
103
+ },
104
+ ];
105
+ /**
106
+ * Re-exported so a caller declares no plugin version of its own.
107
+ *
108
+ * `defineConfig` rather than `tseslint.config`, which typescript-eslint deprecated in favour of it: the
109
+ * composition of flat-config blocks is ESLint's own concern now, and this is where a governed project
110
+ * reaches it. `eslint` is declared as a dependency of this package for the same reason - every plugin
111
+ * imported above already requires it at runtime, and resolving it out of whatever the consumer happens
112
+ * to have hoisted is a resolution this package should not depend on.
113
+ *
114
+ * Typed against `FlatConfigBlock` rather than against `defineConfig`'s own parameter type, for the
115
+ * reason that alias exists. This assertion is the single place the widening happens: every caller then
116
+ * composes blocks without one of its own, and a block's shape is still checked by ESLint, which is the
117
+ * only thing that can validate a rule's options anyway.
118
+ */
119
+ export const compose = defineConfig;
120
+ /** Formatting is Biome's job; this disables every conflicting stylistic rule and must stay last. */
121
+ export const prettierLast = prettier;
122
+ // A warning severity does not exist in a governed repository: a check has two verdicts, and a finding
123
+ // that prints and exits 0 is neither. Several presets ship rules at `warn` anyway - 31 of jsdoc's and 6
124
+ // of regexp's, as of this writing - so every finding they report was invisible to the build.
125
+ //
126
+ // The list of which rules those are is NOT written down here. It would be a copy of what the presets
127
+ // declare, and it would go stale on the next upgrade in the one direction that fails open. Each layer's
128
+ // own declarations are re-read instead and raised to `error`, options intact. A rule a preset turns
129
+ // `off` stays off: that is a decision about whether the rule runs, not about how loudly it speaks.
130
+ const isOff = (severity) => severity === 'off' || severity === 0;
131
+ const escalate = (setting) => {
132
+ if (!Array.isArray(setting)) {
133
+ return isOff(setting) ? setting : 'error';
134
+ }
135
+ // Re-bound through `unknown[]`: `Array.isArray` narrows an `unknown` to `any[]`, and reading an
136
+ // element out of that would put an `any` back into the rule table this function exists to keep honest.
137
+ const declared = setting;
138
+ return isOff(declared[0]) ? setting : ['error', ...declared.slice(1)];
139
+ };
140
+ // `object` and `Reflect.get`, rather than an indexable type and a property read. Each preset layer
141
+ // arrives as the plugin's own declared interface, and an interface carries no index signature, so
142
+ // naming one here would force an assertion at every call site - which the type coverage measurement
143
+ // counts as uncovered. What this function needs is one property, read the way an unknown shape is read.
144
+ const withoutWarnings = (layer) => {
145
+ const rules = Reflect.get(layer, 'rules');
146
+ return isRuleTable(rules)
147
+ ? {
148
+ ...layer,
149
+ rules: Object.fromEntries(Object.entries(rules).map(([id, setting]) => [
150
+ id,
151
+ escalate(setting),
152
+ ])),
153
+ }
154
+ : { ...layer };
155
+ };
156
+ /** The preset layers every ploaness-governed project runs, none of which may report at `warn`. */
157
+ export const baseLayers = [
158
+ js.configs.recommended,
159
+ ...tseslint.configs.strictTypeChecked,
160
+ ...tseslint.configs.stylisticTypeChecked,
161
+ sonarjs.configs.recommended,
162
+ unicorn.configs.recommended,
163
+ // The plugin's own error-severity variant of the same preset. The escalation above would raise it
164
+ // anyway; naming the variant states the intent where a reader of the layer list will see it.
165
+ jsdoc.configs['flat/recommended-typescript-error'],
166
+ comments.recommended, // disciplined eslint-disable comments (scoped + justified)
167
+ regexp.configs['flat/recommended'], // regex correctness and safety
168
+ ].map((layer) => withoutWarnings(layer));
169
+ /**
170
+ * Type-aware parsing, with the caller supplying the parser options its layout needs.
171
+ * @param parserOptions the typescript-eslint parser options.
172
+ * @returns a flat-config block.
173
+ */
174
+ export const typeAwareParsing = (parserOptions) => ({
175
+ languageOptions: { parserOptions },
176
+ });
177
+ /** The maximum-explicit rule set: the caps, the explicitness rules, the bans, the mock ban. */
178
+ export const guidelineRules = {
179
+ // Explicitness - types must be written, not just inferred at boundaries.
180
+ '@typescript-eslint/explicit-function-return-type': 'error',
181
+ '@typescript-eslint/explicit-module-boundary-types': 'error',
182
+ // Require an explicit type annotation on every `const`/`let` (not just boundaries), so the
183
+ // declared type is written rather than left to inference. Arrow-function consts are exempt -
184
+ // their signature is already covered by explicit-function-return-type.
185
+ '@typescript-eslint/typedef': [
186
+ 'error',
187
+ { variableDeclaration: true, variableDeclarationIgnoreFunction: true },
188
+ ],
189
+ // Conflicts with the explicit philosophy: typedef requires annotations, this rule would strip
190
+ // the "trivially inferable" ones (e.g. `const x: string = '...'`). We want them written.
191
+ '@typescript-eslint/no-inferrable-types': 'off',
192
+ '@typescript-eslint/no-non-null-assertion': 'error',
193
+ '@typescript-eslint/strict-boolean-expressions': 'error',
194
+ '@typescript-eslint/switch-exhaustiveness-check': 'error',
195
+ '@typescript-eslint/no-unnecessary-condition': 'error',
196
+ // Defect patterns - bugs the type-checker can prove.
197
+ '@typescript-eslint/no-floating-promises': 'error',
198
+ // The rule above accepts exactly four spellings of a deliberately-discarded promise, and three of
199
+ // them are unavailable in the shape that most often needs one: an async function handed to a
200
+ // SYNCHRONOUS Node callback, which is every `net`/`http` connection listener, `process.on`, and
201
+ // `setInterval`. `await` is impossible in a synchronous callback; `.catch()` and
202
+ // `.then(undefined, fn)` are both reported by `unicorn/prefer-await`, which arrives on by preset.
203
+ // That leaves `void`, which `no-floating-promises` names in its own error text as the marker to
204
+ // use - and Biome's `noVoid` banned it, so a governed project had NO legal spelling at all.
205
+ // `ploaness format` drove a project into the wall rather than around it: ESLint's autofix inserts
206
+ // `void`, and the next Biome pass rejected what it had just written.
207
+ //
208
+ // So `noVoid` is gone from `biome-core.json` and the ban is stated here instead, where it can carry
209
+ // the one exemption that resolves the contradiction. `allowAsStatement` permits `void promise()`
210
+ // standing alone as a statement - the discard position, and the only one the other rule mandates -
211
+ // while `void 0` as an EXPRESSION stays banned, which is the obfuscation both tools were aimed at.
212
+ // A whole rule was not turned off; a rule was narrowed to what it meant, in the tool that can say so.
213
+ 'no-void': ['error', { allowAsStatement: true }],
214
+ '@typescript-eslint/no-misused-promises': 'error',
215
+ '@typescript-eslint/default-param-last': 'error',
216
+ '@typescript-eslint/require-array-sort-compare': ['error', { ignoreStringArrays: true }],
217
+ // Ban calls into APIs marked `@deprecated` - yours or a framework's (Payload/Next/React). The
218
+ // type-checker reads the JSDoc tag and fails the build at the call site, so deprecated usage is
219
+ // caught statically rather than at runtime. (AI models especially reach for deprecated APIs from
220
+ // stale training data; this stops that at the gate.)
221
+ '@typescript-eslint/no-deprecated': 'error',
222
+ // Naming - ban the "plumbing" type-name suffixes (`Service`/`Manager`/`Handler`/`Provider`/
223
+ // `Util`): a type is a noun of the domain, or the operation belongs to the domain type that owns
224
+ // it (mask on `MaskedToken`, not a `TokenMasker`). Scoped to type declarations, so a domain
225
+ // property like `packageManager` or a framework import like `NextIntlClientProvider` is untouched.
226
+ '@typescript-eslint/naming-convention': [
227
+ 'error',
228
+ {
229
+ selector: ['class', 'interface', 'typeAlias', 'enum'],
230
+ format: null,
231
+ custom: { regex: '(Service|Manager|Handler|Provider|Util)$', match: false },
232
+ },
233
+ ],
234
+ // Suppressions must be deliberate: every eslint-disable needs a reason, and dead ones fail.
235
+ '@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
236
+ '@eslint-community/eslint-comments/no-unused-disable': 'error',
237
+ // Complexity caps - keep every unit small, flat and testable.
238
+ complexity: ['error', COMPLEXITY_MAX],
239
+ 'sonarjs/cognitive-complexity': ['error', COMPLEXITY_MAX],
240
+ 'max-params': ['error', MAX_PARAMS],
241
+ 'max-depth': ['error', MAX_DEPTH],
242
+ 'max-lines': ['error', { max: MAX_LINES_PER_FILE, skipBlankLines: true, skipComments: true }],
243
+ 'max-lines-per-function': [
244
+ 'error',
245
+ { max: MAX_LINES_PER_FUNCTION, skipBlankLines: true, skipComments: true },
246
+ ],
247
+ // Bare numbers. The governing standard bans a number outside the declaration of a named constant,
248
+ // and allows "a short structural-value allowlist, such as 0 and 1". -1 earns its place beside them
249
+ // as the indexOf/findIndex sentinel, which is a structural value rather than a magic one.
250
+ // The @typescript-eslint variant, not the base rule: only it understands enums, numeric literal
251
+ // types, and type indexes, all of which a Payload project has. Biome's noMagicNumbers is
252
+ // deliberately NOT enabled alongside it - that rule has no allowlist option, so it cannot express
253
+ // the carve-out the standard grants, and it would double-report every finding across two gates.
254
+ '@typescript-eslint/no-magic-numbers': [
255
+ 'error',
256
+ {
257
+ ignore: STRUCTURAL_NUMBERS,
258
+ ignoreArrayIndexes: true,
259
+ ignoreDefaultValues: true,
260
+ ignoreClassFieldInitialValues: true,
261
+ ignoreEnums: true,
262
+ ignoreNumericLiteralTypes: true,
263
+ ignoreReadonlyClassProperties: true,
264
+ ignoreTypeIndexes: true,
265
+ enforceConst: true,
266
+ detectObjects: false,
267
+ },
268
+ ],
269
+ // One-character names. `maskedToken` says what `m` hides. `_` is exempt because it is the
270
+ // placeholder the language reserves for an unused value, which TypeScript's noUnusedParameters
271
+ // already honours. Object properties are exempt because a key is frequently an external API's
272
+ // vocabulary rather than a name anyone chose.
273
+ 'id-length': ['error', { min: MIN_NAME_LENGTH, properties: 'never', exceptions: ['_'] }],
274
+ // The two deferred-work markers this rule's own options name are banned outright, and the unicorn
275
+ // rule disabled beneath it arrives ON from the recommended preset and contradicts that: its whole
276
+ // premise is that such a marker is acceptable once it carries an expiry date. The ban has no such
277
+ // exception, so that rule is turned off rather than left to license what this one forbids.
278
+ //
279
+ // Neither marker, and neither is the disabled rule's name, is spelled in this comment. `location:
280
+ // 'anywhere'` scans comment text, so a comment explaining the ban would otherwise report itself -
281
+ // the same self-reference `banned-typography.ts` answers by naming characters as code points.
282
+ 'no-warning-comments': ['error', { terms: ['todo', 'fixme'], location: 'anywhere' }],
283
+ 'unicorn/expiring-todo-comments': 'off',
284
+ // Composition over inheritance. A type inherits only from a base the language or a dependency
285
+ // requires, and the error base is the one such base that appears in ordinary code.
286
+ 'no-restricted-syntax': [...NO_INHERITANCE],
287
+ // Control-flow clarity.
288
+ eqeqeq: ['error', 'always'],
289
+ curly: ['error', 'all'],
290
+ 'default-case': 'error',
291
+ 'no-else-return': ['error', { allowElseIf: false }],
292
+ // Unicorn tuned for the Payload/Next reality.
293
+ 'unicorn/prevent-abbreviations': 'off', // req/params/props/config are intentional here.
294
+ // Keep the sibling name-replacements rule ON (it catches genuine abbreviations like refs/dev), but
295
+ // exempt the words the frameworks mandate verbatim: `params` (the Next.js App Router route prop and
296
+ // its `generateStaticParams` export) and `req`/`doc` (Payload's fixed hook/endpoint argument names -
297
+ // every Payload hook, access function, and endpoint handler receives `req`, and change/read hooks
298
+ // receive `doc`). `repository` is exempt for the same kind of reason one layer up: it is the domain
299
+ // term the scope model is built from - a repository holds members, and a rule is repository-scope or
300
+ // package-scope - so every comment, type name and finding string says it. Shortening the identifiers
301
+ // to `repo` would leave the code disagreeing with the prose that explains it.
302
+ // `: false` removes only those replacements; every other abbreviation is still reported.
303
+ 'unicorn/name-replacements': [
304
+ 'error',
305
+ { replacements: { params: false, req: false, doc: false, repository: false } },
306
+ ],
307
+ // A predicate named as a third-person verb - `matchesAny`, `reachesThreshold`, `declaresKey` -
308
+ // already says plainly what it does, which is what the naming rule asks for. Rewriting those as
309
+ // `isMatchingAny` would make them read worse, so the verb forms are allowed alongside the default
310
+ // prefixes. Every other boolean still has to announce itself as one.
311
+ 'unicorn/consistent-boolean-name': [
312
+ 'error',
313
+ {
314
+ prefixes: {
315
+ // React mandates it. A hook MUST begin with `use` or React's own rules of hooks do not apply to
316
+ // it, so a boolean-returning hook cannot be renamed to satisfy this rule - the only move left
317
+ // was a suppression on every such hook, spending a project's ceiling on a decision React made.
318
+ use: true,
319
+ matches: true,
320
+ reaches: true,
321
+ opens: true,
322
+ declares: true,
323
+ ends: true,
324
+ exists: true,
325
+ contains: true,
326
+ starts: true,
327
+ carries: true,
328
+ covers: true,
329
+ },
330
+ },
331
+ ],
332
+ // These three propose methods that the declared `lib` target does not carry: Iterator#toArray,
333
+ // Array#toSorted, and Set#union are all newer than the platform this contract compiles against. Their
334
+ // autofixes produce code that does not compile, which is worse than the pattern they replace. None of
335
+ // them carries a rule of the governing standard - they are style preferences about newer syntax.
336
+ // `reduce` is how a fold is written without a mutable accumulator, which is precisely what the
337
+ // immutability rule above requires. Banning it would leave no way to satisfy both, and the governing
338
+ // standard says nothing about which array method expresses a fold.
339
+ // Omitting a key by destructuring binds a name precisely so it can be dropped; that binding is
340
+ // deliberately unused and is the idiomatic alternative to `delete`, which mutates.
341
+ '@typescript-eslint/no-unused-vars': [
342
+ 'error',
343
+ {
344
+ ignoreRestSiblings: true,
345
+ varsIgnorePattern: '^_',
346
+ argsIgnorePattern: '^_',
347
+ caughtErrorsIgnorePattern: '^_',
348
+ },
349
+ ],
350
+ 'sonarjs/no-unused-vars': 'off', // the typed rule above decides this, with the project's options.
351
+ // ploaness exists to invoke analyzers, and it invokes them by name so the project's own installed
352
+ // version is the one that runs. Resolving each to an absolute path would pin the harness to one
353
+ // layout and defeat that. The governing standard says nothing about PATH.
354
+ 'sonarjs/no-os-command-from-path': 'off',
355
+ 'unicorn/no-array-reduce': 'off',
356
+ 'unicorn/prefer-iterator-to-array': 'off',
357
+ 'unicorn/no-array-sort': 'off',
358
+ 'unicorn/prefer-set-methods': 'off',
359
+ 'unicorn/no-null': 'off', // React and Payload use `null` deliberately.
360
+ 'unicorn/no-keyword-prefix': 'off',
361
+ // A DIRECT contradiction rather than a preference. Biome's `useNumberNamespace` requires
362
+ // `Number.NaN` and `Number.POSITIVE_INFINITY`; this rule requires the bare globals, so no source
363
+ // text satisfies both linters and the only escapes are a suppression or contorting the code to
364
+ // avoid the constant. Biome decides, for the reason it already decides formatting: the two must
365
+ // never disagree about the same character, so one of them owns style and the other defers.
366
+ 'unicorn/prefer-global-number-constants': 'off',
367
+ // New in unicorn 73. It would expand every concise one-line `/** ... */` export doc into a
368
+ // three-line block, and would also rewrite the `GENERATED AUTOMATICALLY BY PAYLOAD` /
369
+ // `DO NOT MODIFY` headers that Payload writes into the `src/app/(payload)` scaffolding.
370
+ 'unicorn/single-line-block-comment-style': 'off',
371
+ 'unicorn/filename-case': [
372
+ 'error',
373
+ { cases: { camelCase: true, pascalCase: true, kebabCase: true } },
374
+ ],
375
+ // JSDoc: require a doc *block* on public helpers (below), but never the per-`@param root0`
376
+ // ceremony that destructured arrow signatures produce. Keep the correctness checks on.
377
+ 'jsdoc/require-jsdoc': 'off',
378
+ 'jsdoc/require-param': 'off',
379
+ 'jsdoc/require-returns': 'off',
380
+ // No mocks - ban the mocking entry points and libraries build-wide.
381
+ 'no-restricted-properties': ['error', ...NO_MOCK_PROPERTIES],
382
+ 'no-restricted-imports': [
383
+ 'error',
384
+ { paths: MOCKING_PACKAGES.map((name) => ({ name, message: NO_MOCKS_MESSAGE })) },
385
+ ],
386
+ };
387
+ export const vitestPlugin = vitest;
388
+ /** The Vitest half of the test-integrity block, shared by every config that lints a Vitest suite. */
389
+ export const testIntegrityRules = {
390
+ 'vitest/no-focused-tests': 'error', // ban `.only` - it skips the rest of the suite.
391
+ 'vitest/no-disabled-tests': 'error', // ban `.skip` / `xit` / `xdescribe`.
392
+ 'vitest/no-commented-out-tests': 'error', // a commented-out test is a deleted test that looks present.
393
+ // a fast-check fc.assert property counts as the assertion too.
394
+ 'vitest/expect-expect': ['error', { assertFunctionNames: ['expect', 'fc.assert'] }],
395
+ // vitest/expect-expect (above) is the assertion-presence gate here and understands fc.assert.
396
+ // sonarjs/assertions-in-tests is a less-configurable duplicate that does not, so it defers in
397
+ // this scope.
398
+ 'sonarjs/assertions-in-tests': 'off',
399
+ 'vitest/valid-expect': 'error', // `expect` is called correctly (awaited, matcher present).
400
+ 'vitest/no-standalone-expect': 'error', // assertions must live inside a test.
401
+ 'vitest/no-conditional-expect': 'error', // an assertion behind an `if` may never execute.
402
+ 'vitest/no-conditional-tests': 'error', // tests must not be defined conditionally.
403
+ 'vitest/no-identical-title': 'error', // duplicate titles hide one test behind another.
404
+ 'vitest/valid-title': 'error',
405
+ 'vitest/no-import-node-test': 'error', // use the Vitest API, not node:test.
406
+ 'vitest/consistent-test-it': ['error', { fn: 'it' }],
407
+ 'vitest/prefer-hooks-on-top': 'error', // setup before assertions, so reading order matches run order.
408
+ };
409
+ /**
410
+ * The framework idiom a spec is written in, which the production rules were never aimed at.
411
+ *
412
+ * Only idiom is relaxed. Every rule that carries a rule of the governing standard - the size caps, the
413
+ * explicitness rules, the ban on a non-null assertion, the unsafe-any family - stays ON, because the
414
+ * standard says test code passes the same static-analysis checks as production code. A relaxation that
415
+ * made a test easier to write by making it less checked would exempt the suite from the contract it
416
+ * exists to enforce.
417
+ *
418
+ * Shared by every shipped config rather than stated in each, because what earns the exemption is being a
419
+ * SPEC rather than being a Payload application's spec. Stated twice it drifted: a library consumer's
420
+ * tests were held to the bare-number ban a Payload consumer's were not, and neither config was wrong
421
+ * read on its own, which is why nothing found it for as long as it stood.
422
+ */
423
+ export const testIdiomRules = {
424
+ // A test's expected value IS its specification. Naming it moves the specification away from the
425
+ // assertion that reads it, which makes the test harder to check by eye rather than easier - the
426
+ // opposite of what the bare-number ban exists to achieve. This is a role distinction, not a
427
+ // convenience: production code names its constants, and a spec states its literals.
428
+ '@typescript-eslint/no-magic-numbers': 'off',
429
+ 'sonarjs/no-duplicate-string': 'off', // fixture data repeats by nature; naming each is noise.
430
+ 'unicorn/no-top-level-assignment-in-function': 'off', // the standard vitest beforeAll pattern.
431
+ 'unicorn/max-nested-calls': 'off', // expect(fn(arg(value))) assertions are idiomatic.
432
+ 'unicorn/numeric-separators-style': 'off', // fixtures use code points (0x2026); ungrouped reads better.
433
+ };
434
+ // Property tests must stay deterministic: one seed decides the whole suite, and a per-call override
435
+ // reintroduces a gate whose verdict changes between two runs of an unchanged repository, which is the
436
+ // one thing a check may never do.
437
+ //
438
+ // Setting that global seed is the project's job, in its own `vitest.setup.ts`. It cannot be done from
439
+ // the shipped setup file: that file lives inside node_modules, and a `fast-check` reached from there is
440
+ // a different module record from the one the suite loads, so configuring it would configure nothing. The
441
+ // message therefore says whose job it is rather than claiming ploaness has already done it.
442
+ const NO_FAST_CHECK_SEED = [
443
+ {
444
+ selector: "CallExpression[callee.object.name='fc'][callee.property.name='assert'] > " +
445
+ "ObjectExpression > Property[key.name='seed']",
446
+ message: 'Do not set a per-call fast-check seed. Set one global seed in vitest.setup.ts, so a failing ' +
447
+ 'property is reproducible by rerunning.',
448
+ },
449
+ ];
450
+ // The two determinism mechanisms the shipped Vitest config installs, and the ways a spec could reach
451
+ // past them. Neither is a substitute for the mechanism: the network guard is installed non-configurable
452
+ // and the sequence block is out of a project's reach entirely. Raw datagrams and a new process or worker
453
+ // do not pass through the patched runtime, so those entry points are refused statically instead of being
454
+ // wrapped incompletely. The selectors make every escape attempt a finding before the suite runs.
455
+ const NO_NETWORK_GUARD_ESCAPE = [
456
+ {
457
+ selector: 'ImportDeclaration[source.value=/^(?:node:)?(?:child_process|cluster|dgram|worker_threads)$/]',
458
+ message: 'Tests may not import datagram, process, or worker APIs, because they run outside the network ' +
459
+ 'guard. Use a real component on this machine from the guarded test runtime.',
460
+ },
461
+ {
462
+ selector: 'ImportExpression[source.value=/^(?:node:)?(?:child_process|cluster|dgram|worker_threads)$/]',
463
+ message: 'Tests may not dynamically import datagram, process, or worker APIs, because they run outside ' +
464
+ 'the network guard.',
465
+ },
466
+ {
467
+ selector: "CallExpression[callee.name='require']" +
468
+ '[arguments.0.value=/^(?:node:)?(?:child_process|cluster|dgram|worker_threads)$/]',
469
+ message: 'Tests may not require datagram, process, or worker APIs, because they run outside the network ' +
470
+ 'guard.',
471
+ },
472
+ {
473
+ selector: "CallExpression[callee.object.name='process'][callee.property.name='getBuiltinModule']" +
474
+ '[arguments.0.value=/^(?:node:)?(?:child_process|cluster|dgram|worker_threads)$/]',
475
+ message: 'Tests may not load datagram, process, or worker builtins, because they run outside the network ' +
476
+ 'guard.',
477
+ },
478
+ {
479
+ selector: 'NewExpression[callee.name=/^(?:SharedWorker|Worker)$/]',
480
+ message: 'Tests may not create a worker, because its isolated runtime does not carry the network guard.',
481
+ },
482
+ {
483
+ selector: "AssignmentExpression[left.object.property.name='prototype'][left.property.name='connect']",
484
+ message: 'Do not reinstall a socket method. A test reaches no network beyond the machine it runs on, ' +
485
+ "and the guard that decides that is the harness's.",
486
+ },
487
+ {
488
+ selector: "AssignmentExpression[left.object.name='globalThis'][left.property.name='fetch']",
489
+ message: 'Do not replace the global fetch. Point the test at a real component on this machine instead.',
490
+ },
491
+ // The bare assignment is not the only way to replace it, and it is the least likely one to be
492
+ // reached for by someone working around the guard on purpose. `globalThis.fetch` is the one property
493
+ // the guard leaves configurable - a DOM environment swaps the globals between files and a frozen
494
+ // fetch would break that - so these three routes to it are named as well.
495
+ {
496
+ selector: "CallExpression[callee.object.name='Object'][callee.property.name='defineProperty']" +
497
+ "[arguments.0.name='globalThis'][arguments.1.value='fetch']",
498
+ message: 'Do not redefine the global fetch. Point the test at a real component on this machine instead.',
499
+ },
500
+ {
501
+ selector: "CallExpression[callee.object.name='Reflect'][callee.property.name='set']" +
502
+ "[arguments.0.name='globalThis'][arguments.1.value='fetch']",
503
+ message: 'Do not replace the global fetch. Point the test at a real component on this machine instead.',
504
+ },
505
+ {
506
+ selector: "AssignmentExpression[left.object.name='global'][left.property.name='fetch']",
507
+ message: 'Do not replace the global fetch. Point the test at a real component on this machine instead.',
508
+ },
509
+ ];
510
+ // Ordering is decided once, by the shipped sequence block. A per-test escape reintroduces exactly the
511
+ // coupling the shuffle exists to find, and `vi.setConfig` reintroduces the per-run seed.
512
+ // `describe.shuffle` is deliberately absent from this list: it only strengthens.
513
+ const NO_TEST_ORDER_ESCAPE = [
514
+ {
515
+ selector: "MemberExpression[object.name=/^(?:it|test|describe|suite)$/][property.name='sequential']",
516
+ message: 'Do not pin one test to declaration order. A test reaches its verdict whatever order the suite ' +
517
+ 'runs in; if this one cannot, the coupling is the defect.',
518
+ },
519
+ {
520
+ selector: "MemberExpression[object.name=/^(?:it|test|describe|suite)$/][property.name='concurrent']",
521
+ message: 'Do not run tests concurrently. Interleaving makes order-coupling harder to see rather than ' +
522
+ 'impossible to have.',
523
+ },
524
+ {
525
+ selector: "CallExpression[callee.object.name='vi'][callee.property.name='setConfig']",
526
+ message: "Do not reconfigure the runner from a spec. The sequence and its seed are the harness's.",
527
+ },
528
+ ];
529
+ /**
530
+ * Everything a Vitest spec is guarded against, as ONE table.
531
+ *
532
+ * Assembled here rather than at each call site because a block that sets `no-restricted-syntax`
533
+ * REPLACES the setting rather than adding to it. A config listing four of these five has not weakened a
534
+ * rule anybody can read; it has silently dropped the fifth. The library config listed none of them and
535
+ * kept only the inheritance ban it inherited from the guideline layer, so a library's specs could assert
536
+ * a literal against itself, pin their own order, seed a property test per-call, and open a transport the
537
+ * network guard cannot reach - seventeen selectors, none of them Payload-specific.
538
+ *
539
+ * A table rather than the bare setting, so a call site spreads it as it spreads every other table here.
540
+ * Handed out as a setting it has to be re-wrapped under its key at each call site, and that re-wrapping
541
+ * is the replacement - the one move this constant exists to stop anybody making by hand.
542
+ */
543
+ export const testSuiteSyntaxRules = {
544
+ 'no-restricted-syntax': [
545
+ ...NO_INHERITANCE,
546
+ ...NO_LITERAL_ASSERTIONS,
547
+ ...NO_FAST_CHECK_SEED,
548
+ ...NO_TEST_ORDER_ESCAPE,
549
+ ...NO_NETWORK_GUARD_ESCAPE,
550
+ ],
551
+ };
552
+ /**
553
+ * The same contract, applied to a file that is JavaScript rather than TypeScript.
554
+ *
555
+ * A check a repository implements itself is source code of that repository, so the programs that
556
+ * implement one are held to the code rules like anything else - and a build script or a runtime shim
557
+ * cannot be TypeScript, because the tool that loads it reads it as JavaScript. The rule list is not
558
+ * restated here: it is the same object, minus the four rules that ask for syntax a JavaScript file
559
+ * cannot carry, and with the bare-number ban expressed through the base rule because the typed variant
560
+ * reads type information such a file does not have. The allowlist it reads is the same one.
561
+ * @param files the glob patterns this block governs.
562
+ * @returns a flat-config block.
563
+ */
564
+ export const javascriptBlock = (files) => ({
565
+ files,
566
+ extends: [tseslint.configs.disableTypeChecked],
567
+ // Declared rather than inferred. `no-undef` is off for every TypeScript file here because the
568
+ // compiler decides that question; on a JavaScript file the rule is live, and it has to be told which
569
+ // names the runtime supplies. These programs run on node without a CommonJS wrapper.
570
+ languageOptions: { globals: globals.nodeBuiltin },
571
+ rules: {
572
+ ...guidelineRules,
573
+ // Re-applied AFTER the rule list above. `extends` places the disabling block BEFORE the block's own
574
+ // rules, so spreading the list on top would switch every type-aware rule back on for a file that has
575
+ // no type information for one to read - which fails the run outright rather than reporting.
576
+ ...tseslint.configs.disableTypeChecked.rules,
577
+ // A JavaScript file has nowhere to write a return type, a parameter type, or a variable annotation.
578
+ '@typescript-eslint/explicit-function-return-type': 'off',
579
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
580
+ '@typescript-eslint/typedef': 'off',
581
+ // The typed variant understands enums and type indexes, which is why it is the one TypeScript reads;
582
+ // here there are none, and the base rule expresses the same allowlist.
583
+ '@typescript-eslint/no-magic-numbers': 'off',
584
+ 'no-magic-numbers': [
585
+ 'error',
586
+ {
587
+ ignore: STRUCTURAL_NUMBERS,
588
+ ignoreArrayIndexes: true,
589
+ ignoreDefaultValues: true,
590
+ ignoreClassFieldInitialValues: true,
591
+ enforceConst: true,
592
+ detectObjects: false,
593
+ },
594
+ ],
595
+ },
596
+ });
597
+ // The verdict a program returns, which is the process rather than the program's own data.
598
+ //
599
+ // Assigning it is the ONLY spelling of that verdict this config leaves legal: `unicorn/no-process-exit`
600
+ // arrives on by preset, so `process.exit()` is banned too, and with no carve-out here a Node entry point
601
+ // cannot report a non-zero exit at all. Every governed `scripts/` therefore paid a suppression for the
602
+ // one line it exists to end on - ploaness's own `bin.ts` among them, which is how this was found.
603
+ const PROCESS_VERDICT = 'process.exitCode';
604
+ // The environment, exempt in the setup file alone, because configuring the process before any spec reads
605
+ // it is that file's whole job.
606
+ //
607
+ // ploaness exempts its OWN setup file from this rule and shipped no equivalent, while closing every
608
+ // sanctioned alternative a project had: `vi.stubEnv` is banned outright by `NO_MOCK_PROPERTIES`, the
609
+ // Vitest config must be a bare re-export so `test.env` cannot be added, and the `tests` gate runs the
610
+ // runner directly rather than through a package script that could carry the assignment. A rule that
611
+ // reports the only remaining door is aimed at the wrong thing.
612
+ const PROCESS_ENVIRONMENT = 'process.env.*';
613
+ // The global object, exempt in the setup file alone, so a project can supply an API its test runtime
614
+ // does not implement.
615
+ //
616
+ // ploaness MANDATES jsdom for `tests/component/**` - the suite table in `vitest.ts` is not a project's
617
+ // to change - and jsdom implements no `matchMedia`, no `ResizeObserver`, no `IntersectionObserver`. A
618
+ // component that reads one cannot be rendered until something supplies it, and every way of supplying
619
+ // it was closed: `vi.stubGlobal` by `NO_MOCK_PROPERTIES`, `test.environmentOptions` by the Vitest
620
+ // config having to be a bare re-export, and both spellings of an assignment by the rules below. The
621
+ // harness required an environment and then reported the only means of completing it.
622
+ //
623
+ // Written WITHOUT a trailing `.*`, which is the whole of the precision here rather than an oversight.
624
+ // An accessor pattern matches the path being written, and for `Object.defineProperty(globalThis, ...)`
625
+ // that path is the bare argument - so `globalThis` admits the call, while `globalThis.matchMedia = fn`
626
+ // keeps the path `globalThis.matchMedia`, matches nothing, and stays reported. That leaves exactly one
627
+ // legal spelling, and it is the one `unicorn/no-global-object-property-assignment` already demands: the
628
+ // two rules were pointing at each other, one banning the assignment and naming `defineProperty` as its
629
+ // fix, the other banning `defineProperty`. Same shape as the `void` marker, resolved the same way.
630
+ const RUNTIME_GLOBALS = 'globalThis';
631
+ // One constructor rather than an option object written at each site. A block that sets this rule REPLACES
632
+ // its options rather than adding to them, so the setup file's block has to restate the base carve-out to
633
+ // keep it - and restating it by hand is exactly how it would be dropped.
634
+ //
635
+ // An accessor pattern normalises computed access, so this covers `process.env['TZ']` as well as
636
+ // `process.env.TZ`, and neither reaches an unrelated `object.property`.
637
+ const immutableData = (accessors) => [
638
+ 'error',
639
+ { ignoreAccessorPattern: [PROCESS_VERDICT, ...accessors] },
640
+ ];
641
+ /** No `let`, no in-place mutation. The caller supplies the files and the generated-role exemptions. */
642
+ export const immutabilityBlock = (files, ignores) => ({
643
+ files,
644
+ ignores,
645
+ plugins: { functional },
646
+ rules: {
647
+ 'functional/no-let': 'error',
648
+ 'functional/immutable-data': immutableData([]),
649
+ },
650
+ });
651
+ /**
652
+ * The setup file's role, which is to configure the process rather than to hold program logic.
653
+ *
654
+ * A block of its own rather than a whole-file entry in `immutabilityBlock`'s `ignores`, so the file is
655
+ * still held to `no-let` and to every mutation that is not the environment. ploaness's own exemption is
656
+ * whole-file because installing a guard on `net.Socket.prototype` IS mutation of an existing object; a
657
+ * consumer's setup file needs the environment and nothing more.
658
+ */
659
+ export const processConfigBlock = () => ({
660
+ files: [PROJECT_SETUP_FILE],
661
+ // Mounted here rather than inherited. A setup file sits at the project ROOT, which the source globs of
662
+ // the block above need not reach - this repository's own do not - and a rule whose plugin no matching
663
+ // block defines makes ESLint reject the entire config. The plugin belongs in the block that states the
664
+ // rules naming it, which is the same reason `eslint-library.ts` mounts the Vitest plugin where it does.
665
+ plugins: { functional },
666
+ rules: {
667
+ 'functional/immutable-data': immutableData([PROCESS_ENVIRONMENT, RUNTIME_GLOBALS]),
668
+ },
669
+ });
670
+ /**
671
+ * A spec's need to VARY the environment, which the setup file's carve-out does not cover.
672
+ *
673
+ * The block above admits `process.env` at `vitest.setup.ts`, which configures the process once, before
674
+ * any spec reads it. That is not the same permission a spec needs: a branch taken only when a variable
675
+ * is unset, and another taken only when it is set, cannot both be observed from a value fixed for the
676
+ * whole run. Varying it per case was the missing half, and every mechanism for doing so was banned -
677
+ * `vi.stubEnv` by the mock ban, `test.env` by the Vitest config having to be a bare re-export, and a
678
+ * direct assignment by this rule. Three bans meeting over one need, while the coverage floor still
679
+ * required the branch. Found by a consumer whose endpoint returns 503 with no API key configured, which
680
+ * no legal test could reach.
681
+ *
682
+ * The exemption is the environment and nothing else, so `no-let` and every other in-place mutation still
683
+ * apply to a spec - and it is `tests/**` in both shipped configs rather than the Vitest globs alone,
684
+ * because two configs that disagree about one path are what `eslint-process-scope.spec.ts` exists to
685
+ * catch.
686
+ */
687
+ export const specEnvironmentBlock = () => ({
688
+ files: ['tests/**'],
689
+ // Mounted for the reason the block above states: a rule whose plugin no matching block defines makes
690
+ // ESLint reject the entire config.
691
+ plugins: { functional },
692
+ rules: {
693
+ 'functional/immutable-data': immutableData([PROCESS_ENVIRONMENT]),
694
+ },
695
+ });
696
+ /** The shared selectors, re-exported so a scoped block can spread rather than restate them. */
697
+ export { NO_FAST_CHECK_SEED, NO_INHERITANCE, NO_LITERAL_ASSERTIONS, NO_MOCK_PROPERTIES, NO_NETWORK_GUARD_ESCAPE, NO_TEST_ORDER_ESCAPE, };
698
+ //# sourceMappingURL=eslint-core.js.map