@cplieger/deadset-ts 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +9 -16
  2. package/dist/src/analysis.js +142 -0
  3. package/dist/src/components.js +289 -0
  4. package/dist/src/config.js +6 -0
  5. package/dist/src/discover.js +98 -24
  6. package/dist/src/entry-point-gaps.js +278 -0
  7. package/dist/src/entry-points.js +385 -0
  8. package/dist/src/finding.js +12 -0
  9. package/dist/src/glob.js +65 -0
  10. package/dist/src/graph.js +116 -0
  11. package/dist/src/index.js +1 -2
  12. package/dist/src/manifest.js +120 -0
  13. package/dist/src/print.js +3 -6
  14. package/dist/src/references.js +156 -106
  15. package/dist/src/resolve.js +82 -20
  16. package/dist/src/roots.js +345 -0
  17. package/dist/src/run.js +59 -10
  18. package/dist/src/schema.js +1 -1
  19. package/dist/src/session.js +6 -1
  20. package/dist/src/source-files.js +122 -0
  21. package/dist/src/sweep.js +156 -0
  22. package/dist/src/version.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +3 -3
  25. package/src/analysis.ts +236 -0
  26. package/src/components.ts +372 -0
  27. package/src/config.ts +30 -2
  28. package/src/discover.ts +144 -29
  29. package/src/entry-point-gaps.ts +304 -0
  30. package/src/entry-points.ts +496 -0
  31. package/src/finding.ts +43 -0
  32. package/src/glob.ts +67 -0
  33. package/src/graph.ts +178 -0
  34. package/src/index.ts +1 -20
  35. package/src/manifest.ts +174 -0
  36. package/src/print.ts +5 -6
  37. package/src/references.ts +201 -111
  38. package/src/resolve.ts +117 -24
  39. package/src/roots.ts +467 -0
  40. package/src/run.ts +66 -11
  41. package/src/schema.ts +5 -2
  42. package/src/session.ts +6 -1
  43. package/src/source-files.ts +157 -0
  44. package/src/sweep.ts +230 -0
  45. package/src/version.ts +1 -1
