@bamboocss/parser 1.48.5 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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);
@@ -1171,6 +1158,17 @@ const combineResult = (unboxed) => {
1171
1158
  ...unboxed.spreadConditions
1172
1159
  ];
1173
1160
  };
1161
+ /** The exact binding token accounting visits for a token call. */
1162
+ const tokenCalleeRange = (call) => {
1163
+ if (!Node.isCallExpression(call)) return void 0;
1164
+ let current = call.expression;
1165
+ while (Node.isPropertyAccessExpression(current)) current = current.expression;
1166
+ if (!Node.isIdentifier(current)) return void 0;
1167
+ return {
1168
+ start: current.getStart(),
1169
+ end: current.getEnd()
1170
+ };
1171
+ };
1174
1172
  const defaultEnv = { preset: "ECMA" };
1175
1173
  /**
1176
1174
  * `fallback()` is generated into `styled-system/css`, so evaluating a call to it would mean
@@ -1184,16 +1182,16 @@ function createParser(context) {
1184
1182
  const { jsx, imports, recipes } = context;
1185
1183
  return function parse(sourceFile, encoder, options, resolveModule) {
1186
1184
  if (!sourceFile) return;
1187
- const importDeclarations = getImportDeclarations(context, sourceFile);
1185
+ const importDeclarations = getImportDeclarations$1(context, sourceFile);
1188
1186
  const file = imports.file(importDeclarations);
1189
- const filePath = sourceFile.getFilePath();
1187
+ const filePath = pathOf(sourceFile);
1190
1188
  logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
1191
1189
  const parserResult = new ParserResult(context, encoder);
1192
1190
  const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
1193
1191
  if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
1194
1192
  parserResult.importedRecipes = importedRecipes;
1195
1193
  for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
1196
- if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
1194
+ if (file.importsRecipeFactory()) for (const statement of sourceFile.statements) {
1197
1195
  if (!ts.isVariableStatement(statement)) continue;
1198
1196
  if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
1199
1197
  for (const declaration of statement.declarationList.declarations) {
@@ -1218,7 +1216,7 @@ function createParser(context) {
1218
1216
  * Defined out here rather than inside `getEvaluateOptions`, which runs per call node.
1219
1217
  */
