@bamboocss/vite 1.22.0 → 1.24.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/dist/index.cjs +425 -15
- package/dist/index.d.cts +1 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +427 -17
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -754,6 +754,313 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
754
754
|
};
|
|
755
755
|
};
|
|
756
756
|
//#endregion
|
|
757
|
+
//#region src/fold-recipe.ts
|
|
758
|
+
/**
|
|
759
|
+
* Binding name → the config it was declared with.
|
|
760
|
+
*
|
|
761
|
+
* Built from the definitions the parser already recorded, walking each one to the declaration
|
|
762
|
+
* that names it. The parser records a definition under the name it was *imported* as (`cva`),
|
|
763
|
+
* and a call under the name the file *bound* (`badge`); this is what joins the two.
|
|
764
|
+
*
|
|
765
|
+
* Reads `cva` and not `sva`, which is load-bearing rather than an omission. The parser records
|
|
766
|
+
* a call of *either* as a recipe call, but an `sva` invocation returns one class per slot — an
|
|
767
|
+
* object, not a string — so there is no literal to substitute. Leaving slot recipes out of this
|
|
768
|
+
* map is what makes them decline as `unknown-recipe` instead of folding to a string that would
|
|
769
|
+
* break every consumer reading `.root` off it.
|
|
770
|
+
*/
|
|
771
|
+
const collectRecipeConfigs = (parserResult) => {
|
|
772
|
+
const configs = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const definition of parserResult.cva) {
|
|
774
|
+
const node = definition.box?.getNode?.();
|
|
775
|
+
if (!node) continue;
|
|
776
|
+
const nameNode = ((ts_morph.Node.isCallExpression(node) ? node : node.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression))?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration))?.getNameNode();
|
|
777
|
+
if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
|
|
778
|
+
if (definition.data?.length !== 1) {
|
|
779
|
+
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const config = definition.data[0];
|
|
783
|
+
if (!config || typeof config !== "object") continue;
|
|
784
|
+
if (configs.has(nameNode.getText())) {
|
|
785
|
+
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
configs.set(nameNode.getText(), {
|
|
789
|
+
config,
|
|
790
|
+
name: (0, _bamboocss_shared.getRecipeIdentity)(config),
|
|
791
|
+
box: definition.box
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return configs;
|
|
795
|
+
};
|
|
796
|
+
/** The generated binding a lowered dynamic axis calls. Lives in `cx`, which pulls no engine. */
|
|
797
|
+
const RECIPE_PICK_HELPER = "cvaPick";
|
|
798
|
+
const HELPER = RECIPE_PICK_HELPER;
|
|
799
|
+
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
800
|
+
const AMBIGUOUS = Object.freeze({
|
|
801
|
+
config: {},
|
|
802
|
+
name: "",
|
|
803
|
+
box: void 0
|
|
804
|
+
});
|
|
805
|
+
const LITERAL_KINDS = new Set([
|
|
806
|
+
ts_morph.SyntaxKind.StringLiteral,
|
|
807
|
+
ts_morph.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
808
|
+
ts_morph.SyntaxKind.NumericLiteral,
|
|
809
|
+
ts_morph.SyntaxKind.TrueKeyword,
|
|
810
|
+
ts_morph.SyntaxKind.FalseKeyword
|
|
811
|
+
]);
|
|
812
|
+
/**
|
|
813
|
+
* The value a literal node denotes, or `undefined` for anything else.
|
|
814
|
+
*
|
|
815
|
+
* Read off the node rather than from the extractor's resolved data, because that data is lossy
|
|
816
|
+
* in the direction that matters: a property it could not resolve is *dropped*, so `badge({ tone })`
|
|
817
|
+
* and `badge({})` are identical there. Folding the first as if it were the second emits a class
|
|
818
|
+
* string missing the variant — the element renders, wrongly, with no report.
|
|
819
|
+
*/
|
|
820
|
+
const literalValue = (node) => {
|
|
821
|
+
if (!node || !LITERAL_KINDS.has(node.getKind())) return void 0;
|
|
822
|
+
if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node)) return node.getLiteralValue();
|
|
823
|
+
if (ts_morph.Node.isNumericLiteral(node)) return node.getLiteralValue();
|
|
824
|
+
if (node.getKind() === ts_morph.SyntaxKind.TrueKeyword) return true;
|
|
825
|
+
if (node.getKind() === ts_morph.SyntaxKind.FalseKeyword) return false;
|
|
826
|
+
};
|
|
827
|
+
/**
|
|
828
|
+
* The property name a key node denotes.
|
|
829
|
+
*
|
|
830
|
+
* Read off the node rather than unquoted from its text. `{ '\\u0074one': 'a' }` names the
|
|
831
|
+
* variant `tone`, and stripping the surrounding quotes leaves the escape uninterpreted — so
|
|
832
|
+
* the variant did not match, its class was dropped, and the element rendered without it. A
|
|
833
|
+
* numeric key normalises the same way: `{ 0x10: 'a' }` is the key `16`.
|
|
834
|
+
*/
|
|
835
|
+
const propertyKey = (nameNode) => {
|
|
836
|
+
if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText();
|
|
837
|
+
if (ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNoSubstitutionTemplateLiteral(nameNode)) return nameNode.getLiteralValue();
|
|
838
|
+
if (ts_morph.Node.isNumericLiteral(nameNode)) return String(nameNode.getLiteralValue());
|
|
839
|
+
};
|
|
840
|
+
/**
|
|
841
|
+
* Make `cvaPick` callable at this call site, by whatever name the file gives it.
|
|
842
|
+
*
|
|
843
|
+
* Not `ensureCxImport`: that one resolves `cx` and finds the declaration to extend by
|
|
844
|
+
* matching the *callee* against an import. An inline recipe's callee is a local binding, so
|
|
845
|
+
* there is nothing to match — the host here is any import of the generated css module, which
|
|
846
|
+
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
847
|
+
*/
|
|
848
|
+
const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
849
|
+
const sourceFile = call.getSourceFile();
|
|
850
|
+
let host;
|
|
851
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
852
|
+
const mod = declaration.getModuleSpecifierValue();
|
|
853
|
+
if (declaration.isTypeOnly()) continue;
|
|
854
|
+
for (const named of declaration.getNamedImports()) {
|
|
855
|
+
if (named.isTypeOnly()) continue;
|
|
856
|
+
if (named.getNameNode().getText() === "cvaPick") {
|
|
857
|
+
if (!isBambooCssModule(mod)) return void 0;
|
|
858
|
+
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
859
|
+
return isShadowed(call, local) ? void 0 : { name: local };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
863
|
+
}
|
|
864
|
+
if (!host) return void 0;
|
|
865
|
+
if (declaredAtModuleScope(sourceFile).has("cvaPick")) return void 0;
|
|
866
|
+
if (isShadowed(call, "cvaPick")) return void 0;
|
|
867
|
+
const last = host.getNamedImports().at(-1);
|
|
868
|
+
if (!last) return void 0;
|
|
869
|
+
return {
|
|
870
|
+
name: RECIPE_PICK_HELPER,
|
|
871
|
+
insert: {
|
|
872
|
+
pos: last.getEnd(),
|
|
873
|
+
names: [RECIPE_PICK_HELPER]
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
/**
|
|
878
|
+
* Lower one invocation, or say why not.
|
|
879
|
+
*
|
|
880
|
+
* Every property written at the call site has to be a literal. A selection is not additive —
|
|
881
|
+
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
882
|
+
* recipe applies — so a partially-known selection is not foldable at all.
|
|
883
|
+
*/
|
|
884
|
+
const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
885
|
+
if (!entry || entry === AMBIGUOUS) return {
|
|
886
|
+
kind: "decline",
|
|
887
|
+
reason: "unknown-recipe"
|
|
888
|
+
};
|
|
889
|
+
const { config, name } = entry;
|
|
890
|
+
if (config.slots !== void 0) return {
|
|
891
|
+
kind: "decline",
|
|
892
|
+
reason: "unsupported-shape"
|
|
893
|
+
};
|
|
894
|
+
if (!config.base && !config.variants && !config.className) return {
|
|
895
|
+
kind: "decline",
|
|
896
|
+
reason: "unknown-recipe"
|
|
897
|
+
};
|
|
898
|
+
if (!ts_morph.Node.isCallExpression(call)) return {
|
|
899
|
+
kind: "decline",
|
|
900
|
+
reason: "unsupported-shape"
|
|
901
|
+
};
|
|
902
|
+
const args = call.getArguments();
|
|
903
|
+
if (args.length > 1) return {
|
|
904
|
+
kind: "decline",
|
|
905
|
+
reason: "unsupported-shape"
|
|
906
|
+
};
|
|
907
|
+
const selection = {};
|
|
908
|
+
/** Variant → the source expression selecting it, for axes that stay runtime decisions. */
|
|
909
|
+
const dynamicAxes = /* @__PURE__ */ new Map();
|
|
910
|
+
/**
|
|
911
|
+
* Variants whose expression could run something, in the order the source evaluates them.
|
|
912
|
+
*
|
|
913
|
+
* The text is kept, not just the key: a later property writing the same key replaces the
|
|
914
|
+
* entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
|
|
915
|
+
*/
|
|
916
|
+
const effectful = [];
|
|
917
|
+
if (args.length === 1) {
|
|
918
|
+
const arg = args[0];
|
|
919
|
+
if (!arg || !ts_morph.Node.isObjectLiteralExpression(arg)) return {
|
|
920
|
+
kind: "decline",
|
|
921
|
+
reason: "dynamic"
|
|
922
|
+
};
|
|
923
|
+
for (const property of arg.getProperties()) {
|
|
924
|
+
if (ts_morph.Node.isSpreadAssignment(property)) return {
|
|
925
|
+
kind: "decline",
|
|
926
|
+
reason: "dynamic"
|
|
927
|
+
};
|
|
928
|
+
if (ts_morph.Node.isShorthandPropertyAssignment(property)) {
|
|
929
|
+
dynamicAxes.set(property.getName(), property.getName());
|
|
930
|
+
delete selection[property.getName()];
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (!ts_morph.Node.isPropertyAssignment(property)) return {
|
|
934
|
+
kind: "decline",
|
|
935
|
+
reason: "dynamic"
|
|
936
|
+
};
|
|
937
|
+
const nameNode = property.getNameNode();
|
|
938
|
+
if (ts_morph.Node.isComputedPropertyName(nameNode)) return {
|
|
939
|
+
kind: "decline",
|
|
940
|
+
reason: "dynamic"
|
|
941
|
+
};
|
|
942
|
+
const key = propertyKey(nameNode);
|
|
943
|
+
if (key === void 0) return {
|
|
944
|
+
kind: "decline",
|
|
945
|
+
reason: "dynamic"
|
|
946
|
+
};
|
|
947
|
+
const initializer = property.getInitializer();
|
|
948
|
+
if (initializer && !isInert(initializer)) {
|
|
949
|
+
if (!Object.hasOwn(config.variants ?? {}, key)) return {
|
|
950
|
+
kind: "decline",
|
|
951
|
+
reason: "dynamic"
|
|
952
|
+
};
|
|
953
|
+
effectful.push({
|
|
954
|
+
key,
|
|
955
|
+
text: initializer.getText()
|
|
956
|
+
});
|
|
957
|
+
dynamicAxes.set(key, initializer.getText());
|
|
958
|
+
delete selection[key];
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const literal = literalValue(initializer);
|
|
962
|
+
if (literal !== void 0) {
|
|
963
|
+
selection[key] = literal;
|
|
964
|
+
dynamicAxes.delete(key);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
968
|
+
if (!initializer) return {
|
|
969
|
+
kind: "decline",
|
|
970
|
+
reason: "dynamic"
|
|
971
|
+
};
|
|
972
|
+
dynamicAxes.set(key, initializer.getText());
|
|
973
|
+
delete selection[key];
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const value = resolvedSelection[key];
|
|
977
|
+
if (value !== null && typeof value === "object") return {
|
|
978
|
+
kind: "decline",
|
|
979
|
+
reason: "dynamic"
|
|
980
|
+
};
|
|
981
|
+
selection[key] = value;
|
|
982
|
+
dynamicAxes.delete(key);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Every expression that could run something has to reach the output carrying its own text.
|
|
987
|
+
*
|
|
988
|
+
* A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
|
|
989
|
+
* tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
|
|
990
|
+
* literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
|
|
991
|
+
* typecheck and does transform `.js`, so this is reachable.
|
|
992
|
+
*/
|
|
993
|
+
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
994
|
+
const merged = {
|
|
995
|
+
...config.defaultVariants ?? {},
|
|
996
|
+
...(0, _bamboocss_shared.compact)(selection)
|
|
997
|
+
};
|
|
998
|
+
const format = (0, _bamboocss_core.classFormatter)(ctx);
|
|
999
|
+
if (dynamicAxes.size === 0) {
|
|
1000
|
+
if (!everyEffectSurvives()) return {
|
|
1001
|
+
kind: "decline",
|
|
1002
|
+
reason: "dynamic"
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
kind: "class",
|
|
1006
|
+
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants, merged, ctx.utility.separator, format)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (!everyEffectSurvives()) return {
|
|
1010
|
+
kind: "decline",
|
|
1011
|
+
reason: "dynamic"
|
|
1012
|
+
};
|
|
1013
|
+
if (effectful.length > 1) {
|
|
1014
|
+
const variantOrder = Object.keys(config.variants ?? {});
|
|
1015
|
+
const keys = effectful.map((entry) => entry.key);
|
|
1016
|
+
if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
|
|
1017
|
+
kind: "decline",
|
|
1018
|
+
reason: "dynamic"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
1022
|
+
if (dynamicAxes.size === 0) return {
|
|
1023
|
+
kind: "class",
|
|
1024
|
+
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants, merged, ctx.utility.separator, format)
|
|
1025
|
+
};
|
|
1026
|
+
const ownClass = format(name);
|
|
1027
|
+
const parts = [JSON.stringify(ownClass)];
|
|
1028
|
+
const classNames = [ownClass];
|
|
1029
|
+
for (const key of Object.keys(config.variants ?? {})) {
|
|
1030
|
+
const expression = dynamicAxes.get(key);
|
|
1031
|
+
if (expression === void 0) {
|
|
1032
|
+
const value = merged[key];
|
|
1033
|
+
if (value == null) continue;
|
|
1034
|
+
const declared = config.variants?.[key];
|
|
1035
|
+
if (!declared || !Object.hasOwn(declared, value) || declared[value] == null) continue;
|
|
1036
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(value)}`);
|
|
1037
|
+
parts.push(JSON.stringify(` ${className}`));
|
|
1038
|
+
classNames.push(className);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const values = config.variants[key];
|
|
1042
|
+
const table = {};
|
|
1043
|
+
for (const value of Object.keys(values)) {
|
|
1044
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(value)}`);
|
|
1045
|
+
table[value] = ` ${className}`;
|
|
1046
|
+
classNames.push(className);
|
|
1047
|
+
}
|
|
1048
|
+
const fallbackValue = config.defaultVariants?.[key];
|
|
1049
|
+
const fallback = fallbackValue != null && values[fallbackValue] != null ? ` ${format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(fallbackValue)}`)}` : void 0;
|
|
1050
|
+
parts.push(`${HELPER}(${expression}, ${JSON.stringify(table)}${fallback === void 0 ? "" : `, ${JSON.stringify(fallback)}`})`);
|
|
1051
|
+
}
|
|
1052
|
+
if (parts.length === 1) return {
|
|
1053
|
+
kind: "class",
|
|
1054
|
+
className: ownClass
|
|
1055
|
+
};
|
|
1056
|
+
return {
|
|
1057
|
+
kind: "expression",
|
|
1058
|
+
expression: parts.join(" + "),
|
|
1059
|
+
classNames,
|
|
1060
|
+
staticClasses: ownClass
|
|
1061
|
+
};
|
|
1062
|
+
};
|
|
1063
|
+
//#endregion
|
|
757
1064
|
//#region src/runtime-css.ts
|
|
758
1065
|
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
759
1066
|
const createCssContext = (ctx) => ({
|
|
@@ -862,22 +1169,13 @@ const createRuntimeRecipe = (ctx) => {
|
|
|
862
1169
|
//#endregion
|
|
863
1170
|
//#region src/fold.ts
|
|
864
1171
|
/**
|
|
865
|
-
* `cva`/`sva` return a function, so neither can collapse to a class string.
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
* through its own path rather than being declined outright.
|
|
869
|
-
*
|
|
870
|
-
* Folding an invocation is now *possible* in a way it was not: a recipe's classes are named
|
|
871
|
-
* semantically, so the build knows every class a call can produce from the config alone.
|
|
872
|
-
* What is missing is upstream — the parser matches calls by imported name, so a local
|
|
873
|
-
* `button()` from `const button = cva(...)` is never recorded, and tracking those bindings
|
|
874
|
-
* is a change to the extractor rather than to this set.
|
|
1172
|
+
* `cva`/`sva` return a function, so neither *definition* can collapse to a class string.
|
|
1173
|
+
* `token` also resolves to no class, but it does resolve to a literal, so it folds through
|
|
1174
|
+
* its own path rather than being declined outright.
|
|
875
1175
|
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* two implementations, not a measurement — benchmark it before deciding it is worth the
|
|
880
|
-
* extractor work.
|
|
1176
|
+
* Their invocations are a different matter and do fold — `cva`'s through `fold-recipe`,
|
|
1177
|
+
* which is a separate set because the call is recorded under the name the file bound rather
|
|
1178
|
+
* than the name it imported. `sva`'s do not: a slot recipe resolves to one class per slot.
|
|
881
1179
|
*/
|
|
882
1180
|
const FOLDABLE_TYPES = new Set([
|
|
883
1181
|
"css",
|
|
@@ -899,6 +1197,18 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
899
1197
|
*/
|
|
900
1198
|
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
901
1199
|
/**
|
|
1200
|
+
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
1201
|
+
*
|
|
1202
|
+
* Folded when the whole selection resolves, reported under this reason when it does not.
|
|
1203
|
+
* Deliberately all-or-nothing: an unresolved variant does not merely omit its own class, so a
|
|
1204
|
+
* partially-known selection is not foldable at all.
|
|
1205
|
+
*
|
|
1206
|
+
* Visible at all because it used to not be. The parser matched calls by imported name, so a
|
|
1207
|
+
* local binding was never recorded, and an unfoldable invocation looked identical to code
|
|
1208
|
+
* nothing had parsed.
|
|
1209
|
+
*/
|
|
1210
|
+
const RECIPE_CALL_TYPE = "cva-call";
|
|
1211
|
+
/**
|
|
902
1212
|
* An argument that cannot run anything when it is evaluated.
|
|
903
1213
|
*
|
|
904
1214
|
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
@@ -1217,6 +1527,21 @@ const foldSource = (options) => {
|
|
|
1217
1527
|
const skipped = [];
|
|
1218
1528
|
const candidates = [];
|
|
1219
1529
|
const seenRanges = /* @__PURE__ */ new Set();
|
|
1530
|
+
/** Built on first use: most modules declare no inline recipe. */
|
|
1531
|
+
let recipeConfigs;
|
|
1532
|
+
/**
|
|
1533
|
+
* Per inline recipe binding: calls seen, calls lowered.
|
|
1534
|
+
*
|
|
1535
|
+
* A binding whose every call lowered is no longer read, so its `cva({ … })` config can leave
|
|
1536
|
+
* the bundle — which is the whole point, the config being far larger than the runtime. But a
|
|
1537
|
+
* bundler will not drop the call on its own: `cva` closes over the config and builds an
|
|
1538
|
+
* object, and Rollup cannot prove that is side-effect free, so it keeps the expression and
|
|
1539
|
+
* the module ends up *larger* than before folding. The annotation below is what makes the
|
|
1540
|
+
* saving real, and it is only correct to claim it once nothing reads the binding.
|
|
1541
|
+
*/
|
|
1542
|
+
const recipeCalls = /* @__PURE__ */ new Map();
|
|
1543
|
+
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1544
|
+
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1220
1545
|
const importCache = /* @__PURE__ */ new Map();
|
|
1221
1546
|
const importsFor = (sourceFile) => {
|
|
1222
1547
|
let names = importCache.get(sourceFile);
|
|
@@ -1330,6 +1655,80 @@ const foldSource = (options) => {
|
|
|
1330
1655
|
start: call.getStart(),
|
|
1331
1656
|
end: call.getEnd()
|
|
1332
1657
|
});
|
|
1658
|
+
if (call && type === RECIPE_CALL_TYPE && !isShadowed(call, name)) {
|
|
1659
|
+
const start = call.getStart();
|
|
1660
|
+
const end = call.getEnd();
|
|
1661
|
+
const rangeKey = `${start}:${end}`;
|
|
1662
|
+
if (!reportedRanges.has(rangeKey)) {
|
|
1663
|
+
reportedRanges.add(rangeKey);
|
|
1664
|
+
if (code.slice(start, end) !== call.getText()) {
|
|
1665
|
+
skipped.push({
|
|
1666
|
+
name,
|
|
1667
|
+
reason: "no-call-expression",
|
|
1668
|
+
start: 0,
|
|
1669
|
+
end: 0
|
|
1670
|
+
});
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
recipeConfigs ??= collectRecipeConfigs(parserResult);
|
|
1674
|
+
const tally = recipeCalls.get(name) ?? {
|
|
1675
|
+
seen: 0,
|
|
1676
|
+
lowered: 0
|
|
1677
|
+
};
|
|
1678
|
+
tally.seen++;
|
|
1679
|
+
recipeCalls.set(name, tally);
|
|
1680
|
+
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1681
|
+
const entry = recipeConfigs.get(name);
|
|
1682
|
+
const lowered = lowerRecipeCall(call, entry, ctx, isInertExpression, resolvedSelection);
|
|
1683
|
+
if (lowered.kind === "expression") {
|
|
1684
|
+
const helper = ensureRecipeHelperImport(call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1685
|
+
if (helper) {
|
|
1686
|
+
tally.lowered++;
|
|
1687
|
+
candidates.push({
|
|
1688
|
+
item,
|
|
1689
|
+
call,
|
|
1690
|
+
node: call,
|
|
1691
|
+
start,
|
|
1692
|
+
end,
|
|
1693
|
+
replacement: helper.name === "cvaPick" ? lowered.expression : lowered.expression.replaceAll(`${RECIPE_PICK_HELPER}(`, `${helper.name}(`),
|
|
1694
|
+
className: lowered.staticClasses,
|
|
1695
|
+
classNames: lowered.classNames,
|
|
1696
|
+
insert: helper.insert,
|
|
1697
|
+
configBox: entry?.box
|
|
1698
|
+
});
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
skipped.push({
|
|
1702
|
+
name,
|
|
1703
|
+
reason: "recipe-call",
|
|
1704
|
+
start,
|
|
1705
|
+
end
|
|
1706
|
+
});
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
if (lowered.kind === "class") {
|
|
1710
|
+
tally.lowered++;
|
|
1711
|
+
candidates.push({
|
|
1712
|
+
item,
|
|
1713
|
+
call,
|
|
1714
|
+
node: call,
|
|
1715
|
+
start,
|
|
1716
|
+
end,
|
|
1717
|
+
replacement: JSON.stringify(lowered.className),
|
|
1718
|
+
className: lowered.className,
|
|
1719
|
+
classNames: lowered.className.split(" ").filter(Boolean),
|
|
1720
|
+
configBox: entry?.box
|
|
1721
|
+
});
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
skipped.push({
|
|
1725
|
+
name,
|
|
1726
|
+
reason: "recipe-call",
|
|
1727
|
+
start,
|
|
1728
|
+
end
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1333
1732
|
continue;
|
|
1334
1733
|
}
|
|
1335
1734
|
if (!call) {
|
|
@@ -1484,6 +1883,7 @@ const foldSource = (options) => {
|
|
|
1484
1883
|
end
|
|
1485
1884
|
});
|
|
1486
1885
|
collectSourceFiles(item.box, dependencyScan);
|
|
1886
|
+
if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
|
|
1487
1887
|
continue;
|
|
1488
1888
|
}
|
|
1489
1889
|
let className;
|
|
@@ -1532,6 +1932,16 @@ const foldSource = (options) => {
|
|
|
1532
1932
|
});
|
|
1533
1933
|
collectSourceFiles(item.box, dependencyScan);
|
|
1534
1934
|
}
|
|
1935
|
+
for (const [binding, tally] of recipeCalls) {
|
|
1936
|
+
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
1937
|
+
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
|
1938
|
+
if (!definition) continue;
|
|
1939
|
+
const call = ts_morph.Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression);
|
|
1940
|
+
if (!call) continue;
|
|
1941
|
+
const start = call.getStart();
|
|
1942
|
+
if (code.slice(start, call.getEnd()) !== call.getText()) continue;
|
|
1943
|
+
magic.appendLeft(start, "/*#__PURE__*/");
|
|
1944
|
+
}
|
|
1535
1945
|
if (folded.length === 0) return {
|
|
1536
1946
|
code,
|
|
1537
1947
|
map: null,
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,6 @@ import { Plugin } from "vite";
|
|
|
2
2
|
import { Context } from "@bamboocss/core";
|
|
3
3
|
import { Dict, ParserResultInterface } from "@bamboocss/types";
|
|
4
4
|
import MagicString from "magic-string";
|
|
5
|
-
|
|
6
5
|
//#region src/css.d.ts
|
|
7
6
|
/**
|
|
8
7
|
* What a project imports to get the stylesheet.
|
|
@@ -61,7 +60,7 @@ declare const createRuntimeCss: (ctx: Context) => RuntimeCss;
|
|
|
61
60
|
* Why a call site was left alone. Surfaced through `panda`-style diagnostics so a
|
|
62
61
|
* user can tell the difference between "this folded" and "this silently didn't".
|
|
63
62
|
*/
|
|
64
|
-
type SkipReason = 'dynamic' | 'raw-call' | 'not-foldable' | 'unsupported-kind' | 'not-imported' | 'no-call-expression' | 'overlapping' | 'empty' | 'unresolved-token';
|
|
63
|
+
type SkipReason = 'dynamic' | 'raw-call' | 'not-foldable' | 'recipe-call' | 'unsupported-kind' | 'not-imported' | 'no-call-expression' | 'overlapping' | 'empty' | 'unresolved-token';
|
|
65
64
|
interface FoldedCall {
|
|
66
65
|
name: string;
|
|
67
66
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MagicString from "magic-string";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
2
3
|
import { Context } from "@bamboocss/core";
|
|
3
4
|
import { Plugin } from "vite";
|
|
4
5
|
import { Dict, ParserResultInterface } from "@bamboocss/types";
|
|
@@ -61,7 +62,7 @@ declare const createRuntimeCss: (ctx: Context) => RuntimeCss;
|
|
|
61
62
|
* Why a call site was left alone. Surfaced through `panda`-style diagnostics so a
|
|
62
63
|
* user can tell the difference between "this folded" and "this silently didn't".
|
|
63
64
|
*/
|
|
64
|
-
type SkipReason = 'dynamic' | 'raw-call' | 'not-foldable' | 'unsupported-kind' | 'not-imported' | 'no-call-expression' | 'overlapping' | 'empty' | 'unresolved-token';
|
|
65
|
+
type SkipReason = 'dynamic' | 'raw-call' | 'not-foldable' | 'recipe-call' | 'unsupported-kind' | 'not-imported' | 'no-call-expression' | 'overlapping' | 'empty' | 'unresolved-token';
|
|
65
66
|
interface FoldedCall {
|
|
66
67
|
name: string;
|
|
67
68
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
|
|
|
4
4
|
import { box, maybeBoxNode, unbox } from "@bamboocss/extractor";
|
|
5
5
|
import MagicString from "magic-string";
|
|
6
6
|
import { Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
|
7
|
-
import { Recipes } from "@bamboocss/core";
|
|
8
|
-
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";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
//#region src/css.ts
|
|
11
11
|
/**
|
|
@@ -727,6 +727,313 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
727
727
|
};
|
|
728
728
|
};
|
|
729
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, isInert, 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
|
+
/**
|
|
884
|
+
* Variants whose expression could run something, in the order the source evaluates them.
|
|
885
|
+
*
|
|
886
|
+
* The text is kept, not just the key: a later property writing the same key replaces the
|
|
887
|
+
* entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
|
|
888
|
+
*/
|
|
889
|
+
const effectful = [];
|
|
890
|
+
if (args.length === 1) {
|
|
891
|
+
const arg = args[0];
|
|
892
|
+
if (!arg || !Node.isObjectLiteralExpression(arg)) return {
|
|
893
|
+
kind: "decline",
|
|
894
|
+
reason: "dynamic"
|
|
895
|
+
};
|
|
896
|
+
for (const property of arg.getProperties()) {
|
|
897
|
+
if (Node.isSpreadAssignment(property)) return {
|
|
898
|
+
kind: "decline",
|
|
899
|
+
reason: "dynamic"
|
|
900
|
+
};
|
|
901
|
+
if (Node.isShorthandPropertyAssignment(property)) {
|
|
902
|
+
dynamicAxes.set(property.getName(), property.getName());
|
|
903
|
+
delete selection[property.getName()];
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (!Node.isPropertyAssignment(property)) return {
|
|
907
|
+
kind: "decline",
|
|
908
|
+
reason: "dynamic"
|
|
909
|
+
};
|
|
910
|
+
const nameNode = property.getNameNode();
|
|
911
|
+
if (Node.isComputedPropertyName(nameNode)) return {
|
|
912
|
+
kind: "decline",
|
|
913
|
+
reason: "dynamic"
|
|
914
|
+
};
|
|
915
|
+
const key = propertyKey(nameNode);
|
|
916
|
+
if (key === void 0) return {
|
|
917
|
+
kind: "decline",
|
|
918
|
+
reason: "dynamic"
|
|
919
|
+
};
|
|
920
|
+
const initializer = property.getInitializer();
|
|
921
|
+
if (initializer && !isInert(initializer)) {
|
|
922
|
+
if (!Object.hasOwn(config.variants ?? {}, key)) return {
|
|
923
|
+
kind: "decline",
|
|
924
|
+
reason: "dynamic"
|
|
925
|
+
};
|
|
926
|
+
effectful.push({
|
|
927
|
+
key,
|
|
928
|
+
text: initializer.getText()
|
|
929
|
+
});
|
|
930
|
+
dynamicAxes.set(key, initializer.getText());
|
|
931
|
+
delete selection[key];
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const literal = literalValue(initializer);
|
|
935
|
+
if (literal !== void 0) {
|
|
936
|
+
selection[key] = literal;
|
|
937
|
+
dynamicAxes.delete(key);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
941
|
+
if (!initializer) return {
|
|
942
|
+
kind: "decline",
|
|
943
|
+
reason: "dynamic"
|
|
944
|
+
};
|
|
945
|
+
dynamicAxes.set(key, initializer.getText());
|
|
946
|
+
delete selection[key];
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const value = resolvedSelection[key];
|
|
950
|
+
if (value !== null && typeof value === "object") return {
|
|
951
|
+
kind: "decline",
|
|
952
|
+
reason: "dynamic"
|
|
953
|
+
};
|
|
954
|
+
selection[key] = value;
|
|
955
|
+
dynamicAxes.delete(key);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Every expression that could run something has to reach the output carrying its own text.
|
|
960
|
+
*
|
|
961
|
+
* A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
|
|
962
|
+
* tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
|
|
963
|
+
* literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
|
|
964
|
+
* typecheck and does transform `.js`, so this is reachable.
|
|
965
|
+
*/
|
|
966
|
+
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
967
|
+
const merged = {
|
|
968
|
+
...config.defaultVariants ?? {},
|
|
969
|
+
...compact(selection)
|
|
970
|
+
};
|
|
971
|
+
const format = classFormatter(ctx);
|
|
972
|
+
if (dynamicAxes.size === 0) {
|
|
973
|
+
if (!everyEffectSurvives()) return {
|
|
974
|
+
kind: "decline",
|
|
975
|
+
reason: "dynamic"
|
|
976
|
+
};
|
|
977
|
+
return {
|
|
978
|
+
kind: "class",
|
|
979
|
+
className: getRecipeClassNames(name, config.variants, merged, ctx.utility.separator, format)
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (!everyEffectSurvives()) return {
|
|
983
|
+
kind: "decline",
|
|
984
|
+
reason: "dynamic"
|
|
985
|
+
};
|
|
986
|
+
if (effectful.length > 1) {
|
|
987
|
+
const variantOrder = Object.keys(config.variants ?? {});
|
|
988
|
+
const keys = effectful.map((entry) => entry.key);
|
|
989
|
+
if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
|
|
990
|
+
kind: "decline",
|
|
991
|
+
reason: "dynamic"
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
995
|
+
if (dynamicAxes.size === 0) return {
|
|
996
|
+
kind: "class",
|
|
997
|
+
className: getRecipeClassNames(name, config.variants, merged, ctx.utility.separator, format)
|
|
998
|
+
};
|
|
999
|
+
const ownClass = format(name);
|
|
1000
|
+
const parts = [JSON.stringify(ownClass)];
|
|
1001
|
+
const classNames = [ownClass];
|
|
1002
|
+
for (const key of Object.keys(config.variants ?? {})) {
|
|
1003
|
+
const expression = dynamicAxes.get(key);
|
|
1004
|
+
if (expression === void 0) {
|
|
1005
|
+
const value = merged[key];
|
|
1006
|
+
if (value == null) continue;
|
|
1007
|
+
const declared = config.variants?.[key];
|
|
1008
|
+
if (!declared || !Object.hasOwn(declared, value) || declared[value] == null) continue;
|
|
1009
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${withoutSpace(value)}`);
|
|
1010
|
+
parts.push(JSON.stringify(` ${className}`));
|
|
1011
|
+
classNames.push(className);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const values = config.variants[key];
|
|
1015
|
+
const table = {};
|
|
1016
|
+
for (const value of Object.keys(values)) {
|
|
1017
|
+
const className = format(`${name}--${key}${ctx.utility.separator}${withoutSpace(value)}`);
|
|
1018
|
+
table[value] = ` ${className}`;
|
|
1019
|
+
classNames.push(className);
|
|
1020
|
+
}
|
|
1021
|
+
const fallbackValue = config.defaultVariants?.[key];
|
|
1022
|
+
const fallback = fallbackValue != null && values[fallbackValue] != null ? ` ${format(`${name}--${key}${ctx.utility.separator}${withoutSpace(fallbackValue)}`)}` : void 0;
|
|
1023
|
+
parts.push(`${HELPER}(${expression}, ${JSON.stringify(table)}${fallback === void 0 ? "" : `, ${JSON.stringify(fallback)}`})`);
|
|
1024
|
+
}
|
|
1025
|
+
if (parts.length === 1) return {
|
|
1026
|
+
kind: "class",
|
|
1027
|
+
className: ownClass
|
|
1028
|
+
};
|
|
1029
|
+
return {
|
|
1030
|
+
kind: "expression",
|
|
1031
|
+
expression: parts.join(" + "),
|
|
1032
|
+
classNames,
|
|
1033
|
+
staticClasses: ownClass
|
|
1034
|
+
};
|
|
1035
|
+
};
|
|
1036
|
+
//#endregion
|
|
730
1037
|
//#region src/runtime-css.ts
|
|
731
1038
|
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
732
1039
|
const createCssContext = (ctx) => ({
|
|
@@ -835,22 +1142,13 @@ const createRuntimeRecipe = (ctx) => {
|
|
|
835
1142
|
//#endregion
|
|
836
1143
|
//#region src/fold.ts
|
|
837
1144
|
/**
|
|
838
|
-
* `cva`/`sva` return a function, so neither can collapse to a class string.
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* through its own path rather than being declined outright.
|
|
842
|
-
*
|
|
843
|
-
* Folding an invocation is now *possible* in a way it was not: a recipe's classes are named
|
|
844
|
-
* semantically, so the build knows every class a call can produce from the config alone.
|
|
845
|
-
* What is missing is upstream — the parser matches calls by imported name, so a local
|
|
846
|
-
* `button()` from `const button = cva(...)` is never recorded, and tracking those bindings
|
|
847
|
-
* is a change to the extractor rather than to this set.
|
|
1145
|
+
* `cva`/`sva` return a function, so neither *definition* can collapse to a class string.
|
|
1146
|
+
* `token` also resolves to no class, but it does resolve to a literal, so it folds through
|
|
1147
|
+
* its own path rather than being declined outright.
|
|
848
1148
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* two implementations, not a measurement — benchmark it before deciding it is worth the
|
|
853
|
-
* extractor work.
|
|
1149
|
+
* Their invocations are a different matter and do fold — `cva`'s through `fold-recipe`,
|
|
1150
|
+
* which is a separate set because the call is recorded under the name the file bound rather
|
|
1151
|
+
* than the name it imported. `sva`'s do not: a slot recipe resolves to one class per slot.
|
|
854
1152
|
*/
|
|
855
1153
|
const FOLDABLE_TYPES = new Set([
|
|
856
1154
|
"css",
|
|
@@ -872,6 +1170,18 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
872
1170
|
*/
|
|
873
1171
|
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
874
1172
|
/**
|
|
1173
|
+
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
1174
|
+
*
|
|
1175
|
+
* Folded when the whole selection resolves, reported under this reason when it does not.
|
|
1176
|
+
* Deliberately all-or-nothing: an unresolved variant does not merely omit its own class, so a
|
|
1177
|
+
* partially-known selection is not foldable at all.
|
|
1178
|
+
*
|
|
1179
|
+
* Visible at all because it used to not be. The parser matched calls by imported name, so a
|
|
1180
|
+
* local binding was never recorded, and an unfoldable invocation looked identical to code
|
|
1181
|
+
* nothing had parsed.
|
|
1182
|
+
*/
|
|
1183
|
+
const RECIPE_CALL_TYPE = "cva-call";
|
|
1184
|
+
/**
|
|
875
1185
|
* An argument that cannot run anything when it is evaluated.
|
|
876
1186
|
*
|
|
877
1187
|
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
@@ -1190,6 +1500,21 @@ const foldSource = (options) => {
|
|
|
1190
1500
|
const skipped = [];
|
|
1191
1501
|
const candidates = [];
|
|
1192
1502
|
const seenRanges = /* @__PURE__ */ new Set();
|
|
1503
|
+
/** Built on first use: most modules declare no inline recipe. */
|
|
1504
|
+
let recipeConfigs;
|
|
1505
|
+
/**
|
|
1506
|
+
* Per inline recipe binding: calls seen, calls lowered.
|
|
1507
|
+
*
|
|
1508
|
+
* A binding whose every call lowered is no longer read, so its `cva({ … })` config can leave
|
|
1509
|
+
* the bundle — which is the whole point, the config being far larger than the runtime. But a
|
|
1510
|
+
* bundler will not drop the call on its own: `cva` closes over the config and builds an
|
|
1511
|
+
* object, and Rollup cannot prove that is side-effect free, so it keeps the expression and
|
|
1512
|
+
* the module ends up *larger* than before folding. The annotation below is what makes the
|
|
1513
|
+
* saving real, and it is only correct to claim it once nothing reads the binding.
|
|
1514
|
+
*/
|
|
1515
|
+
const recipeCalls = /* @__PURE__ */ new Map();
|
|
1516
|
+
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1517
|
+
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1193
1518
|
const importCache = /* @__PURE__ */ new Map();
|
|
1194
1519
|
const importsFor = (sourceFile) => {
|
|
1195
1520
|
let names = importCache.get(sourceFile);
|
|
@@ -1303,6 +1628,80 @@ const foldSource = (options) => {
|
|
|
1303
1628
|
start: call.getStart(),
|
|
1304
1629
|
end: call.getEnd()
|
|
1305
1630
|
});
|
|
1631
|
+
if (call && type === RECIPE_CALL_TYPE && !isShadowed(call, name)) {
|
|
1632
|
+
const start = call.getStart();
|
|
1633
|
+
const end = call.getEnd();
|
|
1634
|
+
const rangeKey = `${start}:${end}`;
|
|
1635
|
+
if (!reportedRanges.has(rangeKey)) {
|
|
1636
|
+
reportedRanges.add(rangeKey);
|
|
1637
|
+
if (code.slice(start, end) !== call.getText()) {
|
|
1638
|
+
skipped.push({
|
|
1639
|
+
name,
|
|
1640
|
+
reason: "no-call-expression",
|
|
1641
|
+
start: 0,
|
|
1642
|
+
end: 0
|
|
1643
|
+
});
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
recipeConfigs ??= collectRecipeConfigs(parserResult);
|
|
1647
|
+
const tally = recipeCalls.get(name) ?? {
|
|
1648
|
+
seen: 0,
|
|
1649
|
+
lowered: 0
|
|
1650
|
+
};
|
|
1651
|
+
tally.seen++;
|
|
1652
|
+
recipeCalls.set(name, tally);
|
|
1653
|
+
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1654
|
+
const entry = recipeConfigs.get(name);
|
|
1655
|
+
const lowered = lowerRecipeCall(call, entry, ctx, isInertExpression, resolvedSelection);
|
|
1656
|
+
if (lowered.kind === "expression") {
|
|
1657
|
+
const helper = ensureRecipeHelperImport(call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1658
|
+
if (helper) {
|
|
1659
|
+
tally.lowered++;
|
|
1660
|
+
candidates.push({
|
|
1661
|
+
item,
|
|
1662
|
+
call,
|
|
1663
|
+
node: call,
|
|
1664
|
+
start,
|
|
1665
|
+
end,
|
|
1666
|
+
replacement: helper.name === "cvaPick" ? lowered.expression : lowered.expression.replaceAll(`${RECIPE_PICK_HELPER}(`, `${helper.name}(`),
|
|
1667
|
+
className: lowered.staticClasses,
|
|
1668
|
+
classNames: lowered.classNames,
|
|
1669
|
+
insert: helper.insert,
|
|
1670
|
+
configBox: entry?.box
|
|
1671
|
+
});
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
skipped.push({
|
|
1675
|
+
name,
|
|
1676
|
+
reason: "recipe-call",
|
|
1677
|
+
start,
|
|
1678
|
+
end
|
|
1679
|
+
});
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (lowered.kind === "class") {
|
|
1683
|
+
tally.lowered++;
|
|
1684
|
+
candidates.push({
|
|
1685
|
+
item,
|
|
1686
|
+
call,
|
|
1687
|
+
node: call,
|
|
1688
|
+
start,
|
|
1689
|
+
end,
|
|
1690
|
+
replacement: JSON.stringify(lowered.className),
|
|
1691
|
+
className: lowered.className,
|
|
1692
|
+
classNames: lowered.className.split(" ").filter(Boolean),
|
|
1693
|
+
configBox: entry?.box
|
|
1694
|
+
});
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
skipped.push({
|
|
1698
|
+
name,
|
|
1699
|
+
reason: "recipe-call",
|
|
1700
|
+
start,
|
|
1701
|
+
end
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1306
1705
|
continue;
|
|
1307
1706
|
}
|
|
1308
1707
|
if (!call) {
|
|
@@ -1457,6 +1856,7 @@ const foldSource = (options) => {
|
|
|
1457
1856
|
end
|
|
1458
1857
|
});
|
|
1459
1858
|
collectSourceFiles(item.box, dependencyScan);
|
|
1859
|
+
if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
|
|
1460
1860
|
continue;
|
|
1461
1861
|
}
|
|
1462
1862
|
let className;
|
|
@@ -1505,6 +1905,16 @@ const foldSource = (options) => {
|
|
|
1505
1905
|
});
|
|
1506
1906
|
collectSourceFiles(item.box, dependencyScan);
|
|
1507
1907
|
}
|
|
1908
|
+
for (const [binding, tally] of recipeCalls) {
|
|
1909
|
+
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
1910
|
+
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
|
1911
|
+
if (!definition) continue;
|
|
1912
|
+
const call = Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(SyntaxKind.CallExpression);
|
|
1913
|
+
if (!call) continue;
|
|
1914
|
+
const start = call.getStart();
|
|
1915
|
+
if (code.slice(start, call.getEnd()) !== call.getText()) continue;
|
|
1916
|
+
magic.appendLeft(start, "/*#__PURE__*/");
|
|
1917
|
+
}
|
|
1508
1918
|
if (folded.length === 0) return {
|
|
1509
1919
|
code,
|
|
1510
1920
|
map: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Vite integration for Bamboo CSS",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"magic-string": "0.30.21",
|
|
39
39
|
"ts-morph": "28.0.0",
|
|
40
|
-
"@bamboocss/config": "1.
|
|
41
|
-
"@bamboocss/core": "1.
|
|
42
|
-
"@bamboocss/
|
|
43
|
-
"@bamboocss/
|
|
44
|
-
"@bamboocss/node": "1.
|
|
45
|
-
"@bamboocss/shared": "1.
|
|
46
|
-
"@bamboocss/types": "1.
|
|
40
|
+
"@bamboocss/config": "1.24.0",
|
|
41
|
+
"@bamboocss/core": "1.24.0",
|
|
42
|
+
"@bamboocss/extractor": "1.24.0",
|
|
43
|
+
"@bamboocss/logger": "1.24.0",
|
|
44
|
+
"@bamboocss/node": "1.24.0",
|
|
45
|
+
"@bamboocss/shared": "1.24.0",
|
|
46
|
+
"@bamboocss/types": "1.24.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
50
50
|
"vite": "7.2.6",
|
|
51
|
-
"@bamboocss/fixture": "1.
|
|
51
|
+
"@bamboocss/fixture": "1.24.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"vite": ">=5"
|