@@ -0,0 +1,345 @@
1
+ /**
2
+ * The root set of one project: every declaration the analysis keeps live without a
3
+ * reference, and why.
4
+ *
5
+ * A root makes its declaration live under REACHABILITY and plays no part in
6
+ * reference counting, which asks only whether some declaration of the loaded graph
7
+ * names it. The two relations are different sets, so an exported declaration that
8
+ * something outside the program reaches is live under the first and still a
9
+ * candidate under the second.
10
+ */
11
+ import { REPOSITORY_DOCUMENT } from "./config.js";
12
+ import { entryPoints } from "./entry-points.js";
13
+ import { globExpression } from "./glob.js";
14
+ import { nodeKey } from "./inventory.js";
15
+ import { byPosition } from "./position.js";
16
+ import { sourceFilesOf } from "./source-files.js";
17
+ /** The code the Contract gives a configured root that matches nothing. */
18
+ const UNMATCHED_ROOT_CODE = "DS1704";
19
+ /** The subject kind a finding under that code carries, from the Contract's vocabulary. */
20
+ const UNMATCHED_ROOT_SUBJECT = "root";
21
+ /** The fixed position of a finding about a document rather than about a line of one. */
22
+ const DOCUMENT_POSITION = 1;
23
+ /** Whether a configured string carries either special character. */
24
+ function isPattern(text) {
25
+ return text.includes("*") || text.includes("?");
26
+ }
27
+ /**
28
+ * Whether ref matches a pattern whose only special characters are `*`, which stands
29
+ * for any run of characters including the solidus, and `?`, which stands for exactly
30
+ * one character counted as a code point. A star keeps the position it last stood at,
31
+ * so the shortest run it can stand for is tried first.
32
+ */
33
+ function matchGlob(pattern, ref) {
34
+ let next = 0;
35
+ let star = -1;
36
+ let resume = 0;
37
+ let at = 0;
38
+ while (at < ref.length) {
39
+ if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
40
+ next += 1;
41
+ at += 1;
42
+ }
43
+ else if (next < pattern.length && pattern[next] === "*") {
44
+ star = next;
45
+ resume = at;
46
+ next += 1;
47
+ }
48
+ else if (star >= 0) {
49
+ resume += 1;
50
+ next = star + 1;
51
+ at = resume;
52
+ }
53
+ else {
54
+ return false;
55
+ }
56
+ }
57
+ while (next < pattern.length && pattern[next] === "*") {
58
+ next += 1;
59
+ }
60
+ return next === pattern.length;
61
+ }
62
+ /**
63
+ * Whether one configured string names the symbol reference ref. A string carrying
64
+ * neither special character names only the reference it spells.
65
+ *
66
+ * Both sides are read as code points: a character outside the basic plane is two
67
+ * string units, and `?` stands for one character.
68
+ */
69
+ export function matchRef(pattern, ref) {
70
+ if (!isPattern(pattern)) {
71
+ return pattern === ref;
72
+ }
73
+ return matchGlob(Array.from(pattern), Array.from(ref));
74
+ }
75
+ /** Two strings ordered bytewise, which is the order every set of a run is read in. */
76
+ function compare(a, b) {
77
+ if (a === b) {
78
+ return 0;
79
+ }
80
+ return a < b ? -1 : 1;
81
+ }
82
+ /** The members of one declaration, indexed by the container that holds them. */
83
+ function childrenOf(held) {
84
+ const byParent = new Map();
85
+ for (const symbol of held.symbols) {
86
+ const siblings = byParent.get(symbol.parent);
87
+ if (siblings === undefined) {
88
+ byParent.set(symbol.parent, [symbol]);
89
+ }
90
+ else {
91
+ siblings.push(symbol);
92
+ }
93
+ }
94
+ return byParent;
95
+ }
96
+ /**
97
+ * The roots one project holds, and every configured string that named nothing in it.
98
+ *
99
+ * A file a manifest member, an entry-file pattern or a tool's configuration names
100
+ * is a root with everything its module exports: each name of the module's export
101
+ * table, and where a name is a re-export, every link of the chain to the declaration
102
+ * it carries, so a re-exported declaration is a root of the file that publishes it.
103
+ * A file a runtime executes without reading its exports, a test file or a worker, is
104
+ * a root by itself.
105
+ */
106
+ export function roots(project, held, targetRoot, options) {
107
+ const files = sourceFilesOf(project, targetRoot);
108
+ const declared = held.declarations;
109
+ const fileIds = new Map();
110
+ for (const file of files.byName.values()) {
111
+ const id = declared.get(nodeKey(file, file));
112
+ if (id !== undefined) {
113
+ fileIds.set(file.fileName, id);
114
+ }
115
+ }
116
+ const found = new Map();
117
+ const add = (id, kind, source) => {
118
+ found.set([id, kind, source].join("\u0000"), { id, kind, source });
119
+ };
120
+ const entered = [];
121
+ const enter = (file, kind, source, exports) => {
122
+ entered.push({ file, kind, source, exports });
123
+ };
124
+ const published = [];
125
+ const publishes = (entry) => entry.published &&
126
+ (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
127
+ for (const entry of options.manifest.entries) {
128
+ for (const file of files.named(entry.path)) {
129
+ enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
130
+ if (options.publishedAPI && publishes(entry)) {
131
+ published.push(file);
132
+ }
133
+ }
134
+ }
135
+ for (const pattern of options.entryFiles) {
136
+ const expression = globExpression(pattern);
137
+ for (const [path, file] of files.byPath) {
138
+ if (expression.test(path)) {
139
+ enter(file, "entry-file", pattern, true);
140
+ }
141
+ }
142
+ }
143
+ for (const point of entryPoints(project, files, options.testFiles)) {
144
+ enter(point.file, point.rule, point.source, point.exports);
145
+ }
146
+ const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
147
+ for (const entry of entered) {
148
+ const id = fileIds.get(entry.file.fileName);
149
+ if (id === undefined) {
150
+ continue;
151
+ }
152
+ add(id, entry.kind, entry.source);
153
+ if (entry.exports) {
154
+ for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
155
+ add(exported, entry.kind, entry.source);
156
+ }
157
+ }
158
+ }
159
+ if (options.publishedAPI) {
160
+ for (const id of publishedAPI(published, exportsOf, held)) {
161
+ add(id, "published-api", "");
162
+ }
163
+ }
164
+ const unmatched = configured(held, options.patterns, add);
165
+ const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
166
+ const ordered = [...found.values()].sort((a, b) => {
167
+ const left = sites.get(a.id);
168
+ const right = sites.get(b.id);
169
+ const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
170
+ return site || compare(a.kind, b.kind) || compare(a.source, b.source);
171
+ });
172
+ return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
173
+ }
174
+ /**
175
+ * Every declaration each file's module exports, by the file's name: the declaration
176
+ * each name of the export table is, and every link of a re-export's chain. A name
177
+ * whose declaration is outside the project's own files belongs to another program
178
+ * and names nothing here.
179
+ *
180
+ * One batch resolves every file to its module and one request per module reads
181
+ * its export table, star re-exports resolved into it. A file that is no module
182
+ * exports nothing.
183
+ */
184
+ function exportTables(project, held, wanted, ownFiles) {
185
+ const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
186
+ const tables = new Map();
187
+ if (unique.length === 0) {
188
+ return tables;
189
+ }
190
+ const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
191
+ const chains = new Map();
192
+ const chainOf = (symbol) => {
193
+ const cached = chains.get(symbol.id);
194
+ if (cached !== undefined) {
195
+ return cached;
196
+ }
197
+ const ids = [];
198
+ const walked = new Set();
199
+ let at = symbol;
200
+ while (at !== undefined && !walked.has(at.id)) {
201
+ walked.add(at.id);
202
+ for (const handle of at.declarations) {
203
+ if (!ownFiles.has(handle.path)) {
204
+ continue;
205
+ }
206
+ const node = project.declarationAt(handle)?.node;
207
+ const id = node === undefined
208
+ ? undefined
209
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
210
+ if (id !== undefined && !ids.includes(id)) {
211
+ ids.push(id);
212
+ }
213
+ }
214
+ at = project.aliasStepOf(at);
215
+ }
216
+ chains.set(symbol.id, ids);
217
+ return ids;
218
+ };
219
+ unique.forEach((file, index) => {
220
+ const module = modules[index];
221
+ if (module === undefined) {
222
+ tables.set(file.fileName, []);
223
+ return;
224
+ }
225
+ const ids = new Set();
226
+ for (const exported of project.checker.getExportsOfModule(module)) {
227
+ for (const id of chainOf(exported)) {
228
+ ids.add(id);
229
+ }
230
+ }
231
+ tables.set(file.fileName, [...ids]);
232
+ });
233
+ return tables;
234
+ }
235
+ /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
236
+ const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
237
+ /**
238
+ * Every declaration a consumer of the library can name: what the published files
239
+ * export, and below each, the members a consumer reaches through it.
240
+ *
241
+ * The published files are the ones `exports` reaches where the manifest declares
242
+ * one, because a package that declares `exports` has said exactly what a consumer may
243
+ * import; where it declares none, they are the ones the members that preceded it
244
+ * name. Below a published module or namespace the walk follows what its export table
245
+ * names; below any other published declaration it follows every member a consumer
246
+ * can write: a public one, and a protected one, which a consumer's subclass names. A
247
+ * private member is not published, nor is anything below one, and a type parameter
248
+ * is not published either, because a consumer supplies a type argument by position.
249
+ *
250
+ * A published file is not itself published. A consumer imports a module and names
251
+ * what it exports; the file is a root because the manifest names it, which is a
252
+ * different reason.
253
+ */
254
+ function publishedAPI(published, exportsOf, held) {
255
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
256
+ const children = childrenOf(held);
257
+ const reached = new Set();
258
+ const pending = [];
259
+ for (const file of published) {
260
+ for (const id of exportsOf.get(file.fileName) ?? []) {
261
+ if (byId.get(id)?.kind === "file") {
262
+ pending.push(id);
263
+ }
264
+ else if (!reached.has(id)) {
265
+ reached.add(id);
266
+ pending.push(id);
267
+ }
268
+ }
269
+ }
270
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
271
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
272
+ for (const member of children.get(next) ?? []) {
273
+ const reachable = exporting
274
+ ? member.exported
275
+ : member.visibility !== "private" && member.visibility !== "private-name";
276
+ if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
277
+ continue;
278
+ }
279
+ reached.add(member.id);
280
+ pending.push(member.id);
281
+ }
282
+ }
283
+ return [...reached];
284
+ }
285
+ /**
286
+ * Keeps every declaration the configuration names and returns every string that
287
+ * named none, in configuration order. A string the configuration lists twice is one
288
+ * configured root.
289
+ */
290
+ function configured(held, patterns, add) {
291
+ const unmatched = [];
292
+ for (const pattern of new Set(patterns)) {
293
+ const kind = isPattern(pattern) ? "pattern" : "configured";
294
+ let matched = false;
295
+ for (const symbol of held.symbols) {
296
+ if (matchRef(pattern, symbol.ref)) {
297
+ add(symbol.id, kind, pattern);
298
+ matched = true;
299
+ }
300
+ }
301
+ if (!matched) {
302
+ unmatched.push(pattern);
303
+ }
304
+ }
305
+ return unmatched;
306
+ }
307
+ /**
308
+ * Every configured string that named no declaration in ANY project of the run, in
309
+ * configuration order and each once.
310
+ *
311
+ * A finding about a configured root is about the configuration and not about a
312
+ * declaration, so there is one per run: a string that names a declaration of one
313
+ * project has named something, and the projects it named nothing in are not a
314
+ * finding. A run with no project names nothing with any of them.
315
+ */
316
+ export function unmatchedEverywhere(patterns, perProject) {
317
+ return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
318
+ }
319
+ /**
320
+ * The findings a run reports for the configured strings that named nothing.
321
+ *
322
+ * `document` is the path of the document that declared the roots, below the target
323
+ * root; where the run read them from no document of the target it is empty and the
324
+ * finding names the conventional repository configuration, which is the document a
325
+ * maintainer writes a configured root in. Every finding of one run sits at that
326
+ * document's first position: the roots are written as an array whose members carry
327
+ * no line of their own, so the string each finding names is what tells one from
328
+ * another.
329
+ */
330
+ export function unmatchedRoots(sources, document) {
331
+ const position = {
332
+ path: document === "" ? REPOSITORY_DOCUMENT : document,
333
+ line: DOCUMENT_POSITION,
334
+ column: DOCUMENT_POSITION,
335
+ endLine: DOCUMENT_POSITION,
336
+ };
337
+ return sources.map((source) => ({
338
+ code: UNMATCHED_ROOT_CODE,
339
+ position,
340
+ symbol: { ref: source, kind: UNMATCHED_ROOT_SUBJECT, name: source, sizeLines: 1 },
341
+ message: isPattern(source)
342
+ ? "configured root pattern matches no symbol of the inventory"
343
+ : "configured root matches no symbol of the inventory",
344
+ }));
345
+ }
package/dist/src/run.js CHANGED
@@ -1,15 +1,15 @@
1
- import { ConfigError } from "./config.js";
1
+ import { runRoots } from "./analysis.js";
2
+ import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
2
3
  import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
