@bamboocss/vite 1.43.1 → 1.44.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
@@ -1025,34 +1025,33 @@ const collectModuleScopeNames = (sourceFile) => {
1025
1025
  return names;
1026
1026
  };
1027
1027
  /**
1028
- * Every identifier in this module that reads the binding `name` declared at `declaration`.
1029
- *
1030
- * Replaces `nameNode.findReferencesAsNodes()`, which is a TypeScript *language-service*
1031
- * query. The first such query forces `synchronizeHostData` -> `createProgram`, binding the
1032
- * whole transitive `.d.ts` closure of the project — the exact cost `createTsProject`'s
1033
- * `skipAddingFilesFromTsConfig`, `skipFileDependencyResolution` and `skipLoadingLibFiles`
1034
- * exist to avoid, and which none of them govern. On a 2,278-file app it loaded 24,081 source
1035
- * files and 4.4 GB of AST and symbols, 80% of the heap, and OOMed the build. The retained
1036
- * strings were `googleapis` and `typescript`; none of it can reference a recipe binding.
1037
- *
1038
- * A recipe binding is module-scoped or imported, so every read of it is in this file. A
1039
- * syntactic walk answers the same question over one AST the parser has already built.
1040
- *
1041
- * ## Where this deliberately over-reports
1042
- *
1043
- * Shadowing is not resolved. If any nested scope declares the same name, every matching
1044
- * identifier is returned rather than only the ones that bind to `declaration`. That direction
1045
- * is chosen on purpose: over-reporting fails the build with a diagnostic naming a real line,
1046
- * while under-reporting ships an element whose class has no rule behind it and says nothing.
1047
- * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
1048
- */
1049
- /**
1050
1028
  * Every identifier in the module, grouped by the name it spells.
1051
1029
  *
1052
1030
  * Built once per pass and handed to each lookup, because walking the whole tree per binding
1053
1031
  * made that O(bindings x identifiers): a module declaring ten recipes walked its identifiers
1054
1032
  * ten times.
1055
1033
  *
1034
+ * ## Why this is a raw walk rather than `getDescendantsOfKind`
1035
+ *
1036
+ * `SyntaxKind.Identifier` sorts *below* `SyntaxKind.FirstNode`, which is what ts-morph tests to
1037
+ * decide whether it may search the parse tree. For a kind below that line it falls back to
1038
+ * materialising the whole **token** tree — every brace, comma and keyword becomes a ts-morph node
1039
+ * on the way to collecting the identifiers. On 55 KB of real tsx that measured 22ms against
1040
+ * 0.22ms for the same collection over compiler nodes, and the node cache does not help: a second
1041
+ * call cost the same 22ms.
1042
+ *
1043
+ * Wrapping is what costs, so only the buckets a caller actually reads are wrapped, on the first
1044
+ * read and cached after. Nothing enumerates this index — both callers ask for one name — so the
1045
+ * rest is never built. `_getNodeFromCompilerNode` is ts-morph's own memoized wrapper factory, so
1046
+ * a node handed back here is the very object `getDescendantsOfKind` would have returned, which
1047
+ * `localReferencesTo` depends on: it compares against the declaration by identity.
1048
+ *
1049
+ * JSDoc is walked explicitly. `ts.forEachChild` does not descend into it, while the token path
1050
+ * this replaces does, so a name mentioned only in a `@type` annotation was previously found and
1051
+ * would otherwise stop being — a silent narrowing of what counts as a surviving reference.
1052
+ * Keyed on `escapedText` because that is what ts-morph's `Identifier.getText()` returns: the name
1053
+ * as the compiler resolves it, so `\u0062adge` and `badge` share a bucket exactly as before.
1054
+ *
1056
1055
  * Deliberately *not* memoized across passes, unlike the module-scope names beside it. That
1057
1056
  * cache holds strings, which outlive anything; this one holds nodes, and a node does not
1058
1057
  * survive its source file being replaced — `addSourceFile` overwrites, which forgets every
@@ -1061,18 +1060,32 @@ const collectModuleScopeNames = (sourceFile) => {
1061
1060
  * `Attempted to get information from a node that was removed or forgotten` on the next read.
1062
1061
  */