1220
1218
  const reportUnresolvedRaw = (base, at) => {
1221
- const { line, column } = sourceFile.getLineAndColumnAtPos(at.getStart());
1219
+ const { line, column } = getLineAndColumnAtPos(sourceFile, at.getStart());
1222
1220
  parserResult.unresolved.push({
1223
1221
  kind: "atomic",
1224
1222
  prop: base,
@@ -1261,7 +1259,7 @@ function createParser(context) {
1261
1259
  },
1262
1260
  getEvaluateOptions: (node) => {
1263
1261
  if (!Node.isCallExpression(node)) return evaluateOptions;
1264
- const propAccessExpr = node.getExpression();
1262
+ const propAccessExpr = node.expression;
1265
1263
  if (Node.isIdentifier(propAccessExpr)) {
1266
1264
  const local = propAccessExpr.getText();
1267
1265
  if (!file.match(local) || file.getName(local) !== "fallback") return evaluateOptions;
@@ -1306,7 +1304,8 @@ function createParser(context) {
1306
1304
  if (query.kind === "call-expression") parserResult.setToken({
1307
1305
  name: "token.value",
1308
1306
  box: query.box.value[0] ?? box.fallback(query.box),
1309
- data: combineResult(unbox(query.box.value[0]))
1307
+ data: combineResult(unbox(query.box.value[0])),
1308
+ tokenCalleeRange: tokenCalleeRange(query.box.getNode())
1310
1309
  }, "tokenValue");
1311
1310
  });
1312
1311
  return;
@@ -1329,12 +1328,13 @@ function createParser(context) {
1329
1328
  if (query.kind === "call-expression") parserResult.setToken({
1330
1329
  name,
1331
1330
  box: query.box.value[0] ?? box.fallback(query.box),
1332
- data: combineResult(unbox(query.box.value[0]))
1331
+ data: combineResult(unbox(query.box.value[0])),
1332
+ tokenCalleeRange: tokenCalleeRange(query.box.getNode())
1333
1333
  });
1334
1334
  });
1335
1335
  }).when(file.isValidPattern, (name) => {
1336
1336
  result.queryList.forEach((query) => {
1337
- if (query.kind === "call-expression") parserResult.setPattern(name, {
1337
+ if (query.kind === "call-expression") parserResult.setPattern(name ?? "", {
1338
1338
  name,
1339
1339
  box: query.box.value[0] ?? box.fallback(query.box),
1340
1340
  data: combineResult(unbox(query.box.value[0]))
@@ -1342,7 +1342,7 @@ function createParser(context) {
1342
1342
  });
1343
1343
  }).when(file.isValidRecipe, (name) => {
1344
1344
  result.queryList.forEach((query) => {
1345
- if (query.kind === "call-expression") parserResult.setRecipe(name, {
1345
+ if (query.kind === "call-expression") parserResult.setRecipe(name ?? "", {
1346
1346
  name,
1347
1347
  box: query.box.value[0] ?? box.fallback(query.box),
1348
1348
  data: combineResult(unbox(query.box.value[0]))
@@ -1360,8 +1360,8 @@ function createParser(context) {
1360
1360
  } else if (jsx.isEnabled && result.kind === "component") result.queryList.forEach((query) => {
1361
1361
  const data = combineResult(unbox(query.box));
1362
1362
  for (const tag of [name, alias]) {
1363
- if (!jsx.isJsxTagRecipe(tag)) continue;
1364
- recipes.filter(tag).forEach((recipe) => {
1363
+ if (!jsx.isJsxTagRecipe(tag ?? "")) continue;
1364
+ recipes.filter(tag ?? "").forEach((recipe) => {
1365
1365
  parserResult.setRecipe(recipe.baseName, {
1366
1366
  type: "jsx-recipe",
1367
1367
  name: tag,
@@ -1374,19 +1374,16 @@ function createParser(context) {
1374
1374
  });
1375
1375
  });
1376
1376
  parserResult.deadCalls = file.getDeadCalls();
1377
- if (exportReadPairs.size) {
1378
- const project = sourceFile.getProject();
1379
- parserResult.setExportReads([...exportReadPairs].map((pair) => {
1380
- const at = pair.indexOf("\0");
1381
- const file = pair.slice(0, at);
1382
- const name = pair.slice(at + 1);
1383
- return {
1384
- file,
1385
- name,
1386
- digest: digestExportValue(project.getSourceFile(file), name, resolveModule)
1387
- };
1388
- }));
1389
- }
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
+ }));
1390
1387
  return parserResult;
1391
1388
  };
1392
1389
  }
@@ -1403,11 +1400,16 @@ const invalidateResolutions = () => {
1403
1400
  clearBoxNodeCache();
1404
1401
  clearImportedRecipeCache();
1405
1402
  };
1406
- const normalizeCompilerOptions = (raw, basePath = process.cwd()) => {
1407
- if (!raw) return {};
1408
- const { options } = ts.convertCompilerOptionsFromJson(raw, basePath);
1409
- return options;
1410
- };
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 ?? {};
1411
1413
  /** Snapshot the JSON-shaped ts-morph options while retaining opaque hosts and callbacks. */
1412
1414
  const snapshotProjectOption = (value) => {
1413
1415
  if (Array.isArray(value)) return value.map(snapshotProjectOption);
@@ -1618,32 +1620,33 @@ var Project = class {
1618
1620
  this.#assertSourceFilesAccessor();
1619
1621
  const revision = this.#sourceFiles.revision;
1620
1622
  this.#sourceFiles.phase = "loading";
1623
+ let candidate;
1621
1624
  try {
1622
- const candidate = createTsProject(this.#sourceFiles.projectOptions);
1625
+ candidate = createTsProject(this.#sourceFiles.projectOptions);
1623
1626
  this.#assertSourceFilesTransaction(revision);
1624
1627
  let loaded = 0;
1628
+ const entries = [];
1625
1629
  for (const [index, file] of this.#sourceFiles.initialFiles.entries()) try {
1626
1630
  const content = read(file, index);
1627
1631
  this.#assertSourceFilesTransaction(revision);
1628
- candidate.createSourceFile(file, content, {
1629
- overwrite: true,
1630
- scriptKind: scriptKindFor(file)
1631
- });
1632
+ entries.push([file, content]);
1632
1633
  this.#assertSourceFilesTransaction(revision);
1633
1634
  loaded++;
1634
1635
  } catch (error) {
1635
1636
  this.#assertSourceFilesTransaction(revision);
1636
1637
  if (!skipMissing || error?.code !== "ENOENT") throw error;
1637
1638
  }
1639
+ candidate.addSourceFiles(entries);
1638
1640
  this.#assertSourceFilesTransaction(revision);
1639
1641
  if (loaded > 0) invalidateResolutions();
1640
- this.#moduleResolutionCache = void 0;
1642
+ this.#resolver = void 0;
1641
1643
  this.#fileTreeRevision++;
1642
1644
  this.#assertSourceFilesTransaction(revision);
1643
1645
  this.#sourceFiles.project = candidate;
1644
1646
  this.#sourceFiles.phase = "ready";
1645
1647
  return;
1646
1648
  } catch (error) {
1649
+ candidate?.dispose();
1647
1650
  this.#sourceFiles.project = void 0;
1648
1651
  this.#sourceFiles.phase = "pending";
1649
1652
  throw error;
@@ -1716,7 +1719,7 @@ var Project = class {
1716
1719
  sourceFilesRead: 0
1717
1720
  };
1718
1721
  resetResolutionState = () => {
1719
- this.#moduleResolutionCache = void 0;
1722
+ this.#resolver = void 0;
1720
1723
  this.#fileTreeRevision++;
1721
1724
  this.dependents = /* @__PURE__ */ new Map();
1722
1725
  this.dependencies = /* @__PURE__ */ new Map();
@@ -1757,12 +1760,24 @@ var Project = class {
1757
1760
  };
1758
1761
  getSourceFile = (filePath) => {
1759
1762
  this.#assertNotLoading();
1763
+ if (!this.project.has(filePath)) return void 0;
1760
1764
  return this.project.getSourceFile(filePath);
1761
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
+ };
1762
1777
  /** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
1763
1778
  normalizePath = (filePath) => filePath.replaceAll("\\", "/");
1764
1779
  /** Shared filesystem-only resolution cache; no type checker is constructed. */
1765
- #moduleResolutionCache;
1780
+ #resolver;
1766
1781
  /**
1767
1782
  * Everything memoized against the shape of the file tree, including the negative half.
1768
1783
  *
@@ -1795,7 +1810,7 @@ var Project = class {
1795
1810
  }
1796
1811
  invalidateResolutions();
1797
1812
  if (fileTreeChanged) {
1798
- this.#moduleResolutionCache = void 0;
1813
+ this.#resolver = void 0;
1799
1814
  this.#fileTreeRevision++;
1800
1815
  }
1801
1816
  };
@@ -1818,17 +1833,10 @@ var Project = class {
1818
1833
  ...this.#sourceFiles.projectOptions,
1819
1834
  compilerOptions: snapshotProjectOption(next)
1820
1835
  };
1821
- if (this.#sourceFiles.phase === "ready") {
1822
- const current = this.project.getCompilerOptions();
1823
- const cleared = Object.fromEntries(Object.keys(current).map((key) => [key, void 0]));
1824
- this.project.compilerOptions.set({
1825
- ...cleared,
1826
- ...next
1827
- });
1828
- }
1836
+ this.#sourceFiles.project?.setCompilerOptions(next);
1829
1837
  }
1830
1838
  invalidateResolutions();
1831
- this.#moduleResolutionCache = void 0;
1839
+ this.#resolver = void 0;
1832
1840
  this.#fileTreeRevision++;
1833
1841
  this.sourcePreparations.clear();
1834
1842
  };
@@ -1874,33 +1882,33 @@ var Project = class {
1874
1882
  this.#assertNotLoading();
1875
1883
  const project = this.project;
1876
1884
  const compilerOptions = project.getCompilerOptions();
1877
- this.#moduleResolutionCache ??= ts.createModuleResolutionCache(project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
1878
1885
  const configurationFiles = /* @__PURE__ */ new Set();
1879
- const resolutionHost = project.getModuleResolutionHost();
1880
1886
  const recordConfigurationFile = (filePath) => {
1881
1887
  const normalized = this.normalizePath(filePath);
1882
1888
  if (!normalized.endsWith("/package.json") || normalized.includes("/node_modules/")) return;
1883
1889
  if (!this.isInCheckout(normalized)) return;
1884
1890
  configurationFiles.add(normalized);
1885
1891
  };
1886
- const host = {
1887
- ...resolutionHost,
1888
- fileExists: (filePath) => {
1889
- recordConfigurationFile(filePath);
1890
- return resolutionHost.fileExists(filePath);
1891
- },
1892
- readFile: (filePath) => {
1893
- recordConfigurationFile(filePath);
1894
- 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)
1895
1897
  }
1896
- };
1898
+ });
1899
+ const configured = this.#sourceFiles.projectOptions.compilerOptions ?? compilerOptions;
1897
1900
  this.resolutionWork.moduleResolutionsAttempted++;
1898
- const resolved = ts.resolveModuleName(moduleName, from.getFilePath(), compilerOptions, host, this.#moduleResolutionCache);
1899
- const failedLookupLocations = resolved.failedLookupLocations;
1900
- const affectingLocations = resolved.affectingLocations;
1901
- for (const filePath of affectingLocations ?? []) recordConfigurationFile(filePath);
1902
- const pendingCandidates = this.getLocalFailedLookupCandidates(failedLookupLocations);
1903
- 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;
1904
1912
  if (!module) return {
1905
1913
  configurationFiles: [...configurationFiles].sort(),
1906
1914
  local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates) || moduleName.startsWith("#"),
@@ -1917,7 +1925,7 @@ var Project = class {
1917
1925
  local: this.isUnresolvedLocalSpecifier(moduleName, pendingCandidates),
1918
1926
  pendingCandidates
1919
1927
  };
1920
- const existing = project.getSourceFile(name);
1928
+ const existing = this.project.has(name) ? project.getSourceFile(name) : void 0;
1921
1929
  if (existing) {
1922
1930
  this.removedSourcePaths.delete(name);
1923
1931
  return {
@@ -1936,12 +1944,10 @@ var Project = class {
1936
1944
  try {
1937
1945
  this.resolutionWork.sourceFilesRead++;
1938
1946
  const content = project.getFileSystem().readFileSync(name);
1939
- const sourceFile = project.createSourceFile(name, content, {
1940
- overwrite: true,
1941
- scriptKind: scriptKindFor(name)
1942
- });
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`);
1943
1949
  this.resolutionWork.sourceFilesAdded++;
1944
- this.canonicalPaths.set(name, this.normalizePath(sourceFile.getFilePath()));
1950
+ this.canonicalPaths.set(name, this.normalizePath(pathOf(sourceFile)));
1945
1951
  return {
1946
1952
  configurationFiles: [...configurationFiles].sort(),
1947
1953
  local: true,
@@ -1995,28 +2001,28 @@ var Project = class {
1995
2001
  };
1996
2002
  ensureResolutionFacts = (sourceFile) => {
1997
2003
  this.#assertNotLoading();
1998
- const importer = this.normalizePath(sourceFile.getFilePath());
2004
+ const importer = this.normalizePath(pathOf(sourceFile));
1999
2005
  const text = sourceFile.getFullText();
2000
2006
  const cached = this.resolutionsByImporter.get(importer);
2001
2007
  if (cached?.sourceFile === sourceFile && cached.text === text && cached.treeRevision === this.#fileTreeRevision) return cached;
2002
- const declarations = [...sourceFile.getImportDeclarations().map((declaration) => ({
2008
+ const declarations = [...getImportDeclarations(sourceFile).map((declaration) => ({
2003
2009
  declaration,
2004
2010
  kind: "import"
2005
- })), ...sourceFile.getExportDeclarations().map((declaration) => ({
2011
+ })), ...getExportDeclarations(sourceFile).map((declaration) => ({
2006
2012
  declaration,
2007
2013
  kind: "export"
2008
- }))].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());
2009
2015
  const facts = [];
2010
2016
  const pendingCandidates = [];
2011
2017
  const configurationFiles = [];
2012
2018
  for (const [ordinal, { declaration, kind }] of declarations.entries()) {
2013
- const specifier = declaration.getModuleSpecifierValue();
2019
+ const specifier = getModuleSpecifierValue(declaration);
2014
2020
  if (!specifier) continue;
2015
2021
  const resolved = this.resolveSpecifier(specifier, sourceFile);
2016
2022
  if (!resolved.local) continue;
2017
2023
  facts.push(Object.freeze({
2018
2024
  importer,
2019
- target: resolved.sourceFile ? this.normalizePath(resolved.sourceFile.getFilePath()) : null,
2025
+ target: resolved.sourceFile ? this.normalizePath(pathOf(resolved.sourceFile)) : null,
2020
2026
  specifier,
2021
2027
  kind,
2022
2028
  ordinal
@@ -2041,22 +2047,22 @@ var Project = class {
2041
2047
  };
2042
2048
  /** The sole cross-file source resolver supplied to parser and extractor. */
2043
2049
  resolveModule = (specifier, from) => {
2044
- 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);
2045
2051
  if (!target) return;
2046
2052
  const sourceFile = this.project.getSourceFile(target);
2047
2053
  if (!sourceFile) return;
2048
- this.prepareEffectiveSource(sourceFile.getFilePath(), sourceFile);
2054
+ this.prepareEffectiveSource(pathOf(sourceFile), sourceFile);
2049
2055
  return sourceFile;
2050
2056
  };
2051
2057
  trackDependencies = (filePath, sourceFile) => {
2052
2058
  this.#assertNotLoading();
2053
- const importer = this.normalizePath(sourceFile.getFilePath());
2059
+ const importer = this.normalizePath(pathOf(sourceFile));
2054
2060
  this.canonicalPaths.set(this.normalizePath(filePath), importer);
2055
2061
  this.canonicalPaths.set(importer, importer);
2056
2062
  this.ensureResolutionFacts(sourceFile);
2057
2063
  };
2058
2064
  invalidateSourcePreparation = (filePath, sourceFile) => {
2059
- const sourcePath = this.normalizePath(sourceFile?.getFilePath() ?? filePath);
2065
+ const sourcePath = this.normalizePath(pathOf(sourceFile) ?? filePath);
2060
2066
  const preparation = this.sourcePreparations.get(sourcePath);
2061
2067
  if (preparation?.state === "preparing") preparation.invalidated = true;
2062
2068
  this.sourcePreparations.delete(sourcePath);
@@ -2072,7 +2078,7 @@ var Project = class {
2072
2078
  */
2073
2079
  prepareEffectiveSource = (filePath, sourceFile, hookFilePath = filePath) => {
2074
2080
  this.#assertNotLoading();
2075
- const sourcePath = this.normalizePath(sourceFile.getFilePath());
2081
+ const sourcePath = this.normalizePath(pathOf(sourceFile));
2076
2082
  const currentText = sourceFile.getFullText();
2077
2083
  const current = this.sourcePreparations.get(sourcePath);
2078
2084
  if (current?.state === "ready" && current.sourceFile === sourceFile && current.effectiveText === currentText) {
@@ -2117,7 +2123,7 @@ var Project = class {
2117
2123
  assertTransaction(currentText);
2118
2124
  const transformed = custom ?? this.transformFile(hookFilePath, currentText);
2119
2125
  assertTransaction(currentText);
2120
- if (currentText !== transformed) sourceFile.replaceWithText(transformed);
2126
+ if (currentText !== transformed) sourceFile = this.project.addSourceFile(filePath, transformed) ?? sourceFile;
2121
2127
  assertTransaction(transformed);
2122
2128
  this.trackDependencies(filePath, sourceFile);
2123
2129
  assertTransaction(transformed);
@@ -2134,7 +2140,7 @@ var Project = class {
2134
2140
  if (this.sourcePreparations.get(sourcePath) === transaction) this.sourcePreparations.delete(sourcePath);
2135
2141
  this.retractImporter(sourcePath);
2136
2142
  try {
2137
- if (sourceFile.getFullText() !== currentText) sourceFile.replaceWithText(currentText);
2143
+ if (sourceFile.getFullText() !== currentText) this.project.addSourceFile(filePath, currentText);
2138
2144
  } catch {}
2139
2145
  throw error;
2140
2146
  }
@@ -2161,7 +2167,7 @@ var Project = class {
2161
2167
  this.retractImporter(target);
2162
2168
  this.removedSourcePaths.add(target);
2163
2169
  this.canonicalPaths.set(target, target);
2164
- this.canonicalPaths.set(this.normalizePath(sourceFile.getFilePath()), target);
2170
+ this.canonicalPaths.set(this.normalizePath(pathOf(sourceFile)), target);
2165
2171
  };
2166
2172
  /**
2167
2173
  * Every file that transitively imports `filePath`, so a watcher can re-parse the
@@ -2170,7 +2176,7 @@ var Project = class {
2170
2176
  getDependents = (filePath) => {
2171
2177
  this.#assertNotLoading();
2172
2178
  const given = this.normalizePath(filePath);
2173
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2179
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2174
2180
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2175
2181
  const seen = /* @__PURE__ */ new Set();
2176
2182
  const queue = [start];
@@ -2198,7 +2204,7 @@ var Project = class {
2198
2204
  getDependencies = (filePath, targets) => {
2199
2205
  this.#assertNotLoading();
2200
2206
  const given = this.normalizePath(filePath);
2201
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2207
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2202
2208
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2203
2209
  const seen = /* @__PURE__ */ new Set();
2204
2210
  const importersByDependency = /* @__PURE__ */ new Map();
@@ -2219,7 +2225,7 @@ var Project = class {
2219
2225
  const selected = /* @__PURE__ */ new Set();
2220
2226
  const reverse = Array.from(targets, (target) => {
2221
2227
  const normalized = this.normalizePath(target);
2222
- const source = this.project.getSourceFile(target)?.getFilePath();
2228
+ const source = pathOf(this.project.getSourceFile(target));
2223
2229
  return source ? this.normalizePath(source) : this.canonicalPaths.get(normalized) ?? normalized;
2224
2230
  }).filter((target) => seen.has(target));
2225
2231
  let reverseCursor = 0;
@@ -2245,7 +2251,7 @@ var Project = class {
2245
2251
  const selected = new Set(dependencies);
2246
2252
  const retained = new Set([...previous?.dependencies ?? [], ...previous?.pendingCandidates ?? []]);
2247
2253
  const given = this.normalizePath(filePath);
2248
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2254
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2249
2255
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2250
2256
  const pendingCandidates = /* @__PURE__ */ new Set();
2251
2257
  for (const importer of [start, ...dependencies]) {
@@ -2278,7 +2284,7 @@ var Project = class {
2278
2284
  const selected = new Set(dependencies);
2279
2285
  const retained = new Set(Array.from(previous, (file) => this.normalizePath(file)));
2280
2286
  const given = this.normalizePath(filePath);
2281
- const resolved = this.project.getSourceFile(filePath)?.getFilePath();
2287
+ const resolved = pathOf(this.project.getSourceFile(filePath));
2282
2288
  const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
2283
2289
  const files = /* @__PURE__ */ new Set();
2284
2290
  let hasSemanticFact = false;
@@ -2305,10 +2311,9 @@ var Project = class {
2305
2311
  this.invalidateSourcePreparation(filePath, existing);
2306
2312
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2307
2313
  this.invalidate();
2308
- return this.project.createSourceFile(filePath, content, {
2309
- overwrite: true,
2310
- scriptKind: scriptKindFor(filePath)
2311
- });
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;
2312
2317
  };
2313
2318
  createSourceFiles = () => {
2314
2319
  this.#assertNotLoading();
@@ -2347,17 +2352,15 @@ var Project = class {
2347
2352
  * matches its own source, falls through, and is overwritten exactly as before.
2348
2353
  */
2349
2354
  if (existing && existing.getFullText() === content) {
2350
- this.markAuxiliary(existing.getFilePath(), options.auxiliary);
2355
+ this.markAuxiliary(pathOf(existing), options.auxiliary);
2351
2356
  return existing;
2352
2357
  }
2353
2358
  this.invalidateSourcePreparation(filePath, existing);
2354
2359
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2355
- this.invalidate(!existing, existing?.getFilePath());
2356
- const sourceFile = this.project.createSourceFile(filePath, content, {
2357
- overwrite: true,
2358
- scriptKind: scriptKindFor(filePath)
2359
- });
2360
- 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);
2361
2364
  return sourceFile;
2362
2365
  };
2363
2366
  /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
@@ -2373,10 +2376,10 @@ var Project = class {
2373
2376
  if (sourceFile) {
2374
2377
  this.invalidate();
2375
2378
  this.invalidateSourcePreparation(filePath, sourceFile);
2376
- this.markTargetRemoved(this.normalizePath(sourceFile.getFilePath()), sourceFile);
2377
- this.options.parserOptions.encoder.releaseFile(sourceFile.getFilePath());
2378
- this.auxiliarySources.delete(this.normalizePath(sourceFile.getFilePath()));
2379
- 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));
2380
2383
  }
2381
2384
  return false;
2382
2385
  };
@@ -2395,10 +2398,10 @@ var Project = class {
2395
2398
  this.#assertNotLoading();
2396
2399
  this.#ensureSourceFiles();
2397
2400
  const sourceFile = this.getSourceFile(filePath);
2398
- this.invalidate(false, sourceFile?.getFilePath());
2401
+ this.invalidate(false, pathOf(sourceFile));
2399
2402
  if (!sourceFile) return;
2400
2403
  this.invalidateSourcePreparation(filePath, sourceFile);
2401
- return sourceFile.refreshFromFileSystemSync();
2404
+ return this.project.reloadSourceFile(filePath);
2402
2405
  };
2403
2406
  reloadSourceFiles = () => {
2404
2407
  this.#assertNotLoading();
@@ -2409,7 +2412,7 @@ var Project = class {
2409
2412
  const source = this.getSourceFile(file);
2410
2413
  if (source) {
2411
2414
  this.invalidateSourcePreparation(file, source);
2412
- source.refreshFromFileSystemSync();
2415
+ this.project.reloadSourceFile(file);
2413
2416
  } else {
2414
2417
  this.invalidateSourcePreparation(file);
2415
2418
  this.removedSourcePaths.delete(this.normalizePath(file));
@@ -2451,10 +2454,12 @@ var Project = class {
2451
2454
  const { hooks } = this.options;
2452
2455
  const hookFilePath = options.hookFilePath ?? filePath;
2453
2456
  if (filePath.endsWith(".json")) return this.parseJson(filePath, encoder);
2454
- const sourceFile = this.project.getSourceFile(filePath);
2457
+ let sourceFile = this.project.getSourceFile(filePath);
2455
2458
  if (!sourceFile) return;
2456
- const { options: parserOptions } = this.prepareEffectiveSource(filePath, sourceFile, hookFilePath);
2457
- 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);
2458
2463
  hooks["parser:after"]?.({
2459
2464
  filePath: hookFilePath,
2460
2465
  result