@bamboocss/vite 1.21.0 → 1.23.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 +13 -0
- package/dist/index.cjs +530 -28
- package/dist/index.d.cts +52 -7
- package/dist/index.d.mts +52 -7
- package/dist/index.mjs +531 -31
- package/package.json +12 -10
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,96 @@
|
|
|
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";
|
|
4
6
|
import { Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
|
5
|
-
import { Recipes } from "@bamboocss/core";
|
|
6
|
-
import { compact, createCssUncached, createMergeCss, getSlotCompoundVariant, memo, withoutSpace } from "@bamboocss/shared";
|
|
7
|
+
import { Recipes, classFormatter } from "@bamboocss/core";
|
|
8
|
+
import { compact, createCssUncached, createMergeCss, getRecipeClassNames, getRecipeIdentity, getSlotCompoundVariant, memo, withoutSpace } from "@bamboocss/shared";
|
|
7
9
|
import { resolve } from "node:path";
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
@@ -646,10 +727,272 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
646
727
|
};
|
|
647
728
|
};
|
|
648
729
|
//#endregion
|
|
730
|
+
//#region src/fold-recipe.ts
|
|
731
|
+
/**
|
|
732
|
+
* Binding name → the config it was declared with.
|
|
733
|
+
*
|
|
734
|
+
* Built from the definitions the parser already recorded, walking each one to the declaration
|
|
735
|
+
* that names it. The parser records a definition under the name it was *imported* as (`cva`),
|
|
736
|
+
* and a call under the name the file *bound* (`badge`); this is what joins the two.
|
|
737
|
+
*
|
|
738
|
+
* Reads `cva` and not `sva`, which is load-bearing rather than an omission. The parser records
|
|
739
|
+
* a call of *either* as a recipe call, but an `sva` invocation returns one class per slot — an
|
|
740
|
+
* object, not a string — so there is no literal to substitute. Leaving slot recipes out of this
|
|
741
|
+
* map is what makes them decline as `unknown-recipe` instead of folding to a string that would
|
|
742
|
+
* break every consumer reading `.root` off it.
|
|
743
|
+
*/
|
|
744
|
+
const collectRecipeConfigs = (parserResult) => {
|
|
745
|
+
const configs = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const definition of parserResult.cva) {
|
|
747
|
+
const node = definition.box?.getNode?.();
|
|
748
|
+
if (!node) continue;
|
|
749
|
+
const nameNode = ((Node.isCallExpression(node) ? node : node.getFirstAncestorByKind(SyntaxKind.CallExpression))?.getFirstAncestorByKind(SyntaxKind.VariableDeclaration))?.getNameNode();
|
|
750
|
+
if (!nameNode || !Node.isIdentifier(nameNode)) continue;
|
|
751
|
+
if (definition.data?.length !== 1) {
|
|
752
|
+
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const config = definition.data[0];
|
|
756
|
+
if (!config || typeof config !== "object") continue;
|
|
757
|
+
if (configs.has(nameNode.getText())) {
|
|
758
|
+
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
configs.set(nameNode.getText(), {
|
|
762
|
+
config,
|
|
763
|
+
name: getRecipeIdentity(config),
|
|
764
|
+
box: definition.box
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return configs;
|
|
768
|
+
};
|
|
769
|
+
/** The generated binding a lowered dynamic axis calls. Lives in `cx`, which pulls no engine. */
|
|
770
|
+
const RECIPE_PICK_HELPER = "cvaPick";
|
|
771
|
+
const HELPER = RECIPE_PICK_HELPER;
|
|
772
|
+
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
773
|
+
const AMBIGUOUS = Object.freeze({
|
|
774
|
+
config: {},
|
|
775
|
+
name: "",
|
|
776
|
+
box: void 0
|
|
777
|
+
});
|
|
778
|
+
const LITERAL_KINDS = new Set([
|
|
779
|
+
SyntaxKind.StringLiteral,
|
|
780
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
781
|
+
SyntaxKind.NumericLiteral,
|
|
782
|
+
SyntaxKind.TrueKeyword,
|
|
783
|
+
SyntaxKind.FalseKeyword
|
|
784
|
+
]);
|
|
785
|
+
/**
|
|
786
|
+
* The value a literal node denotes, or `undefined` for anything else.
|
|
787
|
+
*
|
|
788
|
+
* Read off the node rather than from the extractor's resolved data, because that data is lossy
|
|
789
|
+
* in the direction that matters: a property it could not resolve is *dropped*, so `badge({ tone })`
|
|
790
|
+
* and `badge({})` are identical there. Folding the first as if it were the second emits a class
|
|
791
|
+
* string missing the variant — the element renders, wrongly, with no report.
|
|
792
|
+
*/
|
|
793
|
+
const literalValue = (node) => {
|
|
794
|
+
if (!node || !LITERAL_KINDS.has(node.getKind())) return void 0;
|
|
795
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) return node.getLiteralValue();
|
|
796
|
+
if (Node.isNumericLiteral(node)) return node.getLiteralValue();
|
|
797
|
+
if (node.getKind() === SyntaxKind.TrueKeyword) return true;
|
|
798
|
+
if (node.getKind() === SyntaxKind.FalseKeyword) return false;
|
|
799
|
+
};
|
|
800
|
+
/**
|
|
801
|
+
* The property name a key node denotes.
|
|
802
|
+
*
|
|
803
|
+
* Read off the node rather than unquoted from its text. `{ '\\u0074one': 'a' }` names the
|
|
804
|
+
* variant `tone`, and stripping the surrounding quotes leaves the escape uninterpreted — so
|
|
805
|
+
* the variant did not match, its class was dropped, and the element rendered without it. A
|
|
806
|
+
* numeric key normalises the same way: `{ 0x10: 'a' }` is the key `16`.
|
|
807
|
+
*/
|
|
808
|
+
const propertyKey = (nameNode) => {
|
|
809
|
+
if (Node.isIdentifier(nameNode)) return nameNode.getText();
|
|
810
|
+
if (Node.isStringLiteral(nameNode) || Node.isNoSubstitutionTemplateLiteral(nameNode)) return nameNode.getLiteralValue();
|
|
811
|
+
if (Node.isNumericLiteral(nameNode)) return String(nameNode.getLiteralValue());
|
|
812
|
+
};
|
|
813
|
+
/**
|
|
814
|
+
* Make `cvaPick` callable at this call site, by whatever name the file gives it.
|
|
815
|
+
*
|
|
816
|
+
* Not `ensureCxImport`: that one resolves `cx` and finds the declaration to extend by
|
|
817
|
+
* matching the *callee* against an import. An inline recipe's callee is a local binding, so
|
|
818
|
+
* there is nothing to match — the host here is any import of the generated css module, which
|
|
819
|
+
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
820
|
+
*/
|
|
821
|
+
const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
822
|
+
const sourceFile = call.getSourceFile();
|
|
823
|
+
let host;
|
|
824
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
825
|
+
const mod = declaration.getModuleSpecifierValue();
|
|
826
|
+
if (declaration.isTypeOnly()) continue;
|
|
827
|
+
for (const named of declaration.getNamedImports()) {
|
|
828
|
+
if (named.isTypeOnly()) continue;
|
|
829
|
+
if (named.getNameNode().getText() === "cvaPick") {
|
|
830
|
+
if (!isBambooCssModule(mod)) return void 0;
|
|
831
|
+
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
832
|
+
return isShadowed(call, local) ? void 0 : { name: local };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
836
|
+
}
|
|
837
|
+
if (!host) return void 0;
|
|
838
|
+
if (declaredAtModuleScope(sourceFile).has("cvaPick")) return void 0;
|
|
839
|
+
if (isShadowed(call, "cvaPick")) return void 0;
|
|
840
|
+
const last = host.getNamedImports().at(-1);
|
|
841
|
+
if (!last) return void 0;
|
|
842
|
+
return {
|
|
843
|
+
name: RECIPE_PICK_HELPER,
|
|
844
|
+
insert: {
|
|
845
|
+
pos: last.getEnd(),
|
|
846
|
+
names: [RECIPE_PICK_HELPER]
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* Lower one invocation, or say why not.
|
|
852
|
+
*
|
|
853
|
+
* Every property written at the call site has to be a literal. A selection is not additive —
|
|
854
|
+
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
855
|
+
* recipe applies — so a partially-known selection is not foldable at all.
|
|
856
|
+
*/
|
|
857
|
+
const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
858
|
+
if (!entry || entry === AMBIGUOUS) return {
|
|
859
|
+
kind: "decline",
|
|
860
|
+
reason: "unknown-recipe"
|
|
861
|
+
};
|
|
862
|
+
const { config, name } = entry;
|
|
863
|
+
if (config.slots !== void 0) return {
|
|
864
|
+
kind: "decline",
|
|
865
|
+
reason: "unsupported-shape"
|
|
866
|
+
};
|
|
867
|
+
if (!config.base && !config.variants && !config.className) return {
|
|
868
|
+
kind: "decline",
|
|
869
|
+
reason: "unknown-recipe"
|
|
870
|
+
};
|
|
871
|
+
if (!Node.isCallExpression(call)) return {
|
|
872
|
+
kind: "decline",
|
|
873
|
+
reason: "unsupported-shape"
|
|
874
|
+
};
|
|
875
|
+
const args = call.getArguments();
|
|
876
|
+
if (args.length > 1) return {
|
|
877
|
+
kind: "decline",
|
|
878
|
+
reason: "unsupported-shape"
|
|
879
|
+
};
|
|
880
|
+
const selection = {};
|
|
881
|
+
/** Variant → the source expression selecting it, for axes that stay runtime decisions. */
|
|
882
|
+
const dynamicAxes = /* @__PURE__ */ new Map();
|
|
883
|
+
if (args.length === 1) {
|
|
884
|
+
const arg = args[0];
|
|
885
|
+
if (!arg || !Node.isObjectLiteralExpression(arg)) return {
|
|
886
|
+
kind: "decline",
|
|
887
|
+
reason: "dynamic"
|
|
888
|
+
};
|
|
889
|
+
for (const property of arg.getProperties()) {
|
|
890
|
+
if (Node.isSpreadAssignment(property)) return {
|
|
891
|
+
kind: "decline",
|
|
892
|
+
reason: "dynamic"
|
|
893
|
+
};
|
|
894
|
+
if (Node.isShorthandPropertyAssignment(property)) {
|
|
895
|
+
dynamicAxes.set(property.getName(), property.getName());
|
|
896
|
+
delete selection[property.getName()];
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (!Node.isPropertyAssignment(property)) return {
|
|
900
|
+
kind: "decline",
|
|
901
|
+
reason: "dynamic"
|
|
902
|
+
};
|
|
903
|
+
const nameNode = property.getNameNode();
|
|
904
|
+
if (Node.isComputedPropertyName(nameNode)) return {
|
|
905
|
+
kind: "decline",
|
|
906
|
+
reason: "dynamic"
|
|
907
|
+
};
|
|
908
|
+
const key = propertyKey(nameNode);
|
|
909
|
+
if (key === void 0) return {
|
|
910
|
+
kind: "decline",
|
|
911
|
+
reason: "dynamic"
|
|
912
|
+
};
|
|
913
|
+
const literal = literalValue(property.getInitializer());
|
|
914
|
+
if (literal !== void 0) {
|
|
915
|
+
selection[key] = literal;
|
|
916
|
+
dynamicAxes.delete(key);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
920
|
+
const initializer = property.getInitializer();
|
|
921
|
+
if (!initializer) return {
|
|
922
|
+
kind: "decline",
|
|
923
|
+
reason: "dynamic"
|
|
924
|
+
};
|
|
925
|
+
dynamicAxes.set(key, initializer.getText());
|
|
926
|
+
delete selection[key];
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const value = resolvedSelection[key];
|
|
930
|
+
if (value !== null && typeof value === "object") return {
|
|
931
|
+
kind: "decline",
|
|
932
|
+
reason: "dynamic"
|
|
933
|
+
};
|
|
934
|
+
selection[key] = value;
|
|
935
|
+
dynamicAxes.delete(key);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const merged = {
|
|
939
|
+
...config.defaultVariants ?? {},
|
|
940
|
+
...compact(selection)
|
|
941
|
+
};
|
|
942
|
+
const format = classFormatter(ctx);
|
|
943
|
+
if (dynamicAxes.size === 0) return {
|
|
944
|
+
kind: "class",
|
|
945
|
+
className: getRecipeClassNames(name, config.variants, merged, ctx.utility.separator, format)
|
|
946
|
+
};
|
|
947
|
+
for (const key of [...dynamicAxes.keys()]) if (!config.variants?.[key]) dynamicAxes.delete(key);
|
|
948
|
+
if (dynamicAxes.size === 0) {
|
|
949
|
+
const staticOnly = { ...merged };
|
|
950
|
+
for (const key of dynamicAxes.keys()) delete staticOnly[key];
|
|
951
|
+
return {
|
|
952
|
+
kind: "class",
|
|
953
|
+
className: getRecipeClassNames(name, config.variants, staticOnly, ctx.utility.separator, format)
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const ownClass = format(name);
|
|
957
|
+
const parts = [JSON.stringify(ownClass)];
|
|
958
|
+
const classNames = [ownClass];
|
|
959
|
+
for (const key of Object.keys(config.variants ?? {})) {
|
|
960
|
+
const expression = dynamicAxes.get(key);
|
|
961
|
+
if (expression === void 0) {
|
|
962
|
+
const value = merged[key];
|
|
963
|
+
if (value == null) continue;
|
|
964
|
+
if (config.variants?.[key]?.[value] == null) continue;
|
|
965
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${withoutSpace(value)}`);
|
|
966
|
+
parts.push(JSON.stringify(` ${className}`));
|
|
967
|
+
classNames.push(className);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const values = config.variants[key];
|
|
971
|
+
const table = {};
|
|
972
|
+
for (const value of Object.keys(values)) {
|
|
973
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${withoutSpace(value)}`);
|
|
974
|
+
table[value] = ` ${className}`;
|
|
975
|
+
classNames.push(className);
|
|
976
|
+
}
|
|
977
|
+
const fallbackValue = config.defaultVariants?.[key];
|
|
978
|
+
const fallback = fallbackValue != null && values[fallbackValue] != null ? ` ${format(`${name}--${key}${ctx.utility.separator}${withoutSpace(fallbackValue)}`)}` : void 0;
|
|
979
|
+
parts.push(`${HELPER}(${expression}, ${JSON.stringify(table)}${fallback === void 0 ? "" : `, ${JSON.stringify(fallback)}`})`);
|
|
980
|
+
}
|
|
981
|
+
if (parts.length === 1) return {
|
|
982
|
+
kind: "class",
|
|
983
|
+
className: ownClass
|
|
984
|
+
};
|
|
985
|
+
return {
|
|
986
|
+
kind: "expression",
|
|
987
|
+
expression: parts.join(" + "),
|
|
988
|
+
classNames,
|
|
989
|
+
staticClasses: ownClass
|
|
990
|
+
};
|
|
991
|
+
};
|
|
992
|
+
//#endregion
|
|
649
993
|
//#region src/runtime-css.ts
|
|
650
994
|
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
651
995
|
const createCssContext = (ctx) => ({
|
|
652
|
-
grouped: ctx.config.cssMode === "grouped",
|
|
653
996
|
hash: Boolean(ctx.hash.className),
|
|
654
997
|
conditions: {
|
|
655
998
|
shift: ctx.conditions.shift,
|
|
@@ -755,22 +1098,13 @@ const createRuntimeRecipe = (ctx) => {
|
|
|
755
1098
|
//#endregion
|
|
756
1099
|
//#region src/fold.ts
|
|
757
1100
|
/**
|
|
758
|
-
* `cva`/`sva` return a function, so neither can collapse to a class string.
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* through its own path rather than being declined outright.
|
|
1101
|
+
* `cva`/`sva` return a function, so neither *definition* can collapse to a class string.
|
|
1102
|
+
* `token` also resolves to no class, but it does resolve to a literal, so it folds through
|
|
1103
|
+
* its own path rather than being declined outright.
|
|
762
1104
|
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
* `button()` from `const button = cva(...)` is never recorded, and tracking those bindings
|
|
767
|
-
* is a change to the extractor rather than to this set.
|
|
768
|
-
*
|
|
769
|
-
* Worth knowing before taking that on: semantic naming already took most of the prize.
|
|
770
|
-
* `cvaFn` used to run `mergeCss` and name a class per property on every call; it is now a
|
|
771
|
-
* memoized loop over `variantKeys` doing string concatenation. That is an inspection of the
|
|
772
|
-
* two implementations, not a measurement — benchmark it before deciding it is worth the
|
|
773
|
-
* extractor work.
|
|
1105
|
+
* Their invocations are a different matter and do fold — `cva`'s through `fold-recipe`,
|
|
1106
|
+
* which is a separate set because the call is recorded under the name the file bound rather
|
|
1107
|
+
* than the name it imported. `sva`'s do not: a slot recipe resolves to one class per slot.
|
|
774
1108
|
*/
|
|
775
1109
|
const FOLDABLE_TYPES = new Set([
|
|
776
1110
|
"css",
|
|
@@ -792,6 +1126,18 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
792
1126
|
*/
|
|
793
1127
|
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
794
1128
|
/**
|
|
1129
|
+
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
1130
|
+
*
|
|
1131
|
+
* Folded when the whole selection resolves, reported under this reason when it does not.
|
|
1132
|
+
* Deliberately all-or-nothing: an unresolved variant does not merely omit its own class, so a
|
|
1133
|
+
* partially-known selection is not foldable at all.
|
|
1134
|
+
*
|
|
1135
|
+
* Visible at all because it used to not be. The parser matched calls by imported name, so a
|
|
1136
|
+
* local binding was never recorded, and an unfoldable invocation looked identical to code
|
|
1137
|
+
* nothing had parsed.
|
|
1138
|
+
*/
|
|
1139
|
+
const RECIPE_CALL_TYPE = "cva-call";
|
|
1140
|
+
/**
|
|
795
1141
|
* An argument that cannot run anything when it is evaluated.
|
|
796
1142
|
*
|
|
797
1143
|
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
@@ -1110,6 +1456,21 @@ const foldSource = (options) => {
|
|
|
1110
1456
|
const skipped = [];
|
|
1111
1457
|
const candidates = [];
|
|
1112
1458
|
const seenRanges = /* @__PURE__ */ new Set();
|
|
1459
|
+
/** Built on first use: most modules declare no inline recipe. */
|
|
1460
|
+
let recipeConfigs;
|
|
1461
|
+
/**
|
|
1462
|
+
* Per inline recipe binding: calls seen, calls lowered.
|
|
1463
|
+
*
|
|
1464
|
+
* A binding whose every call lowered is no longer read, so its `cva({ … })` config can leave
|
|
1465
|
+
* the bundle — which is the whole point, the config being far larger than the runtime. But a
|
|
1466
|
+
* bundler will not drop the call on its own: `cva` closes over the config and builds an
|
|
1467
|
+
* object, and Rollup cannot prove that is side-effect free, so it keeps the expression and
|
|
1468
|
+
* the module ends up *larger* than before folding. The annotation below is what makes the
|
|
1469
|
+
* saving real, and it is only correct to claim it once nothing reads the binding.
|
|
1470
|
+
*/
|
|
1471
|
+
const recipeCalls = /* @__PURE__ */ new Map();
|
|
1472
|
+
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1473
|
+
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1113
1474
|
const importCache = /* @__PURE__ */ new Map();
|
|
1114
1475
|
const importsFor = (sourceFile) => {
|
|
1115
1476
|
let names = importCache.get(sourceFile);
|
|
@@ -1223,6 +1584,83 @@ const foldSource = (options) => {
|
|
|
1223
1584
|
start: call.getStart(),
|
|
1224
1585
|
end: call.getEnd()
|
|
1225
1586
|
});
|
|
1587
|
+
if (call && type === RECIPE_CALL_TYPE && !isShadowed(call, name)) {
|
|
1588
|
+
const start = call.getStart();
|
|
1589
|
+
const end = call.getEnd();
|
|
1590
|
+
const rangeKey = `${start}:${end}`;
|
|
1591
|
+
if (!reportedRanges.has(rangeKey)) {
|
|
1592
|
+
reportedRanges.add(rangeKey);
|
|
1593
|
+
if (code.slice(start, end) !== call.getText()) {
|
|
1594
|
+
skipped.push({
|
|
1595
|
+
name,
|
|
1596
|
+
reason: "no-call-expression",
|
|
1597
|
+
start: 0,
|
|
1598
|
+
end: 0
|
|
1599
|
+
});
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
recipeConfigs ??= collectRecipeConfigs(parserResult);
|
|
1603
|
+
const tally = recipeCalls.get(name) ?? {
|
|
1604
|
+
seen: 0,
|
|
1605
|
+
lowered: 0
|
|
1606
|
+
};
|
|
1607
|
+
tally.seen++;
|
|
1608
|
+
recipeCalls.set(name, tally);
|
|
1609
|
+
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1610
|
+
const entry = recipeConfigs.get(name);
|
|
1611
|
+
const lowered = Node.isCallExpression(call) && call.getArguments().every(isInertExpression) ? lowerRecipeCall(call, entry, ctx, resolvedSelection) : {
|
|
1612
|
+
kind: "decline",
|
|
1613
|
+
reason: "dynamic"
|
|
1614
|
+
};
|
|
1615
|
+
if (lowered.kind === "expression") {
|
|
1616
|
+
const helper = ensureRecipeHelperImport(call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1617
|
+
if (helper) {
|
|
1618
|
+
tally.lowered++;
|
|
1619
|
+
candidates.push({
|
|
1620
|
+
item,
|
|
1621
|
+
call,
|
|
1622
|
+
node: call,
|
|
1623
|
+
start,
|
|
1624
|
+
end,
|
|
1625
|
+
replacement: helper.name === "cvaPick" ? lowered.expression : lowered.expression.replaceAll(`${RECIPE_PICK_HELPER}(`, `${helper.name}(`),
|
|
1626
|
+
className: lowered.staticClasses,
|
|
1627
|
+
classNames: lowered.classNames,
|
|
1628
|
+
insert: helper.insert,
|
|
1629
|
+
configBox: entry?.box
|
|
1630
|
+
});
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
skipped.push({
|
|
1634
|
+
name,
|
|
1635
|
+
reason: "recipe-call",
|
|
1636
|
+
start,
|
|
1637
|
+
end
|
|
1638
|
+
});
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (lowered.kind === "class") {
|
|
1642
|
+
tally.lowered++;
|
|
1643
|
+
candidates.push({
|
|
1644
|
+
item,
|
|
1645
|
+
call,
|
|
1646
|
+
node: call,
|
|
1647
|
+
start,
|
|
1648
|
+
end,
|
|
1649
|
+
replacement: JSON.stringify(lowered.className),
|
|
1650
|
+
className: lowered.className,
|
|
1651
|
+
classNames: lowered.className.split(" ").filter(Boolean),
|
|
1652
|
+
configBox: entry?.box
|
|
1653
|
+
});
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
skipped.push({
|
|
1657
|
+
name,
|
|
1658
|
+
reason: "recipe-call",
|
|
1659
|
+
start,
|
|
1660
|
+
end
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1226
1664
|
continue;
|
|
1227
1665
|
}
|
|
1228
1666
|
if (!call) {
|
|
@@ -1377,6 +1815,7 @@ const foldSource = (options) => {
|
|
|
1377
1815
|
end
|
|
1378
1816
|
});
|
|
1379
1817
|
collectSourceFiles(item.box, dependencyScan);
|
|
1818
|
+
if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
|
|
1380
1819
|
continue;
|
|
1381
1820
|
}
|
|
1382
1821
|
let className;
|
|
@@ -1425,6 +1864,16 @@ const foldSource = (options) => {
|
|
|
1425
1864
|
});
|
|
1426
1865
|
collectSourceFiles(item.box, dependencyScan);
|
|
1427
1866
|
}
|
|
1867
|
+
for (const [binding, tally] of recipeCalls) {
|
|
1868
|
+
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
1869
|
+
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
|
1870
|
+
if (!definition) continue;
|
|
1871
|
+
const call = Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(SyntaxKind.CallExpression);
|
|
1872
|
+
if (!call) continue;
|
|
1873
|
+
const start = call.getStart();
|
|
1874
|
+
if (code.slice(start, call.getEnd()) !== call.getText()) continue;
|
|
1875
|
+
magic.appendLeft(start, "/*#__PURE__*/");
|
|
1876
|
+
}
|
|
1428
1877
|
if (folded.length === 0) return {
|
|
1429
1878
|
code,
|
|
1430
1879
|
map: null,
|
|
@@ -1475,6 +1924,23 @@ const isGeneratedOutput = (filePath, ctx) => {
|
|
|
1475
1924
|
const file = slashed(filePath);
|
|
1476
1925
|
return file === root || file.startsWith(`${root}/`);
|
|
1477
1926
|
};
|
|
1927
|
+
/**
|
|
1928
|
+
* The skip reasons that leave a `css()`-family call in the output.
|
|
1929
|
+
*
|
|
1930
|
+
* `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
|
|
1931
|
+
* function of the same name — neither leaves a call of ours. `not-foldable` is a `cva`/`sva`
|
|
1932
|
+
* definition, which keeps the recipe runtime rather than the css engine; see `strict`.
|
|
1933
|
+
*/
|
|
1934
|
+
const SURVIVES_TO_RUNTIME = new Set([
|
|
1935
|
+
"dynamic",
|
|
1936
|
+
"raw-call",
|
|
1937
|
+
"unsupported-kind",
|
|
1938
|
+
"no-call-expression",
|
|
1939
|
+
"empty",
|
|
1940
|
+
"unresolved-token"
|
|
1941
|
+
]);
|
|
1942
|
+
/** 1-indexed line of a source offset, for an error a user can navigate to. */
|
|
1943
|
+
const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
|
|
1478
1944
|
const formatSkipped = (id, skipped) => {
|
|
1479
1945
|
const counts = /* @__PURE__ */ new Map();
|
|
1480
1946
|
for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
|
|
@@ -1483,16 +1949,17 @@ const formatSkipped = (id, skipped) => {
|
|
|
1483
1949
|
/**
|
|
1484
1950
|
* Vite integration for Bamboo CSS.
|
|
1485
1951
|
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1952
|
+
* Two plugins, because they do unrelated jobs on different schedules. The first emits the
|
|
1953
|
+
* stylesheet as a virtual module and runs in dev and build alike — that is the integration,
|
|
1954
|
+
* and nothing styles without it. The second is the optional build-time fold.
|
|
1488
1955
|
*
|
|
1489
|
-
*
|
|
1956
|
+
* The fold runs with `enforce: 'pre'` so it sees module source as close as possible to what
|
|
1490
1957
|
* the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
|
|
1491
1958
|
* sees them would otherwise make the two disagree, and a folded class could end up
|
|
1492
1959
|
* with no matching rule.
|
|
1493
1960
|
*/
|
|
1494
1961
|
const bamboocss = (options = {}) => {
|
|
1495
|
-
const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
|
|
1962
|
+
const { transform = true, partial, configPath, cwd, reportSkipped = false, reportSummary = true, strict = false } = options;
|
|
1496
1963
|
/** Totals across the build, for the summary. */
|
|
1497
1964
|
const totals = {
|
|
1498
1965
|
folded: 0,
|
|
@@ -1500,6 +1967,8 @@ const bamboocss = (options = {}) => {
|
|
|
1500
1967
|
filesWithFolds: 0,
|
|
1501
1968
|
skipped: /* @__PURE__ */ new Map()
|
|
1502
1969
|
};
|
|
1970
|
+
/** Under `strict`, every call that would still reach the runtime. */
|
|
1971
|
+
const survivors = [];
|
|
1503
1972
|
let ctx;
|
|
1504
1973
|
let runtimeCss;
|
|
1505
1974
|
let setup;
|
|
@@ -1513,8 +1982,11 @@ const bamboocss = (options = {}) => {
|
|
|
1513
1982
|
});
|
|
1514
1983
|
await setup;
|
|
1515
1984
|
};
|
|
1516
|
-
return {
|
|
1517
|
-
|
|
1985
|
+
return [bamboocssCss({
|
|
1986
|
+
configPath,
|
|
1987
|
+
cwd
|
|
1988
|
+
}), {
|
|
1989
|
+
name: "bamboocss:fold",
|
|
1518
1990
|
enforce: "pre",
|
|
1519
1991
|
apply: "build",
|
|
1520
1992
|
async buildStart() {
|
|
@@ -1523,6 +1995,7 @@ const bamboocss = (options = {}) => {
|
|
|
1523
1995
|
totals.files = 0;
|
|
1524
1996
|
totals.filesWithFolds = 0;
|
|
1525
1997
|
totals.skipped.clear();
|
|
1998
|
+
survivors.length = 0;
|
|
1526
1999
|
await ensureContext();
|
|
1527
2000
|
},
|
|
1528
2001
|
/**
|
|
@@ -1584,6 +2057,23 @@ const bamboocss = (options = {}) => {
|
|
|
1584
2057
|
totals.folded += result.folded.length;
|
|
1585
2058
|
if (result.folded.length) totals.filesWithFolds++;
|
|
1586
2059
|
for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
|
|
2060
|
+
if (strict) {
|
|
2061
|
+
for (const entry of result.skipped) {
|
|
2062
|
+
if (!SURVIVES_TO_RUNTIME.has(entry.reason)) continue;
|
|
2063
|
+
survivors.push({
|
|
2064
|
+
file: filePath,
|
|
2065
|
+
line: lineAt(code, entry.start),
|
|
2066
|
+
name: entry.name,
|
|
2067
|
+
reason: entry.reason
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
if (result.code.includes("cssLeaf(")) survivors.push({
|
|
2071
|
+
file: filePath,
|
|
2072
|
+
line: lineAt(result.code, result.code.indexOf("cssLeaf(")),
|
|
2073
|
+
name: "cssLeaf",
|
|
2074
|
+
reason: "lowered-leaf"
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
1587
2077
|
if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
1588
2078
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
1589
2079
|
if (!result.folded.length) return null;
|
|
@@ -1594,6 +2084,16 @@ const bamboocss = (options = {}) => {
|
|
|
1594
2084
|
};
|
|
1595
2085
|
},
|
|
1596
2086
|
buildEnd() {
|
|
2087
|
+
if (strict && survivors.length) {
|
|
2088
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2089
|
+
for (const entry of survivors) {
|
|
2090
|
+
const list = byFile.get(entry.file) ?? [];
|
|
2091
|
+
list.push(entry);
|
|
2092
|
+
byFile.set(entry.file, list);
|
|
2093
|
+
}
|
|
2094
|
+
const detail = Array.from(byFile.entries()).map(([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${e.name}() — ${e.reason}`)].join("\n")).join("\n");
|
|
2095
|
+
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.`);
|
|
2096
|
+
}
|
|
1597
2097
|
if (!transform || !reportSummary) return;
|
|
1598
2098
|
const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
1599
2099
|
const total = totals.folded + declined;
|
|
@@ -1602,7 +2102,7 @@ const bamboocss = (options = {}) => {
|
|
|
1602
2102
|
const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
1603
2103
|
logger.info("vite:transform", `Folded ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
1604
2104
|
}
|
|
1605
|
-
};
|
|
2105
|
+
}];
|
|
1606
2106
|
};
|
|
1607
2107
|
//#endregion
|
|
1608
|
-
export { bamboocss, bamboocss as default, createRuntimeCss, foldSource };
|
|
2108
|
+
export { VIRTUAL_CSS_ID, bamboocss, bamboocss as default, bamboocssCss, createRuntimeCss, foldSource };
|