@bamboocss/vite 1.21.0 → 1.22.0

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/client.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ambient declaration for the virtual stylesheet, so `import 'virtual:bamboo.css'`
3
+ * typechecks.
4
+ *
5
+ * Reference it once, next to the `vite/client` reference a vite project already has:
6
+ *
7
+ * /// <reference types="@bamboocss/vite/client" />
8
+ *
9
+ * Shipped as a separate entry rather than folded into the package's own types, because a
10
+ * `declare module` in the main entry would apply to every consumer of the exported API —
11
+ * including builds that do not use the plugin.
12
+ */
13
+ declare module 'virtual:bamboo.css' {}
package/dist/index.cjs CHANGED
@@ -24,6 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
+ let _bamboocss_node = require("@bamboocss/node");
28
+ let _bamboocss_logger = require("@bamboocss/logger");
27
29
  let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
28
30
  let _bamboocss_extractor = require("@bamboocss/extractor");
29
31
  let magic_string = require("magic-string");
@@ -32,8 +34,90 @@ let ts_morph = require("ts-morph");
32
34
  let _bamboocss_core = require("@bamboocss/core");
33
35
  let _bamboocss_shared = require("@bamboocss/shared");
34
36
  let node_path = require("node:path");
35
- let _bamboocss_logger = require("@bamboocss/logger");
36
- let _bamboocss_node = require("@bamboocss/node");
37
+ //#region src/css.ts
38
+ /**
39
+ * What a project imports to get the stylesheet.
40
+ *
41
+ * Spelled with a `.css` extension because that is how vite decides what a module is: the
42
+ * id is all it has for a module with no file behind it, so `virtual:bamboo` would be
43
+ * bundled as javascript and injected as a script.
44
+ */
45
+ const VIRTUAL_CSS_ID = "virtual:bamboo.css";
46
+ /**
47
+ * Rollup's convention for a module with no file: a leading NUL tells every other plugin
48
+ * not to try reading it off disk.
49
+ */
50
+ const RESOLVED_ID = `\0${VIRTUAL_CSS_ID}`;
51
+ /**
52
+ * Serve bamboo's stylesheet as a virtual module, in dev and in build.
53
+ *
54
+ * This is the integration itself, not an optimisation: without it nothing emits css and
55
+ * the generated `styled-system` runtime names classes no rule exists for.
56
+ *
57
+ * A virtual module rather than a file written to disk, because vite already owns the two
58
+ * things a file would have to reimplement. In dev it injects css over the websocket and
59
+ * replaces it in place, so an edit repaints without reloading; in build it hashes the
60
+ * content into the asset graph and lets the bundler decide where it lands. Writing
61
+ * `styles.css` and asking the project to import it means the build reads a file the same
62
+ * process just wrote, which is a race on any watch rebuild.
63
+ */
64
+ const bamboocssCss = (options = {}) => {
65
+ const { configPath, cwd } = options;
66
+ const builder = new _bamboocss_node.Builder();
67
+ let server;
68
+ /**
69
+ * Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
70
+ * context. Two overlapping passes would extract into the same encoder and emit the
71
+ * stylesheet twice over.
72
+ */
73
+ let pending;
74
+ const build = async () => {
75
+ await builder.setup({
76
+ configPath,
77
+ cwd
78
+ });
79
+ await builder.emit();
80
+ builder.extract();
81
+ return builder.toCss({ layerParams: true });
82
+ };
83
+ const generate = () => {
84
+ pending = Promise.resolve(pending).catch(() => void 0).then(build);
85
+ return pending;
86
+ };
87
+ return {
88
+ name: "bamboocss:css",
89
+ resolveId(id) {
90
+ if (id === "virtual:bamboo.css") return RESOLVED_ID;
91
+ return null;
92
+ },
93
+ async load(id) {
94
+ if (id !== RESOLVED_ID) return null;
95
+ const css = await generate();
96
+ if (this.addWatchFile) for (const file of builder.context?.getFiles() ?? []) this.addWatchFile(builder.context.runtime.path.abs(builder.context.config.cwd, file));
97
+ return css;
98
+ },
99
+ configureServer(devServer) {
100
+ server = devServer;
101
+ const invalidate = (file) => {
102
+ const ctx = builder.context;
103
+ if (!ctx) return;
104
+ if (!ctx.getFiles().some((f) => ctx.runtime.path.abs(ctx.config.cwd, f) === file)) return;
105
+ const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
106
+ if (!mod) return;
107
+ server?.moduleGraph.invalidateModule(mod);
108
+ server?.ws.send({
109
+ type: "update",
110
+ updates: []
111
+ });
112
+ _bamboocss_logger.logger.debug("vite", `styles invalidated by ${file}`);
113
+ };
114
+ devServer.watcher.on("change", invalidate);
115
+ devServer.watcher.on("add", invalidate);
116
+ devServer.watcher.on("unlink", invalidate);
117
+ }
118
+ };
119
+ };
120
+ //#endregion
37
121
  //#region src/fold-partial.ts
