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