@bamboocss/vite 1.35.5 → 1.36.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.
Files changed (3) hide show
  1. package/dist/index.cjs +100 -21
  2. package/dist/index.mjs +100 -21
  3. package/package.json +9 -9
package/dist/index.cjs CHANGED
@@ -89,6 +89,33 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
89
89
  removed = true;
90
90
  });
91
91
  }
92
+ if (prune) {
93
+ const present = /* @__PURE__ */ new Set();
94
+ root.walkRules((rule) => {
95
+ if (!isUtilityRule(rule)) return;
96
+ try {
97
+ (0, postcss_selector_parser.default)((selectors) => {
98
+ selectors.walkClasses((classNode) => {
99
+ present.add(classNode.toString().slice(1));
100
+ });
101
+ }).processSync(rule.selector);
102
+ } catch {}
103
+ });
104
+ const orphaned = [];
105
+ for (const className of session.usedClasses) {
106
+ if (/\s/.test(className)) {
107
+ orphaned.push(className);
108
+ continue;
109
+ }
110
+ if (!session.prunableClasses.has(className)) continue;
111
+ if (present.has(className)) continue;
112
+ orphaned.push(className);
113
+ }
114
+ if (orphaned.length) throw new Error(`bamboocss: ${orphaned.length} compiled class(es) have no rule in the emitted stylesheet. Elements carrying them would render unstyled.\n\n${(0, _bamboocss_shared.truncateList)(orphaned, {
115
+ unit: "class",
116
+ separator: "\n"
117
+ })}\n\nThis is a compiler bug rather than anything to fix in your source — please report it with the class names above.`);
118
+ }
92
119
  if (session.denseClassNames) root.walkRules((rule) => {
93
120
  if (!isUtilityRule(rule)) return;
94
121
  const transitionClasses = /* @__PURE__ */ new Set();
@@ -635,6 +662,54 @@ const collectModuleScopeNames = (sourceFile) => {
635
662
  }
636
663
  return names;
637
664
  };
665
+ /**
666
+ * Every identifier in this module that reads the binding `name` declared at `declaration`.
667
+ *
668
+ * Replaces `nameNode.findReferencesAsNodes()`, which is a TypeScript *language-service*
669
+ * query. The first such query forces `synchronizeHostData` -> `createProgram`, binding the
670
+ * whole transitive `.d.ts` closure of the project — the exact cost `createTsProject`'s
671
+ * `skipAddingFilesFromTsConfig`, `skipFileDependencyResolution` and `skipLoadingLibFiles`
672
+ * exist to avoid, and which none of them govern. On a 2,278-file app it loaded 24,081 source
673
+ * files and 4.4 GB of AST and symbols, 80% of the heap, and OOMed the build. The retained
674
+ * strings were `googleapis` and `typescript`; none of it can reference a recipe binding.
675
+ *
676
+ * A recipe binding is module-scoped or imported, so every read of it is in this file. A
677
+ * syntactic walk answers the same question over one AST the parser has already built.
678
+ *
679
+ * ## Where this deliberately over-reports
680
+ *
681
+ * Shadowing is not resolved. If any nested scope declares the same name, every matching
682
+ * identifier is returned rather than only the ones that bind to `declaration`. That direction
683
+ * is chosen on purpose: over-reporting fails the build with a diagnostic naming a real line,
684
+ * while under-reporting ships an element whose class has no rule behind it and says nothing.
685
+ * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
686
+ */
687
+ const localReferencesTo = (sourceFile, name, declaration) => {
688
+ const references = [];
689
+ for (const identifier of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier)) {
690
+ if (identifier.getText() !== name) continue;
691
+ if (identifier === declaration) continue;
692
+ const parent = identifier.getParent();
693
+ if (!parent) continue;
694
+ if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) continue;
695
+ if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) continue;
696
+ if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) continue;
697
+ if ((ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) && parent.getTagNameNode() === identifier) continue;
698
+ if (ts_morph.Node.isJsxAttribute(parent) && parent.getNameNode() === identifier) continue;
699
+ if ((ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) && parent.getNameNode() === identifier) continue;
700
+ if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent) || ts_morph.Node.isBindingElement(parent)) && parent.getNameNode() === identifier) continue;
701
+ if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) {
702
+ if (parent.getNameNode() !== identifier) continue;
703
+ if (ts_morph.Node.isExportSpecifier(parent)) {
704
+ references.push(identifier);
705
+ continue;
706
+ }
707
+ continue;
708
+ }
709
+ references.push(identifier);
710
+ }
711
+ return references;
712
+ };
638
713
  //#endregion
639
714
  //#region src/fold-recipe.ts