38
122
  /**
39
123
  * Statically resolvable means: every box in the tree carries a known value.
@@ -480,9 +564,6 @@ const planPartialFold = (argument, boxNode, styles, deps) => {
480
564
  if (!partition) return void 0;
481
565
  const className = deps.runtimeCss(partition.staticStyles);
482
566
  if (!className && !partition.finite.length) return void 0;
483
- if (deps.ctx.config.cssMode === "grouped") {
484
- if ((className ? 1 : 0) + partition.finite.length + (partition.dynamicText.length ? 1 : 0) > 1) return void 0;
485
- }
486
567
  return {
487
568
  className,
488
569
  dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
@@ -676,7 +757,6 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
676
757
  //#region src/runtime-css.ts
677
758
  /** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
678
759
  const createCssContext = (ctx) => ({
679
- grouped: ctx.config.cssMode === "grouped",
680
760
  hash: Boolean(ctx.hash.className),
681
761
  conditions: {
682
762
  shift: ctx.conditions.shift,
@@ -1502,6 +1582,23 @@ const isGeneratedOutput = (filePath, ctx) => {
1502
1582
  const file = slashed(filePath);
1503
1583
  return file === root || file.startsWith(`${root}/`);
1504
1584
  };
1585
+ /**
1586
+ * The skip reasons that leave a `css()`-family call in the output.
1587
+ *
1588
+ * `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
1589
+ * function of the same name — neither leaves a call of ours. `not-foldable` is a `cva`/`sva`
1590
+ * definition, which keeps the recipe runtime rather than the css engine; see `strict`.
1591
+ */
1592
+ const SURVIVES_TO_RUNTIME = new Set([
1593
+ "dynamic",
1594
+ "raw-call",
1595
+ "unsupported-kind",
1596
+ "no-call-expression",
1597
+ "empty",
1598
+ "unresolved-token"
1599
+ ]);
1600
+ /** 1-indexed line of a source offset, for an error a user can navigate to. */
1601
+ const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
1505
1602
  const formatSkipped = (id, skipped) => {
1506
1603
  const counts = /* @__PURE__ */ new Map();
1507
1604
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -1510,16 +1607,17 @@ const formatSkipped = (id, skipped) => {
1510
1607
  /**
1511
1608
  * Vite integration for Bamboo CSS.
1512
1609
  *
1513
- * This plugin does not emit CSS keep your existing PostCSS setup for that. Its only
1514
- * job is the optional build-time fold.
1610
+ * Two plugins, because they do unrelated jobs on different schedules. The first emits the
1611
+ * stylesheet as a virtual module and runs in dev and build alike — that is the integration,
1612
+ * and nothing styles without it. The second is the optional build-time fold.
1515
1613
  *
1516
- * It runs with `enforce: 'pre'` so it sees module source as close as possible to what
1614
+ * The fold runs with `enforce: 'pre'` so it sees module source as close as possible to what
1517
1615
  * the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
1518
1616
  * sees them would otherwise make the two disagree, and a folded class could end up
1519
1617
  * with no matching rule.
1520
1618
  */
1521
1619
  const bamboocss = (options = {}) => {
1522
- const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
1620
+ const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true, strict = false } = options;
1523
1621
  /** Totals across the build, for the summary. */
1524
1622
  const totals = {
1525
1623
  folded: 0,
@@ -1527,6 +1625,8 @@ const bamboocss = (options = {}) => {
1527
1625
  filesWithFolds: 0,
1528
1626
  skipped: /* @__PURE__ */ new Map()
1529
1627
  };
1628
+ /** Under `strict`, every call that would still reach the runtime. */
1629
+ const survivors = [];
1530
1630
  let ctx;
1531
1631
  let runtimeCss;
1532
1632
  let setup;
@@ -1540,8 +1640,11 @@ const bamboocss = (options = {}) => {
1540
1640
  });
1541
1641
  await setup;
1542
1642
  };
1543
- return {
1544
- name: "bamboocss",
1643
+ return [bamboocssCss({
1644
+ configPath,
1645
+ cwd
1646
+ }), {
1647
+ name: "bamboocss:fold",
1545
1648
  enforce: "pre",
1546
1649
  apply: "build",
1547
1650
  async buildStart() {
@@ -1550,6 +1653,7 @@ const bamboocss = (options = {}) => {
1550
1653
  totals.files = 0;
1551
1654
  totals.filesWithFolds = 0;
1552
1655
  totals.skipped.clear();
1656
+ survivors.length = 0;
1553
1657
  await ensureContext();
1554
1658
  },
1555
1659
  /**
@@ -1611,6 +1715,23 @@ const bamboocss = (options = {}) => {
1611
1715
  totals.folded += result.folded.length;
1612
1716
  if (result.folded.length) totals.filesWithFolds++;
1613
1717
  for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
1718
+ if (strict) {
1719
+ for (const entry of result.skipped) {
1720
+ if (!SURVIVES_TO_RUNTIME.has(entry.reason)) continue;
1721
+ survivors.push({
1722
+ file: filePath,
1723
+ line: lineAt(code, entry.start),
1724
+ name: entry.name,
1725
+ reason: entry.reason
1726
+ });
1727
+ }
1728
+ if (result.code.includes("cssLeaf(")) survivors.push({
1729
+ file: filePath,
1730
+ line: lineAt(result.code, result.code.indexOf("cssLeaf(")),
1731
+ name: "cssLeaf",
1732
+ reason: "lowered-leaf"
1733
+ });
1734
+ }
1614
1735
  if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
1615
1736
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
1616
1737
  if (!result.folded.length) return null;
@@ -1621,6 +1742,16 @@ const bamboocss = (options = {}) => {
1621
1742
  };
1622
1743
  },
1623
1744
  buildEnd() {
1745
+ if (strict && survivors.length) {
1746
+ const byFile = /* @__PURE__ */ new Map();
1747
+ for (const entry of survivors) {
1748
+ const list = byFile.get(entry.file) ?? [];
1749
+ list.push(entry);
1750
+ byFile.set(entry.file, list);
1751
+ }
1752
+ const detail = Array.from(byFile.entries()).map(([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${e.name}() — ${e.reason}`)].join("\n")).join("\n");
1753
+ throw new Error(`bamboocss: ${survivors.length} call(s) could not be folded, and \`strict\` is on.\n\n${detail}\n\nEach one keeps \`styled-system/css\` in the bundle, so the engine cannot be dropped however many other calls folded. Make the values static, move the variation into a \`cva\` variant, or generate them with \`staticCss\` — or set \`strict: false\` to accept the runtime.`);
1754
+ }
1624
1755
  if (!transform || !reportSummary) return;
1625
1756
  const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
1626
1757
  const total = totals.folded + declined;
@@ -1629,10 +1760,12 @@ const bamboocss = (options = {}) => {
1629
1760
  const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
1630
1761
  _bamboocss_logger.logger.info("vite:transform", `Folded ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
1631
1762
  }
1632
- };
1763
+ }];
1633
1764
  };
1634
1765
  //#endregion
1766
+ exports.VIRTUAL_CSS_ID = VIRTUAL_CSS_ID;
1635
1767
  exports.bamboocss = bamboocss;
1768
+ exports.bamboocssCss = bamboocssCss;
1636
1769
  exports.createRuntimeCss = createRuntimeCss;
1637
1770
  exports.default = bamboocss;
1638
1771
  exports.foldSource = foldSource;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,36 @@
1
+ import { Plugin } from "vite";
1
2
  import { Context } from "@bamboocss/core";
2
3
  import { Dict, ParserResultInterface } from "@bamboocss/types";
3
4
  import MagicString from "magic-string";
4
- import { Plugin } from "vite";
5
5
 
6
+ //#region src/css.d.ts
7
+ /**
8
+ * What a project imports to get the stylesheet.
9
+ *
10
+ * Spelled with a `.css` extension because that is how vite decides what a module is: the
11
+ * id is all it has for a module with no file behind it, so `virtual:bamboo` would be
12
+ * bundled as javascript and injected as a script.
13
+ */
14
+ declare const VIRTUAL_CSS_ID = "virtual:bamboo.css";
15
+ interface BambooCssPluginOptions {
16
+ configPath?: string;
17
+ cwd?: string;
18
+ }
19
+ /**
20
+ * Serve bamboo's stylesheet as a virtual module, in dev and in build.
21
+ *
22
+ * This is the integration itself, not an optimisation: without it nothing emits css and
23
+ * the generated `styled-system` runtime names classes no rule exists for.
24
+ *
25
+ * A virtual module rather than a file written to disk, because vite already owns the two
26
+ * things a file would have to reimplement. In dev it injects css over the websocket and
27
+ * replaces it in place, so an edit repaints without reloading; in build it hashes the
28
+ * content into the asset graph and lets the bundler decide where it lands. Writing
29
+ * `styles.css` and asking the project to import it means the build reads a file the same
30
+ * process just wrote, which is a race on any watch rebuild.
31
+ */
32
+ declare const bamboocssCss: (options?: BambooCssPluginOptions) => Plugin;
33
+ //#endregion
6
34
  //#region src/runtime-css.d.ts
7
35
  /**
8
36
  * The generated runtime's `css`, rebuilt in-process from a resolved context.
@@ -144,18 +172,35 @@ interface BambooVitePluginOptions {
144
172
  * @default true
145
173
  */
146
174
  reportSummary?: boolean;
175
+ /**
176
+ * Fail the build when a `css()` or pattern call is left for the runtime.
177
+ *
178
+ * The fold's value is not the per-call CPU it saves — it is that a bundle where *every*
179
+ * such call folded no longer imports `styled-system/css` at all, and the engine behind it
180
+ * drops out. One survivor keeps the whole thing, so a coverage percentage cannot tell you
181
+ * whether you got the prize. This can.
182
+ *
183
+ * Deliberately silent about `cva`/`sva`. A `cva(...)` definition returns a function and can
184
+ * never collapse to a class string, so failing on it would make this unusable for anyone
185
+ * writing recipes — and recipes keep their own much smaller runtime by design. What this
186
+ * guarantees is narrower and checkable: nothing still calls `css()`.
187
+ *
188
+ * @default false
189
+ */
190
+ strict?: boolean;
147
191
  }
148
192
  /**
149
193
  * Vite integration for Bamboo CSS.
150
194
  *
151
- * This plugin does not emit CSS keep your existing PostCSS setup for that. Its only
152
- * job is the optional build-time fold.
195
+ * Two plugins, because they do unrelated jobs on different schedules. The first emits the
196
+ * stylesheet as a virtual module and runs in dev and build alike — that is the integration,
197
+ * and nothing styles without it. The second is the optional build-time fold.
153
198
  *
154
- * It runs with `enforce: 'pre'` so it sees module source as close as possible to what
199
+ * The fold runs with `enforce: 'pre'` so it sees module source as close as possible to what
155
200
  * the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
156
201
  * sees them would otherwise make the two disagree, and a folded class could end up
157
202
  * with no matching rule.
158
203
  */
159
- declare const bamboocss: (options?: BambooVitePluginOptions) => Plugin;
204
+ declare const bamboocss: (options?: BambooVitePluginOptions) => Plugin[];
160
205
  //#endregion
161
- export { type BambooVitePluginOptions, type FoldOptions, type FoldResult, type FoldedCall, type RuntimeCss, type SkipReason, type SkippedCall, bamboocss, bamboocss as default, createRuntimeCss, foldSource };
206
+ export { type BambooCssPluginOptions, type BambooVitePluginOptions, type FoldOptions, type FoldResult, type FoldedCall, type RuntimeCss, type SkipReason, type SkippedCall, VIRTUAL_CSS_ID, bamboocss, bamboocss as default, bamboocssCss, createRuntimeCss, foldSource };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,36 @@
1
1
  import MagicString from "magic-string";
2
2
  import { Context } from "@bamboocss/core";
3
- import { Dict, ParserResultInterface } from "@bamboocss/types";
4
3
  import { Plugin } from "vite";
4
+ import { Dict, ParserResultInterface } from "@bamboocss/types";
5
5
 
6
+ //#region src/css.d.ts
7
+ /**
8
+ * What a project imports to get the stylesheet.
9
+ *
10
+ * Spelled with a `.css` extension because that is how vite decides what a module is: the
11
+ * id is all it has for a module with no file behind it, so `virtual:bamboo` would be
12
+ * bundled as javascript and injected as a script.
13
+ */
14
+ declare const VIRTUAL_CSS_ID = "virtual:bamboo.css";
15
+ interface BambooCssPluginOptions {
16
+ configPath?: string;
17
+ cwd?: string;
18
+ }
19
+ /**
20
+ * Serve bamboo's stylesheet as a virtual module, in dev and in build.
21
+ *
22
+ * This is the integration itself, not an optimisation: without it nothing emits css and
23
+ * the generated `styled-system` runtime names classes no rule exists for.
24
+ *
25
+ * A virtual module rather than a file written to disk, because vite already owns the two
26
+ * things a file would have to reimplement. In dev it injects css over the websocket and
27
+ * replaces it in place, so an edit repaints without reloading; in build it hashes the
28
+ * content into the asset graph and lets the bundler decide where it lands. Writing
29
+ * `styles.css` and asking the project to import it means the build reads a file the same
30
+ * process just wrote, which is a race on any watch rebuild.
31
+ */
32
+ declare const bamboocssCss: (options?: BambooCssPluginOptions) => Plugin;
33
+ //#endregion
6
34
  //#region src/runtime-css.d.ts
7
35
  /**
8
36
  * The generated runtime's `css`, rebuilt in-process from a resolved context.
@@ -144,18 +172,35 @@ interface BambooVitePluginOptions {
144
172
  * @default true
145
173
  */
146
174
  reportSummary?: boolean;
175
+ /**
176
+ * Fail the build when a `css()` or pattern call is left for the runtime.
177
+ *
178
+ * The fold's value is not the per-call CPU it saves — it is that a bundle where *every*
179
+ * such call folded no longer imports `styled-system/css` at all, and the engine behind it
180
+ * drops out. One survivor keeps the whole thing, so a coverage percentage cannot tell you
181
+ * whether you got the prize. This can.
182
+ *
183
+ * Deliberately silent about `cva`/`sva`. A `cva(...)` definition returns a function and can
184
+ * never collapse to a class string, so failing on it would make this unusable for anyone
185
+ * writing recipes — and recipes keep their own much smaller runtime by design. What this
186
+ * guarantees is narrower and checkable: nothing still calls `css()`.
187
+ *
188
+ * @default false
189
+ */
190
+ strict?: boolean;
147
191
  }
148
192
  /**
149
193
  * Vite integration for Bamboo CSS.
150
194
  *
151
- * This plugin does not emit CSS keep your existing PostCSS setup for that. Its only
152
- * job is the optional build-time fold.
195
+ * Two plugins, because they do unrelated jobs on different schedules. The first emits the
196
+ * stylesheet as a virtual module and runs in dev and build alike — that is the integration,
197
+ * and nothing styles without it. The second is the optional build-time fold.
153
198
  *
154
- * It runs with `enforce: 'pre'` so it sees module source as close as possible to what
199
+ * The fold runs with `enforce: 'pre'` so it sees module source as close as possible to what
155
200
  * the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
156
201
  * sees them would otherwise make the two disagree, and a folded class could end up
157
202
  * with no matching rule.
158
203
  */
159
- declare const bamboocss: (options?: BambooVitePluginOptions) => Plugin;
204
+ declare const bamboocss: (options?: BambooVitePluginOptions) => Plugin[];
160
205
  //#endregion
161
- export { type BambooVitePluginOptions, type FoldOptions, type FoldResult, type FoldedCall, type RuntimeCss, type SkipReason, type SkippedCall, bamboocss, bamboocss as default, createRuntimeCss, foldSource };
206
+ export { type BambooCssPluginOptions, type BambooVitePluginOptions, type FoldOptions, type FoldResult, type FoldedCall, type RuntimeCss, type SkipReason, type SkippedCall, VIRTUAL_CSS_ID, bamboocss, bamboocss as default, bamboocssCss, createRuntimeCss, foldSource };
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { Builder, loadConfigAndCreateContext } from "@bamboocss/node";
2
+ import { logger } from "@bamboocss/logger";
1
3
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
2
4
  import { box, maybeBoxNode, unbox } from "@bamboocss/extractor";
3
5
  import MagicString from "magic-string";
@@ -5,8 +7,90 @@ import { Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
5
7
  import { Recipes } from "@bamboocss/core";
6
8
  import { compact, createCssUncached, createMergeCss, getSlotCompoundVariant, memo, withoutSpace } from "@bamboocss/shared";
7
9
  import { resolve } from "node:path";
8
- import { logger } from "@bamboocss/logger";
9
- import { loadConfigAndCreateContext } from "@bamboocss/node";
10
+ //#region src/css.ts
11
+ /**
12
+ * What a project imports to get the stylesheet.
13
+ *
14
+ * Spelled with a `.css` extension because that is how vite decides what a module is: the
15
+ * id is all it has for a module with no file behind it, so `virtual:bamboo` would be
16
+ * bundled as javascript and injected as a script.
17
+ */
18
+ const VIRTUAL_CSS_ID = "virtual:bamboo.css";
19
+ /**
20
+ * Rollup's convention for a module with no file: a leading NUL tells every other plugin
21
+ * not to try reading it off disk.
22
+ */
23
+ const RESOLVED_ID = `\0${VIRTUAL_CSS_ID}`;
24
+ /**
25
+ * Serve bamboo's stylesheet as a virtual module, in dev and in build.
26
+ *
27
+ * This is the integration itself, not an optimisation: without it nothing emits css and
28
+ * the generated `styled-system` runtime names classes no rule exists for.
29
+ *
30
+ * A virtual module rather than a file written to disk, because vite already owns the two
31
+ * things a file would have to reimplement. In dev it injects css over the websocket and
32
+ * replaces it in place, so an edit repaints without reloading; in build it hashes the
33
+ * content into the asset graph and lets the bundler decide where it lands. Writing
34
+ * `styles.css` and asking the project to import it means the build reads a file the same
35
+ * process just wrote, which is a race on any watch rebuild.
36
+ */
37
+ const bamboocssCss = (options = {}) => {
38
+ const { configPath, cwd } = options;
39
+ const builder = new Builder();
40
+ let server;
41
+ /**
42
+ * Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
43
+ * context. Two overlapping passes would extract into the same encoder and emit the
44
+ * stylesheet twice over.
45
+ */
46
+ let pending;
47
+ const build = async () => {
48
+ await builder.setup({
49
+ configPath,
50
+ cwd
51
+ });
52
+ await builder.emit();
53
+ builder.extract();
54
+ return builder.toCss({ layerParams: true });
55
+ };
56
+ const generate = () => {
57
+ pending = Promise.resolve(pending).catch(() => void 0).then(build);
58
+ return pending;
59
+ };
60
+ return {
61
+ name: "bamboocss:css",
62
+ resolveId(id) {
63
+ if (id === "virtual:bamboo.css") return RESOLVED_ID;
64
+ return null;
65
+ },
66
+ async load(id) {
67
+ if (id !== RESOLVED_ID) return null;
68
+ const css = await generate();
69
+ if (this.addWatchFile) for (const file of builder.context?.getFiles() ?? []) this.addWatchFile(builder.context.runtime.path.abs(builder.context.config.cwd, file));
70
+ return css;
71
+ },
72
+ configureServer(devServer) {
73
+ server = devServer;
74
+ const invalidate = (file) => {
75
+ const ctx = builder.context;
76
+ if (!ctx) return;
77
+ if (!ctx.getFiles().some((f) => ctx.runtime.path.abs(ctx.config.cwd, f) === file)) return;
78
+ const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
79
+ if (!mod) return;
80
+ server?.moduleGraph.invalidateModule(mod);
81
+ server?.ws.send({
82
+ type: "update",
83
+ updates: []
84
+ });
85
+ logger.debug("vite", `styles invalidated by ${file}`);
86
+ };
87
+ devServer.watcher.on("change", invalidate);
88
+ devServer.watcher.on("add", invalidate);
89
+ devServer.watcher.on("unlink", invalidate);
90
+ }
91
+ };
92
+ };
93
+ //#endregion
10
94
  //#region src/fold-partial.ts
11
95
  /**
12
96
  * Statically resolvable means: every box in the tree carries a known value.
@@ -453,9 +537,6 @@ const planPartialFold = (argument, boxNode, styles, deps) => {
453
537
  if (!partition) return void 0;
454
538
  const className = deps.runtimeCss(partition.staticStyles);
455
539
  if (!className && !partition.finite.length) return void 0;
456
- if (deps.ctx.config.cssMode === "grouped") {
457
- if ((className ? 1 : 0) + partition.finite.length + (partition.dynamicText.length ? 1 : 0) > 1) return void 0;
458
- }
459
540
  return {
460
541
  className,
461
542
  dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
@@ -649,7 +730,6 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
649
730
  //#region src/runtime-css.ts
650
731
  /** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
651
732
  const createCssContext = (ctx) => ({
652
- grouped: ctx.config.cssMode === "grouped",
653
733
  hash: Boolean(ctx.hash.className),
654
734
  conditions: {
655
735
  shift: ctx.conditions.shift,
@@ -1475,6 +1555,23 @@ const isGeneratedOutput = (filePath, ctx) => {
1475
1555
  const file = slashed(filePath);
1476
1556
  return file === root || file.startsWith(`${root}/`);
1477
1557
  };
1558
+ /**
1559
+ * The skip reasons that leave a `css()`-family call in the output.
1560
+ *
1561
+ * `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
1562
+ * function of the same name — neither leaves a call of ours. `not-foldable` is a `cva`/`sva`
1563
+ * definition, which keeps the recipe runtime rather than the css engine; see `strict`.
1564
+ */
1565
+ const SURVIVES_TO_RUNTIME = new Set([
1566
+ "dynamic",
1567
+ "raw-call",
1568
+ "unsupported-kind",
1569
+ "no-call-expression",
1570
+ "empty",
1571
+ "unresolved-token"
1572
+ ]);
1573
+ /** 1-indexed line of a source offset, for an error a user can navigate to. */
1574
+ const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
1478
1575
  const formatSkipped = (id, skipped) => {
1479
1576
  const counts = /* @__PURE__ */ new Map();
1480
1577
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -1483,16 +1580,17 @@ const formatSkipped = (id, skipped) => {
1483
1580
  /**
1484
1581
  * Vite integration for Bamboo CSS.
1485
1582
  *
1486
- * This plugin does not emit CSS keep your existing PostCSS setup for that. Its only
1487
- * job is the optional build-time fold.
1583
+ * Two plugins, because they do unrelated jobs on different schedules. The first emits the
1584
+ * stylesheet as a virtual module and runs in dev and build alike — that is the integration,
1585
+ * and nothing styles without it. The second is the optional build-time fold.
1488
1586
  *
1489
- * It runs with `enforce: 'pre'` so it sees module source as close as possible to what
1587
+ * The fold runs with `enforce: 'pre'` so it sees module source as close as possible to what
1490
1588
  * the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
1491
1589
  * sees them would otherwise make the two disagree, and a folded class could end up
1492
1590
  * with no matching rule.
1493
1591
  */
1494
1592
  const bamboocss = (options = {}) => {
1495
- const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
1593
+ const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true, strict = false } = options;
1496
1594
  /** Totals across the build, for the summary. */
1497
1595
  const totals = {
1498
1596
  folded: 0,
@@ -1500,6 +1598,8 @@ const bamboocss = (options = {}) => {
1500
1598
  filesWithFolds: 0,
1501
1599
  skipped: /* @__PURE__ */ new Map()
1502
1600
  };
1601
+ /** Under `strict`, every call that would still reach the runtime. */
1602
+ const survivors = [];
1503
1603
  let ctx;
1504
1604
  let runtimeCss;
1505
1605
  let setup;
@@ -1513,8 +1613,11 @@ const bamboocss = (options = {}) => {
1513
1613
  });
1514
1614
  await setup;
1515
1615
  };
1516
- return {
1517
- name: "bamboocss",
1616
+ return [bamboocssCss({
1617
+ configPath,
1618
+ cwd
1619
+ }), {
1620
+ name: "bamboocss:fold",
1518
1621
  enforce: "pre",
1519
1622
  apply: "build",
1520
1623
  async buildStart() {
@@ -1523,6 +1626,7 @@ const bamboocss = (options = {}) => {
1523
1626
  totals.files = 0;
1524
1627
  totals.filesWithFolds = 0;
1525
1628
  totals.skipped.clear();
1629
+ survivors.length = 0;
1526
1630
  await ensureContext();
1527
1631
  },
1528
1632
  /**
@@ -1584,6 +1688,23 @@ const bamboocss = (options = {}) => {
1584
1688
  totals.folded += result.folded.length;
1585
1689
  if (result.folded.length) totals.filesWithFolds++;
1586
1690
  for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
1691
+ if (strict) {
1692
+ for (const entry of result.skipped) {
1693
+ if (!SURVIVES_TO_RUNTIME.has(entry.reason)) continue;
1694
+ survivors.push({
1695
+ file: filePath,
1696
+ line: lineAt(code, entry.start),
1697
+ name: entry.name,
1698
+ reason: entry.reason
1699
+ });
1700
+ }
1701
+ if (result.code.includes("cssLeaf(")) survivors.push({
1702
+ file: filePath,
1703
+ line: lineAt(result.code, result.code.indexOf("cssLeaf(")),
1704
+ name: "cssLeaf",
1705
+ reason: "lowered-leaf"
1706
+ });
1707
+ }
1587
1708
  if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
1588
1709
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
1589
1710
  if (!result.folded.length) return null;
@@ -1594,6 +1715,16 @@ const bamboocss = (options = {}) => {
1594
1715
  };
1595
1716
  },
1596
1717
  buildEnd() {
1718
+ if (strict && survivors.length) {
1719
+ const byFile = /* @__PURE__ */ new Map();
1720
+ for (const entry of survivors) {
1721
+ const list = byFile.get(entry.file) ?? [];
1722
+ list.push(entry);
1723
+ byFile.set(entry.file, list);
1724
+ }
1725
+ const detail = Array.from(byFile.entries()).map(([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${e.name}() — ${e.reason}`)].join("\n")).join("\n");
1726
+ throw new Error(`bamboocss: ${survivors.length} call(s) could not be folded, and \`strict\` is on.\n\n${detail}\n\nEach one keeps \`styled-system/css\` in the bundle, so the engine cannot be dropped however many other calls folded. Make the values static, move the variation into a \`cva\` variant, or generate them with \`staticCss\` — or set \`strict: false\` to accept the runtime.`);
1727
+ }
1597
1728
  if (!transform || !reportSummary) return;
1598
1729
  const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
1599
1730
  const total = totals.folded + declined;
@@ -1602,7 +1733,7 @@ const bamboocss = (options = {}) => {
1602
1733
  const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
1603
1734
  logger.info("vite:transform", `Folded ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
1604
1735
  }
1605
- };
1736
+ }];
1606
1737
  };
1607
1738
  //#endregion
1608
- export { bamboocss, bamboocss as default, createRuntimeCss, foldSource };
1739
+ export { VIRTUAL_CSS_ID, bamboocss, bamboocss as default, bamboocssCss, createRuntimeCss, foldSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@
11
11
  "directory": "packages/vite"
12
12
  },
13
13
  "files": [
14
+ "client.d.ts",
14
15
  "dist"
15
16
  ],
16
17
  "sideEffects": false,
@@ -27,7 +28,8 @@
27
28
  "default": "./dist/index.mjs"
28
29
  }
29
30
  },
30
- "./package.json": "./package.json"
31
+ "./package.json": "./package.json",
32
+ "./client": "./client.d.ts"
31
33
  },
32
34
  "publishConfig": {
33
35
  "access": "public"
@@ -35,18 +37,18 @@
35
37
  "dependencies": {
36
38
  "magic-string": "0.30.21",
37
39
  "ts-morph": "28.0.0",
38
- "@bamboocss/config": "1.21.0",
39
- "@bamboocss/core": "1.21.0",
40
- "@bamboocss/extractor": "1.21.0",
41
- "@bamboocss/logger": "1.21.0",
42
- "@bamboocss/node": "1.21.0",
43
- "@bamboocss/shared": "1.21.0",
44
- "@bamboocss/types": "1.21.0"
40
+ "@bamboocss/config": "1.22.0",
41
+ "@bamboocss/core": "1.22.0",
42
+ "@bamboocss/logger": "1.22.0",
43
+ "@bamboocss/extractor": "1.22.0",
44
+ "@bamboocss/node": "1.22.0",
45
+ "@bamboocss/shared": "1.22.0",
46
+ "@bamboocss/types": "1.22.0"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@jridgewell/trace-mapping": "^0.3.31",
48
50
  "vite": "7.2.6",
49
- "@bamboocss/fixture": "1.21.0"
51
+ "@bamboocss/fixture": "1.22.0"
50
52
  },
51
53
  "peerDependencies": {
52
54
  "vite": ">=5"