@shivaedev/quality 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2 - 2026-10-04
4
+
5
+ ### Changed
6
+
7
+ - A quoted object key or type property is a foreign name, such as a query parameter an outside API reads or an environment variable, and the naming rules skip it: `{ "per_page": 50 }` passes, `{ per_page: 50 }` is a finding. The new `key-names` plugin checks unquoted keys in camelCase or PascalCase in place of `useNamingConvention`, which cannot tell a quoted key from an unquoted one, and the formatter keeps the quotes a key is written with. `schema-struct-keys` still reports a snake_case `Schema.Struct` field when it is quoted. A key finding now reports under `biome/plugin` instead of `biome/lint/style/useNamingConvention`, so a repository that upgrades records its key findings again with `quality baseline write --rule biome/plugin` and prunes the old entries with `quality baseline prune`.
8
+ - `handler-names` accepts a state setter passed as it is to an `onX` prop, such as `onOpenChange={setOpen}`. Only a handler function defined in the component needs the `handleX` name.
9
+
10
+ ### Fixed
11
+
12
+ - `files/other-names` accepts a double underscore in a content file name, which separates the parts of an id, as in `forest_path__clearing.json`.
13
+ - `files/named-after-export` names a file after its default export: `export default ItemPanel`, `export default function ItemPanel`, `export default memo(ItemPanel)` and `export { ItemPanel as default }` all count as the export `ItemPanel`, where it used to report the file as exporting nothing. `noDefaultExport` still reports the default export itself. An overloaded function counts as one export, so a file whose only export is an overloaded function is now named after it.
14
+ - The preset turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render.
15
+ - `files/other-names` counts as an import of a stylesheet only a static `import`, relative or through a `#` alias that the `imports` of its `package.json` resolves, and a CSS `@import`. A quoted path in a test, such as one passed to `resolve()`, no longer counts, and a stylesheet that another stylesheet imports is shared and named in kebab-case.
16
+
3
17
  ## 0.8.1 - 2026-10-04
4
18
 
5
19
  ### Changed
package/README.md CHANGED
@@ -383,14 +383,14 @@ quality baseline migrate [--config <file>] [--from <file>]
383
383
 
384
384
  The preset sets:
385
385
 