1063
1062
  const identifierIndex = (sourceFile) => {
1064
- const index = /* @__PURE__ */ new Map();
1065
- for (const identifier of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier)) {
1066
- const text = identifier.getText();
1067
- const known = index.get(text);
1068
- if (known) known.push(identifier);
1069
- else index.set(text, [identifier]);
1070
- }
1071
- return index;
1063
+ const compilerNodes = /* @__PURE__ */ new Map();
1064
+ const collect = (node) => {
1065
+ if (node.kind === ts_morph.ts.SyntaxKind.Identifier) {
1066
+ const name = String(node.escapedText);
1067
+ const known = compilerNodes.get(name);
1068
+ if (known) known.push(node);
1069
+ else compilerNodes.set(name, [node]);
1070
+ }
1071
+ const jsDoc = node.jsDoc;
1072
+ if (jsDoc) for (const doc of jsDoc) collect(doc);
1073
+ ts_morph.ts.forEachChild(node, collect);
1074
+ };
1075
+ ts_morph.ts.forEachChild(sourceFile.compilerNode, collect);
1076
+ const wrapped = /* @__PURE__ */ new Map();
1077
+ const wrap = sourceFile;
1078
+ return { get: (name) => {
1079
+ const known = wrapped.get(name);
1080
+ if (known) return known;
1081
+ const nodes = (compilerNodes.get(name) ?? []).map((node) => wrap._getNodeFromCompilerNode(node));
1082
+ wrapped.set(name, nodes);
1083
+ return nodes;
1084
+ } };
1072
1085
  };
