@untangle-code/file-graph 0.0.0-stage → 0.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.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +284 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +75 -0
- package/dist/assignment.js.map +1 -0
- package/dist/boundaries/endpoints.d.ts +11 -0
- package/dist/boundaries/endpoints.d.ts.map +1 -0
- package/dist/boundaries/endpoints.js +195 -0
- package/dist/boundaries/endpoints.js.map +1 -0
- package/dist/boundaries/index.d.ts +9 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +93 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/requests.d.ts +12 -0
- package/dist/boundaries/requests.d.ts.map +1 -0
- package/dist/boundaries/requests.js +311 -0
- package/dist/boundaries/requests.js.map +1 -0
- package/dist/edges.d.ts +49 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +223 -0
- package/dist/edges.js.map +1 -0
- package/dist/file-index.d.ts +37 -0
- package/dist/file-index.d.ts.map +1 -0
- package/dist/file-index.js +103 -0
- package/dist/file-index.js.map +1 -0
- package/dist/graph.d.ts +160 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +448 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +67 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +272 -0
- package/dist/metrics.js.map +1 -0
- package/dist/package-catalog.d.ts +43 -0
- package/dist/package-catalog.d.ts.map +1 -0
- package/dist/package-catalog.js +136 -0
- package/dist/package-catalog.js.map +1 -0
- package/dist/paths.d.ts +56 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +129 -0
- package/dist/paths.js.map +1 -0
- package/dist/persistence.d.ts +83 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +538 -0
- package/dist/persistence.js.map +1 -0
- package/dist/resolve/candidates.d.ts +52 -0
- package/dist/resolve/candidates.d.ts.map +1 -0
- package/dist/resolve/candidates.js +202 -0
- package/dist/resolve/candidates.js.map +1 -0
- package/dist/resolve/package-exports.d.ts +31 -0
- package/dist/resolve/package-exports.d.ts.map +1 -0
- package/dist/resolve/package-exports.js +229 -0
- package/dist/resolve/package-exports.js.map +1 -0
- package/dist/resolve/pipeline.d.ts +11 -0
- package/dist/resolve/pipeline.d.ts.map +1 -0
- package/dist/resolve/pipeline.js +109 -0
- package/dist/resolve/pipeline.js.map +1 -0
- package/dist/resolve/resolvers.d.ts +25 -0
- package/dist/resolve/resolvers.d.ts.map +1 -0
- package/dist/resolve/resolvers.js +367 -0
- package/dist/resolve/resolvers.js.map +1 -0
- package/dist/restore.d.ts +75 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/restore.js +136 -0
- package/dist/restore.js.map +1 -0
- package/dist/topology/compose.d.ts +27 -0
- package/dist/topology/compose.d.ts.map +1 -0
- package/dist/topology/compose.js +208 -0
- package/dist/topology/compose.js.map +1 -0
- package/dist/topology/jsonc.d.ts +38 -0
- package/dist/topology/jsonc.d.ts.map +1 -0
- package/dist/topology/jsonc.js +137 -0
- package/dist/topology/jsonc.js.map +1 -0
- package/dist/topology/load.d.ts +61 -0
- package/dist/topology/load.d.ts.map +1 -0
- package/dist/topology/load.js +436 -0
- package/dist/topology/load.js.map +1 -0
- package/dist/topology/manifest.d.ts +48 -0
- package/dist/topology/manifest.d.ts.map +1 -0
- package/dist/topology/manifest.js +109 -0
- package/dist/topology/manifest.js.map +1 -0
- package/dist/topology/project-config.d.ts +39 -0
- package/dist/topology/project-config.d.ts.map +1 -0
- package/dist/topology/project-config.js +150 -0
- package/dist/topology/project-config.js.map +1 -0
- package/dist/topology/workspace-definition.d.ts +26 -0
- package/dist/topology/workspace-definition.d.ts.map +1 -0
- package/dist/topology/workspace-definition.js +104 -0
- package/dist/topology/workspace-definition.js.map +1 -0
- package/dist/traverse.d.ts +49 -0
- package/dist/traverse.d.ts.map +1 -0
- package/dist/traverse.js +167 -0
- package/dist/traverse.js.map +1 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -3
- package/src/assignment.ts +83 -0
- package/src/boundaries/endpoints.ts +273 -0
- package/src/boundaries/index.ts +118 -0
- package/src/boundaries/requests.ts +411 -0
- package/src/edges.ts +334 -0
- package/src/file-index.ts +127 -0
- package/src/graph.ts +591 -0
- package/src/index.ts +58 -0
- package/src/metrics.ts +350 -0
- package/src/package-catalog.ts +145 -0
- package/src/paths.ts +138 -0
- package/src/persistence.ts +746 -0
- package/src/resolve/candidates.ts +230 -0
- package/src/resolve/package-exports.ts +268 -0
- package/src/resolve/pipeline.ts +140 -0
- package/src/resolve/resolvers.ts +468 -0
- package/src/restore.ts +201 -0
- package/src/topology/compose.ts +268 -0
- package/src/topology/jsonc.ts +147 -0
- package/src/topology/load.ts +644 -0
- package/src/topology/manifest.ts +132 -0
- package/src/topology/project-config.ts +183 -0
- package/src/topology/workspace-definition.ts +115 -0
- package/src/traverse.ts +232 -0
- package/src/types.ts +372 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kandidatenbildung: aus einem Pfad ohne Erweiterung die Liste der Dateien,
|
|
3
|
+
* die gemeint sein koennen — in fester Reihenfolge.
|
|
4
|
+
*
|
|
5
|
+
* DREI DINGE, DIE HIER PASSIEREN und ohne die jeder zweite Import dieses
|
|
6
|
+
* Repositories „unresolved" waere:
|
|
7
|
+
*
|
|
8
|
+
* 1. ERWEITERUNGSPROBE. `"./util"` kann `util.ts`, `util.tsx`, `util.js`,
|
|
9
|
+
* … oder `util/index.ts` meinen.
|
|
10
|
+
*
|
|
11
|
+
* 2. RUECKABBILDUNG DER ESM-ENDUNG. TypeScript unter `NodeNext` schreibt
|
|
12
|
+
* `import "./registry.js"` und meint `registry.ts`. Wer nur exakt
|
|
13
|
+
* vergleicht, findet in einem modernen TypeScript-Monorepo praktisch
|
|
14
|
+
* keinen einzigen relativen Import.
|
|
15
|
+
*
|
|
16
|
+
* 3. RUECKABBILDUNG DER BUILD-AUSGABE. Eine `exports`-Map zeigt auf
|
|
17
|
+
* `dist/index.js`. Im Quellindex gibt es nur `src/index.ts`. Mit
|
|
18
|
+
* `rootDir`/`outDir` aus der Projektkonfiguration wird daraus wieder eine
|
|
19
|
+
* Quelldatei — und der Workspace-Import zeigt auf echten Quelltext statt
|
|
20
|
+
* auf ein Artefakt, das gar nicht indiziert ist.
|
|
21
|
+
*
|
|
22
|
+
* Jede Probe, die NICHT getroffen hat, wird mitgefuehrt und landet als
|
|
23
|
+
* `rejectedCandidates` an der unaufgeloesten Kante (E4): wer die Aufloesung
|
|
24
|
+
* spaeter debuggt, sieht, wonach gesucht wurde.
|
|
25
|
+
*/
|
|
26
|
+
import { basenameOf, extensionOf, joinRelative, normalizePath, relativeWithin } from "../paths.js";
|
|
27
|
+
import type { ResolveContextV1, WorkspacePackageV1 } from "../types.js";
|
|
28
|
+
|
|
29
|
+
/** Erweiterungen in Probierreihenfolge. Quelltext vor Build-Ausgabe. */
|
|
30
|
+
export const DEFAULT_EXTENSION_ORDER = [
|
|
31
|
+
".ts",
|
|
32
|
+
".tsx",
|
|
33
|
+
".mts",
|
|
34
|
+
".cts",
|
|
35
|
+
".d.ts",
|
|
36
|
+
".js",
|
|
37
|
+
".jsx",
|
|
38
|
+
".mjs",
|
|
39
|
+
".cjs",
|
|
40
|
+
".json",
|
|
41
|
+
".py",
|
|
42
|
+
".vue",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Endung, wie sie im Spezifizierer steht → Endungen, die tatsaechlich gemeint
|
|
47
|
+
* sein koennen. Die geschriebene Endung steht bewusst mit in der Liste, denn
|
|
48
|
+
* es gibt Projekte, in denen `./bundle.js` wirklich `bundle.js` meint.
|
|
49
|
+
*
|
|
50
|
+
* WELCHE ZUERST PROBIERT WIRD, haengt an der Sprache der QUELLDATEI
|
|
51
|
+
* (`preferSourceExtension`): in einer TypeScript-Datei meint `./util.js`
|
|
52
|
+
* unter `NodeNext` die Datei `util.ts`, also gewinnt dort die Quellendung.
|
|
53
|
+
* In einer JavaScript-Datei ist `./util.js` woertlich gemeint, also gewinnt
|
|
54
|
+
* dort die geschriebene Endung. Liegen `util.ts` und `util.js` nebeneinander,
|
|
55
|
+
* ist das der einzige Fall, in dem die Reihenfolge ueberhaupt zaehlt — und
|
|
56
|
+
* dann ist sie so, wie der jeweilige Uebersetzer sie auch waehlt.
|
|
57
|
+
*/
|
|
58
|
+
export const ESM_EXTENSION_REMAP: Readonly<Record<string, readonly string[]>> = {
|
|
59
|
+
".js": [".ts", ".tsx", ".d.ts", ".js"],
|
|
60
|
+
".jsx": [".tsx", ".jsx"],
|
|
61
|
+
".mjs": [".mts", ".d.mts", ".mjs"],
|
|
62
|
+
".cjs": [".cts", ".d.cts", ".cjs"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Erweiterungen, die als „nicht ausfuehrbarer Inhalt" gelten (spec.md §19/§20). */
|
|
66
|
+
export const ASSET_EXTENSIONS = new Set([
|
|
67
|
+
".avif",
|
|
68
|
+
".css",
|
|
69
|
+
".gif",
|
|
70
|
+
".graphql",
|
|
71
|
+
".ico",
|
|
72
|
+
".jpeg",
|
|
73
|
+
".jpg",
|
|
74
|
+
".less",
|
|
75
|
+
".md",
|
|
76
|
+
".mdx",
|
|
77
|
+
".png",
|
|
78
|
+
".sass",
|
|
79
|
+
".scss",
|
|
80
|
+
".svg",
|
|
81
|
+
".txt",
|
|
82
|
+
".webp",
|
|
83
|
+
".woff",
|
|
84
|
+
".woff2",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** Ergebnis einer Kandidatenprobe. */
|
|
88
|
+
export interface CandidateProbeV1 {
|
|
89
|
+
/** Gefundener repo-relativer Pfad; `null`, wenn keine Probe traf. */
|
|
90
|
+
path: string | null;
|
|
91
|
+
/** Alle geprueften Pfade in Probierreihenfolge — auch der Treffer. */
|
|
92
|
+
tried: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bildet alle Kandidatenpfade zu einem Basispfad, ohne den Index zu befragen.
|
|
97
|
+
* Reihenfolge ist Teil des Vertrags: sie entscheidet, welche Datei gewinnt,
|
|
98
|
+
* wenn mehrere existieren.
|
|
99
|
+
*/
|
|
100
|
+
export function candidatePaths(
|
|
101
|
+
basePath: string,
|
|
102
|
+
extensionOrder: readonly string[] = DEFAULT_EXTENSION_ORDER,
|
|
103
|
+
options: { preferSourceExtension?: boolean } = {},
|
|
104
|
+
): string[] {
|
|
105
|
+
const preferSourceExtension = options.preferSourceExtension ?? true;
|
|
106
|
+
const normalized = normalizePath(basePath);
|
|
107
|
+
const candidates: string[] = [];
|
|
108
|
+
const push = (candidate: string): void => {
|
|
109
|
+
if (!candidates.includes(candidate)) {
|
|
110
|
+
candidates.push(candidate);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const written = extensionOf(normalized);
|
|
115
|
+
if (written !== "") {
|
|
116
|
+
const remap = ESM_EXTENSION_REMAP[written];
|
|
117
|
+
if (remap === undefined || !preferSourceExtension) {
|
|
118
|
+
push(normalized);
|
|
119
|
+
}
|
|
120
|
+
if (remap !== undefined) {
|
|
121
|
+
const stem = normalized.slice(0, normalized.length - written.length);
|
|
122
|
+
for (const extension of remap) {
|
|
123
|
+
push(`${stem}${extension}`);
|
|
124
|
+
}
|
|
125
|
+
push(normalized);
|
|
126
|
+
}
|
|
127
|
+
// Ein Punkt im Namen ist NICHT immer eine Erweiterung: `./vitest.config`
|
|
128
|
+
// meint `vitest.config.ts`. Deshalb wird nur dann noch eine Erweiterung
|
|
129
|
+
// angehaengt, wenn der geschriebene Suffix keine bekannte ist — sonst
|
|
130
|
+
// entstuenden Unsinnskandidaten wie `context.js.cts`, die die
|
|
131
|
+
// `rejectedCandidates` unlesbar machen.
|
|
132
|
+
if (!isKnownExtension(written, extensionOrder)) {
|
|
133
|
+
for (const extension of extensionOrder) {
|
|
134
|
+
push(`${normalized}${extension}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Auch ein Pfad mit Punkt kann ein Verzeichnis sein.
|
|
138
|
+
for (const extension of extensionOrder) {
|
|
139
|
+
push(`${normalized}/index${extension}`);
|
|
140
|
+
}
|
|
141
|
+
return candidates;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
push(normalized);
|
|
145
|
+
for (const extension of extensionOrder) {
|
|
146
|
+
push(`${normalized}${extension}`);
|
|
147
|
+
}
|
|
148
|
+
for (const extension of extensionOrder) {
|
|
149
|
+
push(`${normalized}/index${extension}`);
|
|
150
|
+
}
|
|
151
|
+
// Python: ein Paketverzeichnis wird ueber seine Initialisierungsdatei erreicht.
|
|
152
|
+
push(`${normalized}/__init__.py`);
|
|
153
|
+
return candidates;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Bildet einen Pfad in der Build-Ausgabe eines Pakets auf den entsprechenden
|
|
158
|
+
* Quellpfad ab. `null`, wenn der Pfad nicht in einer bekannten Build-Ausgabe
|
|
159
|
+
* liegt oder das Paket keine Abbildung deklariert.
|
|
160
|
+
*/
|
|
161
|
+
export function reverseBuildOutput(
|
|
162
|
+
packages: Iterable<WorkspacePackageV1>,
|
|
163
|
+
repoId: string,
|
|
164
|
+
path: string,
|
|
165
|
+
): string | null {
|
|
166
|
+
const normalized = normalizePath(path);
|
|
167
|
+
for (const entry of packages) {
|
|
168
|
+
if (entry.repoId !== repoId) continue;
|
|
169
|
+
if (entry.outDir === undefined || entry.rootDir === undefined) continue;
|
|
170
|
+
const withinOutput = relativeWithin(entry.outDir, normalized);
|
|
171
|
+
if (withinOutput === null || withinOutput === "") continue;
|
|
172
|
+
const mapped = joinRelative(entry.rootDir, withinOutput);
|
|
173
|
+
if (mapped !== null) {
|
|
174
|
+
return mapped;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sucht die erste existierende Datei zu einem Basispfad.
|
|
182
|
+
*
|
|
183
|
+
* Zuerst die direkten Kandidaten, danach — und nur, wenn keiner traf — die
|
|
184
|
+
* ueber `rootDir`/`outDir` zurueckgebildeten. Diese Reihenfolge ist wichtig:
|
|
185
|
+
* existiert die Build-Ausgabe wirklich im Index (weil jemand `dist/` mit
|
|
186
|
+
* indiziert hat), gewinnt sie, und wir erfinden keine Quelldatei.
|
|
187
|
+
*/
|
|
188
|
+
export function probeCandidates(
|
|
189
|
+
context: ResolveContextV1,
|
|
190
|
+
repoId: string,
|
|
191
|
+
basePath: string,
|
|
192
|
+
): CandidateProbeV1 {
|
|
193
|
+
const tried: string[] = [];
|
|
194
|
+
// In einer JavaScript-Quelle ist die geschriebene Endung woertlich gemeint;
|
|
195
|
+
// in einer TypeScript-Quelle ist sie die Endung der BUILD-Ausgabe.
|
|
196
|
+
const probeOptions = { preferSourceExtension: context.fromLanguageId !== "javascript" };
|
|
197
|
+
const direct = candidatePaths(basePath, context.extensionOrder, probeOptions);
|
|
198
|
+
for (const candidate of direct) {
|
|
199
|
+
tried.push(candidate);
|
|
200
|
+
if (context.files.hasFile(repoId, candidate)) {
|
|
201
|
+
return { path: candidate, tried };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const mapped = reverseBuildOutput(context.packageCatalog.all(), repoId, basePath);
|
|
205
|
+
if (mapped === null) {
|
|
206
|
+
return { path: null, tried };
|
|
207
|
+
}
|
|
208
|
+
for (const candidate of candidatePaths(mapped, context.extensionOrder, probeOptions)) {
|
|
209
|
+
if (tried.includes(candidate)) continue;
|
|
210
|
+
tried.push(candidate);
|
|
211
|
+
if (context.files.hasFile(repoId, candidate)) {
|
|
212
|
+
return { path: candidate, tried };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { path: null, tried };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isKnownExtension(extension: string, extensionOrder: readonly string[]): boolean {
|
|
219
|
+
return (
|
|
220
|
+
extensionOrder.includes(extension) ||
|
|
221
|
+
extension in ESM_EXTENSION_REMAP ||
|
|
222
|
+
ASSET_EXTENSIONS.has(extension)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** True, wenn der Spezifizierer auf eine Nicht-Code-Ressource zeigt. */
|
|
227
|
+
export function isAssetSpecifier(specifier: string): boolean {
|
|
228
|
+
const withoutQuery = specifier.split("?")[0] ?? specifier;
|
|
229
|
+
return ASSET_EXTENSIONS.has(extensionOf(basenameOf(withoutQuery)));
|
|
230
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auswertung der `exports`- und `imports`-Karten eines Paketmanifests
|
|
3
|
+
* (spec.md §14.3 „Node Package Exports").
|
|
4
|
+
*
|
|
5
|
+
* Was hier abgedeckt ist: die String-Kurzform, die Bedingungsobjekte
|
|
6
|
+
* (`types`/`node`/`import`/`require`/`default`), die Subpfadkarte, die
|
|
7
|
+
* Musterform mit `*`, Arrays als Fallbackliste und `null` als bewusste
|
|
8
|
+
* Sperre eines Subpfads.
|
|
9
|
+
*
|
|
10
|
+
* BEDINGUNGSREIHENFOLGE: Laufzeitbedingungen kommen VOR `types`. Grund: ein
|
|
11
|
+
* `types`-Treffer zeigt auf eine Deklarationsdatei (`.d.ts`), und die ist im
|
|
12
|
+
* Quellindex eines Monorepos praktisch nie vorhanden — die Kante waere
|
|
13
|
+
* unaufgeloest, obwohl das Paket eindeutig bestimmt ist. Ueber die
|
|
14
|
+
* Laufzeitbedingung landet man auf der Build-Ausgabe, und die
|
|
15
|
+
* `rootDir`/`outDir`-Rueckabbildung fuehrt von dort zur Quelldatei.
|
|
16
|
+
*
|
|
17
|
+
* `null` als Ziel ist KEIN „nicht gefunden": es ist die ausdrueckliche
|
|
18
|
+
* Aussage des Paketautors, dass dieser Subpfad nicht importiert werden darf.
|
|
19
|
+
* Das wird als eigener Grund gemeldet und nicht mit einem Fehlschlag
|
|
20
|
+
* verwechselt (E4).
|
|
21
|
+
*/
|
|
22
|
+
import { joinRelative, normalizePath } from "../paths.js";
|
|
23
|
+
import type { WorkspacePackageV1 } from "../types.js";
|
|
24
|
+
|
|
25
|
+
/** Bedingungen in Auswertungsreihenfolge. */
|
|
26
|
+
export const DEFAULT_EXPORT_CONDITIONS = ["node", "import", "require", "default", "types"] as const;
|
|
27
|
+
|
|
28
|
+
/** Ergebnis der Subpfadauswertung. */
|
|
29
|
+
export type SubpathResolution =
|
|
30
|
+
| { status: "target"; target: string; via: "exports" | "imports" | "manifest-field" | "direct" }
|
|
31
|
+
| { status: "blocked"; detail: string }
|
|
32
|
+
| { status: "no_match"; detail: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Loest einen Subpfad eines Pakets auf einen repo-relativen Basispfad auf.
|
|
36
|
+
*
|
|
37
|
+
* `subpath` ist `"."` fuer den Paketwurzel-Import oder `"./unter/pfad"`.
|
|
38
|
+
* Das Ergebnis ist ein Pfad OHNE Existenzpruefung — die uebernimmt
|
|
39
|
+
* `probeCandidates`.
|
|
40
|
+
*/
|
|
41
|
+
export function resolvePackageSubpath(
|
|
42
|
+
entry: WorkspacePackageV1,
|
|
43
|
+
subpath: string,
|
|
44
|
+
conditions: readonly string[] = DEFAULT_EXPORT_CONDITIONS,
|
|
45
|
+
): SubpathResolution {
|
|
46
|
+
const normalizedSubpath = subpath === "" ? "." : subpath;
|
|
47
|
+
|
|
48
|
+
if (entry.exports !== undefined) {
|
|
49
|
+
const matched = matchExportsMap(entry.exports, normalizedSubpath, conditions);
|
|
50
|
+
if (matched.status === "target") {
|
|
51
|
+
const joined = joinRelative(entry.packageDir, matched.target);
|
|
52
|
+
if (joined === null) {
|
|
53
|
+
return {
|
|
54
|
+
status: "no_match",
|
|
55
|
+
detail: `exports target "${matched.target}" escapes the package directory "${entry.packageDir}"`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { status: "target", target: joined, via: "exports" };
|
|
59
|
+
}
|
|
60
|
+
if (matched.status === "blocked") {
|
|
61
|
+
return matched;
|
|
62
|
+
}
|
|
63
|
+
// Kein Subpfadtreffer in einer vorhandenen exports-Karte: das Paket
|
|
64
|
+
// kapselt seinen Inhalt, der Import ist ungueltig. Kein stiller Rueckfall
|
|
65
|
+
// auf den Verzeichnispfad — das waere geraten.
|
|
66
|
+
return {
|
|
67
|
+
status: "no_match",
|
|
68
|
+
detail: `package "${entry.name}" declares "exports" but no entry matches subpath "${normalizedSubpath}"`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (normalizedSubpath === ".") {
|
|
73
|
+
for (const field of ["module", "main", "types"] as const) {
|
|
74
|
+
const value = entry[field];
|
|
75
|
+
if (value === undefined) continue;
|
|
76
|
+
const joined = joinRelative(entry.packageDir, value);
|
|
77
|
+
if (joined !== null) {
|
|
78
|
+
return { status: "target", target: joined, via: "manifest-field" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const indexPath = joinRelative(entry.packageDir, "index");
|
|
82
|
+
if (indexPath === null) {
|
|
83
|
+
return {
|
|
84
|
+
status: "no_match",
|
|
85
|
+
detail: `package directory "${entry.packageDir}" is not usable`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { status: "target", target: indexPath, via: "direct" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const joined = joinRelative(entry.packageDir, normalizedSubpath);
|
|
92
|
+
if (joined === null) {
|
|
93
|
+
return {
|
|
94
|
+
status: "no_match",
|
|
95
|
+
detail: `subpath "${normalizedSubpath}" escapes the package directory "${entry.packageDir}"`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { status: "target", target: joined, via: "direct" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Loest einen `#`-Subpath-Import gegen die `imports`-Karte eines Pakets auf. */
|
|
102
|
+
export function resolvePackageImports(
|
|
103
|
+
entry: WorkspacePackageV1,
|
|
104
|
+
specifier: string,
|
|
105
|
+
conditions: readonly string[] = DEFAULT_EXPORT_CONDITIONS,
|
|
106
|
+
): SubpathResolution {
|
|
107
|
+
if (entry.imports === undefined) {
|
|
108
|
+
return {
|
|
109
|
+
status: "no_match",
|
|
110
|
+
detail: `package "${entry.name}" declares no "imports" map for "${specifier}"`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const matched = matchExportsMap(entry.imports, specifier, conditions);
|
|
114
|
+
if (matched.status !== "target") {
|
|
115
|
+
return matched;
|
|
116
|
+
}
|
|
117
|
+
const joined = joinRelative(entry.packageDir, matched.target);
|
|
118
|
+
if (joined === null) {
|
|
119
|
+
return {
|
|
120
|
+
status: "no_match",
|
|
121
|
+
detail: `imports target "${matched.target}" escapes the package directory "${entry.packageDir}"`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { status: "target", target: joined, via: "imports" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type MapMatch =
|
|
128
|
+
| { status: "target"; target: string }
|
|
129
|
+
| { status: "blocked"; detail: string }
|
|
130
|
+
| { status: "no_match"; detail: string };
|
|
131
|
+
|
|
132
|
+
function matchExportsMap(map: unknown, key: string, conditions: readonly string[]): MapMatch {
|
|
133
|
+
// Kurzform: `"exports": "./dist/index.js"` gilt nur fuer ".".
|
|
134
|
+
if (typeof map === "string") {
|
|
135
|
+
return key === "."
|
|
136
|
+
? { status: "target", target: map }
|
|
137
|
+
: { status: "no_match", detail: `string "exports" only covers subpath "."` };
|
|
138
|
+
}
|
|
139
|
+
if (map === null) {
|
|
140
|
+
return { status: "blocked", detail: `"exports" is null — the package blocks all subpaths` };
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(map)) {
|
|
143
|
+
return selectFromList(map, key, conditions);
|
|
144
|
+
}
|
|
145
|
+
if (typeof map !== "object") {
|
|
146
|
+
return { status: "no_match", detail: `"exports" is neither string, array nor object` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const record = map as Record<string, unknown>;
|
|
150
|
+
const keys = Object.keys(record);
|
|
151
|
+
const isSubpathMap = keys.some(
|
|
152
|
+
(entry) => entry === "." || entry.startsWith("./") || entry.startsWith("#"),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!isSubpathMap) {
|
|
156
|
+
// Reines Bedingungsobjekt: gilt fuer ".".
|
|
157
|
+
if (key !== ".") {
|
|
158
|
+
return { status: "no_match", detail: `condition-only "exports" only covers subpath "."` };
|
|
159
|
+
}
|
|
160
|
+
return selectCondition(record, conditions);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exact = record[key];
|
|
164
|
+
if (exact !== undefined) {
|
|
165
|
+
return resolveConditionValue(exact, conditions, key);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Musterform: "./features/*": "./dist/features/*.js"
|
|
169
|
+
let best: { pattern: string; value: unknown; captured: string } | null = null;
|
|
170
|
+
for (const pattern of keys) {
|
|
171
|
+
const starIndex = pattern.indexOf("*");
|
|
172
|
+
if (starIndex === -1) continue;
|
|
173
|
+
const prefix = pattern.slice(0, starIndex);
|
|
174
|
+
const suffix = pattern.slice(starIndex + 1);
|
|
175
|
+
if (!key.startsWith(prefix) || !key.endsWith(suffix)) continue;
|
|
176
|
+
if (key.length < prefix.length + suffix.length) continue;
|
|
177
|
+
const captured = key.slice(prefix.length, key.length - suffix.length);
|
|
178
|
+
// Node waehlt bei mehreren Mustern das mit dem laengsten Praefix.
|
|
179
|
+
if (best === null || prefix.length > best.pattern.indexOf("*")) {
|
|
180
|
+
best = { pattern, value: record[pattern], captured };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (best === null) {
|
|
184
|
+
return { status: "no_match", detail: `no "exports" entry matches "${key}"` };
|
|
185
|
+
}
|
|
186
|
+
const resolved = resolveConditionValue(best.value, conditions, key);
|
|
187
|
+
if (resolved.status !== "target") {
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
return { status: "target", target: resolved.target.split("*").join(best.captured) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveConditionValue(
|
|
194
|
+
value: unknown,
|
|
195
|
+
conditions: readonly string[],
|
|
196
|
+
key: string,
|
|
197
|
+
): MapMatch {
|
|
198
|
+
if (value === null) {
|
|
199
|
+
return {
|
|
200
|
+
status: "blocked",
|
|
201
|
+
detail: `"exports" entry for "${key}" is null — import is forbidden`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
return { status: "target", target: value };
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return selectFromList(value, key, conditions);
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "object") {
|
|
211
|
+
return selectCondition(value as Record<string, unknown>, conditions);
|
|
212
|
+
}
|
|
213
|
+
return { status: "no_match", detail: `"exports" entry for "${key}" has an unusable type` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function selectFromList(
|
|
217
|
+
list: readonly unknown[],
|
|
218
|
+
key: string,
|
|
219
|
+
conditions: readonly string[],
|
|
220
|
+
): MapMatch {
|
|
221
|
+
const details: string[] = [];
|
|
222
|
+
for (const candidate of list) {
|
|
223
|
+
const resolved = resolveConditionValue(candidate, conditions, key);
|
|
224
|
+
if (resolved.status === "target") {
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
details.push(resolved.detail);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
status: "no_match",
|
|
231
|
+
detail: `no usable entry in "exports" array for "${key}" (${details.join("; ")})`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function selectCondition(record: Record<string, unknown>, conditions: readonly string[]): MapMatch {
|
|
236
|
+
for (const condition of conditions) {
|
|
237
|
+
if (!(condition in record)) continue;
|
|
238
|
+
const value = record[condition];
|
|
239
|
+
const resolved = resolveConditionValue(value, conditions, condition);
|
|
240
|
+
if (resolved.status === "target" || resolved.status === "blocked") {
|
|
241
|
+
return resolved;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
status: "no_match",
|
|
246
|
+
detail: `none of the conditions [${conditions.join(", ")}] is present in [${Object.keys(record).join(", ")}]`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Zerlegt einen Paketspezifizierer in Paketname und Subpfad. */
|
|
251
|
+
export function splitPackageSpecifier(specifier: string): { name: string; subpath: string } | null {
|
|
252
|
+
const normalized = normalizePath(specifier);
|
|
253
|
+
if (normalized === "") {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const segments = normalized.split("/");
|
|
257
|
+
if (normalized.startsWith("@")) {
|
|
258
|
+
if (segments.length < 2 || segments[1] === undefined || segments[1] === "") {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const name = `${segments[0] as string}/${segments[1]}`;
|
|
262
|
+
const rest = segments.slice(2).join("/");
|
|
263
|
+
return { name, subpath: rest === "" ? "." : `./${rest}` };
|
|
264
|
+
}
|
|
265
|
+
const name = segments[0] as string;
|
|
266
|
+
const rest = segments.slice(1).join("/");
|
|
267
|
+
return { name, subpath: rest === "" ? "." : `./${rest}` };
|
|
268
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Die Resolver-Pipeline (architecture.md §16.2).
|
|
3
|
+
*
|
|
4
|
+
* Reihenfolge wie dort festgelegt: relativer Pfad → Sprachaliase →
|
|
5
|
+
* Projektaliase → Workspace-Paketkarte → installierte Paketmetadaten →
|
|
6
|
+
* Klassifikation als extern/unaufgeloest.
|
|
7
|
+
*
|
|
8
|
+
* DER WICHTIGE TEIL IST DAS SCHEITERN. Ein Resolver, der zustaendig war und
|
|
9
|
+
* NICHT getroffen hat, beendet die Suche nicht: der naechste zustaendige darf
|
|
10
|
+
* es versuchen. Erst wenn keiner mehr uebrig ist, entsteht eine unaufgeloeste
|
|
11
|
+
* Kante — und die traegt dann den Grund des ERSTEN zustaendigen Resolvers,
|
|
12
|
+
* ergaenzt um die Namen aller weiteren Versuche und die Vereinigung aller
|
|
13
|
+
* geprueften Kandidaten. Ein pauschales „konnte nicht aufgeloest werden" ohne
|
|
14
|
+
* diese Spur waere genau die stille Ausblendung, die E4 verbietet.
|
|
15
|
+
*/
|
|
16
|
+
import { compareCodepoints } from "@untangle-code/domain";
|
|
17
|
+
|
|
18
|
+
import type { ImportResolverV1, ResolveContextV1, ResolveResultV1 } from "../types.js";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
configuredAliasResolver,
|
|
22
|
+
installedPackageResolver,
|
|
23
|
+
projectAliasResolver,
|
|
24
|
+
pythonModuleResolver,
|
|
25
|
+
relativePathResolver,
|
|
26
|
+
runtimeBuiltinResolver,
|
|
27
|
+
subpathImportResolver,
|
|
28
|
+
workspacePackageResolver,
|
|
29
|
+
} from "./resolvers.js";
|
|
30
|
+
|
|
31
|
+
/** Die Standardpipeline in der Reihenfolge von architecture.md §16.2. */
|
|
32
|
+
export const DEFAULT_RESOLVER_PIPELINE: readonly ImportResolverV1[] = [
|
|
33
|
+
relativePathResolver,
|
|
34
|
+
runtimeBuiltinResolver,
|
|
35
|
+
subpathImportResolver,
|
|
36
|
+
pythonModuleResolver,
|
|
37
|
+
projectAliasResolver,
|
|
38
|
+
configuredAliasResolver,
|
|
39
|
+
workspacePackageResolver,
|
|
40
|
+
installedPackageResolver,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Ergebnis eines Pipelinelaufs samt der Spur der befragten Resolver. */
|
|
44
|
+
export interface PipelineOutcomeV1 {
|
|
45
|
+
result: ResolveResultV1;
|
|
46
|
+
/** Namen aller Resolver, die sich zustaendig erklaert haben, in Reihenfolge. */
|
|
47
|
+
attemptedResolvers: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveImport(
|
|
51
|
+
specifier: string,
|
|
52
|
+
context: ResolveContextV1,
|
|
53
|
+
pipeline: readonly ImportResolverV1[] = DEFAULT_RESOLVER_PIPELINE,
|
|
54
|
+
): PipelineOutcomeV1 {
|
|
55
|
+
const attempted: string[] = [];
|
|
56
|
+
const failures: {
|
|
57
|
+
resolver: string;
|
|
58
|
+
result: Extract<ResolveResultV1, { status: "unresolved" }>;
|
|
59
|
+
}[] = [];
|
|
60
|
+
|
|
61
|
+
for (const resolver of pipeline) {
|
|
62
|
+
let supports: boolean;
|
|
63
|
+
try {
|
|
64
|
+
supports = resolver.supports(specifier, context);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Ein werfender Resolver darf die Kette nicht abbrechen — der Fehler
|
|
67
|
+
// wird zum benannten Fehlschlag, nicht zum verschluckten Ausnahmefall.
|
|
68
|
+
attempted.push(resolver.name);
|
|
69
|
+
failures.push({
|
|
70
|
+
resolver: resolver.name,
|
|
71
|
+
result: {
|
|
72
|
+
status: "unresolved",
|
|
73
|
+
reason: "no_resolver_supports_specifier",
|
|
74
|
+
detail: `resolver "${resolver.name}" threw while deciding support: ${messageOf(error)}`,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!supports) continue;
|
|
80
|
+
attempted.push(resolver.name);
|
|
81
|
+
|
|
82
|
+
let outcome: ResolveResultV1 | null;
|
|
83
|
+
try {
|
|
84
|
+
outcome = resolver.resolve(specifier, context);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
failures.push({
|
|
87
|
+
resolver: resolver.name,
|
|
88
|
+
result: {
|
|
89
|
+
status: "unresolved",
|
|
90
|
+
reason: "target_file_not_found",
|
|
91
|
+
detail: `resolver "${resolver.name}" threw: ${messageOf(error)}`,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (outcome === null) continue;
|
|
97
|
+
if (outcome.status !== "unresolved") {
|
|
98
|
+
return { result: outcome, attemptedResolvers: attempted };
|
|
99
|
+
}
|
|
100
|
+
failures.push({ resolver: resolver.name, result: outcome });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (failures.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
result: {
|
|
106
|
+
status: "unresolved",
|
|
107
|
+
reason: "no_resolver_supports_specifier",
|
|
108
|
+
detail: `no resolver in [${pipeline.map((entry) => entry.name).join(", ")}] claims specifier "${specifier}" from "${context.fromPath}"`,
|
|
109
|
+
},
|
|
110
|
+
attemptedResolvers: attempted,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const first = failures[0] as {
|
|
115
|
+
resolver: string;
|
|
116
|
+
result: Extract<ResolveResultV1, { status: "unresolved" }>;
|
|
117
|
+
};
|
|
118
|
+
const rejected = new Set<string>();
|
|
119
|
+
for (const failure of failures) {
|
|
120
|
+
for (const candidate of failure.result.rejectedCandidates ?? []) {
|
|
121
|
+
rejected.add(candidate);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const trail = failures
|
|
125
|
+
.map((failure) => `${failure.resolver}: ${failure.result.detail}`)
|
|
126
|
+
.join(" | ");
|
|
127
|
+
return {
|
|
128
|
+
result: {
|
|
129
|
+
status: "unresolved",
|
|
130
|
+
reason: first.result.reason,
|
|
131
|
+
detail: trail,
|
|
132
|
+
...(rejected.size > 0 ? { rejectedCandidates: [...rejected].sort(compareCodepoints) } : {}),
|
|
133
|
+
},
|
|
134
|
+
attemptedResolvers: attempted,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function messageOf(error: unknown): string {
|
|
139
|
+
return error instanceof Error ? error.message : String(error);
|
|
140
|
+
}
|