@bamboocss/extractor 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 CHANGED
@@ -306,6 +306,65 @@ const box = {
306
306
  }
307
307
  };
308
308
  //#endregion
309
+ //#region src/dependency-cache.ts
310
+ const EMPTY_DEPENDENCIES = [];
311
+ /** One synchronous extraction context owns one nested capture stack. */
312
+ const trackers = /* @__PURE__ */ new WeakMap();
313
+ /**
314
+ * Attribute one resolved local module to every cache computation currently enclosing it.
315
+ *
316
+ * Paths, rather than SourceFile objects, are the replay payload. A source replacement may
317
+ * retire every node in a file, while its stable path is exactly the fact a fresh ParserResult
318
+ * needs. The caller-facing callback stays outside the cache and therefore belongs to the
319
+ * current query/environment rather than whichever parse populated the entry.
320
+ */
321
+ const recordModuleDependency = (ctx, filePath) => {
322
+ const normalized = filePath.replaceAll("\\", "/");
323
+ const tracker = trackers.get(ctx);
324
+ if (tracker) for (const frame of tracker.frames) {
325
+ frame.dependencies ??= /* @__PURE__ */ new Set();
326
+ frame.dependencies.add(normalized);
327
+ }
328
+ ctx.recordDependency?.(normalized);
329
+ };
330
+ /** Begin one nested cache computation without allocating a Set on dependency-free paths. */
331
+ const beginDependencyCapture = (ctx) => {
332
+ let tracker = trackers.get(ctx);
333
+ if (!tracker) {
334
+ tracker = { frames: [] };
335
+ trackers.set(ctx, tracker);
336
+ }
337
+ const frame = {};
338
+ tracker.frames.push(frame);
339
+ return {
340
+ entry(value) {
341
+ return {
342
+ dependencies: frame.dependencies ? [...frame.dependencies].sort() : EMPTY_DEPENDENCIES,
343
+ resolveModule: ctx.resolveModule,
344
+ value
345
+ };
346
+ },
347
+ end() {
348
+ tracker.frames.pop();
349
+ }
350
+ };
351
+ };
352
+ /**
353
+ * Replay a hit only inside the resolver scope which computed it.
354
+ *
355
+ * The same ts-morph node can be inspected under two extractor contexts with different module
356
+ * placement rules. Returning the first context's value or dependency paths to the second would
357
+ * be cross-Project leakage, so a scope mismatch is a cache miss and gets overwritten normally.
358
+ */
359
+ const replayDependencyCache = (entry, ctx) => {
360
+ if (entry.resolveModule !== ctx.resolveModule) return { hit: false };
361
+ for (const dependency of entry.dependencies) recordModuleDependency(ctx, dependency);
362
+ return {
363
+ hit: true,
364
+ value: entry.value
365
+ };
366
+ };
367
+ //#endregion
309
368
  //#region src/resolve-imported-value.ts
310
369
  /**
311
370
  * The values an expression borrows from other modules, resolved by reading the imports.
@@ -322,9 +381,9 @@ const box = {
322
381
  * rather than with the number of style calls — a large project reported a 5.4x extraction
323
382
  * slowdown and an OOM in CI.
324
383
  *
325
- * Following an import is two cheap steps this package already had: resolve the specifier to
326
- * a file (`ts.resolveModuleName`, path lookup, no checker), then read that file's exported
327
- * declaration. Crossing the import boundary is the only thing the checker was doing here.
384
+ * Following an import is two cheap steps: ask the caller's source-graph resolver to place the
385
+ * specifier, then read that file's exported declaration. Crossing the import boundary is the
386
+ * only thing the checker was doing here.
328
387
  *
329
388
  * The evaluator resolves everything *within* a module by walking scopes, so a helper that
330
389
  * refers to another binding in its own file needs nothing from us.
@@ -332,7 +391,14 @@ const box = {
332
391
  /** How far a chain of helpers importing helpers is followed. */
333
392
  const MAX_DEPTH = 4;
334
393
  /** Evaluated once per declaration, however many call sites reach it. */
335
- const evaluatedValues = /* @__PURE__ */ new WeakMap();
394
+ let evaluatedValues = /* @__PURE__ */ new WeakMap();
395
+ let hasEvaluatedValues = false;
396
+ /** @internal Drop imported declarations whose closure may include an edited module. */
397
+ const clearImportedValueCache = () => {
398
+ if (!hasEvaluatedValues) return;
399
+ evaluatedValues = /* @__PURE__ */ new WeakMap();
400
+ hasEvaluatedValues = false;
401
+ };
336
402
  /**
337
403
  * Local name to the name and declaration it was imported from.
338
404
  *
@@ -362,17 +428,28 @@ const importBindingsFor = (sourceFile) => {
362
428
  };
363
429
  /** What an imported name refers to, or nothing if it cannot be reached safely. */
364
430
  const valueForBinding = (binding, ctx, stack, depth, evaluateExpression) => {
365
- const sourceFile = getModuleSpecifierSourceFile(binding.declaration);
431
+ const sourceFile = getModuleSpecifierSourceFile(binding.declaration, ctx);
366
432
  if (!sourceFile) return;
367
433
  if (sourceFile.isInNodeModules()) return;
368
434
  const declaration = getExportedVarDeclarationWithName(binding.exportedName, sourceFile, stack, ctx);
369
435
  if (!declaration) return;
370
- if (evaluatedValues.has(declaration)) return { value: evaluatedValues.get(declaration) };
436
+ const cached = evaluatedValues.get(declaration);
437
+ if (cached) {
438
+ const replayed = replayDependencyCache(cached, ctx);
439
+ if (replayed.hit) return { value: replayed.value };
440
+ }
371
441
  const initializer = declaration.getInitializer();
372
442
  if (!initializer) return;
373
- const value = evaluateExpression(initializer, stack, ctx, depth + 1);
443
+ const capture = beginDependencyCapture(ctx);
444
+ let value;
445
+ try {
446
+ value = evaluateExpression(initializer, stack, ctx, depth + 1);
447
+ } finally {
448
+ capture.end();
449
+ }
374
450
  if (value === void 0) return;
375
- evaluatedValues.set(declaration, value);
451
+ evaluatedValues.set(declaration, capture.entry(value));
452
+ hasEvaluatedValues = true;
376
453
  return { value };
377
454
  };
378
455
  /**
@@ -385,27 +462,34 @@ const valueForBinding = (binding, ctx, stack, depth, evaluateExpression) => {
385
462
  */
