@bamboocss/vite 1.37.8 → 1.37.10

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
@@ -80,20 +80,31 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
80
80
  };
81
81
  if (prune) root.walkRules((rule) => {
82
82
  if (!isUtilityRule(rule)) return;
83
- const classes = /* @__PURE__ */ new Set();
83
+ let removedAny = false;
84
+ let selector;
84
85
  try {
85
- (0, postcss_selector_parser.default)((selectors) => {
86
- selectors.walkClasses((classNode) => {
87
- classes.add(bare(classNode.toString().slice(1)));
86
+ selector = (0, postcss_selector_parser.default)((selectors) => {
87
+ selectors.each((candidate) => {
88
+ const classes = /* @__PURE__ */ new Set();
89
+ candidate.walkClasses((classNode) => {
90
+ classes.add(bare(classNode.toString().slice(1)));
91
+ });
92
+ if (classes.size !== 1) return;
93
+ const [className] = classes;
94
+ if (!className || !prunable.has(className) || used.has(className)) return;
95
+ candidate.remove();
96
+ removedAny = true;
88
97
  });
89
98
  }).processSync(rule.selector);
90
99
  } catch {
91
100
  return;
92
101
  }
93
- if (classes.size !== 1) return;
94
- const [className] = classes;
95
- if (!className || !prunable.has(className) || used.has(className)) return;
96
- rule.remove();
102
+ if (!removedAny) return;
103
+ if (!selector.trim()) {
104
+ rule.remove();
105
+ return;
106
+ }
107
+ rule.selector = selector;
97
108
  });
98
109
  let removed = true;
99
110
  while (removed) {
@@ -740,10 +751,28 @@ const collectModuleScopeNames = (sourceFile) => {
740
751
  * while under-reporting ships an element whose class has no rule behind it and says nothing.
741
752
  * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
742
753
  */
754
+ const identifiersByName = /* @__PURE__ */ new WeakMap();
755
+ /**
756
+ * Every identifier in the module, grouped by the name it spells.
757
+ *
758
+ * Walking the whole tree per binding made this O(bindings x identifiers): a module declaring
759
+ * ten recipes walked its identifiers ten times. One walk answers for all of them, and the
760
+ * result is cached against the source text like the module-scope names beside it, so a
761
+ * re-transform of unchanged text reuses it.
762
+ */
763
+ const identifierIndex = (sourceFile) => byText(identifiersByName, sourceFile, () => {
764
+ const index = /* @__PURE__ */ new Map();
765
+ for (const identifier of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier)) {
766
+ const text = identifier.getText();
767
+ const known = index.get(text);
768
+ if (known) known.push(identifier);
769
+ else index.set(text, [identifier]);
770
+ }
771
+ return index;
772
+ });
743
773
  const localReferencesTo = (sourceFile, name, declaration) => {
744
774
  const references = [];
745
- for (const identifier of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier)) {
746
- if (identifier.getText() !== name) continue;
775
+ for (const identifier of identifierIndex(sourceFile).get(name) ?? []) {
747
776
  if (identifier === declaration) continue;
748
777
  const parent = identifier.getParent();
749
778
  if (!parent) continue;
@@ -2828,18 +2857,24 @@ const bamboocss = (options = {}) => {
2828
2857
  skipped: /* @__PURE__ */ new Map()
2829
2858
  };
2830
2859
  const staticSession = createStaticCompilationSession();
2831
- const survivors = [];
2832
- const survivorKeys = /* @__PURE__ */ new Set();
2860
+ /**
2861
+ * Indexed by file, because the only bulk operation on it is "forget this one's".
2862
+ *
2863
+ * A flat array meant every transform scanned every survivor and then rebuilt the dedupe key
2864
+ * set from scratch — O(modules x survivors) across a build, and worst exactly when a build is
2865
+ * already failing and the user is iterating on it. One project had 736 of them across 9,461
2866
+ * modules, which is seven million string builds to discard.
2867
+ */
2868
+ const survivorsByFile = /* @__PURE__ */ new Map();
2869
+ const allSurvivors = () => [...survivorsByFile.values()].flat();
2833
2870
  const addSurvivor = (entry) => {
2834
- const key = `${entry.file}:${entry.line}:${entry.name}:${entry.reason}`;
2835
- if (survivorKeys.has(key)) return;
2836
- survivorKeys.add(key);
2837
- survivors.push(entry);
2871
+ const forFile = survivorsByFile.get(entry.file) ?? [];
2872
+ if (forFile.some((seen) => seen.line === entry.line && seen.name === entry.name && seen.reason === entry.reason)) return;
2873
+ forFile.push(entry);
2874
+ survivorsByFile.set(entry.file, forFile);
2838
2875
  };
2839
2876
  const clearSurvivorsFor = (file) => {
2840
- for (let index = survivors.length - 1; index >= 0; index--) if (survivors[index]?.file === file) survivors.splice(index, 1);
2841
- survivorKeys.clear();
2842
- for (const entry of survivors) survivorKeys.add(`${entry.file}:${entry.line}:${entry.name}:${entry.reason}`);
2877
+ survivorsByFile.delete(file);
2843
2878
  };
2844
2879
  const createSurvivorError = (entries) => {
2845
2880
  const byFile = /* @__PURE__ */ new Map();
@@ -2898,8 +2933,7 @@ const bamboocss = (options = {}) => {
2898
2933
  totals.files = 0;
2899
2934
  totals.filesWithFolds = 0;
2900
2935
  totals.skipped.clear();
2901
- survivors.length = 0;
2902
- survivorKeys.clear();
2936
+ survivorsByFile.clear();
2903
2937
  recipeConfigCache.clear();
2904
2938
  resetStaticCompilationSession(staticSession);
2905
2939
  }
@@ -3002,7 +3036,8 @@ const bamboocss = (options = {}) => {
3002
3036
  }
3003
3037
  if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
3004
3038
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
3005
- if (command === "serve" && survivors.some((entry) => entry.file === filePath)) throw createSurvivorError(survivors.filter((entry) => entry.file === filePath));
3039
+ const forFile = survivorsByFile.get(filePath);
3040
+ if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
3006
3041
  if (!result.folded.length) return null;
3007
3042
  _bamboocss_logger.logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
3008
3043
  return {
@@ -3011,6 +3046,7 @@ const bamboocss = (options = {}) => {
3011
3046
  };
3012
3047
  },
3013
3048
  buildEnd() {
3049
+ const survivors = allSurvivors();
3014
3050
  if (survivors.length) throw createSurvivorError(survivors);
3015
3051
  if (typeof this.getModuleInfo === "function") {
3016
3052
  if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
package/dist/index.mjs CHANGED
@@ -50,20 +50,31 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
50
50
  };
51
51
  if (prune) root.walkRules((rule) => {
52
52
  if (!isUtilityRule(rule)) return;
53
- const classes = /* @__PURE__ */ new Set();
53
+ let removedAny = false;
54
+ let selector;
54
55
  try {
55
- selectorParser((selectors) => {
56
- selectors.walkClasses((classNode) => {
57
- classes.add(bare(classNode.toString().slice(1)));
56
+ selector = selectorParser((selectors) => {
57
+ selectors.each((candidate) => {
58
+ const classes = /* @__PURE__ */ new Set();
59
+ candidate.walkClasses((classNode) => {
60
+ classes.add(bare(classNode.toString().slice(1)));
61
+ });
62
+ if (classes.size !== 1) return;
63
+ const [className] = classes;
64
+ if (!className || !prunable.has(className) || used.has(className)) return;
65
+ candidate.remove();
66
+ removedAny = true;
58
67
  });
59
68
  }).processSync(rule.selector);
60
69
  } catch {
61
70
  return;
62
71
  }
63
- if (classes.size !== 1) return;
64
- const [className] = classes;
65
- if (!className || !prunable.has(className) || used.has(className)) return;
66
- rule.remove();
72
+ if (!removedAny) return;
73
+ if (!selector.trim()) {
74
+ rule.remove();
75
+ return;
76
+ }
77
+ rule.selector = selector;
67
78
  });
68
79
  let removed = true;
69
80
  while (removed) {
@@ -710,10 +721,28 @@ const collectModuleScopeNames = (sourceFile) => {
710
721
  * while under-reporting ships an element whose class has no rule behind it and says nothing.
711
722
  * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
712
723
  */
724
+ const identifiersByName = /* @__PURE__ */ new WeakMap();
725
+ /**
726
+ * Every identifier in the module, grouped by the name it spells.
727
+ *
728
+ * Walking the whole tree per binding made this O(bindings x identifiers): a module declaring
729
+ * ten recipes walked its identifiers ten times. One walk answers for all of them, and the
730
+ * result is cached against the source text like the module-scope names beside it, so a
731
+ * re-transform of unchanged text reuses it.
732
+ */
733
+ const identifierIndex = (sourceFile) => byText(identifiersByName, sourceFile, () => {
734
+ const index = /* @__PURE__ */ new Map();
735
+ for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
736
+ const text = identifier.getText();
737
+ const known = index.get(text);
738
+ if (known) known.push(identifier);
739
+ else index.set(text, [identifier]);
740
+ }
741
+ return index;
742
+ });
713
743
  const localReferencesTo = (sourceFile, name, declaration) => {
714
744
  const references = [];
715
- for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
716
- if (identifier.getText() !== name) continue;
745
+ for (const identifier of identifierIndex(sourceFile).get(name) ?? []) {
717
746
  if (identifier === declaration) continue;
718
747
  const parent = identifier.getParent();
719
748
  if (!parent) continue;
@@ -2798,18 +2827,24 @@ const bamboocss = (options = {}) => {
2798
2827
  skipped: /* @__PURE__ */ new Map()
2799
2828
  };
2800
2829
  const staticSession = createStaticCompilationSession();
2801
- const survivors = [];
2802
- const survivorKeys = /* @__PURE__ */ new Set();
2830
+ /**
2831
+ * Indexed by file, because the only bulk operation on it is "forget this one's".
2832
+ *
2833
+ * A flat array meant every transform scanned every survivor and then rebuilt the dedupe key
2834
+ * set from scratch — O(modules x survivors) across a build, and worst exactly when a build is
2835
+ * already failing and the user is iterating on it. One project had 736 of them across 9,461
2836
+ * modules, which is seven million string builds to discard.
2837
+ */
2838
+ const survivorsByFile = /* @__PURE__ */ new Map();
2839
+ const allSurvivors = () => [...survivorsByFile.values()].flat();
2803
2840
  const addSurvivor = (entry) => {
2804
- const key = `${entry.file}:${entry.line}:${entry.name}:${entry.reason}`;
2805
- if (survivorKeys.has(key)) return;
2806
- survivorKeys.add(key);
2807
- survivors.push(entry);
2841
+ const forFile = survivorsByFile.get(entry.file) ?? [];
2842
+ if (forFile.some((seen) => seen.line === entry.line && seen.name === entry.name && seen.reason === entry.reason)) return;
2843
+ forFile.push(entry);
2844
+ survivorsByFile.set(entry.file, forFile);
2808
2845
  };
2809
2846
  const clearSurvivorsFor = (file) => {
2810
- for (let index = survivors.length - 1; index >= 0; index--) if (survivors[index]?.file === file) survivors.splice(index, 1);
2811
- survivorKeys.clear();
2812
- for (const entry of survivors) survivorKeys.add(`${entry.file}:${entry.line}:${entry.name}:${entry.reason}`);
2847
+ survivorsByFile.delete(file);
2813
2848
  };
2814
2849
  const createSurvivorError = (entries) => {
2815
2850
  const byFile = /* @__PURE__ */ new Map();
@@ -2868,8 +2903,7 @@ const bamboocss = (options = {}) => {
2868
2903
  totals.files = 0;
2869
2904
  totals.filesWithFolds = 0;
2870
2905
  totals.skipped.clear();
2871
- survivors.length = 0;
2872
- survivorKeys.clear();
2906
+ survivorsByFile.clear();
2873
2907
  recipeConfigCache.clear();
2874
2908
  resetStaticCompilationSession(staticSession);
2875
2909
  }
@@ -2972,7 +3006,8 @@ const bamboocss = (options = {}) => {
2972
3006
  }
2973
3007
  if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
2974
3008
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
2975
- if (command === "serve" && survivors.some((entry) => entry.file === filePath)) throw createSurvivorError(survivors.filter((entry) => entry.file === filePath));
3009
+ const forFile = survivorsByFile.get(filePath);
3010
+ if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
2976
3011
  if (!result.folded.length) return null;
2977
3012
  logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
2978
3013
  return {
@@ -2981,6 +3016,7 @@ const bamboocss = (options = {}) => {
2981
3016
  };
2982
3017
  },
2983
3018
  buildEnd() {
3019
+ const survivors = allSurvivors();
2984
3020
  if (survivors.length) throw createSurvivorError(survivors);
2985
3021
  if (typeof this.getModuleInfo === "function") {
2986
3022
  if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.37.8",
3
+ "version": "1.37.10",
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.37.8",
44
- "@bamboocss/core": "1.37.8",
45
- "@bamboocss/extractor": "1.37.8",
46
- "@bamboocss/logger": "1.37.8",
47
- "@bamboocss/node": "1.37.8",
48
- "@bamboocss/shared": "1.37.8",
49
- "@bamboocss/types": "1.37.8"
43
+ "@bamboocss/config": "1.37.10",
44
+ "@bamboocss/core": "1.37.10",
45
+ "@bamboocss/extractor": "1.37.10",
46
+ "@bamboocss/node": "1.37.10",
47
+ "@bamboocss/logger": "1.37.10",
48
+ "@bamboocss/types": "1.37.10",
49
+ "@bamboocss/shared": "1.37.10"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.37.8"
54
+ "@bamboocss/fixture": "1.37.10"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"