@cplieger/deadset-ts 4.0.0 → 5.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.
- package/README.md +68 -50
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +230 -37
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- package/dist/src/discover.js +11 -4
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/references.js +209 -30
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +6 -3
- package/dist/src/resolve.js +31 -6
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +4 -2
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +395 -71
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- package/src/discover.ts +11 -7
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +6 -3
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +4 -2
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
|
|
10
|
+
import { emittedFrom } from "./emit-map.ts";
|
|
11
|
+
import type { Host } from "./host.ts";
|
|
12
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
13
|
+
import { satisfies } from "./version-range.ts";
|
|
14
|
+
import type { Workspace, WorkspaceMember } from "./workspace.ts";
|
|
15
|
+
|
|
16
|
+
/** What the compiler's own resolution answered for one import. */
|
|
17
|
+
export interface DefaultResolution {
|
|
18
|
+
readonly resolvedFileName: string;
|
|
19
|
+
/** Whether the compiler reads the file as a library's rather than the program's own. */
|
|
20
|
+
readonly isExternalLibraryImport?: boolean | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The answer for one import. */
|
|
24
|
+
export type MemberResolution =
|
|
25
|
+
/** No member import, or one the compiler's resolution already reads as it should. */
|
|
26
|
+
| { readonly kind: "default" }
|
|
27
|
+
/** A member's source file, read as a file of the program. */
|
|
28
|
+
| { readonly kind: "source"; readonly file: string; readonly member: string }
|
|
29
|
+
/** A member import no source file answers for, with why. */
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: "unresolved";
|
|
32
|
+
readonly member: string;
|
|
33
|
+
readonly subpath: string;
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The manifest members that name an entry point for the subpath `.` without `exports`. */
|
|
38
|
+
const LEGACY_MEMBERS = ["types", "typings", "module", "main"] as const;
|
|
39
|
+
|
|
40
|
+
/** The dependency members a manifest declares other packages under. */
|
|
41
|
+
const DEPENDENCY_MEMBERS = [
|
|
42
|
+
"dependencies",
|
|
43
|
+
"devDependencies",
|
|
44
|
+
"peerDependencies",
|
|
45
|
+
"optionalDependencies",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
|
|
49
|
+
const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
|
|
50
|
+
|
|
51
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The package name a bare specifier names, or `undefined` for any other specifier. */
|
|
56
|
+
export function packageNameOf(specifier: string): string | undefined {
|
|
57
|
+
if (
|
|
58
|
+
specifier === "" ||
|
|
59
|
+
specifier.startsWith(".") ||
|
|
60
|
+
specifier.startsWith("#") ||
|
|
61
|
+
isAbsolutePath(specifier) ||
|
|
62
|
+
specifier.includes(":")
|
|
63
|
+
) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const [first, second] = specifier.split("/");
|
|
67
|
+
if (specifier.startsWith("@")) {
|
|
68
|
+
return second === undefined || second === "" ? undefined : `${first ?? ""}/${second}`;
|
|
69
|
+
}
|
|
70
|
+
return first;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inside(dir: string, path: string): boolean {
|
|
74
|
+
return path === dir || relativePath(dir, path) !== undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The `exports` entry one subpath selects, with what a pattern's `*` matched. */
|
|
78
|
+
function exportsEntry(
|
|
79
|
+
exportsField: unknown,
|
|
80
|
+
subpath: string,
|
|
81
|
+
): { readonly value: unknown; readonly match?: string } | undefined {
|
|
82
|
+
const map =
|
|
83
|
+
isRecord(exportsField) && Object.keys(exportsField).some((key) => key.startsWith("."))
|
|
84
|
+
? exportsField
|
|
85
|
+
: { ".": exportsField };
|
|
86
|
+
if (Object.hasOwn(map, subpath)) {
|
|
87
|
+
return { value: map[subpath] };
|
|
88
|
+
}
|
|
89
|
+
let best: { readonly key: string; readonly prefix: number; readonly match: string } | undefined;
|
|
90
|
+
for (const key of Object.keys(map)) {
|
|
91
|
+
const star = key.indexOf("*");
|
|
92
|
+
if (star < 0 || key.slice(star + 1).includes("*")) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const prefix = key.slice(0, star);
|
|
96
|
+
const suffix = key.slice(star + 1);
|
|
97
|
+
if (
|
|
98
|
+
subpath.length >= key.length &&
|
|
99
|
+
subpath.startsWith(prefix) &&
|
|
100
|
+
subpath.endsWith(suffix) &&
|
|
101
|
+
(best === undefined ||
|
|
102
|
+
prefix.length > best.prefix ||
|
|
103
|
+
(prefix.length === best.prefix && key.length > best.key.length))
|
|
104
|
+
) {
|
|
105
|
+
best = {
|
|
106
|
+
key,
|
|
107
|
+
prefix: prefix.length,
|
|
108
|
+
match: subpath.slice(prefix.length, subpath.length - suffix.length),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return best === undefined ? undefined : { value: map[best.key], match: best.match };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The leaf a condition set selects from one `exports` value, in the order Node reads it. */
|
|
116
|
+
function selected(value: unknown, conditions: ReadonlySet<string>): string | undefined {
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
for (const alternative of value) {
|
|
122
|
+
const found = selected(alternative, conditions);
|
|
123
|
+
if (found !== undefined) {
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
if (!isRecord(value)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
for (const [key, held] of Object.entries(value)) {
|
|
133
|
+
if (conditions.has(key)) {
|
|
134
|
+
return selected(held, conditions);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every leaf one `exports` value carries, in document order. */
|
|
141
|
+
function leaves(value: unknown): readonly string[] {
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
return [value];
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return value.flatMap(leaves);
|
|
147
|
+
}
|
|
148
|
+
return isRecord(value) ? Object.values(value).flatMap(leaves) : [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Whether one dependency specifier names one member, written from one directory. */
|
|
152
|
+
function dependsOn(specifier: string, member: WorkspaceMember, fromDir: string): boolean {
|
|
153
|
+
const atPath = (path: string): boolean => resolvePath(fromDir, path) === member.dir;
|
|
154
|
+
const inRange = (range: string): boolean =>
|
|
155
|
+
member.version !== undefined && satisfies(member.version, range);
|
|
156
|
+
if (specifier.startsWith("workspace:")) {
|
|
157
|
+
const rest = specifier.slice("workspace:".length);
|
|
158
|
+
if (rest.startsWith(".") || rest.startsWith("/")) {
|
|
159
|
+
return atPath(rest);
|
|
160
|
+
}
|
|
161
|
+
const alias = /^((?:@[^/@]+\/)?[^/@]+)@(.*)$/u.exec(rest);
|
|
162
|
+
const range = alias === null ? rest : (alias[2] ?? "");
|
|
163
|
+
if (alias !== null && alias[1] !== member.name) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return range === "*" || range === "^" || range === "~" || inRange(range);
|
|
167
|
+
}
|
|
168
|
+
for (const protocol of ["file:", "link:"]) {
|
|
169
|
+
if (specifier.startsWith(protocol)) {
|
|
170
|
+
return atPath(specifier.slice(protocol.length));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return inRange(specifier);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The resolution of member imports over one workspace. */
|
|
177
|
+
export interface WorkspaceResolver {
|
|
178
|
+
readonly workspace: Workspace;
|
|
179
|
+
/** Whether one specifier names a member at all, which is all that is asked before the compiler is. */
|
|
180
|
+
names(specifier: string): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* The member's source file one of its paths is or is compiled from, or `undefined`
|
|
183
|
+
* where none is: a path that is build output and has no source is not one.
|
|
184
|
+
*/
|
|
185
|
+
sourceOf(member: WorkspaceMember, path: string): string | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* The answer for one import from one directory, under the conditions its resolution
|
|
188
|
+
* mode selects. `answered` is the compiler's own resolution of it.
|
|
189
|
+
*/
|
|
190
|
+
resolve(
|
|
191
|
+
specifier: string,
|
|
192
|
+
fromDir: string,
|
|
193
|
+
conditions: ReadonlySet<string>,
|
|
194
|
+
answered: DefaultResolution | undefined,
|
|
195
|
+
): MemberResolution;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The member one import names, the ambiguity named, or none. */
|
|
199
|
+
type Linked =
|
|
200
|
+
| { readonly kind: "member"; readonly member: WorkspaceMember }
|
|
201
|
+
| { readonly kind: "none" }
|
|
202
|
+
| { readonly kind: "ambiguous"; readonly dirs: readonly string[] };
|
|
203
|
+
|
|
204
|
+
/** The member imports of one workspace, read from the members' sources. */
|
|
205
|
+
export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceResolver {
|
|
206
|
+
const links = new Map<string, string | undefined>();
|
|
207
|
+
/** The directory `node_modules/<name>` names from one directory, links followed. */
|
|
208
|
+
const linkedFrom = (name: string, dir: string): string | undefined => {
|
|
209
|
+
const key = `${name}\u0000${dir}`;
|
|
210
|
+
if (links.has(key)) {
|
|
211
|
+
return links.get(key);
|
|
212
|
+
}
|
|
213
|
+
const candidate = joinPath(dir, "node_modules", name);
|
|
214
|
+
let found: string | undefined;
|
|
215
|
+
if (host.kindOf(candidate) === "absent") {
|
|
216
|
+
const up = dirnamePath(dir);
|
|
217
|
+
found = up === dir ? undefined : linkedFrom(name, up);
|
|
218
|
+
} else {
|
|
219
|
+
try {
|
|
220
|
+
found = host.realPath(candidate);
|
|
221
|
+
} catch {
|
|
222
|
+
found = undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
links.set(key, found);
|
|
226
|
+
return found;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const linkOf = (candidates: readonly WorkspaceMember[], fromDir: string): Linked => {
|
|
230
|
+
const name = candidates[0]?.name ?? "";
|
|
231
|
+
const linked = linkedFrom(name, fromDir);
|
|
232
|
+
if (linked !== undefined) {
|
|
233
|
+
const member = candidates.find((one) => one.dir === linked);
|
|
234
|
+
return member === undefined ? { kind: "none" } : { kind: "member", member };
|
|
235
|
+
}
|
|
236
|
+
const scope = workspace.memberOf(joinPath(fromDir, "package.json"));
|
|
237
|
+
if (scope?.name === name) {
|
|
238
|
+
return { kind: "member", member: scope };
|
|
239
|
+
}
|
|
240
|
+
const manifest = scope?.manifest;
|
|
241
|
+
const declared = DEPENDENCY_MEMBERS.flatMap((field) => {
|
|
242
|
+
const held = manifest?.[field];
|
|
243
|
+
const specifier = isRecord(held) ? held[name] : undefined;
|
|
244
|
+
return typeof specifier === "string" ? [specifier] : [];
|
|
245
|
+
});
|
|
246
|
+
const matched = candidates.filter((member) =>
|
|
247
|
+
declared.some((specifier) => dependsOn(specifier, member, scope?.dir ?? fromDir)),
|
|
248
|
+
);
|
|
249
|
+
const [one, two] = matched;
|
|
250
|
+
if (one === undefined) {
|
|
251
|
+
return { kind: "none" };
|
|
252
|
+
}
|
|
253
|
+
return two === undefined
|
|
254
|
+
? { kind: "member", member: one }
|
|
255
|
+
: { kind: "ambiguous", dirs: matched.map((member) => member.dir) };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/** The files a member's configurations compile, which is what makes a file there source. */
|
|
259
|
+
const compiled = (member: WorkspaceMember, path: string): boolean =>
|
|
260
|
+
member.configurations.some((configuration) => configuration.fileNames.has(path));
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The member's source one of its paths is, or is compiled from: a TypeScript source
|
|
264
|
+
* file, the source an output path maps back to, or a file one of the member's
|
|
265
|
+
* configurations compiles. `undefined` where none is.
|
|
266
|
+
*/
|
|
267
|
+
const sourceOf = (member: WorkspaceMember, path: string): string | undefined => {
|
|
268
|
+
if (SOURCE.test(path) && host.kindOf(path) === "file") {
|
|
269
|
+
return path;
|
|
270
|
+
}
|
|
271
|
+
for (const { layout } of member.configurations) {
|
|
272
|
+
const candidates = layout === undefined ? [] : emittedFrom(path, layout);
|
|
273
|
+
const found = candidates.find((candidate) => host.kindOf(candidate) === "file");
|
|
274
|
+
if (found !== undefined) {
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (OUTPUT.test(path)) {
|
|
279
|
+
const beside = emittedFrom(path, { sourceDirs: [] });
|
|
280
|
+
const found = beside.find((candidate) => host.kindOf(candidate) === "file");
|
|
281
|
+
if (found !== undefined) {
|
|
282
|
+
return found;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** The targets a member's manifest names for one subpath, the selected one first. */
|
|
289
|
+
const targetsOf = (
|
|
290
|
+
member: WorkspaceMember,
|
|
291
|
+
subpath: string,
|
|
292
|
+
conditions: ReadonlySet<string>,
|
|
293
|
+
): readonly string[] => {
|
|
294
|
+
const exportsField = member.manifest["exports"];
|
|
295
|
+
if (exportsField !== undefined) {
|
|
296
|
+
const entry = exportsEntry(exportsField, subpath);
|
|
297
|
+
if (entry === undefined) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
const first = selected(entry.value, conditions);
|
|
301
|
+
const written = [...(first === undefined ? [] : [first]), ...leaves(entry.value)];
|
|
302
|
+
const found = written
|
|
303
|
+
.filter((target) => target.startsWith("./"))
|
|
304
|
+
.map((target) =>
|
|
305
|
+
entry.match === undefined ? target : target.replaceAll("*", entry.match),
|
|
306
|
+
);
|
|
307
|
+
return [...new Set(found)];
|
|
308
|
+
}
|
|
309
|
+
if (subpath === ".") {
|
|
310
|
+
return LEGACY_MEMBERS.flatMap((field) => {
|
|
311
|
+
const value = member.manifest[field];
|
|
312
|
+
return typeof value === "string" ? [value] : [];
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return [
|
|
316
|
+
subpath,
|
|
317
|
+
`${subpath}.d.ts`,
|
|
318
|
+
`${subpath}.js`,
|
|
319
|
+
`${subpath}/index.d.ts`,
|
|
320
|
+
`${subpath}/index.js`,
|
|
321
|
+
];
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
workspace,
|
|
326
|
+
sourceOf,
|
|
327
|
+
names: (specifier) => {
|
|
328
|
+
const name = packageNameOf(specifier);
|
|
329
|
+
return name !== undefined && workspace.named.has(name);
|
|
330
|
+
},
|
|
331
|
+
resolve(specifier, fromDir, conditions, answered) {
|
|
332
|
+
const name = packageNameOf(specifier);
|
|
333
|
+
const candidates = name === undefined ? undefined : workspace.named.get(name);
|
|
334
|
+
if (name === undefined || candidates === undefined) {
|
|
335
|
+
return { kind: "default" };
|
|
336
|
+
}
|
|
337
|
+
const subpath = specifier === name ? "." : `.${specifier.slice(name.length)}`;
|
|
338
|
+
const linked = linkOf(candidates, fromDir);
|
|
339
|
+
if (linked.kind === "none") {
|
|
340
|
+
return { kind: "default" };
|
|
341
|
+
}
|
|
342
|
+
if (linked.kind === "ambiguous") {
|
|
343
|
+
return {
|
|
344
|
+
kind: "unresolved",
|
|
345
|
+
member: name,
|
|
346
|
+
subpath,
|
|
347
|
+
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`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const { member } = linked;
|
|
351
|
+
// A member no compiler configuration builds is read as the package manager
|
|
352
|
+
// installed it, wherever the compiler's resolution found it.
|
|
353
|
+
if (
|
|
354
|
+
answered !== undefined &&
|
|
355
|
+
(member.configurations.length === 0 || !inside(member.dir, answered.resolvedFileName))
|
|
356
|
+
) {
|
|
357
|
+
return { kind: "default" };
|
|
358
|
+
}
|
|
359
|
+
if (answered !== undefined) {
|
|
360
|
+
const source = sourceOf(member, answered.resolvedFileName);
|
|
361
|
+
if (source !== undefined) {
|
|
362
|
+
return source === answered.resolvedFileName && answered.isExternalLibraryImport !== true
|
|
363
|
+
? { kind: "default" }
|
|
364
|
+
: { kind: "source", file: source, member: member.name };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const targets = targetsOf(member, subpath, conditions);
|
|
368
|
+
for (const target of targets) {
|
|
369
|
+
const source = sourceOf(member, joinPath(member.dir, target));
|
|
370
|
+
if (source !== undefined) {
|
|
371
|
+
return { kind: "source", file: source, member: member.name };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// A file the member's configurations neither compile nor write, a hand-written
|
|
375
|
+
// declaration file or a bundler's output, is read as the package manager installed it.
|
|
376
|
+
if (answered !== undefined) {
|
|
377
|
+
return { kind: "default" };
|
|
378
|
+
}
|
|
379
|
+
const named = targets.length === 0 ? "no target" : targets.join(", ");
|
|
380
|
+
const built = targets.some((target) => host.kindOf(joinPath(member.dir, target)) === "file");
|
|
381
|
+
return {
|
|
382
|
+
kind: "unresolved",
|
|
383
|
+
member: name,
|
|
384
|
+
subpath,
|
|
385
|
+
reason: built
|
|
386
|
+
? `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`
|
|
387
|
+
: member.configurations.length === 0
|
|
388
|
+
? `the package has no tsconfig*.json and its manifest names ${named}, which does not exist; build the package`
|
|
389
|
+
: `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`,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one run reads of the workspace its target sits in: the programs that read
|
|
3
|
+
* member imports from source, the configurations whose member imports have no source,
|
|
4
|
+
* the files each configuration holds only for their references, and the entry points
|
|
5
|
+
* the members' manifests name. A target in no workspace reads none of it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LocatedDiagnostic } from "./discover.ts";
|
|
9
|
+
import type { Host } from "./host.ts";
|
|
10
|
+
import type { Manifest, ManifestEntry } from "./manifest.ts";
|
|
11
|
+
import { relativePath } from "./paths.ts";
|
|
12
|
+
import type { Engine } from "./session.ts";
|
|
13
|
+
import { findWorkspace, referenceOnlyIn, type ParsedLayout } from "./workspace.ts";
|
|
14
|
+
import { memberEntries, targetManifestIn } from "./workspace-manifests.ts";
|
|
15
|
+
import {
|
|
16
|
+
workspacePrograms,
|
|
17
|
+
type MemberReading,
|
|
18
|
+
type WorkspaceProgram,
|
|
19
|
+
} from "./workspace-programs.ts";
|
|
20
|
+
import { workspaceResolver } from "./workspace-resolution.ts";
|
|
21
|
+
|
|
22
|
+
/** One run's workspace. */
|
|
23
|
+
export interface WorkspaceRun {
|
|
24
|
+
/** Per configuration that imports a member, how the snapshot reads it. */
|
|
25
|
+
readonly programs: ReadonlyMap<string, WorkspaceProgram>;
|
|
26
|
+
/**
|
|
27
|
+
* Per configuration whose component files the run cannot read, the reason, in the
|
|
28
|
+
* order the configurations are named.
|
|
29
|
+
*/
|
|
30
|
+
readonly componentsUnread: readonly { readonly configFile: string; readonly reason: string }[];
|
|
31
|
+
/**
|
|
32
|
+
* Why one configuration cannot be analyzed, a member it imports having no source, or
|
|
33
|
+
* `undefined` where every member import it makes has one.
|
|
34
|
+
*/
|
|
35
|
+
unbuildable(configFile: string): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Whether one configuration holds one file only for the references it makes: a
|
|
38
|
+
* member's file neither its parsed file list nor `compiled` names, the files its
|
|
39
|
+
* project names once opened.
|
|
40
|
+
*/
|
|
41
|
+
referenceOnly(configFile: string, compiled?: readonly string[]): (file: string) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether one file a target configuration's program holds is the target's own. A
|
|
44
|
+
* workspace package's file is by its path alone, whichever import reached it: the
|
|
45
|
+
* compiler's answer for a file reached both through `node_modules` and from source
|
|
46
|
+
* depends on which import it followed first. Any other file is unless the compiler
|
|
47
|
+
* reached it as a library's.
|
|
48
|
+
*/
|
|
49
|
+
ownFile(file: string, fromExternalLibrary: boolean): boolean;
|
|
50
|
+
/** The target's own manifest, its targets read back to source. */
|
|
51
|
+
readonly manifest: Manifest;
|
|
52
|
+
/** The entry points the other members' manifests name. */
|
|
53
|
+
readonly entries: readonly ManifestEntry[];
|
|
54
|
+
/** The directory of every member, the target's included, in the workspace's order. */
|
|
55
|
+
readonly memberDirs: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function layoutOf(engine: Engine, configFile: string): ParsedLayout | undefined {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = engine.parseConfigFile(configFile);
|
|
61
|
+
return {
|
|
62
|
+
fileNames: parsed.fileNames,
|
|
63
|
+
outDir: parsed.options.outDir,
|
|
64
|
+
declarationDir: parsed.options.declarationDir,
|
|
65
|
+
rootDir: parsed.options.rootDir,
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The setup failure one configuration's unresolved member imports state, with the fix.
|
|
74
|
+
* An import in a file the configuration holds only for its references is that file's
|
|
75
|
+
* own configuration's to answer for, like any other error there.
|
|
76
|
+
*/
|
|
77
|
+
function unbuildableReason(
|
|
78
|
+
reading: MemberReading,
|
|
79
|
+
referenceOnly: (file: string) => boolean,
|
|
80
|
+
targetRoot: string,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
const own = reading.unresolved.filter((one) => !referenceOnly(one.file));
|
|
83
|
+
const [first] = own;
|
|
84
|
+
if (first === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const where = relativePath(targetRoot, first.file) ?? first.file;
|
|
88
|
+
const more = own.length > 1 ? ` (and ${String(own.length - 1)} more)` : "";
|
|
89
|
+
return `${where} imports ${first.specifier}, a package of the workspace with no source to read: ${first.reason}${more}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The workspace the target at `targetRoot` sits in, read for the configurations named,
|
|
94
|
+
* or `undefined` where the target sits in none.
|
|
95
|
+
*/
|
|
96
|
+
export function workspaceRun(
|
|
97
|
+
engine: Engine,
|
|
98
|
+
host: Host,
|
|
99
|
+
targetRoot: string,
|
|
100
|
+
configFiles: readonly string[],
|
|
101
|
+
manifest: Manifest,
|
|
102
|
+
): WorkspaceRun | undefined {
|
|
103
|
+
const workspace = findWorkspace(host, targetRoot, (configFile) => layoutOf(engine, configFile));
|
|
104
|
+
if (workspace === undefined) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const resolver = workspaceResolver(host, workspace);
|
|
108
|
+
const read = workspacePrograms(engine, host, configFiles, resolver);
|
|
109
|
+
const readings = new Map(read.readings.map((reading) => [reading.configFile, reading]));
|
|
110
|
+
const referenceOnly = (
|
|
111
|
+
configFile: string,
|
|
112
|
+
compiled: readonly string[] = [],
|
|
113
|
+
): ((file: string) => boolean) =>
|
|
114
|
+
referenceOnlyIn(
|
|
115
|
+
workspace,
|
|
116
|
+
new Set([...(readings.get(configFile)?.fileNames ?? []), ...compiled]),
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
programs: read.programs,
|
|
120
|
+
componentsUnread: read.readings.flatMap(({ configFile, componentsUnread }) =>
|
|
121
|
+
componentsUnread === undefined ? [] : [{ configFile, reason: componentsUnread }],
|
|
122
|
+
),
|
|
123
|
+
unbuildable: (configFile) => {
|
|
124
|
+
const reading = readings.get(configFile);
|
|
125
|
+
return reading === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: unbuildableReason(reading, referenceOnly(configFile), targetRoot);
|
|
128
|
+
},
|
|
129
|
+
referenceOnly,
|
|
130
|
+
ownFile: (file, fromExternalLibrary) =>
|
|
131
|
+
workspace.memberOf(file) !== undefined
|
|
132
|
+
? relativePath(targetRoot, file) !== undefined
|
|
133
|
+
: !fromExternalLibrary,
|
|
134
|
+
manifest: targetManifestIn(resolver, targetRoot, manifest),
|
|
135
|
+
entries: memberEntries(host, resolver, targetRoot),
|
|
136
|
+
memberDirs: workspace.members.map((member) => member.dir),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The errors one configuration answers for: none in a file it holds only for its references. */
|
|
141
|
+
export function ownErrors(
|
|
142
|
+
errors: readonly LocatedDiagnostic[],
|
|
143
|
+
referenceOnly: (file: string) => boolean,
|
|
144
|
+
): readonly LocatedDiagnostic[] {
|
|
145
|
+
return errors.filter(
|
|
146
|
+
({ diagnostic }) => diagnostic.fileName === undefined || !referenceOnly(diagnostic.fileName),
|
|
147
|
+
);
|
|
148
|
+
}
|