@bamboocss/parser 1.26.0 → 1.28.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
@@ -65,7 +65,7 @@ function classifyProject(ctx, resultMap) {
65
65
  const { item, kind, filepath, localMaps } = opts;
66
66
  if (!item.box || _bamboocss_extractor.box.isUnresolvable(item.box)) return;
67
67
  if (!item.data) return;
68
- if (item.type === "cva-call") return;
68
+ if (item.type === "cva-call" || item.type === "tokenVar") return;
69
69
  const componentReportItem = {
70
70
  componentIndex: String(componentIndex++),
71
71
  componentName: item.name,
@@ -410,6 +410,11 @@ function createReportMaps() {
410
410
  }
411
411
  //#endregion
412
412
  //#region src/get-module-specifier-value.ts
413
+ /**
414
+ * Both declaration kinds carry a specifier, and `export { x } from './m'` is how a barrel
415
+ * forwards a recipe — so this reads either. An `export { x }` with no `from` returns
416
+ * undefined, which is the same answer the throwing case gives.
417
+ */
413
418
  const getModuleSpecifierValue = (node) => {
414
419
  try {
415
420
  return node.getModuleSpecifierValue();
@@ -418,6 +423,194 @@ const getModuleSpecifierValue = (node) => {
418
423
  }
419
424
  };
420
425
  //#endregion
426
+ //#region src/imported-recipes.ts
427
+ /**
428
+ * A module's exported recipes, memoized.
429
+ *
430
+ * Per *target*, so a barrel imported by two hundred files is walked once. Cleared alongside
431
+ * the box-node cache, which is the same invalidation this needs: both memoize a conclusion
432
+ * drawn from another file's contents.
433
+ */
434
+ const exportedRecipes = /* @__PURE__ */ new Map();
435
+ const clearImportedRecipeCache = () => {
436
+ exportedRecipes.clear();
437
+ };
438
+ /** The names a file bound `cva`/`sva` to, or empty when it imports neither. */
439
+ const recipeFactoryAliases = (sourceFile, imports) => {
440
+ const aliases = /* @__PURE__ */ new Set();
441
+ for (const declaration of sourceFile.getImportDeclarations()) {
442
+ if (declaration.isTypeOnly()) continue;
443
+ const mod = getModuleSpecifierValue(declaration);
444
+ if (!mod) continue;
445
+ for (const specifier of declaration.getNamedImports()) {
446
+ if (specifier.isTypeOnly()) continue;
447
+ const name = specifier.getNameNode().getText();
448
+ if (name !== "cva" && name !== "sva") continue;
449
+ const alias = specifier.getAliasNode()?.getText() || name;
450
+ if (!imports.match({
451
+ name,
452
+ alias,
453
+ mod,
454
+ kind: "named"
455
+ })) continue;
456
+ aliases.add(alias);
457
+ }
458
+ }
459
+ return aliases;
460
+ };
461
+ /** Recipes this module declares itself, by the name it declared them under. */
462
+ const declaredRecipes = (sourceFile, imports) => {
463
+ const declared = /* @__PURE__ */ new Map();
464
+ const exported = /* @__PURE__ */ new Set();
465
+ const factories = recipeFactoryAliases(sourceFile, imports);
466
+ if (factories.size === 0) return {
467
+ declared,
468
+ exported
469
+ };
470
+ const filePath = sourceFile.getFilePath();
471
+ for (const statement of sourceFile.compilerNode.statements) {
472
+ if (!ts_morph.ts.isVariableStatement(statement)) continue;
473
+ if (!(statement.declarationList.flags & ts_morph.ts.NodeFlags.Const)) continue;
474
+ for (const declaration of statement.declarationList.declarations) {
475
+ const initializer = declaration.initializer;
476
+ if (!initializer || !ts_morph.ts.isCallExpression(initializer)) continue;
477
+ const callee = initializer.expression;
478
+ if (!ts_morph.ts.isIdentifier(callee) || !ts_morph.ts.isIdentifier(declaration.name)) continue;
479
+ if (!factories.has(callee.text)) continue;
480
+ declared.set(declaration.name.text, {
481
+ filePath,
482
+ name: declaration.name.text
483
+ });
484
+ if (statement.modifiers?.some((modifier) => modifier.kind === ts_morph.ts.SyntaxKind.ExportKeyword)) exported.add(declaration.name.text);
485
+ }
486
+ }
487
+ return {
488
+ declared,
489
+ exported
490
+ };
491
+ };
492
+ /**
493
+ * Every name this module exports that is bound to a recipe, and where it was declared.
494
+ *
495
+ * Follows `export { x } from './m'`, `export * from './m'`, and `import { x } … export { x }`,
496
+ * because a barrel is how these are reached in practice. The origin is carried through each
497
+ * hop rather than recomputed, so a consumer learns the *declaring* module however many
498
+ * re-exports stand between them.
499
+ *
500
+ * A star export contributes only names nothing else exports, which is what the language does:
501
+ * an explicit export shadows one arriving through `export *`. Resolving that the other way
502
+ * folded a call against a config it would never have run.
503
+ */
504
+ const walkExports = (sourceFile, imports, resolveModule, seen) => {
505
+ const cached = exportedRecipes.get(sourceFile);
506
+ if (cached) return {
507
+ names: cached,
508
+ complete: true
509
+ };
510
+ if (seen.has(sourceFile)) return {
511
+ names: /* @__PURE__ */ new Map(),
512
+ complete: false
513
+ };
514
+ seen.add(sourceFile);
515
+ const names = /* @__PURE__ */ new Map();
516
+ /** Reachable under a local name — declarations, plus recipes this module imports. */
517
+ const local = /* @__PURE__ */ new Map();
518
+ /** Held back so an explicit export of the same name wins. */
519
+ const starred = /* @__PURE__ */ new Map();
520
+ /** Names two different star exports both carry, which resolve to neither. */
521
+ const ambiguous = /* @__PURE__ */ new Set();
522
+ let complete = true;
523
+ const { declared, exported } = declaredRecipes(sourceFile, imports);
524
+ for (const [name, origin] of declared) {
525
+ local.set(name, origin);
526
+ if (exported.has(name)) names.set(name, origin);
527
+ }
528
+ const imported = walkImports(sourceFile, imports, resolveModule, seen);
529
+ complete &&= imported.complete;
530
+ for (const [alias, origin] of imported.bindings) if (!local.has(alias)) local.set(alias, origin);
531
+ for (const declaration of sourceFile.getExportDeclarations()) {
532
+ if (declaration.isTypeOnly()) continue;
533
+ const specifier = getModuleSpecifierValue(declaration);
534
+ const target = specifier ? resolveModule(specifier, sourceFile) : void 0;
535
+ if (specifier && !target) continue;
536
+ if (declaration.getNamespaceExport()) continue;
537
+ if (declaration.isNamespaceExport()) {
538
+ if (!target) continue;
539
+ const walk = walkExports(target, imports, resolveModule, seen);
540
+ complete &&= walk.complete;
541
+ for (const [name, origin] of walk.names) {
542
+ const existing = starred.get(name);
543
+ if (existing && (existing.filePath !== origin.filePath || existing.name !== origin.name)) {
544
+ ambiguous.add(name);
545
+ continue;
546
+ }
547
+ starred.set(name, origin);
548
+ }
549
+ continue;
550
+ }
551
+ let source = local;
552
+ if (target) {
553
+ const walk = walkExports(target, imports, resolveModule, seen);
554
+ complete &&= walk.complete;
555
+ source = walk.names;
556
+ }
557
+ for (const exportSpecifier of declaration.getNamedExports()) {
558
+ if (exportSpecifier.isTypeOnly()) continue;
559
+ const name = exportSpecifier.getNameNode().getText();
560
+ const origin = source.get(name);
561
+ if (origin) names.set(exportSpecifier.getAliasNode()?.getText() || name, origin);
562
+ }
563
+ }
564
+ for (const [name, origin] of starred) if (!names.has(name) && !ambiguous.has(name)) names.set(name, origin);
565
+ if (complete) exportedRecipes.set(sourceFile, names);
566
+ return {
567
+ names,
568
+ complete
569
+ };
570
+ };
571
+ /**
572
+ * Local names in this file bound, through an import, to a recipe declared elsewhere.
573
+ *
574
+ * Reports truncation for the same reason `walkExports` does: `export { x }` with no `from`
575
+ * resolves through here, so a walk cut short by a cycle can leave a name out — and caching
576
+ * that as the whole answer is what makes visibility depend on where the walk began.
577
+ */
578
+ const walkImports = (sourceFile, imports, resolveModule, seen) => {
579
+ const bindings = /* @__PURE__ */ new Map();
580
+ let complete = true;
581
+ for (const declaration of sourceFile.getImportDeclarations()) {
582
+ if (declaration.isTypeOnly()) continue;
583
+ const named = declaration.getNamedImports();
584
+ if (named.length === 0) continue;
585
+ const specifier = getModuleSpecifierValue(declaration);
586
+ if (!specifier) continue;
587
+ const target = resolveModule(specifier, sourceFile);
588
+ if (!target || target === sourceFile) continue;
589
+ const walk = walkExports(target, imports, resolveModule, seen);
590
+ complete &&= walk.complete;
591
+ const { names } = walk;
592
+ if (names.size === 0) continue;
593
+ for (const importSpecifier of named) {
594
+ if (importSpecifier.isTypeOnly()) continue;
595
+ const origin = names.get(importSpecifier.getNameNode().getText());
596
+ if (!origin) continue;
597
+ bindings.set(importSpecifier.getAliasNode()?.getText() || importSpecifier.getNameNode().getText(), origin);
598
+ }
599
+ }
600
+ return {
601
+ bindings,
602
+ complete
603
+ };
604
+ };
605
+ /**
606
+ * Local names in this file bound, through an import, to a recipe declared elsewhere.
607
+ *
608
+ * Only specifiers the caller's resolver can place inside the project. A recipe outside the
609
+ * project's `include` is never parsed, so it has no rules in the stylesheet and could not be
610
+ * folded against even if the binding resolved.
611
+ */
612
+ const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
613
+ //#endregion
421
614
  //#region src/get-import-declarations.ts
422
615
  function getImportDeclarations(context, sourceFile) {
423
616
  const { imports, tsOptions } = context;
@@ -769,8 +962,14 @@ var ParserResult = class {
769
962
  const unresolved = findUnresolvedRecipeStyles(result);
770
963
  if (unresolved.length) this.unresolved.push(...unresolved);
771
964
  }
772
- setToken(result) {
773
- this.token.add(this.append(Object.assign({ type: "token" }, result)));
965
+ /**
966
+ * `kind` separates `token()` from `token.var()`, which resolve to the value and to the
967
+ * variable reference respectively. They share this set deliberately: everything that reads
968
+ * a result for the token *path* — `collectTokenReferences`, keeping a declaration alive
969
+ * through pruning — wants both, and only the fold cares which half was asked for.
970
+ */
971
+ setToken(result, kind = "token") {
972
+ this.token.add(this.append(Object.assign({ type: kind }, result)));
774
973
  }
775
974
  setViewTransition(result) {
776
975
  this.viewTransition.add(this.append(Object.assign({ type: "viewTransition" }, result)));
@@ -860,14 +1059,16 @@ const fallbackImpl = (...values) => values.some((value) => value === void 0) ? v
860
1059
  const evaluateOptions = { environment: defaultEnv };
861
1060
  function createParser(context) {
862
1061
  const { jsx, imports, recipes } = context;
863
- return function parse(sourceFile, encoder, options) {
1062
+ return function parse(sourceFile, encoder, options, resolveModule) {
864
1063
  if (!sourceFile) return;
865
1064
  const importDeclarations = getImportDeclarations(context, sourceFile);
866
1065
  const file = imports.file(importDeclarations);
867
1066
  const filePath = sourceFile.getFilePath();
868
1067
  _bamboocss_logger.logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
869
1068
  const parserResult = new ParserResult(context, encoder);
870
- if (file.isEmpty() && !jsx.isEnabled) return parserResult;
1069
+ const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
1070
+ if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
1071
+ for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
871
1072
  if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
872
1073
  if (!ts_morph.ts.isVariableStatement(statement)) continue;
873
1074
  if (!(statement.declarationList.flags & ts_morph.ts.NodeFlags.Const)) continue;
@@ -964,11 +1165,22 @@ function createParser(context) {
964
1165
  if (query.kind === "call-expression") parserResult.setCvaCall(alias, {
965
1166
  name: alias,
966
1167
  box: query.box.value[0] ?? _bamboocss_extractor.box.fallback(query.box),
967
- data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
1168
+ data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0])),
1169
+ origin: importedRecipes.get(alias)
968
1170
  });
969
1171
  });
970
1172
  return;
971
1173
  }
1174
+ if (file.isTokenVarFn(alias)) {
1175
+ result.queryList.forEach((query) => {
1176
+ if (query.kind === "call-expression") parserResult.setToken({
1177
+ name: "token.var",
1178
+ box: query.box.value[0] ?? _bamboocss_extractor.box.fallback(query.box),
1179
+ data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
1180
+ }, "tokenVar");
1181
+ });
1182
+ return;
1183
+ }
972
1184
  (0, ts_pattern.match)(name).when(imports.matchers.css.match, (name) => {
973
1185
  result.queryList.forEach((query) => {
974
1186
  if (query.kind === "call-expression") if (query.box.value.length > 1) parserResult.set(name, {
@@ -1036,6 +1248,17 @@ function createParser(context) {
1036
1248
  }
1037
1249
  //#endregion
1038
1250
  //#region src/project.ts
1251
+ /**
1252
+ * Everything memoized against another file's contents.
1253
+ *
1254
+ * Both caches answer a question about a *different* module than the one being parsed — what
1255
+ * an identifier resolved to, and which recipes a module exports — so both go stale on exactly
1256
+ * the same events, and clearing one without the other leaves the pair disagreeing.
1257
+ */
1258
+ const invalidateResolutions = () => {
1259
+ (0, _bamboocss_extractor.clearBoxNodeCache)();
1260
+ clearImportedRecipeCache();
1261
+ };
1039
1262
  const normalizeCompilerOptions = (raw) => {
1040
1263
  if (!raw) return {};
1041
1264
  const { options } = ts_morph.ts.convertCompilerOptionsFromJson(raw, process.cwd());
@@ -1119,6 +1342,18 @@ var Project = class {
1119
1342
  * `.d.ts` into the project. The graph only tracks files bamboo already scans.
1120
1343
  */
1121
1344
  moduleResolutionCache;
1345
+ /**
1346
+ * Everything memoized against the shape of the file tree, including the negative half.
1347
+ *
1348
+ * `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
1349
+ * its target existed stays unresolved for the life of the process. That silently dropped a
1350
+ * dependency edge; now it would also leave a recipe permanently invisible to the module
1351
+ * importing it, since resolution is what finds one.
1352
+ */
1353
+ invalidate = (fileTreeChanged = true) => {
1354
+ invalidateResolutions();
1355
+ if (fileTreeChanged) this.moduleResolutionCache = void 0;
1356
+ };
1122
1357
  resolveImport = (decl) => {
1123
1358
  const moduleName = decl.getModuleSpecifierValue();
1124
1359
  if (!moduleName) return;
@@ -1127,6 +1362,16 @@ var Project = class {
1127
1362
  const name = ts_morph.ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
1128
1363
  return name ? this.project.getSourceFile(name) : void 0;
1129
1364
  };
1365
+ /**
1366
+ * `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
1367
+ *
1368
+ * Shares the module resolution cache above, so a barrel resolved while tracking
1369
+ * dependencies is not resolved again while looking for recipes.
1370
+ */
1371
+ resolveModule = (specifier, from) => this.resolveImport({
1372
+ getModuleSpecifierValue: () => specifier,
1373
+ getSourceFile: () => from
1374
+ });
1130
1375
  trackDependencies = (filePath, sourceFile) => {
1131
1376
  const importer = this.normalizePath(sourceFile.getFilePath());
1132
1377
  this.canonicalPaths.set(this.normalizePath(filePath), importer);
@@ -1173,7 +1418,7 @@ var Project = class {
1173
1418
  };
1174
1419
  createSourceFile = (filePath) => {
1175
1420
  const { readFile } = this.options;
1176
- (0, _bamboocss_extractor.clearBoxNodeCache)();
1421
+ this.invalidate();
1177
1422
  return this.project.createSourceFile(filePath, readFile(filePath), {
1178
1423
  overwrite: true,
1179
1424
  scriptKind: ts_morph.ScriptKind.TSX
@@ -1184,7 +1429,7 @@ var Project = class {
1184
1429
  for (const file of files) this.createSourceFile(file);
1185
1430
  };
1186
1431
  addSourceFile = (filePath, content) => {
1187
- (0, _bamboocss_extractor.clearBoxNodeCache)();
1432
+ this.invalidate(!(filePath.includes("/") && this.project.getSourceFile(filePath)));
1188
1433
  return this.project.createSourceFile(filePath, content, {
1189
1434
  overwrite: true,
1190
1435
  scriptKind: ts_morph.ScriptKind.TSX
@@ -1193,18 +1438,18 @@ var Project = class {
1193
1438
  removeSourceFile = (filePath) => {
1194
1439
  const sourceFile = this.project.getSourceFile(filePath);
1195
1440
  if (sourceFile) {
1196
- (0, _bamboocss_extractor.clearBoxNodeCache)();
1441
+ this.invalidate();
1197
1442
  return this.project.removeSourceFile(sourceFile);
1198
1443
  }
1199
1444
  return false;
1200
1445
  };
1201
1446
  reloadSourceFile = (filePath) => {
1202
- (0, _bamboocss_extractor.clearBoxNodeCache)();
1447
+ this.invalidate(false);
1203
1448
  return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
1204
1449
  };
1205
1450
  reloadSourceFiles = () => {
1206
1451
  const files = this.getFiles();
1207
- (0, _bamboocss_extractor.clearBoxNodeCache)();
1452
+ this.invalidate();
1208
1453
  for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
1209
1454
  };
1210
1455
  get readFile() {
@@ -1238,7 +1483,7 @@ var Project = class {
1238
1483
  }
1239
1484
  }) ?? this.transformFile(filePath, original);
1240
1485
  if (original !== transformed) sourceFile.replaceWithText(transformed);
1241
- const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
1486
+ const result = this.parser(sourceFile, encoder, options, this.resolveModule)?.setFilePath(filePath);
1242
1487
  hooks["parser:after"]?.({
1243
1488
  filePath,
1244
1489
  result
package/dist/index.d.cts CHANGED
@@ -136,6 +136,9 @@ declare class Generator extends Context {
136
136
  * Get CSS for a specific theme
137
137
  */
138
138
  //#endregion
139
+ //#region src/imported-recipes.d.ts
140
+ type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
141
+ //#endregion
139
142
  //#region src/unresolved-styles.d.ts
140
143
  interface UnresolvedStyle {
141
144
  /**
@@ -226,7 +229,13 @@ declare class ParserResult implements ParserResultInterface {
226
229
  * hash except an explicit `className`, which is what the message says to reach for.
227
230
  */
228
231
  private reportUnresolvedRecipe;
229
- setToken(result: ResultItem): void;
232
+ /**
233
+ * `kind` separates `token()` from `token.var()`, which resolve to the value and to the
234
+ * variable reference respectively. They share this set deliberately: everything that reads
235
+ * a result for the token *path* — `collectTokenReferences`, keeping a declaration alive
236
+ * through pruning — wants both, and only the fold cares which half was asked for.
237
+ */
238
+ setToken(result: ResultItem, kind?: 'token' | 'tokenVar'): void;
230
239
  setViewTransition(result: ResultItem): void;
231
240
  setPattern(name: string, result: ResultItem): void;
232
241
  setRecipe(recipeName: string, result: ResultItem): void;
@@ -251,7 +260,16 @@ declare class ParserResult implements ParserResultInterface {
251
260
  }
252
261
  //#endregion
253
262
  //#region src/parser.d.ts
254
- declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions) => ParserResult | undefined;
263
+ declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions,
264
+ /**
265
+ * How to place a module specifier inside the project.
266
+ *
267
+ * Supplied by the caller because it owns the resolution cache, and because the cheap
268
+ * implementation is a filesystem lookup rather than a symbol-table walk. Absent, a
269
+ * recipe declared in another module stays invisible exactly as before.
270
+ */
271
+
272
+ resolveModule?: ResolveModule) => ParserResult | undefined;
255
273
  //#endregion
256
274
  //#region src/project.d.ts
257
275
  interface ProjectOptions extends ProjectOptions$1 {
@@ -315,7 +333,23 @@ declare class Project {
315
333
  * `.d.ts` into the project. The graph only tracks files bamboo already scans.
316
334
  */
317
335
  private moduleResolutionCache;
336
+ /**
337
+ * Everything memoized against the shape of the file tree, including the negative half.
338
+ *
339
+ * `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
340
+ * its target existed stays unresolved for the life of the process. That silently dropped a
341
+ * dependency edge; now it would also leave a recipe permanently invisible to the module
342
+ * importing it, since resolution is what finds one.
343
+ */
344
+ private invalidate;
318
345
  private resolveImport;
346
+ /**
347
+ * `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
348
+ *
349
+ * Shares the module resolution cache above, so a barrel resolved while tracking
350
+ * dependencies is not resolved again while looking for recipes.
351
+ */
352
+ private resolveModule;
319
353
  private trackDependencies;
320
354
  /**
321
355
  * Every file that transitively imports `filePath`, so a watcher can re-parse the
package/dist/index.d.mts CHANGED
@@ -136,6 +136,9 @@ declare class Generator extends Context {
136
136
  * Get CSS for a specific theme
137
137
  */
138
138
  //#endregion
139
+ //#region src/imported-recipes.d.ts
140
+ type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
141
+ //#endregion
139
142
  //#region src/unresolved-styles.d.ts
140
143
  interface UnresolvedStyle {
141
144
  /**
@@ -226,7 +229,13 @@ declare class ParserResult implements ParserResultInterface {
226
229
  * hash except an explicit `className`, which is what the message says to reach for.
227
230
  */
228
231
  private reportUnresolvedRecipe;
229
- setToken(result: ResultItem): void;
232
+ /**
233
+ * `kind` separates `token()` from `token.var()`, which resolve to the value and to the
234
+ * variable reference respectively. They share this set deliberately: everything that reads
235
+ * a result for the token *path* — `collectTokenReferences`, keeping a declaration alive
236
+ * through pruning — wants both, and only the fold cares which half was asked for.
237
+ */
238
+ setToken(result: ResultItem, kind?: 'token' | 'tokenVar'): void;
230
239
  setViewTransition(result: ResultItem): void;
231
240
  setPattern(name: string, result: ResultItem): void;
232
241
  setRecipe(recipeName: string, result: ResultItem): void;
@@ -251,7 +260,16 @@ declare class ParserResult implements ParserResultInterface {
251
260
  }
252
261
  //#endregion
253
262
  //#region src/parser.d.ts
254
- declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions) => ParserResult | undefined;
263
+ declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions,
264
+ /**
265
+ * How to place a module specifier inside the project.
266
+ *
267
+ * Supplied by the caller because it owns the resolution cache, and because the cheap
268
+ * implementation is a filesystem lookup rather than a symbol-table walk. Absent, a
269
+ * recipe declared in another module stays invisible exactly as before.
270
+ */
271
+
272
+ resolveModule?: ResolveModule) => ParserResult | undefined;
255
273
  //#endregion
256
274
  //#region src/project.d.ts
257
275
  interface ProjectOptions extends ProjectOptions$1 {
@@ -315,7 +333,23 @@ declare class Project {
315
333
  * `.d.ts` into the project. The graph only tracks files bamboo already scans.
316
334
  */
317
335
  private moduleResolutionCache;
336
+ /**
337
+ * Everything memoized against the shape of the file tree, including the negative half.
338
+ *
339
+ * `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
340
+ * its target existed stays unresolved for the life of the process. That silently dropped a
341
+ * dependency edge; now it would also leave a recipe permanently invisible to the module
342
+ * importing it, since resolution is what finds one.
343
+ */
344
+ private invalidate;
318
345
  private resolveImport;
346
+ /**
347
+ * `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
348
+ *
349
+ * Shares the module resolution cache above, so a barrel resolved while tracking
350
+ * dependencies is not resolved again while looking for recipes.
351
+ */
352
+ private resolveModule;
319
353
  private trackDependencies;
320
354
  /**
321
355
  * Every file that transitively imports `filePath`, so a watcher can re-parse the
package/dist/index.mjs CHANGED
@@ -64,7 +64,7 @@ function classifyProject(ctx, resultMap) {
64
64
  const { item, kind, filepath, localMaps } = opts;
65
65
  if (!item.box || box.isUnresolvable(item.box)) return;
66
66
  if (!item.data) return;
67
- if (item.type === "cva-call") return;
67
+ if (item.type === "cva-call" || item.type === "tokenVar") return;
68
68
  const componentReportItem = {
69
69
  componentIndex: String(componentIndex++),
70
70
  componentName: item.name,
@@ -409,6 +409,11 @@ function createReportMaps() {
409
409
  }
410
410
  //#endregion
411
411
  //#region src/get-module-specifier-value.ts
412
+ /**
413
+ * Both declaration kinds carry a specifier, and `export { x } from './m'` is how a barrel
414
+ * forwards a recipe — so this reads either. An `export { x }` with no `from` returns
415
+ * undefined, which is the same answer the throwing case gives.
416
+ */
412
417
  const getModuleSpecifierValue = (node) => {
413
418
  try {
414
419
  return node.getModuleSpecifierValue();
@@ -417,6 +422,194 @@ const getModuleSpecifierValue = (node) => {
417
422
  }
418
423
  };
419
424
  //#endregion
425
+ //#region src/imported-recipes.ts
426
+ /**
427
+ * A module's exported recipes, memoized.
428
+ *
429
+ * Per *target*, so a barrel imported by two hundred files is walked once. Cleared alongside
430
+ * the box-node cache, which is the same invalidation this needs: both memoize a conclusion
431
+ * drawn from another file's contents.
432
+ */
433
+ const exportedRecipes = /* @__PURE__ */ new Map();
434
+ const clearImportedRecipeCache = () => {
435
+ exportedRecipes.clear();
436
+ };
437
+ /** The names a file bound `cva`/`sva` to, or empty when it imports neither. */
438
+ const recipeFactoryAliases = (sourceFile, imports) => {
439
+ const aliases = /* @__PURE__ */ new Set();
440
+ for (const declaration of sourceFile.getImportDeclarations()) {
441
+ if (declaration.isTypeOnly()) continue;
442
+ const mod = getModuleSpecifierValue(declaration);
443
+ if (!mod) continue;
444
+ for (const specifier of declaration.getNamedImports()) {
445
+ if (specifier.isTypeOnly()) continue;
446
+ const name = specifier.getNameNode().getText();
447
+ if (name !== "cva" && name !== "sva") continue;
448
+ const alias = specifier.getAliasNode()?.getText() || name;
449
+ if (!imports.match({
450
+ name,
451
+ alias,
452
+ mod,
453
+ kind: "named"
454
+ })) continue;
455
+ aliases.add(alias);
456
+ }
457
+ }
458
+ return aliases;
459
+ };
460
+ /** Recipes this module declares itself, by the name it declared them under. */
461
+ const declaredRecipes = (sourceFile, imports) => {
462
+ const declared = /* @__PURE__ */ new Map();
463
+ const exported = /* @__PURE__ */ new Set();
464
+ const factories = recipeFactoryAliases(sourceFile, imports);
465
+ if (factories.size === 0) return {
466
+ declared,
467
+ exported
468
+ };
469
+ const filePath = sourceFile.getFilePath();
470
+ for (const statement of sourceFile.compilerNode.statements) {
471
+ if (!ts.isVariableStatement(statement)) continue;
472
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
473
+ for (const declaration of statement.declarationList.declarations) {
474
+ const initializer = declaration.initializer;
475
+ if (!initializer || !ts.isCallExpression(initializer)) continue;
476
+ const callee = initializer.expression;
477
+ if (!ts.isIdentifier(callee) || !ts.isIdentifier(declaration.name)) continue;
478
+ if (!factories.has(callee.text)) continue;
479
+ declared.set(declaration.name.text, {
480
+ filePath,
481
+ name: declaration.name.text
482
+ });
483
+ if (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) exported.add(declaration.name.text);
484
+ }
485
+ }
486
+ return {
487
+ declared,
488
+ exported
489
+ };
490
+ };
491
+ /**
492
+ * Every name this module exports that is bound to a recipe, and where it was declared.
493
+ *
494
+ * Follows `export { x } from './m'`, `export * from './m'`, and `import { x } … export { x }`,
495
+ * because a barrel is how these are reached in practice. The origin is carried through each
496
+ * hop rather than recomputed, so a consumer learns the *declaring* module however many
497
+ * re-exports stand between them.
498
+ *
499
+ * A star export contributes only names nothing else exports, which is what the language does:
500
+ * an explicit export shadows one arriving through `export *`. Resolving that the other way
501
+ * folded a call against a config it would never have run.
502
+ */
503
+ const walkExports = (sourceFile, imports, resolveModule, seen) => {
504
+ const cached = exportedRecipes.get(sourceFile);
505
+ if (cached) return {
506
+ names: cached,
507
+ complete: true
508
+ };
509
+ if (seen.has(sourceFile)) return {
510
+ names: /* @__PURE__ */ new Map(),
511
+ complete: false
512
+ };
513
+ seen.add(sourceFile);
514
+ const names = /* @__PURE__ */ new Map();
515
+ /** Reachable under a local name — declarations, plus recipes this module imports. */
516
+ const local = /* @__PURE__ */ new Map();
517
+ /** Held back so an explicit export of the same name wins. */
518
+ const starred = /* @__PURE__ */ new Map();
519
+ /** Names two different star exports both carry, which resolve to neither. */
520
+ const ambiguous = /* @__PURE__ */ new Set();
521
+ let complete = true;
522
+ const { declared, exported } = declaredRecipes(sourceFile, imports);
523
+ for (const [name, origin] of declared) {
524
+ local.set(name, origin);
525
+ if (exported.has(name)) names.set(name, origin);
526
+ }
527
+ const imported = walkImports(sourceFile, imports, resolveModule, seen);
528
+ complete &&= imported.complete;
529
+ for (const [alias, origin] of imported.bindings) if (!local.has(alias)) local.set(alias, origin);
530
+ for (const declaration of sourceFile.getExportDeclarations()) {
531
+ if (declaration.isTypeOnly()) continue;
532
+ const specifier = getModuleSpecifierValue(declaration);
533
+ const target = specifier ? resolveModule(specifier, sourceFile) : void 0;
534
+ if (specifier && !target) continue;
535
+ if (declaration.getNamespaceExport()) continue;
536
+ if (declaration.isNamespaceExport()) {
537
+ if (!target) continue;
538
+ const walk = walkExports(target, imports, resolveModule, seen);
539
+ complete &&= walk.complete;
540
+ for (const [name, origin] of walk.names) {
541
+ const existing = starred.get(name);
542
+ if (existing && (existing.filePath !== origin.filePath || existing.name !== origin.name)) {
543
+ ambiguous.add(name);
544
+ continue;
545
+ }
546
+ starred.set(name, origin);
547
+ }
548
+ continue;
549
+ }
550
+ let source = local;
551
+ if (target) {
552
+ const walk = walkExports(target, imports, resolveModule, seen);
553
+ complete &&= walk.complete;
554
+ source = walk.names;
555
+ }
556
+ for (const exportSpecifier of declaration.getNamedExports()) {
557
+ if (exportSpecifier.isTypeOnly()) continue;
558
+ const name = exportSpecifier.getNameNode().getText();
559
+ const origin = source.get(name);
560
+ if (origin) names.set(exportSpecifier.getAliasNode()?.getText() || name, origin);
561
+ }
562
+ }
563
+ for (const [name, origin] of starred) if (!names.has(name) && !ambiguous.has(name)) names.set(name, origin);
564
+ if (complete) exportedRecipes.set(sourceFile, names);
565
+ return {
566
+ names,
567
+ complete
568
+ };
569
+ };
570
+ /**
571
+ * Local names in this file bound, through an import, to a recipe declared elsewhere.
572
+ *
573
+ * Reports truncation for the same reason `walkExports` does: `export { x }` with no `from`
574
+ * resolves through here, so a walk cut short by a cycle can leave a name out — and caching
575
+ * that as the whole answer is what makes visibility depend on where the walk began.
576
+ */
577
+ const walkImports = (sourceFile, imports, resolveModule, seen) => {
578
+ const bindings = /* @__PURE__ */ new Map();
579
+ let complete = true;
580
+ for (const declaration of sourceFile.getImportDeclarations()) {
581
+ if (declaration.isTypeOnly()) continue;
582
+ const named = declaration.getNamedImports();
583
+ if (named.length === 0) continue;
584
+ const specifier = getModuleSpecifierValue(declaration);
585
+ if (!specifier) continue;
586
+ const target = resolveModule(specifier, sourceFile);
587
+ if (!target || target === sourceFile) continue;
588
+ const walk = walkExports(target, imports, resolveModule, seen);
589
+ complete &&= walk.complete;
590
+ const { names } = walk;
591
+ if (names.size === 0) continue;
592
+ for (const importSpecifier of named) {
593
+ if (importSpecifier.isTypeOnly()) continue;
594
+ const origin = names.get(importSpecifier.getNameNode().getText());
595
+ if (!origin) continue;
596
+ bindings.set(importSpecifier.getAliasNode()?.getText() || importSpecifier.getNameNode().getText(), origin);
597
+ }
598
+ }
599
+ return {
600
+ bindings,
601
+ complete
602
+ };
603
+ };
604
+ /**
605
+ * Local names in this file bound, through an import, to a recipe declared elsewhere.
606
+ *
607
+ * Only specifiers the caller's resolver can place inside the project. A recipe outside the
608
+ * project's `include` is never parsed, so it has no rules in the stylesheet and could not be
609
+ * folded against even if the binding resolved.
610
+ */
611
+ const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
612
+ //#endregion
420
613
  //#region src/get-import-declarations.ts
421
614
  function getImportDeclarations(context, sourceFile) {
422
615
  const { imports, tsOptions } = context;
@@ -768,8 +961,14 @@ var ParserResult = class {
768
961
  const unresolved = findUnresolvedRecipeStyles(result);
769
962
  if (unresolved.length) this.unresolved.push(...unresolved);
770
963
  }
771
- setToken(result) {
772
- this.token.add(this.append(Object.assign({ type: "token" }, result)));
964
+ /**
965
+ * `kind` separates `token()` from `token.var()`, which resolve to the value and to the
966
+ * variable reference respectively. They share this set deliberately: everything that reads
967
+ * a result for the token *path* — `collectTokenReferences`, keeping a declaration alive
968
+ * through pruning — wants both, and only the fold cares which half was asked for.
969
+ */
970
+ setToken(result, kind = "token") {
971
+ this.token.add(this.append(Object.assign({ type: kind }, result)));
773
972
  }
774
973
  setViewTransition(result) {
775
974
  this.viewTransition.add(this.append(Object.assign({ type: "viewTransition" }, result)));
@@ -859,14 +1058,16 @@ const fallbackImpl = (...values) => values.some((value) => value === void 0) ? v
859
1058
  const evaluateOptions = { environment: defaultEnv };
860
1059
  function createParser(context) {
861
1060
  const { jsx, imports, recipes } = context;
862
- return function parse(sourceFile, encoder, options) {
1061
+ return function parse(sourceFile, encoder, options, resolveModule) {
863
1062
  if (!sourceFile) return;
864
1063
  const importDeclarations = getImportDeclarations(context, sourceFile);
865
1064
  const file = imports.file(importDeclarations);
866
1065
  const filePath = sourceFile.getFilePath();
867
1066
  logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
868
1067
  const parserResult = new ParserResult(context, encoder);
869
- if (file.isEmpty() && !jsx.isEnabled) return parserResult;
1068
+ const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
1069
+ if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
1070
+ for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
870
1071
  if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
871
1072
  if (!ts.isVariableStatement(statement)) continue;
872
1073
  if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
@@ -963,11 +1164,22 @@ function createParser(context) {
963
1164
  if (query.kind === "call-expression") parserResult.setCvaCall(alias, {
964
1165
  name: alias,
965
1166
  box: query.box.value[0] ?? box.fallback(query.box),
966
- data: combineResult(unbox(query.box.value[0]))
1167
+ data: combineResult(unbox(query.box.value[0])),
1168
+ origin: importedRecipes.get(alias)
967
1169
  });
968
1170
  });
969
1171
  return;
970
1172
  }
1173
+ if (file.isTokenVarFn(alias)) {
1174
+ result.queryList.forEach((query) => {
1175
+ if (query.kind === "call-expression") parserResult.setToken({
1176
+ name: "token.var",
1177
+ box: query.box.value[0] ?? box.fallback(query.box),
1178
+ data: combineResult(unbox(query.box.value[0]))
1179
+ }, "tokenVar");
1180
+ });
1181
+ return;
1182
+ }
971
1183
  match(name).when(imports.matchers.css.match, (name) => {
972
1184
  result.queryList.forEach((query) => {
973
1185
  if (query.kind === "call-expression") if (query.box.value.length > 1) parserResult.set(name, {
@@ -1035,6 +1247,17 @@ function createParser(context) {
1035
1247
  }
1036
1248
  //#endregion
1037
1249
  //#region src/project.ts
1250
+ /**
1251
+ * Everything memoized against another file's contents.
1252
+ *
1253
+ * Both caches answer a question about a *different* module than the one being parsed — what
1254
+ * an identifier resolved to, and which recipes a module exports — so both go stale on exactly
1255
+ * the same events, and clearing one without the other leaves the pair disagreeing.
1256
+ */
1257
+ const invalidateResolutions = () => {
1258
+ clearBoxNodeCache();
1259
+ clearImportedRecipeCache();
1260
+ };
1038
1261
  const normalizeCompilerOptions = (raw) => {
1039
1262
  if (!raw) return {};
1040
1263
  const { options } = ts.convertCompilerOptionsFromJson(raw, process.cwd());
@@ -1118,6 +1341,18 @@ var Project = class {
1118
1341
  * `.d.ts` into the project. The graph only tracks files bamboo already scans.
1119
1342
  */
1120
1343
  moduleResolutionCache;
1344
+ /**
1345
+ * Everything memoized against the shape of the file tree, including the negative half.
1346
+ *
1347
+ * `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
1348
+ * its target existed stays unresolved for the life of the process. That silently dropped a
1349
+ * dependency edge; now it would also leave a recipe permanently invisible to the module
1350
+ * importing it, since resolution is what finds one.
1351
+ */
1352
+ invalidate = (fileTreeChanged = true) => {
1353
+ invalidateResolutions();
1354
+ if (fileTreeChanged) this.moduleResolutionCache = void 0;
1355
+ };
1121
1356
  resolveImport = (decl) => {
1122
1357
  const moduleName = decl.getModuleSpecifierValue();
1123
1358
  if (!moduleName) return;
@@ -1126,6 +1361,16 @@ var Project = class {
1126
1361
  const name = ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
1127
1362
  return name ? this.project.getSourceFile(name) : void 0;
1128
1363
  };
1364
+ /**
1365
+ * `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
1366
+ *
1367
+ * Shares the module resolution cache above, so a barrel resolved while tracking
1368
+ * dependencies is not resolved again while looking for recipes.
1369
+ */
1370
+ resolveModule = (specifier, from) => this.resolveImport({
1371
+ getModuleSpecifierValue: () => specifier,
1372
+ getSourceFile: () => from
1373
+ });
1129
1374
  trackDependencies = (filePath, sourceFile) => {
1130
1375
  const importer = this.normalizePath(sourceFile.getFilePath());
1131
1376
  this.canonicalPaths.set(this.normalizePath(filePath), importer);
@@ -1172,7 +1417,7 @@ var Project = class {
1172
1417
  };
1173
1418
  createSourceFile = (filePath) => {
1174
1419
  const { readFile } = this.options;
1175
- clearBoxNodeCache();
1420
+ this.invalidate();
1176
1421
  return this.project.createSourceFile(filePath, readFile(filePath), {
1177
1422
  overwrite: true,
1178
1423
  scriptKind: ScriptKind.TSX
@@ -1183,7 +1428,7 @@ var Project = class {
1183
1428
  for (const file of files) this.createSourceFile(file);
1184
1429
  };
1185
1430
  addSourceFile = (filePath, content) => {
1186
- clearBoxNodeCache();
1431
+ this.invalidate(!(filePath.includes("/") && this.project.getSourceFile(filePath)));
1187
1432
  return this.project.createSourceFile(filePath, content, {
1188
1433
  overwrite: true,
1189
1434
  scriptKind: ScriptKind.TSX
@@ -1192,18 +1437,18 @@ var Project = class {
1192
1437
  removeSourceFile = (filePath) => {
1193
1438
  const sourceFile = this.project.getSourceFile(filePath);
1194
1439
  if (sourceFile) {
1195
- clearBoxNodeCache();
1440
+ this.invalidate();
1196
1441
  return this.project.removeSourceFile(sourceFile);
1197
1442
  }
1198
1443
  return false;
1199
1444
  };
1200
1445
  reloadSourceFile = (filePath) => {
1201
- clearBoxNodeCache();
1446
+ this.invalidate(false);
1202
1447
  return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
1203
1448
  };
1204
1449
  reloadSourceFiles = () => {
1205
1450
  const files = this.getFiles();
1206
- clearBoxNodeCache();
1451
+ this.invalidate();
1207
1452
  for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
1208
1453
  };
1209
1454
  get readFile() {
@@ -1237,7 +1482,7 @@ var Project = class {
1237
1482
  }
1238
1483
  }) ?? this.transformFile(filePath, original);
1239
1484
  if (original !== transformed) sourceFile.replaceWithText(transformed);
1240
- const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
1485
+ const result = this.parser(sourceFile, encoder, options, this.resolveModule)?.setFilePath(filePath);
1241
1486
  hooks["parser:after"]?.({
1242
1487
  filePath,
1243
1488
  result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/parser",
3
- "version": "1.26.0",
3
+ "version": "1.28.1",
4
4
  "description": "The static parser for bamboo css",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -34,17 +34,17 @@
34
34
  "dependencies": {
35
35
  "ts-morph": "28.0.0",
36
36
  "ts-pattern": "5.9.0",
37
- "@bamboocss/config": "^1.26.0",
38
- "@bamboocss/core": "^1.26.0",
39
- "@bamboocss/extractor": "1.26.0",
40
- "@bamboocss/logger": "1.26.0",
41
- "@bamboocss/shared": "1.26.0",
42
- "@bamboocss/types": "1.26.0"
37
+ "@bamboocss/extractor": "1.28.1",
38
+ "@bamboocss/core": "^1.28.1",
39
+ "@bamboocss/config": "^1.28.1",
40
+ "@bamboocss/logger": "1.28.1",
41
+ "@bamboocss/shared": "1.28.1",
42
+ "@bamboocss/types": "1.28.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@bamboocss/generator": "1.26.0",
46
- "@bamboocss/plugin-svelte": "1.26.0",
47
- "@bamboocss/plugin-vue": "1.26.0"
45
+ "@bamboocss/generator": "1.28.1",
46
+ "@bamboocss/plugin-svelte": "1.28.1",
47
+ "@bamboocss/plugin-vue": "1.28.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsdown src/index.ts --format=esm,cjs --dts",