640
715
  /**
@@ -2383,30 +2458,36 @@ const foldSource = (options) => {
2383
2458
  * also allowed to remain when it joins an arbitrary external class; only fully analyzable
2384
2459
  * arguments receive Bamboo's semantic composition guarantee.
2385
2460
  */
2461
+ /** The specifier this module imported `binding` through, if it did. */
2462
+ function importSpecifierFor(sourceFile, binding) {
2463
+ for (const declaration of sourceFile.getImportDeclarations()) {
2464
+ if (declaration.isTypeOnly()) continue;
2465
+ for (const named of declaration.getNamedImports()) {
2466
+ if (named.isTypeOnly()) continue;
2467
+ if ((named.getAliasNode() ?? named.getNameNode()).getText() === binding) return named.getAliasNode() ?? named.getNameNode();
2468
+ }
2469
+ }
2470
+ }
2386
2471
  function reportRuntimeBindings() {
2387
2472
  const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
2388
2473
  if (!sourceFile) return;
2389
- for (const [binding, entry] of recipeConfigs) {
2474
+ const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
2475
+ for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
2476
+ const entry = recipeConfigs.get(binding);
2390
2477
  if (entry === AMBIGUOUS) continue;
2391
- const definition = entry.box?.getNode?.();
2392
- if (!definition || definition.getSourceFile() !== sourceFile) continue;
2393
- const nameNode = definition.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode();
2478
+ if (!entry && !importedRecipeBindings.has(binding)) continue;
2479
+ const definition = entry?.box?.getNode?.();
2480
+ const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
2394
2481
  if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
2395
- const references = nameNode.findReferencesAsNodes();
2482
+ const references = localReferencesTo(sourceFile, binding, nameNode);
2396
2483
  if (skipped.filter((item) => SURVIVES_TO_RUNTIME.has(item.reason) && item.end > item.start).some((item) => references.some((ref) => ref.getStart() >= item.start && ref.getStart() < item.end))) continue;
2397
2484
  const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
2398
2485
  if (!survivor) continue;
2399
- const survivorFile = survivor.getSourceFile();
2400
- const foreign = survivorFile !== sourceFile;
2401
2486
  skipped.push({
2402
2487
  name: binding,
2403
2488
  reason: "runtime-binding",
2404
- start: foreign ? nameNode.getStart() : survivor.getStart(),
2405
- end: foreign ? nameNode.getEnd() : survivor.getEnd(),
2406
- ...foreign ? { origin: {
2407
- filePath: survivorFile.getFilePath(),
2408
- line: survivor.getStartLineNumber()
2409
- } } : {}
2489
+ start: survivor.getStart(),
2490
+ end: survivor.getEnd()
2410
2491
  });
2411
2492
  }
2412
2493
  const bambooModules = [
@@ -2639,8 +2720,11 @@ const createStaticCompilationSession = (denseClassNames = true) => {
2639
2720
  }).join(" ");
2640
2721
  },
2641
2722
  markClassUsed(className) {
2642
- const semantic = session.semanticClasses.get(className) ?? className;
2643
- session.usedClasses.add((0, _bamboocss_shared.esc)(semantic));
2723
+ for (const token of className.split(" ")) {
2724
+ if (!token) continue;
2725
+ const semantic = session.semanticClasses.get(token) ?? token;
2726
+ session.usedClasses.add((0, _bamboocss_shared.esc)(semantic));
2727
+ }
2644
2728
  }
2645
2729
  };
2646
2730
  return session;
