@bamboocss/vite 1.55.0 → 1.55.2

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.
@@ -1115,6 +1115,7 @@ const FOLDABLE_TYPES = new Set([
1115
1115
  */
1116
1116
  const SURVIVES_TO_RUNTIME = new Set([
1117
1117
  "dynamic",
1118
+ "opaque-composition",
1118
1119
  "runtime-binding",
1119
1120
  "raw-call",
1120
1121
  "unsupported-kind",
@@ -2056,19 +2057,29 @@ const foldSource = (options) => {
2056
2057
  supported = false;
2057
2058
  break;
2058
2059
  }
2059
- if (dynamic.length > 1) {
2060
- skipped.push({
2061
- name: "cx",
2062
- reason: "dynamic",
2063
- start: call.getStart(),
2064
- end: call.getEnd()
2065
- });
2066
- continue;
2067
- }
2068
- if (!supported) {
2060
+ if (!supported || dynamic.length > 1) {
2061
+ /**
2062
+ * Does this argument carry declarations Bamboo compiled?
2063
+ *
2064
+ * Only the candidate half of `take`, because the other half is already known here:
2065
+ * `take` returned false, which happens precisely when an argument is opaque. What is
2066
+ * missing after that abort is whether a *later* argument was one of ours, which the
2067
+ * partially populated `matched`/`dynamic` lists cannot answer — the walk stops at the
2068
+ * first thing it cannot take, and everything after it is unvisited.
2069
+ *
2070
+ * Declared inside the branch so the common path — every `cx()` that compiles — does
2071
+ * not allocate a closure it never calls. The fold runs on every module of every dev
2072
+ * transform, so per-call-site work here is not free.
2073
+ */
2074
+ const composesStyleSet = (arg) => {
2075
+ const candidate = byRange.get(`${arg.getStart()}:${arg.getEnd()}`);
2076
+ if (candidate?.styleSet || candidate?.styleMap?.outputKind === "class" || candidate?.replacement) return true;
2077
+ return _bamboocss_ts_ast.Node.isArrayLiteralExpression(arg) && arg.elements.some(composesStyleSet);
2078
+ };
2079
+ const mixed = !supported && ((0, _bamboocss_ts_ast.childOf)(call, "arguments") ?? []).some(composesStyleSet);
2069
2080
  skipped.push({
2070
2081
  name: "cx",
2071
- reason: "dynamic",
2082
+ reason: mixed ? "opaque-composition" : "dynamic",
2072
2083
  start: call.getStart(),
2073
2084
  end: call.getEnd()
2074
2085
  });
@@ -1113,6 +1113,7 @@ const FOLDABLE_TYPES = new Set([
1113
1113
  */
1114
1114
  const SURVIVES_TO_RUNTIME = new Set([
1115
1115
  "dynamic",
1116
+ "opaque-composition",
1116
1117
  "runtime-binding",
1117
1118
  "raw-call",
1118
1119
  "unsupported-kind",
@@ -2054,19 +2055,29 @@ const foldSource = (options) => {
2054
2055
  supported = false;
2055
2056
  break;
2056
2057
  }
2057
- if (dynamic.length > 1) {
2058
- skipped.push({
2059
- name: "cx",
2060
- reason: "dynamic",
2061
- start: call.getStart(),
2062
- end: call.getEnd()
2063
- });
2064
- continue;
2065
- }
2066
- if (!supported) {
2058
+ if (!supported || dynamic.length > 1) {
2059
+ /**
2060
+ * Does this argument carry declarations Bamboo compiled?
2061
+ *
2062
+ * Only the candidate half of `take`, because the other half is already known here:
2063
+ * `take` returned false, which happens precisely when an argument is opaque. What is
2064
+ * missing after that abort is whether a *later* argument was one of ours, which the
2065
+ * partially populated `matched`/`dynamic` lists cannot answer — the walk stops at the
2066
+ * first thing it cannot take, and everything after it is unvisited.
2067
+ *
2068
+ * Declared inside the branch so the common path — every `cx()` that compiles — does
2069
+ * not allocate a closure it never calls. The fold runs on every module of every dev
2070
+ * transform, so per-call-site work here is not free.
2071
+ */
2072
+ const composesStyleSet = (arg) => {
2073
+ const candidate = byRange.get(`${arg.getStart()}:${arg.getEnd()}`);
2074
+ if (candidate?.styleSet || candidate?.styleMap?.outputKind === "class" || candidate?.replacement) return true;
2075
+ return Node.isArrayLiteralExpression(arg) && arg.elements.some(composesStyleSet);
2076
+ };
2077
+ const mixed = !supported && (childOf(call, "arguments") ?? []).some(composesStyleSet);
2067
2078
  skipped.push({
2068
2079
  name: "cx",
2069
- reason: "dynamic",
2080
+ reason: mixed ? "opaque-composition" : "dynamic",
2070
2081
  start: call.getStart(),
2071
2082
  end: call.getEnd()
2072
2083
  });
package/dist/index.cjs CHANGED
@@ -532,7 +532,7 @@ const bamboocssCss = (options) => {
532
532
  * for the client graph and once for SSR — and each load used to run a complete extraction
533
533
  * and optimization pass to produce byte-identical CSS. The sheet is a function of the source
534
534
  * files alone, and the watcher below is the single point every event that can reach it
535
- * passes through — Vite's own propagation only arrives via the watch edges `load` registers,
535
+ * passes through — Vite's own propagation only arrives via the watch edges `transform` registers,
536
536
  * over the same extracted files the watcher checks. A monotonic counter bumped there is
537
537
  * therefore enough to know whether a build already reflects the world a load is asking about.
538
538
  *
@@ -745,13 +745,10 @@ const bamboocssCss = (options) => {
745
745
  if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
746
746
  session.cssLoaded = true;
747
747
  const generationAtStart = changeGeneration;
748
- if (command === "serve" && servedCss?.generation === generationAtStart) {
749
- if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
750
- return servedCss.map ? {
751
- code: servedCss.css,
752
- map: servedCss.map
753
- } : servedCss.css;
754
- }
748
+ if (command === "serve" && servedCss?.generation === generationAtStart) return servedCss.map ? {
749
+ code: servedCss.css,
750
+ map: servedCss.map
751
+ } : servedCss.css;
755
752
  let css;
756
753
  let map;
757
754
  try {
@@ -770,18 +767,52 @@ const bamboocssCss = (options) => {
770
767
  css,
771
768
  map
772
769
  };
773
- if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
770
+ if (command === "build" && this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
774
771
  return map ? {
775
772
  code: css,
776
773
  map
777
774
  } : css;
778
775
  },
776
+ /**
777
+ * Register every extracted file against the stylesheet, in the graph that just asked for it.
778
+ *
779
+ * In dev this has to happen here rather than in `load`. Vite attaches what `load` registers to
780
+ * the module's node in the graph, and a request for a module the graph has no node for yet runs
781
+ * `load` first and creates the node afterwards, so everything `load` registered is dropped
782
+ * without a word. `vite:css-analysis` then records no importer edges, Vite's own propagation
783
+ * never reaches the sheet in that environment, and the watcher below does not force a reload
784
+ * either, because the edited file does have a module there. The component repaints with a class
785
+ * whose rule never arrives, and stays that way until a restart.
786
+ *
787
+ * A browser's import never meets it, since the importer's analysis creates the node before the
788
+ * sheet is requested. A `transformRequest` that reaches the sheet first does. TanStack Start
789
+ * sends one on every page load when `__root.tsx` imports the stylesheet: its dev-only SSR style
790
+ * collection transforms the sheet in the client environment on the server, before the browser's
791
+ * import has put a node there.
792
+ *
793
+ * By `transform` the node exists, and the context is the one `vite:css-analysis` reads after
794
+ * every plugin has run, so the edges are recorded however the request arrived. From the
795
+ * session's set rather than `extractedSourceFiles()`, which re-globs the include patterns per
796
+ * call; every pass assigns the set from that same expression.
797
+ *
798
+ * Filtered by id, so a bundler that honours hook filters never calls in for any other module.
799
+ * The exact check stays for one that does not.
800
+ */
801
+ transform: {
802
+ filter: { id: /virtual:bamboo\.css/ },
803
+ handler(_code, id) {
804
+ if (command !== "serve") return;
805
+ const query = queryOf(id);
806
+ if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return;
807
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
808
+ }
809
+ },
779
810
  configureServer(devServer) {
780
811
  server = devServer;
781
812
  /**
782
813
  * The graph the stylesheet's own module lives in, which is the one that has to reach it.
783
814
  *
784
- * `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
815
+ * `transform` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
785
816
  * turns those into real importer edges — the virtual module ends up a direct importer of
786
817
  * each file the extractor read. So an edit to any of them propagates to the stylesheet on
787
818
  * Vite's own pass, in whichever environment holds that edge.
@@ -1127,7 +1158,8 @@ const bamboocss = (options = {}) => {
1127
1158
  "overlapping",
1128
1159
  "unresolved-token",
1129
1160
  "runtime-binding",
1130
- "compile-failed"
1161
+ "compile-failed",
1162
+ "opaque-composition"
1131
1163
  ]);
1132
1164
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1133
1165
  const isNonNegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
@@ -2609,7 +2641,7 @@ const bamboocss = (options = {}) => {
2609
2641
  const survivorsHere = [];
2610
2642
  for (const entry of result.skipped) {
2611
2643
  if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
2612
- if (entry.name === "cx" && entry.reason === "dynamic") continue;
2644
+ if (entry.name === "cx" && (entry.reason === "dynamic" || entry.reason === "opaque-composition")) continue;
2613
2645
  survivorsHere.push({
2614
2646
  line: lineAt(code, entry.start),
2615
2647
  name: entry.name,
package/dist/index.mjs CHANGED
@@ -527,7 +527,7 @@ const bamboocssCss = (options) => {
527
527
  * for the client graph and once for SSR — and each load used to run a complete extraction
528
528
  * and optimization pass to produce byte-identical CSS. The sheet is a function of the source
529
529
  * files alone, and the watcher below is the single point every event that can reach it
530
- * passes through — Vite's own propagation only arrives via the watch edges `load` registers,
530
+ * passes through — Vite's own propagation only arrives via the watch edges `transform` registers,
531
531
  * over the same extracted files the watcher checks. A monotonic counter bumped there is
532
532
  * therefore enough to know whether a build already reflects the world a load is asking about.
533
533
  *
@@ -740,13 +740,10 @@ const bamboocssCss = (options) => {
740
740
  if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
741
741
  session.cssLoaded = true;
742
742
  const generationAtStart = changeGeneration;
743
- if (command === "serve" && servedCss?.generation === generationAtStart) {
744
- if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
745
- return servedCss.map ? {
746
- code: servedCss.css,
747
- map: servedCss.map
748
- } : servedCss.css;
749
- }
743
+ if (command === "serve" && servedCss?.generation === generationAtStart) return servedCss.map ? {
744
+ code: servedCss.css,
745
+ map: servedCss.map
746
+ } : servedCss.css;
750
747
  let css;
751
748
  let map;
752
749
  try {
@@ -765,18 +762,52 @@ const bamboocssCss = (options) => {
765
762
  css,
766
763
  map
767
764
  };
768
- if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
765
+ if (command === "build" && this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
769
766
  return map ? {
770
767
  code: css,
771
768
  map
772
769
  } : css;
773
770
  },
771
+ /**
772
+ * Register every extracted file against the stylesheet, in the graph that just asked for it.
773
+ *
774
+ * In dev this has to happen here rather than in `load`. Vite attaches what `load` registers to
775
+ * the module's node in the graph, and a request for a module the graph has no node for yet runs
776
+ * `load` first and creates the node afterwards, so everything `load` registered is dropped
777
+ * without a word. `vite:css-analysis` then records no importer edges, Vite's own propagation
778
+ * never reaches the sheet in that environment, and the watcher below does not force a reload
779
+ * either, because the edited file does have a module there. The component repaints with a class
780
+ * whose rule never arrives, and stays that way until a restart.
781
+ *
782
+ * A browser's import never meets it, since the importer's analysis creates the node before the
783
+ * sheet is requested. A `transformRequest` that reaches the sheet first does. TanStack Start
784
+ * sends one on every page load when `__root.tsx` imports the stylesheet: its dev-only SSR style
785
+ * collection transforms the sheet in the client environment on the server, before the browser's
786
+ * import has put a node there.
787
+ *
788
+ * By `transform` the node exists, and the context is the one `vite:css-analysis` reads after
789
+ * every plugin has run, so the edges are recorded however the request arrived. From the
790
+ * session's set rather than `extractedSourceFiles()`, which re-globs the include patterns per
791
+ * call; every pass assigns the set from that same expression.
792
+ *
793
+ * Filtered by id, so a bundler that honours hook filters never calls in for any other module.
794
+ * The exact check stays for one that does not.
795
+ */
796
+ transform: {
797
+ filter: { id: /virtual:bamboo\.css/ },
798
+ handler(_code, id) {
799
+ if (command !== "serve") return;
800
+ const query = queryOf(id);
801
+ if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return;
802
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
803
+ }
804
+ },
774
805
  configureServer(devServer) {
775
806
  server = devServer;
776
807
  /**
777
808
  * The graph the stylesheet's own module lives in, which is the one that has to reach it.
778
809
  *
779
- * `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
810
+ * `transform` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
780
811
  * turns those into real importer edges — the virtual module ends up a direct importer of
781
812
  * each file the extractor read. So an edit to any of them propagates to the stylesheet on
782
813
  * Vite's own pass, in whichever environment holds that edge.
@@ -1122,7 +1153,8 @@ const bamboocss = (options = {}) => {
1122
1153
  "overlapping",
1123
1154
  "unresolved-token",
1124
1155
  "runtime-binding",
1125
- "compile-failed"
1156
+ "compile-failed",
1157
+ "opaque-composition"
1126
1158
  ]);
1127
1159
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1128
1160
  const isNonNegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
@@ -2604,7 +2636,7 @@ const bamboocss = (options = {}) => {
2604
2636
  const survivorsHere = [];
2605
2637
  for (const entry of result.skipped) {
2606
2638
  if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
2607
- if (entry.name === "cx" && entry.reason === "dynamic") continue;
2639
+ if (entry.name === "cx" && (entry.reason === "dynamic" || entry.reason === "opaque-composition")) continue;
2608
2640
  survivorsHere.push({
2609
2641
  line: lineAt(code, entry.start),
2610
2642
  name: entry.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.55.0",
3
+ "version": "1.55.2",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -42,20 +42,20 @@
42
42
  "magic-string": "0.30.21",
43
43
  "postcss": "8.5.26",
44
44
  "postcss-selector-parser": "7.1.5",
45
- "@bamboocss/config": "1.55.0",
46
- "@bamboocss/extractor": "1.55.0",
47
- "@bamboocss/core": "1.55.0",
48
- "@bamboocss/node": "1.55.0",
49
- "@bamboocss/logger": "1.55.0",
50
- "@bamboocss/types": "1.55.0",
51
- "@bamboocss/shared": "1.55.0",
52
- "@bamboocss/ts-ast": "1.55.0"
45
+ "@bamboocss/config": "1.55.2",
46
+ "@bamboocss/core": "1.55.2",
47
+ "@bamboocss/extractor": "1.55.2",
48
+ "@bamboocss/logger": "1.55.2",
49
+ "@bamboocss/node": "1.55.2",
50
+ "@bamboocss/shared": "1.55.2",
51
+ "@bamboocss/ts-ast": "1.55.2",
52
+ "@bamboocss/types": "1.55.2"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@jridgewell/trace-mapping": "^0.3.31",
56
56
  "@typescript/api": "npm:typescript@7.1.0-dev.20260826.1",
57
57
  "vite": "7.2.6",
58
- "@bamboocss/fixture": "1.55.0"
58
+ "@bamboocss/fixture": "1.55.2"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "vite": ">=5"