386
463
  const importedEnvironmentFor = (node, ctx, stack, depth, evaluateExpression) => {
387
464
  if (depth >= MAX_DEPTH) return;
388
- const calls = node.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression);
389
- if (ts_morph.Node.isCallExpression(node)) calls.unshift(node);
390
- if (!calls.length) return;
391
465
  const bindings = importBindingsFor(node.getSourceFile());
392
466
  if (!bindings.size) return;
467
+ const references = node.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier);
468
+ if (ts_morph.Node.isIdentifier(node)) references.unshift(node);
469
+ if (!references.length) return;
393
470
  let environment;
394
- for (const call of calls) {
395
- const callee = call.getExpression();
396
- const binding = bindings.get(callee.getText());
397
- if (!binding || environment?.[callee.getText()] !== void 0) continue;
471
+ for (const reference of references) {
472
+ const name = reference.getText();
473
+ const binding = bindings.get(name);
474
+ if (!binding || environment?.[name] !== void 0) continue;
398
475
  const resolved = valueForBinding(binding, ctx, stack, depth, evaluateExpression);
399
476
  if (!resolved) continue;
400
477
  environment ??= {};
401
- environment[callee.getText()] = resolved.value;
478
+ environment[name] = resolved.value;
402
479
  }
403
480
  return environment;
404
481
  };
405
482
  //#endregion
406
483
  //#region src/evaluate-node.ts
407
484
  const TsEvalError = Symbol("EvalError");
408
- const cacheMap$2 = /* @__PURE__ */ new WeakMap();
485
+ let cacheMap$2 = /* @__PURE__ */ new WeakMap();
486
+ let hasCachedEntries$1 = false;
487
+ /** @internal Drop values whose expression may close over an edited source file. */
488
+ const clearEvaluateNodeCache = () => {
489
+ if (!hasCachedEntries$1) return;
490
+ cacheMap$2 = /* @__PURE__ */ new WeakMap();
491
+ hasCachedEntries$1 = false;
492
+ };
409
493
  /** @see https://github.com/wessberg/ts-evaluator#setting-up-policies */
410
494
  const POLICY = {
411
495
  console: false,
@@ -436,13 +520,39 @@ const POLICY = {
436
520
  const evaluateNode = (node, stack, ctx, depth = 0) => {
437
521
  if (ctx.flags?.skipEvaluate) return;
438
522
  if (ctx.canEval && !ctx.canEval?.(node, stack)) return;
439
- if (depth === 0 && cacheMap$2.has(node)) return cacheMap$2.get(node);
440
- const options = {
523
+ const cached = depth === 0 ? cacheMap$2.get(node) : void 0;
524
+ if (cached) {
525
+ const replayed = replayDependencyCache(cached, ctx);
526
+ if (replayed.hit) return replayed.value;
527
+ }
528
+ const capture = depth === 0 ? beginDependencyCapture(ctx) : void 0;
529
+ try {
530
+ return evaluateNodeUncached(node, stack, ctx, depth, capture?.entry);
531
+ } finally {
532
+ capture?.end();
533
+ }
534
+ };
535
+ const evaluateNodeUncached = (node, stack, ctx, depth, dependencyEntry) => {
536
+ let options = {
441
537
  policy: { ...POLICY },
442
538
  ...ctx.getEvaluateOptions?.(node, stack),
443
539
  node: node.compilerNode,
444
540
  typescript: ts_morph.ts
445
541
  };
542
+ let imported = depth > 0 ? importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode) : void 0;
543
+ if (imported) {
544
+ const environment = options.environment;
545
+ options = {
546
+ ...options,
547
+ environment: {
548
+ ...environment,
549
+ extra: {
550
+ ...environment?.extra,
551
+ ...imported
552
+ }
553
+ }
554
+ };
555
+ }
446
556
  let result = (0, ts_evaluator.evaluate)(options);
447
557
  /**
448
558
  * Retried only on failure, with whatever this expression imports put in scope.
@@ -454,17 +564,26 @@ const evaluateNode = (node, stack, ctx, depth = 0) => {
454
564
  * the path of every build.
455
565
  */
456
566
  if (!result.success) {
457
- const imported = importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode);
458
- if (imported) result = (0, ts_evaluator.evaluate)({
459
- ...options,
460
- environment: {
461
- ...options.environment,
462
- extra: imported
463
- }
464
- });
567
+ imported ??= importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode);
568
+ if (imported) {
569
+ const environment = options.environment;
570
+ result = (0, ts_evaluator.evaluate)({
571
+ ...options,
572
+ environment: {
573
+ ...environment,
574
+ extra: {
575
+ ...environment?.extra,
576
+ ...imported
577
+ }
578
+ }
579
+ });
580
+ }
465
581
  }
466
582
  const expr = result.success ? result.value : TsEvalError;
467
- if (depth === 0) cacheMap$2.set(node, expr);
583
+ if (depth === 0) {
584
+ cacheMap$2.set(node, dependencyEntry(expr));
585
+ hasCachedEntries$1 = true;
586
+ }
468
587
  return expr;
469
588
  };
