@bamboocss/parser 1.49.0 → 1.50.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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Node, Project as Project$1, ScriptKind, ts } from "ts-morph";
1
+ import { Node, Project as Project$1, ScriptKind, childOf, createResolver, getAliasNode, getExportDeclarations, getImportDeclarations, getLineAndColumnAtPos, getModuleSpecifierValue, getName, getNamedExports, getNamedImports, getNamespaceExport, getNamespaceImport, isStarExport, isTypeOnly, nameNodeOf, pathOf, ts } from "@bamboocss/ts-ast";
2
2
  import { box, clearBoxNodeCache, extract, getExportedVarDeclarationWithName, invalidateDependencyPath, maybeBoxNode, unbox, unwrapExpression } from "@bamboocss/extractor";
3
3
  import { BambooError, compact, createPatternFns, getOrCreateSet } from "@bamboocss/shared";
4
4
  import { createHash } from "node:crypto";
@@ -410,20 +410,6 @@ function createReportMaps() {
410
410
  };
411
411
  }
412
412
  //#endregion
413
- //#region src/get-module-specifier-value.ts
414
- /**
415
- * Both declaration kinds carry a specifier, and `export { x } from './m'` is how a barrel
416
- * forwards a recipe — so this reads either. An `export { x }` with no `from` returns
417
- * undefined, which is the same answer the throwing case gives.
418
- */
419
- const getModuleSpecifierValue = (node) => {
420
- try {
421
- return node.getModuleSpecifierValue();
422
- } catch {
423
- return;
424
- }
425
- };
426
- //#endregion
427
413
  //#region src/imported-recipes.ts
428
414
  /**
429
415
  * A module's exported recipes, memoized.
@@ -439,15 +425,15 @@ const clearImportedRecipeCache = () => {
439
425
  /** The names a file bound `cva`/`sva` to, or empty when it imports neither. */
