exadev-eslint-config 2.34.1 → 2.35.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.
- package/README.md +159 -2
- package/dist/index.cjs +30 -10
- package/dist/index.d.cts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +30 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -529,7 +529,7 @@ export default defineConfig(
|
|
|
529
529
|
|
|
530
530
|
A policy selects files with `files` and `ignores` and holds them to:
|
|
531
531
|
|
|
532
|
-
- `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
|
|
532
|
+
- `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); `allowImportNames` is its inverse, banning every imported name except those, so a name the module gains later is banned until it is listed. An entry takes one of the two, not both. Either way a namespace import, a dynamic import, `require` and `export *` take every name, so they are reported. `allowTypeImports` leaves erased imports alone.
|
|
533
533
|
- `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`.
|
|
534
534
|
- `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.
|
|
535
535
|
- `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.
|
|
@@ -556,7 +556,164 @@ export default defineConfig(
|
|
|
556
556
|
);
|
|
557
557
|
```
|
|
558
558
|
|
|
559
|
-
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.
|
|
559
|
+
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`, `allowImportNames` 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, or whose names are limited by `allowImportNames`, 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.
|
|
560
|
+
|
|
561
|
+
### Import-policy recipes
|
|
562
|
+
|
|
563
|
+
Repository conventions that come up again and again, each written as the config to place after `...exadevConfig()` in `defineConfig(...)`. Every recipe is linted by [`src/import-policy-recipes.unit.test.ts`](src/import-policy-recipes.unit.test.ts), against [`src/import-policy-recipes.ts`](src/import-policy-recipes.ts), which holds the same configs, so a recipe that stops reporting what this section says fails a test. Paths and package names are placeholders. Policies for different conventions can share one `importPolicyConfig([...])` call or sit in several; either way every policy that selects a file applies to it.
|
|
564
|
+
|
|
565
|
+
#### A vendor SDK behind its adapter
|
|
566
|
+
|
|
567
|
+
The vendor SDK is imported only in its adapter directory, and deterministic code (here, workflow code that is replayed) imports neither the SDK nor the adapter:
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
...importPolicyConfig([
|
|
571
|
+
{
|
|
572
|
+
files: ['src/**'],
|
|
573
|
+
ignores: ['src/**/*.test.ts'],
|
|
574
|
+
confine: [{ specifiers: ['@vendor/model-sdk'], onlyIn: ['src/adapters/model/**'], message: 'call the model through src/adapters/model, which owns the vendor SDK' }],
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
files: ['src/workflows/**'],
|
|
578
|
+
deny: [
|
|
579
|
+
{
|
|
580
|
+
specifiers: ['@vendor/model-sdk', 'src/adapters/model'],
|
|
581
|
+
allowTypeImports: true,
|
|
582
|
+
message: 'workflow code is replayed and must be deterministic; call the model from an activity',
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
]),
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
`confine` covers subpaths and every import form, so `await import('@vendor/model-sdk/streaming')` in a feature is reported as well. The deny's `src/adapters/model` pattern matches the relative specifier a workflow writes (`../adapters/model/summarise`) by the path it resolves to. `allowTypeImports` lets workflow code name the adapter's types, which are erased before it runs; the confine has no such flag, so even a type import of the SDK outside the adapter is reported, keeping vendor types from leaking into domain code. A workflow importing the SDK breaks both policies and is reported once for each, with each message.
|
|
590
|
+
|
|
591
|
+
#### Test doubles, fixtures and conformance kits stay out of shipped code
|
|
592
|
+
|
|
593
|
+
One deny list over every shipped file, extending [Bans that dynamic `import()` bypasses](#bans-that-dynamic-import-bypasses):
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
...importPolicyConfig([
|
|
597
|
+
{
|
|
598
|
+
files: ['src/**'],
|
|
599
|
+
ignores: ['src/**/*.test.ts', 'src/**/*.fake.ts', 'src/testing/**'],
|
|
600
|
+
deny: [
|
|
601
|
+
{
|
|
602
|
+
specifiers: ['src/testing', 'src/**/__fixtures__', 'src/**/*.fake{,.js}', '@scope/*/testing', '@scope/*/conformance'],
|
|
603
|
+
message: 'test doubles, fixtures and conformance kits stay out of shipped code; import them from tests only',
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
computedSpecifiers: 'report',
|
|
607
|
+
},
|
|
608
|
+
]),
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
The relative patterns select a shared testing directory, a fixtures directory at any depth and a fake beside the code it stands in for, written with or without the `.js` extension that `NodeNext` resolution needs. The package patterns select the testing and conformance entry points of every package in the scope, so `@scope/orders-contract/conformance` is reported in shipped code while `@scope/orders-contract` is not. The test files, the fakes themselves and the testing directory are ignored, since they are where these modules belong. `computedSpecifiers: 'report'` closes the gap a lazy `` import(`./plugins/${name}`) `` would otherwise leave, at the price of every computed specifier in shipped code being reported.
|
|
612
|
+
|
|
613
|
+
#### Handlers built only from the authenticated procedure
|
|
614
|
+
|
|
615
|
+
Every request handler is built from the procedure that requires a signed-in caller. Listing the names a router may import, rather than the ones it may not, keeps a builder the procedures module adds later out of the routers until it is listed:
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
...importPolicyConfig([
|
|
619
|
+
{
|
|
620
|
+
files: ['src/server/routers/**'],
|
|
621
|
+
deny: [
|
|
622
|
+
{
|
|
623
|
+
specifiers: ['src/server/procedures'],
|
|
624
|
+
allowImportNames: ['router', 'protectedProcedure'],
|
|
625
|
+
allowTypeImports: true,
|
|
626
|
+
message: 'every handler requires a signed-in caller; build it from protectedProcedure',
|
|
627
|
+
},
|
|
628
|
+
{ specifiers: ['@scope/rpc-server'], allowTypeImports: true, message: 'procedures are built in src/server/procedures, not in a router' },
|
|
629
|
+
],
|
|
630
|
+
exceptEdges: [{ file: 'src/server/routers/health.ts', specifier: '../procedures', reason: 'the health check answers before sign-in' }],
|
|
631
|
+
},
|
|
632
|
+
]),
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
`import { publicProcedure } from '../procedures'` is reported, and so is `import * as procedures from '../procedures'`, since a namespace takes every name. The second entry stops a router building its own procedure from the RPC library. The exception lets `health.ts`, and no other router, import any name from the procedures module; the RPC server package stays banned there too, since the exception names only the `../procedures` specifier.
|
|
636
|
+
|
|
637
|
+
#### Model constructors in one module
|
|
638
|
+
|
|
639
|
+
The models are constructed in one module, which pins their settings, while the SDK's other exports (error classes, types) stay importable wherever the SDK is:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
...importPolicyConfig([
|
|
643
|
+
{
|
|
644
|
+
files: ['src/**'],
|
|
645
|
+
ignores: ['src/adapters/model/models.ts'],
|
|
646
|
+
deny: [{ specifiers: ['@vendor/model-sdk'], importNames: ['ChatModel', 'EmbeddingModel'], message: 'models are constructed once, in src/adapters/model/models.ts' }],
|
|
647
|
+
},
|
|
648
|
+
]),
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
A renamed import (`EmbeddingModel as Embedder`) is matched by the name it imports, and a namespace import is reported because it reaches the constructors. Combined with the vendor SDK recipe above, which confines the SDK to `src/adapters/model/**`, the two narrow the constructors to the one file.
|
|
652
|
+
|
|
653
|
+
#### Syntax bans scoped to a set of files
|
|
654
|
+
|
|
655
|
+
Some conventions are about what a file does rather than what it imports. A scoped [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) ([latest archived copy](https://web.archive.org/web/https://eslint.org/docs/latest/rules/no-restricted-syntax)) block states those, with an [esquery selector](https://eslint.org/docs/latest/extend/selectors) ([latest archived copy](https://web.archive.org/web/https://eslint.org/docs/latest/extend/selectors)) per entry. Provider keys are not read in browser files, roles are not compared inline in handlers, and telemetry content recording is switched on or off only in its settings module:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
const telemetryContentFlags = [
|
|
659
|
+
{
|
|
660
|
+
selector: 'ObjectExpression > Property:matches([key.name=/^record(Inputs|Outputs)$/], [key.value=/^record(Inputs|Outputs)$/])',
|
|
661
|
+
message: 'telemetry content recording is set in src/telemetry/settings.ts only',
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
const providerKeyReads = [
|
|
665
|
+
{
|
|
666
|
+
selector: "MemberExpression[object.object.name='process'][object.property.name='env']:matches([property.name=/_(API_KEY|SECRET|TOKEN)$/], [property.value=/_(API_KEY|SECRET|TOKEN)$/])",
|
|
667
|
+
message: 'browser code is shipped to the user, so provider keys are read on the server only',
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
selector: "VariableDeclarator[init.object.name='process'][init.property.name='env'] > ObjectPattern > Property:matches([key.name=/_(API_KEY|SECRET|TOKEN)$/], [key.value=/_(API_KEY|SECRET|TOKEN)$/])",
|
|
671
|
+
message: 'browser code is shipped to the user, so provider keys are read on the server only',
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
const inlineRoleChecks = [
|
|
675
|
+
{ selector: "BinaryExpression[operator=/^[!=]==?$/] > MemberExpression[property.name='role']", message: 'check permissions through the can() policy helper, not by comparing roles in a handler' },
|
|
676
|
+
{ selector: "SwitchStatement > MemberExpression.discriminant[property.name='role']", message: 'check permissions through the can() policy helper, not by comparing roles in a handler' },
|
|
677
|
+
{ selector: "CallExpression[callee.property.name='includes'] > MemberExpression.arguments[property.name='role']", message: 'check permissions through the can() policy helper, not by comparing roles in a handler' },
|
|
678
|
+
];
|
|
679
|
+
|
|
680
|
+
export default defineConfig(
|
|
681
|
+
...exadevConfig(),
|
|
682
|
+
{
|
|
683
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
684
|
+
ignores: ['src/telemetry/settings.ts'],
|
|
685
|
+
rules: { 'no-restricted-syntax': ['error', ...telemetryContentFlags] },
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
files: ['src/client/**/*.{ts,tsx}'],
|
|
689
|
+
rules: { 'no-restricted-syntax': ['error', ...telemetryContentFlags, ...providerKeyReads] },
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
files: ['src/server/routers/**/*.ts'],
|
|
693
|
+
rules: { 'no-restricted-syntax': ['error', ...telemetryContentFlags, ...inlineRoleChecks] },
|
|
694
|
+
},
|
|
695
|
+
);
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**The flat-config trap.** When two blocks set the same rule for the same file, the later block's options replace the earlier block's; they are not merged. A router file is selected by both the `src/**` block and the routers block, so if the routers block listed only `inlineRoleChecks`, the telemetry ban would silently stop applying to every router. Hence each block restates the entries of every broader block that also selects its files, built from shared constants so the lists cannot drift. `exadevConfig()` sets no `no-restricted-syntax` list of its own, so the only lists to restate are the repository's own; a repository that already has one for these files adds it to every block in the same way. Import bans do not have this problem, which is why they belong in `importPolicyConfig` rather than in `no-restricted-syntax` or `no-restricted-imports` blocks.
|
|
699
|
+
|
|
700
|
+
How the selectors read, confirmed by the tests:
|
|
701
|
+
|
|
702
|
+
- The key reads match `process.env.MODEL_API_KEY`, `process.env['WEBHOOK_SECRET']` (`property.value` is the string key, `property.name` the identifier) and `const { SEARCH_TOKEN } = process.env`. A name built at runtime (`process.env[name]`), an alias of `process.env`, or a framework's own environment object is not matched, so pair the ban with keeping the module that reads secrets out of browser code, as an import policy deny.
|
|
703
|
+
- The role checks match `ctx.user.role === 'admin'` with the property on either side, `switch (user.role)` and `roles.includes(user.role)`. A role read into a local first (`const { role } = user`) is not matched; the selector judges the syntax of one expression.
|
|
704
|
+
- The telemetry flag matches a `recordInputs` or `recordOutputs` key in an object literal, quoted or not, whatever its value, so setting it to `false` elsewhere is reported too: where it is set is the convention. `ObjectExpression >` keeps destructuring (`const { recordInputs } = options`) out of it.
|
|
705
|
+
|
|
706
|
+
#### Conventions that are not per-file lint rules
|
|
707
|
+
|
|
708
|
+
A lint rule sees one file. A convention whose correctness depends on another file's contents cannot be stated as one, and a recipe that approximated it would pass code that breaks the convention. These go elsewhere:
|
|
709
|
+
|
|
710
|
+
| Convention | Why one file is not enough | Where it goes |
|
|
711
|
+
| --- | --- | --- |
|
|
712
|
+
| Types derived from a schema, never hand-written beside it | Whether a type duplicates a schema depends on the schema, declared in another file or package. | [`workspace-conformance`](https://github.com/ExaDev/workspace-conformance): its `command-types` check requires every exported command type to be inferred from a Standard Schema; other kinds of type need the same type-graph check over their files. |
|
|
713
|
+
| Codec pairs | An encoder and its decoder live in different files, and they agree only if one undoes the other. | That every encoder has a decoder is a type-graph check for `workspace-conformance`, in the shape of its `aggregate-mappers` check; that they round-trip is a property test. |
|
|
714
|
+
| Row types derived from the table schema | The row type is right only if it is inferred from the table definition in another file. | A type-graph check in `workspace-conformance`. `command-types` recognises Standard Schema inference, which a table builder's own inference is not. |
|
|
715
|
+
| Error-channel schemas | Whether every error a contract can raise has a schema needs the contract's error union and the schema module together. | A type-graph check in `workspace-conformance`. |
|
|
716
|
+
| Pagination slicing | A page is correct only if the slice a repository returns agrees with the cursor and limit the contract declares, and `.slice()` on an array is ordinary code everywhere else. | Catalogue-only: a conformance suite that every repository implementation runs, which [`required-imports`](#required-imports) can require each adapter's tests to wire in. |
|
|
560
717
|
|
|
561
718
|
## Pure modules
|
|
562
719
|
|
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.35.0";
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/is-record.ts
|
|
191
191
|
/**
|
|
@@ -2565,6 +2565,7 @@ const importPoliciesSchema = {
|
|
|
2565
2565
|
properties: {
|
|
2566
2566
|
specifiers: stringList$1,
|
|
2567
2567
|
importNames: stringList$1,
|
|
2568
|
+
allowImportNames: stringList$1,
|
|
2568
2569
|
allowTypeImports: { type: "boolean" },
|
|
2569
2570
|
message: {
|
|
2570
2571
|
type: "string",
|
|
@@ -2572,6 +2573,10 @@ const importPoliciesSchema = {
|
|
|
2572
2573
|
}
|
|
2573
2574
|
},
|
|
2574
2575
|
required: ["specifiers", "message"],
|
|
2576
|
+
not: {
|
|
2577
|
+
type: "object",
|
|
2578
|
+
required: ["importNames", "allowImportNames"]
|
|
2579
|
+
},
|
|
2575
2580
|
additionalProperties: false
|
|
2576
2581
|
}
|
|
2577
2582
|
},
|
|
@@ -2649,17 +2654,27 @@ function readDeny(entry, name) {
|
|
|
2649
2654
|
assertOnlyKeys(entry, [
|
|
2650
2655
|
"specifiers",
|
|
2651
2656
|
"importNames",
|
|
2657
|
+
"allowImportNames",
|
|
2652
2658
|
"allowTypeImports",
|
|
2653
2659
|
"message"
|
|
2654
2660
|
], name);
|
|
2655
2661
|
const importNames = entry["importNames"] === void 0 ? void 0 : readRequiredStrings(entry, "importNames", name);
|
|
2662
|
+
const allowImportNames = entry["allowImportNames"] === void 0 ? void 0 : readRequiredStrings(entry, "allowImportNames", name);
|
|
2656
2663
|
const allowTypeImports = readBoolean$3(entry, "allowTypeImports", name);
|
|
2657
|
-
|
|
2664
|
+
const base = {
|
|
2658
2665
|
specifiers: readRequiredStrings(entry, "specifiers", name),
|
|
2659
|
-
...importNames !== void 0 && { importNames },
|
|
2660
2666
|
...allowTypeImports !== void 0 && { allowTypeImports },
|
|
2661
2667
|
message: readRequiredString(entry, "message", name)
|
|
2662
2668
|
};
|
|
2669
|
+
if (allowImportNames === void 0) return {
|
|
2670
|
+
...base,
|
|
2671
|
+
...importNames !== void 0 && { importNames }
|
|
2672
|
+
};
|
|
2673
|
+
if (importNames !== void 0) throw new Error(`@exadev/eslint-config: "${name}" takes "importNames" or "allowImportNames", not both: the first lists the names it bans, the second the only names it permits.`);
|
|
2674
|
+
return {
|
|
2675
|
+
...base,
|
|
2676
|
+
allowImportNames
|
|
2677
|
+
};
|
|
2663
2678
|
}
|
|
2664
2679
|
function readConfine(entry, name) {
|
|
2665
2680
|
assertOnlyKeys(entry, [
|
|
@@ -2801,17 +2816,22 @@ function hasComputedSpecifier(node, isRequireGlobal) {
|
|
|
2801
2816
|
const argument = requireArgument(node, isRequireGlobal);
|
|
2802
2817
|
return argument !== void 0 && staticString(argument) === void 0;
|
|
2803
2818
|
}
|
|
2804
|
-
function
|
|
2819
|
+
function takesBannedName(deny, referenceNames) {
|
|
2820
|
+
const { importNames, allowImportNames } = deny;
|
|
2821
|
+
if (allowImportNames !== void 0) return referenceNames.some((name) => !allowImportNames.includes(name));
|
|
2805
2822
|
if (importNames === void 0) return true;
|
|
2806
2823
|
return referenceNames.includes("*") || referenceNames.some((name) => importNames.includes(name));
|
|
2807
2824
|
}
|
|
2808
2825
|
function compilePolicy(policy) {
|
|
2809
|
-
const deny = (policy.deny ?? []).map((
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2826
|
+
const deny = (policy.deny ?? []).map((entry) => {
|
|
2827
|
+
const { specifiers, allowTypeImports = false, message } = entry;
|
|
2828
|
+
return {
|
|
2829
|
+
matches: createSpecifierMatcher(specifiers),
|
|
2830
|
+
allowTypeImports,
|
|
2831
|
+
forbids: (reference) => takesBannedName(entry, reference.names),
|
|
2832
|
+
message
|
|
2833
|
+
};
|
|
2834
|
+
});
|
|
2815
2835
|
const confine = (policy.confine ?? []).map(({ specifiers, onlyIn, allowTypeImports = false, message }) => {
|
|
2816
2836
|
const allowedHere = createFileScope(onlyIn);
|
|
2817
2837
|
return {
|
package/dist/index.d.cts
CHANGED
|
@@ -239,15 +239,21 @@ interface ToolingWiringOptions {
|
|
|
239
239
|
declare function toolingWiringConfig(options?: ToolingWiringOptions): PublicConfigArray;
|
|
240
240
|
//#endregion
|
|
241
241
|
//#region src/rules/import-policy-options.d.ts
|
|
242
|
-
|
|
243
|
-
* Specifiers that the files of a policy may not import.
|
|
244
|
-
*/
|
|
245
|
-
interface ImportDeny {
|
|
242
|
+
interface ImportDenyBase {
|
|
246
243
|
readonly specifiers: readonly string[];
|
|
247
|
-
readonly importNames?: readonly string[];
|
|
248
244
|
readonly allowTypeImports?: boolean;
|
|
249
245
|
readonly message: string;
|
|
250
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Specifiers that the files of a policy may not import. `importNames` narrows the ban to the names it lists; `allowImportNames` inverts it, banning every imported name except the ones it lists. The two cannot be combined. A namespace import, a dynamic import, a `require` call and `export *` take every name, so either way they are reported.
|
|
249
|
+
*/
|
|
250
|
+
type ImportDeny = ImportDenyBase & ({
|
|
251
|
+
readonly importNames?: readonly string[];
|
|
252
|
+
readonly allowImportNames?: never;
|
|
253
|
+
} | {
|
|
254
|
+
readonly allowImportNames: readonly string[];
|
|
255
|
+
readonly importNames?: never;
|
|
256
|
+
});
|
|
251
257
|
/**
|
|
252
258
|
* Specifiers that the files of a policy may import only in the files `onlyIn` selects.
|
|
253
259
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -239,15 +239,21 @@ interface ToolingWiringOptions {
|
|
|
239
239
|
declare function toolingWiringConfig(options?: ToolingWiringOptions): PublicConfigArray;
|
|
240
240
|
//#endregion
|
|
241
241
|
//#region src/rules/import-policy-options.d.ts
|
|
242
|
-
|
|
243
|
-
* Specifiers that the files of a policy may not import.
|
|
244
|
-
*/
|
|
245
|
-
interface ImportDeny {
|
|
242
|
+
interface ImportDenyBase {
|
|
246
243
|
readonly specifiers: readonly string[];
|
|
247
|
-
readonly importNames?: readonly string[];
|
|
248
244
|
readonly allowTypeImports?: boolean;
|
|
249
245
|
readonly message: string;
|
|
250
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Specifiers that the files of a policy may not import. `importNames` narrows the ban to the names it lists; `allowImportNames` inverts it, banning every imported name except the ones it lists. The two cannot be combined. A namespace import, a dynamic import, a `require` call and `export *` take every name, so either way they are reported.
|
|
249
|
+
*/
|
|
250
|
+
type ImportDeny = ImportDenyBase & ({
|
|
251
|
+
readonly importNames?: readonly string[];
|
|
252
|
+
readonly allowImportNames?: never;
|
|
253
|
+
} | {
|
|
254
|
+
readonly allowImportNames: readonly string[];
|
|
255
|
+
readonly importNames?: never;
|
|
256
|
+
});
|
|
251
257
|
/**
|
|
252
258
|
* Specifiers that the files of a policy may import only in the files `onlyIn` selects.
|
|
253
259
|
*/
|
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.35.0";
|
|
154
154
|
//#endregion
|
|
155
155
|
//#region src/is-record.ts
|
|
156
156
|
/**
|
|
@@ -2530,6 +2530,7 @@ const importPoliciesSchema = {
|
|
|
2530
2530
|
properties: {
|
|
2531
2531
|
specifiers: stringList$1,
|
|
2532
2532
|
importNames: stringList$1,
|
|
2533
|
+
allowImportNames: stringList$1,
|
|
2533
2534
|
allowTypeImports: { type: "boolean" },
|
|
2534
2535
|
message: {
|
|
2535
2536
|
type: "string",
|
|
@@ -2537,6 +2538,10 @@ const importPoliciesSchema = {
|
|
|
2537
2538
|
}
|
|
2538
2539
|
},
|
|
2539
2540
|
required: ["specifiers", "message"],
|
|
2541
|
+
not: {
|
|
2542
|
+
type: "object",
|
|
2543
|
+
required: ["importNames", "allowImportNames"]
|
|
2544
|
+
},
|
|
2540
2545
|
additionalProperties: false
|
|
2541
2546
|
}
|
|
2542
2547
|
},
|
|
@@ -2614,17 +2619,27 @@ function readDeny(entry, name) {
|
|
|
2614
2619
|
assertOnlyKeys(entry, [
|
|
2615
2620
|
"specifiers",
|
|
2616
2621
|
"importNames",
|
|
2622
|
+
"allowImportNames",
|
|
2617
2623
|
"allowTypeImports",
|
|
2618
2624
|
"message"
|
|
2619
2625
|
], name);
|
|
2620
2626
|
const importNames = entry["importNames"] === void 0 ? void 0 : readRequiredStrings(entry, "importNames", name);
|
|
2627
|
+
const allowImportNames = entry["allowImportNames"] === void 0 ? void 0 : readRequiredStrings(entry, "allowImportNames", name);
|
|
2621
2628
|
const allowTypeImports = readBoolean$3(entry, "allowTypeImports", name);
|
|
2622
|
-
|
|
2629
|
+
const base = {
|
|
2623
2630
|
specifiers: readRequiredStrings(entry, "specifiers", name),
|
|
2624
|
-
...importNames !== void 0 && { importNames },
|
|
2625
2631
|
...allowTypeImports !== void 0 && { allowTypeImports },
|
|
2626
2632
|
message: readRequiredString(entry, "message", name)
|
|
2627
2633
|
};
|
|
2634
|
+
if (allowImportNames === void 0) return {
|
|
2635
|
+
...base,
|
|
2636
|
+
...importNames !== void 0 && { importNames }
|
|
2637
|
+
};
|
|
2638
|
+
if (importNames !== void 0) throw new Error(`@exadev/eslint-config: "${name}" takes "importNames" or "allowImportNames", not both: the first lists the names it bans, the second the only names it permits.`);
|
|
2639
|
+
return {
|
|
2640
|
+
...base,
|
|
2641
|
+
allowImportNames
|
|
2642
|
+
};
|
|
2628
2643
|
}
|
|
2629
2644
|
function readConfine(entry, name) {
|
|
2630
2645
|
assertOnlyKeys(entry, [
|
|
@@ -2766,17 +2781,22 @@ function hasComputedSpecifier(node, isRequireGlobal) {
|
|
|
2766
2781
|
const argument = requireArgument(node, isRequireGlobal);
|
|
2767
2782
|
return argument !== void 0 && staticString(argument) === void 0;
|
|
2768
2783
|
}
|
|
2769
|
-
function
|
|
2784
|
+
function takesBannedName(deny, referenceNames) {
|
|
2785
|
+
const { importNames, allowImportNames } = deny;
|
|
2786
|
+
if (allowImportNames !== void 0) return referenceNames.some((name) => !allowImportNames.includes(name));
|
|
2770
2787
|
if (importNames === void 0) return true;
|
|
2771
2788
|
return referenceNames.includes("*") || referenceNames.some((name) => importNames.includes(name));
|
|
2772
2789
|
}
|
|
2773
2790
|
function compilePolicy(policy) {
|
|
2774
|
-
const deny = (policy.deny ?? []).map((
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2791
|
+
const deny = (policy.deny ?? []).map((entry) => {
|
|
2792
|
+
const { specifiers, allowTypeImports = false, message } = entry;
|
|
2793
|
+
return {
|
|
2794
|
+
matches: createSpecifierMatcher(specifiers),
|
|
2795
|
+
allowTypeImports,
|
|
2796
|
+
forbids: (reference) => takesBannedName(entry, reference.names),
|
|
2797
|
+
message
|
|
2798
|
+
};
|
|
2799
|
+
});
|
|
2780
2800
|
const confine = (policy.confine ?? []).map(({ specifiers, onlyIn, allowTypeImports = false, message }) => {
|
|
2781
2801
|
const allowedHere = createFileScope(onlyIn);
|
|
2782
2802
|
return {
|
package/package.json
CHANGED