470
589
  const safeEvaluateNode = (node, stack, ctx, depth = 0) => {
@@ -487,7 +606,7 @@ function getDeclarationFor(node, stack, ctx) {
487
606
  declaration = parent;
488
607
  } else if (ts_morph.Node.isImportSpecifier(parent) && (parent.getNameNode() == node || parent.getAliasNode() == node)) {
489
608
  if (ctx.flags?.skipTraverseFiles) return;
490
- const sourceFile = getModuleSpecifierSourceFile(parent.getImportDeclaration());
609
+ const sourceFile = getModuleSpecifierSourceFile(parent.getImportDeclaration(), ctx);
491
610
  if (sourceFile) {
492
611
  const exportStack = [parent, sourceFile];
493
612
  const maybeVar = getExportedVarDeclarationWithName(parent.getNameNode().getText(), sourceFile, exportStack, ctx);
@@ -505,53 +624,74 @@ const getInnermostScope = (from) => {
505
624
  while (scope && !isScope(scope)) scope = scope.getParent();
506
625
  return scope;
507
626
  };
627
+ /**
628
+ * Keyed on the compiler node, never the ts-morph wrapper.
629
+ *
630
+ * `Project.replaceWithText` — which is how a re-parse installs a source transform — keeps the
631
+ * wrapper identity and swaps the compiler node underneath it. A wrapper-keyed cache would
632
+ * therefore answer a rebuild from the previous revision of the file, which is a wrong
633
+ * stylesheet rather than a slow one. Keying on the compiler node makes the cache
634
+ * self-invalidating: ts-morph reuses a compiler node exactly when its subtree did not change.
635
+ */
636
+ const declarationIndexes = /* @__PURE__ */ new WeakMap();
637
+ const isDeclarationName = (node, parent) => {
638
+ if (ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isEnumDeclaration(parent) || ts_morph.Node.isBindingElement(parent)) return parent.getNameNode() == node;
639
+ return ts_morph.Node.isImportSpecifier(parent) && (parent.getNameNode() == node || parent.getAliasNode() == node);
640
+ };
641
+ /**
642
+ * Walk a scope once, so that every later lookup inside it is a map read.
643
+ *
644
+ * This used to be a `forEachDescendant` per identifier, widening to each enclosing scope and
645
+ * re-walking from scratch, calling `getText()` on every identifier it passed. A module whose
646
+ * declarations are referenced n times paid n full traversals of itself, and traversal is where
647
+ * ts-morph charges for wrapping each node — it measured ~10% of extraction on its own.
648
+ */
649
+ const declarationIndexFor = (scope) => {
650
+ const cached = declarationIndexes.get(scope.compilerNode);
651
+ if (cached) return cached;
652
+ const index = /* @__PURE__ */ new Map();
653
+ scope.forEachDescendant((node) => {
654
+ if (!ts_morph.Node.isIdentifier(node)) return;
655
+ const parent = node.getParent();
656
+ if (!parent || !isDeclarationName(node, parent)) return;
657
+ const name = node.getText();
658
+ const declared = index.get(name);
659
+ if (declared) declared.push(node);
660
+ else index.set(name, [node]);
661
+ });
662
+ declarationIndexes.set(scope.compilerNode, index);
663
+ return index;
664
+ };
508
665
  function findIdentifierValueDeclaration(identifier, stack, ctx, visitedsWithStack = /* @__PURE__ */ new Map()) {
509
666
  let scope = identifier;
510
- let foundNode;
511
- let isUnresolvable = false;
512
667
  let count = 0;
513
668
  const innerStack = [];
669
+ const refName = identifier.getText();
514
670
  do {
515
671
  scope = getInnermostScope(scope);
516
672
  count++;
517
673
  if (!scope) return;
518
- const refName = identifier.getText();
519
- scope.forEachDescendant((node, traversal) => {
520
- if (visitedsWithStack.has(node)) {
521
- traversal.skip();
522
- innerStack.push(...visitedsWithStack.get(node));
523
- return;
674
+ for (const candidate of declarationIndexFor(scope).get(refName) ?? []) {
675
+ if (candidate == identifier) continue;
676
+ if (visitedsWithStack.has(candidate)) continue;
677
+ visitedsWithStack.set(candidate, innerStack);
678
+ const declarationStack = [candidate];
679
+ const maybeDeclaration = getDeclarationFor(candidate, declarationStack, ctx);
680
+ if (!maybeDeclaration) continue;
681
+ if (ts_morph.Node.isParameterDeclaration(maybeDeclaration)) {
682
+ const initializer = maybeDeclaration.getInitializer();
683
+ const typeNode = maybeDeclaration.getTypeNode();
684
+ if (initializer) innerStack.push(...declarationStack.concat(initializer));
685
+ else if (typeNode && ts_morph.Node.isTypeLiteral(typeNode)) innerStack.push(...declarationStack.concat(typeNode));
686
+ else return;
687
+ stack.push(...innerStack);
688
+ return maybeDeclaration;
524
689
  }
525
- if (node == identifier) return;
526
- visitedsWithStack.set(node, innerStack);
527
- if (ts_morph.Node.isIdentifier(node) && node.getText() == refName) {
528
- const declarationStack = [node];
529
- const maybeDeclaration = getDeclarationFor(node, declarationStack, ctx);
530
- if (maybeDeclaration) {
531
- if (ts_morph.Node.isParameterDeclaration(maybeDeclaration)) {
532
- const initializer = maybeDeclaration.getInitializer();
533
- const typeNode = maybeDeclaration.getTypeNode();
534
- if (initializer) {
535
- innerStack.push(...declarationStack.concat(initializer));
536
- foundNode = maybeDeclaration;
537
- } else if (typeNode && ts_morph.Node.isTypeLiteral(typeNode)) {
538
- innerStack.push(...declarationStack.concat(typeNode));
539
- foundNode = maybeDeclaration;
540
- } else isUnresolvable = true;
541
- traversal.stop();
542
- return;
543
- }
544
- innerStack.push(...declarationStack);
545
- foundNode = maybeDeclaration;
546
- traversal.stop();
547
- }
548
- }
549
- });
550
- if (foundNode || isUnresolvable) {
551
- if (foundNode) stack.push(...innerStack);
552
- return foundNode;
690
+ innerStack.push(...declarationStack);
691
+ stack.push(...innerStack);
692
+ return maybeDeclaration;
553
693
  }
554
- } while (scope && !ts_morph.Node.isSourceFile(scope) && !foundNode && !isUnresolvable && count < 100);
694
+ } while (scope && !ts_morph.Node.isSourceFile(scope) && count < 100);
555
695
  }
556
696
  //#endregion
557
697
  //#region src/get-property-name.ts
@@ -665,7 +805,6 @@ const getObjectLiteralExpressionPropPairs = (expression, expressionStack, ctx, m
665
805
  //#endregion
666
806
  //#region src/maybe-box-node.ts
667
807
  let cacheMap$1 = /* @__PURE__ */ new WeakMap();
668
- const isCached = (node) => cacheMap$1.has(node);
669
808
  const getCached = (node) => cacheMap$1.get(node);
670
809
  /**
671
810
  * Drops every memoized resolution.
@@ -676,6 +815,8 @@ const getCached = (node) => cacheMap$1.get(node);
676
815
  * otherwise keep serving the value read before the edit.
677
816
  */
678
817
  const clearBoxNodeCache = () => {
818
+ clearEvaluateNodeCache();
819
+ clearImportedValueCache();
679
820
  if (!hasCachedEntries) return;
680
821
  cacheMap$1 = /* @__PURE__ */ new WeakMap();
681
822
  hasCachedEntries = false;
@@ -688,12 +829,24 @@ const canReturnWhenTrueInLogicalExpression = (op) => {
688
829
  return op === ts_morph.ts.SyntaxKind.BarBarToken || op === ts_morph.ts.SyntaxKind.QuestionQuestionToken;
689
830
  };
690
831
  function maybeBoxNode(node, stack, ctx, matchProp) {
832
+ const cached = getCached(node);
833
+ if (cached) {
834
+ const replayed = replayDependencyCache(cached, ctx);
835
+ if (replayed.hit) return replayed.value;
836
+ }
837
+ const capture = beginDependencyCapture(ctx);
838
+ try {
839
+ return maybeBoxNodeUncached(node, stack, ctx, matchProp, capture.entry);
840
+ } finally {
841
+ capture.end();
842
+ }
843
+ }
844
+ function maybeBoxNodeUncached(node, stack, ctx, matchProp, dependencyEntry) {
691
845
  const cache = (value) => {
692
- cacheMap$1.set(node, value);
846
+ cacheMap$1.set(node, dependencyEntry(value));
693
847
  hasCachedEntries = true;
694
848
  return value;
695
849
  };
696
- if (isCached(node)) return getCached(node);
697
850
  if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node)) {
698
851
  const value = trimWhitespace(node.getLiteralValue());
699
852
  return cache(box.literal(value, node, stack));
@@ -1127,21 +1280,19 @@ const resolveExportedName = (name, exportDeclaration) => {
1127
1280
  * which costs a minimum of around 90ms (and scales up with the file/project, could be hundreds of ms)
1128
1281
  * @see https://github.com/dsherret/ts-morph/blob/42d811ed9a5177fc678a5bfec4923a2048124fe0/packages/ts-morph/src/compiler/ast/module/ExportDeclaration.ts#L160
1129
1282
  */
1130
- const getModuleSpecifierSourceFile = (declaration) => {
1131
- const project = declaration.getProject();
1283
+ const getModuleSpecifierSourceFile = (declaration, ctx) => {
1132
1284
  const moduleName = declaration.getModuleSpecifierValue();
1133
1285
  if (!moduleName) return;
1134
- const containingFile = declaration.getSourceFile().getFilePath();
1135
- const resolved = ts_morph.ts.resolveModuleName(moduleName, containingFile, project.getCompilerOptions(), project.getModuleResolutionHost());
1136
- if (!resolved.resolvedModule) return;
1137
- return project.addSourceFileAtPath(resolved.resolvedModule.resolvedFileName);
1286
+ const sourceFile = ctx.resolveModule?.(moduleName, declaration.getSourceFile());
1287
+ if (sourceFile) recordModuleDependency(ctx, sourceFile.getFilePath());
1288
+ return sourceFile;
1138
1289
  };
1139
1290
  function resolveVarDeclarationFromExportWithName(symbolName, sourceFile, stack, ctx, visited) {
1140
1291
  for (const exportDeclaration of sourceFile.getExportDeclarations()) {
1141
1292
  const exportStack = [exportDeclaration];
1142
1293
  const sourceName = resolveExportedName(symbolName, exportDeclaration);
1143
1294
  if (sourceName === void 0) continue;
1144
- const maybeFile = getModuleSpecifierSourceFile(exportDeclaration);
1295
+ const maybeFile = getModuleSpecifierSourceFile(exportDeclaration, ctx);
1145
1296
  if (!maybeFile) {
1146
1297
  const localVar = sourceFile.getVariableDeclaration(sourceName);
1147
1298
  if (localVar) {
package/dist/index.d.cts CHANGED
@@ -184,7 +184,18 @@ interface ComponentMatchers {
184
184
  matchTag: (element: MatchTagArgs) => boolean;
185
185
  matchProp: (prop: Pick<MatchTagArgs, 'tagName' | 'tagNode'> & MatchPropArgs) => boolean;
186
186
  }
187
+ /**
188
+ * Place a module specifier inside the source graph owned by the caller.
189
+ *
190
+ * @internal Bamboo's parser supplies its Project resolver. The extractor deliberately has
191
+ * no filesystem fallback: a cross-file read that bypasses the owner cannot be invalidated or
192
+ * represented in the owner's dependency graph.
193
+ */
194
+ type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
187
195
  interface BoxContext {
196
+ resolveModule?: ResolveModule;
197
+ /** @internal Receives local source paths crossed by this exact extraction context. */
198
+ recordDependency?: (filePath: string) => void;
188
199
  getEvaluateOptions?: (node: Expression, stack: Node[]) => Omit<EvaluateOptions, 'node' | 'policy'> | void;
189
200
  canEval?: (node: Expression, stack: Node[]) => boolean;
190
201
  tokens?: {
@@ -324,4 +335,4 @@ interface Unboxed {
324
335
  }
325
336
  declare const unbox: (node: BoxNodeType, ctx?: Pick<UnboxContext, "cache">) => Unboxed;
326
337
  //#endregion
327
- export { type BoxContext, type BoxNode, BoxNodeArray, BoxNodeConditional, BoxNodeEmptyInitializer, BoxNodeLiteral, BoxNodeMap, BoxNodeObject, BoxNodeUnresolvable, type EvaluateOptions, type ExtractOptions, type ExtractResultByName, type ExtractResultItem, type ExtractedComponentInstance, type ExtractedComponentResult, type ExtractedFunctionInstance, type ExtractedFunctionResult, type NodeRange, type PrimitiveType, type Unboxed, box, clearBoxNodeCache, extract, extractCallExpressionArguments, extractJsxAttribute, extractJsxElementProps, extractJsxSpreadAttributeValues, findIdentifierValueDeclaration, isBoxNode, maybeBoxNode, maybeIdentifierValue, unbox, unwrapExpression };
338
+ export { type BoxContext, type BoxNode, BoxNodeArray, BoxNodeConditional, BoxNodeEmptyInitializer, BoxNodeLiteral, BoxNodeMap, BoxNodeObject, BoxNodeUnresolvable, type EvaluateOptions, type ExtractOptions, type ExtractResultByName, type ExtractResultItem, type ExtractedComponentInstance, type ExtractedComponentResult, type ExtractedFunctionInstance, type ExtractedFunctionResult, type NodeRange, type PrimitiveType, type ResolveModule, type Unboxed, box, clearBoxNodeCache, extract, extractCallExpressionArguments, extractJsxAttribute, extractJsxElementProps, extractJsxSpreadAttributeValues, findIdentifierValueDeclaration, isBoxNode, maybeBoxNode, maybeIdentifierValue, unbox, unwrapExpression };
package/dist/index.d.mts CHANGED
@@ -184,7 +184,18 @@ interface ComponentMatchers {
184
184
  matchTag: (element: MatchTagArgs) => boolean;
185
185
  matchProp: (prop: Pick<MatchTagArgs, 'tagName' | 'tagNode'> & MatchPropArgs) => boolean;
186
186
  }
187
+ /**
188
+ * Place a module specifier inside the source graph owned by the caller.
189
+ *
190
+ * @internal Bamboo's parser supplies its Project resolver. The extractor deliberately has
191
+ * no filesystem fallback: a cross-file read that bypasses the owner cannot be invalidated or
192
+ * represented in the owner's dependency graph.
193
+ */
194
+ type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
187
195
  interface BoxContext {
196
+ resolveModule?: ResolveModule;
197
+ /** @internal Receives local source paths crossed by this exact extraction context. */
198
+ recordDependency?: (filePath: string) => void;
188
199
  getEvaluateOptions?: (node: Expression, stack: Node[]) => Omit<EvaluateOptions, 'node' | 'policy'> | void;
189
200
  canEval?: (node: Expression, stack: Node[]) => boolean;
190
201
  tokens?: {
@@ -324,4 +335,4 @@ interface Unboxed {
324
335
  }
325
336
  declare const unbox: (node: BoxNodeType, ctx?: Pick<UnboxContext, "cache">) => Unboxed;
326
337
  //#endregion
327
- export { type BoxContext, type BoxNode, BoxNodeArray, BoxNodeConditional, BoxNodeEmptyInitializer, BoxNodeLiteral, BoxNodeMap, BoxNodeObject, BoxNodeUnresolvable, type EvaluateOptions, type ExtractOptions, type ExtractResultByName, type ExtractResultItem, type ExtractedComponentInstance, type ExtractedComponentResult, type ExtractedFunctionInstance, type ExtractedFunctionResult, type NodeRange, type PrimitiveType, type Unboxed, box, clearBoxNodeCache, extract, extractCallExpressionArguments, extractJsxAttribute, extractJsxElementProps, extractJsxSpreadAttributeValues, findIdentifierValueDeclaration, isBoxNode, maybeBoxNode, maybeIdentifierValue, unbox, unwrapExpression };
338
+ export { type BoxContext, type BoxNode, BoxNodeArray, BoxNodeConditional, BoxNodeEmptyInitializer, BoxNodeLiteral, BoxNodeMap, BoxNodeObject, BoxNodeUnresolvable, type EvaluateOptions, type ExtractOptions, type ExtractResultByName, type ExtractResultItem, type ExtractedComponentInstance, type ExtractedComponentResult, type ExtractedFunctionInstance, type ExtractedFunctionResult, type NodeRange, type PrimitiveType, type ResolveModule, type Unboxed, box, clearBoxNodeCache, extract, extractCallExpressionArguments, extractJsxAttribute, extractJsxElementProps, extractJsxSpreadAttributeValues, findIdentifierValueDeclaration, isBoxNode, maybeBoxNode, maybeIdentifierValue, unbox, unwrapExpression };
package/dist/index.mjs CHANGED
@@ -305,6 +305,65 @@ const box = {
305
305
  }
306
306
  };
307
307
  //#endregion
308
+ //#region src/dependency-cache.ts
309
+ const EMPTY_DEPENDENCIES = [];
310
+ /** One synchronous extraction context owns one nested capture stack. */
311
+ const trackers = /* @__PURE__ */ new WeakMap();
312
+ /**
313
+ * Attribute one resolved local module to every cache computation currently enclosing it.
314
+ *
315
+ * Paths, rather than SourceFile objects, are the replay payload. A source replacement may
316
+ * retire every node in a file, while its stable path is exactly the fact a fresh ParserResult
317
+ * needs. The caller-facing callback stays outside the cache and therefore belongs to the
318
+ * current query/environment rather than whichever parse populated the entry.
319
+ */
320
+ const recordModuleDependency = (ctx, filePath) => {
321
+ const normalized = filePath.replaceAll("\\", "/");
322
+ const tracker = trackers.get(ctx);
323
+ if (tracker) for (const frame of tracker.frames) {
324
+ frame.dependencies ??= /* @__PURE__ */ new Set();
325
+ frame.dependencies.add(normalized);
326
+ }
327
+ ctx.recordDependency?.(normalized);
328
+ };
329
+ /** Begin one nested cache computation without allocating a Set on dependency-free paths. */
330
+ const beginDependencyCapture = (ctx) => {
331
+ let tracker = trackers.get(ctx);
332
+ if (!tracker) {
333
+ tracker = { frames: [] };
334
+ trackers.set(ctx, tracker);
335
+ }
336
+ const frame = {};
337
+ tracker.frames.push(frame);
338
+ return {
339
+ entry(value) {
340
+ return {
341
+ dependencies: frame.dependencies ? [...frame.dependencies].sort() : EMPTY_DEPENDENCIES,
342
+ resolveModule: ctx.resolveModule,
343
+ value
344
+ };
345
+ },
346
+ end() {
347
+ tracker.frames.pop();
348
+ }
349
+ };
350
+ };
351
+ /**
352
+ * Replay a hit only inside the resolver scope which computed it.
353
+ *
354
+ * The same ts-morph node can be inspected under two extractor contexts with different module
355
+ * placement rules. Returning the first context's value or dependency paths to the second would
356
+ * be cross-Project leakage, so a scope mismatch is a cache miss and gets overwritten normally.
357
+ */
358
+ const replayDependencyCache = (entry, ctx) => {
359
+ if (entry.resolveModule !== ctx.resolveModule) return { hit: false };
360
+ for (const dependency of entry.dependencies) recordModuleDependency(ctx, dependency);
361
+ return {
362
+ hit: true,
363
+ value: entry.value
364
+ };
365
+ };
366
+ //#endregion
308
367
  //#region src/resolve-imported-value.ts
309
368
  /**
310
369
  * The values an expression borrows from other modules, resolved by reading the imports.
@@ -321,9 +380,9 @@ const box = {
321
380
  * rather than with the number of style calls — a large project reported a 5.4x extraction
322
381
  * slowdown and an OOM in CI.
323
382
  *
324
- * Following an import is two cheap steps this package already had: resolve the specifier to
325
- * a file (`ts.resolveModuleName`, path lookup, no checker), then read that file's exported
326
- * declaration. Crossing the import boundary is the only thing the checker was doing here.
383
+ * Following an import is two cheap steps: ask the caller's source-graph resolver to place the
384
+ * specifier, then read that file's exported declaration. Crossing the import boundary is the
385
+ * only thing the checker was doing here.
327
386
  *
328
387
  * The evaluator resolves everything *within* a module by walking scopes, so a helper that
329
388
  * refers to another binding in its own file needs nothing from us.
@@ -331,7 +390,14 @@ const box = {
331
390
  /** How far a chain of helpers importing helpers is followed. */
332
391
  const MAX_DEPTH = 4;
333
392
  /** Evaluated once per declaration, however many call sites reach it. */
334
- const evaluatedValues = /* @__PURE__ */ new WeakMap();
393
+ let evaluatedValues = /* @__PURE__ */ new WeakMap();
394
+ let hasEvaluatedValues = false;
395
+ /** @internal Drop imported declarations whose closure may include an edited module. */
396
+ const clearImportedValueCache = () => {
397
+ if (!hasEvaluatedValues) return;
398
+ evaluatedValues = /* @__PURE__ */ new WeakMap();
399
+ hasEvaluatedValues = false;
400
+ };
335
401
  /**
336
402
  * Local name to the name and declaration it was imported from.
337
403
  *
@@ -361,17 +427,28 @@ const importBindingsFor = (sourceFile) => {
361
427
  };
362
428
  /** What an imported name refers to, or nothing if it cannot be reached safely. */
363
429
  const valueForBinding = (binding, ctx, stack, depth, evaluateExpression) => {
364
- const sourceFile = getModuleSpecifierSourceFile(binding.declaration);
430
+ const sourceFile = getModuleSpecifierSourceFile(binding.declaration, ctx);
365
431
  if (!sourceFile) return;
366
432
  if (sourceFile.isInNodeModules()) return;
367
433
  const declaration = getExportedVarDeclarationWithName(binding.exportedName, sourceFile, stack, ctx);
368
434
  if (!declaration) return;
369
- if (evaluatedValues.has(declaration)) return { value: evaluatedValues.get(declaration) };
435
+ const cached = evaluatedValues.get(declaration);
436
+ if (cached) {
437
+ const replayed = replayDependencyCache(cached, ctx);
438
+ if (replayed.hit) return { value: replayed.value };
439
+ }
370
440
  const initializer = declaration.getInitializer();
371
441
  if (!initializer) return;
372
- const value = evaluateExpression(initializer, stack, ctx, depth + 1);
442
+ const capture = beginDependencyCapture(ctx);
443
+ let value;
444
+ try {
445
+ value = evaluateExpression(initializer, stack, ctx, depth + 1);
446
+ } finally {
447
+ capture.end();
448
+ }
373
449
  if (value === void 0) return;
374
- evaluatedValues.set(declaration, value);
450
+ evaluatedValues.set(declaration, capture.entry(value));
451
+ hasEvaluatedValues = true;
375
452
  return { value };
376
453
  };
377
454
  /**
@@ -384,27 +461,34 @@ const valueForBinding = (binding, ctx, stack, depth, evaluateExpression) => {
384
461
  */
385
462
  const importedEnvironmentFor = (node, ctx, stack, depth, evaluateExpression) => {
386
463
  if (depth >= MAX_DEPTH) return;
387
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
388
- if (Node.isCallExpression(node)) calls.unshift(node);
389
- if (!calls.length) return;
390
464
  const bindings = importBindingsFor(node.getSourceFile());
391
465
  if (!bindings.size) return;
466
+ const references = node.getDescendantsOfKind(SyntaxKind.Identifier);
467
+ if (Node.isIdentifier(node)) references.unshift(node);
468
+ if (!references.length) return;
392
469
  let environment;
393
- for (const call of calls) {
394
- const callee = call.getExpression();
395
- const binding = bindings.get(callee.getText());
396
- if (!binding || environment?.[callee.getText()] !== void 0) continue;
470
+ for (const reference of references) {
471
+ const name = reference.getText();
472
+ const binding = bindings.get(name);
473
+ if (!binding || environment?.[name] !== void 0) continue;
397
474
  const resolved = valueForBinding(binding, ctx, stack, depth, evaluateExpression);
398
475
  if (!resolved) continue;
399
476
  environment ??= {};
400
- environment[callee.getText()] = resolved.value;
477
+ environment[name] = resolved.value;
401
478
  }
402
479
  return environment;
403
480
  };
404
481
  //#endregion
405
482
  //#region src/evaluate-node.ts
406
483
  const TsEvalError = Symbol("EvalError");
407
- const cacheMap$2 = /* @__PURE__ */ new WeakMap();
484
+ let cacheMap$2 = /* @__PURE__ */ new WeakMap();
485
+ let hasCachedEntries$1 = false;
486
+ /** @internal Drop values whose expression may close over an edited source file. */
487
+ const clearEvaluateNodeCache = () => {
488
+ if (!hasCachedEntries$1) return;
489
+ cacheMap$2 = /* @__PURE__ */ new WeakMap();
490
+ hasCachedEntries$1 = false;
491
+ };
408
492
  /** @see https://github.com/wessberg/ts-evaluator#setting-up-policies */
409
493
  const POLICY = {
410
494
  console: false,
@@ -435,13 +519,39 @@ const POLICY = {
435
519
  const evaluateNode = (node, stack, ctx, depth = 0) => {
436
520
  if (ctx.flags?.skipEvaluate) return;
437
521
  if (ctx.canEval && !ctx.canEval?.(node, stack)) return;
438
- if (depth === 0 && cacheMap$2.has(node)) return cacheMap$2.get(node);
439
- const options = {
522
+ const cached = depth === 0 ? cacheMap$2.get(node) : void 0;
523
+ if (cached) {
524
+ const replayed = replayDependencyCache(cached, ctx);
525
+ if (replayed.hit) return replayed.value;
526
+ }
527
+ const capture = depth === 0 ? beginDependencyCapture(ctx) : void 0;
528
+ try {
529
+ return evaluateNodeUncached(node, stack, ctx, depth, capture?.entry);
530
+ } finally {
531
+ capture?.end();
532
+ }
533
+ };
534
+ const evaluateNodeUncached = (node, stack, ctx, depth, dependencyEntry) => {
535
+ let options = {
440
536
  policy: { ...POLICY },
441
537
  ...ctx.getEvaluateOptions?.(node, stack),
442
538
  node: node.compilerNode,
443
539
  typescript: ts
444
540
  };
541
+ let imported = depth > 0 ? importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode) : void 0;
542
+ if (imported) {
543
+ const environment = options.environment;
544
+ options = {
545
+ ...options,
546
+ environment: {
547
+ ...environment,
548
+ extra: {
549
+ ...environment?.extra,
550
+ ...imported
551
+ }
552
+ }
553
+ };
554
+ }
445
555
  let result = evaluate(options);
446
556
  /**
447
557
  * Retried only on failure, with whatever this expression imports put in scope.
@@ -453,17 +563,26 @@ const evaluateNode = (node, stack, ctx, depth = 0) => {
453
563
  * the path of every build.
454
564
  */
455
565
  if (!result.success) {
456
- const imported = importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode);
457
- if (imported) result = evaluate({
458
- ...options,
459
- environment: {
460
- ...options.environment,
461
- extra: imported
462
- }
463
- });
566
+ imported ??= importedEnvironmentFor(node, ctx, stack, depth, safeEvaluateNode);
567
+ if (imported) {
568
+ const environment = options.environment;
569
+ result = evaluate({
570
+ ...options,
571
+ environment: {
572
+ ...environment,
573
+ extra: {
574
+ ...environment?.extra,
575
+ ...imported
576
+ }
577
+ }
578
+ });
579
+ }
464
580
  }
465
581
  const expr = result.success ? result.value : TsEvalError;
466
- if (depth === 0) cacheMap$2.set(node, expr);
582
+ if (depth === 0) {
583
+ cacheMap$2.set(node, dependencyEntry(expr));
584
+ hasCachedEntries$1 = true;
585
+ }
467
586
  return expr;
468
587
  };
469
588
  const safeEvaluateNode = (node, stack, ctx, depth = 0) => {
@@ -486,7 +605,7 @@ function getDeclarationFor(node, stack, ctx) {
486
605
  declaration = parent;
487
606
  } else if (Node.isImportSpecifier(parent) && (parent.getNameNode() == node || parent.getAliasNode() == node)) {
488
607
  if (ctx.flags?.skipTraverseFiles) return;
489
- const sourceFile = getModuleSpecifierSourceFile(parent.getImportDeclaration());
608
+ const sourceFile = getModuleSpecifierSourceFile(parent.getImportDeclaration(), ctx);
490
609
  if (sourceFile) {
491
610
  const exportStack = [parent, sourceFile];
492
611
  const maybeVar = getExportedVarDeclarationWithName(parent.getNameNode().getText(), sourceFile, exportStack, ctx);
@@ -504,53 +623,74 @@ const getInnermostScope = (from) => {
504
623
  while (scope && !isScope(scope)) scope = scope.getParent();
505
624
  return scope;
506
625
  };
626
+ /**
627
+ * Keyed on the compiler node, never the ts-morph wrapper.
628
+ *
629
+ * `Project.replaceWithText` — which is how a re-parse installs a source transform — keeps the
630
+ * wrapper identity and swaps the compiler node underneath it. A wrapper-keyed cache would
631
+ * therefore answer a rebuild from the previous revision of the file, which is a wrong
632
+ * stylesheet rather than a slow one. Keying on the compiler node makes the cache
633
+ * self-invalidating: ts-morph reuses a compiler node exactly when its subtree did not change.
634
+ */
635
+ const declarationIndexes = /* @__PURE__ */ new WeakMap();
636
+ const isDeclarationName = (node, parent) => {
637
+ if (Node.isVariableDeclaration(parent) || Node.isParameterDeclaration(parent) || Node.isFunctionDeclaration(parent) || Node.isEnumDeclaration(parent) || Node.isBindingElement(parent)) return parent.getNameNode() == node;
638
+ return Node.isImportSpecifier(parent) && (parent.getNameNode() == node || parent.getAliasNode() == node);
639
+ };
640
+ /**
641
+ * Walk a scope once, so that every later lookup inside it is a map read.
642
+ *
643
+ * This used to be a `forEachDescendant` per identifier, widening to each enclosing scope and
644
+ * re-walking from scratch, calling `getText()` on every identifier it passed. A module whose
645
+ * declarations are referenced n times paid n full traversals of itself, and traversal is where
646
+ * ts-morph charges for wrapping each node — it measured ~10% of extraction on its own.
647
+ */
648
+ const declarationIndexFor = (scope) => {
649
+ const cached = declarationIndexes.get(scope.compilerNode);
650
+ if (cached) return cached;
651
+ const index = /* @__PURE__ */ new Map();
652
+ scope.forEachDescendant((node) => {
653
+ if (!Node.isIdentifier(node)) return;
654
+ const parent = node.getParent();
655
+ if (!parent || !isDeclarationName(node, parent)) return;
656
+ const name = node.getText();
657
+ const declared = index.get(name);
658
+ if (declared) declared.push(node);
659
+ else index.set(name, [node]);
660
+ });
661
+ declarationIndexes.set(scope.compilerNode, index);
662
+ return index;
663
+ };
507
664
  function findIdentifierValueDeclaration(identifier, stack, ctx, visitedsWithStack = /* @__PURE__ */ new Map()) {
508
665
  let scope = identifier;
509
- let foundNode;
510
- let isUnresolvable = false;
511
666
  let count = 0;
512
667
  const innerStack = [];
668
+ const refName = identifier.getText();
513
669
  do {
514
670
  scope = getInnermostScope(scope);
515
671
  count++;
516
672
  if (!scope) return;
517
- const refName = identifier.getText();
518
- scope.forEachDescendant((node, traversal) => {
519
- if (visitedsWithStack.has(node)) {
520
- traversal.skip();
521
- innerStack.push(...visitedsWithStack.get(node));
522
- return;
673
+ for (const candidate of declarationIndexFor(scope).get(refName) ?? []) {
674
+ if (candidate == identifier) continue;
675
+ if (visitedsWithStack.has(candidate)) continue;
676
+ visitedsWithStack.set(candidate, innerStack);
677
+ const declarationStack = [candidate];
678
+ const maybeDeclaration = getDeclarationFor(candidate, declarationStack, ctx);
679
+ if (!maybeDeclaration) continue;
680
+ if (Node.isParameterDeclaration(maybeDeclaration)) {
681
+ const initializer = maybeDeclaration.getInitializer();
682
+ const typeNode = maybeDeclaration.getTypeNode();
683
+ if (initializer) innerStack.push(...declarationStack.concat(initializer));
684
+ else if (typeNode && Node.isTypeLiteral(typeNode)) innerStack.push(...declarationStack.concat(typeNode));
685
+ else return;
686
+ stack.push(...innerStack);
687
+ return maybeDeclaration;
523
688
  }
524
- if (node == identifier) return;
525
- visitedsWithStack.set(node, innerStack);
526
- if (Node.isIdentifier(node) && node.getText() == refName) {
527
- const declarationStack = [node];
528
- const maybeDeclaration = getDeclarationFor(node, declarationStack, ctx);
529
- if (maybeDeclaration) {
530
- if (Node.isParameterDeclaration(maybeDeclaration)) {
531
- const initializer = maybeDeclaration.getInitializer();
532
- const typeNode = maybeDeclaration.getTypeNode();
533
- if (initializer) {
534
- innerStack.push(...declarationStack.concat(initializer));
535
- foundNode = maybeDeclaration;
536
- } else if (typeNode && Node.isTypeLiteral(typeNode)) {
537
- innerStack.push(...declarationStack.concat(typeNode));
538
- foundNode = maybeDeclaration;
539
- } else isUnresolvable = true;
540
- traversal.stop();
541
- return;
542
- }
543
- innerStack.push(...declarationStack);
544
- foundNode = maybeDeclaration;
545
- traversal.stop();
546
- }
547
- }
548
- });
549
- if (foundNode || isUnresolvable) {
550
- if (foundNode) stack.push(...innerStack);
551
- return foundNode;
689
+ innerStack.push(...declarationStack);
690
+ stack.push(...innerStack);
691
+ return maybeDeclaration;
552
692
  }
553
- } while (scope && !Node.isSourceFile(scope) && !foundNode && !isUnresolvable && count < 100);
693
+ } while (scope && !Node.isSourceFile(scope) && count < 100);
554
694
  }
555
695
  //#endregion
556
696
  //#region src/get-property-name.ts
@@ -664,7 +804,6 @@ const getObjectLiteralExpressionPropPairs = (expression, expressionStack, ctx, m
664
804
  //#endregion
665
805
  //#region src/maybe-box-node.ts
666
806
  let cacheMap$1 = /* @__PURE__ */ new WeakMap();
667
- const isCached = (node) => cacheMap$1.has(node);
668
807
  const getCached = (node) => cacheMap$1.get(node);
669
808
  /**
670
809
  * Drops every memoized resolution.
@@ -675,6 +814,8 @@ const getCached = (node) => cacheMap$1.get(node);
675
814
  * otherwise keep serving the value read before the edit.
676
815
  */
677
816
  const clearBoxNodeCache = () => {
817
+ clearEvaluateNodeCache();
818
+ clearImportedValueCache();
678
819
  if (!hasCachedEntries) return;
679
820
  cacheMap$1 = /* @__PURE__ */ new WeakMap();
680
821
  hasCachedEntries = false;
@@ -687,12 +828,24 @@ const canReturnWhenTrueInLogicalExpression = (op) => {
687
828
  return op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken;
688
829
  };
689
830
  function maybeBoxNode(node, stack, ctx, matchProp) {
831
+ const cached = getCached(node);
832
+ if (cached) {
833
+ const replayed = replayDependencyCache(cached, ctx);
834
+ if (replayed.hit) return replayed.value;
835
+ }
836
+ const capture = beginDependencyCapture(ctx);
837
+ try {
838
+ return maybeBoxNodeUncached(node, stack, ctx, matchProp, capture.entry);
839
+ } finally {
840
+ capture.end();
841
+ }
842
+ }
843
+ function maybeBoxNodeUncached(node, stack, ctx, matchProp, dependencyEntry) {
690
844
  const cache = (value) => {
691
- cacheMap$1.set(node, value);
845
+ cacheMap$1.set(node, dependencyEntry(value));
692
846
  hasCachedEntries = true;
693
847
  return value;
694
848
  };
695
- if (isCached(node)) return getCached(node);
696
849
  if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
697
850
  const value = trimWhitespace(node.getLiteralValue());
698
851
  return cache(box.literal(value, node, stack));
@@ -1126,21 +1279,19 @@ const resolveExportedName = (name, exportDeclaration) => {
1126
1279
  * which costs a minimum of around 90ms (and scales up with the file/project, could be hundreds of ms)
1127
1280
  * @see https://github.com/dsherret/ts-morph/blob/42d811ed9a5177fc678a5bfec4923a2048124fe0/packages/ts-morph/src/compiler/ast/module/ExportDeclaration.ts#L160
1128
1281
  */
1129
- const getModuleSpecifierSourceFile = (declaration) => {
1130
- const project = declaration.getProject();
1282
+ const getModuleSpecifierSourceFile = (declaration, ctx) => {
1131
1283
  const moduleName = declaration.getModuleSpecifierValue();
1132
1284
  if (!moduleName) return;
1133
- const containingFile = declaration.getSourceFile().getFilePath();
1134
- const resolved = ts.resolveModuleName(moduleName, containingFile, project.getCompilerOptions(), project.getModuleResolutionHost());
1135
- if (!resolved.resolvedModule) return;
1136
- return project.addSourceFileAtPath(resolved.resolvedModule.resolvedFileName);
1285
+ const sourceFile = ctx.resolveModule?.(moduleName, declaration.getSourceFile());
1286
+ if (sourceFile) recordModuleDependency(ctx, sourceFile.getFilePath());
1287
+ return sourceFile;
1137
1288
  };
1138
1289
  function resolveVarDeclarationFromExportWithName(symbolName, sourceFile, stack, ctx, visited) {
1139
1290
  for (const exportDeclaration of sourceFile.getExportDeclarations()) {
1140
1291
  const exportStack = [exportDeclaration];
1141
1292
  const sourceName = resolveExportedName(symbolName, exportDeclaration);
1142
1293
  if (sourceName === void 0) continue;
1143
- const maybeFile = getModuleSpecifierSourceFile(exportDeclaration);
1294
+ const maybeFile = getModuleSpecifierSourceFile(exportDeclaration, ctx);
1144
1295
  if (!maybeFile) {
1145
1296
  const localVar = sourceFile.getVariableDeclaration(sourceName);
1146
1297
  if (localVar) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/extractor",
3
- "version": "1.45.5",
3
+ "version": "1.46.1",
4
4
  "description": "The css extractor for css bamboo",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "ts-evaluator": "2.0.0",
37
37
  "ts-morph": "28.0.0",
38
- "@bamboocss/shared": "1.45.5"
38
+ "@bamboocss/shared": "1.46.1"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsdown src/index.ts --format=cjs,esm --shims --dts",