@oa-sdk/spec-bundler 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 +201 -0
- package/README.md +240 -0
- package/dist/archiver.d.ts +22 -0
- package/dist/archiver.js +155 -0
- package/dist/category-parser.d.ts +15 -0
- package/dist/category-parser.js +115 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +760 -0
- package/dist/context-generator.d.ts +14 -0
- package/dist/context-generator.js +297 -0
- package/dist/freshness.d.ts +41 -0
- package/dist/freshness.js +365 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +705 -0
- package/dist/link-rewriter.d.ts +65 -0
- package/dist/link-rewriter.js +777 -0
- package/dist/resolver.d.ts +27 -0
- package/dist/resolver.js +276 -0
- package/dist/schema.d.ts +563 -0
- package/dist/schema.js +670 -0
- package/dist/section-slicer.d.ts +12 -0
- package/dist/section-slicer.js +114 -0
- package/dist/topic.d.ts +62 -0
- package/dist/topic.js +262 -0
- package/dist/tree-shaker.d.ts +40 -0
- package/dist/tree-shaker.js +523 -0
- package/dist/types.d.ts +369 -0
- package/dist/types.js +2 -0
- package/package.json +29 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { maskCodeBlocks, resolveLocalPath } from "./link-rewriter.js";
|
|
4
|
+
import { matchesAnyPattern } from "./resolver.js";
|
|
5
|
+
import { parseDocumentCategory } from "./category-parser.js";
|
|
6
|
+
const TEXT_EXTENSIONS = new Set([
|
|
7
|
+
".md",
|
|
8
|
+
".mdx",
|
|
9
|
+
".tsp",
|
|
10
|
+
".json",
|
|
11
|
+
".yaml",
|
|
12
|
+
".yml",
|
|
13
|
+
".txt",
|
|
14
|
+
".ts",
|
|
15
|
+
".js",
|
|
16
|
+
".html",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Extracts outgoing local file target candidates from text file content.
|
|
20
|
+
*/
|
|
21
|
+
function extractOutboundReferences(content, extension) {
|
|
22
|
+
const references = [];
|
|
23
|
+
if (extension === ".md" || extension === ".mdx") {
|
|
24
|
+
const { masked } = maskCodeBlocks(content);
|
|
25
|
+
// 1. Inline links: [text](target) and  (including nested images in labels)
|
|
26
|
+
// Supports balanced parentheses in destinations and titles in "", '', or ()
|
|
27
|
+
const linkRegex = /(!?\[)((?:!\[[^\]]*\]\([^)]*\)|[^\]])*)\]\((<[^>]+>|(?:\\.|[^()\s]|\((?:\\.|[^()\s])*\))+)([ \t]+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\)/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = linkRegex.exec(masked)) !== null) {
|
|
30
|
+
references.push(match[3]);
|
|
31
|
+
if (match[2].includes("![")) {
|
|
32
|
+
const innerImgRegex = /(!?\[)([^\]]*)\]\((<[^>]+>|(?:\\.|[^()\s]|\((?:\\.|[^()\s])*\))+)([ \t]+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\)/g;
|
|
33
|
+
let innerMatch;
|
|
34
|
+
while ((innerMatch = innerImgRegex.exec(match[2])) !== null) {
|
|
35
|
+
references.push(innerMatch[3]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 2. Reference definitions: [label]: target (supports blockquotes, list markers, and destination on next line)
|
|
40
|
+
const refDefRegex = /^[ \t>]*(?:(?:[-*+]|\d+[.)])[ \t>]+)?\[[^\]]+\]:[ \t]*(?:\r?\n[ \t>]+)?(<[^>]+>|\S+)/gm;
|
|
41
|
+
while ((match = refDefRegex.exec(masked)) !== null) {
|
|
42
|
+
references.push(match[1]);
|
|
43
|
+
}
|
|
44
|
+
// 3. HTML tags: <img src="target">, <a href="target">
|
|
45
|
+
const htmlRegex = /(?:src|href)=["']([^"']+)["']/g;
|
|
46
|
+
while ((match = htmlRegex.exec(masked)) !== null) {
|
|
47
|
+
references.push(match[1]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (extension === ".tsp") {
|
|
51
|
+
// TypeSpec @evidence("target#anchor", "rationale") and imports
|
|
52
|
+
const evidenceRegex = /@evidence\s*\(\s*["']([^"']+)["']/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = evidenceRegex.exec(content)) !== null) {
|
|
55
|
+
references.push(match[1]);
|
|
56
|
+
}
|
|
57
|
+
const importRegex = /import\s+["'](\.[^"']+)["']/g;
|
|
58
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
59
|
+
references.push(match[1]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (extension === ".json") {
|
|
63
|
+
// JSON Schema $ref and $schema references
|
|
64
|
+
const refRegex = /"(?:\$ref|\$schema)"\s*:\s*["']([^"']+)["']/g;
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = refRegex.exec(content)) !== null) {
|
|
67
|
+
references.push(match[1]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return references;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Performs reachability traversal (downstream, upstream, or bidirectional)
|
|
74
|
+
* starting from declarative entrypoints with category filtering and boundary enforcement.
|
|
75
|
+
*/
|
|
76
|
+
export function pruneUnreachableEntries(entries, config, workspaceRoot) {
|
|
77
|
+
const traversalMode = config.traversalMode ??
|
|
78
|
+
(config.preserveTargets === true ? "hybrid" : "reachability");
|
|
79
|
+
if (config.enabled === false || traversalMode === "explicit") {
|
|
80
|
+
return {
|
|
81
|
+
reachableEntries: entries,
|
|
82
|
+
eliminatedEntries: [],
|
|
83
|
+
warnings: [],
|
|
84
|
+
lineage: new Map(),
|
|
85
|
+
boundaryTerminals: [],
|
|
86
|
+
boundaryEdges: new Map(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const warnings = [];
|
|
90
|
+
const entrypointsList = config.entrypoints ?? [];
|
|
91
|
+
if (entrypointsList.length === 0) {
|
|
92
|
+
if (traversalMode === "hybrid" || config.preserveTargets === true) {
|
|
93
|
+
return {
|
|
94
|
+
reachableEntries: entries,
|
|
95
|
+
eliminatedEntries: [],
|
|
96
|
+
warnings,
|
|
97
|
+
lineage: new Map(),
|
|
98
|
+
boundaryTerminals: [],
|
|
99
|
+
boundaryEdges: new Map(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
throw new Error("Tree-shaking failed: none of the specified entrypoints ([]) matched any candidate target.");
|
|
103
|
+
}
|
|
104
|
+
// Build reverse and canonical lookup maps
|
|
105
|
+
const sourceAbsToDest = new Map();
|
|
106
|
+
const sourceRelToDest = new Map();
|
|
107
|
+
const destToEntry = new Map();
|
|
108
|
+
for (const [dest, entry] of entries.entries()) {
|
|
109
|
+
const absNorm = path.resolve(entry.sourceAbsolutePath);
|
|
110
|
+
sourceAbsToDest.set(absNorm, dest);
|
|
111
|
+
sourceRelToDest.set(entry.sourceRelativePath.replace(/\\/g, "/"), dest);
|
|
112
|
+
destToEntry.set(dest, entry);
|
|
113
|
+
}
|
|
114
|
+
// Resolve entrypoints to bundle destinations
|
|
115
|
+
const entrypointDests = [];
|
|
116
|
+
for (const rawEp of entrypointsList) {
|
|
117
|
+
const epClean = rawEp.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
118
|
+
let matchedDest;
|
|
119
|
+
if (destToEntry.has(epClean)) {
|
|
120
|
+
matchedDest = epClean;
|
|
121
|
+
}
|
|
122
|
+
else if (sourceRelToDest.has(epClean)) {
|
|
123
|
+
matchedDest = sourceRelToDest.get(epClean);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const absEp = path.resolve(workspaceRoot, rawEp);
|
|
127
|
+
matchedDest = sourceAbsToDest.get(absEp);
|
|
128
|
+
}
|
|
129
|
+
if (matchedDest) {
|
|
130
|
+
if (!entrypointDests.includes(matchedDest)) {
|
|
131
|
+
entrypointDests.push(matchedDest);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
warnings.push(`Tree-shake entrypoint '${rawEp}' does not match any resolved bundle candidate target.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (entrypointDests.length === 0) {
|
|
139
|
+
if (traversalMode === "hybrid" || config.preserveTargets === true) {
|
|
140
|
+
warnings.push(`Tree-shaking: none of the entrypoints (${JSON.stringify(entrypointsList)}) matched; retaining declared targets.`);
|
|
141
|
+
return {
|
|
142
|
+
reachableEntries: entries,
|
|
143
|
+
eliminatedEntries: [],
|
|
144
|
+
warnings,
|
|
145
|
+
lineage: new Map(),
|
|
146
|
+
boundaryTerminals: [],
|
|
147
|
+
boundaryEdges: new Map(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Tree-shaking failed: none of the specified entrypoints (${JSON.stringify(entrypointsList)}) matched any candidate target.`);
|
|
151
|
+
}
|
|
152
|
+
// Build Bidirectional Reference Graph (outbound & inbound) and parse categories
|
|
153
|
+
const inboundGraph = new Map();
|
|
154
|
+
const nodeCategories = new Map();
|
|
155
|
+
const fileContents = new Map();
|
|
156
|
+
for (const [dest, entry] of entries.entries()) {
|
|
157
|
+
const ext = path.posix.extname(dest).toLowerCase();
|
|
158
|
+
if (!inboundGraph.has(dest))
|
|
159
|
+
inboundGraph.set(dest, []);
|
|
160
|
+
if (!TEXT_EXTENSIONS.has(ext) || !fs.existsSync(entry.sourceAbsolutePath)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let content = "";
|
|
164
|
+
try {
|
|
165
|
+
content = fs.readFileSync(entry.sourceAbsolutePath, "utf8");
|
|
166
|
+
fileContents.set(dest, content);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const cat = parseDocumentCategory(content, entry.sourceAbsolutePath);
|
|
172
|
+
nodeCategories.set(dest, cat);
|
|
173
|
+
const rawRefs = extractOutboundReferences(content, ext);
|
|
174
|
+
for (const rawRefWithAngle of rawRefs) {
|
|
175
|
+
const rawRef = rawRefWithAngle.startsWith("<") && rawRefWithAngle.endsWith(">")
|
|
176
|
+
? rawRefWithAngle.slice(1, -1)
|
|
177
|
+
: rawRefWithAngle;
|
|
178
|
+
const normalizedRef = rawRef.replace(/\\/g, "/");
|
|
179
|
+
if (normalizedRef.startsWith("#"))
|
|
180
|
+
continue;
|
|
181
|
+
if (!normalizedRef.startsWith("file:") && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(normalizedRef)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const splitIndex = normalizedRef.search(/[#?]/);
|
|
185
|
+
const basePath = splitIndex === -1 ? normalizedRef : normalizedRef.slice(0, splitIndex);
|
|
186
|
+
if (!basePath)
|
|
187
|
+
continue;
|
|
188
|
+
const resolvedAbs = resolveLocalPath(basePath, entry.sourceAbsolutePath, workspaceRoot);
|
|
189
|
+
if (!resolvedAbs)
|
|
190
|
+
continue;
|
|
191
|
+
const targetDest = sourceAbsToDest.get(resolvedAbs);
|
|
192
|
+
if (targetDest) {
|
|
193
|
+
if (!inboundGraph.has(targetDest))
|
|
194
|
+
inboundGraph.set(targetDest, []);
|
|
195
|
+
inboundGraph.get(targetDest).push({ source: dest, rawRef });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const direction = config.direction ?? "downstream";
|
|
200
|
+
const upstreamDepth = config.upstreamDepth ?? 1;
|
|
201
|
+
const maxDepth = config.maxDepth ?? Infinity;
|
|
202
|
+
const lineage = new Map();
|
|
203
|
+
const boundaryTerminalsSet = new Set();
|
|
204
|
+
const boundaryEdges = new Map();
|
|
205
|
+
const upstreamNodesSet = new Set();
|
|
206
|
+
const downstreamNodesSet = new Set();
|
|
207
|
+
const isCategoryPermitted = (cat) => {
|
|
208
|
+
if (!cat)
|
|
209
|
+
return true;
|
|
210
|
+
if (config.includeCategories && config.includeCategories.length > 0) {
|
|
211
|
+
if (!config.includeCategories.includes(cat))
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (config.excludeCategories && config.excludeCategories.length > 0) {
|
|
215
|
+
if (config.excludeCategories.includes(cat))
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
};
|
|
220
|
+
// Traversal 1: Upstream (Inbound references from consumers / parent contracts)
|
|
221
|
+
const traverseUpstream = () => {
|
|
222
|
+
const queue = entrypointDests.map((dest) => ({
|
|
223
|
+
dest,
|
|
224
|
+
depth: 0,
|
|
225
|
+
}));
|
|
226
|
+
const seen = new Set(entrypointDests);
|
|
227
|
+
while (queue.length > 0) {
|
|
228
|
+
const { dest: currentDest, depth: currentDepth } = queue.shift();
|
|
229
|
+
if (currentDepth >= upstreamDepth)
|
|
230
|
+
continue;
|
|
231
|
+
const inEdges = inboundGraph.get(currentDest) ?? [];
|
|
232
|
+
for (const edge of inEdges) {
|
|
233
|
+
const callerDest = edge.source;
|
|
234
|
+
const callerEntry = destToEntry.get(callerDest);
|
|
235
|
+
if (!callerEntry)
|
|
236
|
+
continue;
|
|
237
|
+
const relToWorkspace = callerEntry.sourceRelativePath.replace(/\\/g, "/");
|
|
238
|
+
const matchesStopAt = config.stopAt && config.stopAt.length > 0
|
|
239
|
+
? matchesAnyPattern(relToWorkspace, config.stopAt) ||
|
|
240
|
+
matchesAnyPattern(callerDest, config.stopAt)
|
|
241
|
+
: false;
|
|
242
|
+
const outOfScope = config.scope && config.scope.length > 0
|
|
243
|
+
? !(matchesAnyPattern(relToWorkspace, config.scope) ||
|
|
244
|
+
matchesAnyPattern(callerDest, config.scope))
|
|
245
|
+
: false;
|
|
246
|
+
const callerCat = nodeCategories.get(callerDest);
|
|
247
|
+
if (!isCategoryPermitted(callerCat))
|
|
248
|
+
continue;
|
|
249
|
+
if (matchesStopAt || outOfScope) {
|
|
250
|
+
boundaryTerminalsSet.add(callerDest);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!seen.has(callerDest)) {
|
|
254
|
+
seen.add(callerDest);
|
|
255
|
+
upstreamNodesSet.add(callerDest);
|
|
256
|
+
lineage.set(callerDest, { parentDest: currentDest, rawRef: edge.rawRef });
|
|
257
|
+
queue.push({ dest: callerDest, depth: currentDepth + 1 });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
// Traversal 2: Downstream (Outbound references to dependencies / models)
|
|
263
|
+
const traverseDownstream = (startNodes) => {
|
|
264
|
+
const queue = startNodes.map((dest) => ({
|
|
265
|
+
dest,
|
|
266
|
+
depth: 0,
|
|
267
|
+
}));
|
|
268
|
+
const seen = new Set(startNodes);
|
|
269
|
+
while (queue.length > 0) {
|
|
270
|
+
const { dest: currentDest, depth: currentDepth } = queue.shift();
|
|
271
|
+
const currentEntry = destToEntry.get(currentDest);
|
|
272
|
+
if (!currentEntry)
|
|
273
|
+
continue;
|
|
274
|
+
const ext = path.posix.extname(currentDest).toLowerCase();
|
|
275
|
+
if (!TEXT_EXTENSIONS.has(ext))
|
|
276
|
+
continue;
|
|
277
|
+
const content = fileContents.get(currentDest);
|
|
278
|
+
if (!content)
|
|
279
|
+
continue;
|
|
280
|
+
const rawRefs = extractOutboundReferences(content, ext);
|
|
281
|
+
for (const rawRefWithAngle of rawRefs) {
|
|
282
|
+
const rawRef = rawRefWithAngle.startsWith("<") && rawRefWithAngle.endsWith(">")
|
|
283
|
+
? rawRefWithAngle.slice(1, -1)
|
|
284
|
+
: rawRefWithAngle;
|
|
285
|
+
const normalizedRef = rawRef.replace(/\\/g, "/");
|
|
286
|
+
if (normalizedRef.startsWith("#"))
|
|
287
|
+
continue;
|
|
288
|
+
if (!normalizedRef.startsWith("file:") && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(normalizedRef)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const splitIndex = normalizedRef.search(/[#?]/);
|
|
292
|
+
const basePath = splitIndex === -1 ? normalizedRef : normalizedRef.slice(0, splitIndex);
|
|
293
|
+
if (!basePath)
|
|
294
|
+
continue;
|
|
295
|
+
const resolvedAbs = resolveLocalPath(basePath, currentEntry.sourceAbsolutePath, workspaceRoot);
|
|
296
|
+
if (!resolvedAbs)
|
|
297
|
+
continue;
|
|
298
|
+
const relToWorkspace = path.relative(workspaceRoot, resolvedAbs).replace(/\\/g, "/");
|
|
299
|
+
const targetDest = sourceAbsToDest.get(resolvedAbs);
|
|
300
|
+
// Check 1: Terminal boundary patterns (stopAt)
|
|
301
|
+
const matchesStopAt = config.stopAt && config.stopAt.length > 0
|
|
302
|
+
? matchesAnyPattern(relToWorkspace, config.stopAt) ||
|
|
303
|
+
(targetDest ? matchesAnyPattern(targetDest, config.stopAt) : false) ||
|
|
304
|
+
matchesAnyPattern(basePath, config.stopAt)
|
|
305
|
+
: false;
|
|
306
|
+
// Check 2: Domain scope boundary (scope)
|
|
307
|
+
const outOfScope = config.scope && config.scope.length > 0
|
|
308
|
+
? !(matchesAnyPattern(relToWorkspace, config.scope) ||
|
|
309
|
+
(targetDest ? matchesAnyPattern(targetDest, config.scope) : false))
|
|
310
|
+
: false;
|
|
311
|
+
// Check 3: Max traversal hop depth
|
|
312
|
+
const exceedsMaxDepth = currentDepth + 1 > maxDepth;
|
|
313
|
+
// Check 4: Category filter
|
|
314
|
+
const targetCat = targetDest ? nodeCategories.get(targetDest) : undefined;
|
|
315
|
+
const categoryExcluded = !isCategoryPermitted(targetCat);
|
|
316
|
+
if (matchesStopAt || outOfScope || exceedsMaxDepth || categoryExcluded) {
|
|
317
|
+
const terminalTarget = targetDest ?? relToWorkspace;
|
|
318
|
+
boundaryTerminalsSet.add(terminalTarget);
|
|
319
|
+
const edgeList = boundaryEdges.get(currentDest) ?? [];
|
|
320
|
+
if (!edgeList.some((e) => e.target === terminalTarget)) {
|
|
321
|
+
edgeList.push({ target: terminalTarget, rawRef });
|
|
322
|
+
boundaryEdges.set(currentDest, edgeList);
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!targetDest) {
|
|
327
|
+
boundaryTerminalsSet.add(relToWorkspace);
|
|
328
|
+
const edgeList = boundaryEdges.get(currentDest) ?? [];
|
|
329
|
+
if (!edgeList.some((e) => e.target === relToWorkspace)) {
|
|
330
|
+
edgeList.push({ target: relToWorkspace, rawRef });
|
|
331
|
+
boundaryEdges.set(currentDest, edgeList);
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!seen.has(targetDest)) {
|
|
336
|
+
seen.add(targetDest);
|
|
337
|
+
downstreamNodesSet.add(targetDest);
|
|
338
|
+
lineage.set(targetDest, { parentDest: currentDest, rawRef });
|
|
339
|
+
queue.push({ dest: targetDest, depth: currentDepth + 1 });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
if (direction === "upstream") {
|
|
345
|
+
traverseUpstream();
|
|
346
|
+
}
|
|
347
|
+
else if (direction === "bidirectional") {
|
|
348
|
+
traverseUpstream();
|
|
349
|
+
traverseDownstream([...entrypointDests, ...upstreamNodesSet]);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// "downstream" (default)
|
|
353
|
+
traverseDownstream(entrypointDests);
|
|
354
|
+
}
|
|
355
|
+
const visited = new Set([
|
|
356
|
+
...entrypointDests,
|
|
357
|
+
...upstreamNodesSet,
|
|
358
|
+
...downstreamNodesSet,
|
|
359
|
+
]);
|
|
360
|
+
// Partition candidate entries into reachable and eliminated
|
|
361
|
+
const reachableEntries = new Map();
|
|
362
|
+
const eliminatedEntries = [];
|
|
363
|
+
for (const [dest, entry] of entries.entries()) {
|
|
364
|
+
const isPreserved = traversalMode === "hybrid" ||
|
|
365
|
+
config.preserveTargets === true ||
|
|
366
|
+
(config.preserve && config.preserve.length > 0
|
|
367
|
+
? matchesAnyPattern(dest, config.preserve) ||
|
|
368
|
+
matchesAnyPattern(entry.sourceRelativePath.replace(/\\/g, "/"), config.preserve)
|
|
369
|
+
: false);
|
|
370
|
+
const cat = nodeCategories.get(dest);
|
|
371
|
+
const categoryExcluded = !isCategoryPermitted(cat);
|
|
372
|
+
if (!categoryExcluded && (visited.has(dest) || isPreserved)) {
|
|
373
|
+
reachableEntries.set(dest, entry);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
eliminatedEntries.push(entry);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
reachableEntries,
|
|
381
|
+
eliminatedEntries,
|
|
382
|
+
warnings,
|
|
383
|
+
lineage,
|
|
384
|
+
boundaryTerminals: Array.from(boundaryTerminalsSet),
|
|
385
|
+
boundaryEdges,
|
|
386
|
+
upstreamNodes: Array.from(upstreamNodesSet),
|
|
387
|
+
downstreamNodes: Array.from(downstreamNodesSet),
|
|
388
|
+
nodeCategories,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Traces the provenance chain from an entrypoint down to the given target destination.
|
|
393
|
+
*/
|
|
394
|
+
export function explainReachability(targetDest, lineage, entrypoints, boundaryEdges) {
|
|
395
|
+
const normTarget = targetDest.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
396
|
+
if (entrypoints.some((ep) => ep.replace(/\\/g, "/").replace(/^\.\//, "") === normTarget)) {
|
|
397
|
+
return []; // Is an entrypoint itself
|
|
398
|
+
}
|
|
399
|
+
if (lineage.has(normTarget)) {
|
|
400
|
+
const hops = [];
|
|
401
|
+
let curr = normTarget;
|
|
402
|
+
const seen = new Set([curr]);
|
|
403
|
+
while (lineage.has(curr)) {
|
|
404
|
+
const edge = lineage.get(curr);
|
|
405
|
+
hops.unshift({
|
|
406
|
+
from: edge.parentDest,
|
|
407
|
+
to: curr,
|
|
408
|
+
ref: edge.rawRef,
|
|
409
|
+
});
|
|
410
|
+
curr = edge.parentDest;
|
|
411
|
+
if (seen.has(curr))
|
|
412
|
+
break; // Cycle prevention
|
|
413
|
+
seen.add(curr);
|
|
414
|
+
}
|
|
415
|
+
return hops.length > 0 ? hops : null;
|
|
416
|
+
}
|
|
417
|
+
if (boundaryEdges) {
|
|
418
|
+
for (const [parentDest, terminals] of boundaryEdges.entries()) {
|
|
419
|
+
const match = terminals.find((t) => t.target === normTarget ||
|
|
420
|
+
t.target.endsWith(`/${normTarget}`) ||
|
|
421
|
+
t.rawRef === targetDest);
|
|
422
|
+
if (match) {
|
|
423
|
+
const parentHops = explainReachability(parentDest, lineage, entrypoints, boundaryEdges) ?? [];
|
|
424
|
+
return [
|
|
425
|
+
...parentHops,
|
|
426
|
+
{
|
|
427
|
+
from: parentDest,
|
|
428
|
+
to: match.target,
|
|
429
|
+
ref: match.rawRef,
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Constructs a visual hierarchical dependency tree for bundle inspection.
|
|
439
|
+
*/
|
|
440
|
+
export function buildDependencyTree(entries, config, workspaceRoot) {
|
|
441
|
+
const { reachableEntries, lineage, boundaryEdges, upstreamNodes, nodeCategories, } = pruneUnreachableEntries(entries, config, workspaceRoot);
|
|
442
|
+
const upstreamSet = new Set(upstreamNodes ?? []);
|
|
443
|
+
// Group children by parentDest
|
|
444
|
+
const parentToChildren = new Map();
|
|
445
|
+
for (const [childDest, hop] of lineage.entries()) {
|
|
446
|
+
const list = parentToChildren.get(hop.parentDest) ?? [];
|
|
447
|
+
list.push(childDest);
|
|
448
|
+
parentToChildren.set(hop.parentDest, list);
|
|
449
|
+
}
|
|
450
|
+
// Build tree nodes recursively
|
|
451
|
+
const buildNode = (dest, isEntrypoint, visited) => {
|
|
452
|
+
const entry = reachableEntries.get(dest);
|
|
453
|
+
const size = entry?.size ?? 0;
|
|
454
|
+
const childrenDests = parentToChildren.get(dest) ?? [];
|
|
455
|
+
const children = [];
|
|
456
|
+
visited.add(dest);
|
|
457
|
+
for (const child of childrenDests) {
|
|
458
|
+
if (!visited.has(child)) {
|
|
459
|
+
children.push(buildNode(child, false, new Set(visited)));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const terminals = boundaryEdges.get(dest) ?? [];
|
|
463
|
+
for (const term of terminals) {
|
|
464
|
+
children.push({
|
|
465
|
+
path: term.target,
|
|
466
|
+
size: 0,
|
|
467
|
+
isEntrypoint: false,
|
|
468
|
+
isBoundaryTerminal: true,
|
|
469
|
+
children: [],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const dir = upstreamSet.has(dest) ? "upstream" : "downstream";
|
|
473
|
+
const cat = nodeCategories?.get(dest);
|
|
474
|
+
return {
|
|
475
|
+
path: dest,
|
|
476
|
+
size,
|
|
477
|
+
isEntrypoint,
|
|
478
|
+
direction: isEntrypoint ? undefined : dir,
|
|
479
|
+
category: cat,
|
|
480
|
+
children,
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
const trees = [];
|
|
484
|
+
for (const ep of config.entrypoints) {
|
|
485
|
+
const cleanEp = ep.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
486
|
+
let matchedDest = cleanEp;
|
|
487
|
+
for (const [dest, entry] of entries.entries()) {
|
|
488
|
+
if (dest === cleanEp ||
|
|
489
|
+
entry.sourceRelativePath === cleanEp ||
|
|
490
|
+
entry.sourceAbsolutePath === path.resolve(workspaceRoot, cleanEp)) {
|
|
491
|
+
matchedDest = dest;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (reachableEntries.has(matchedDest)) {
|
|
496
|
+
trees.push(buildNode(matchedDest, true, new Set()));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return trees;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Formats a hierarchical dependency tree as human-readable ASCII text.
|
|
503
|
+
*/
|
|
504
|
+
export function renderAsciiTree(nodes, prefix = "") {
|
|
505
|
+
let output = "";
|
|
506
|
+
nodes.forEach((node, index) => {
|
|
507
|
+
const isLast = index === nodes.length - 1;
|
|
508
|
+
const marker = isLast ? "└── " : "├── ";
|
|
509
|
+
const nextPrefix = prefix + (isLast ? " " : "│ ");
|
|
510
|
+
const sizeFormatted = (node.size / 1024).toFixed(1) + " KB";
|
|
511
|
+
const entrypointTag = node.isEntrypoint ? " (entrypoint)" : "";
|
|
512
|
+
const terminalTag = node.isBoundaryTerminal ? " [boundary terminal, not traversed]" : "";
|
|
513
|
+
const sizeTag = node.isBoundaryTerminal ? "" : ` [${sizeFormatted}]`;
|
|
514
|
+
const dirTag = node.direction === "upstream" ? " ▲ [UPSTREAM]" : "";
|
|
515
|
+
const catTag = node.category ? ` [${node.category}]` : "";
|
|
516
|
+
output += `${prefix}${marker}${node.path}${catTag}${sizeTag}${dirTag}${entrypointTag}${terminalTag}\n`;
|
|
517
|
+
if (node.children && node.children.length > 0) {
|
|
518
|
+
output += renderAsciiTree(node.children, nextPrefix);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return output;
|
|
522
|
+
}
|
|
523
|
+
//# sourceMappingURL=tree-shaker.js.map
|