440
426
  const recipeFactoryAliases = (sourceFile, imports) => {
441
427
  const aliases = /* @__PURE__ */ new Set();
442
- for (const declaration of sourceFile.getImportDeclarations()) {
443
- if (declaration.isTypeOnly()) continue;
428
+ for (const declaration of getImportDeclarations(sourceFile)) {
429
+ if (isTypeOnly(declaration)) continue;
444
430
  const mod = getModuleSpecifierValue(declaration);
445
431
  if (!mod) continue;
446
- for (const specifier of declaration.getNamedImports()) {
447
- if (specifier.isTypeOnly()) continue;
448
- const name = specifier.getNameNode().getText();
432
+ for (const specifier of getNamedImports(declaration)) {
433
+ if (isTypeOnly(specifier)) continue;
434
+ const name = nameNodeOf(specifier)?.getText();
449
435
  if (name !== "cva" && name !== "sva") continue;
450
- const alias = specifier.getAliasNode()?.getText() || name;
436
+ const alias = getAliasNode(specifier)?.getText() || name;
451
437
  if (!imports.match({
452
438
  name,
453
439
  alias,
@@ -468,8 +454,8 @@ const declaredRecipes = (sourceFile, imports) => {
468
454
  declared,
469
455
  exported
470
456
  };
471
- const filePath = sourceFile.getFilePath();
472
- for (const statement of sourceFile.compilerNode.statements) {
457
+ const filePath = pathOf(sourceFile);
458
+ for (const statement of sourceFile.statements) {
473
459
  if (!ts.isVariableStatement(statement)) continue;
474
460
  if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
475
461
  for (const declaration of statement.declarationList.declarations) {
@@ -529,13 +515,13 @@ const walkExports = (sourceFile, imports, resolveModule, seen) => {
529
515
  const imported = walkImports(sourceFile, imports, resolveModule, seen);
530
516
  complete &&= imported.complete;
531
517
  for (const [alias, origin] of imported.bindings) if (!local.has(alias)) local.set(alias, origin);
532
- for (const declaration of sourceFile.getExportDeclarations()) {
533
- if (declaration.isTypeOnly()) continue;
518
+ for (const declaration of getExportDeclarations(sourceFile)) {
519
+ if (isTypeOnly(declaration)) continue;
534
520
  const specifier = getModuleSpecifierValue(declaration);
535
521
  const target = specifier ? resolveModule(specifier, sourceFile) : void 0;
536
522
  if (specifier && !target) continue;
537
- if (declaration.getNamespaceExport()) continue;
538
- if (declaration.isNamespaceExport()) {
523
+ if (getNamespaceExport(declaration)) continue;
524
+ if (isStarExport(declaration)) {
539
525
  if (!target) continue;
540
526
  const walk = walkExports(target, imports, resolveModule, seen);
541
527
  complete &&= walk.complete;
@@ -555,11 +541,11 @@ const walkExports = (sourceFile, imports, resolveModule, seen) => {
555
541
  complete &&= walk.complete;
556
542
  source = walk.names;
557
543
  }
558
- for (const exportSpecifier of declaration.getNamedExports()) {
559
- if (exportSpecifier.isTypeOnly()) continue;
560
- const name = exportSpecifier.getNameNode().getText();
561
- const origin = source.get(name);
562
- if (origin) names.set(exportSpecifier.getAliasNode()?.getText() || name, origin);
544
+ for (const exportSpecifier of getNamedExports(declaration)) {
545
+ if (isTypeOnly(exportSpecifier)) continue;
546
+ const name = nameNodeOf(exportSpecifier)?.getText();
547
+ const origin = source.get(name ?? "");
548
+ if (origin) names.set(getAliasNode(exportSpecifier)?.getText() || (name ?? ""), origin);
563
549
  }
564
550
  }
565
551
  for (const [name, origin] of starred) if (!names.has(name) && !ambiguous.has(name)) names.set(name, origin);
@@ -579,9 +565,9 @@ const walkExports = (sourceFile, imports, resolveModule, seen) => {
579
565
  const walkImports = (sourceFile, imports, resolveModule, seen) => {
580
566
  const bindings = /* @__PURE__ */ new Map();
581
567
  let complete = true;
582
- for (const declaration of sourceFile.getImportDeclarations()) {
583
- if (declaration.isTypeOnly()) continue;
584
- const named = declaration.getNamedImports();
568
+ for (const declaration of getImportDeclarations(sourceFile)) {
569
+ if (isTypeOnly(declaration)) continue;
570
+ const named = getNamedImports(declaration);
585
571
  if (named.length === 0) continue;
586
572
  const specifier = getModuleSpecifierValue(declaration);
587
573
  if (!specifier) continue;
@@ -592,10 +578,10 @@ const walkImports = (sourceFile, imports, resolveModule, seen) => {
592
578
  const { names } = walk;
593
579
  if (names.size === 0) continue;
594
580
  for (const importSpecifier of named) {
595
- if (importSpecifier.isTypeOnly()) continue;
596
- const origin = names.get(importSpecifier.getNameNode().getText());
581
+ if (isTypeOnly(importSpecifier)) continue;
582
+ const origin = names.get(nameNodeOf(importSpecifier)?.getText() ?? "");
597
583
  if (!origin) continue;
598
- bindings.set(importSpecifier.getAliasNode()?.getText() || importSpecifier.getNameNode().getText(), origin);
584
+ bindings.set(getAliasNode(importSpecifier)?.getText() || (nameNodeOf(importSpecifier)?.getText() ?? ""), origin);
599
585
  }
600
586
  }
601
587
  return {
@@ -644,7 +630,7 @@ const digestExportValue = (sourceFile, exportedName, resolveModule, onCrossing)
644
630
  try {
645
631
  const declaration = getExportedVarDeclarationWithName(exportedName, sourceFile, [], boxCtx);
646
632
  if (!declaration) return "bamboo:export-missing";
647
- const initializer = declaration.getInitializer?.();
633
+ const initializer = childOf(declaration, "initializer");
648
634
  if (!initializer) return void 0;
649
635
  const box = maybeBoxNode(initializer, [], boxCtx);
650
636
  if (!box || Array.isArray(box)) return void 0;
@@ -659,17 +645,18 @@ const digestExportValue = (sourceFile, exportedName, resolveModule, onCrossing)
659
645
  };
660
646
  //#endregion
661
647
  //#region src/get-import-declarations.ts
662
- function getImportDeclarations(context, sourceFile) {
648
+ function getImportDeclarations$1(context, sourceFile) {
663
649
  const { imports, tsOptions } = context;
664
650
  const importDeclarations = [];
665
- sourceFile.getImportDeclarations().forEach((node) => {
651
+ getImportDeclarations(sourceFile).forEach((node) => {
666
652
  const mod = getModuleSpecifierValue(node);
667
653
  if (!mod) return;
668
- node.getNamedImports().forEach((specifier) => {
669
- const name = specifier.getNameNode().getText();
654
+ getNamedImports(node).forEach((specifier) => {
655
+ const name = nameNodeOf(specifier)?.getText();
656
+ const alias = getAliasNode(specifier)?.getText() || name;
670
657
  const result = {
671
- name,
672
- alias: specifier.getAliasNode()?.getText() || name,
658
+ name: name ?? "",
659
+ alias: alias ?? "",
673
660
  mod,
674
661
  kind: "named"
675
662
  };
@@ -679,7 +666,7 @@ function getImportDeclarations(context, sourceFile) {
679
666
  })) return;
680
667
  importDeclarations.push(result);
681
668
  });
682
- const namespace = node.getNamespaceImport();
669
+ const namespace = getNamespaceImport(node);
683
670
  if (namespace) {
684
671
  const name = namespace.getText();
685
672
  const result = {
@@ -740,16 +727,16 @@ const findUnresolvable = (node, path, out, seen = /* @__PURE__ */ new Set()) =>
740
727
  const writtenProps = (node) => {
741
728
  let literal = node;
742
729
  if (literal && Node.isCallExpression(literal)) {
743
- const args = literal.getArguments();
730
+ const args = literal.arguments;
744
731
  if (args.length !== 1) return void 0;
745
732
  literal = args[0];
746
733
  }
747
734
  if (!literal || !Node.isObjectLiteralExpression(literal)) return void 0;
748
735
  const names = [];
749
736
  let uncertain = false;
750
- for (const property of literal.getProperties()) {
737
+ for (const property of literal.properties) {
751
738
  if (Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property)) {
752
- const name = property.getName();
739
+ const name = getName(property) ?? "";
753
740
  if (name.startsWith("[")) {
754
741
  uncertain = true;
755
742
  continue;
@@ -786,7 +773,7 @@ const findUnresolvedInValue = (node, resolved, path, out) => {
786
773
  const value = node ? unwrapExpression(node) : void 0;
787
774
  if (!value) return;
788
775
  if (Node.isArrayLiteralExpression(value)) {
789
- value.getElements().forEach((element, index) => {
776
+ value.elements.forEach((element, index) => {
790
777
  const at = path ? `${path}.${index}` : String(index);
791
778
  const element_ = resolved?.[index];
792
779
  if (Node.isSpreadElement(element) && element_ == null) {
@@ -801,16 +788,16 @@ const findUnresolvedInValue = (node, resolved, path, out) => {
801
788
  return;
802
789
  }
803
790
  if (!Node.isObjectLiteralExpression(value)) return;
804
- const properties = value.getProperties();
791
+ const properties = value.properties;
805
792
  const written = [];
806
793
  let uncertain = false;
807
794
  for (const property of properties) {
808
795
  if (Node.isSpreadAssignment(property)) {
809
- const spread = unwrapExpression(property.getExpression());
796
+ const spread = unwrapExpression(property.expression);
810
797
  if (Node.isObjectLiteralExpression(spread)) {
811
- for (const inner of spread.getProperties()) {
798
+ for (const inner of spread.properties) {
812
799
  if (!Node.isPropertyAssignment(inner) && !Node.isShorthandPropertyAssignment(inner)) continue;
813
- const innerName = inner.getName();
800
+ const innerName = getName(inner) ?? "";
814
801
  if (!innerName.startsWith("[")) written.push(innerName.replace(/^['"]|['"]$/g, ""));
815
802
  }
816
803
  continue;
@@ -819,7 +806,7 @@ const findUnresolvedInValue = (node, resolved, path, out) => {
819
806
  continue;
820
807
  }
821
808
  if (!Node.isPropertyAssignment(property) && !Node.isShorthandPropertyAssignment(property)) continue;
822
- const name = property.getName();
809
+ const name = getName(property) ?? "";
823
810
  if (name.startsWith("[")) {
824
811
  uncertain = true;
825
812
  continue;
@@ -837,9 +824,9 @@ const findUnresolvedInValue = (node, resolved, path, out) => {
837
824
  });
838
825
  for (const property of properties) {
839
826
  if (!Node.isPropertyAssignment(property)) continue;
840
- const name = property.getName().replace(/^['"]|['"]$/g, "");
827
+ const name = (getName(property) ?? "").replace(/^['"]|['"]$/g, "");
841
828
  if (name.startsWith("[")) continue;
842
- findUnresolvedInValue(property.getInitializer(), resolved?.[name], path ? `${path}.${name}` : name, out);
829
+ findUnresolvedInValue(property.initializer, resolved?.[name], path ? `${path}.${name}` : name, out);
843
830
  }
844
831
  };
845
832
  /** A recipe config the build could not fully read — see `findUnresolvedInValue`. */
@@ -849,7 +836,7 @@ const findUnresolvedRecipeStyles = (item) => {
849
836
  if (!node || !sourceFile) return [];
850
837
  let argument = node;
851
838
  if (Node.isCallExpression(argument)) {
852
- const args = argument.getArguments();
839
+ const args = argument.arguments;
853
840
  if (args.length !== 1) return [];
854
841
  argument = args[0];
855
842
  }
@@ -858,10 +845,10 @@ const findUnresolvedRecipeStyles = (item) => {
858
845
  if (!config || !Node.isObjectLiteralExpression(config)) losses.push({ reason: "unresolvable-value" });
859
846
  else findUnresolvedInValue(config, item.data[0], "", losses);
860
847
  if (!losses.length) return [];
861
- const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
848
+ const { line, column } = getLineAndColumnAtPos(sourceFile, node.getStart());
862
849
  return losses.map((loss) => ({
863
850
  column,
864
- filePath: sourceFile.getFilePath(),
851
+ filePath: pathOf(sourceFile),
865
852
  kind: "recipe",
866
853
  line,
867
854
  ...loss
@@ -891,9 +878,9 @@ const findUnresolvedStyles = (item, kind) => {
891
878
  if (written.uncertain && !hasKeyOutside(resolved, written.names)) losses.push({ reason: "unenumerable-keys" });
892
879
  }
893
880
  if (!losses.length) return [];
894
- const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
881
+ const { line, column } = getLineAndColumnAtPos(sourceFile, node.getStart());
895
882
  const at = {
896
- filePath: sourceFile.getFilePath(),
883
+ filePath: pathOf(sourceFile),
897
884
  line,
898
885
  column
899
886
  };
@@ -1119,7 +1106,7 @@ var ParserResult = class {
1119
1106
  const visit = (node) => {
1120
1107
  if (!node || seen.has(node)) return;
1121
1108
  seen.add(node);
1122
- const path = node.getNode?.()?.getSourceFile().getFilePath().replaceAll("\\", "/");
1109
+ const path = pathOf(node.getNode?.()?.getSourceFile())?.replaceAll("\\", "/");
1123
1110
  if (path && path !== own) paths.add(path);
1124
1111
  if (box.isMap(node)) for (const child of node.value.values()) visit(child);
1125
1112
  else if (box.isArray(node)) for (const child of node.value) visit(child);
@@ -1174,8 +1161,8 @@ const combineResult = (unboxed) => {
1174
1161
  /** The exact binding token accounting visits for a token call. */
1175
1162
  const tokenCalleeRange = (call) => {
1176
1163
  if (!Node.isCallExpression(call)) return void 0;
1177
- let current = call.getExpression();
1178
- while (Node.isPropertyAccessExpression(current)) current = current.getExpression();
1164
+ let current = call.expression;
1165
+ while (Node.isPropertyAccessExpression(current)) current = current.expression;
1179
1166
  if (!Node.isIdentifier(current)) return void 0;
1180
1167
  return {
1181
1168
  start: current.getStart(),
@@ -1195,16 +1182,16 @@ function createParser(context) {
1195
1182
  const { jsx, imports, recipes } = context;
1196
1183
  return function parse(sourceFile, encoder, options, resolveModule) {
1197
1184
  if (!sourceFile) return;
1198
- const importDeclarations = getImportDeclarations(context, sourceFile);
1185
+ const importDeclarations = getImportDeclarations$1(context, sourceFile);
1199
1186
  const file = imports.file(importDeclarations);
1200
- const filePath = sourceFile.getFilePath();
1187
+ const filePath = pathOf(sourceFile);
1201
1188
  logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
1202
1189
  const parserResult = new ParserResult(context, encoder);
1203
1190
  const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
1204
1191
  if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
1205
1192
  parserResult.importedRecipes = importedRecipes;
1206
1193
  for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
1207
- if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
1194
+ if (file.importsRecipeFactory()) for (const statement of sourceFile.statements) {
1208
1195
  if (!ts.isVariableStatement(statement)) continue;
1209
1196
  if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
1210
1197
  for (const declaration of statement.declarationList.declarations) {
@@ -1229,7 +1216,7 @@ function createParser(context) {
1229
1216
  * Defined out here rather than inside `getEvaluateOptions`, which runs per call node.
1230
1217
  */
1231
1218
  const reportUnresolvedRaw = (base, at) => {
1232
- const { line, column } = sourceFile.getLineAndColumnAtPos(at.getStart());
1219
+ const { line, column } = getLineAndColumnAtPos(sourceFile, at.getStart());
1233
1220
  parserResult.unresolved.push({
1234
1221
  kind: "atomic",
1235
1222
  prop: base,
@@ -1272,7 +1259,7 @@ function createParser(context) {
1272
1259
  },
1273
1260
  getEvaluateOptions: (node) => {
1274
1261
  if (!Node.isCallExpression(node)) return evaluateOptions;
1275
- const propAccessExpr = node.getExpression();
1262
+ const propAccessExpr = node.expression;
1276
1263
  if (Node.isIdentifier(propAccessExpr)) {
1277
1264
  const local = propAccessExpr.getText();
1278
1265
  if (!file.match(local) || file.getName(local) !== "fallback") return evaluateOptions;
@@ -1347,7 +1334,7 @@ function createParser(context) {
1347
1334
  });
1348
1335
  }).when(file.isValidPattern, (name) => {
1349
1336
  result.queryList.forEach((query) => {
1350
- if (query.kind === "call-expression") parserResult.setPattern(name, {
1337
+ if (query.kind === "call-expression") parserResult.setPattern(name ?? "", {
1351
1338
  name,
1352
1339
  box: query.box.value[0] ?? box.fallback(query.box),
1353
1340
  data: combineResult(unbox(query.box.value[0]))
@@ -1355,7 +1342,7 @@ function createParser(context) {
1355
1342
  });
1356
1343
  }).when(file.isValidRecipe, (name) => {
1357
1344
  result.queryList.forEach((query) => {
1358
- if (query.kind === "call-expression") parserResult.setRecipe(name, {
1345
+ if (query.kind === "call-expression") parserResult.setRecipe(name ?? "", {
1359
1346
  name,
1360
1347
  box: query.box.value[0] ?? box.fallback(query.box),
1361
1348
  data: combineResult(unbox(query.box.value[0]))
@@ -1373,8 +1360,8 @@ function createParser(context) {
1373
1360
  } else if (jsx.isEnabled && result.kind === "component") result.queryList.forEach((query) => {
1374
1361
  const data = combineResult(unbox(query.box));
1375
1362
  for (const tag of [name, alias]) {
1376
- if (!jsx.isJsxTagRecipe(tag)) continue;
1377
- recipes.filter(tag).forEach((recipe) => {
1363
+ if (!jsx.isJsxTagRecipe(tag ?? "")) continue;
1364
+ recipes.filter(tag ?? "").forEach((recipe) => {
1378
1365
  parserResult.setRecipe(recipe.baseName, {
1379
1366
  type: "jsx-recipe",
1380
1367
  name: tag,
@@ -1387,19 +1374,16 @@ function createParser(context) {
1387
1374
  });
1388
1375
  });
1389
1376
  parserResult.deadCalls = file.getDeadCalls();
1390
- if (exportReadPairs.size) {
1391
- const project = sourceFile.getProject();
1392
- parserResult.setExportReads([...exportReadPairs].map((pair) => {
1393
- const at = pair.indexOf("\0");
1394
- const file = pair.slice(0, at);
1395
- const name = pair.slice(at + 1);
1396
- return {
1397
- file,
1398
- name,
1399
- digest: digestExportValue(project.getSourceFile(file), name, resolveModule)
1400
- };
1401
- }));
1402
- }
1377
+ if (exportReadPairs.size) parserResult.setExportReads([...exportReadPairs].map((pair) => {
1378
+ const at = pair.indexOf("\0");
1379
+ const file = pair.slice(0, at);
1380
+ const name = pair.slice(at + 1);
1381
+ return {
1382
+ file,
1383
+ name,
1384
+ digest: digestExportValue(resolveModule?.(file, sourceFile), name, resolveModule)
1385
+ };
1386
+ }));
1403
1387
  return parserResult;
1404
1388
  };
1405
1389
  }
@@ -1416,11 +1400,16 @@ const invalidateResolutions = () => {
1416
1400
  clearBoxNodeCache();
1417
1401
  clearImportedRecipeCache();
1418
1402
  };
1419
- const normalizeCompilerOptions = (raw, basePath = process.cwd()) => {
1420
- if (!raw) return {};
1421
- const { options } = ts.convertCompilerOptionsFromJson(raw, basePath);
1422
- return options;
1423
- };
1403
+ /**
1404
+ * Compiler options as given.
1405
+ *
1406
+ * TypeScript 6 needed `convertCompilerOptionsFromJson` to turn a tsconfig's JSON spellings —
1407
+ * `"target": "esnext"`, relative `paths` — into the enum values and absolute paths a program
1408
+ * consumed. TypeScript 7 parses the tsconfig in the Go process and hands back options already
1409
+ * in that form, so there is nothing left to convert; the argument survives because callers pass
1410
+ * overrides that are already normalized.
1411
+ */
1412
+ const normalizeCompilerOptions = (raw, _basePath = process.cwd()) => raw ?? {};
1424
1413
  /** Snapshot the JSON-shaped ts-morph options while retaining opaque hosts and callbacks. */
1425
1414
  const snapshotProjectOption = (value) => {
1426
1415
  if (Array.isArray(value)) return value.map(snapshotProjectOption);
@@ -1631,32 +1620,33 @@ var Project = class {
1631
1620
  this.#assertSourceFilesAccessor();
1632
1621
  const revision = this.#sourceFiles.revision;
1633
1622
  this.#sourceFiles.phase = "loading";
1623
+ let candidate;
1634
1624
  try {
1635
- const candidate = createTsProject(this.#sourceFiles.projectOptions);
1625
+ candidate = createTsProject(this.#sourceFiles.projectOptions);
1636
1626
  this.#assertSourceFilesTransaction(revision);
1637
1627
  let loaded = 0;
1628
+ const entries = [];
1638
1629
  for (const [index, file] of this.#sourceFiles.initialFiles.entries()) try {
1639
1630
  const content = read(file, index);
1640
1631
  this.#assertSourceFilesTransaction(revision);
1641
- candidate.createSourceFile(file, content, {
1642
- overwrite: true,
1643
- scriptKind: scriptKindFor(file)
1644
- });
1632
+ entries.push([file, content]);
1645
1633
  this.#assertSourceFilesTransaction(revision);
1646
1634
  loaded++;
1647
1635
  } catch (error) {
1648
1636
  this.#assertSourceFilesTransaction(revision);
1649
1637
  if (!skipMissing || error?.code !== "ENOENT") throw error;
1650
1638
  }
1639
+ candidate.addSourceFiles(entries);
1651
1640
  this.#assertSourceFilesTransaction(revision);
1652
1641
  if (loaded > 0) invalidateResolutions();
1653
- this.#moduleResolutionCache = void 0;
1642
+ this.#resolver = void 0;
1654
1643
  this.#fileTreeRevision++;
1655
1644
  this.#assertSourceFilesTransaction(revision);
1656
1645
  this.#sourceFiles.project = candidate;
1657
1646
  this.#sourceFiles.phase = "ready";
1658
1647
  return;
1659
1648
  } catch (error) {
1649
+ candidate?.dispose();
1660
1650
  this.#sourceFiles.project = void 0;
1661
1651
  this.#sourceFiles.phase = "pending";
1662
1652
  throw error;
@@ -1729,7 +1719,7 @@ var Project = class {
1729
1719
  sourceFilesRead: 0
1730
1720
  };
1731
1721
  resetResolutionState = () => {
1732
- this.#moduleResolutionCache = void 0;
1722
+ this.#resolver = void 0;
1733
1723
  this.#fileTreeRevision++;
1734
1724
  this.dependents = /* @__PURE__ */ new Map();
1735
1725
  this.dependencies = /* @__PURE__ */ new Map();
@@ -1770,12 +1760,24 @@ var Project = class {
1770
1760
  };
1771
1761
  getSourceFile = (filePath) => {
1772
1762
  this.#assertNotLoading();
1763
+ if (!this.project.has(filePath)) return void 0;
1773
1764
  return this.project.getSourceFile(filePath);
1774
1765
  };
1766
+ /**
1767
+ * How many syntax errors this file's parse produced.
1768
+ *
1769
+ * Asked by the token accounting, which may only speak for a file whose tree it can trust: a
1770
+ * construct the parser could not read leaves an ast that stops early, and every call below
1771
+ * the offending line silently ceases to exist. Zero means the file parsed as written.
1772
+ */
1773
+ getSyntacticDiagnosticCount = (filePath) => {
1774
+ this.#assertNotLoading();
1775
+ return this.project.getSyntacticDiagnosticCount(filePath);
1776
+ };
1775
1777
  /** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
1776
1778
  normalizePath = (filePath) => filePath.replaceAll("\\", "/");
1777
1779
  /** Shared filesystem-only resolution cache; no type checker is constructed. */
1778
- #moduleResolutionCache;
1780
+ #resolver;
1779
1781
  /**
1780
1782
  * Everything memoized against the shape of the file tree, including the negative half.
1781
1783
  *
@@ -1808,7 +1810,7 @@ var Project = class {
1808
1810
  }
1809
1811
  invalidateResolutions();
1810
1812
  if (fileTreeChanged) {
1811
- this.#moduleResolutionCache = void 0;
1813
+ this.#resolver = void 0;
1812
1814
  this.#fileTreeRevision++;
1813
1815
  }
1814
1816
  };
@@ -1831,17 +1833,10 @@ var Project = class {
1831
1833
  ...this.#sourceFiles.projectOptions,
1832
1834
  compilerOptions: snapshotProjectOption(next)
1833
1835
  };
1834
- if (this.#sourceFiles.phase === "ready") {
1835
- const current = this.project.getCompilerOptions();
1836
- const cleared = Object.fromEntries(Object.keys(current).map((key) => [key, void 0]));
1837
- this.project.compilerOptions.set({
1838
- ...cleared,
1839
- ...next
1840
- });
1841
- }
1836
+ this.#sourceFiles.project?.setCompilerOptions(next);
1842
1837
  }
1843
1838
  invalidateResolutions();
1844
- this.#moduleResolutionCache = void 0;
1839
+ this.#resolver = void 0;
1845
1840
  this.#fileTreeRevision++;
1846
1841
  this.sourcePreparations.clear();
1847
1842
  };
@@ -1887,33 +1882,33 @@ var Project = class {
1887
1882
  this.#assertNotLoading();
1888
1883
  const project = this.project;
1889
1884
  const compilerOptions = project.getCompilerOptions();
1890
- this.#moduleResolutionCache ??= ts.createModuleResolutionCache(project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
1891
1885
  const configurationFiles = /* @__PURE__ */ new Set();
1892
- const resolutionHost = project.getModuleResolutionHost();
1893
1886
  const recordConfigurationFile = (filePath) => {
1894
1887
  const normalized = this.normalizePath(filePath);
1895
1888
  if (!normalized.endsWith("/package.json") || normalized.includes("/node_modules/")) return;
1896
1889
  if (!this.isInCheckout(normalized)) return;
1897
1890
  configurationFiles.add(normalized);
1898
1891
  };
1899
- const host = {
1900
- ...resolutionHost,
1901
- fileExists: (filePath) => {
1902
- recordConfigurationFile(filePath);
1903
- return resolutionHost.fileExists(filePath);
1904
- },
1905
- readFile: (filePath) => {
1906
- recordConfigurationFile(filePath);
1907
- return resolutionHost.readFile?.(filePath);
1892
+ this.#resolver ??= createResolver({
1893
+ cwd: project.getCurrentDirectory(),
1894
+ fs: {
1895
+ fileExists: (filePath) => project.fileExists(filePath),
1896
+ readFile: (filePath) => project.readFile(filePath)
1908
1897
  }
1909
- };
1898
+ });
1899
+ const configured = this.#sourceFiles.projectOptions.compilerOptions ?? compilerOptions;
1910
1900
  this.resolutionWork.moduleResolutionsAttempted++;
1911
- const resolved = ts.resolveModuleName(moduleName, from.getFilePath(), compilerOptions, host, this.#moduleResolutionCache);
1912
- const failedLookupLocations = resolved.failedLookupLocations;
1913
- const affectingLocations = resolved.affectingLocations;
1914
- for (const filePath of affectingLocations ?? []) recordConfigurationFile(filePath);
1915
- const pendingCandidates = this.getLocalFailedLookupCandidates(failedLookupLocations);
1916
- const module = resolved.resolvedModule;
1901
+ const resolved = this.#resolver(moduleName, {
1902
+ importer: pathOf(from),
1903
+ baseUrl: configured?.baseUrl,
1904
+ paths: configured?.paths
1905
+ });
1906
+ for (const filePath of resolved.affectingFiles) recordConfigurationFile(filePath);
1907
+ const pendingCandidates = this.getLocalFailedLookupCandidates(resolved.failedLookups);
1908
+ const module = resolved.path ? {
1909
+ resolvedFileName: resolved.path,
1910
+ isExternalLibraryImport: resolved.path.includes("/node_modules/")
1911
+ } : void 0;
1917
1912
  if (!module) return {
1918
1913
  configurationFiles: [...configurationFiles].sort(),
1919
1914
  local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates) || moduleName.startsWith("#"),
@@ -1930,7 +1925,7 @@ var Project = class {
1930
1925
  local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates),
1931
1926
  pendingCandidates
1932
1927
  };
1933
- const existing = project.getSourceFile(name);
1928
+ const existing = this.project.has(name) ? project.getSourceFile(name) : void 0;
1934
1929
  if (existing) {
1935
1930
  this.removedSourcePaths.delete(name);
1936
1931
  return {
@@ -1949,12 +1944,10 @@ var Project = class {
1949
1944
  try {
1950
1945
  this.resolutionWork.sourceFilesRead++;
1951
1946
  const content = project.getFileSystem().readFileSync(name);
1952
- const sourceFile = project.createSourceFile(name, content, {
1953
- overwrite: true,
1954
- scriptKind: scriptKindFor(name)
1955
- });
1947
+ const sourceFile = project.createSourceFile(name, content, { scriptKind: scriptKindFor(name) });
1948
+ if (!sourceFile) throw new Error(`bamboo: resolved ${name} but the project produced no source file`);
1956
1949
  this.resolutionWork.sourceFilesAdded++;
1957
- this.canonicalPaths.set(name, this.normalizePath(sourceFile.getFilePath()));
1950
+ this.canonicalPaths.set(name, this.normalizePath(pathOf(sourceFile)));
1958
1951
  return {
1959
1952
  configurationFiles: [...configurationFiles].sort(),
1960
1953
  local: true,
@@ -2008,28 +2001,28 @@ var Project = class {
2008
2001
  };
2009
2002
  ensureResolutionFacts = (sourceFile) => {
2010
2003
  this.#assertNotLoading();
2011
- const importer = this.normalizePath(sourceFile.getFilePath());
2004
+ const importer = this.normalizePath(pathOf(sourceFile));
2012
2005
  const text = sourceFile.getFullText();
2013
2006
  const cached = this.resolutionsByImporter.get(importer);
2014
2007
  if (cached?.sourceFile === sourceFile && cached.text === text && cached.treeRevision === this.#fileTreeRevision) return cached;
2015
- const declarations = [...sourceFile.getImportDeclarations().map((declaration) => ({
2008
+ const declarations = [...getImportDeclarations(sourceFile).map((declaration) => ({
2016
2009
  declaration,
2017
2010
  kind: "import"
2018
- })), ...sourceFile.getExportDeclarations().map((declaration) => ({
2011
+ })), ...getExportDeclarations(sourceFile).map((declaration) => ({
2019
2012
  declaration,
2020
2013
  kind: "export"
2021
- }))].filter(({ declaration }) => declaration.getModuleSpecifierValue() !== void 0).sort((left, right) => left.declaration.getStart() - right.declaration.getStart());
2014
+ }))].filter(({ declaration }) => getModuleSpecifierValue(declaration) !== void 0).sort((left, right) => left.declaration.getStart() - right.declaration.getStart());
2022
2015
  const facts = [];
2023
2016
  const pendingCandidates = [];
2024
2017
  const configurationFiles = [];
2025
2018
  for (const [ordinal, { declaration, kind }] of declarations.entries()) {
2026
- const specifier = declaration.getModuleSpecifierValue();
2019
+ const specifier = getModuleSpecifierValue(declaration);
2027
2020
  if (!specifier) continue;
2028
2021
  const resolved = this.resolveSpecifier(specifier, sourceFile);
2029
2022
  if (!resolved.local) continue;
2030
2023
  facts.push(Object.freeze({
2031
2024
  importer,
2032
- target: resolved.sourceFile ? this.normalizePath(resolved.sourceFile.getFilePath()) : null,
2025
+ target: resolved.sourceFile ? this.normalizePath(pathOf(resolved.sourceFile)) : null,
2033
2026
  specifier,
2034
2027
  kind,
2035
2028
  ordinal
@@ -2054,22 +2047,22 @@ var Project = class {
2054
2047
  };
2055
2048
  /** The sole cross-file source resolver supplied to parser and extractor. */
2056
2049
  resolveModule = (specifier, from) => {
2057
- const target = this.ensureResolutionFacts(from).facts.find((fact) => fact.specifier === specifier)?.target;
2050
+ const target = this.ensureResolutionFacts(from).facts.find((fact) => fact.specifier === specifier)?.target ?? (specifier.startsWith("/") ? specifier : void 0);
2058
2051
  if (!target) return;
2059
2052
  const sourceFile = this.project.getSourceFile(target);
2060
2053
  if (!sourceFile) return;
2061
- this.prepareEffectiveSource(sourceFile.getFilePath(), sourceFile);
2054
+ this.prepareEffectiveSource(pathOf(sourceFile), sourceFile);
2062
2055
  return sourceFile;
2063
2056
  };
2064
2057
  trackDependencies = (filePath, sourceFile) => {
2065
2058
  this.#assertNotLoading();
2066
- const importer = this.normalizePath(sourceFile.getFilePath());
2059
+ const importer = this.normalizePath(pathOf(sourceFile));
2067
2060
  this.canonicalPaths.set(this.normalizePath(filePath), importer);
2068
2061
  this.canonicalPaths.set(importer, importer);
2069
2062
  this.ensureResolutionFacts(sourceFile);
2070
2063
  };
2071
2064
  invalidateSourcePreparation = (filePath, sourceFile) => {
2072
- const sourcePath = this.normalizePath(sourceFile?.getFilePath() ?? filePath);
2065
+ const sourcePath = this.normalizePath(pathOf(sourceFile) ?? filePath);
2073
2066
  const preparation = this.sourcePreparations.get(sourcePath);
2074
2067
  if (preparation?.state === "preparing") preparation.invalidated = true;
2075
2068
  this.sourcePreparations.delete(sourcePath);
@@ -2085,7 +2078,7 @@ var Project = class {
2085
2078
  */
2086
2079
  prepareEffectiveSource = (filePath, sourceFile, hookFilePath = filePath) => {
2087
2080
  this.#assertNotLoading();
2088
- const sourcePath = this.normalizePath(sourceFile.getFilePath());
2081
+ const sourcePath = this.normalizePath(pathOf(sourceFile));
2089
2082
  const currentText = sourceFile.getFullText();
2090
2083
  const current = this.sourcePreparations.get(sourcePath);
2091
2084
  if (current?.state === "ready" && current.sourceFile === sourceFile && current.effectiveText === currentText) {
@@ -2130,7 +2123,7 @@ var Project = class {
2130
2123
  assertTransaction(currentText);
2131
2124
  const transformed = custom ?? this.transformFile(hookFilePath, currentText);
2132
2125
  assertTransaction(currentText);
2133
- if (currentText !== transformed) sourceFile.replaceWithText(transformed);
2126
+ if (currentText !== transformed) sourceFile = this.project.addSourceFile(filePath, transformed) ?? sourceFile;
2134
2127
  assertTransaction(transformed);
2135
2128
  this.trackDependencies(filePath, sourceFile);
2136
2129
  assertTransaction(transformed);
@@ -2147,7 +2140,7 @@ var Project = class {
2147
2140
  if (this.sourcePreparations.get(sourcePath) === transaction) this.sourcePreparations.delete(sourcePath);
2148
2141
  this.retractImporter(sourcePath);
2149
2142
  try {
2150
- if (sourceFile.getFullText() !== currentText) sourceFile.replaceWithText(currentText);
2143
+ if (sourceFile.getFullText() !== currentText) this.project.addSourceFile(filePath, currentText);
2151
2144
  } catch {}
2152
2145
  throw error;
2153
2146
  }
@@ -2174,7 +2167,7 @@ var Project = class {
2174
2167
  this.retractImporter(target);
2175
2168
  this.removedSourcePaths.add(target);
2176
2169
  this.canonicalPaths.set(target, target);
2177
- this.canonicalPaths.set(this.normalizePath(sourceFile.getFilePath()), target);
2170
+ this.canonicalPaths.set(this.normalizePath(pathOf(sourceFile)), target);
2178
2171
  };
2179
2172
  /**
2180
2173
  * Every file that transitively imports `filePath`, so a watcher can re-parse the
@@ -2183,7 +2176,7 @@ var Project = class {
2183
2176
  getDependents = (filePath) => {
2184
2177
  this.#assertNotLoading();
2185
2178
  const given = this.normalizePath(filePath);
2186
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2179
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2187
2180
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2188
2181
  const seen = /* @__PURE__ */ new Set();
2189
2182
  const queue = [start];
@@ -2211,7 +2204,7 @@ var Project = class {
2211
2204
  getDependencies = (filePath, targets) => {
2212
2205
  this.#assertNotLoading();
2213
2206
  const given = this.normalizePath(filePath);
2214
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2207
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2215
2208
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2216
2209
  const seen = /* @__PURE__ */ new Set();
2217
2210
  const importersByDependency = /* @__PURE__ */ new Map();
@@ -2232,7 +2225,7 @@ var Project = class {
2232
2225
  const selected = /* @__PURE__ */ new Set();
2233
2226
  const reverse = Array.from(targets, (target) => {
2234
2227
  const normalized = this.normalizePath(target);
2235
- const source = this.project.getSourceFile(target)?.getFilePath();
2228
+ const source = pathOf(this.project.getSourceFile(target));
2236
2229
  return source ? this.normalizePath(source) : this.canonicalPaths.get(normalized) ?? normalized;
2237
2230
  }).filter((target) => seen.has(target));
2238
2231
  let reverseCursor = 0;
@@ -2258,7 +2251,7 @@ var Project = class {
2258
2251
  const selected = new Set(dependencies);
2259
2252
  const retained = new Set([...previous?.dependencies ?? [], ...previous?.pendingCandidates ?? []]);
2260
2253
  const given = this.normalizePath(filePath);
2261
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2254
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2262
2255
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2263
2256
  const pendingCandidates = /* @__PURE__ */ new Set();
2264
2257
  for (const importer of [start, ...dependencies]) {
@@ -2291,7 +2284,7 @@ var Project = class {
2291
2284
  const selected = new Set(dependencies);
2292
2285
  const retained = new Set(Array.from(previous, (file) => this.normalizePath(file)));
2293
2286
  const given = this.normalizePath(filePath);
2294
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2287
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2295
2288
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2296
2289
  const files = /* @__PURE__ */ new Set();
2297
2290
  let hasSemanticFact = false;
@@ -2318,10 +2311,9 @@ var Project = class {
2318
2311
  this.invalidateSourcePreparation(filePath, existing);
2319
2312
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2320
2313
  this.invalidate();
2321
- return this.project.createSourceFile(filePath, content, {
2322
- overwrite: true,
2323
- scriptKind: scriptKindFor(filePath)
2324
- });
2314
+ const created = this.project.createSourceFile(filePath, content, { scriptKind: scriptKindFor(filePath) });
2315
+ if (!created) throw new Error(`bamboo: could not add ${filePath} to the project`);
2316
+ return created;
2325
2317
  };
2326
2318
  createSourceFiles = () => {
2327
2319
  this.#assertNotLoading();
@@ -2360,17 +2352,15 @@ var Project = class {
2360
2352
  * matches its own source, falls through, and is overwritten exactly as before.
2361
2353
  */
2362
2354
  if (existing && existing.getFullText() === content) {
2363
- this.markAuxiliary(existing.getFilePath(), options.auxiliary);
2355
+ this.markAuxiliary(pathOf(existing), options.auxiliary);
2364
2356
  return existing;
2365
2357
  }
2366
2358
  this.invalidateSourcePreparation(filePath, existing);
2367
2359
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2368
- this.invalidate(!existing, existing?.getFilePath());
2369
- const sourceFile = this.project.createSourceFile(filePath, content, {
2370
- overwrite: true,
2371
- scriptKind: scriptKindFor(filePath)
2372
- });
2373
- this.markAuxiliary(sourceFile.getFilePath(), options.auxiliary);
2360
+ this.invalidate(!existing, pathOf(existing));
2361
+ const sourceFile = this.project.createSourceFile(filePath, content, { scriptKind: scriptKindFor(filePath) });
2362
+ if (!sourceFile) throw new Error(`bamboo: could not add ${filePath} to the project`);
2363
+ this.markAuxiliary(pathOf(sourceFile), options.auxiliary);
2374
2364
  return sourceFile;
2375
2365
  };
2376
2366
  /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
@@ -2386,10 +2376,10 @@ var Project = class {
2386
2376
  if (sourceFile) {
2387
2377
  this.invalidate();
2388
2378
  this.invalidateSourcePreparation(filePath, sourceFile);
2389
- this.markTargetRemoved(this.normalizePath(sourceFile.getFilePath()), sourceFile);
2390
- this.options.parserOptions.encoder.releaseFile(sourceFile.getFilePath());
2391
- this.auxiliarySources.delete(this.normalizePath(sourceFile.getFilePath()));
2392
- return this.project.removeSourceFile(sourceFile);
2379
+ this.markTargetRemoved(this.normalizePath(pathOf(sourceFile)), sourceFile);
2380
+ this.options.parserOptions.encoder.releaseFile(pathOf(sourceFile));
2381
+ this.auxiliarySources.delete(this.normalizePath(pathOf(sourceFile)));
2382
+ return this.project.removeSourceFile(pathOf(sourceFile));
2393
2383
  }
2394
2384
  return false;
2395
2385
  };
@@ -2408,10 +2398,10 @@ var Project = class {
2408
2398
  this.#assertNotLoading();
2409
2399
  this.#ensureSourceFiles();
2410
2400
  const sourceFile = this.getSourceFile(filePath);
2411
- this.invalidate(false, sourceFile?.getFilePath());
2401
+ this.invalidate(false, pathOf(sourceFile));
2412
2402
  if (!sourceFile) return;
2413
2403
  this.invalidateSourcePreparation(filePath, sourceFile);
2414
- return sourceFile.refreshFromFileSystemSync();
2404
+ return this.project.reloadSourceFile(filePath);
2415
2405
  };
2416
2406
  reloadSourceFiles = () => {
2417
2407
  this.#assertNotLoading();
@@ -2422,7 +2412,7 @@ var Project = class {
2422
2412
  const source = this.getSourceFile(file);
2423
2413
  if (source) {
2424
2414
  this.invalidateSourcePreparation(file, source);
2425
- source.refreshFromFileSystemSync();
2415
+ this.project.reloadSourceFile(file);
2426
2416
  } else {
2427
2417
  this.invalidateSourcePreparation(file);
2428
2418
  this.removedSourcePaths.delete(this.normalizePath(file));
@@ -2464,10 +2454,12 @@ var Project = class {
2464
2454
  const { hooks } = this.options;
2465
2455
  const hookFilePath = options.hookFilePath ?? filePath;
2466
2456
  if (filePath.endsWith(".json")) return this.parseJson(filePath, encoder);
2467
- const sourceFile = this.project.getSourceFile(filePath);
2457
+ let sourceFile = this.project.getSourceFile(filePath);
2468
2458
  if (!sourceFile) return;
2469
- const { options: parserOptions } = this.prepareEffectiveSource(filePath, sourceFile, hookFilePath);
2470
- const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, parserOptions, this.resolveModule))?.setFilePath(filePath);
2459
+ const prepared = this.prepareEffectiveSource(filePath, sourceFile, hookFilePath);
2460
+ const parserOptions = prepared.options;
2461
+ sourceFile = prepared.sourceFile ?? sourceFile;
2462
+ const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", pathOf(sourceFile), () => this.parser(sourceFile, encoder, parserOptions, this.resolveModule))?.setFilePath(filePath);
2471
2463
  hooks["parser:after"]?.({
2472
2464
  filePath: hookFilePath,
2473
2465
  result