@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.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.
Files changed (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. package/src/workspace.ts +1 -9
@@ -3,15 +3,18 @@
3
3
  * An object-literal property writes the member of its name on each constituent of the
4
4
  * literal's contextual type that declares one. A value reaching a position typed by another
5
5
  * object type reads each of its members that type declares, through array elements and
6
- * function parameters and results too; a class instance there is the
7
- * interface-satisfaction class's evidence instead.
6
+ * the parameters and results of functions and constructors too; a class instance there is
7
+ * the interface-satisfaction class's evidence instead.
8
8
  */
9
- import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
- import { SignatureKind } from "@typescript/native/unstable/sync";
9
+ import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
+ import { SignatureKind, SymbolFlags } from "@typescript/native/unstable/sync";
11
11
  import { isAnswered } from "./query.js";
12
12
  /** The member kinds a contextual type writes and reads: data members and the members of object types. */
13
13
  const MEMBER_KINDS = new Set(["type-member", "class-member"]);
14
- /** The name one literal property is written under, where the name is literal text. */
14
+ /**
15
+ * The name one literal property is written under: its literal text, or the computed name
16
+ * node whose key the checker names, a unique symbol among them.
17
+ */
15
18
  function propertyName(property) {
16
19
  if (!isPropertyAssignment(property) &&
17
20
  !isShorthandPropertyAssignment(property) &&
@@ -29,6 +32,7 @@ function propertyName(property) {
29
32
  if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
30
33
  return { node: name, text: key.text };
31
34
  }
35
+ return { node: name, text: undefined };
32
36
  }
33
37
  return undefined;
34
38
  }
@@ -41,6 +45,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
41
45
  const queries = project.queries;
42
46
  const ownPaths = project.ownPaths();
43
47
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
48
+ const privateNames = new Set(held.symbols
49
+ .filter((symbol) => symbol.visibility === "private-name")
50
+ .map((symbol) => symbol.id));
44
51
  let batches = 0;
45
52
  let lookups = 0;
46
53
  const batchesOf = (count) => Math.ceil(count / cap);
@@ -80,6 +87,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
80
87
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
81
88
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
82
89
  own: property.declarations.some((handle) => ownPaths.has(handle.path)),
90
+ accessor: (property.flags & SymbolFlags.Accessor) !== 0,
83
91
  });
84
92
  }
85
93
  tables.set(type.id, table);
@@ -88,6 +96,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
88
96
  const writes = [];
89
97
  batches += batchesOf(literals.length);
90
98
  const contexts = queries.contextualTypes(literals.map((one) => one.literal), cap);