386
- - **Formatting:** tabs, a line width of 150, double quotes, semicolons, trailing commas and operators at the start of a wrapped line.
386
+ - **Formatting:** tabs, a line width of 150, double quotes, semicolons, trailing commas and operators at the start of a wrapped line. An object key keeps its quotes, because a quoted key marks a [foreign name](#foreign-names).
387
387
  - **Lint:** every rule Biome recommends, at `error`, and a list of stricter rules on top, among them `noUnsafeTypeAssertion`, `useBlockStatements`, `useNumericSeparators`, `useUnicodeRegex`, `useLiteralKeys`, `noFloatingPromises`, `useExhaustiveDependencies`, `it` for every test, function declarations over function expressions, interfaces for object types, a cognitive complexity limit of 15, no nested ternaries, and the naming rules under [Naming](#naming).
388
388
  - **Imports:** organized in five groups: Node and Bun builtins, packages, `@shivaedev/*` packages, aliases and relative paths. Biome counts as an alias a specifier that starts with `#`, `@/`, `~`, `$` or `%`; a tsconfig path such as `@app/*` sorts with the packages, and Biome's `noUndeclaredDependencies` takes it for one, so name aliases in a form Biome recognizes.
389
389
  - **Assist:** organized imports and sorted keys, attributes, enum members, interface members and properties. Keys are sorted in JSON and in object literals alike, but only reported: `quality fix` leaves the order to the author. `package.json` is left out, because `manifests/sorted` gives it the order npm users expect.
390
390
  - **Plugins:** GritQL rules that ban ambient time, randomness, `console` and `process.env` for Effect's services, and the naming plugins under [Naming](#naming). They load from `./node_modules/@shivaedev/quality/biome/plugins`, so the package must be installed at the repository root.
391
391
  - Files ignored by git are skipped.
392
392
 
393
- The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, and allows default exports in `*.config.*` files, which tools load through the default export. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared. Every other weakening a repository adds is an override it declares with a reason.
393
+ The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared. Every other weakening a repository adds is an override it declares with a reason.
394
394
 
395
395
  An autofix applies only a change that removes no decision; a fix that can change behavior or delete something written on purpose reports only, and the author decides. The preset keeps `noAccessKey`, `noAriaHiddenOnFocusable`, `noAutofocus`, `noInteractiveElementToNoninteractiveRole`, `noNoninteractiveElementToInteractiveRole`, `noNoninteractiveTabindex`, `noRedundantRoles`, `useValidAriaProps`, `useValidAriaRole`, `noImportantStyles`, `noConstAssign`, `noUnusedPrivateClassMembers`, `useExhaustiveDependencies`, `noFloatingPromises`, `useRegexpTest`, `useUnicodeRegex`, `noNonNullAssertion`, `useAtIndex`, `noParametersOnlyUsedInRecursion` and `useNamingConvention` at `error` with their fixes off, and `quality fix` skips `noDuplicateObjectKeys` and `useSortedKeys`, whose fixes Biome's config cannot turn off.
396
396
 
@@ -411,15 +411,27 @@ A name says what a thing is, and the same kind of thing is named the same way ev
411
411
  | Types, interfaces, classes and enums | PascalCase, without an `I` prefix: `Item`, not `IItem`. | `useNamingConvention` |
412
412
  | Type parameters | `T`, or `T` followed by a PascalCase name: `TItem`, `TResult`. Effect's positional `A`, `E` and `R` are valid as they are. | `useNamingConvention` |
413
413
  | Module-level constants | A string or number literal at module level is CONSTANT_CASE: `const MAX_ITEMS = 50`. A constant inside a function is camelCase. | `constant-names` plugin, `useNamingConvention` |
414
- | Object keys and type properties | camelCase or PascalCase, after any leading `_` or `$` (`_tag`, `$transaction`). snake_case and CONSTANT_CASE keys are never valid, also for outside data and in a `Record`. | `useNamingConvention` |
414
+ | Object keys and type properties | camelCase or PascalCase, after any leading `_` or `$` (`_tag`, `$transaction`), also in a `Record`. A quoted key is a [foreign name](#foreign-names) and is not checked. | `key-names` plugin |
415
415
  | Schemas | PascalCase and named like their type: `const Item = Schema.Struct(...)` with `type Item = typeof Item.Type`. | `schema-names` plugin |
416
- | `Schema.Struct` fields | camelCase. Outside data keeps its keys only at the edge: `Schema.Struct({ createdAt: Schema.String }).pipe(Schema.encodeKeys({ createdAt: "created_at" }))`. | `schema-struct-keys` plugin, `useNamingConvention` |
416
+ | `Schema.Struct` fields | camelCase, also when quoted. Outside data keeps its keys only at the edge: `Schema.Struct({ createdAt: Schema.String }).pipe(Schema.encodeKeys({ createdAt: "created_at" }))`. | `schema-struct-keys` plugin, `key-names` plugin |
417
417
  | `Effect.fn` spans | `"Owner.operation"`, where the owner is the service or module and the operation is the name the function is bound to: `const loadItem = Effect.fn("ItemStore.loadItem")`, and `loadItem: Effect.fn("ItemStore.loadItem")` in an object. | `effect-fn-spans` plugin |
418
418
  | Service Layers | A static `layer` on the service, read as `ItemStore.layer`. No exported `ItemStoreLive` or `ItemStoreLayer` constant. A Layer that wires an application together stays unexported. | `service-layers` plugin |
419
419
  | Private members | A `#field`. No `private`, `protected` or `public` modifier. | `useConsistentMemberAccessibility` |
420
- | React | A function passed to an `onX` prop is `handleX`, or an `onX` prop forwarded as it is. A context is `ThemeContext`, a ref `inputRef`, an id `fieldId`. A module that exports a component exports only components. | `handler-names` plugin, `useReactNamingConvention`, `useComponentExportOnlyModules` |
420
+ | React | A function passed to an `onX` prop is `handleX`, an `onX` prop forwarded as it is, or a state setter passed as it is: `onOpenChange={setOpen}`. A context is `ThemeContext`, a ref `inputRef`, an id `fieldId`. A module that exports a component exports only components; a test may define the components it renders. | `handler-names` plugin, `useReactNamingConvention`, `useComponentExportOnlyModules` |
421
421
  | Booleans | A question: `isOpen`, `hasSave`, `canRetry`, `shouldFlush`. A name the DOM or React gives, such as `open` or `disabled`, stays. This is a convention only; no rule checks it. | Review |
422
422
 
423
+ ### Foreign names
424
+
425
+ Some names are not ours to choose: a query parameter an outside API reads, an environment variable, an operator a query builder takes. Write such a name as a quoted key, and the naming rules skip it:
426
+
427
+ ```ts
428
+ const query = { "per_page": 50, "sort_by": "created" };
429
+ const env = { "DATABASE_URL": url };
430
+ const where = { "OR": [{ id }, { slug }] };
431
+ ```
432
+
433
+ An unquoted key is our own name and is camelCase or PascalCase. The formatter keeps the quotes as written, so a quoted key stays quoted. A `Schema.Struct` field is the exception: the struct is our model of the data, so the `schema-struct-keys` plugin reports a snake_case field even when it is quoted, and points to `Schema.encodeKeys`, which maps camelCase fields to the outside names at the edge.
434
+
423
435
  ### Re-exports
424
436
 
425
437
  No file re-exports, package entry files included: every module is imported from the file that defines it. `noBarrelFile` reports `export { … } from`, `noReExportAll` reports `export *`, the `type-re-exports` plugin reports `export type … from`, and `noExportedImports` reports an import that is exported again. Its message suggests `export … from`, which is a re-export as well: import the name where it is used instead.
@@ -437,6 +449,7 @@ The plugins report under the one Biome category `plugin`, so their findings shar
437
449
  | `item/Panel.tsx` or `ItemPanel.tsx` | `ItemPanel`, with `ItemPanelProps` beside it | Yes |
438
450
  | `Panel/Panel.tsx` | `Panel`: the main file of a module folder repeats the folder | Yes |
439
451
  | `limits.ts` | `MAX_ITEMS` and `MAX_DEPTH`: a topic file of several exports is camelCase | Yes |
452
+ | `ItemPanel.tsx` | `export default memo(ItemPanel)`: a default export counts under the name it resolves to | Yes |
440
453
  | `setupTests.ts` | Nothing, so it is camelCase | Yes |
441
454
  | `script/build-docs.ts` | Run by a `package.json` script or `bin`, or starts with `#!`, so it is kebab-case | Yes |
442
455
  | `order.ts` | `type Order` | No: the file's case follows the export, so it is `Order.ts` |
@@ -461,9 +474,9 @@ The `toolOwned` option lists folders a tool names, `generated/` and `migrations/
461
474
  `files/other-names` names the files that are not code.
462
475
 
463
476
  - Markdown, JSON, GritQL, images, fonts and SVG are kebab-case, with optional dotted parts: `release-notes.md`, `icons.sprite.svg`. Conventional upper-case names stay: `README.md`, `CHANGELOG.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `SECURITY.md` and `SKILL.md`.
464
- - A stylesheet that one component imports is named after it: `ItemPanel.css` beside `ItemPanel.tsx`. A shared stylesheet is kebab-case: `form-controls.css`.
477
+ - A stylesheet that one component imports is named after it: `ItemPanel.css` beside `ItemPanel.tsx`. A shared stylesheet is kebab-case: `form-controls.css`. An import is a static `import` in a code file, relative or through a `#` alias from the `imports` of its `package.json`, or an `@import` in a stylesheet; a stylesheet that another stylesheet imports is shared. A path in a string, such as a test that reads the file, is not an import.
465
478
  - A Prisma schema is camelCase: `schema.prisma`.
466
- - A file the `content` option lists is snake_case: `forest_path.json`.
479
+ - A file the `content` option lists is snake_case: `forest_path.json`. A double underscore separates the parts of an id: `forest_path__clearing.json`.
467
480
 
468
481
  The `toolOwned` option lists files a tool names: dotfiles, `package.json`, `tsconfig*.json`, `biome.json`, `*.config.*`, `migrations/` and `generated/` by default.
469
482
 
@@ -14,6 +14,11 @@
14
14
  "reason": "Tools load their config files through the default export.",
15
15
  "rule": "lint/style/noDefaultExport"
16
16
  },
17
+ {
18
+ "includes": ["**/*.test.*", "**/*.spec.*"],
19
+ "reason": "A test defines the helper and harness components it renders, and no test module is hot-reloaded, so the rule's fast-refresh boundary does not apply.",
20
+ "rule": "lint/style/useComponentExportOnlyModules"
21
+ },
17
22
  {
18
23
  "includes": ["**"],
19
24
  "reason": "quality fix applies every Biome lint fix, and this rule's fix changed behavior or did not finish on real code, so the rule is off until it is reviewed.",
@@ -6,5 +6,6 @@ JsxAttribute(name=$attribute, initializer=JsxAttributeInitializerClause(value=Js
6
6
  $handler <: JsIdentifierExpression(),
7
7
  ! $handler <: r"handle[A-Z][A-Za-z0-9]*",
8
8
  ! $handler <: r"on[A-Z][A-Za-z0-9]*",
9
- register_diagnostic(span=$handler, message="A function passed to an onX prop is named handleX, such as onClick={handleClick}, or is an onX prop forwarded as it is.")
9
+ ! $handler <: r"set[A-Z][A-Za-z0-9]*",
10
+ register_diagnostic(span=$handler, message="A function passed to an onX prop is named handleX, such as onClick={handleClick}, or is an onX prop forwarded as it is, or a state setter passed as it is, such as onChange={setValue}.")
10
11
  }
@@ -0,0 +1,12 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsPropertyObjectMember(name=$key),
6
+ TsPropertySignatureTypeMember(name=$key)
7
+ } where {
8
+ $key <: JsLiteralMemberName(),
9
+ $key <: r"[A-Za-z_$][A-Za-z0-9_$]*",
10
+ ! $key <: r"[_$]*(?:[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*[A-Z]?|(?:[A-Z][0-9]+)*[A-Z][0-9]*[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*[A-Z]?|[A-Z][0-9]*)",
11
+ register_diagnostic(span=$key, message="An object key or type property is camelCase or PascalCase after any leading _ or $, with acronyms spelled as words: perPage, userId. A name an outside API or the environment decides is written quoted: { 'per_page': 50 }.")
12
+ }
package/biome/preset.json CHANGED
@@ -77,7 +77,7 @@
77
77
  "arrowParentheses": "always",
78
78
  "jsxQuoteStyle": "double",
79
79
  "operatorLinebreak": "before",
80
- "quoteProperties": "asNeeded",
80
+ "quoteProperties": "preserve",
81
81
  "quoteStyle": "double",
82
82
  "semicolons": "always",
83
83
  "trailingCommas": "all"
@@ -462,8 +462,8 @@
462
462
  { "formats": ["PascalCase"], "match": "(T|A|E|R)|T([A-Z][A-Za-z0-9]*)", "selector": { "kind": "typeParameter" } },
463
463
  { "formats": ["PascalCase"], "match": "([A-HJ-Z][A-Za-z0-9]*|I[a-z0-9][A-Za-z0-9]*|I)", "selector": { "kind": "typeLike" } },
464
464
  { "formats": ["camelCase", "PascalCase"], "selector": { "kind": "function" } },
465
- { "formats": ["camelCase", "PascalCase"], "match": "[_$]*(.+)", "selector": { "kind": "objectLiteralProperty" } },
466
- { "formats": ["camelCase", "PascalCase"], "match": "[_$]*(.+)", "selector": { "kind": "typeProperty" } }
465
+ { "match": ".*", "selector": { "kind": "objectLiteralProperty" } },
466
+ { "match": ".*", "selector": { "kind": "typeProperty" } }
467
467
  ],
468
468
  "requireAscii": true,
469
469
  "strictCase": true
@@ -622,6 +622,16 @@
622
622
  }
623
623
  }