@@ -2870,12 +2954,7 @@ const bamboocss = (options = {}) => {
2870
2954
  for (const entry of result.skipped) {
2871
2955
  if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
2872
2956
  if (entry.name === "cx" && entry.reason === "dynamic") continue;
2873
- addSurvivor(entry.origin ? {
2874
- file: entry.origin.filePath,
2875
- line: entry.origin.line,
2876
- name: entry.name,
2877
- reason: entry.reason
2878
- } : {
2957
+ addSurvivor({
2879
2958
  file: filePath,
2880
2959
  line: lineAt(code, entry.start),
2881
2960
  name: entry.name,
package/dist/index.mjs CHANGED
@@ -59,6 +59,33 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
59
59
  removed = true;
60
60
  });
61
61
  }
62
+ if (prune) {
63
+ const present = /* @__PURE__ */ new Set();
64
+ root.walkRules((rule) => {
65
+ if (!isUtilityRule(rule)) return;
66
+ try {
67
+ selectorParser((selectors) => {
68
+ selectors.walkClasses((classNode) => {
69
+ present.add(classNode.toString().slice(1));
70
+ });
71
+ }).processSync(rule.selector);
72
+ } catch {}
73
+ });
74
+ const orphaned = [];
75
+ for (const className of session.usedClasses) {
76
+ if (/\s/.test(className)) {
77
+ orphaned.push(className);
78
+ continue;
79
+ }
80
+ if (!session.prunableClasses.has(className)) continue;
81
+ if (present.has(className)) continue;
82
+ orphaned.push(className);
83
+ }
84
+ if (orphaned.length) throw new Error(`bamboocss: ${orphaned.length} compiled class(es) have no rule in the emitted stylesheet. Elements carrying them would render unstyled.\n\n${truncateList(orphaned, {
85
+ unit: "class",
86
+ separator: "\n"
87
+ })}\n\nThis is a compiler bug rather than anything to fix in your source — please report it with the class names above.`);
88
+ }
62
89
  if (session.denseClassNames) root.walkRules((rule) => {
63
90
  if (!isUtilityRule(rule)) return;
64
91
  const transitionClasses = /* @__PURE__ */ new Set();
@@ -605,6 +632,54 @@ const collectModuleScopeNames = (sourceFile) => {
605
632
  }
606
633
  return names;
607
634
  };
635
+ /**
636
+ * Every identifier in this module that reads the binding `name` declared at `declaration`.
637
+ *
638
+ * Replaces `nameNode.findReferencesAsNodes()`, which is a TypeScript *language-service*
639
+ * query. The first such query forces `synchronizeHostData` -> `createProgram`, binding the
640
+ * whole transitive `.d.ts` closure of the project — the exact cost `createTsProject`'s
641
+ * `skipAddingFilesFromTsConfig`, `skipFileDependencyResolution` and `skipLoadingLibFiles`
642
+ * exist to avoid, and which none of them govern. On a 2,278-file app it loaded 24,081 source
643
+ * files and 4.4 GB of AST and symbols, 80% of the heap, and OOMed the build. The retained
644
+ * strings were `googleapis` and `typescript`; none of it can reference a recipe binding.
645
+ *
646
+ * A recipe binding is module-scoped or imported, so every read of it is in this file. A
647
+ * syntactic walk answers the same question over one AST the parser has already built.
648
+ *
649
+ * ## Where this deliberately over-reports
650
+ *
651
+ * Shadowing is not resolved. If any nested scope declares the same name, every matching
652
+ * identifier is returned rather than only the ones that bind to `declaration`. That direction
653
+ * is chosen on purpose: over-reporting fails the build with a diagnostic naming a real line,
654
+ * while under-reporting ships an element whose class has no rule behind it and says nothing.
655
+ * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
656
+ */
657
+ const localReferencesTo = (sourceFile, name, declaration) => {
658
+ const references = [];
659
+ for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
660
+ if (identifier.getText() !== name) continue;
661
+ if (identifier === declaration) continue;
662
+ const parent = identifier.getParent();
663
+ if (!parent) continue;
664
+ if (Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) continue;
665
+ if (Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) continue;
666
+ if (Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) continue;
667
+ if ((Node.isJsxOpeningElement(parent) || Node.isJsxSelfClosingElement(parent) || Node.isJsxClosingElement(parent)) && parent.getTagNameNode() === identifier) continue;
668
+ if (Node.isJsxAttribute(parent) && parent.getNameNode() === identifier) continue;
669
+ if ((Node.isMethodDeclaration(parent) || Node.isPropertyDeclaration(parent) || Node.isGetAccessorDeclaration(parent) || Node.isSetAccessorDeclaration(parent) || Node.isMethodSignature(parent) || Node.isPropertySignature(parent) || Node.isEnumMember(parent)) && parent.getNameNode() === identifier) continue;
670
+ if ((Node.isVariableDeclaration(parent) || Node.isParameterDeclaration(parent) || Node.isFunctionDeclaration(parent) || Node.isClassDeclaration(parent) || Node.isBindingElement(parent)) && parent.getNameNode() === identifier) continue;
671
+ if (Node.isImportSpecifier(parent) || Node.isExportSpecifier(parent)) {
672
+ if (parent.getNameNode() !== identifier) continue;
673
+ if (Node.isExportSpecifier(parent)) {
674
+ references.push(identifier);
675
+ continue;
676
+ }
677
+ continue;
678
+ }
679
+ references.push(identifier);
680
+ }
681
+ return references;
682
+ };
608
683
  //#endregion
609
684
  //#region src/fold-recipe.ts
610
685
  /**
@@ -2353,30 +2428,36 @@ const foldSource = (options) => {
2353
2428
  * also allowed to remain when it joins an arbitrary external class; only fully analyzable
2354
2429
  * arguments receive Bamboo's semantic composition guarantee.
2355
2430
  */
2431
+ /** The specifier this module imported `binding` through, if it did. */
2432
+ function importSpecifierFor(sourceFile, binding) {
2433
+ for (const declaration of sourceFile.getImportDeclarations()) {
2434
+ if (declaration.isTypeOnly()) continue;
2435
+ for (const named of declaration.getNamedImports()) {
2436
+ if (named.isTypeOnly()) continue;
2437
+ if ((named.getAliasNode() ?? named.getNameNode()).getText() === binding) return named.getAliasNode() ?? named.getNameNode();
2438
+ }
2439
+ }
2440
+ }
2356
2441
  function reportRuntimeBindings() {
2357
2442
  const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
2358
2443
  if (!sourceFile) return;
2359
- for (const [binding, entry] of recipeConfigs) {
2444
+ const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
2445
+ for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
2446
+ const entry = recipeConfigs.get(binding);
2360
2447
  if (entry === AMBIGUOUS) continue;
2361
- const definition = entry.box?.getNode?.();
2362
- if (!definition || definition.getSourceFile() !== sourceFile) continue;
2363
- const nameNode = definition.getFirstAncestorByKind(SyntaxKind.VariableDeclaration)?.getNameNode();
2448
+ if (!entry && !importedRecipeBindings.has(binding)) continue;
2449
+ const definition = entry?.box?.getNode?.();
2450
+ const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
2364
2451
  if (!nameNode || !Node.isIdentifier(nameNode)) continue;
2365
- const references = nameNode.findReferencesAsNodes();
2452
+ const references = localReferencesTo(sourceFile, binding, nameNode);
2366
2453
  if (skipped.filter((item) => SURVIVES_TO_RUNTIME.has(item.reason) && item.end > item.start).some((item) => references.some((ref) => ref.getStart() >= item.start && ref.getStart() < item.end))) continue;
2367
2454
  const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
2368
2455
  if (!survivor) continue;
2369
- const survivorFile = survivor.getSourceFile();
2370
- const foreign = survivorFile !== sourceFile;
2371
2456
  skipped.push({
2372
2457
  name: binding,
2373
2458
  reason: "runtime-binding",
2374
- start: foreign ? nameNode.getStart() : survivor.getStart(),
2375
- end: foreign ? nameNode.getEnd() : survivor.getEnd(),
2376
- ...foreign ? { origin: {
2377
- filePath: survivorFile.getFilePath(),
2378
- line: survivor.getStartLineNumber()
2379
- } } : {}
2459
+ start: survivor.getStart(),
2460
+ end: survivor.getEnd()
2380
2461
  });
2381
2462
  }
2382
2463
  const bambooModules = [
@@ -2609,8 +2690,11 @@ const createStaticCompilationSession = (denseClassNames = true) => {
2609
2690
  }).join(" ");
2610
2691
  },
2611
2692
  markClassUsed(className) {
2612
- const semantic = session.semanticClasses.get(className) ?? className;
2613
- session.usedClasses.add(esc(semantic));
2693
+ for (const token of className.split(" ")) {
2694
+ if (!token) continue;
2695
+ const semantic = session.semanticClasses.get(token) ?? token;
2696
+ session.usedClasses.add(esc(semantic));
2697
+ }
2614
2698
  }
2615
2699
  };
2616
2700
  return session;
@@ -2840,12 +2924,7 @@ const bamboocss = (options = {}) => {
2840
2924
  for (const entry of result.skipped) {
2841
2925
  if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
2842
2926
  if (entry.name === "cx" && entry.reason === "dynamic") continue;
2843
- addSurvivor(entry.origin ? {
2844
- file: entry.origin.filePath,
2845
- line: entry.origin.line,
2846
- name: entry.name,
2847
- reason: entry.reason
2848
- } : {
2927
+ addSurvivor({
2849
2928
  file: filePath,
2850
2929
  line: lineAt(code, entry.start),
2851
2930
  name: entry.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.35.5",
3
+ "version": "1.36.1",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -40,18 +40,18 @@
40
40
  "postcss": "8.5.26",
41
41
  "postcss-selector-parser": "7.1.5",
42
42
  "ts-morph": "28.0.0",
43
- "@bamboocss/config": "1.35.5",
44
- "@bamboocss/core": "1.35.5",
45
- "@bamboocss/extractor": "1.35.5",
46
- "@bamboocss/shared": "1.35.5",
47
- "@bamboocss/logger": "1.35.5",
48
- "@bamboocss/node": "1.35.5",
49
- "@bamboocss/types": "1.35.5"
43
+ "@bamboocss/config": "1.36.1",
44
+ "@bamboocss/extractor": "1.36.1",
45
+ "@bamboocss/logger": "1.36.1",
46
+ "@bamboocss/node": "1.36.1",
47
+ "@bamboocss/core": "1.36.1",
48
+ "@bamboocss/types": "1.36.1",
49
+ "@bamboocss/shared": "1.36.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.35.5"
54
+ "@bamboocss/fixture": "1.36.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"