99
+ // A computed name that is not literal text names the property the literal declares
100
+ // under the checker's own key, which is how a type declares a member a unique symbol
101
+ // keys: asked once, for the literals that have a contextual type.
102
+ const computed = literals.flatMap(({ literal }, index) => contexts[index] === undefined || !isAnswered(contexts[index])
103
+ ? []
104
+ : literal.properties.flatMap((property) => {
105
+ const name = propertyName(property);
106
+ return name !== undefined && name.text === undefined ? [name.node] : [];
107
+ }));
108
+ batches += batchesOf(computed.length);
109
+ const keys = new Map();
110
+ (computed.length === 0
111
+ ? []
112
+ : project.symbolsAt(computed.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
113
+ const node = computed[index];
114
+ if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
115
+ keys.set(node, symbol.name);
116
+ }
117
+ });
91
118
  literals.forEach(({ literal, from }, index) => {
92
119
  const context = contexts[index];
93
120
  if (context === undefined || !isAnswered(context)) {
@@ -96,10 +123,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
96
123
  const tablesOf = partsOf(context).map(membersOf);
97
124
  for (const property of literal.properties) {
98
125
  const name = propertyName(property);
99
- if (name === undefined) {
126
+ const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
127
+ if (name === undefined || text === undefined) {
100
128
  continue;
101
129
  }
102
- const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
130
+ const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
103
131
  if (ids.length > 0) {
104
132
  writes.push({ at: name.node, from, ids });
105
133
  }
@@ -134,13 +162,22 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
134
162
  return element;
135
163
  };
136
164
  const calls = new Map();
137
- /** The parameter and return types of a type's one call signature, read once per type. */
165
+ /**
166
+ * The parameter and return types of a type's one call signature, or of its one construct
167
+ * signature where it has no call signature, read once per type.
168
+ */
138
169
  const callOf = (type) => {
139
170
  if (calls.has(type.id)) {
140
171
  return calls.get(type.id);
141
172
  }
142
173
  lookups += 1;
143
- const signatures = queries.signaturesOf(type, SignatureKind.Call);
174
+ let kind = SignatureKind.Call;
175
+ let signatures = queries.signaturesOf(type, kind);
176
+ if (isAnswered(signatures) && signatures.length === 0) {
177
+ lookups += 1;
178
+ kind = SignatureKind.Construct;
179
+ signatures = queries.signaturesOf(type, kind);
180
+ }
144
181
  let called;
145
182
  const [signature, ...more] = isAnswered(signatures) ? signatures : [];
146
183
  if (signature !== undefined && more.length === 0) {
@@ -148,6 +185,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
148
185
  const result = queries.returnTypeOf(signature);
149
186
  lookups += parameters.length + 1;
150
187
  called = {
188
+ kind,
151
189
  parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
152
190
  result: isAnswered(result) ? result : undefined,
153
191
  };
@@ -188,7 +226,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
188
226
  continue;
189
227
  }
190
228
  const other = called === undefined ? undefined : callOf(part);
191
- if (called === undefined || other === undefined) {
229
+ if (called === undefined || other?.kind !== called.kind) {
192
230
  continue;
193
231
  }
194
232
  other.parameters.forEach((parameter, index) => {
@@ -264,5 +302,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
264
302
  reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
265
303
  }
266
304
  });
305
+ const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
306
+ batches += batchesOf(spreads.length);
307
+ const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
308
+ spreads.forEach(({ value, from }, index) => {
309
+ const type = spreadTypes[index];
310
+ if (type === undefined || !isAnswered(type)) {
311
+ return;
312
+ }
313
+ // A spread gets every own enumerable property, an overwritten key's getter included.
314
+ // So it reads each data member but a class's accessor, which sits on the prototype with
315
+ // the class's methods (never data members), and a private name, which is no property
316
+ // (https://tc39.es/ecma262/#sec-copydataproperties).
317
+ const own = (member, id) => !privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
318
+ const ids = [
319
+ ...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
320
+ ];
321
+ if (ids.length > 0) {
322
+ reads.push({ at: value, from, ids });
323
+ }
324
+ });
267
325
  return { writes, reads, batches, lookups };
268
326
  }
@@ -39,8 +39,43 @@ const BABEL_CONFIG = [
39
39
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
40
40
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
41
41
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
42
+ /**
43
+ * The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
44
+ * resolves `setupFiles` against that root (https://vitest.dev/config/setupfiles).
45
+ */
46
+ const VITEST_ROOTS = ["test.root", "root"];
47
+ /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
48
+ const ANGULAR_ENTRIES = [
49
+ "src/{main,main.server,server,polyfills,test}.ts",
50
+ "server.ts",
51
+ "projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
52
+ "projects/*/server.ts",
53
+ ];
54
+ /** The `angular.json` members whose strings name the packages a build loads. */
55
+ const ANGULAR_PACKAGE_KEYS = [
56
+ "builder",
57
+ "polyfills",
58
+ "styles",
59
+ "scripts",
60
+ ].map((key) => ({ key, files: ["angular.json"] }));
42
61
  /** Every row, ordered by name and then by range. */
43
62
  export const CONVENTION_ROWS = [
63
+ {
64
+ name: "angular",
65
+ package: "@angular/build",
66
+ range: ">=17.0.0 <22.0.0",
67
+ entries: ANGULAR_ENTRIES,
68
+ moves: [],
69
+ packageKeys: ANGULAR_PACKAGE_KEYS,
70
+ },
71
+ {
72
+ name: "angular-devkit",
73
+ package: "@angular-devkit/build-angular",
74
+ range: ">=12.0.0 <22.0.0",
75
+ entries: ANGULAR_ENTRIES,
76
+ moves: [],
77
+ packageKeys: ANGULAR_PACKAGE_KEYS,
78
+ },
44
79
  {
45
80
  name: "astro",
46
81
  package: "astro",
@@ -472,6 +507,18 @@ export const CONVENTION_ROWS = [
472
507
  },
473
508
  ],
474
509
  },
510
+ {
511
+ name: "vitepress",
512
+ package: "vitepress",
513
+ range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
514
+ entries: [
515
+ `.vitepress/config.${ANY_MODULE}`,
516
+ `.vitepress/theme/index.${ANY_MODULE}`,
517
+ `**/*.data.${ANY_MODULE}`,
518
+ "**/*.vue",
519
+ ],
520
+ moves: [],
521
+ },
475
522
  {
476
523
  name: "vitest",
477
524
  package: "vitest",
@@ -493,5 +540,6 @@ export const CONVENTION_ROWS = [
493
540
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
494
541
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
495
542
  ],
543
+ testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
496
544
  },
497
545
  ];
@@ -413,6 +413,35 @@ function holdsValue(held, value) {
413
413
  return first !== undefined && literalText(first) === value;
414
414
  }));
415
415
  }
416
+ /** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
417
+ function testFileRoot(file, dir, reading) {
418
+ for (const property of reading.roots) {
419
+ const values = valuesAt(file, property, reading.call);
420
+ if (values.length > 0) {
421
+ const texts = new Set(values.map(literalText));
422
+ const [text] = texts;
423
+ return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
424
+ }
425
+ }
426
+ return dir;
427
+ }
428
+ /** The files an applied row's test-file properties name. */
429
+ function testFilesNamed(host, parse, dir, readings, into) {
430
+ for (const reading of readings) {
431
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
432
+ const at = testFileRoot(file, dir, reading);
433
+ if (at === undefined) {
434
+ continue;
435
+ }
436
+ for (const node of stringsAt(file, reading.property, reading.call)) {
437
+ const text = literalText(node);
438
+ if (text !== undefined && text !== "") {
439
+ into.add(resolvePath(at, text));
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
416
445
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
417
446
  function selectedStrings(host, parse, dir, readings, into) {
418
447
  for (const reading of readings) {
@@ -602,6 +631,50 @@ function helperUses(host, parse, dir, manifest, readings) {
602
631
  readingFiles(host, parse, dir, reading.files).some(({ file }) => valuesAt(file, reading.property, reading.call).some((held) => holdsValue(held, reading.value))))
603
632
  .map((reading) => reading.package);
604
633
  }
634
+ /**
635
+ * The dependencies one manifest declares that an applied row's package keys use: each
636
+ * whose name a string under a member of a key's name equals, or begins followed by `/` or
637
+ * by `:` and a name. A member's own name is no such string.
638
+ */
639
+ function packageKeyUses(host, parse, dir, manifest, keys) {
640
+ const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
641
+ const held = isRecord(manifest) ? manifest[section] : undefined;
642
+ return isRecord(held) ? Object.keys(held) : [];
643
+ });
644
+ const strings = [];
645
+ const collect = (node) => {
646
+ const text = literalText(node);
647
+ if (text !== undefined) {
648
+ strings.push(text);
649
+ }
650
+ else if (isPropertyAssignment(node)) {
651
+ collect(node.initializer);
652
+ }
653
+ else {
654
+ node.forEachChild(collect);
655
+ }
656
+ };
657
+ for (const { key, files } of keys) {
658
+ const visit = (node) => {
659
+ if (isPropertyAssignment(node) && keyOf(node) === key) {
660
+ collect(node.initializer);
661
+ }
662
+ node.forEachChild(visit);
663
+ };
664
+ for (const { file } of readingFiles(host, parse, dir, files)) {
665
+ const values = declaredValues(file);
666
+ for (const one of defaultExports(file)) {
667
+ const object = objectOf(one, values, true);
668
+ if (object !== undefined) {
669
+ visit(object);
670
+ }
671
+ }
672
+ }
673
+ }
674
+ return declared.filter((name) => strings.some((text) => text === name ||
675
+ text.startsWith(`${name}/`) ||
676
+ (text.startsWith(`${name}:`) && text.length > name.length + 1)));
677
+ }
605
678
  /** Two strings ordered bytewise. */
606
679
  function compare(a, b) {
607
680
  if (a === b) {
@@ -620,6 +693,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
620
693
  const failures = [];
621
694
  const uses = new Set();
622
695
  const selected = new Map();
696
+ const testFiles = new Set();
623
697
  for (const manifest of manifests) {
624
698
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
625
699
  const versions = new Map();
@@ -680,6 +754,10 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
680
754
  for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
681
755
  uses.add(name);
682
756
  }
757
+ const keys = row.packageKeys ?? [];
758
+ for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
759
+ uses.add(name);
760
+ }
683
761
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
684
762
  uses.add(name);
685
763
  }
@@ -687,6 +765,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
687
765
  uses.add(name);
688
766
  }
689
767
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
768
+ testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
690
769
  const excludes = [
691
770
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
692
771
  ].map(globExpression);
@@ -714,6 +793,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
714
793
  failures,
715
794
  uses: [...uses].sort(compare),
716
795
  selected,
796
+ testFiles,
717
797
  };
718
798
  }
719
799
  /**
@@ -1,17 +1,19 @@
1
1
  /**
2
- * The dependencies the target's manifest and its workspace members' manifests declare, and
3
- * what the projects' own files need from outside the target. A file needs a package it
4
- * imports, re-exports from, augments or references the types of, by name or through the
5
- * file the specifier resolves to; a compiler configuration needs one it names, and a
6
- * configuration file a tool loads needs each dependency its strings spell. A package one
7
- * dependency pulls in for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and what the
3
+ * projects' own files need from outside the target. A file needs a package it imports, re-exports
4
+ * from, augments, references the types of or asks the runtime to resolve, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a configuration
6
+ * file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
7
+ * itself is that dependency's need, not the target's.
8
8
  */
9
- import { isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { isCallExpression, isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNamespaceExport, isPropertyAccessExpression, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
10
10
  import { configurationFiles, declaredIn as declaredBeside, dependenciesNamed, moduleStrings, } from "./configuration-files.js";
11
11
  import { nodeKey, packageScope } from "./inventory.js";
12
+ import { scriptTokens } from "./manifest.js";
12
13
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
13
14
  import { UNANSWERED } from "./query.js";
14
15
  import { sourceFilesOf } from "./source-files.js";
16
+ import { workflowTokens } from "./workflow-steps.js";
15
17
  const MANIFEST = "package.json";
16
18
  const MODULES_DIR = "node_modules";
17
19
  const MODULES_SEGMENT = `/${MODULES_DIR}/`;
@@ -29,7 +31,7 @@ export function configurationNeeds(packages) {
29
31
  return {
30
32
  packages: new Set(packages),
31
33
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
32
- unanswered: false,
34
+ unanswered: [],
33
35
  };
34
36
  }
35
37
  function isRecord(value) {
@@ -219,9 +221,11 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
219
221
  }
220
222
  };
221
223
  use([PROJECT_OWNER], packages);
222
- let unanswered = false;
224
+ const unanswered = [];
223
225
  for (const file of project.ownSourceFiles()) {
224
- unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
226
+ if (fileNeeds(project, held, file, programPackages, use)) {
227
+ unanswered.push(file.fileName);
228
+ }
225
229
  }
226
230
  const beside = new Map();
227
231
  const declaredAt = (path) => {
@@ -240,16 +244,20 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
240
244
  use(ownersOf(file, held, node), dependenciesNamed(text, names));
241
245
  }
242
246
  }
243
- for (const document of configuration.documents) {
244
- const names = declaredAt(document.path);
245
- for (const text of document.strings) {
246
- use([PROJECT_OWNER], dependenciesNamed(text, names));
247
+ for (const { strings, manifestDirs } of configuration.documents) {
248
+ for (const manifestDir of manifestDirs) {
249
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
250
+ for (const text of strings) {
251
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
252
+ }
247
253
  }
248
254
  }
249
- for (const { file, manifestDir } of configuration.outside) {
250
- const names = declaredAt(joinPath(manifestDir, "package.json"));
251
- for (const { text } of moduleStrings(file)) {
252
- use([PROJECT_OWNER], dependenciesNamed(text, names));
255
+ for (const { file, manifestDirs } of configuration.outside) {
256
+ for (const manifestDir of manifestDirs) {
257
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
258
+ for (const { text } of moduleStrings(file)) {
259
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
260
+ }
253
261
  }
254
262
  }
255
263
  return { packages, uses, unanswered };
@@ -365,8 +373,40 @@ function fileNeeds(project, held, file, programPackages, use) {
365
373
  for (const reference of file.typeReferenceDirectives) {
366
374
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
367
375
  }
376
+ for (const specifier of resolvedSpecifiers(file)) {
377
+ const named = packageOfSpecifier(specifier.text);
378
+ if (named !== undefined) {
379
+ use(ownersOf(file, held, specifier), [named]);
380
+ }
381
+ }
368
382
  return resolved.slice(0, specifiers.length).includes(UNANSWERED);
369
383
  }
384
+ /**
385
+ * The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
386
+ * or `require.resolve`, each of which needs the package it names installed.
387
+ */
388
+ function resolvedSpecifiers(file) {
389
+ const found = [];
390
+ if (!file.text.includes(".resolve(")) {
391
+ return found;
392
+ }
393
+ const visit = (node) => {
394
+ if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
395
+ const callee = node.expression;
396
+ const [argument] = node.arguments;
397
+ const receiver = callee.expression;
398
+ const resolving = callee.name.text === "resolve" &&
399
+ ((isMetaProperty(receiver) && receiver.name.text === "meta") ||
400
+ (isIdentifier(receiver) && receiver.text === "require"));
401
+ if (resolving && argument !== undefined && isLiteralText(argument)) {
402
+ found.push(argument);
403
+ }
404
+ }
405
+ node.forEachChild(visit);
406
+ };
407
+ visit(file);
408
+ return found;
409
+ }
370
410
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
371
411
  function valueEnd(text, at) {
372
412
  const first = text.charAt(at);
@@ -455,20 +495,16 @@ function installedOf(host, root, name) {
455
495
  for (let dir = root;; dir = dirnamePath(dir)) {
456
496
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
457
497
  if (read !== undefined && isRecord(read.value)) {
458
- const manifest = read.value;
459
- const bin = manifest["bin"];
460
- const peers = manifest["peerDependencies"];
461
- const meta = isRecord(manifest["peerDependenciesMeta"])
462
- ? manifest["peerDependenciesMeta"]
463
- : {};
498
+ const bin = read.value["bin"];
499
+ // A string `bin` names one command, the package's name without its scope.
464
500
  return {
465
- command: (typeof bin === "string" && bin !== "") || (isRecord(bin) && Object.keys(bin).length > 0),
466
- peers: isRecord(peers)
467
- ? Object.keys(peers).filter((peer) => {
468
- const about = meta[peer];
469
- return !(isRecord(about) && about["optional"] === true);
470
- })
471
- : [],
501
+ commands: typeof bin === "string"
502
+ ? bin === ""
503
+ ? []
504
+ : [name.slice(name.indexOf("/") + 1)]
505
+ : isRecord(bin)
506
+ ? Object.keys(bin)
507
+ : [],
472
508
  };
473
509
  }
474
510
  if (dirnamePath(dir) === dir) {
@@ -501,37 +537,70 @@ function declaredIn(text, manifest, path, scope) {
501
537
  }
502
538
  return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
503
539
  }
540
+ /** The key one declared dependency is known by: its manifest's path and its name. */
541
+ export function declarationKey(dependency) {
542
+ return `${dependency.position.path}\u0000${dependency.name}`;
543
+ }
544
+ /** Every token of every value of one manifest's `scripts` member. */
545
+ function scriptTokensOf(manifest) {
546
+ const scripts = manifest["scripts"];
547
+ const found = new Set();
548
+ if (isRecord(scripts)) {
549
+ for (const command of Object.values(scripts)) {
550
+ if (typeof command === "string") {
551
+ scriptTokens(command).forEach((token) => found.add(token));
552
+ }
553
+ }
554
+ }
555
+ return found;
556
+ }
557
+ /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
558
+ function isInside(path, dir) {
559
+ return dir === "" || path.startsWith(`${dir}/`);
560
+ }
504
561
  /**
505
562
  * The run's dependency evidence: the target's manifest and each workspace member's in
506
563
  * `members` below the target root, read with the position of every dependency each
507
- * declares, the installed copy of each found from its manifest's directory, what the
508
- * projects need, and the last uses each deletion candidate holds. A manifest the
509
- * program cannot read declares nothing.
564
+ * declares, what the projects and the commands its scripts and workflow steps run need,
565
+ * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
566
+ * in configurations the run could not build. A manifest the program cannot read declares
567
+ * nothing.
510
568
  */
511
- export function dependenciesOf(host, targetRoot, perProject, candidates, members = []) {
569
+ export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
512
570
  const root = resolvePath(host.workingDirectory(), targetRoot);
513
571
  const scopeOf = packageScope(host, targetRoot);
514
572
  const manifests = new Map([[root, MANIFEST]]);
515
- for (const dir of members) {
573
+ const unbuilt = new Set();
574
+ for (const dir of [...members, ...unbuiltMembers]) {
516
575
  const below = relativePath(root, dir);
517
576
  if (below !== undefined && !manifests.has(dir)) {
518
577
  manifests.set(dir, `${below}/${MANIFEST}`);
519
578
  }
520
579
  }
580
+ for (const dir of unbuiltMembers) {
581
+ const path = manifests.get(dir);
582
+ if (path !== undefined && dir !== root) {
583
+ unbuilt.add(path);
584
+ }
585
+ }
586
+ const workflow = new Set(workflowTokens(host, targetRoot));
521
587
  const declared = [];
522
- const installed = new Map();
588
+ const ran = new Set();
523
589
  for (const [dir, path] of manifests) {
524
590
  const read = readJSON(host, joinPath(dir, MANIFEST));
525
591
  if (read === undefined || !isRecord(read.value)) {
526
592
  continue;
527
593
  }
594
+ const tokens = scriptTokensOf(read.value);
528
595
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
529
596
  declared.push(dependency);
530
- if (!installed.has(dependency.name)) {
531
- const found = installedOf(host, dir, dependency.name);
532
- if (found !== undefined) {
533
- installed.set(dependency.name, found);
534
- }
597
+ const found = installedOf(host, dir, dependency.name);
598
+ if (found === undefined) {
599
+ continue;
600
+ }
601
+ const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
602
+ if (found.commands.some(runs)) {
603
+ ran.add(declarationKey(dependency));
535
604
  }
536
605
  }
537
606
  }
@@ -540,9 +609,15 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
540
609
  const needed = new Set();
541
610
  const uses = new Map();
542
611
  for (const project of perProject) {
543
- // A specifier the checker left unanswered may resolve to any declared dependency,
544
- // and what the configuration needs is never deleted, so it is needed there.
545
- const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
612
+ // A specifier the checker left unanswered may resolve to any dependency a manifest at
613
+ // or above its file declares, and what the configuration needs is never deleted.
614
+ const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
615
+ const unknown = declared
616
+ .filter((dependency) => {
617
+ const dir = dirnamePath(dependency.position.path);
618
+ return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
619
+ })
620
+ .map((dependency) => dependency.name);
546
621
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
547
622
  for (const [id, used] of [...project.uses, ...held]) {
548
623
  const merged = uses.get(id) ?? new Set();
@@ -563,5 +638,5 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
563
638
  lastUses.set(id, sole.sort((a, b) => (a < b ? -1 : 1)));
564
639
  }
565
640
  }
566
- return { declared, installed, needed, lastUses };
641
+ return { declared, needed, ran, unbuilt, lastUses };
567
642
  }
@@ -23,7 +23,7 @@ export class DiscoveryError extends Error {
23
23
  /** The category an error diagnostic carries, which the protocol sends as a number. */
24
24
  const ERROR_CATEGORY = DiagnosticCategory.Error;
25
25
  /** Whether one diagnostic is an error, which is what fails a run closed. */
26
- export function isError(diagnostic) {
26
+ function isError(diagnostic) {
27
27
  return diagnostic.category === ERROR_CATEGORY;
28
28
  }
29
29
  /**
package/dist/src/edges.js CHANGED
@@ -25,7 +25,7 @@ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and a
25
25
  const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
26
26
  const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
27
27
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
28
- export class EdgesError extends ConfigError {
28
+ class EdgesError extends ConfigError {
29
29
  constructor(at, text, want) {
30
30
  super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
31
31
  this.name = "EdgesError";