624
624
  }
625
+ },
626
+ {
627
+ "includes": ["**/*.test.*", "**/*.spec.*"],
628
+ "linter": {
629
+ "rules": {
630
+ "style": {
631
+ "useComponentExportOnlyModules": "off"
632
+ }
633
+ }
634
+ }
625
635
  }
626
636
  ],
627
637
  "plugins": [
@@ -641,6 +651,10 @@
641
651
  "includes": ["**/*.ts", "**/*.tsx", "!**/*.d.ts"],
642
652
  "path": "./node_modules/@shivaedev/quality/biome/plugins/handler-names.grit"
643
653
  },
654
+ {
655
+ "includes": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
656
+ "path": "./node_modules/@shivaedev/quality/biome/plugins/key-names.grit"
657
+ },
644
658
  {
645
659
  "includes": ["**/*.ts", "**/*.tsx", "!**/*.d.ts"],
646
660
  "path": "./node_modules/@shivaedev/quality/biome/plugins/schema-names.grit"
@@ -1 +1 @@
1
- {"version":3,"file":"ownExports.d.ts","sourceRoot":"","sources":["../../src/naming/ownExports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAgFD,wBAAgB,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,SAAS,EAAE,CAQtE"}
1
+ {"version":3,"file":"ownExports.d.ts","sourceRoot":"","sources":["../../src/naming/ownExports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAiHD,wBAAgB,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,SAAS,EAAE,CAItE"}
@@ -19,9 +19,7 @@ function isPrimitiveLiteral(node) {
19
19
  }
20
20
  function isExported(statement) {
21
21
  const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
22
- const exported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
23
- const isDefault = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
24
- return exported && !isDefault;
22
+ return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
25
23
  }
26
24
  function variables(statement) {
27
25
  const constant = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0;
@@ -57,22 +55,50 @@ function listed(statement, known) {
57
55
  return [];
58
56
  }
59
57
  return clause.elements.flatMap((element) => {
60
- const name = element.name.text;
61
58
  const local = known.get((element.propertyName ?? element.name).text);
62
- if (name === "default" || local === undefined) {
59
+ if (local === undefined) {
63
60
  return [];
64
61
  }
62
+ const name = element.name.text === "default" ? local.name : element.name.text;
65
63
  const typeOnly = statement.isTypeOnly || element.isTypeOnly;
66
64
  return [{ constant: local.constant, kind: typeOnly ? "type" : local.kind, name }];
67
65
  });
68
66
  }
67
+ function wrappedName(expression) {
68
+ if (ts.isIdentifier(expression)) {
69
+ return expression.text;
70
+ }
71
+ if (ts.isFunctionExpression(expression) || ts.isClassExpression(expression)) {
72
+ return expression.name?.text;
73
+ }
74
+ if (ts.isCallExpression(expression)) {
75
+ const [wrapped] = expression.arguments;
76
+ return wrapped === undefined ? undefined : wrappedName(wrapped);
77
+ }
78
+ return ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)
79
+ ? wrappedName(expression.expression)
80
+ : undefined;
81
+ }
82
+ // The default export counts under the name it resolves to, as in memo(ItemPanel); noDefaultExport reports the default export itself.
83
+ function defaulted(statement, known) {
84
+ const name = statement.isExportEquals ? undefined : wrappedName(statement.expression);
85
+ if (name === undefined) {
86
+ return [];
87
+ }
88
+ return [known.get(name) ?? { constant: false, kind: "value", name }];
89
+ }
90
+ function exportsOf(statement, known) {
91
+ if (ts.isExportDeclaration(statement)) {
92
+ return listed(statement, known);
93
+ }
94
+ if (ts.isExportAssignment(statement)) {
95
+ return defaulted(statement, known);
96
+ }
97
+ return isExported(statement) ? declared(statement) : [];
98
+ }
69
99
  export function ownExports(source) {
70
100
  const known = locals(source);
71
- return source.statements.flatMap((statement) => {
72
- if (ts.isExportDeclaration(statement)) {
73
- return listed(statement, known);
74
- }
75
- return isExported(statement) ? declared(statement) : [];
76
- });
101
+ const all = source.statements.flatMap((statement) => exportsOf(statement, known));
102
+ return [...new Map(all.map((entry) => [`${entry.kind} ${entry.name}`, entry])).values()];
77
103
  }
78
104
  //# sourceMappingURL=ownExports.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ownExports.js","sourceRoot":"","sources":["../../src/naming/ownExports.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ownExports.js","sourceRoot":"","sources":["../../src/naming/ownExports.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"otherNames.d.ts","sourceRoot":"","sources":["../../../src/rules/files/otherNames.ts"],"names":[],"mappings":"AAqGA,eAAO,MAAM,UAAU;;;EAgBrB,CAAC"}
1
+ {"version":3,"file":"otherNames.d.ts","sourceRoot":"","sources":["../../../src/rules/files/otherNames.ts"],"names":[],"mappings":"AA2FA,eAAO,MAAM,UAAU;;;EAgBrB,CAAC"}
@@ -3,6 +3,7 @@ import { Effect, Schema } from "effect";
3
3
  import ignore, {} from "ignore";
4
4
  import { CAMEL, KEBAB } from "#naming/words.ts";
5
5
  import { defineRule } from "#rule.ts";
6
+ import { stylesheetImporters } from "./stylesheetImporters.js";
6
7
  const DEFAULT_TOOL_OWNED = [
7
8
  ".*",
8
9
  "package.json",
@@ -35,26 +36,15 @@ const KEBAB_KINDS = new Set([
35
36
  ]);
36
37
  const STYLESHEETS = new Set(["css", "scss"]);
37
38
  const DOTTED_KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$/u;
38
- const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
39
- const STYLESHEET_IMPORT = /["'](?<stylesheet>\.{1,2}\/[^"'\n]+\.s?css)["']/gu;
39
+ const SNAKE = /^[a-z0-9]+(?:__?[a-z0-9]+)*$/u;
40
40
  const CODE = /\.[cm]?[jt]sx?$/u;
41
41
  const OtherNamesOptions = Schema.Struct({
42
42
  content: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
43
43
  toolOwned: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TOOL_OWNED))),
44
44
  });
45
- function importers(sources) {
46
- const found = new Map();
47
- for (const source of sources) {
48
- for (const match of source.text.matchAll(STYLESHEET_IMPORT)) {
49
- const target = posix.normalize(posix.join(posix.dirname(source.path), match.groups?.stylesheet ?? ""));
50
- found.set(target, [...new Set([...(found.get(target) ?? []), source.path])]);
51
- }
52
- }
53
- return found;
54
- }
55
45
  function stylesheetMessage(path, stem, importedBy) {
56
46
  const [only] = importedBy;
57
- if (importedBy.length === 1 && only !== undefined) {
47
+ if (importedBy.length === 1 && only !== undefined && CODE.test(only)) {
58
48
  const owner = posix.basename(only).replace(CODE, "");
59
49
  return stem === owner ? undefined : `"${stem}" is styling for ${posix.basename(only)} alone, so it is named ${owner}${posix.extname(path)}.`;
60
50
  }
@@ -68,7 +58,9 @@ function messageFor(path, context) {
68
58
  return undefined;
69
59
  }
70
60
  if (context.content.ignores(path)) {
71
- return SNAKE.test(stem) ? undefined : `"${name}" is content, so it is named in snake_case, such as forest_path.json.`;
61
+ return SNAKE.test(stem)
62
+ ? undefined
63
+ : `"${name}" is content, so it is named in snake_case, such as forest_path.json, with a double underscore between the parts of an id, such as forest_path__clearing.json.`;
72
64
  }
73
65
  if (STYLESHEETS.has(kind)) {
74
66
  return stylesheetMessage(path, stem, context.stylesheets.get(path) ?? []);
@@ -81,10 +73,10 @@ function messageFor(path, context) {
81
73
  : undefined;
82
74
  }
83
75
  export const otherNames = defineRule({
84
- check: ({ files, options, sources }) => {
76
+ check: async ({ files, options, readText, sources }) => {
85
77
  const context = {
86
78
  content: ignore().add([...options.content]),
87
- stylesheets: importers(sources),
79
+ stylesheets: await stylesheetImporters({ files, readText, sources }),
88
80
  toolOwned: ignore().add([...options.toolOwned]),
89
81
  };
90
82
  return files.flatMap((path) => {
@@ -1 +1 @@
1
- {"version":3,"file":"otherNames.js","sourceRoot":"","sources":["../../../src/rules/files/otherNames.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"otherNames.js","sourceRoot":"","sources":["../../../src/rules/files/otherNames.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { RuleInputs } from "#rule.ts";
2
+ export type PackageImports = (importer: string, specifier: string) => string | undefined;
3
+ export declare function packageImports({ files, readText }: Pick<RuleInputs, "files" | "readText">): Promise<PackageImports>;
4
+ //# sourceMappingURL=packageImports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packageImports.d.ts","sourceRoot":"","sources":["../../../src/rules/files/packageImports.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AAgDzF,wBAAsB,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBzH"}
@@ -0,0 +1,60 @@
1
+ import { posix } from "node:path";
2
+ const MANIFEST = "package.json";
3
+ function importsField(text) {
4
+ if (text === undefined) {
5
+ return undefined;
6
+ }
7
+ try {
8
+ const manifest = JSON.parse(text);
9
+ return typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "imports") : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function targetsIn(value) {
16
+ if (typeof value === "string") {
17
+ return [value];
18
+ }
19
+ if (Array.isArray(value)) {
20
+ return value.flatMap(targetsIn);
21
+ }
22
+ return typeof value === "object" && value !== null ? Object.values(value).flatMap(targetsIn) : [];
23
+ }
24
+ function matched(imports, specifier) {
25
+ if (typeof imports !== "object" || imports === null) {
26
+ return [];
27
+ }
28
+ const entries = Object.entries(imports);
29
+ const exact = entries.find(([key]) => key === specifier);
30
+ if (exact !== undefined) {
31
+ return targetsIn(exact[1]);
32
+ }
33
+ const patterns = entries.flatMap(([key, value]) => {
34
+ const [prefix = "", suffix, ...rest] = key.split("*");
35
+ const fits = suffix !== undefined && rest.length === 0 && specifier.length >= prefix.length + suffix.length;
36
+ return fits && specifier.startsWith(prefix) && specifier.endsWith(suffix) ? [{ prefix, suffix, value }] : [];
37
+ });
38
+ const [best] = patterns.toSorted((left, right) => right.prefix.length - left.prefix.length);
39
+ if (best === undefined) {
40
+ return [];
41
+ }
42
+ const star = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
43
+ return targetsIn(best.value).map((target) => target.replaceAll("*", star));
44
+ }
45
+ export async function packageImports({ files, readText }) {
46
+ const directories = files.filter((path) => posix.basename(path) === MANIFEST).map((path) => posix.dirname(path));
47
+ const manifests = await Promise.all(directories.map(async (directory) => ({ directory, imports: importsField(await readText(posix.join(directory, MANIFEST))) })));
48
+ const deepestFirst = manifests.toSorted((left, right) => right.directory.length - left.directory.length);
49
+ const present = new Set(files);
50
+ return (importer, specifier) => {
51
+ const owner = deepestFirst.find(({ directory }) => directory === "." || importer.startsWith(`${directory}/`));
52
+ if (owner === undefined) {
53
+ return undefined;
54
+ }
55
+ return matched(owner.imports, specifier)
56
+ .map((target) => posix.normalize(posix.join(owner.directory, target)))
57
+ .find((path) => present.has(path));
58
+ };
59
+ }
60
+ //# sourceMappingURL=packageImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packageImports.js","sourceRoot":"","sources":["../../../src/rules/files/packageImports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { RuleInputs } from "#rule.ts";
2
+ export declare function stylesheetImporters(inputs: Pick<RuleInputs, "files" | "readText" | "sources">): Promise<ReadonlyMap<string, readonly string[]>>;
3
+ //# sourceMappingURL=stylesheetImporters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stylesheetImporters.d.ts","sourceRoot":"","sources":["../../../src/rules/files/stylesheetImporters.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,UAAU,CAAC;AAyCvD,wBAAsB,mBAAmB,CACxC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC,GACxD,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAmBjD"}
@@ -0,0 +1,48 @@
1
+ import { posix } from "node:path";
2
+ import ts from "typescript";
3
+ import { parse } from "#rules/syntax.ts";
4
+ import { packageImports } from "./packageImports.js";
5
+ const STYLESHEET = /\.s?css$/u;
6
+ const MENTIONS_STYLESHEET = /\.s?css["'?]/u;
7
+ const CSS_IMPORT = /@import\s+(?:url\(\s*)?(?<quote>["']?)(?<target>[^"'()\s;]+)\k<quote>/gu;
8
+ const CSS_COMMENT = /\/\*[\s\S]*?\*\//gu;
9
+ const QUERY = /\?.*$/u;
10
+ const PACKAGE_IMPORT = "#";
11
+ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/)/iu;
12
+ function moduleImports(file) {
13
+ const source = MENTIONS_STYLESHEET.test(file.text) ? parse(file) : undefined;
14
+ return (source?.statements ?? []).flatMap((statement) => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) ? [statement.moduleSpecifier.text] : []);
15
+ }
16
+ function cssImports(text) {
17
+ return [...text.replace(CSS_COMMENT, "").matchAll(CSS_IMPORT)].flatMap((match) => match.groups?.target ?? []);
18
+ }
19
+ function resolver(aliases, present, fromStylesheet) {
20
+ return (importer, raw) => {
21
+ const specifier = raw.replace(QUERY, "");
22
+ if (specifier.startsWith(PACKAGE_IMPORT)) {
23
+ return aliases(importer, specifier);
24
+ }
25
+ const relative = specifier.startsWith(".") || (fromStylesheet && !EXTERNAL.test(specifier));
26
+ const path = posix.normalize(posix.join(posix.dirname(importer), specifier));
27
+ return relative && present.has(path) ? path : undefined;
28
+ };
29
+ }
30
+ export async function stylesheetImporters(inputs) {
31
+ const present = new Set(inputs.files);
32
+ const aliases = await packageImports(inputs);
33
+ const stylesheets = await Promise.all(inputs.files.filter((path) => STYLESHEET.test(path)).map(async (path) => ({ path, text: (await inputs.readText(path)) ?? "" })));
34
+ const fromModule = resolver(aliases, present, false);
35
+ const fromStylesheet = resolver(aliases, present, true);
36
+ const edges = [
37
+ ...inputs.sources.flatMap((source) => moduleImports(source).map((specifier) => [source.path, fromModule(source.path, specifier)])),
38
+ ...stylesheets.flatMap((sheet) => cssImports(sheet.text).map((specifier) => [sheet.path, fromStylesheet(sheet.path, specifier)])),
39
+ ];
40
+ const found = new Map();
41
+ for (const [importer, target] of edges) {
42
+ if (target !== undefined && STYLESHEET.test(target)) {
43
+ found.set(target, [...new Set([...(found.get(target) ?? []), importer])]);
44
+ }
45
+ }
46
+ return found;
47
+ }
48
+ //# sourceMappingURL=stylesheetImporters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stylesheetImporters.js","sourceRoot":"","sources":["../../../src/rules/files/stylesheetImporters.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/quality",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
6
6
  "bugs": {
@@ -29,9 +29,7 @@ function isPrimitiveLiteral(node: ts.Expression | undefined): boolean {
29
29
 
30
30
  function isExported(statement: ts.Statement): boolean {
31
31
  const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
32
- const exported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
33
- const isDefault = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
34
- return exported && !isDefault;
32
+ return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
35
33
  }
36
34
 
37
35
  function variables(statement: ts.VariableStatement): readonly OwnExport[] {
@@ -74,22 +72,53 @@ function listed(statement: ts.ExportDeclaration, known: ReadonlyMap<string, OwnE
74
72
  return [];
75
73
  }
76
74
  return clause.elements.flatMap((element) => {
77
- const name = element.name.text;
78
75
  const local = known.get((element.propertyName ?? element.name).text);
79
- if (name === "default" || local === undefined) {
76
+ if (local === undefined) {
80
77
  return [];
81
78
  }
79
+ const name = element.name.text === "default" ? local.name : element.name.text;
82
80
  const typeOnly = statement.isTypeOnly || element.isTypeOnly;
83
81
  return [{ constant: local.constant, kind: typeOnly ? ("type" as const) : local.kind, name }];
84
82
  });
85
83
  }
86
84
 
85
+ function wrappedName(expression: ts.Expression): string | undefined {
86
+ if (ts.isIdentifier(expression)) {
87
+ return expression.text;
88
+ }
89
+ if (ts.isFunctionExpression(expression) || ts.isClassExpression(expression)) {
90
+ return expression.name?.text;
91
+ }
92
+ if (ts.isCallExpression(expression)) {
93
+ const [wrapped] = expression.arguments;
94
+ return wrapped === undefined ? undefined : wrappedName(wrapped);
95
+ }
96
+ return ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)
97
+ ? wrappedName(expression.expression)
98
+ : undefined;
99
+ }
100
+
101
+ // The default export counts under the name it resolves to, as in memo(ItemPanel); noDefaultExport reports the default export itself.
102
+ function defaulted(statement: ts.ExportAssignment, known: ReadonlyMap<string, OwnExport>): readonly OwnExport[] {
103
+ const name = statement.isExportEquals ? undefined : wrappedName(statement.expression);
104
+ if (name === undefined) {
105
+ return [];
106
+ }
107
+ return [known.get(name) ?? { constant: false, kind: "value", name }];
108
+ }
109
+
110
+ function exportsOf(statement: ts.Statement, known: ReadonlyMap<string, OwnExport>): readonly OwnExport[] {
111
+ if (ts.isExportDeclaration(statement)) {
112
+ return listed(statement, known);
113
+ }
114
+ if (ts.isExportAssignment(statement)) {
115
+ return defaulted(statement, known);
116
+ }
117
+ return isExported(statement) ? declared(statement) : [];
118
+ }
119
+
87
120
  export function ownExports(source: ts.SourceFile): readonly OwnExport[] {
88
121
  const known = locals(source);
89
- return source.statements.flatMap((statement) => {
90
- if (ts.isExportDeclaration(statement)) {
91
- return listed(statement, known);
92
- }
93
- return isExported(statement) ? declared(statement) : [];
94
- });
122
+ const all = source.statements.flatMap((statement) => exportsOf(statement, known));
123
+ return [...new Map(all.map((entry) => [`${entry.kind} ${entry.name}`, entry])).values()];
95
124
  }
@@ -2,7 +2,8 @@ import { posix } from "node:path";
2
2
  import { Effect, Schema } from "effect";
3
3
  import ignore, { type Ignore } from "ignore";
4
4
  import { CAMEL, KEBAB } from "#naming/words.ts";
5
- import { defineRule, type Finding, type SourceFile } from "#rule.ts";
5
+ import { defineRule, type Finding } from "#rule.ts";
6
+ import { stylesheetImporters } from "./stylesheetImporters.ts";
6
7
 
7
8
  const DEFAULT_TOOL_OWNED: readonly string[] = [
8
9
  ".*",
@@ -41,9 +42,7 @@ const STYLESHEETS: ReadonlySet<string> = new Set(["css", "scss"]);
41
42
 
42
43
  const DOTTED_KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$/u;
43
44
 
44
- const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
45
-
46
- const STYLESHEET_IMPORT = /["'](?<stylesheet>\.{1,2}\/[^"'\n]+\.s?css)["']/gu;
45
+ const SNAKE = /^[a-z0-9]+(?:__?[a-z0-9]+)*$/u;
47
46
 
48
47
  const CODE = /\.[cm]?[jt]sx?$/u;
49
48
 
@@ -52,20 +51,9 @@ const OtherNamesOptions = Schema.Struct({
52
51
  toolOwned: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TOOL_OWNED))),
53
52
  });
54
53
 
55
- function importers(sources: readonly SourceFile[]): ReadonlyMap<string, readonly string[]> {
56
- const found = new Map<string, string[]>();
57
- for (const source of sources) {
58
- for (const match of source.text.matchAll(STYLESHEET_IMPORT)) {
59
- const target = posix.normalize(posix.join(posix.dirname(source.path), match.groups?.stylesheet ?? ""));
60
- found.set(target, [...new Set([...(found.get(target) ?? []), source.path])]);
61
- }
62
- }
63
- return found;
64
- }
65
-
66
54
  function stylesheetMessage(path: string, stem: string, importedBy: readonly string[]): string | undefined {
67
55
  const [only] = importedBy;
68
- if (importedBy.length === 1 && only !== undefined) {
56
+ if (importedBy.length === 1 && only !== undefined && CODE.test(only)) {
69
57
  const owner = posix.basename(only).replace(CODE, "");
70
58
  return stem === owner ? undefined : `"${stem}" is styling for ${posix.basename(only)} alone, so it is named ${owner}${posix.extname(path)}.`;
71
59
  }
@@ -86,7 +74,9 @@ function messageFor(path: string, context: Context): string | undefined {
86
74
  return undefined;
87
75
  }
88
76
  if (context.content.ignores(path)) {
89
- return SNAKE.test(stem) ? undefined : `"${name}" is content, so it is named in snake_case, such as forest_path.json.`;
77
+ return SNAKE.test(stem)
78
+ ? undefined
79
+ : `"${name}" is content, so it is named in snake_case, such as forest_path.json, with a double underscore between the parts of an id, such as forest_path__clearing.json.`;
90
80
  }
91
81
  if (STYLESHEETS.has(kind)) {
92
82
  return stylesheetMessage(path, stem, context.stylesheets.get(path) ?? []);
@@ -100,10 +90,10 @@ function messageFor(path: string, context: Context): string | undefined {
100
90
  }
101
91
 
102
92
  export const otherNames = defineRule({
103
- check: ({ files, options, sources }) => {
93
+ check: async ({ files, options, readText, sources }) => {
104
94
  const context: Context = {
105
95
  content: ignore().add([...options.content]),
106
- stylesheets: importers(sources),
96
+ stylesheets: await stylesheetImporters({ files, readText, sources }),
107
97
  toolOwned: ignore().add([...options.toolOwned]),
108
98
  };
109
99
  return files.flatMap((path): readonly Finding[] => {
@@ -0,0 +1,68 @@
1
+ import { posix } from "node:path";
2
+ import type { RuleInputs } from "#rule.ts";
3
+
4
+ export type PackageImports = (importer: string, specifier: string) => string | undefined;
5
+
6
+ const MANIFEST = "package.json";
7
+
8
+ function importsField(text: string | undefined): unknown {
9
+ if (text === undefined) {
10
+ return undefined;
11
+ }
12
+ try {
13
+ const manifest: unknown = JSON.parse(text);
14
+ return typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "imports") : undefined;
15
+ } catch {
16
+ return undefined;
17
+ }
18
+ }
19
+
20
+ function targetsIn(value: unknown): readonly string[] {
21
+ if (typeof value === "string") {
22
+ return [value];
23
+ }
24
+ if (Array.isArray(value)) {
25
+ return value.flatMap(targetsIn);
26
+ }
27
+ return typeof value === "object" && value !== null ? Object.values(value).flatMap(targetsIn) : [];
28
+ }
29
+
30
+ function matched(imports: unknown, specifier: string): readonly string[] {
31
+ if (typeof imports !== "object" || imports === null) {
32
+ return [];
33
+ }
34
+ const entries = Object.entries(imports);
35
+ const exact = entries.find(([key]) => key === specifier);
36
+ if (exact !== undefined) {
37
+ return targetsIn(exact[1]);
38
+ }
39
+ const patterns = entries.flatMap(([key, value]) => {
40
+ const [prefix = "", suffix, ...rest] = key.split("*");
41
+ const fits = suffix !== undefined && rest.length === 0 && specifier.length >= prefix.length + suffix.length;
42
+ return fits && specifier.startsWith(prefix) && specifier.endsWith(suffix) ? [{ prefix, suffix, value }] : [];
43
+ });
44
+ const [best] = patterns.toSorted((left, right) => right.prefix.length - left.prefix.length);
45
+ if (best === undefined) {
46
+ return [];
47
+ }
48
+ const star = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
49
+ return targetsIn(best.value).map((target) => target.replaceAll("*", star));
50
+ }
51
+
52
+ export async function packageImports({ files, readText }: Pick<RuleInputs, "files" | "readText">): Promise<PackageImports> {
53
+ const directories = files.filter((path) => posix.basename(path) === MANIFEST).map((path) => posix.dirname(path));
54
+ const manifests = await Promise.all(
55
+ directories.map(async (directory) => ({ directory, imports: importsField(await readText(posix.join(directory, MANIFEST))) })),
56
+ );
57
+ const deepestFirst = manifests.toSorted((left, right) => right.directory.length - left.directory.length);
58
+ const present = new Set(files);
59
+ return (importer, specifier) => {
60
+ const owner = deepestFirst.find(({ directory }) => directory === "." || importer.startsWith(`${directory}/`));
61
+ if (owner === undefined) {
62
+ return undefined;
63
+ }
64
+ return matched(owner.imports, specifier)
65
+ .map((target) => posix.normalize(posix.join(owner.directory, target)))
66
+ .find((path) => present.has(path));
67
+ };
68
+ }
@@ -0,0 +1,65 @@
1
+ import { posix } from "node:path";
2
+ import ts from "typescript";
3
+ import type { RuleInputs, SourceFile } from "#rule.ts";
4
+ import { parse } from "#rules/syntax.ts";
5
+ import { type PackageImports, packageImports } from "./packageImports.ts";
6
+
7
+ const STYLESHEET = /\.s?css$/u;
8
+
9
+ const MENTIONS_STYLESHEET = /\.s?css["'?]/u;
10
+
11
+ const CSS_IMPORT = /@import\s+(?:url\(\s*)?(?<quote>["']?)(?<target>[^"'()\s;]+)\k<quote>/gu;
12
+
13
+ const CSS_COMMENT = /\/\*[\s\S]*?\*\//gu;
14
+
15
+ const QUERY = /\?.*$/u;
16
+
17
+ const PACKAGE_IMPORT = "#";
18
+
19
+ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/)/iu;
20
+
21
+ function moduleImports(file: SourceFile): readonly string[] {
22
+ const source = MENTIONS_STYLESHEET.test(file.text) ? parse(file) : undefined;
23
+ return (source?.statements ?? []).flatMap((statement) =>
24
+ ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) ? [statement.moduleSpecifier.text] : [],
25
+ );
26
+ }
27
+
28
+ function cssImports(text: string): readonly string[] {
29
+ return [...text.replace(CSS_COMMENT, "").matchAll(CSS_IMPORT)].flatMap((match) => match.groups?.target ?? []);
30
+ }
31
+
32
+ function resolver(aliases: PackageImports, present: ReadonlySet<string>, fromStylesheet: boolean) {
33
+ return (importer: string, raw: string): string | undefined => {
34
+ const specifier = raw.replace(QUERY, "");
35
+ if (specifier.startsWith(PACKAGE_IMPORT)) {
36
+ return aliases(importer, specifier);
37
+ }
38
+ const relative = specifier.startsWith(".") || (fromStylesheet && !EXTERNAL.test(specifier));
39
+ const path = posix.normalize(posix.join(posix.dirname(importer), specifier));
40
+ return relative && present.has(path) ? path : undefined;
41
+ };
42
+ }
43
+
44
+ export async function stylesheetImporters(
45
+ inputs: Pick<RuleInputs, "files" | "readText" | "sources">,
46
+ ): Promise<ReadonlyMap<string, readonly string[]>> {
47
+ const present = new Set(inputs.files);
48
+ const aliases = await packageImports(inputs);
49
+ const stylesheets = await Promise.all(
50
+ inputs.files.filter((path) => STYLESHEET.test(path)).map(async (path) => ({ path, text: (await inputs.readText(path)) ?? "" })),
51
+ );
52
+ const fromModule = resolver(aliases, present, false);
53
+ const fromStylesheet = resolver(aliases, present, true);
54
+ const edges = [
55
+ ...inputs.sources.flatMap((source) => moduleImports(source).map((specifier) => [source.path, fromModule(source.path, specifier)] as const)),
56
+ ...stylesheets.flatMap((sheet) => cssImports(sheet.text).map((specifier) => [sheet.path, fromStylesheet(sheet.path, specifier)] as const)),
57
+ ];
58
+ const found = new Map<string, readonly string[]>();
59
+ for (const [importer, target] of edges) {
60
+ if (target !== undefined && STYLESHEET.test(target)) {
61
+ found.set(target, [...new Set([...(found.get(target) ?? []), importer])]);
62
+ }
63
+ }
64
+ return found;
65
+ }