3
4
  import { joinPath, resolvePath } from "./paths.js";
4
5
  import { printConfig } from "./print.js";
5
- import { resolve } from "./resolve.js";
6
+ import { resolve, resolveMatrix } from "./resolve.js";
6
7
  import { readScope, ScopeError, scopeForDir } from "./scope.js";
7
8
  import { diagnosticsOf, openEngine, runSession } from "./session.js";
8
9
  import { CONTRACT_VERSION } from "./version.js";
9
- /** The repository configuration's name at the target root. */
10
- const REPOSITORY_DOCUMENT = "deadset.json";
11
10
  /** The exit codes the Contract's exit-code table names. */
12
11
  const EXIT_CLEAN = 0;
12
+ const EXIT_FINDINGS = 1;
13
13
  const EXIT_USAGE = 2;
14
14
  const EXIT_FAILURE = 3;
15
15
  const VERBS = [
@@ -171,6 +171,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
171
171
  if (verb === "print-projects") {
172
172
  return printProjectsVerb(args.slice(1), out, err, host, openClient);
173
173
  }
174
+ if (verb === "print-roots") {
175
+ return printRootsVerb(args.slice(1), out, err, host, openClient);
176
+ }
174
177
  if (verb !== undefined && VERBS.includes(verb)) {
175
178
  err.write(`deadset-ts: ${verb} is not implemented\n`);
176
179
  return EXIT_USAGE;
@@ -194,21 +197,25 @@ function printConfigVerb(args, out, err, host) {
194
197
  }
195
198
  }
196
199
  /**
197
- * Writes the compiler configuration of every project one run analyzes, one per
198
- * line, in discovery order, having read each project's diagnostics first.
200
+ * Writes the name of every project one run analyzes, one per line, having read each
201
+ * project's diagnostics first: the identifier the build matrix declares for it, or,
202
+ * where discovery derived the project, its configuration file's path below the
203
+ * target root.
199
204
  */
200
205
  function printProjectsVerb(args, out, err, host, openClient) {
201
206
  let engine;
202
207
  try {
203
208
  const options = readOptions(args);
209
+ const matrix = resolveMatrix(inputsOf(host, options));
204
210
  const scope = scopeOf(host, options);
205
211
  engine = openClient(false);
206
- const discovered = discoverProjects(engine, host, scope);
212
+ const discovered = discoverProjects(engine, host, scope, matrix);
213
+ const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
207
214
  const failures = [];
208
215
  const { projects } = runSession(engine, discovered.configFiles, (project) => {
209
216
  const errors = diagnosticErrors(diagnosticsOf(project));
210
217
  failures.push(...errors.map(renderDiagnostic));
211
- return project.configFile;
218
+ return ids.get(project.configFile) ?? project.configFile;
212
219
  });
213
220
  if (failures.length > 0) {
214
221
  for (const line of failures) {
@@ -217,8 +224,8 @@ function printProjectsVerb(args, out, err, host, openClient) {
217
224
  err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
218
225
  return EXIT_FAILURE;
219
226
  }
220
- for (const configFile of projects) {
221
- out.write(`${configFile}\n`);
227
+ for (const id of projects) {
228
+ out.write(`${id}\n`);
222
229
  }
223
230
  return EXIT_CLEAN;
224
231
  }
@@ -227,6 +234,48 @@ function printProjectsVerb(args, out, err, host, openClient) {
227
234
  return refuse(error, err);
228
235
  }
229
236
  }
237
+ /**
238
+ * One root as a line: the declaration's reference, why it is a root, and the string
239
+ * that named it, separated by tabs. A run of several configurations adds the ones
240
+ * whose project holds the root as a fourth field, and writes the third present and
241
+ * empty where no string named the root, so every line of one run holds the same
242
+ * fields.
243
+ */
244
+ function rootLine(root, several) {
245
+ const line = `${root.ref}\t${root.kind}`;
246
+ if (several) {
247
+ return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
248
+ }
249
+ return root.source === "" ? line : `${line}\t${root.source}`;
250
+ }
251
+ /**
252
+ * Writes the root set of the run, one root per line, and then names on the error
253
+ * stream, by its issue kind, every configured root or pattern that named nothing;
254
+ * one such string fails the run. The line shape is this command's own rather than a
255
+ * Contract format, so that sort and cut read it.
256
+ */
257
+ function printRootsVerb(args, out, err, host, openClient) {
258
+ let engine;
259
+ try {
260
+ const options = readOptions(args);
261
+ const { config, provenance } = resolve(inputsOf(host, options));
262
+ const scope = scopeOf(host, options);
263
+ engine = openClient(false);
264
+ const answer = runRoots(engine, host, scope, config, provenance);
265
+ const several = answer.configurations.length > 1;
266
+ for (const root of answer.roots) {
267
+ out.write(`${rootLine(root, several)}\n`);
268
+ }
269
+ for (const finding of answer.findings) {
270
+ err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
271
+ }
272
+ return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
273
+ }
274
+ catch (error) {
275
+ engine?.close();
276
+ return refuse(error, err);
277
+ }
278
+ }
230
279
  /** Turns one refusal into the exit code the Contract's table gives it. */
231
280
  function refuse(error, err) {
232
281
  if (error instanceof ConfigError) {
@@ -19,7 +19,7 @@ export const SCHEMA_ROOT = {
19
19
  consumer_tests: leaf,
20
20
  configurations: {
21
21
  kind: "list",
22
- members: { id: leaf, os: leaf, arch: leaf, tags: leaf },
22
+ members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
23
23
  },
24
24
  matrix: { kind: "section", members: { complete: leaf } },
25
25
  template_dirs: leaf,
@@ -1,4 +1,4 @@
1
- import { API } from "@typescript/native/unstable/sync";
1
+ import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import { isIdentifier } from "@typescript/native/unstable/ast";
3
3
  /**
4
4
  * Opens the compiler client the analysis reads through.
@@ -53,6 +53,11 @@ function viewOf(project) {
53
53
  resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
54
54
  shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
55
55
  aliasStepOf: (symbol) => {
56
+ // The step is asked of an alias only: asked of any other symbol, the compiler
57
+ // fails an assertion and the session ends.
58
+ if ((symbol.flags & SymbolFlags.Alias) === 0) {
59
+ return undefined;
60
+ }
56
61
  const held = project.checker.getImmediateAliasedSymbol(symbol);
57
62
  // An alias that resolves to nothing answers with the checker's own unknown
58
63
  // symbol, which declares nothing; a symbol that is no alias answers with
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The target's own source files of one project, looked up the ways a path written
3
+ * in a document or a string names one: as the file itself, or as a file the
4
+ * compiler emits from it.
5
+ */
6
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
7
+ /** The source extensions the compiler emits each output extension from. */
8
+ const EMITTED = [
9
+ { emitted: ".d.ts", sources: [".ts", ".tsx"] },
10
+ { emitted: ".d.mts", sources: [".mts"] },
11
+ { emitted: ".d.cts", sources: [".cts"] },
12
+ { emitted: ".js", sources: [".ts", ".tsx"] },
13
+ { emitted: ".jsx", sources: [".tsx"] },
14
+ { emitted: ".mjs", sources: [".mts"] },
15
+ { emitted: ".cjs", sources: [".cts"] },
16
+ ];
17
+ /** The one character a subpath pattern of a manifest's `exports` holds. */
18
+ const SUBPATH_WILDCARD = "*";
19
+ /** One path with its extension replaced, where it carries the one named. */
20
+ function reExtended(path, emitted, source) {
21
+ return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
22
+ }
23
+ /**
24
+ * The longest directory every path shares, which is the directory the compiler
25
+ * emits from where the configuration declares no root directory.
26
+ */
27
+ function commonDirectory(paths) {
28
+ const first = paths[0];
29
+ if (first === undefined) {
30
+ return "";
31
+ }
32
+ let common = dirnamePath(first).split("/");
33
+ for (const path of paths.slice(1)) {
34
+ const segments = dirnamePath(path).split("/");
35
+ let shared = 0;
36
+ while (shared < common.length &&
37
+ shared < segments.length &&
38
+ common[shared] === segments[shared]) {
39
+ shared += 1;
40
+ }
41
+ common = common.slice(0, shared);
42
+ }
43
+ return common.join("/");
44
+ }
45
+ /**
46
+ * Whether one path is what a subpath pattern stands for: the text before the
47
+ * wildcard, any run of characters including the solidus, then the text after it.
48
+ */
49
+ function matchesSubpath(pattern, path) {
50
+ const at = pattern.indexOf(SUBPATH_WILDCARD);
51
+ const before = pattern.slice(0, at);
52
+ const after = pattern.slice(at + SUBPATH_WILDCARD.length);
53
+ return (path.length >= before.length + after.length && path.startsWith(before) && path.endsWith(after));
54
+ }
55
+ /**
56
+ * The lookups over one project's own files.
57
+ *
58
+ * A path that names an emitted file is read back to the source the project would
59
+ * emit it from through the project's own emit mapping and nothing else: the
60
+ * extension is one the compiler emits from a source extension, and a path below the
61
+ * output directory is read below the directory the compiler emits from, which the
62
+ * configuration declares or which is the directory every input file shares.
63
+ */
64
+ export function sourceFilesOf(project, targetRoot) {
65
+ const byName = new Map();
66
+ const byPath = new Map();
67
+ for (const file of project.ownSourceFiles()) {
68
+ byName.set(file.fileName, file);
69
+ const path = relativePath(targetRoot, file.fileName);
70
+ if (path !== undefined) {
71
+ byPath.set(path, file);
72
+ }
73
+ }
74
+ const options = project.program.getCompilerOptions();
75
+ const configDir = dirnamePath(project.configFile);
76
+ const at = (option) => option === undefined ? undefined : resolvePath(configDir, option);
77
+ const outDir = at(options.outDir);
78
+ const declarationDir = at(options.declarationDir);
79
+ const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
80
+ /** One path read below the directory the compiler emits from, where it is below the one it emits to. */
81
+ const fromEmit = (path, emitted) => {
82
+ const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
83
+ if (emitDir === undefined) {
84
+ return undefined;
85
+ }
86
+ const below = relativePath(emitDir, path);
87
+ return below === undefined ? undefined : joinPath(sourceDir, below);
88
+ };
89
+ /** The source paths one written path may stand for, the path itself first. */
90
+ const candidates = (path) => {
91
+ const found = [path];
92
+ for (const { emitted, sources } of EMITTED) {
93
+ const mapped = fromEmit(path, emitted);
94
+ for (const source of sources) {
95
+ for (const from of [path, mapped]) {
96
+ const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
97
+ if (candidate !== undefined) {
98
+ found.push(candidate);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return found;
104
+ };
105
+ return {
106
+ byPath,
107
+ byName,
108
+ named: (path) => {
109
+ if (!path.includes(SUBPATH_WILDCARD)) {
110
+ for (const candidate of candidates(path)) {
111
+ const held = byName.get(candidate);
112
+ if (held !== undefined) {
113
+ return [held];
114
+ }
115
+ }
116
+ return [];
117
+ }
118
+ const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
119
+ return [...byName.values()].filter((file) => patterns.some((pattern) => matchesSubpath(pattern, file.fileName)));
120
+ },
121
+ };
122
+ }