exadev-eslint-config 2.32.1 → 2.33.1
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 +41 -7
- package/dist/index.cjs +49 -8
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +49 -8
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -233,6 +233,19 @@ React/hooks/a11y and Next.js rule blocks fold in automatically, with no separate
|
|
|
233
233
|
pnpm add -D @next/eslint-plugin-next # Next.js support
|
|
234
234
|
```
|
|
235
235
|
If none of these resolve, `@exadev/eslint-config`'s default export is byte-for-byte identical to the plain TypeScript ruleset — nothing about the base package changes.
|
|
236
|
+
|
|
237
|
+
If you run [`eslint-plugin-depend`](https://github.com/es-tooling/eslint-plugin-depend)'s `ban-dependencies` on your `package.json`, it reports `eslint-plugin-react`, which the [module-replacements](https://github.com/es-tooling/module-replacements) `preferred` preset lists with a [suggested alternative](https://github.com/es-tooling/module-replacements/blob/main/docs/modules/eslint-plugin-react.md); the other React and Next.js peers are not listed. This package's React support is built on `eslint-plugin-react`, and it does not bundle `eslint-plugin-depend`, so allow the package in your own block. `allowed` takes exact package names, and the block needs no `language` of its own because `exadevConfig()` already lints `package.json` as `json/json`:
|
|
238
|
+
```ts
|
|
239
|
+
import { defineConfig } from 'eslint/config';
|
|
240
|
+
import depend from 'eslint-plugin-depend';
|
|
241
|
+
import { exadevConfig } from '@exadev/eslint-config';
|
|
242
|
+
|
|
243
|
+
export default defineConfig(...exadevConfig(), {
|
|
244
|
+
files: ['package.json'],
|
|
245
|
+
plugins: { depend },
|
|
246
|
+
rules: { 'depend/ban-dependencies': ['error', { allowed: ['eslint-plugin-react'] }] },
|
|
247
|
+
});
|
|
248
|
+
```
|
|
236
249
|
2. **For React specifically, the file must actually be `.jsx`/`.tsx`.** The React/hooks/a11y rule block is scoped to `files: ['**/*.jsx', '**/*.tsx']`, so even if `eslint-plugin-react` is resolvable only incidentally (e.g. hoisted as a transitive dependency of something unrelated in a monorepo, with zero real JSX anywhere in the linted project), its rules never match a file that isn't JSX. `@next/eslint-plugin-next`'s own block carries no such glob: its own presence is already an unambiguous signal (nothing installs it except a real Next.js project). The [server component boundary](#server-component-boundary) rules added beside it are scoped to the same JSX files.
|
|
237
250
|
|
|
238
251
|
React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `flat/jsx-runtime` config, turning [`react/react-in-jsx-scope`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md) and [`react/jsx-uses-react`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md) back off. `flat/recommended` alone assumes the classic runtime, where every file using JSX needs `import React` in scope; the automatic JSX runtime (the default since React 17, and the only mode Next.js's own compiler supports) needs no such import. Without this pairing, a consumer on the automatic runtime would see `react/react-in-jsx-scope` fire on every JSX file.
|
|
@@ -322,14 +335,14 @@ The plugin's own full-canonicalization rule ([`no-insignificant-whitespace`](htt
|
|
|
322
335
|
|
|
323
336
|
A hand-picked subset of [`@stylistic/eslint-plugin`](https://eslint.style) is bundled unconditionally, the same way jsdoc/tsdoc and RFC 8785 JSON support are: `@stylistic/eslint-plugin` is a plain dependency of this package, so every consumer already has it, and every rule below matches purely on comment shape, statement/class-member placement, or JSX prop/tag conventions, never on formatting (indentation, quotes, semicolons, trailing commas, line wrapping, bracket spacing), which stays Prettier's job in every consumer of this package. None of these rules appears in [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier)'s own disabled-rules list.
|
|
324
337
|
|
|
325
|
-
- **[`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style)** is deliberately NOT enabled
|
|
326
|
-
- **[`spaced-comment`](https://eslint.style/rules/spaced-comment)**: requires a space after `//`/`/*`, without fighting a `/**`-opening doc comment or a shebang line. Adds `{ block: { markers: ['!'] } }` on top of the bare default so a `/*!` license/banner block is not flagged either. A
|
|
338
|
+
- **[`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style)** is deliberately NOT enabled, because its `bare-block` fixer still merges a `TODO`/`FIXME` line in a run of `//` prose into one block comment. The installed 6.0.0-beta.6 carries the fix from [PR #1251](https://github.com/eslint-stylistic/eslint-stylistic/pull/1251) for [issue #1249](https://github.com/eslint-stylistic/eslint-stylistic/issues/1249), so `prettier-ignore`, `c8 ignore`/`v8 ignore`, `istanbul ignore`, `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check` and triple-slash reference/AMD directives (including the consecutive pair [issue #1285](https://github.com/eslint-stylistic/eslint-stylistic/issues/1285) reports) now stay standalone lines, but `TODO`/`FIXME` are not in the plugin's directive list and the rule has no option to add them. With the rule on, `exadev/prefer-doc-comment` stops converting the prose below a `TODO` into the declaration's doc comment, because the merged block it is left with contains a directive, so an exported declaration silently loses its doc comment. The fixer also writes a bare `\n` inside the merged block in a CRLF file. Tracked at [ExaDev/eslint-config#45](https://github.com/ExaDev/eslint-config/issues/45).
|
|
339
|
+
- **[`spaced-comment`](https://eslint.style/rules/spaced-comment)**: requires a space after `//`/`/*`, without fighting a `/**`-opening doc comment or a shebang line. Adds `{ block: { markers: ['!'] } }` on top of the bare default so a `/*!` license/banner block is not flagged either. A triple-slash reference/AMD directive is exempted by the rule itself in both its spaced form (`/// <reference types="..." />`, TypeScript's own compiler-emitted spelling) and its no-space form (`///<reference types="..." />`, also valid TypeScript syntax); before `@stylistic/eslint-plugin` 6.0.0, `--fix` rewrote the no-space form into a broken `// /<reference ...>` comment ([ExaDev/eslint-config#47](https://github.com/ExaDev/eslint-config/issues/47)). Also adds `{ line: { markers: ['#', '#region', '#endregion'] } }`: without it, the bare default rewrites a `//# sourceMappingURL=...`/`//# sourceURL=...` source-map comment into `// # sourceMappingURL=...`, which browsers and Node no longer recognise, and a bare `line: { markers: ['#'] }` alone would instead rewrite an unspaced `//#region`/`//#endregion` editor fold marker into `//# region`, which VS Code's own folding regex no longer matches. Listing all three markers together, confirmed directly against the installed rule, leaves every one of those four shapes untouched.
|
|
327
340
|
- **[`lines-between-class-members`](https://eslint.style/rules/lines-between-class-members)** at its bare default: a blank line between class members, except after a `.d.ts`-style method overload signature.
|
|
328
341
|
- **[`line-comment-position`](https://eslint.style/rules/line-comment-position)** at `'above'`: a `//` comment belongs above the code it describes, never trailing beside it on the same line. Not autofixable; a flagged trailing comment needs moving by hand. A trailing `// cspell:disable-line` is exempt (`ignorePattern`): cspell honours that directive only on the line it suppresses, so moving it above, as the report demands, would silently disable spell-checking for the wrong line. Every other directive this package recognises applies to the line(s) below it and is correctly relocated.
|
|
329
342
|
- **[`padding-line-between-statements`](https://eslint.style/rules/padding-line-between-statements)**: a blank line is required after the last statement of a directive prologue (not between two consecutive directives), after the last CommonJS or ES import in a run (not between two consecutive imports), and before a `return` statement.
|
|
330
|
-
- **[`jsx-curly-brace-presence`](https://eslint.style/rules/jsx-curly-brace-presence)**, **[`jsx-pascal-case`](https://eslint.style/rules/jsx-pascal-case)**, and **[`jsx-self-closing-comp`](https://eslint.style/rules/jsx-self-closing-comp)**, each at its own bare default: none of the three has any dependency on `eslint-plugin-react` or React itself (confirmed directly against `@stylistic/eslint-plugin`'s own `package.json`, whose
|
|
343
|
+
- **[`jsx-curly-brace-presence`](https://eslint.style/rules/jsx-curly-brace-presence)**, **[`jsx-pascal-case`](https://eslint.style/rules/jsx-pascal-case)**, and **[`jsx-self-closing-comp`](https://eslint.style/rules/jsx-self-closing-comp)**, each at its own bare default: none of the three has any dependency on `eslint-plugin-react` or React itself (confirmed directly against `@stylistic/eslint-plugin`'s own `package.json`, whose `peerDependencies` are `eslint` and `oxlint`, both optional), since each matches purely on a JSX AST node shape and is a structural no-op on every non-JSX file.
|
|
331
344
|
|
|
332
|
-
|
|
345
|
+
JSX's shorthand-boolean-prop convention (`<Foo enabled />` vs `<Foo enabled={true} />`) and shorthand-fragment syntax (`<>...</>` vs `<React.Fragment>...</React.Fragment>`) are not enforced: neither `@stylistic/eslint-plugin`'s own [`jsx-shorthand-boolean`](https://eslint.style/rules/jsx-shorthand-boolean) and [`jsx-shorthand-fragment`](https://eslint.style/rules/jsx-shorthand-fragment) nor `eslint-plugin-react`'s `react/jsx-boolean-value` and `react/jsx-fragments` are enabled, here or in the optional [React support](#optional-react-and-nextjs-support) gate above.
|
|
333
346
|
|
|
334
347
|
This package's own **`exadev/prefer-doc-comment`** rule (see [Rules](#rules)) is wired alongside these, in the same unconditional bundle.
|
|
335
348
|
|
|
@@ -515,11 +528,32 @@ A policy selects files with `files` and `ignores` and holds them to:
|
|
|
515
528
|
- `deny`: `specifiers` the files may not import. `message` is required and should name the requirement (why, and what to do instead), not the rule. `importNames` limits the ban to those imported names (`default` for a default import); a namespace import, a dynamic import, `require` and `export *` take every name, so they are still reported. `allowTypeImports` leaves erased imports alone.
|
|
516
529
|
- `confine`: `specifiers` the files may import only in the files `onlyIn` selects, so the inverse case needs no hand-built complement. `allowTypeImports` and an optional `message` work as for `deny`; the default message lists `onlyIn`.
|
|
517
530
|
- `exceptEdges`: one exact `file` and `specifier` pair with a required `reason`. Neither may contain a glob character, so an exception cannot widen. The specifier is compared exactly as written in that file, and `file` is normalised, so `./src/a.ts` and `src/a.ts` name the same file. Creating the config throws for an exception that could never apply (a file the policy does not select, or a specifier that no `deny` entry selects and no `confine` entry forbids in that file, such as one inside the confine's `onlyIn`), so a stale exception fails instead of lingering.
|
|
531
|
+
- `computedSpecifiers`: what to do with a dynamic `import()` or `require()` whose specifier is built at runtime (`` import(`./${name}`) ``), which no restriction can match. `'ignore'` (the default) skips it; `'report'` reports it in every file the policy selects, so those files can only pull a module in by a specifier the policy can check.
|
|
518
532
|
|
|
519
|
-
Specifiers follow [Specifier patterns](#specifier-patterns). The rule is syntactic and covers `import`, `import type`, `export ... from`, `export * from`, `import x = require()`, dynamic `import()` with a
|
|
533
|
+
Specifiers follow [Specifier patterns](#specifier-patterns). The rule is syntactic and covers `import`, `import type`, `export ... from`, `export * from`, `import x = require()`, dynamic `import()` with a string literal or a template with no substitutions, `require()` with such a string that is not shadowed, and `import('x')` type queries. A specifier built at runtime cannot be judged, so it is skipped unless the policy sets `computedSpecifiers: 'report'`.
|
|
520
534
|
|
|
521
535
|
ESLint's core `no-restricted-imports` covers only the static forms (`import`, `export ... from`, `export * from` and `import x = require()`); it has no check for dynamic `import()`, `require()` or `import('x')` type queries, and `no-restricted-modules` is deprecated since ESLint 7. A preset compiled to `no-restricted-imports` blocks would also inherit flat config's rule-level override: when two policies select the same file, the later block's option list replaces the earlier one's, and an exception edge would have to restate every other restriction of the file. So the preset compiles to one block for a purpose-built rule instead, where every policy that selects a file applies to it. Files outside a policy's `files` are untouched.
|
|
522
536
|
|
|
537
|
+
### Bans that dynamic `import()` bypasses
|
|
538
|
+
|
|
539
|
+
A `no-restricted-imports` ban (core or `@typescript-eslint/no-restricted-imports`, which extends it) reports `import { createFake } from 'pkg/fake'` but not `await import('pkg/fake')` or `` await import(`pkg/fake`) ``, because the rule has no handler for dynamic imports. A lazy route or component is exactly where a test double or a heavy module tends to be pulled in, so a ban meant to keep it out of shipped code should be an import policy, which judges the static, dynamic and `require()` forms with the same options:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
export default defineConfig(
|
|
543
|
+
...exadevConfig(),
|
|
544
|
+
...importPolicyConfig([
|
|
545
|
+
{
|
|
546
|
+
files: ['src/**'],
|
|
547
|
+
ignores: ['src/**/*.test.ts'],
|
|
548
|
+
deny: [{ specifiers: ['pkg/fake', 'src/testing'], message: 'test doubles stay out of shipped code; import them from tests only' }],
|
|
549
|
+
computedSpecifiers: 'report',
|
|
550
|
+
},
|
|
551
|
+
]),
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
The `no-restricted-imports` options map across as follows. A `paths` entry's `name` is a `specifiers` entry, except that a specifier also selects everything beneath it, so `pkg` covers `pkg/sub` as well. A `patterns` entry's `group` is a list of globs in the [specifier pattern](#specifier-patterns) dialect, with a leading `!` excluding. There is no `regex`: the glob dialect's `**`, `*`, `?`, `[...]`, braces and `!` excludes express the sets a ban needs, and a character class (`[Ff]ake`) stands in for case-insensitive matching. `importNames` and `message` carry over, `allowTypeImports` is the same flag, and a namespace import, dynamic import, `require()` or `export *` of a module whose `importNames` are banned is reported, since each takes every name. Avoid the `no-restricted-syntax` workaround (`ImportExpression > Literal[value=/.../]`): flat config replaces a rule's options when a later block sets the same rule for the same files, so that selector silently switches off any other `no-restricted-syntax` list the files already have.
|
|
556
|
+
|
|
523
557
|
## Pure modules
|
|
524
558
|
|
|
525
559
|
A functional core should not touch I/O, read the clock, roll dice or wait on anything. `pureModulesConfig` (or `exadevConfig({ pureModules })`) wires `exadev/pure-module` onto the files that must stay pure:
|
|
@@ -1429,7 +1463,7 @@ turboConfig({
|
|
|
1429
1463
|
### Build, test, and lint
|
|
1430
1464
|
|
|
1431
1465
|
```sh
|
|
1432
|
-
pnpm install # requires Node
|
|
1466
|
+
pnpm install # requires Node ^20.19.0 || ^22.13.0 || >=24 and the pnpm version pinned in package.json's packageManager field
|
|
1433
1467
|
pnpm lint
|
|
1434
1468
|
pnpm typecheck
|
|
1435
1469
|
pnpm test
|
|
@@ -1512,7 +1546,7 @@ pnpm build
|
|
|
1512
1546
|
export type { PlaywrightWiringOptions, PublishWiringOptions, RootTool, RootWiringOptions, StrykerWiringOptions, ToolConfigsWiringOptions, ToolingWiringOptions, VitestWiringOptions } from './tooling-wiring';
|
|
1513
1547
|
export type { EffectiveSeverity, EslintSample, EslintViolation, EslintViolationKind, RequiredSeverity, VerifyEslintOptions } from './verify-eslint';
|
|
1514
1548
|
export type { FileRequirement, PackageCondition, PackageRequirement, PackageRequirementsOptions } from './rules/package-requirements-options';
|
|
1515
|
-
export type { ImportConfine, ImportDeny, ImportExceptEdge, ImportPolicy } from './rules/import-policy-options';
|
|
1549
|
+
export type { ComputedSpecifierHandling, ImportConfine, ImportDeny, ImportExceptEdge, ImportPolicy } from './rules/import-policy-options';
|
|
1516
1550
|
export type { FilenamePatternEntry } from './rules/filename-pattern';
|
|
1517
1551
|
export type { RequiredHeading } from './rules/markdown-required-heading';
|
|
1518
1552
|
export type { RequiredExportsEntry } from './rules/required-exports';
|
package/dist/index.cjs
CHANGED
|
@@ -185,7 +185,7 @@ function buildGitignoreConfig(options = {}) {
|
|
|
185
185
|
}
|
|
186
186
|
//#endregion
|
|
187
187
|
//#region package.json
|
|
188
|
-
var version = "2.
|
|
188
|
+
var version = "2.33.1";
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/is-record.ts
|
|
191
191
|
/**
|
|
@@ -2537,6 +2537,7 @@ function stripNodePrefixFromPattern(pattern) {
|
|
|
2537
2537
|
//#endregion
|
|
2538
2538
|
//#region src/rules/import-policy-options.ts
|
|
2539
2539
|
const OPTION_NAME$20 = "importPolicies";
|
|
2540
|
+
const COMPUTED_SPECIFIER_HANDLINGS = ["ignore", "report"];
|
|
2540
2541
|
const GLOB_CHARACTERS$2 = /[*?[\]{}]/u;
|
|
2541
2542
|
const stringList$1 = {
|
|
2542
2543
|
type: "array",
|
|
@@ -2616,6 +2617,10 @@ const importPoliciesSchema = {
|
|
|
2616
2617
|
],
|
|
2617
2618
|
additionalProperties: false
|
|
2618
2619
|
}
|
|
2620
|
+
},
|
|
2621
|
+
computedSpecifiers: {
|
|
2622
|
+
type: "string",
|
|
2623
|
+
enum: [...COMPUTED_SPECIFIER_HANDLINGS]
|
|
2619
2624
|
}
|
|
2620
2625
|
},
|
|
2621
2626
|
required: ["files"],
|
|
@@ -2632,6 +2637,14 @@ function readBoolean$3(entry, field, name) {
|
|
|
2632
2637
|
if (value !== void 0 && typeof value !== "boolean") throw new Error(`@exadev/eslint-config: "${name}" needs "${field}" to be a boolean.`);
|
|
2633
2638
|
return value;
|
|
2634
2639
|
}
|
|
2640
|
+
function isComputedSpecifierHandling(value) {
|
|
2641
|
+
return COMPUTED_SPECIFIER_HANDLINGS.some((handling) => handling === value);
|
|
2642
|
+
}
|
|
2643
|
+
function readComputedSpecifiers(entry, name) {
|
|
2644
|
+
const value = entry["computedSpecifiers"];
|
|
2645
|
+
if (value === void 0 || isComputedSpecifierHandling(value)) return value;
|
|
2646
|
+
throw new Error(`@exadev/eslint-config: "${name}" needs "computedSpecifiers" to be one of ${COMPUTED_SPECIFIER_HANDLINGS.map((handling) => `"${handling}"`).join(", ")}.`);
|
|
2647
|
+
}
|
|
2635
2648
|
function readDeny(entry, name) {
|
|
2636
2649
|
assertOnlyKeys(entry, [
|
|
2637
2650
|
"specifiers",
|
|
@@ -2686,7 +2699,7 @@ function assertEdgeIsLive(edge, policy) {
|
|
|
2686
2699
|
if (!((policy.deny ?? []).some((deny) => createSpecifierMatcher(deny.specifiers)(edge.specifier, file, root)) || (policy.confine ?? []).some((confine) => createSpecifierMatcher(confine.specifiers)(edge.specifier, file, root) && !createFileScope(confine.onlyIn)(file, root)))) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" exception "${edge.specifier}" in "${edge.file}" is not forbidden there by any deny entry or by any confine entry whose onlyIn excludes the file, so it can never apply.`);
|
|
2687
2700
|
}
|
|
2688
2701
|
/**
|
|
2689
|
-
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2702
|
+
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: `computedSpecifiers`, when given, to be `ignore` or `report`; at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2690
2703
|
*/
|
|
2691
2704
|
function readImportPolicies(value) {
|
|
2692
2705
|
return readEntryRecords(value, OPTION_NAME$20).map((entry) => {
|
|
@@ -2695,13 +2708,15 @@ function readImportPolicies(value) {
|
|
|
2695
2708
|
"ignores",
|
|
2696
2709
|
"deny",
|
|
2697
2710
|
"confine",
|
|
2698
|
-
"exceptEdges"
|
|
2711
|
+
"exceptEdges",
|
|
2712
|
+
"computedSpecifiers"
|
|
2699
2713
|
], OPTION_NAME$20);
|
|
2700
2714
|
const files = readFileGlobs(entry["files"], `${OPTION_NAME$20}.files`);
|
|
2701
2715
|
const ignores = entry["ignores"] === void 0 ? void 0 : readFileGlobs(entry["ignores"], `${OPTION_NAME$20}.ignores`);
|
|
2702
2716
|
const deny = readOptionalList(entry, "deny", OPTION_NAME$20, readDeny);
|
|
2703
2717
|
const confine = readOptionalList(entry, "confine", OPTION_NAME$20, readConfine);
|
|
2704
2718
|
const exceptEdges = readOptionalList(entry, "exceptEdges", OPTION_NAME$20, readExceptEdge);
|
|
2719
|
+
const computedSpecifiers = readComputedSpecifiers(entry, OPTION_NAME$20);
|
|
2705
2720
|
if ((deny === void 0 || deny.length === 0) && (confine === void 0 || confine.length === 0)) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" needs every policy to have at least one "deny" or "confine" entry.`);
|
|
2706
2721
|
const policy = {
|
|
2707
2722
|
files,
|
|
@@ -2712,7 +2727,8 @@ function readImportPolicies(value) {
|
|
|
2712
2727
|
for (const edge of exceptEdges ?? []) assertEdgeIsLive(edge, policy);
|
|
2713
2728
|
return {
|
|
2714
2729
|
...policy,
|
|
2715
|
-
...exceptEdges !== void 0 && { exceptEdges }
|
|
2730
|
+
...exceptEdges !== void 0 && { exceptEdges },
|
|
2731
|
+
...computedSpecifiers !== void 0 && { computedSpecifiers }
|
|
2716
2732
|
};
|
|
2717
2733
|
});
|
|
2718
2734
|
}
|
|
@@ -2768,10 +2784,22 @@ function moduleReferenceOf(node, isRequireGlobal) {
|
|
|
2768
2784
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSImportEqualsDeclaration) return node.moduleReference.type === _typescript_eslint_utils.AST_NODE_TYPES.TSExternalModuleReference ? wholeModule(node, staticString(node.moduleReference.expression), node.importKind === "type") : void 0;
|
|
2769
2785
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.ImportExpression) return wholeModule(node, staticString(node.source), false);
|
|
2770
2786
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSImportType) return wholeModule(node, node.source.value, true);
|
|
2787
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2788
|
+
return argument === void 0 ? void 0 : wholeModule(node, staticString(argument), false);
|
|
2789
|
+
}
|
|
2790
|
+
function requireArgument(node, isRequireGlobal) {
|
|
2771
2791
|
if (node.type !== _typescript_eslint_utils.AST_NODE_TYPES.CallExpression) return void 0;
|
|
2772
2792
|
const [argument] = node.arguments;
|
|
2773
2793
|
if (node.callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || node.callee.name !== "require" || node.arguments.length !== 1 || argument === void 0 || !isRequireGlobal(node)) return void 0;
|
|
2774
|
-
return
|
|
2794
|
+
return argument;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Whether the node pulls a module in by a specifier that is not a static string, so no specifier pattern can judge it: a dynamic `import()` or a global `require()` whose argument is anything but a string literal or a substitution-free template. The static-specifier forms are `moduleReferenceOf`'s; a node is never both.
|
|
2798
|
+
*/
|
|
2799
|
+
function hasComputedSpecifier(node, isRequireGlobal) {
|
|
2800
|
+
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.ImportExpression) return staticString(node.source) === void 0;
|
|
2801
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2802
|
+
return argument !== void 0 && staticString(argument) === void 0;
|
|
2775
2803
|
}
|
|
2776
2804
|
function selectsNames(importNames, referenceNames) {
|
|
2777
2805
|
if (importNames === void 0) return true;
|
|
@@ -2796,7 +2824,8 @@ function compilePolicy(policy) {
|
|
|
2796
2824
|
return {
|
|
2797
2825
|
inScope: createFileScope([...policy.files, ...(policy.ignores ?? []).map((glob) => `!${glob}`)]),
|
|
2798
2826
|
restrictions: [...deny, ...confine],
|
|
2799
|
-
exceptEdges: policy.exceptEdges ?? []
|
|
2827
|
+
exceptEdges: policy.exceptEdges ?? [],
|
|
2828
|
+
reportsComputed: policy.computedSpecifiers === "report"
|
|
2800
2829
|
};
|
|
2801
2830
|
}
|
|
2802
2831
|
function isShadowed(sourceCode, call) {
|
|
@@ -2812,7 +2841,10 @@ const importPolicy = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) =>
|
|
|
2812
2841
|
type: "problem",
|
|
2813
2842
|
docs: { description: "Restrict which modules the files matching a glob may import: deny lists, confinement to named files, and exact exception edges." },
|
|
2814
2843
|
schema: [importPoliciesSchema],
|
|
2815
|
-
messages: {
|
|
2844
|
+
messages: {
|
|
2845
|
+
restricted: "\"{{ specifier }}\" cannot be imported here: {{ message }}",
|
|
2846
|
+
computed: "This module specifier is computed at runtime, so the import policy for this file cannot check it. Write it as a string literal."
|
|
2847
|
+
}
|
|
2816
2848
|
},
|
|
2817
2849
|
defaultOptions: [[]],
|
|
2818
2850
|
create(context, [options]) {
|
|
@@ -2820,8 +2852,17 @@ const importPolicy = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) =>
|
|
|
2820
2852
|
const policies = readImportPolicies(options).map(compilePolicy).filter((policy) => policy.inScope(filename, cwd));
|
|
2821
2853
|
if (policies.length === 0) return {};
|
|
2822
2854
|
const relativePath = relativeToCwd(filename, cwd);
|
|
2855
|
+
const reportsComputed = policies.some((policy) => policy.reportsComputed);
|
|
2856
|
+
const isRequireGlobal = (call) => !isShadowed(sourceCode, call);
|
|
2823
2857
|
const check = (node) => {
|
|
2824
|
-
|
|
2858
|
+
if (reportsComputed && hasComputedSpecifier(node, isRequireGlobal)) {
|
|
2859
|
+
context.report({
|
|
2860
|
+
node,
|
|
2861
|
+
messageId: "computed"
|
|
2862
|
+
});
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const reference = moduleReferenceOf(node, isRequireGlobal);
|
|
2825
2866
|
if (reference === void 0) return;
|
|
2826
2867
|
for (const policy of policies) {
|
|
2827
2868
|
if (policy.exceptEdges.some((edge) => edge.file === relativePath && edge.specifier === reference.specifier)) continue;
|
package/dist/index.d.cts
CHANGED
|
@@ -265,6 +265,10 @@ interface ImportExceptEdge {
|
|
|
265
265
|
readonly specifier: string;
|
|
266
266
|
readonly reason: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* What a policy does with a dynamic `import()` or a `require()` whose specifier is not a static string (a variable, a concatenation, or a template with a substitution), which no restriction can match: `ignore` it (the default), or `report` it, so the files the policy selects can only pull modules in by a specifier it can check.
|
|
270
|
+
*/
|
|
271
|
+
type ComputedSpecifierHandling = 'ignore' | 'report';
|
|
268
272
|
/**
|
|
269
273
|
* Import restrictions for the files `files` selects and `ignores` does not. At least one of `deny` and `confine` is required.
|
|
270
274
|
*/
|
|
@@ -274,6 +278,7 @@ interface ImportPolicy {
|
|
|
274
278
|
readonly deny?: readonly ImportDeny[];
|
|
275
279
|
readonly confine?: readonly ImportConfine[];
|
|
276
280
|
readonly exceptEdges?: readonly ImportExceptEdge[];
|
|
281
|
+
readonly computedSpecifiers?: ComputedSpecifierHandling;
|
|
277
282
|
}
|
|
278
283
|
//#endregion
|
|
279
284
|
//#region src/rules/turbo-commands.d.ts
|
|
@@ -524,4 +529,4 @@ interface RequiredImportsEntry {
|
|
|
524
529
|
readonly call?: boolean;
|
|
525
530
|
}
|
|
526
531
|
//#endregion
|
|
527
|
-
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
|
532
|
+
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type ComputedSpecifierHandling, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -265,6 +265,10 @@ interface ImportExceptEdge {
|
|
|
265
265
|
readonly specifier: string;
|
|
266
266
|
readonly reason: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* What a policy does with a dynamic `import()` or a `require()` whose specifier is not a static string (a variable, a concatenation, or a template with a substitution), which no restriction can match: `ignore` it (the default), or `report` it, so the files the policy selects can only pull modules in by a specifier it can check.
|
|
270
|
+
*/
|
|
271
|
+
type ComputedSpecifierHandling = 'ignore' | 'report';
|
|
268
272
|
/**
|
|
269
273
|
* Import restrictions for the files `files` selects and `ignores` does not. At least one of `deny` and `confine` is required.
|
|
270
274
|
*/
|
|
@@ -274,6 +278,7 @@ interface ImportPolicy {
|
|
|
274
278
|
readonly deny?: readonly ImportDeny[];
|
|
275
279
|
readonly confine?: readonly ImportConfine[];
|
|
276
280
|
readonly exceptEdges?: readonly ImportExceptEdge[];
|
|
281
|
+
readonly computedSpecifiers?: ComputedSpecifierHandling;
|
|
277
282
|
}
|
|
278
283
|
//#endregion
|
|
279
284
|
//#region src/rules/turbo-commands.d.ts
|
|
@@ -524,4 +529,4 @@ interface RequiredImportsEntry {
|
|
|
524
529
|
readonly call?: boolean;
|
|
525
530
|
}
|
|
526
531
|
//#endregion
|
|
527
|
-
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
|
532
|
+
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type ComputedSpecifierHandling, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
package/dist/index.js
CHANGED
|
@@ -150,7 +150,7 @@ function buildGitignoreConfig(options = {}) {
|
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
152
|
//#region package.json
|
|
153
|
-
var version = "2.
|
|
153
|
+
var version = "2.33.1";
|
|
154
154
|
//#endregion
|
|
155
155
|
//#region src/is-record.ts
|
|
156
156
|
/**
|
|
@@ -2502,6 +2502,7 @@ function stripNodePrefixFromPattern(pattern) {
|
|
|
2502
2502
|
//#endregion
|
|
2503
2503
|
//#region src/rules/import-policy-options.ts
|
|
2504
2504
|
const OPTION_NAME$20 = "importPolicies";
|
|
2505
|
+
const COMPUTED_SPECIFIER_HANDLINGS = ["ignore", "report"];
|
|
2505
2506
|
const GLOB_CHARACTERS$2 = /[*?[\]{}]/u;
|
|
2506
2507
|
const stringList$1 = {
|
|
2507
2508
|
type: "array",
|
|
@@ -2581,6 +2582,10 @@ const importPoliciesSchema = {
|
|
|
2581
2582
|
],
|
|
2582
2583
|
additionalProperties: false
|
|
2583
2584
|
}
|
|
2585
|
+
},
|
|
2586
|
+
computedSpecifiers: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
enum: [...COMPUTED_SPECIFIER_HANDLINGS]
|
|
2584
2589
|
}
|
|
2585
2590
|
},
|
|
2586
2591
|
required: ["files"],
|
|
@@ -2597,6 +2602,14 @@ function readBoolean$3(entry, field, name) {
|
|
|
2597
2602
|
if (value !== void 0 && typeof value !== "boolean") throw new Error(`@exadev/eslint-config: "${name}" needs "${field}" to be a boolean.`);
|
|
2598
2603
|
return value;
|
|
2599
2604
|
}
|
|
2605
|
+
function isComputedSpecifierHandling(value) {
|
|
2606
|
+
return COMPUTED_SPECIFIER_HANDLINGS.some((handling) => handling === value);
|
|
2607
|
+
}
|
|
2608
|
+
function readComputedSpecifiers(entry, name) {
|
|
2609
|
+
const value = entry["computedSpecifiers"];
|
|
2610
|
+
if (value === void 0 || isComputedSpecifierHandling(value)) return value;
|
|
2611
|
+
throw new Error(`@exadev/eslint-config: "${name}" needs "computedSpecifiers" to be one of ${COMPUTED_SPECIFIER_HANDLINGS.map((handling) => `"${handling}"`).join(", ")}.`);
|
|
2612
|
+
}
|
|
2600
2613
|
function readDeny(entry, name) {
|
|
2601
2614
|
assertOnlyKeys(entry, [
|
|
2602
2615
|
"specifiers",
|
|
@@ -2651,7 +2664,7 @@ function assertEdgeIsLive(edge, policy) {
|
|
|
2651
2664
|
if (!((policy.deny ?? []).some((deny) => createSpecifierMatcher(deny.specifiers)(edge.specifier, file, root)) || (policy.confine ?? []).some((confine) => createSpecifierMatcher(confine.specifiers)(edge.specifier, file, root) && !createFileScope(confine.onlyIn)(file, root)))) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" exception "${edge.specifier}" in "${edge.file}" is not forbidden there by any deny entry or by any confine entry whose onlyIn excludes the file, so it can never apply.`);
|
|
2652
2665
|
}
|
|
2653
2666
|
/**
|
|
2654
|
-
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2667
|
+
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: `computedSpecifiers`, when given, to be `ignore` or `report`; at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2655
2668
|
*/
|
|
2656
2669
|
function readImportPolicies(value) {
|
|
2657
2670
|
return readEntryRecords(value, OPTION_NAME$20).map((entry) => {
|
|
@@ -2660,13 +2673,15 @@ function readImportPolicies(value) {
|
|
|
2660
2673
|
"ignores",
|
|
2661
2674
|
"deny",
|
|
2662
2675
|
"confine",
|
|
2663
|
-
"exceptEdges"
|
|
2676
|
+
"exceptEdges",
|
|
2677
|
+
"computedSpecifiers"
|
|
2664
2678
|
], OPTION_NAME$20);
|
|
2665
2679
|
const files = readFileGlobs(entry["files"], `${OPTION_NAME$20}.files`);
|
|
2666
2680
|
const ignores = entry["ignores"] === void 0 ? void 0 : readFileGlobs(entry["ignores"], `${OPTION_NAME$20}.ignores`);
|
|
2667
2681
|
const deny = readOptionalList(entry, "deny", OPTION_NAME$20, readDeny);
|
|
2668
2682
|
const confine = readOptionalList(entry, "confine", OPTION_NAME$20, readConfine);
|
|
2669
2683
|
const exceptEdges = readOptionalList(entry, "exceptEdges", OPTION_NAME$20, readExceptEdge);
|
|
2684
|
+
const computedSpecifiers = readComputedSpecifiers(entry, OPTION_NAME$20);
|
|
2670
2685
|
if ((deny === void 0 || deny.length === 0) && (confine === void 0 || confine.length === 0)) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" needs every policy to have at least one "deny" or "confine" entry.`);
|
|
2671
2686
|
const policy = {
|
|
2672
2687
|
files,
|
|
@@ -2677,7 +2692,8 @@ function readImportPolicies(value) {
|
|
|
2677
2692
|
for (const edge of exceptEdges ?? []) assertEdgeIsLive(edge, policy);
|
|
2678
2693
|
return {
|
|
2679
2694
|
...policy,
|
|
2680
|
-
...exceptEdges !== void 0 && { exceptEdges }
|
|
2695
|
+
...exceptEdges !== void 0 && { exceptEdges },
|
|
2696
|
+
...computedSpecifiers !== void 0 && { computedSpecifiers }
|
|
2681
2697
|
};
|
|
2682
2698
|
});
|
|
2683
2699
|
}
|
|
@@ -2733,10 +2749,22 @@ function moduleReferenceOf(node, isRequireGlobal) {
|
|
|
2733
2749
|
if (node.type === AST_NODE_TYPES.TSImportEqualsDeclaration) return node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference ? wholeModule(node, staticString(node.moduleReference.expression), node.importKind === "type") : void 0;
|
|
2734
2750
|
if (node.type === AST_NODE_TYPES.ImportExpression) return wholeModule(node, staticString(node.source), false);
|
|
2735
2751
|
if (node.type === AST_NODE_TYPES.TSImportType) return wholeModule(node, node.source.value, true);
|
|
2752
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2753
|
+
return argument === void 0 ? void 0 : wholeModule(node, staticString(argument), false);
|
|
2754
|
+
}
|
|
2755
|
+
function requireArgument(node, isRequireGlobal) {
|
|
2736
2756
|
if (node.type !== AST_NODE_TYPES.CallExpression) return void 0;
|
|
2737
2757
|
const [argument] = node.arguments;
|
|
2738
2758
|
if (node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== "require" || node.arguments.length !== 1 || argument === void 0 || !isRequireGlobal(node)) return void 0;
|
|
2739
|
-
return
|
|
2759
|
+
return argument;
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Whether the node pulls a module in by a specifier that is not a static string, so no specifier pattern can judge it: a dynamic `import()` or a global `require()` whose argument is anything but a string literal or a substitution-free template. The static-specifier forms are `moduleReferenceOf`'s; a node is never both.
|
|
2763
|
+
*/
|
|
2764
|
+
function hasComputedSpecifier(node, isRequireGlobal) {
|
|
2765
|
+
if (node.type === AST_NODE_TYPES.ImportExpression) return staticString(node.source) === void 0;
|
|
2766
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2767
|
+
return argument !== void 0 && staticString(argument) === void 0;
|
|
2740
2768
|
}
|
|
2741
2769
|
function selectsNames(importNames, referenceNames) {
|
|
2742
2770
|
if (importNames === void 0) return true;
|
|
@@ -2761,7 +2789,8 @@ function compilePolicy(policy) {
|
|
|
2761
2789
|
return {
|
|
2762
2790
|
inScope: createFileScope([...policy.files, ...(policy.ignores ?? []).map((glob) => `!${glob}`)]),
|
|
2763
2791
|
restrictions: [...deny, ...confine],
|
|
2764
|
-
exceptEdges: policy.exceptEdges ?? []
|
|
2792
|
+
exceptEdges: policy.exceptEdges ?? [],
|
|
2793
|
+
reportsComputed: policy.computedSpecifiers === "report"
|
|
2765
2794
|
};
|
|
2766
2795
|
}
|
|
2767
2796
|
function isShadowed(sourceCode, call) {
|
|
@@ -2777,7 +2806,10 @@ const importPolicy = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDe
|
|
|
2777
2806
|
type: "problem",
|
|
2778
2807
|
docs: { description: "Restrict which modules the files matching a glob may import: deny lists, confinement to named files, and exact exception edges." },
|
|
2779
2808
|
schema: [importPoliciesSchema],
|
|
2780
|
-
messages: {
|
|
2809
|
+
messages: {
|
|
2810
|
+
restricted: "\"{{ specifier }}\" cannot be imported here: {{ message }}",
|
|
2811
|
+
computed: "This module specifier is computed at runtime, so the import policy for this file cannot check it. Write it as a string literal."
|
|
2812
|
+
}
|
|
2781
2813
|
},
|
|
2782
2814
|
defaultOptions: [[]],
|
|
2783
2815
|
create(context, [options]) {
|
|
@@ -2785,8 +2817,17 @@ const importPolicy = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDe
|
|
|
2785
2817
|
const policies = readImportPolicies(options).map(compilePolicy).filter((policy) => policy.inScope(filename, cwd));
|
|
2786
2818
|
if (policies.length === 0) return {};
|
|
2787
2819
|
const relativePath = relativeToCwd(filename, cwd);
|
|
2820
|
+
const reportsComputed = policies.some((policy) => policy.reportsComputed);
|
|
2821
|
+
const isRequireGlobal = (call) => !isShadowed(sourceCode, call);
|
|
2788
2822
|
const check = (node) => {
|
|
2789
|
-
|
|
2823
|
+
if (reportsComputed && hasComputedSpecifier(node, isRequireGlobal)) {
|
|
2824
|
+
context.report({
|
|
2825
|
+
node,
|
|
2826
|
+
messageId: "computed"
|
|
2827
|
+
});
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const reference = moduleReferenceOf(node, isRequireGlobal);
|
|
2790
2831
|
if (reference === void 0) return;
|
|
2791
2832
|
for (const policy of policies) {
|
|
2792
2833
|
if (policy.exceptEdges.some((edge) => edge.file === relativePath && edge.specifier === reference.specifier)) continue;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exadev-eslint-config",
|
|
3
3
|
"description": "Shared custom ESLint rules and plugin for ExaDev projects",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.33.1",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@eslint/js": "^10.0.1",
|
|
7
7
|
"@microsoft/tsdoc": "0.17.0",
|
|
8
|
-
"@stylistic/eslint-plugin": "
|
|
8
|
+
"@stylistic/eslint-plugin": "6.0.0-beta.6",
|
|
9
9
|
"@typescript-eslint/typescript-estree": "8.67.0",
|
|
10
10
|
"@typescript-eslint/utils": "8.67.0",
|
|
11
11
|
"eslint-plugin-jsdoc": "64.3.6",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
36
36
|
"cosmiconfig": "10.0.1",
|
|
37
37
|
"eslint": "^10.8.0",
|
|
38
|
+
"eslint-plugin-depend": "1.5.0",
|
|
38
39
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
39
40
|
"eslint-plugin-react": "^7.37.5",
|
|
40
41
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"vitest": "^4.1.10"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
|
-
"node": ">=
|
|
54
|
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
54
55
|
},
|
|
55
56
|
"exports": {
|
|
56
57
|
".": {
|