1073
1086
  const localReferencesTo = (index, name, declaration) => {
1074
1087
  const references = [];
1075
- for (const identifier of index.get(name) ?? []) {
1088
+ for (const identifier of index.get(name)) {
1076
1089
  if (identifier === declaration) continue;
1077
1090
  const parent = identifier.getParent();
1078
1091
  if (!parent) continue;
@@ -1705,6 +1718,16 @@ const PERMITTED_BINDINGS = new Set([
1705
1718
  SPLIT_PROPS_HELPER
1706
1719
  ]);
1707
1720
  /**
1721
+ * Whether a module's text could hold a `splitVariantProps` property access.
1722
+ *
1723
+ * A necessary condition, deliberately not a sufficient one: the name inside a string or a
1724
+ * comment opens the walk, which costs what the walk always cost. What it must never do is
1725
+ * close on a module that has one, and an identifier may be spelled with unicode escapes —
1726
+ * `badge.splitVariantProps(p)` reads as the name to the compiler and contains none of it
1727
+ * as text. Both escape forms start `\u`, so one test covers every spelling.
1728
+ */
1729
+ const mayNameSplitVariantProps = (text) => text.includes("splitVariantProps") || text.includes("\\u");
1730
+ /**
1708
1731
  * The pieces `trim` reduces a module specifier by, hoisted because a regex literal
1709
1732
  * constructs a new object every time it is evaluated and `trim` runs per specifier per
1710
1733
  * import declaration per module.
@@ -2512,7 +2535,7 @@ const foldSource = (options) => {
2512
2535
  }
2513
2536
  }
2514
2537
  const byRange = new Map(candidates.map((candidate) => [`${candidate.start}:${candidate.end}`, candidate]));
2515
- for (const call of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression)) {
2538
+ for (const call of cxBindings.size ? sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression) : []) {
2516
2539
  const callee = call.getExpression();
2517
2540
  if (!ts_morph.Node.isIdentifier(callee) || !cxBindings.has(callee.getText()) || isShadowed(call, callee.getText())) continue;
2518
2541
  const matched = [];
@@ -2789,7 +2812,7 @@ const foldSource = (options) => {
2789
2812
  collectSourceFiles(item.box, dependencyScan);
2790
2813
  }
2791
2814
  const recipeSourceFile = candidates[0]?.node.getSourceFile();
2792
- if (recipeSourceFile) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
2815
+ if (recipeSourceFile && mayNameSplitVariantProps(recipeSourceFile.getFullText())) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
2793
2816
  if (access.getName() !== "splitVariantProps") continue;
2794
2817
  const target = access.getExpression();
2795
2818
  if (!ts_morph.Node.isIdentifier(target)) continue;
@@ -2896,7 +2919,13 @@ const foldSource = (options) => {
2896
2919
  ...ctx.imports.matchers.pattern?.mods ?? [],
2897
2920
  ...ctx.imports.matchers.tokens?.mods ?? []
2898
2921
  ];
2899
- for (const call of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression)) {
2922
+ const runtimeCalls = [];
2923
+ const importEquals = [];
2924
+ sourceFile.forEachDescendant((node) => {
2925
+ if (ts_morph.Node.isCallExpression(node)) runtimeCalls.push(node);
2926
+ else if (ts_morph.Node.isImportEqualsDeclaration(node)) importEquals.push(node);
2927
+ });
2928
+ for (const call of runtimeCalls) {
2900
2929
  const callee = call.getExpression();
2901
2930
  const argument = call.getArguments()[0];
2902
2931
  if (!argument || !ts_morph.Node.isStringLiteral(argument) && !ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) continue;
@@ -2911,7 +2940,7 @@ const foldSource = (options) => {
2911
2940
  end: call.getEnd()
2912
2941
  });
2913
2942
  }
2914
- for (const declaration of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.ImportEqualsDeclaration)) {
2943
+ for (const declaration of importEquals) {
2915
2944
  if (declaration.isTypeOnly()) continue;
2916
2945
  const reference = declaration.getModuleReference();
2917
2946
  if (!ts_morph.Node.isExternalModuleReference(reference)) continue;
@@ -2981,7 +3010,7 @@ const foldSource = (options) => {
2981
3010
  if (watched.size === 0) return;
2982
3011
  const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
2983
3012
  const survivors = [];
2984
- for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local) ?? []) {
3013
+ for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local)) {
2985
3014
  const start = identifier.getStart();
2986
3015
  if (identifier.getFirstAncestorByKind(ts_morph.SyntaxKind.ImportDeclaration)) continue;
2987
3016
  if (applied.some(([from, to]) => start >= from && start < to)) continue;
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import selectorParser from "postcss-selector-parser";
9
9
  import { dirname, relative, resolve } from "node:path";
10
10
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
11
11
  import { box, maybeBoxNode } from "@bamboocss/extractor";
12
- import { Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
12
+ import { Node, SyntaxKind, VariableDeclarationKind, ts } from "ts-morph";
13
13
  //#region src/prune-static-css.ts
14
14
  /** The generated declaration that identifies a Bamboo stylesheet after minification. */
15
15
  const SENTINEL = "--made-with-bamboo";
@@ -995,34 +995,33 @@ const collectModuleScopeNames = (sourceFile) => {
995
995
  return names;
996
996
  };
997
997
  /**
998
- * Every identifier in this module that reads the binding `name` declared at `declaration`.
999
- *
1000
- * Replaces `nameNode.findReferencesAsNodes()`, which is a TypeScript *language-service*
1001
- * query. The first such query forces `synchronizeHostData` -> `createProgram`, binding the
1002
- * whole transitive `.d.ts` closure of the project — the exact cost `createTsProject`'s
1003
- * `skipAddingFilesFromTsConfig`, `skipFileDependencyResolution` and `skipLoadingLibFiles`
1004
- * exist to avoid, and which none of them govern. On a 2,278-file app it loaded 24,081 source
1005
- * files and 4.4 GB of AST and symbols, 80% of the heap, and OOMed the build. The retained
1006
- * strings were `googleapis` and `typescript`; none of it can reference a recipe binding.
1007
- *
1008
- * A recipe binding is module-scoped or imported, so every read of it is in this file. A
1009
- * syntactic walk answers the same question over one AST the parser has already built.
1010
- *
1011
- * ## Where this deliberately over-reports
1012
- *
1013
- * Shadowing is not resolved. If any nested scope declares the same name, every matching
1014
- * identifier is returned rather than only the ones that bind to `declaration`. That direction
1015
- * is chosen on purpose: over-reporting fails the build with a diagnostic naming a real line,
1016
- * while under-reporting ships an element whose class has no rule behind it and says nothing.
1017
- * Shadowing a recipe binding is rare; silently shipping unstyled markup is not recoverable.
1018
- */
1019
- /**
1020
998
  * Every identifier in the module, grouped by the name it spells.
1021
999
  *
1022
1000
  * Built once per pass and handed to each lookup, because walking the whole tree per binding
1023
1001
  * made that O(bindings x identifiers): a module declaring ten recipes walked its identifiers
1024
1002
  * ten times.
1025
1003
  *
1004
+ * ## Why this is a raw walk rather than `getDescendantsOfKind`
1005
+ *
1006
+ * `SyntaxKind.Identifier` sorts *below* `SyntaxKind.FirstNode`, which is what ts-morph tests to
1007
+ * decide whether it may search the parse tree. For a kind below that line it falls back to
1008
+ * materialising the whole **token** tree — every brace, comma and keyword becomes a ts-morph node
1009
+ * on the way to collecting the identifiers. On 55 KB of real tsx that measured 22ms against
1010
+ * 0.22ms for the same collection over compiler nodes, and the node cache does not help: a second
1011
+ * call cost the same 22ms.
1012
+ *
1013
+ * Wrapping is what costs, so only the buckets a caller actually reads are wrapped, on the first
1014
+ * read and cached after. Nothing enumerates this index — both callers ask for one name — so the
1015
+ * rest is never built. `_getNodeFromCompilerNode` is ts-morph's own memoized wrapper factory, so
1016
+ * a node handed back here is the very object `getDescendantsOfKind` would have returned, which
1017
+ * `localReferencesTo` depends on: it compares against the declaration by identity.
1018
+ *
1019
+ * JSDoc is walked explicitly. `ts.forEachChild` does not descend into it, while the token path
1020
+ * this replaces does, so a name mentioned only in a `@type` annotation was previously found and
1021
+ * would otherwise stop being — a silent narrowing of what counts as a surviving reference.
1022
+ * Keyed on `escapedText` because that is what ts-morph's `Identifier.getText()` returns: the name
1023
+ * as the compiler resolves it, so `\u0062adge` and `badge` share a bucket exactly as before.
1024
+ *
1026
1025
  * Deliberately *not* memoized across passes, unlike the module-scope names beside it. That
1027
1026
  * cache holds strings, which outlive anything; this one holds nodes, and a node does not
1028
1027
  * survive its source file being replaced — `addSourceFile` overwrites, which forgets every
@@ -1031,18 +1030,32 @@ const collectModuleScopeNames = (sourceFile) => {
1031
1030
  * `Attempted to get information from a node that was removed or forgotten` on the next read.
1032
1031
  */
1033
1032
  const identifierIndex = (sourceFile) => {
1034
- const index = /* @__PURE__ */ new Map();
1035
- for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
1036
- const text = identifier.getText();
1037
- const known = index.get(text);
1038
- if (known) known.push(identifier);
1039
- else index.set(text, [identifier]);
1040
- }
1041
- return index;
1033
+ const compilerNodes = /* @__PURE__ */ new Map();
1034
+ const collect = (node) => {
1035
+ if (node.kind === ts.SyntaxKind.Identifier) {
1036
+ const name = String(node.escapedText);
1037
+ const known = compilerNodes.get(name);
1038
+ if (known) known.push(node);
1039
+ else compilerNodes.set(name, [node]);
1040
+ }
1041
+ const jsDoc = node.jsDoc;
1042
+ if (jsDoc) for (const doc of jsDoc) collect(doc);
1043
+ ts.forEachChild(node, collect);
1044
+ };
1045
+ ts.forEachChild(sourceFile.compilerNode, collect);
1046
+ const wrapped = /* @__PURE__ */ new Map();
1047
+ const wrap = sourceFile;
1048
+ return { get: (name) => {
1049
+ const known = wrapped.get(name);
1050
+ if (known) return known;
1051
+ const nodes = (compilerNodes.get(name) ?? []).map((node) => wrap._getNodeFromCompilerNode(node));
1052
+ wrapped.set(name, nodes);
1053
+ return nodes;
1054
+ } };
1042
1055
  };
1043
1056
  const localReferencesTo = (index, name, declaration) => {
1044
1057
  const references = [];
1045
- for (const identifier of index.get(name) ?? []) {
1058
+ for (const identifier of index.get(name)) {
1046
1059
  if (identifier === declaration) continue;
1047
1060
  const parent = identifier.getParent();
1048
1061
  if (!parent) continue;
@@ -1675,6 +1688,16 @@ const PERMITTED_BINDINGS = new Set([
1675
1688
  SPLIT_PROPS_HELPER
1676
1689
  ]);
1677
1690
  /**
1691
+ * Whether a module's text could hold a `splitVariantProps` property access.
1692
+ *
1693
+ * A necessary condition, deliberately not a sufficient one: the name inside a string or a
1694
+ * comment opens the walk, which costs what the walk always cost. What it must never do is
1695
+ * close on a module that has one, and an identifier may be spelled with unicode escapes —
1696
+ * `badge.splitVariantProps(p)` reads as the name to the compiler and contains none of it
1697
+ * as text. Both escape forms start `\u`, so one test covers every spelling.
1698
+ */
1699
+ const mayNameSplitVariantProps = (text) => text.includes("splitVariantProps") || text.includes("\\u");
1700
+ /**
1678
1701
  * The pieces `trim` reduces a module specifier by, hoisted because a regex literal
1679
1702
  * constructs a new object every time it is evaluated and `trim` runs per specifier per
1680
1703
  * import declaration per module.
@@ -2482,7 +2505,7 @@ const foldSource = (options) => {
2482
2505
  }
2483
2506
  }
2484
2507
  const byRange = new Map(candidates.map((candidate) => [`${candidate.start}:${candidate.end}`, candidate]));
2485
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2508
+ for (const call of cxBindings.size ? sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression) : []) {
2486
2509
  const callee = call.getExpression();
2487
2510
  if (!Node.isIdentifier(callee) || !cxBindings.has(callee.getText()) || isShadowed(call, callee.getText())) continue;
2488
2511
  const matched = [];
@@ -2759,7 +2782,7 @@ const foldSource = (options) => {
2759
2782
  collectSourceFiles(item.box, dependencyScan);
2760
2783
  }
2761
2784
  const recipeSourceFile = candidates[0]?.node.getSourceFile();
2762
- if (recipeSourceFile) for (const access of recipeSourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
2785
+ if (recipeSourceFile && mayNameSplitVariantProps(recipeSourceFile.getFullText())) for (const access of recipeSourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
2763
2786
  if (access.getName() !== "splitVariantProps") continue;
2764
2787
  const target = access.getExpression();
2765
2788
  if (!Node.isIdentifier(target)) continue;
@@ -2866,7 +2889,13 @@ const foldSource = (options) => {
2866
2889
  ...ctx.imports.matchers.pattern?.mods ?? [],
2867
2890
  ...ctx.imports.matchers.tokens?.mods ?? []
2868
2891
  ];
2869
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2892
+ const runtimeCalls = [];
2893
+ const importEquals = [];
2894
+ sourceFile.forEachDescendant((node) => {
2895
+ if (Node.isCallExpression(node)) runtimeCalls.push(node);
2896
+ else if (Node.isImportEqualsDeclaration(node)) importEquals.push(node);
2897
+ });
2898
+ for (const call of runtimeCalls) {
2870
2899
  const callee = call.getExpression();
2871
2900
  const argument = call.getArguments()[0];
2872
2901
  if (!argument || !Node.isStringLiteral(argument) && !Node.isNoSubstitutionTemplateLiteral(argument)) continue;
@@ -2881,7 +2910,7 @@ const foldSource = (options) => {
2881
2910
  end: call.getEnd()
2882
2911
  });
2883
2912
  }
2884
- for (const declaration of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
2913
+ for (const declaration of importEquals) {
2885
2914
  if (declaration.isTypeOnly()) continue;
2886
2915
  const reference = declaration.getModuleReference();
2887
2916
  if (!Node.isExternalModuleReference(reference)) continue;
@@ -2951,7 +2980,7 @@ const foldSource = (options) => {
2951
2980
  if (watched.size === 0) return;
2952
2981
  const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
2953
2982
  const survivors = [];
2954
- for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local) ?? []) {
2983
+ for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local)) {
2955
2984
  const start = identifier.getStart();
2956
2985
  if (identifier.getFirstAncestorByKind(SyntaxKind.ImportDeclaration)) continue;
2957
2986
  if (applied.some(([from, to]) => start >= from && start < to)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.43.1",
3
+ "version": "1.44.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.43.1",
44
- "@bamboocss/extractor": "1.43.1",
45
- "@bamboocss/core": "1.43.1",
46
- "@bamboocss/logger": "1.43.1",
47
- "@bamboocss/node": "1.43.1",
48
- "@bamboocss/shared": "1.43.1",
49
- "@bamboocss/types": "1.43.1"
43
+ "@bamboocss/core": "1.44.1",
44
+ "@bamboocss/config": "1.44.1",
45
+ "@bamboocss/extractor": "1.44.1",
46
+ "@bamboocss/logger": "1.44.1",
47
+ "@bamboocss/node": "1.44.1",
48
+ "@bamboocss/shared": "1.44.1",
49
+ "@bamboocss/types": "1.44.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.43.1"
54
+ "@bamboocss/fixture": "1.44.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"