@bamboocss/parser 1.45.4 → 1.46.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/dist/index.cjs +827 -97
- package/dist/index.d.cts +147 -37
- package/dist/index.d.mts +147 -37
- package/dist/index.mjs +827 -97
- package/package.json +11 -10
package/dist/index.cjs
CHANGED
|
@@ -877,6 +877,8 @@ var ParserResult = class {
|
|
|
877
877
|
pattern = /* @__PURE__ */ new Map();
|
|
878
878
|
filePath;
|
|
879
879
|
encoder;
|
|
880
|
+
/** Resolver targets crossed while extracting values which contributed CSS. */
|
|
881
|
+
dependencies = /* @__PURE__ */ new Set();
|
|
880
882
|
/**
|
|
881
883
|
* `css()` calls whose styles the build could not fully see.
|
|
882
884
|
*
|
|
@@ -1042,6 +1044,33 @@ var ParserResult = class {
|
|
|
1042
1044
|
this.filePath = filePath;
|
|
1043
1045
|
return this;
|
|
1044
1046
|
}
|
|
1047
|
+
/** @internal Called only by the extractor-facing resolver, not by import classification. */
|
|
1048
|
+
addDependency(filePath) {
|
|
1049
|
+
this.dependencies.add(filePath.replaceAll("\\", "/"));
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Local source paths crossed while resolving values this extraction actually encoded.
|
|
1053
|
+
*
|
|
1054
|
+
* The Project ledger deliberately records every local import, including ordinary runtime
|
|
1055
|
+
* bindings. Box nodes retain the declaration node followed while resolving a style value,
|
|
1056
|
+
* so this is the narrow semantic target set consumers can feed back to that ledger to recover
|
|
1057
|
+
* re-export/barrel paths without watching unrelated imports.
|
|
1058
|
+
*/
|
|
1059
|
+
getDependencies() {
|
|
1060
|
+
const own = this.filePath?.replaceAll("\\", "/");
|
|
1061
|
+
const paths = new Set(this.dependencies);
|
|
1062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1063
|
+
const visit = (node) => {
|
|
1064
|
+
if (!node || seen.has(node)) return;
|
|
1065
|
+
seen.add(node);
|
|
1066
|
+
const path = node.getNode?.()?.getSourceFile().getFilePath().replaceAll("\\", "/");
|
|
1067
|
+
if (path && path !== own) paths.add(path);
|
|
1068
|
+
if (_bamboocss_extractor.box.isMap(node)) for (const child of node.value.values()) visit(child);
|
|
1069
|
+
else if (_bamboocss_extractor.box.isArray(node)) for (const child of node.value) visit(child);
|
|
1070
|
+
};
|
|
1071
|
+
for (const item of this.all) if (item.type !== "cva-call") visit(item.box);
|
|
1072
|
+
return [...paths].sort();
|
|
1073
|
+
}
|
|
1045
1074
|
merge(result) {
|
|
1046
1075
|
result.css.forEach((item) => this.css.add(this.append(item)));
|
|
1047
1076
|
result.cva.forEach((item) => this.cva.add(this.append(item)));
|
|
@@ -1058,6 +1087,7 @@ var ParserResult = class {
|
|
|
1058
1087
|
items.forEach((item) => set.add(this.append(item)));
|
|
1059
1088
|
});
|
|
1060
1089
|
if (result.unresolved.length) this.unresolved.push(...result.unresolved);
|
|
1090
|
+
for (const dependency of result.dependencies) this.dependencies.add(dependency);
|
|
1061
1091
|
return this;
|
|
1062
1092
|
}
|
|
1063
1093
|
toArray() {
|
|
@@ -1142,6 +1172,7 @@ function createParser(context) {
|
|
|
1142
1172
|
reason: "unresolved-raw"
|
|
1143
1173
|
});
|
|
1144
1174
|
};
|
|
1175
|
+
const recordDependency = (filePath) => parserResult.addDependency(filePath);
|
|
1145
1176
|
(0, _bamboocss_extractor.extract)({
|
|
1146
1177
|
ast: sourceFile,
|
|
1147
1178
|
tokens: context.tokens ? {
|
|
@@ -1190,7 +1221,9 @@ function createParser(context) {
|
|
|
1190
1221
|
if (file.isValidRecipe(name) || file.isValidPattern(name)) reportUnresolvedRaw(name, node);
|
|
1191
1222
|
return { environment: Object.assign({}, defaultEnv, { extra: { [name]: { raw: (v) => v } } }) };
|
|
1192
1223
|
},
|
|
1193
|
-
flags: { skipTraverseFiles: false }
|
|
1224
|
+
flags: { skipTraverseFiles: false },
|
|
1225
|
+
recordDependency,
|
|
1226
|
+
resolveModule
|
|
1194
1227
|
}).forEach((result, alias) => {
|
|
1195
1228
|
const name = file.getName(file.normalizeFnName(alias));
|
|
1196
1229
|
_bamboocss_logger.logger.debug(`ast:${name}`, name !== alias ? {
|
|
@@ -1298,23 +1331,61 @@ const invalidateResolutions = () => {
|
|
|
1298
1331
|
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
1299
1332
|
clearImportedRecipeCache();
|
|
1300
1333
|
};
|
|
1301
|
-
const normalizeCompilerOptions = (raw) => {
|
|
1334
|
+
const normalizeCompilerOptions = (raw, basePath = process.cwd()) => {
|
|
1302
1335
|
if (!raw) return {};
|
|
1303
|
-
const { options } = ts_morph.ts.convertCompilerOptionsFromJson(raw,
|
|
1336
|
+
const { options } = ts_morph.ts.convertCompilerOptionsFromJson(raw, basePath);
|
|
1304
1337
|
return options;
|
|
1305
1338
|
};
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1339
|
+
/** Snapshot the JSON-shaped ts-morph options while retaining opaque hosts and callbacks. */
|
|
1340
|
+
const snapshotProjectOption = (value) => {
|
|
1341
|
+
if (Array.isArray(value)) return value.map(snapshotProjectOption);
|
|
1342
|
+
if (!value || typeof value !== "object") return value;
|
|
1343
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return value;
|
|
1344
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshotProjectOption(entry)]));
|
|
1345
|
+
};
|
|
1346
|
+
const prepareTsProjectOptions = (options, snapshotNested = false, compilerOptionsBasePath = process.cwd()) => {
|
|
1347
|
+
const snapshot = { ...options };
|
|
1348
|
+
if (snapshotNested) {
|
|
1349
|
+
for (const key of [
|
|
1350
|
+
"compilerOptions",
|
|
1351
|
+
"defaultCompilerOptions",
|
|
1352
|
+
"manipulationSettings"
|
|
1353
|
+
]) if (key in snapshot) snapshot[key] = snapshotProjectOption(snapshot[key]);
|
|
1316
1354
|
}
|
|
1317
|
-
|
|
1355
|
+
return {
|
|
1356
|
+
skipAddingFilesFromTsConfig: true,
|
|
1357
|
+
skipFileDependencyResolution: true,
|
|
1358
|
+
skipLoadingLibFiles: true,
|
|
1359
|
+
...snapshot,
|
|
1360
|
+
compilerOptions: {
|
|
1361
|
+
allowJs: true,
|
|
1362
|
+
strictNullChecks: false,
|
|
1363
|
+
skipLibCheck: true,
|
|
1364
|
+
...normalizeCompilerOptions(snapshot.compilerOptions, compilerOptionsBasePath)
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1368
|
+
const createTsProject = (options) => new ts_morph.Project(options);
|
|
1369
|
+
/** Filesystem errors proving a lexical candidate cannot name a resolvable file. */
|
|
1370
|
+
const isMissingPathShapeError = (error) => {
|
|
1371
|
+
const code = error?.code;
|
|
1372
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "ENAMETOOLONG";
|
|
1373
|
+
};
|
|
1374
|
+
/** Match the successful receiver cases of an ordinary writable data-property assignment. */
|
|
1375
|
+
const setProjectOnReceiver = (receiver, project) => {
|
|
1376
|
+
if (typeof receiver !== "object" && typeof receiver !== "function" || receiver === null) return;
|
|
1377
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(receiver, "project");
|
|
1378
|
+
if (descriptor) {
|
|
1379
|
+
if ("value" in descriptor && descriptor.writable) Reflect.defineProperty(receiver, "project", { value: project });
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
Reflect.defineProperty(receiver, "project", {
|
|
1383
|
+
configurable: true,
|
|
1384
|
+
enumerable: true,
|
|
1385
|
+
value: project,
|
|
1386
|
+
writable: true
|
|
1387
|
+
});
|
|
1388
|
+
};
|
|
1318
1389
|
/**
|
|
1319
1390
|
* How to parse a file, decided by its extension.
|
|
1320
1391
|
*
|
|
@@ -1341,35 +1412,188 @@ const scriptKindFor = (filePath) => {
|
|
|
1341
1412
|
return extension === ".ts" || extension === ".mts" || extension === ".cts" ? ts_morph.ScriptKind.TS : ts_morph.ScriptKind.TSX;
|
|
1342
1413
|
};
|
|
1343
1414
|
var Project = class {
|
|
1344
|
-
|
|
1345
|
-
|
|
1415
|
+
/**
|
|
1416
|
+
* Source-loading contract for the opt-in deferred mode:
|
|
1417
|
+
*
|
|
1418
|
+
* - materializing: `project`, `getSourceFile`, `getDependents`, every create/add/remove/reload
|
|
1419
|
+
* API, and non-JSON `parseSourceFile`;
|
|
1420
|
+
* - graph-independent: `files`, `parser`, `parserOptions`, `readFile`, `getFiles`, the
|
|
1421
|
+
* resolution-ledger/work getters, `getUnresolvedImporters`, `parseJson`/JSON
|
|
1422
|
+
* `parseSourceFile`, `transformFile`, `classify`.
|
|
1423
|
+
*
|
|
1424
|
+
* Graph-independent means outside the atomic preload. While it is `loading`, every public
|
|
1425
|
+
* wrapper entry rejects reentrancy before returning live state or invoking a callback.
|
|
1426
|
+
*
|
|
1427
|
+
* Private resolution and dependency helpers are reachable only after a materializing parse.
|
|
1428
|
+
*/
|
|
1346
1429
|
parser;
|
|
1430
|
+
project;
|
|
1431
|
+
#guardedParser;
|
|
1432
|
+
#parser;
|
|
1433
|
+
#sourceFiles;
|
|
1434
|
+
options;
|
|
1347
1435
|
get parserOptions() {
|
|
1436
|
+
this.#assertNotLoading();
|
|
1348
1437
|
return this.options.parserOptions;
|
|
1349
1438
|
}
|
|
1350
1439
|
constructor(options) {
|
|
1440
|
+
const { deferInitialSourceFiles, getFiles, parserOptions } = options;
|
|
1441
|
+
const tsProjectOptions = { ...options };
|
|
1442
|
+
delete tsProjectOptions.deferInitialSourceFiles;
|
|
1443
|
+
delete tsProjectOptions.resolutionConfigFiles;
|
|
1351
1444
|
this.options = options;
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1445
|
+
this.#sourceFiles = {
|
|
1446
|
+
accessor: void 0,
|
|
1447
|
+
initialFiles: [],
|
|
1448
|
+
phase: deferInitialSourceFiles ? "pending" : "ready",
|
|
1449
|
+
project: void 0,
|
|
1450
|
+
projectOptions: prepareTsProjectOptions(tsProjectOptions, Boolean(deferInitialSourceFiles), parserOptions.config.cwd || process.cwd()),
|
|
1451
|
+
revision: 0
|
|
1452
|
+
};
|
|
1453
|
+
if (!deferInitialSourceFiles) {
|
|
1454
|
+
this.project = createTsProject(this.#sourceFiles.projectOptions);
|
|
1455
|
+
this.#sourceFiles.project = this.project;
|
|
1456
|
+
}
|
|
1457
|
+
const parser = createParser(parserOptions);
|
|
1458
|
+
this.#parser = parser;
|
|
1459
|
+
if (deferInitialSourceFiles) {
|
|
1460
|
+
this.#guardedParser = (...args) => {
|
|
1461
|
+
this.#assertNotLoading();
|
|
1462
|
+
return this.#parser(...args);
|
|
1463
|
+
};
|
|
1464
|
+
Object.defineProperty(this, "parser", {
|
|
1465
|
+
configurable: false,
|
|
1466
|
+
enumerable: true,
|
|
1467
|
+
get: () => {
|
|
1468
|
+
this.#assertNotLoading();
|
|
1469
|
+
return this.#guardedParser;
|
|
1470
|
+
},
|
|
1471
|
+
set: (next) => {
|
|
1472
|
+
this.#assertNotLoading();
|
|
1473
|
+
if (Object.isFrozen(this)) throw new TypeError("Cannot assign to read only property 'parser' of Project");
|
|
1474
|
+
this.#parser = next;
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
this.#sourceFiles.initialFiles = Object.freeze([...getFiles()]);
|
|
1478
|
+
const getProject = () => {
|
|
1479
|
+
this.#ensureSourceFiles();
|
|
1480
|
+
return this.#sourceFiles.project;
|
|
1481
|
+
};
|
|
1482
|
+
const isOwner = (receiver) => receiver === this;
|
|
1483
|
+
const isFrozen = () => Object.isFrozen(this);
|
|
1484
|
+
const setOwnerProject = (project) => {
|
|
1485
|
+
this.#sourceFiles.revision++;
|
|
1486
|
+
if (Object.isFrozen(this)) throw new TypeError("Cannot assign to read only property 'project' of Project");
|
|
1487
|
+
this.#sourceFiles.project = project;
|
|
1488
|
+
this.#sourceFiles.phase = "ready";
|
|
1489
|
+
this.resetResolutionState();
|
|
1490
|
+
};
|
|
1491
|
+
const assertNotLoading = () => this.#assertNotLoading();
|
|
1492
|
+
const setProject = function(project) {
|
|
1493
|
+
assertNotLoading();
|
|
1494
|
+
if (!isOwner(this)) {
|
|
1495
|
+
if (isFrozen()) return;
|
|
1496
|
+
setProjectOnReceiver(this, project);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
setOwnerProject(project);
|
|
1500
|
+
};
|
|
1501
|
+
this.#sourceFiles.accessor = {
|
|
1502
|
+
get: getProject,
|
|
1503
|
+
set: setProject
|
|
1504
|
+
};
|
|
1505
|
+
Object.defineProperty(this, "project", {
|
|
1506
|
+
configurable: false,
|
|
1507
|
+
enumerable: true,
|
|
1508
|
+
get: getProject,
|
|
1509
|
+
set: setProject
|
|
1510
|
+
});
|
|
1511
|
+
} else {
|
|
1512
|
+
this.parser = parser;
|
|
1513
|
+
this.createSourceFiles();
|
|
1514
|
+
}
|
|
1356
1515
|
}
|
|
1357
1516
|
get files() {
|
|
1517
|
+
this.#assertNotLoading();
|
|
1358
1518
|
return this.options.getFiles();
|
|
1359
1519
|
}
|
|
1360
1520
|
/**
|
|
1361
|
-
*
|
|
1362
|
-
* the source file's own normalized path so lookups match regardless of whether
|
|
1363
|
-
* the caller passed a relative, aliased or platform-specific path.
|
|
1521
|
+
* Atomically preload the inventory captured when this wrapper was constructed.
|
|
1364
1522
|
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1523
|
+
* The candidate ts-morph Project stays local until every non-ENOENT read and AST creation
|
|
1524
|
+
* succeeds. A failure discards it, and the next operation retries with a fresh candidate
|
|
1525
|
+
* over the same frozen membership. A materializing callback that re-enters this wrapper
|
|
1526
|
+
* sees no candidate and fails explicitly rather than observing a partial module graph.
|
|
1368
1527
|
*/
|
|
1528
|
+
#hasSourceFilesAccessor = () => {
|
|
1529
|
+
const expected = this.#sourceFiles.accessor;
|
|
1530
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, "project");
|
|
1531
|
+
return Boolean(expected && descriptor?.get === expected.get && descriptor.set === expected.set);
|
|
1532
|
+
};
|
|
1533
|
+
#assertSourceFilesAccessor = () => {
|
|
1534
|
+
if (!this.#hasSourceFilesAccessor()) throw new Error("Project property changed during source initialization");
|
|
1535
|
+
};
|
|
1536
|
+
#assertNotLoading = () => {
|
|
1537
|
+
if (this.#sourceFiles.phase !== "loading") return;
|
|
1538
|
+
this.#sourceFiles.revision++;
|
|
1539
|
+
throw new Error("Project source files are already being initialized");
|
|
1540
|
+
};
|
|
1541
|
+
#assertSourceFilesTransaction = (revision) => {
|
|
1542
|
+
this.#assertSourceFilesAccessor();
|
|
1543
|
+
if (this.#sourceFiles.revision !== revision) throw new Error("Project source files are already being initialized; transaction was re-entered or mutated");
|
|
1544
|
+
};
|
|
1545
|
+
#materializeSourceFiles = (read, skipMissing) => {
|
|
1546
|
+
this.#assertSourceFilesAccessor();
|
|
1547
|
+
const revision = this.#sourceFiles.revision;
|
|
1548
|
+
this.#sourceFiles.phase = "loading";
|
|
1549
|
+
try {
|
|
1550
|
+
const candidate = createTsProject(this.#sourceFiles.projectOptions);
|
|
1551
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1552
|
+
let loaded = 0;
|
|
1553
|
+
for (const [index, file] of this.#sourceFiles.initialFiles.entries()) try {
|
|
1554
|
+
const content = read(file, index);
|
|
1555
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1556
|
+
candidate.createSourceFile(file, content, {
|
|
1557
|
+
overwrite: true,
|
|
1558
|
+
scriptKind: scriptKindFor(file)
|
|
1559
|
+
});
|
|
1560
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1561
|
+
loaded++;
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1564
|
+
if (!skipMissing || error?.code !== "ENOENT") throw error;
|
|
1565
|
+
}
|
|
1566
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1567
|
+
if (loaded > 0) invalidateResolutions();
|
|
1568
|
+
this.#moduleResolutionCache = void 0;
|
|
1569
|
+
this.#fileTreeRevision++;
|
|
1570
|
+
this.#assertSourceFilesTransaction(revision);
|
|
1571
|
+
this.#sourceFiles.project = candidate;
|
|
1572
|
+
this.#sourceFiles.phase = "ready";
|
|
1573
|
+
return;
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
this.#sourceFiles.project = void 0;
|
|
1576
|
+
this.#sourceFiles.phase = "pending";
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
#ensureSourceFiles = () => {
|
|
1581
|
+
if (this.#sourceFiles.phase === "ready") return;
|
|
1582
|
+
this.#assertNotLoading();
|
|
1583
|
+
this.#materializeSourceFiles((file) => this.options.readFile(file), true);
|
|
1584
|
+
};
|
|
1585
|
+
/** Reverse dependency graph: resolved target -> importers. */
|
|
1369
1586
|
dependents = /* @__PURE__ */ new Map();
|
|
1370
1587
|
/** Forward edges, so a re-parse can retract exactly the previous ones. */
|
|
1371
1588
|
dependencies = /* @__PURE__ */ new Map();
|
|
1372
1589
|
/**
|
|
1590
|
+
* Deleted targets retain their last importers until those importers are reparsed.
|
|
1591
|
+
*
|
|
1592
|
+
* A watcher commonly removes first and asks second. The ledger itself must say the target
|
|
1593
|
+
* is now unresolved, while this one-turn tombstone keeps that unlink query answerable.
|
|
1594
|
+
*/
|
|
1595
|
+
removedDependents = /* @__PURE__ */ new Map();
|
|
1596
|
+
/**
|
|
1373
1597
|
* Path as a caller spells it -> the source file's own path.
|
|
1374
1598
|
*
|
|
1375
1599
|
* The graph is keyed on the latter, but callers pass whatever the watcher gave
|
|
@@ -1380,32 +1604,74 @@ var Project = class {
|
|
|
1380
1604
|
*/
|
|
1381
1605
|
canonicalPaths = /* @__PURE__ */ new Map();
|
|
1382
1606
|
/**
|
|
1383
|
-
* Files holding at least one import whose
|
|
1607
|
+
* Files holding at least one import whose local resolution is not final.
|
|
1384
1608
|
*
|
|
1385
|
-
* A broken or not-yet-created import produces no edge
|
|
1386
|
-
*
|
|
1387
|
-
* are the only candidates for
|
|
1609
|
+
* A broken or not-yet-created import produces no edge. A successful fallback edge
|
|
1610
|
+
* likewise cannot point at the missing higher-priority candidate that may replace it.
|
|
1611
|
+
* These importers are the only candidates for either add-event transition, and the
|
|
1612
|
+
* set is normally empty.
|
|
1388
1613
|
*/
|
|
1389
1614
|
unresolvedImporters = /* @__PURE__ */ new Set();
|
|
1390
|
-
/**
|
|
1391
|
-
|
|
1615
|
+
/** Exact post-transform resolution facts, one immutable list per importer. */
|
|
1616
|
+
resolutionsByImporter = /* @__PURE__ */ new Map();
|
|
1617
|
+
/** One hook/transform transaction for each source revision Bamboo can semantically read. */
|
|
1618
|
+
sourcePreparations = /* @__PURE__ */ new Map();
|
|
1619
|
+
/** Paths explicitly removed through this wrapper, until an add/create observes them again. */
|
|
1620
|
+
removedSourcePaths = /* @__PURE__ */ new Set();
|
|
1621
|
+
/** File-tree changes invalidate even successful resolutions (extension precedence can move). */
|
|
1622
|
+
#fileTreeRevision = 0;
|
|
1623
|
+
resolutionWork = {
|
|
1624
|
+
moduleResolutionsAttempted: 0,
|
|
1625
|
+
sourceFilesAdded: 0,
|
|
1626
|
+
sourceFilesRead: 0
|
|
1627
|
+
};
|
|
1628
|
+
resetResolutionState = () => {
|
|
1629
|
+
this.#moduleResolutionCache = void 0;
|
|
1630
|
+
this.#fileTreeRevision++;
|
|
1631
|
+
this.dependents = /* @__PURE__ */ new Map();
|
|
1632
|
+
this.dependencies = /* @__PURE__ */ new Map();
|
|
1633
|
+
this.removedDependents = /* @__PURE__ */ new Map();
|
|
1634
|
+
this.canonicalPaths = /* @__PURE__ */ new Map();
|
|
1635
|
+
this.unresolvedImporters = /* @__PURE__ */ new Set();
|
|
1636
|
+
this.resolutionsByImporter = /* @__PURE__ */ new Map();
|
|
1637
|
+
this.sourcePreparations = /* @__PURE__ */ new Map();
|
|
1638
|
+
this.removedSourcePaths = /* @__PURE__ */ new Set();
|
|
1639
|
+
this.resolutionWork = {
|
|
1640
|
+
moduleResolutionsAttempted: 0,
|
|
1641
|
+
sourceFilesAdded: 0,
|
|
1642
|
+
sourceFilesRead: 0
|
|
1643
|
+
};
|
|
1644
|
+
};
|
|
1645
|
+
/** Files whose imports may resolve differently after a local file appears. */
|
|
1646
|
+
getUnresolvedImporters = () => {
|
|
1647
|
+
this.#assertNotLoading();
|
|
1648
|
+
return [...this.unresolvedImporters].sort();
|
|
1649
|
+
};
|
|
1650
|
+
/** @internal Immutable resolution facts in importer/AST order. */
|
|
1651
|
+
getResolutionLedger = () => {
|
|
1652
|
+
this.#assertNotLoading();
|
|
1653
|
+
return Object.freeze([...this.resolutionsByImporter.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, entry]) => entry.facts));
|
|
1654
|
+
};
|
|
1655
|
+
/** @internal Every distinct local target represented by the current ledger. */
|
|
1656
|
+
getResolvedSourceFiles = () => {
|
|
1657
|
+
this.#assertNotLoading();
|
|
1658
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1659
|
+
for (const entry of this.resolutionsByImporter.values()) for (const fact of entry.facts) if (fact.target) paths.add(fact.target);
|
|
1660
|
+
return Object.freeze([...paths].sort());
|
|
1661
|
+
};
|
|
1662
|
+
/** @internal Deterministic resolver/add/read work, for regression assertions. */
|
|
1663
|
+
getResolutionWork = () => {
|
|
1664
|
+
this.#assertNotLoading();
|
|
1665
|
+
return Object.freeze({ ...this.resolutionWork });
|
|
1666
|
+
};
|
|
1392
1667
|
getSourceFile = (filePath) => {
|
|
1668
|
+
this.#assertNotLoading();
|
|
1393
1669
|
return this.project.getSourceFile(filePath);
|
|
1394
1670
|
};
|
|
1395
1671
|
/** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
|
|
1396
1672
|
normalizePath = (filePath) => filePath.replaceAll("\\", "/");
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1399
|
-
*
|
|
1400
|
-
* Deliberately not `decl.getModuleSpecifierSourceFile()`: that goes through the
|
|
1401
|
-
* symbol table, which forces `initializeTypeChecker` on first use and costs
|
|
1402
|
-
* hundreds of ms on a cold build. `ts.resolveModuleName` is purely a filesystem
|
|
1403
|
-
* lookup, and a shared cache keeps repeat specifiers off the disk.
|
|
1404
|
-
*
|
|
1405
|
-
* Looks the result up rather than adding it, so resolving `react` cannot pull a
|
|
1406
|
-
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
1407
|
-
*/
|
|
1408
|
-
moduleResolutionCache;
|
|
1673
|
+
/** Shared filesystem-only resolution cache; no type checker is constructed. */
|
|
1674
|
+
#moduleResolutionCache;
|
|
1409
1675
|
/**
|
|
1410
1676
|
* Everything memoized against the shape of the file tree, including the negative half.
|
|
1411
1677
|
*
|
|
@@ -1415,56 +1681,383 @@ var Project = class {
|
|
|
1415
1681
|
* importing it, since resolution is what finds one.
|
|
1416
1682
|
*/
|
|
1417
1683
|
invalidate = (fileTreeChanged = true) => {
|
|
1684
|
+
this.#assertNotLoading();
|
|
1418
1685
|
invalidateResolutions();
|
|
1419
|
-
if (fileTreeChanged)
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (!moduleName) return;
|
|
1424
|
-
const compilerOptions = this.project.getCompilerOptions();
|
|
1425
|
-
this.moduleResolutionCache ??= ts_morph.ts.createModuleResolutionCache(this.project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
|
|
1426
|
-
const name = ts_morph.ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
|
|
1427
|
-
return name ? this.project.getSourceFile(name) : void 0;
|
|
1686
|
+
if (fileTreeChanged) {
|
|
1687
|
+
this.#moduleResolutionCache = void 0;
|
|
1688
|
+
this.#fileTreeRevision++;
|
|
1689
|
+
}
|
|
1428
1690
|
};
|
|
1429
1691
|
/**
|
|
1430
|
-
*
|
|
1692
|
+
* Invalidate module/evaluator state after a resolver configuration byte changes.
|
|
1693
|
+
*
|
|
1694
|
+
* Unlike `resetResolutionState`, this keeps the published dependency graph long enough for
|
|
1695
|
+
* an incremental consumer to take its old dependent closure. Each reparsed importer replaces
|
|
1696
|
+
* its own facts under the bumped revision; unrelated graph entries remain available until
|
|
1697
|
+
* they are next queried instead of forcing a whole-project rebuild.
|
|
1431
1698
|
*
|
|
1432
|
-
*
|
|
1433
|
-
* dependencies is not resolved again while looking for recipes.
|
|
1699
|
+
* @internal
|
|
1434
1700
|
*/
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1701
|
+
refreshResolutionConfiguration = (compilerOptions, resolutionConfigFiles, replaceCompilerOptions) => {
|
|
1702
|
+
this.#assertNotLoading();
|
|
1703
|
+
this.options.resolutionConfigFiles = Object.freeze([...new Set(resolutionConfigFiles)].sort());
|
|
1704
|
+
if (replaceCompilerOptions) {
|
|
1705
|
+
const next = prepareTsProjectOptions({ compilerOptions }, false, this.options.parserOptions.config.cwd || process.cwd()).compilerOptions ?? {};
|
|
1706
|
+
this.#sourceFiles.projectOptions = {
|
|
1707
|
+
...this.#sourceFiles.projectOptions,
|
|
1708
|
+
compilerOptions: snapshotProjectOption(next)
|
|
1709
|
+
};
|
|
1710
|
+
if (this.#sourceFiles.phase === "ready") {
|
|
1711
|
+
const current = this.project.getCompilerOptions();
|
|
1712
|
+
const cleared = Object.fromEntries(Object.keys(current).map((key) => [key, void 0]));
|
|
1713
|
+
this.project.compilerOptions.set({
|
|
1714
|
+
...cleared,
|
|
1715
|
+
...next
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
invalidateResolutions();
|
|
1720
|
+
this.#moduleResolutionCache = void 0;
|
|
1721
|
+
this.#fileTreeRevision++;
|
|
1722
|
+
this.sourcePreparations.clear();
|
|
1723
|
+
};
|
|
1724
|
+
isLocalAlias = (specifier) => {
|
|
1725
|
+
const paths = this.project.getCompilerOptions().paths;
|
|
1726
|
+
if (!paths) return false;
|
|
1727
|
+
return Object.keys(paths).some((pattern) => {
|
|
1728
|
+
const wildcard = pattern.indexOf("*");
|
|
1729
|
+
if (wildcard === -1) return pattern === specifier;
|
|
1730
|
+
return specifier.startsWith(pattern.slice(0, wildcard)) && specifier.endsWith(pattern.slice(wildcard + 1));
|
|
1731
|
+
});
|
|
1732
|
+
};
|
|
1733
|
+
getLocalFailedLookupCandidates = (failedLookupLocations) => {
|
|
1734
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1735
|
+
const candidates = [];
|
|
1736
|
+
for (const filePath of failedLookupLocations ?? []) {
|
|
1737
|
+
const normalized = this.normalizePath(filePath);
|
|
1738
|
+
if (normalized.includes("/node_modules/") || normalized.endsWith("/package.json")) continue;
|
|
1739
|
+
if (!seen.has(normalized) && this.isInCheckout(normalized)) {
|
|
1740
|
+
seen.add(normalized);
|
|
1741
|
+
candidates.push(normalized);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return candidates;
|
|
1745
|
+
};
|
|
1746
|
+
isUnresolvedLocalSpecifier = (specifier, pendingCandidates) => specifier.startsWith(".") || specifier.startsWith("/") || this.isLocalAlias(specifier) || pendingCandidates.length > 0;
|
|
1747
|
+
isInCheckout = (filePath) => {
|
|
1748
|
+
if (this.#sourceFiles.projectOptions.useInMemoryFileSystem) return true;
|
|
1749
|
+
const fileSystem = this.project.getFileSystem();
|
|
1750
|
+
const configured = this.options.parserOptions.config.cwd;
|
|
1751
|
+
const spelledRoot = this.normalizePath(configured || fileSystem.getCurrentDirectory()).replace(/\/$/, "");
|
|
1752
|
+
const root = this.normalizePath(fileSystem.realpathSync(spelledRoot)).replace(/\/$/, "");
|
|
1753
|
+
let target;
|
|
1754
|
+
try {
|
|
1755
|
+
target = this.normalizePath(fileSystem.realpathSync(filePath));
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
if (!isMissingPathShapeError(error)) throw error;
|
|
1758
|
+
target = this.normalizePath(filePath);
|
|
1759
|
+
}
|
|
1760
|
+
return [root, spelledRoot].some((boundary) => target === boundary || target.startsWith(`${boundary}/`));
|
|
1761
|
+
};
|
|
1762
|
+
resolveSpecifier = (moduleName, from) => {
|
|
1763
|
+
this.#assertNotLoading();
|
|
1764
|
+
const project = this.project;
|
|
1765
|
+
const compilerOptions = project.getCompilerOptions();
|
|
1766
|
+
this.#moduleResolutionCache ??= ts_morph.ts.createModuleResolutionCache(project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
|
|
1767
|
+
const configurationFiles = /* @__PURE__ */ new Set();
|
|
1768
|
+
const resolutionHost = project.getModuleResolutionHost();
|
|
1769
|
+
const recordConfigurationFile = (filePath) => {
|
|
1770
|
+
const normalized = this.normalizePath(filePath);
|
|
1771
|
+
if (!normalized.endsWith("/package.json") || normalized.includes("/node_modules/")) return;
|
|
1772
|
+
if (!this.isInCheckout(normalized)) return;
|
|
1773
|
+
configurationFiles.add(normalized);
|
|
1774
|
+
};
|
|
1775
|
+
const host = {
|
|
1776
|
+
...resolutionHost,
|
|
1777
|
+
fileExists: (filePath) => {
|
|
1778
|
+
recordConfigurationFile(filePath);
|
|
1779
|
+
return resolutionHost.fileExists(filePath);
|
|
1780
|
+
},
|
|
1781
|
+
readFile: (filePath) => {
|
|
1782
|
+
recordConfigurationFile(filePath);
|
|
1783
|
+
return resolutionHost.readFile?.(filePath);
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
this.resolutionWork.moduleResolutionsAttempted++;
|
|
1787
|
+
const resolved = ts_morph.ts.resolveModuleName(moduleName, from.getFilePath(), compilerOptions, host, this.#moduleResolutionCache);
|
|
1788
|
+
const failedLookupLocations = resolved.failedLookupLocations;
|
|
1789
|
+
const affectingLocations = resolved.affectingLocations;
|
|
1790
|
+
for (const filePath of affectingLocations ?? []) recordConfigurationFile(filePath);
|
|
1791
|
+
const pendingCandidates = this.getLocalFailedLookupCandidates(failedLookupLocations);
|
|
1792
|
+
const module = resolved.resolvedModule;
|
|
1793
|
+
if (!module) return {
|
|
1794
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1795
|
+
local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates) || moduleName.startsWith("#"),
|
|
1796
|
+
pendingCandidates
|
|
1797
|
+
};
|
|
1798
|
+
const name = this.normalizePath(module.resolvedFileName);
|
|
1799
|
+
if (module.isExternalLibraryImport || name.includes("/node_modules/")) return {
|
|
1800
|
+
configurationFiles: [],
|
|
1801
|
+
local: false,
|
|
1802
|
+
pendingCandidates: []
|
|
1803
|
+
};
|
|
1804
|
+
if (!this.isInCheckout(name)) return {
|
|
1805
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1806
|
+
local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates),
|
|
1807
|
+
pendingCandidates
|
|
1808
|
+
};
|
|
1809
|
+
const existing = project.getSourceFile(name);
|
|
1810
|
+
if (existing) {
|
|
1811
|
+
this.removedSourcePaths.delete(name);
|
|
1812
|
+
return {
|
|
1813
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1814
|
+
local: true,
|
|
1815
|
+
pendingCandidates,
|
|
1816
|
+
sourceFile: existing
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
const withResolvedTarget = pendingCandidates.includes(name) ? pendingCandidates : [...pendingCandidates, name];
|
|
1820
|
+
if (this.removedSourcePaths.has(name)) return {
|
|
1821
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1822
|
+
local: true,
|
|
1823
|
+
pendingCandidates: withResolvedTarget
|
|
1824
|
+
};
|
|
1825
|
+
try {
|
|
1826
|
+
this.resolutionWork.sourceFilesRead++;
|
|
1827
|
+
const content = project.getFileSystem().readFileSync(name);
|
|
1828
|
+
const sourceFile = project.createSourceFile(name, content, {
|
|
1829
|
+
overwrite: true,
|
|
1830
|
+
scriptKind: scriptKindFor(name)
|
|
1831
|
+
});
|
|
1832
|
+
this.resolutionWork.sourceFilesAdded++;
|
|
1833
|
+
this.canonicalPaths.set(name, this.normalizePath(sourceFile.getFilePath()));
|
|
1834
|
+
return {
|
|
1835
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1836
|
+
local: true,
|
|
1837
|
+
pendingCandidates,
|
|
1838
|
+
sourceFile
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (error?.code !== "ENOENT") throw error;
|
|
1842
|
+
return {
|
|
1843
|
+
configurationFiles: [...configurationFiles].sort(),
|
|
1844
|
+
local: true,
|
|
1845
|
+
pendingCandidates: withResolvedTarget
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
retractImporter = (importer) => {
|
|
1850
|
+
for (const previous of this.dependencies.get(importer) ?? []) {
|
|
1851
|
+
const reverse = this.dependents.get(previous);
|
|
1852
|
+
reverse?.delete(importer);
|
|
1853
|
+
if (reverse?.size === 0) this.dependents.delete(previous);
|
|
1854
|
+
}
|
|
1855
|
+
this.dependencies.delete(importer);
|
|
1856
|
+
this.unresolvedImporters.delete(importer);
|
|
1857
|
+
this.resolutionsByImporter.delete(importer);
|
|
1858
|
+
for (const [removed, importers] of this.removedDependents) {
|
|
1859
|
+
importers.delete(importer);
|
|
1860
|
+
if (importers.size === 0) this.removedDependents.delete(removed);
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
publishResolutionFacts = (importer, sourceFile, text, facts, pendingCandidates, configurationFiles) => {
|
|
1864
|
+
this.retractImporter(importer);
|
|
1865
|
+
const current = /* @__PURE__ */ new Set();
|
|
1866
|
+
for (const fact of facts) {
|
|
1867
|
+
if (!fact.target || fact.target === importer) continue;
|
|
1868
|
+
const importers = this.dependents.get(fact.target) ?? /* @__PURE__ */ new Set();
|
|
1869
|
+
importers.add(importer);
|
|
1870
|
+
this.dependents.set(fact.target, importers);
|
|
1871
|
+
current.add(fact.target);
|
|
1872
|
+
}
|
|
1873
|
+
this.dependencies.set(importer, current);
|
|
1874
|
+
if (pendingCandidates.length > 0 || facts.some((fact) => fact.target === null)) this.unresolvedImporters.add(importer);
|
|
1875
|
+
this.resolutionsByImporter.set(importer, {
|
|
1876
|
+
configurationFiles: Object.freeze([...configurationFiles]),
|
|
1877
|
+
facts: Object.freeze([...facts]),
|
|
1878
|
+
hasPendingLocalCandidate: pendingCandidates.length > 0,
|
|
1879
|
+
pendingCandidates: Object.freeze([...pendingCandidates]),
|
|
1880
|
+
sourceFile,
|
|
1881
|
+
text,
|
|
1882
|
+
treeRevision: this.#fileTreeRevision
|
|
1883
|
+
});
|
|
1884
|
+
};
|
|
1885
|
+
ensureResolutionFacts = (sourceFile) => {
|
|
1886
|
+
this.#assertNotLoading();
|
|
1887
|
+
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
1888
|
+
const text = sourceFile.getFullText();
|
|
1889
|
+
const cached = this.resolutionsByImporter.get(importer);
|
|
1890
|
+
if (cached?.sourceFile === sourceFile && cached.text === text && cached.treeRevision === this.#fileTreeRevision) return cached;
|
|
1891
|
+
const declarations = [...sourceFile.getImportDeclarations().map((declaration) => ({
|
|
1892
|
+
declaration,
|
|
1893
|
+
kind: "import"
|
|
1894
|
+
})), ...sourceFile.getExportDeclarations().map((declaration) => ({
|
|
1895
|
+
declaration,
|
|
1896
|
+
kind: "export"
|
|
1897
|
+
}))].filter(({ declaration }) => declaration.getModuleSpecifierValue() !== void 0).sort((left, right) => left.declaration.getStart() - right.declaration.getStart());
|
|
1898
|
+
const facts = [];
|
|
1899
|
+
const pendingCandidates = [];
|
|
1900
|
+
const configurationFiles = [];
|
|
1901
|
+
for (const [ordinal, { declaration, kind }] of declarations.entries()) {
|
|
1902
|
+
const specifier = declaration.getModuleSpecifierValue();
|
|
1903
|
+
if (!specifier) continue;
|
|
1904
|
+
const resolved = this.resolveSpecifier(specifier, sourceFile);
|
|
1905
|
+
if (!resolved.local) continue;
|
|
1906
|
+
facts.push(Object.freeze({
|
|
1907
|
+
importer,
|
|
1908
|
+
target: resolved.sourceFile ? this.normalizePath(resolved.sourceFile.getFilePath()) : null,
|
|
1909
|
+
specifier,
|
|
1910
|
+
kind,
|
|
1911
|
+
ordinal
|
|
1912
|
+
}));
|
|
1913
|
+
for (const target of resolved.pendingCandidates) pendingCandidates.push(Object.freeze({
|
|
1914
|
+
importer,
|
|
1915
|
+
target,
|
|
1916
|
+
specifier,
|
|
1917
|
+
kind,
|
|
1918
|
+
ordinal
|
|
1919
|
+
}));
|
|
1920
|
+
for (const target of resolved.configurationFiles) configurationFiles.push(Object.freeze({
|
|
1921
|
+
importer,
|
|
1922
|
+
target,
|
|
1923
|
+
specifier,
|
|
1924
|
+
kind,
|
|
1925
|
+
ordinal
|
|
1926
|
+
}));
|
|
1927
|
+
}
|
|
1928
|
+
this.publishResolutionFacts(importer, sourceFile, text, facts, pendingCandidates, configurationFiles);
|
|
1929
|
+
return this.resolutionsByImporter.get(importer);
|
|
1930
|
+
};
|
|
1931
|
+
/** The sole cross-file source resolver supplied to parser and extractor. */
|
|
1932
|
+
resolveModule = (specifier, from) => {
|
|
1933
|
+
const target = this.ensureResolutionFacts(from).facts.find((fact) => fact.specifier === specifier)?.target;
|
|
1934
|
+
if (!target) return;
|
|
1935
|
+
const sourceFile = this.project.getSourceFile(target);
|
|
1936
|
+
if (!sourceFile) return;
|
|
1937
|
+
this.prepareEffectiveSource(sourceFile.getFilePath(), sourceFile);
|
|
1938
|
+
return sourceFile;
|
|
1939
|
+
};
|
|
1439
1940
|
trackDependencies = (filePath, sourceFile) => {
|
|
1941
|
+
this.#assertNotLoading();
|
|
1440
1942
|
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
1441
1943
|
this.canonicalPaths.set(this.normalizePath(filePath), importer);
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1944
|
+
this.canonicalPaths.set(importer, importer);
|
|
1945
|
+
this.ensureResolutionFacts(sourceFile);
|
|
1946
|
+
};
|
|
1947
|
+
invalidateSourcePreparation = (filePath, sourceFile) => {
|
|
1948
|
+
const sourcePath = this.normalizePath(sourceFile?.getFilePath() ?? filePath);
|
|
1949
|
+
const preparation = this.sourcePreparations.get(sourcePath);
|
|
1950
|
+
if (preparation?.state === "preparing") preparation.invalidated = true;
|
|
1951
|
+
this.sourcePreparations.delete(sourcePath);
|
|
1952
|
+
this.retractImporter(sourcePath);
|
|
1953
|
+
};
|
|
1954
|
+
/**
|
|
1955
|
+
* Apply the parser hook and built-in transform once to one source revision.
|
|
1956
|
+
*
|
|
1957
|
+
* Resolver traversal is a semantic read just as surely as an explicit parse: returning a
|
|
1958
|
+
* raw dependency here would let parse order decide both the value extraction sees and the
|
|
1959
|
+
* downstream edges the ledger records. The transaction is published only after the AST and
|
|
1960
|
+
* its facts agree. A failed or re-entered attempt restores the input AST and can be retried.
|
|
1961
|
+
*/
|
|
1962
|
+
prepareEffectiveSource = (filePath, sourceFile) => {
|
|
1963
|
+
this.#assertNotLoading();
|
|
1964
|
+
const sourcePath = this.normalizePath(sourceFile.getFilePath());
|
|
1965
|
+
const currentText = sourceFile.getFullText();
|
|
1966
|
+
const current = this.sourcePreparations.get(sourcePath);
|
|
1967
|
+
if (current?.state === "ready" && current.sourceFile === sourceFile && current.effectiveText === currentText) {
|
|
1968
|
+
this.trackDependencies(filePath, sourceFile);
|
|
1969
|
+
return current;
|
|
1970
|
+
}
|
|
1971
|
+
if (current?.state === "preparing") {
|
|
1972
|
+
current.invalidated = true;
|
|
1973
|
+
throw new Error(`Project source is already being prepared: ${sourcePath}`);
|
|
1974
|
+
}
|
|
1975
|
+
this.sourcePreparations.delete(sourcePath);
|
|
1976
|
+
this.retractImporter(sourcePath);
|
|
1977
|
+
const transaction = {
|
|
1978
|
+
state: "preparing",
|
|
1979
|
+
sourceFile,
|
|
1980
|
+
inputText: currentText,
|
|
1981
|
+
invalidated: false
|
|
1982
|
+
};
|
|
1983
|
+
this.sourcePreparations.set(sourcePath, transaction);
|
|
1984
|
+
const assertTransaction = (expectedText) => {
|
|
1985
|
+
let actualText;
|
|
1986
|
+
try {
|
|
1987
|
+
actualText = sourceFile.getFullText();
|
|
1988
|
+
} catch {
|
|
1989
|
+
transaction.invalidated = true;
|
|
1990
|
+
actualText = "";
|
|
1451
1991
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
this.
|
|
1457
|
-
|
|
1992
|
+
if (transaction.invalidated || this.sourcePreparations.get(sourcePath) !== transaction || actualText !== expectedText) throw new Error(`Project source changed while it was being prepared: ${sourcePath}`);
|
|
1993
|
+
};
|
|
1994
|
+
const options = {};
|
|
1995
|
+
try {
|
|
1996
|
+
const custom = this.options.hooks["parser:before"]?.({
|
|
1997
|
+
filePath,
|
|
1998
|
+
content: currentText,
|
|
1999
|
+
configure(next) {
|
|
2000
|
+
const { matchTag, matchTagMode, matchTagProp } = next;
|
|
2001
|
+
if (matchTag) options.matchTag = matchTag;
|
|
2002
|
+
if (matchTagMode) options.matchTagMode = matchTagMode;
|
|
2003
|
+
if (matchTagProp) options.matchTagProp = matchTagProp;
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
assertTransaction(currentText);
|
|
2007
|
+
const transformed = custom ?? this.transformFile(filePath, currentText);
|
|
2008
|
+
assertTransaction(currentText);
|
|
2009
|
+
if (currentText !== transformed) sourceFile.replaceWithText(transformed);
|
|
2010
|
+
assertTransaction(transformed);
|
|
2011
|
+
this.trackDependencies(filePath, sourceFile);
|
|
2012
|
+
assertTransaction(transformed);
|
|
2013
|
+
const ready = Object.freeze({
|
|
2014
|
+
state: "ready",
|
|
2015
|
+
sourceFile,
|
|
2016
|
+
inputText: currentText,
|
|
2017
|
+
effectiveText: transformed,
|
|
2018
|
+
options: Object.freeze({ ...options })
|
|
2019
|
+
});
|
|
2020
|
+
this.sourcePreparations.set(sourcePath, ready);
|
|
2021
|
+
return ready;
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
if (this.sourcePreparations.get(sourcePath) === transaction) this.sourcePreparations.delete(sourcePath);
|
|
2024
|
+
this.retractImporter(sourcePath);
|
|
2025
|
+
try {
|
|
2026
|
+
if (sourceFile.getFullText() !== currentText) sourceFile.replaceWithText(currentText);
|
|
2027
|
+
} catch {}
|
|
2028
|
+
throw error;
|
|
1458
2029
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2030
|
+
};
|
|
2031
|
+
markTargetRemoved = (target, sourceFile) => {
|
|
2032
|
+
const importers = new Set(this.dependents.get(target) ?? []);
|
|
2033
|
+
if (importers.size > 0) this.removedDependents.set(target, importers);
|
|
2034
|
+
for (const importer of importers) {
|
|
2035
|
+
const entry = this.resolutionsByImporter.get(importer);
|
|
2036
|
+
if (!entry) continue;
|
|
2037
|
+
const facts = entry.facts.map((fact) => fact.target === target ? Object.freeze({
|
|
2038
|
+
...fact,
|
|
2039
|
+
target: null
|
|
2040
|
+
}) : fact);
|
|
2041
|
+
this.dependencies.get(importer)?.delete(target);
|
|
2042
|
+
if (facts.some((fact) => fact.target === null)) this.unresolvedImporters.add(importer);
|
|
2043
|
+
this.resolutionsByImporter.set(importer, {
|
|
2044
|
+
...entry,
|
|
2045
|
+
facts: Object.freeze(facts),
|
|
2046
|
+
hasPendingLocalCandidate: true
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
this.dependents.delete(target);
|
|
2050
|
+
this.retractImporter(target);
|
|
2051
|
+
this.removedSourcePaths.add(target);
|
|
2052
|
+
this.canonicalPaths.set(target, target);
|
|
2053
|
+
this.canonicalPaths.set(this.normalizePath(sourceFile.getFilePath()), target);
|
|
1462
2054
|
};
|
|
1463
2055
|
/**
|
|
1464
2056
|
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
|
1465
2057
|
* consumers of an edited file. Excludes `filePath` itself.
|
|
1466
2058
|
*/
|
|
1467
2059
|
getDependents = (filePath) => {
|
|
2060
|
+
this.#assertNotLoading();
|
|
1468
2061
|
const given = this.normalizePath(filePath);
|
|
1469
2062
|
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
1470
2063
|
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
@@ -1472,23 +2065,139 @@ var Project = class {
|
|
|
1472
2065
|
const queue = [start];
|
|
1473
2066
|
while (queue.length) {
|
|
1474
2067
|
const current = queue.shift();
|
|
1475
|
-
|
|
2068
|
+
const importers = new Set([...this.dependents.get(current) ?? [], ...this.removedDependents.get(current) ?? []]);
|
|
2069
|
+
for (const importer of [...importers].sort()) {
|
|
1476
2070
|
if (importer === start || seen.has(importer)) continue;
|
|
1477
2071
|
seen.add(importer);
|
|
1478
2072
|
queue.push(importer);
|
|
1479
2073
|
}
|
|
1480
2074
|
}
|
|
1481
|
-
return [...seen];
|
|
2075
|
+
return [...seen].sort();
|
|
2076
|
+
};
|
|
2077
|
+
/**
|
|
2078
|
+
* Every local source transitively read by `filePath`, excluding the file itself. When
|
|
2079
|
+
* `targets` are supplied, retain only paths leading to one of those semantic reads.
|
|
2080
|
+
*
|
|
2081
|
+
* This is the forward half of `getDependents`, exposed for bundler consumers which must
|
|
2082
|
+
* register the complete semantic read-set as watch files. Walking the indexed ledger closure
|
|
2083
|
+
* avoids rescanning every resolution fact once per transformed module.
|
|
2084
|
+
*/
|
|
2085
|
+
getDependencies = (filePath, targets) => {
|
|
2086
|
+
this.#assertNotLoading();
|
|
2087
|
+
const given = this.normalizePath(filePath);
|
|
2088
|
+
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
2089
|
+
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
2090
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2091
|
+
const importersByDependency = /* @__PURE__ */ new Map();
|
|
2092
|
+
const queue = [start];
|
|
2093
|
+
while (queue.length) {
|
|
2094
|
+
const current = queue.shift();
|
|
2095
|
+
for (const dependency of [...this.dependencies.get(current) ?? []].sort()) {
|
|
2096
|
+
const importers = importersByDependency.get(dependency) ?? /* @__PURE__ */ new Set();
|
|
2097
|
+
importers.add(current);
|
|
2098
|
+
importersByDependency.set(dependency, importers);
|
|
2099
|
+
if (dependency === start || seen.has(dependency)) continue;
|
|
2100
|
+
seen.add(dependency);
|
|
2101
|
+
queue.push(dependency);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (targets === void 0) return [...seen].sort();
|
|
2105
|
+
const selected = /* @__PURE__ */ new Set();
|
|
2106
|
+
const reverse = Array.from(targets, (target) => {
|
|
2107
|
+
const normalized = this.normalizePath(target);
|
|
2108
|
+
const source = this.project.getSourceFile(target)?.getFilePath();
|
|
2109
|
+
return source ? this.normalizePath(source) : this.canonicalPaths.get(normalized) ?? normalized;
|
|
2110
|
+
}).filter((target) => seen.has(target));
|
|
2111
|
+
while (reverse.length) {
|
|
2112
|
+
const dependency = reverse.shift();
|
|
2113
|
+
if (selected.has(dependency)) continue;
|
|
2114
|
+
selected.add(dependency);
|
|
2115
|
+
for (const importer of importersByDependency.get(dependency) ?? []) if (importer !== start && !selected.has(importer)) reverse.push(importer);
|
|
2116
|
+
}
|
|
2117
|
+
return [...selected].sort();
|
|
2118
|
+
};
|
|
2119
|
+
/**
|
|
2120
|
+
* Exact semantic dependencies plus missing local resolver candidates which can supersede
|
|
2121
|
+
* one of those dependencies.
|
|
2122
|
+
*
|
|
2123
|
+
* Candidate provenance stays attached to its import/export fact. Filtering those facts
|
|
2124
|
+
* through the selected dependency closure keeps an unrelated runtime import—even a local
|
|
2125
|
+
* alias with its own fallback—out of the watch set without asking consumers to reinterpret
|
|
2126
|
+
* specifiers or scan the checkout.
|
|
2127
|
+
*/
|
|
2128
|
+
getResolutionReadSet = (filePath, targets, previous) => {
|
|
2129
|
+
const dependencies = this.getDependencies(filePath, targets);
|
|
2130
|
+
const selected = new Set(dependencies);
|
|
2131
|
+
const retained = new Set([...previous?.dependencies ?? [], ...previous?.pendingCandidates ?? []]);
|
|
2132
|
+
const given = this.normalizePath(filePath);
|
|
2133
|
+
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
2134
|
+
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
2135
|
+
const pendingCandidates = /* @__PURE__ */ new Set();
|
|
2136
|
+
for (const importer of [start, ...dependencies]) {
|
|
2137
|
+
const resolution = this.resolutionsByImporter.get(importer);
|
|
2138
|
+
if (!resolution) continue;
|
|
2139
|
+
const semanticOrdinals = new Set(resolution.facts.filter((fact) => fact.target !== null && selected.has(fact.target)).map((fact) => fact.ordinal));
|
|
2140
|
+
for (const fact of resolution.facts) {
|
|
2141
|
+
if (fact.target !== null) continue;
|
|
2142
|
+
if (resolution.pendingCandidates.some((candidate) => candidate.ordinal === fact.ordinal && retained.has(candidate.target))) semanticOrdinals.add(fact.ordinal);
|
|
2143
|
+
}
|
|
2144
|
+
for (const candidate of resolution.pendingCandidates) if (semanticOrdinals.has(candidate.ordinal)) pendingCandidates.add(candidate.target);
|
|
2145
|
+
}
|
|
2146
|
+
return Object.freeze({
|
|
2147
|
+
dependencies: Object.freeze([...dependencies]),
|
|
2148
|
+
pendingCandidates: Object.freeze([...pendingCandidates].sort())
|
|
2149
|
+
});
|
|
2150
|
+
};
|
|
2151
|
+
/**
|
|
2152
|
+
* Exact resolution configuration files read by the semantic closure selected by `targets`.
|
|
2153
|
+
*
|
|
2154
|
+
* Package manifests stay attached to their import/export ordinal, so a runtime-only branch
|
|
2155
|
+
* cannot turn an arbitrary package.json into a Builder dependency. Tsconfig files are global
|
|
2156
|
+
* inputs to those same selected facts and are added only when the owner has a semantic
|
|
2157
|
+
* cross-file read.
|
|
2158
|
+
*
|
|
2159
|
+
* @internal
|
|
2160
|
+
*/
|
|
2161
|
+
getResolutionConfigurationFiles = (filePath, targets, previous = []) => {
|
|
2162
|
+
const dependencies = this.getDependencies(filePath, targets);
|
|
2163
|
+
const selected = new Set(dependencies);
|
|
2164
|
+
const retained = new Set(Array.from(previous, (file) => this.normalizePath(file)));
|
|
2165
|
+
const given = this.normalizePath(filePath);
|
|
2166
|
+
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
2167
|
+
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
2168
|
+
const files = /* @__PURE__ */ new Set();
|
|
2169
|
+
let hasSemanticFact = false;
|
|
2170
|
+
for (const importer of [start, ...dependencies]) {
|
|
2171
|
+
const resolution = this.resolutionsByImporter.get(importer);
|
|
2172
|
+
if (!resolution) continue;
|
|
2173
|
+
const semanticOrdinals = new Set(resolution.facts.filter((fact) => fact.target !== null && selected.has(fact.target)).map((fact) => fact.ordinal));
|
|
2174
|
+
for (const fact of resolution.facts) {
|
|
2175
|
+
if (fact.target !== null) continue;
|
|
2176
|
+
if (resolution.configurationFiles.some((configuration) => configuration.ordinal === fact.ordinal && retained.has(configuration.target))) semanticOrdinals.add(fact.ordinal);
|
|
2177
|
+
}
|
|
2178
|
+
if (semanticOrdinals.size > 0) hasSemanticFact = true;
|
|
2179
|
+
for (const configuration of resolution.configurationFiles) if (semanticOrdinals.has(configuration.ordinal)) files.add(configuration.target);
|
|
2180
|
+
}
|
|
2181
|
+
if (hasSemanticFact) for (const file of this.options.resolutionConfigFiles ?? []) files.add(this.normalizePath(file));
|
|
2182
|
+
return Object.freeze([...files].sort());
|
|
1482
2183
|
};
|
|
1483
2184
|
createSourceFile = (filePath) => {
|
|
2185
|
+
this.#assertNotLoading();
|
|
2186
|
+
this.#ensureSourceFiles();
|
|
1484
2187
|
const { readFile } = this.options;
|
|
2188
|
+
const content = readFile(filePath);
|
|
2189
|
+
const existing = this.project.getSourceFile(filePath);
|
|
2190
|
+
this.invalidateSourcePreparation(filePath, existing);
|
|
2191
|
+
this.removedSourcePaths.delete(this.normalizePath(filePath));
|
|
1485
2192
|
this.invalidate();
|
|
1486
|
-
return this.project.createSourceFile(filePath,
|
|
2193
|
+
return this.project.createSourceFile(filePath, content, {
|
|
1487
2194
|
overwrite: true,
|
|
1488
2195
|
scriptKind: scriptKindFor(filePath)
|
|
1489
2196
|
});
|
|
1490
2197
|
};
|
|
1491
2198
|
createSourceFiles = () => {
|
|
2199
|
+
this.#assertNotLoading();
|
|
2200
|
+
this.#ensureSourceFiles();
|
|
1492
2201
|
const files = this.getFiles();
|
|
1493
2202
|
for (const file of files) try {
|
|
1494
2203
|
this.createSourceFile(file);
|
|
@@ -1497,6 +2206,8 @@ var Project = class {
|
|
|
1497
2206
|
}
|
|
1498
2207
|
};
|
|
1499
2208
|
addSourceFile = (filePath, content) => {
|
|
2209
|
+
this.#assertNotLoading();
|
|
2210
|
+
this.#ensureSourceFiles();
|
|
1500
2211
|
const existing = filePath.includes("/") ? this.project.getSourceFile(filePath) : void 0;
|
|
1501
2212
|
/**
|
|
1502
2213
|
* Re-adding a file the text it already holds is a no-op, and saying so is what makes the
|
|
@@ -1521,6 +2232,8 @@ var Project = class {
|
|
|
1521
2232
|
* matches its own source, falls through, and is overwritten exactly as before.
|
|
1522
2233
|
*/
|
|
1523
2234
|
if (existing && existing.getFullText() === content) return existing;
|
|
2235
|
+
this.invalidateSourcePreparation(filePath, existing);
|
|
2236
|
+
this.removedSourcePaths.delete(this.normalizePath(filePath));
|
|
1524
2237
|
this.invalidate(!existing);
|
|
1525
2238
|
return this.project.createSourceFile(filePath, content, {
|
|
1526
2239
|
overwrite: true,
|
|
@@ -1528,54 +2241,69 @@ var Project = class {
|
|
|
1528
2241
|
});
|
|
1529
2242
|
};
|
|
1530
2243
|
removeSourceFile = (filePath) => {
|
|
2244
|
+
this.#assertNotLoading();
|
|
2245
|
+
this.#ensureSourceFiles();
|
|
1531
2246
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
1532
2247
|
if (sourceFile) {
|
|
1533
2248
|
this.invalidate();
|
|
2249
|
+
this.invalidateSourcePreparation(filePath, sourceFile);
|
|
2250
|
+
this.markTargetRemoved(this.normalizePath(sourceFile.getFilePath()), sourceFile);
|
|
1534
2251
|
this.options.parserOptions.encoder.releaseFile(sourceFile.getFilePath());
|
|
1535
2252
|
return this.project.removeSourceFile(sourceFile);
|
|
1536
2253
|
}
|
|
1537
2254
|
return false;
|
|
1538
2255
|
};
|
|
1539
2256
|
reloadSourceFile = (filePath) => {
|
|
2257
|
+
this.#assertNotLoading();
|
|
2258
|
+
this.#ensureSourceFiles();
|
|
1540
2259
|
this.invalidate(false);
|
|
1541
|
-
|
|
2260
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
2261
|
+
if (!sourceFile) return;
|
|
2262
|
+
this.invalidateSourcePreparation(filePath, sourceFile);
|
|
2263
|
+
return sourceFile.refreshFromFileSystemSync();
|
|
1542
2264
|
};
|
|
1543
2265
|
reloadSourceFiles = () => {
|
|
2266
|
+
this.#assertNotLoading();
|
|
2267
|
+
this.#ensureSourceFiles();
|
|
1544
2268
|
const files = this.getFiles();
|
|
1545
2269
|
this.invalidate();
|
|
1546
|
-
for (const file of files)
|
|
2270
|
+
for (const file of files) {
|
|
2271
|
+
const source = this.getSourceFile(file);
|
|
2272
|
+
if (source) {
|
|
2273
|
+
this.invalidateSourcePreparation(file, source);
|
|
2274
|
+
source.refreshFromFileSystemSync();
|
|
2275
|
+
} else {
|
|
2276
|
+
this.invalidateSourcePreparation(file);
|
|
2277
|
+
this.removedSourcePaths.delete(this.normalizePath(file));
|
|
2278
|
+
this.project.createSourceFile(file, this.options.readFile(file), {
|
|
2279
|
+
overwrite: true,
|
|
2280
|
+
scriptKind: scriptKindFor(file)
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
1547
2284
|
};
|
|
1548
2285
|
get readFile() {
|
|
2286
|
+
this.#assertNotLoading();
|
|
1549
2287
|
return this.options.readFile;
|
|
1550
2288
|
}
|
|
1551
2289
|
get getFiles() {
|
|
2290
|
+
this.#assertNotLoading();
|
|
1552
2291
|
return this.options.getFiles;
|
|
1553
2292
|
}
|
|
1554
2293
|
parseJson = (filePath) => {
|
|
2294
|
+
this.#assertNotLoading();
|
|
1555
2295
|
const { readFile, parserOptions } = this.options;
|
|
1556
2296
|
const content = readFile(filePath);
|
|
1557
2297
|
parserOptions.encoder.fromJSON(JSON.parse(content));
|
|
1558
2298
|
return new ParserResult(parserOptions).setFilePath(filePath);
|
|
1559
2299
|
};
|
|
1560
2300
|
parseSourceFile = (filePath, encoder) => {
|
|
2301
|
+
this.#assertNotLoading();
|
|
1561
2302
|
const { hooks } = this.options;
|
|
1562
2303
|
if (filePath.endsWith(".json")) return this.parseJson(filePath);
|
|
1563
2304
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
1564
2305
|
if (!sourceFile) return;
|
|
1565
|
-
this.
|
|
1566
|
-
const original = sourceFile.getText();
|
|
1567
|
-
const options = {};
|
|
1568
|
-
const transformed = hooks["parser:before"]?.({
|
|
1569
|
-
filePath,
|
|
1570
|
-
content: original,
|
|
1571
|
-
configure(opts) {
|
|
1572
|
-
const { matchTag, matchTagMode, matchTagProp } = opts;
|
|
1573
|
-
if (matchTag) options.matchTag = matchTag;
|
|
1574
|
-
if (matchTagMode) options.matchTagMode = matchTagMode;
|
|
1575
|
-
if (matchTagProp) options.matchTagProp = matchTagProp;
|
|
1576
|
-
}
|
|
1577
|
-
}) ?? this.transformFile(filePath, original);
|
|
1578
|
-
if (original !== transformed) sourceFile.replaceWithText(transformed);
|
|
2306
|
+
const { options } = this.prepareEffectiveSource(filePath, sourceFile);
|
|
1579
2307
|
const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, options, this.resolveModule))?.setFilePath(filePath);
|
|
1580
2308
|
hooks["parser:after"]?.({
|
|
1581
2309
|
filePath,
|
|
@@ -1584,9 +2312,11 @@ var Project = class {
|
|
|
1584
2312
|
return result;
|
|
1585
2313
|
};
|
|
1586
2314
|
transformFile = (_filePath, content) => {
|
|
2315
|
+
this.#assertNotLoading();
|
|
1587
2316
|
return content;
|
|
1588
2317
|
};
|
|
1589
2318
|
classify = (fileMap) => {
|
|
2319
|
+
this.#assertNotLoading();
|
|
1590
2320
|
const { parserOptions } = this.options;
|
|
1591
2321
|
return classifyProject(parserOptions, fileMap);
|
|
1592
2322
|
};
|