@cplieger/deadset-ts 4.0.1 → 5.1.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 (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The programs of a workspace's configurations, each reading the members it imports
3
+ * from source. Each round builds the programs, answers their member imports, and gives
4
+ * the answers back as static resolutions, which the compiler reads as the program's own
5
+ * files rather than a library's, until a round adds none. A configuration with no
6
+ * member import keeps the project the compiler opens for it.
7
+ */
8
+ import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
9
+ import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
10
+ import { dirnamePath, joinPath, resolvePath } from "./paths.js";
11
+ /** The most questions one round trip carries. */
12
+ const BATCH = 2048;
13
+ /**
14
+ * The options a configuration's program is built with: its own, less the ones that
15
+ * lay out an emitted program's inputs and outputs. The analysis emits nothing, and a
16
+ * member's source is outside the importer's root directory by construction, which
17
+ * the compiler refuses under such a layout.
18
+ */
19
+ function analysisOptions(parsed) {
20
+ const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
21
+ return composite === true ? { ...kept, declaration: true } : kept;
22
+ }
23
+ /**
24
+ * The references a built program can carry: only where every referenced
25
+ * configuration reads and is composite with output, because the compiler reports a
26
+ * broken reference at syntax a built program does not have.
27
+ */
28
+ function carriedReferences(engine, parsed) {
29
+ const references = parsed.projectReferences ?? [];
30
+ for (const reference of references) {
31
+ // A reference that names a directory names the `tsconfig.json` in it.
32
+ const configFile = reference.path.endsWith(".json")
33
+ ? reference.path
34
+ : joinPath(reference.path, "tsconfig.json");
35
+ let options;
36
+ try {
37
+ options = engine.parseConfigFile(configFile).options;
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ if (options.composite !== true || options.noEmit === true) {
43
+ return [];
44
+ }
45
+ }
46
+ return references;
47
+ }
48
+ /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
49
+ function conditionsOf(parsed, mode) {
50
+ const resolution = parsed.options.moduleResolution;
51
+ const nodeLike = resolution === ModuleResolutionKind.Node16 || resolution === ModuleResolutionKind.NodeNext;
52
+ return new Set([
53
+ "types",
54
+ mode === ModuleKind.CommonJS ? "require" : "import",
55
+ ...(nodeLike ? ["node"] : []),
56
+ ...(parsed.options.customConditions ?? []),
57
+ "default",
58
+ ]);
59
+ }
60
+ function resolutionMode(kind) {
61
+ return kind === ModuleKind.CommonJS || kind === ModuleKind.ESNext ? kind : undefined;
62
+ }
63
+ /** The directories the compiler leaves out of a configuration that names no `exclude`. */
64
+ const DEFAULT_EXCLUDES = ["node_modules", "bower_components", "jspm_packages"];
65
+ /** Why one configuration's answers cannot be `paths`: a specifier read as two files. */
66
+ function unstatable(reading) {
67
+ const answered = new Set(reading.entries.map((entry) => entry.moduleName));
68
+ for (const [specifier, files] of reading.bound) {
69
+ if (answered.has(specifier) && files.size > 1) {
70
+ return `the imports of ${specifier} are read as ${String(files.size)} different files, which a configuration cannot state`;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ /** The path beside `configFile` its written configuration takes, which no file has. */
76
+ function writtenPath(host, configFile) {
77
+ const stem = configFile.replace(/\.json$/u, "");
78
+ for (let counter = 0;; counter += 1) {
79
+ const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
80
+ if (host.kindOf(path) === "absent") {
81
+ return path;
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * The configuration that reads as one configuration's program: it extends it, lays out
87
+ * no output for the reason {@link analysisOptions} gives, maps each answered specifier
88
+ * to its file, and carries the references the program does. The compiler's default
89
+ * `exclude` names the output directories this drops, so where the file list moves it is
90
+ * restated; a list that still differs, or answers `paths` cannot state, are a reason.
91
+ */
92
+ function configurationOf(engine, host, reading) {
93
+ const reason = unstatable(reading);
94
+ if (reason !== undefined) {
95
+ return { kind: "unread", reason };
96
+ }
97
+ const { options } = reading.parsed;
98
+ const raw = options["pathsBasePath"];
99
+ const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
100
+ const paths = {};
101
+ for (const [key, targets] of Object.entries(options.paths ?? {})) {
102
+ paths[key] = targets.map((target) => resolvePath(base, target));
103
+ }
104
+ for (const entry of reading.entries) {
105
+ const [file] = reading.bound.get(entry.moduleName) ?? [];
106
+ if (file !== undefined) {
107
+ paths[entry.moduleName] = [file];
108
+ }
109
+ }
110
+ const json = (exclude) => ({
111
+ extends: reading.configFile,
112
+ compilerOptions: {
113
+ rootDir: null,
114
+ outDir: null,
115
+ declarationDir: null,
116
+ tsBuildInfoFile: null,
117
+ ...(options.composite === true ? { composite: false, declaration: true } : {}),
118
+ paths,
119
+ },
120
+ ...(reading.references.length > 0
121
+ ? { references: reading.references.map((reference) => ({ path: reference.path })) }
122
+ : {}),
123
+ ...(exclude === undefined ? {} : { exclude }),
124
+ });
125
+ const file = writtenPath(host, reading.configFile);
126
+ const expected = [...reading.parsed.fileNames].sort().join("\u0000");
127
+ const listed = (value) => {
128
+ try {
129
+ return [...engine.parseConfigJson(value, file).fileNames].sort().join("\u0000");
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ };
135
+ let value = json(undefined);
136
+ if (listed(value) !== expected) {
137
+ value = json([
138
+ ...DEFAULT_EXCLUDES,
139
+ ...[options.outDir, options.declarationDir].filter((dir) => dir !== undefined),
140
+ ]);
141
+ if (listed(value) !== expected) {
142
+ return {
143
+ kind: "unread",
144
+ reason: "its file list cannot be restated without its output directories",
145
+ };
146
+ }
147
+ }
148
+ return { kind: "configuration", file, text: `${JSON.stringify(value, null, 2)}\n` };
149
+ }
150
+ /** The program one configuration is built as, under the answers it holds so far. */
151
+ function programOf(engine, reading) {
152
+ const compilerOptions = analysisOptions(reading.parsed);
153
+ const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
154
+ return {
155
+ resolver,
156
+ params: {
157
+ rootFiles: reading.parsed.fileNames,
158
+ compilerOptions,
159
+ options: {
160
+ moduleResolver: resolver,
161
+ configFileParsingDiagnostics: reading.parsed.errors,
162
+ ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
163
+ },
164
+ },
165
+ };
166
+ }
167
+ function chunks(items) {
168
+ const found = [];
169
+ for (let at = 0; at < items.length; at += BATCH) {
170
+ found.push(items.slice(at, at + BATCH));
171
+ }
172
+ return found;
173
+ }
174
+ /** Asks one question of every item, in round trips of at most {@link BATCH}. */
175
+ function batched(engine, items, question) {
176
+ return chunks(items).flatMap((run) => engine.batch(run.map(question)));
177
+ }
178
+ /** One round over one program: answers the member imports no earlier round asked about. */
179
+ function scan(engine, program, reading, resolver) {
180
+ const names = program.getSourceFileNames().filter((name) => !name.includes("/node_modules/"));
181
+ const files = batched(engine, names, (name) => program.getSourceFile.gen(name));
182
+ const usages = [];
183
+ files.forEach((file, at) => {
184
+ const name = names[at];
185
+ if (file === undefined || name === undefined) {
186
+ return;
187
+ }
188
+ for (const node of file.imports) {
189
+ if (isStringLiteralLikeNode(node) && resolver.names(node.text)) {
190
+ usages.push({ file: name, node });
191
+ }
192
+ }
193
+ });
194
+ const modes = batched(engine, usages, (usage) => program.getModeForUsageLocation.gen(usage.file, usage.node));
195
+ const asked = [];
196
+ usages.forEach((usage, at) => {
197
+ const mode = resolutionMode(modes[at] ?? ModuleKind.None);
198
+ const key = [usage.node.text, dirnamePath(usage.file), String(mode)].join("\u0000");
199
+ if (!reading.seen.has(key)) {
200
+ reading.seen.add(key);
201
+ asked.push({ usage, mode });
202
+ }
203
+ });
204
+ const answers = batched(engine, asked, ({ usage }) => program.getResolvedModuleFromModuleSpecifier.gen(usage.node, usage.file));
205
+ let added = 0;
206
+ asked.forEach(({ usage, mode }, at) => {
207
+ const answered = answers[at];
208
+ const specifier = usage.node.text;
209
+ const containingDirectory = dirnamePath(usage.file);
210
+ const compiled = answered?.resolvedFileName === undefined || answered.resolvedFileName === ""
211
+ ? undefined
212
+ : answered;
213
+ const answer = resolver.resolve(specifier, containingDirectory, conditionsOf(reading.parsed, mode), compiled);
214
+ if (answer.kind !== "unresolved") {
215
+ const bound = reading.bound.get(specifier) ?? new Set();
216
+ bound.add(answer.kind === "source" ? answer.file : (compiled?.resolvedFileName ?? ""));
217
+ reading.bound.set(specifier, bound);
218
+ }
219
+ if (answer.kind === "source") {
220
+ reading.entries.push({
221
+ moduleName: specifier,
222
+ containingDirectory,
223
+ resolutionMode: mode,
224
+ result: { resolvedFileName: answer.file },
225
+ });
226
+ added += 1;
227
+ }
228
+ else if (answer.kind === "unresolved") {
229
+ reading.unresolved.push({
230
+ file: usage.file,
231
+ specifier,
232
+ member: answer.member,
233
+ subpath: answer.subpath,
234
+ reason: answer.reason,
235
+ });
236
+ }
237
+ });
238
+ return added;
239
+ }
240
+ /**
241
+ * Answers the member imports of every reading `build` describes, round by round, all
242
+ * the readings still adding answers built in one snapshot per round. A reading `build`
243
+ * gives no description for leaves the rounds.
244
+ */
245
+ function readRounds(engine, readings, resolver, build) {
246
+ let pending = readings;
247
+ while (pending.length > 0) {
248
+ const built = pending.flatMap((reading) => {
249
+ const one = build(reading);
250
+ return one === undefined ? [] : [{ reading, one }];
251
+ });
252
+ const programs = built.flatMap(({ one }) => (one.kind === "program" ? [one.params] : []));
253
+ const written = new Map(built.flatMap(({ one }) => one.kind === "configuration" ? [[one.file, one.text]] : []));
254
+ const snapshot = engine.createSnapshot([...written.keys()], programs, written);
255
+ try {
256
+ const created = snapshot.operation.createdPrograms ?? [];
257
+ const opened = new Map(snapshot.getProjects().map((project) => [project.configFileName, project.program]));
258
+ let at = 0;
259
+ pending = built.flatMap(({ reading, one }) => {
260
+ const program = one.kind === "program" ? created[at++] : opened.get(one.file);
261
+ return program !== undefined && scan(engine, program, reading, resolver) > 0
262
+ ? [reading]
263
+ : [];
264
+ });
265
+ }
266
+ finally {
267
+ snapshot.dispose();
268
+ for (const { one } of built) {
269
+ if (one.kind === "program") {
270
+ one.resolver[Symbol.dispose]();
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ /**
277
+ * Reads every configuration's member imports. Where the snapshots read component files,
278
+ * the rounds go on over each reading's configuration, which reads them, for the imports
279
+ * only they make.
280
+ */
281
+ export function workspacePrograms(engine, host, configFiles, resolver) {
282
+ const readings = configFiles.flatMap((configFile) => {
283
+ let parsed;
284
+ try {
285
+ parsed = engine.parseConfigFile(configFile);
286
+ }
287
+ catch {
288
+ // A configuration that does not read is opened as a project, which states why.
289
+ return [];
290
+ }
291
+ return [
292
+ {
293
+ configFile,
294
+ parsed,
295
+ references: carriedReferences(engine, parsed),
296
+ entries: [],
297
+ seen: new Set(),
298
+ bound: new Map(),
299
+ unresolved: [],
300
+ },
301
+ ];
302
+ });
303
+ readRounds(engine, readings, resolver, (reading) => ({
304
+ kind: "program",
305
+ ...programOf(engine, reading),
306
+ }));
307
+ if (engine.readsComponents) {
308
+ readRounds(engine, readings, resolver, (reading) => {
309
+ const one = configurationOf(engine, host, reading);
310
+ return one.kind === "configuration" ? one : undefined;
311
+ });
312
+ }
313
+ const programs = new Map();
314
+ const unread = new Map();
315
+ for (const reading of readings) {
316
+ if (reading.entries.length === 0) {
317
+ continue;
318
+ }
319
+ const one = engine.readsComponents ? configurationOf(engine, host, reading) : undefined;
320
+ if (one !== undefined && one.kind !== "unread") {
321
+ programs.set(reading.configFile, one);
322
+ continue;
323
+ }
324
+ programs.set(reading.configFile, {
325
+ kind: "program",
326
+ params: programOf(engine, reading).params,
327
+ });
328
+ if (one !== undefined) {
329
+ unread.set(reading.configFile, one.reason);
330
+ }
331
+ }
332
+ return {
333
+ programs,
334
+ readings: readings.map((reading) => ({
335
+ configFile: reading.configFile,
336
+ fileNames: reading.parsed.fileNames,
337
+ entries: reading.entries,
338
+ unresolved: reading.unresolved,
339
+ componentsUnread: unread.get(reading.configFile),
340
+ })),
341
+ };
342
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Which file an import of a workspace package's name is read from. The package is the
3
+ * one `node_modules` links the importer to or, with no link, the one the importer's
4
+ * manifest depends on. A source file the compiler reaches stands; any other file is read
5
+ * back to the source it is compiled from or to a manifest target that is source, a file
6
+ * the package's configurations neither compile nor write is read as installed, and an
7
+ * import nothing answers for is unresolved.
8
+ */
9
+ import { emittedFrom } from "./emit-map.js";
10
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
11
+ import { satisfies } from "./version-range.js";
12
+ /** The manifest members that name an entry point for the subpath `.` without `exports`. */
13
+ const LEGACY_MEMBERS = ["types", "typings", "module", "main"];
14
+ /** The dependency members a manifest declares other packages under. */
15
+ const DEPENDENCY_MEMBERS = [
16
+ "dependencies",
17
+ "devDependencies",
18
+ "peerDependencies",
19
+ "optionalDependencies",
20
+ ];
21
+ const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
22
+ const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ /** The package name a bare specifier names, or `undefined` for any other specifier. */
27
+ export function packageNameOf(specifier) {
28
+ if (specifier === "" ||
29
+ specifier.startsWith(".") ||
30
+ specifier.startsWith("#") ||
31
+ isAbsolutePath(specifier) ||
32
+ specifier.includes(":")) {
33
+ return undefined;
34
+ }
35
+ const [first, second] = specifier.split("/");
36
+ if (specifier.startsWith("@")) {
37
+ return second === undefined || second === "" ? undefined : `${first ?? ""}/${second}`;
38
+ }
39
+ return first;
40
+ }
41
+ function inside(dir, path) {
42
+ return path === dir || relativePath(dir, path) !== undefined;
43
+ }
44
+ /** The `exports` entry one subpath selects, with what a pattern's `*` matched. */
45
+ function exportsEntry(exportsField, subpath) {
46
+ const map = isRecord(exportsField) && Object.keys(exportsField).some((key) => key.startsWith("."))
47
+ ? exportsField
48
+ : { ".": exportsField };
49
+ if (Object.hasOwn(map, subpath)) {
50
+ return { value: map[subpath] };
51
+ }
52
+ let best;
53
+ for (const key of Object.keys(map)) {
54
+ const star = key.indexOf("*");
55
+ if (star < 0 || key.slice(star + 1).includes("*")) {
56
+ continue;
57
+ }
58
+ const prefix = key.slice(0, star);
59
+ const suffix = key.slice(star + 1);
60
+ if (subpath.length >= key.length &&
61
+ subpath.startsWith(prefix) &&
62
+ subpath.endsWith(suffix) &&
63
+ (best === undefined ||
64
+ prefix.length > best.prefix ||
65
+ (prefix.length === best.prefix && key.length > best.key.length))) {
66
+ best = {
67
+ key,
68
+ prefix: prefix.length,
69
+ match: subpath.slice(prefix.length, subpath.length - suffix.length),
70
+ };
71
+ }
72
+ }
73
+ return best === undefined ? undefined : { value: map[best.key], match: best.match };
74
+ }
75
+ /** The leaf a condition set selects from one `exports` value, in the order Node reads it. */
76
+ function selected(value, conditions) {
77
+ if (typeof value === "string") {
78
+ return value;
79
+ }
80
+ if (Array.isArray(value)) {
81
+ for (const alternative of value) {
82
+ const found = selected(alternative, conditions);
83
+ if (found !== undefined) {
84
+ return found;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ if (!isRecord(value)) {
90
+ return undefined;
91
+ }
92
+ for (const [key, held] of Object.entries(value)) {
93
+ if (conditions.has(key)) {
94
+ return selected(held, conditions);
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /** Every leaf one `exports` value carries, in document order. */
100
+ function leaves(value) {
101
+ if (typeof value === "string") {
102
+ return [value];
103
+ }
104
+ if (Array.isArray(value)) {
105
+ return value.flatMap(leaves);
106
+ }
107
+ return isRecord(value) ? Object.values(value).flatMap(leaves) : [];
108
+ }
109
+ /** Whether one dependency specifier names one member, written from one directory. */
110
+ function dependsOn(specifier, member, fromDir) {
111
+ const atPath = (path) => resolvePath(fromDir, path) === member.dir;
112
+ const inRange = (range) => member.version !== undefined && satisfies(member.version, range);
113
+ if (specifier.startsWith("workspace:")) {
114
+ const rest = specifier.slice("workspace:".length);
115
+ if (rest.startsWith(".") || rest.startsWith("/")) {
116
+ return atPath(rest);
117
+ }
118
+ const alias = /^((?:@[^/@]+\/)?[^/@]+)@(.*)$/u.exec(rest);
119
+ const range = alias === null ? rest : (alias[2] ?? "");
120
+ if (alias !== null && alias[1] !== member.name) {
121
+ return false;
122
+ }
123
+ return range === "*" || range === "^" || range === "~" || inRange(range);
124
+ }
125
+ for (const protocol of ["file:", "link:"]) {
126
+ if (specifier.startsWith(protocol)) {
127
+ return atPath(specifier.slice(protocol.length));
128
+ }
129
+ }
130
+ return inRange(specifier);
131
+ }
132
+ /** The member imports of one workspace, read from the members' sources. */
133
+ export function workspaceResolver(host, workspace) {
134
+ const links = new Map();
135
+ /** The directory `node_modules/<name>` names from one directory, links followed. */
136
+ const linkedFrom = (name, dir) => {
137
+ const key = `${name}\u0000${dir}`;
138
+ if (links.has(key)) {
139
+ return links.get(key);
140
+ }
141
+ const candidate = joinPath(dir, "node_modules", name);
142
+ let found;
143
+ if (host.kindOf(candidate) === "absent") {
144
+ const up = dirnamePath(dir);
145
+ found = up === dir ? undefined : linkedFrom(name, up);
146
+ }
147
+ else {
148
+ try {
149
+ found = host.realPath(candidate);
150
+ }
151
+ catch {
152
+ found = undefined;
153
+ }
154
+ }
155
+ links.set(key, found);
156
+ return found;
157
+ };
158
+ const linkOf = (candidates, fromDir) => {
159
+ const name = candidates[0]?.name ?? "";
160
+ const linked = linkedFrom(name, fromDir);
161
+ if (linked !== undefined) {
162
+ const member = candidates.find((one) => one.dir === linked);
163
+ return member === undefined ? { kind: "none" } : { kind: "member", member };
164
+ }
165
+ const scope = workspace.memberOf(joinPath(fromDir, "package.json"));
166
+ if (scope?.name === name) {
167
+ return { kind: "member", member: scope };
168
+ }
169
+ const manifest = scope?.manifest;
170
+ const declared = DEPENDENCY_MEMBERS.flatMap((field) => {
171
+ const held = manifest?.[field];
172
+ const specifier = isRecord(held) ? held[name] : undefined;
173
+ return typeof specifier === "string" ? [specifier] : [];
174
+ });
175
+ const matched = candidates.filter((member) => declared.some((specifier) => dependsOn(specifier, member, scope?.dir ?? fromDir)));
176
+ const [one, two] = matched;
177
+ if (one === undefined) {
178
+ return { kind: "none" };
179
+ }
180
+ return two === undefined
181
+ ? { kind: "member", member: one }
182
+ : { kind: "ambiguous", dirs: matched.map((member) => member.dir) };
183
+ };
184
+ /** The files a member's configurations compile, which is what makes a file there source. */
185
+ const compiled = (member, path) => member.configurations.some((configuration) => configuration.fileNames.has(path));
186
+ /**
187
+ * The member's source one of its paths is, or is compiled from: a TypeScript source
188
+ * file, the source an output path maps back to, or a file one of the member's
189
+ * configurations compiles. `undefined` where none is.
190
+ */
191
+ const sourceOf = (member, path) => {
192
+ if (SOURCE.test(path) && host.kindOf(path) === "file") {
193
+ return path;
194
+ }
195
+ for (const { layout } of member.configurations) {
196
+ const candidates = layout === undefined ? [] : emittedFrom(path, layout);
197
+ const found = candidates.find((candidate) => host.kindOf(candidate) === "file");
198
+ if (found !== undefined) {
199
+ return found;
200
+ }
201
+ }
202
+ if (OUTPUT.test(path)) {
203
+ const beside = emittedFrom(path, { sourceDirs: [] });
204
+ const found = beside.find((candidate) => host.kindOf(candidate) === "file");
205
+ if (found !== undefined) {
206
+ return found;
207
+ }
208
+ }
209
+ return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
210
+ };
211
+ /** The targets a member's manifest names for one subpath, the selected one first. */
212
+ const targetsOf = (member, subpath, conditions) => {
213
+ const exportsField = member.manifest["exports"];
214
+ if (exportsField !== undefined) {
215
+ const entry = exportsEntry(exportsField, subpath);
216
+ if (entry === undefined) {
217
+ return [];
218
+ }
219
+ const first = selected(entry.value, conditions);
220
+ const written = [...(first === undefined ? [] : [first]), ...leaves(entry.value)];
221
+ const found = written
222
+ .filter((target) => target.startsWith("./"))
223
+ .map((target) => entry.match === undefined ? target : target.replaceAll("*", entry.match));
224
+ return [...new Set(found)];
225
+ }
226
+ if (subpath === ".") {
227
+ return LEGACY_MEMBERS.flatMap((field) => {
228
+ const value = member.manifest[field];
229
+ return typeof value === "string" ? [value] : [];
230
+ });
231
+ }
232
+ return [
233
+ subpath,
234
+ `${subpath}.d.ts`,
235
+ `${subpath}.js`,
236
+ `${subpath}/index.d.ts`,
237
+ `${subpath}/index.js`,
238
+ ];
239
+ };
240
+ return {
241
+ workspace,
242
+ sourceOf,
243
+ names: (specifier) => {
244
+ const name = packageNameOf(specifier);
245
+ return name !== undefined && workspace.named.has(name);
246
+ },
247
+ resolve(specifier, fromDir, conditions, answered) {
248
+ const name = packageNameOf(specifier);
249
+ const candidates = name === undefined ? undefined : workspace.named.get(name);
250
+ if (name === undefined || candidates === undefined) {
251
+ return { kind: "default" };
252
+ }
253
+ const subpath = specifier === name ? "." : `.${specifier.slice(name.length)}`;
254
+ const linked = linkOf(candidates, fromDir);
255
+ if (linked.kind === "none") {
256
+ return { kind: "default" };
257
+ }
258
+ if (linked.kind === "ambiguous") {
259
+ return {
260
+ kind: "unresolved",
261
+ member: name,
262
+ subpath,
263
+ reason: `${String(linked.dirs.length)} workspace packages are named ${name} (${linked.dirs.join(", ")}) and nothing links the importer to one of them; install the workspace, or declare the dependency with a range only one of them satisfies`,
264
+ };
265
+ }
266
+ const { member } = linked;
267
+ // A member no compiler configuration builds is read as the package manager
268
+ // installed it, wherever the compiler's resolution found it.
269
+ if (answered !== undefined &&
270
+ (member.configurations.length === 0 || !inside(member.dir, answered.resolvedFileName))) {
271
+ return { kind: "default" };
272
+ }
273
+ if (answered !== undefined) {
274
+ const source = sourceOf(member, answered.resolvedFileName);
275
+ if (source !== undefined) {
276
+ return source === answered.resolvedFileName && answered.isExternalLibraryImport !== true
277
+ ? { kind: "default" }
278
+ : { kind: "source", file: source, member: member.name };
279
+ }
280
+ }
281
+ const targets = targetsOf(member, subpath, conditions);
282
+ for (const target of targets) {
283
+ const source = sourceOf(member, joinPath(member.dir, target));
284
+ if (source !== undefined) {
285
+ return { kind: "source", file: source, member: member.name };
286
+ }
287
+ }
288
+ // A file the member's configurations neither compile nor write, a hand-written
289
+ // declaration file or a bundler's output, is read as the package manager installed it.
290
+ if (answered !== undefined) {
291
+ return { kind: "default" };
292
+ }
293
+ const named = targets.length === 0 ? "no target" : targets.join(", ");
294
+ const built = targets.some((target) => host.kindOf(joinPath(member.dir, target)) === "file");
295
+ return {
296
+ kind: "unresolved",
297
+ member: name,
298
+ subpath,
299
+ reason: built
300
+ ? `its manifest names ${named}, output no tsconfig*.json of the package maps back to source, and nothing installed links the importer to it; install the workspace, or point an exports condition the importing configuration's customConditions lists at its source`
301
+ : member.configurations.length === 0
302
+ ? `the package has no tsconfig*.json and its manifest names ${named}, which does not exist; build the package`
303
+ : `its manifest names ${named}, which does not exist and which no tsconfig*.json of the package compiles to; build the package, give the tsconfig.json that compiles it an outDir and rootDir that map those targets, or point an exports condition the importing configuration's customConditions lists at its source`,
304
+ };
305
+ },
306
+ };
307
+ }