@bamboocss/vite 1.23.0 → 1.25.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 +116 -27
- package/dist/index.d.cts +0 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +116 -27
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -795,6 +795,8 @@ const collectRecipeConfigs = (parserResult) => {
|
|
|
795
795
|
};
|
|
796
796
|
/** The generated binding a lowered dynamic axis calls. Lives in `cx`, which pulls no engine. */
|
|
797
797
|
const RECIPE_PICK_HELPER = "cvaPick";
|
|
798
|
+
/** What `recipe.splitVariantProps` calls, reached directly once the call is lowered. */
|
|
799
|
+
const SPLIT_PROPS_HELPER = "splitProps";
|
|
798
800
|
const HELPER = RECIPE_PICK_HELPER;
|
|
799
801
|
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
800
802
|
const AMBIGUOUS = Object.freeze({
|
|
@@ -802,6 +804,9 @@ const AMBIGUOUS = Object.freeze({
|
|
|
802
804
|
name: "",
|
|
803
805
|
box: void 0
|
|
804
806
|
});
|
|
807
|
+
/** `.size`, or `["x-large"]` when the variant is not a valid identifier. */
|
|
808
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
809
|
+
const propertyAccess = (key) => IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
805
810
|
const LITERAL_KINDS = new Set([
|
|
806
811
|
ts_morph.SyntaxKind.StringLiteral,
|
|
807
812
|
ts_morph.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
@@ -845,7 +850,7 @@ const propertyKey = (nameNode) => {
|
|
|
845
850
|
* there is nothing to match — the host here is any import of the generated css module, which
|
|
846
851
|
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
847
852
|
*/
|
|
848
|
-
const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
853
|
+
const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
849
854
|
const sourceFile = call.getSourceFile();
|
|
850
855
|
let host;
|
|
851
856
|
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
@@ -853,7 +858,7 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
853
858
|
if (declaration.isTypeOnly()) continue;
|
|
854
859
|
for (const named of declaration.getNamedImports()) {
|
|
855
860
|
if (named.isTypeOnly()) continue;
|
|
856
|
-
if (named.getNameNode().getText() ===
|
|
861
|
+
if (named.getNameNode().getText() === imported) {
|
|
857
862
|
if (!isBambooCssModule(mod)) return void 0;
|
|
858
863
|
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
859
864
|
return isShadowed(call, local) ? void 0 : { name: local };
|
|
@@ -862,15 +867,15 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
862
867
|
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
863
868
|
}
|
|
864
869
|
if (!host) return void 0;
|
|
865
|
-
if (declaredAtModuleScope(sourceFile).has(
|
|
866
|
-
if (isShadowed(call,
|
|
870
|
+
if (declaredAtModuleScope(sourceFile).has(imported)) return void 0;
|
|
871
|
+
if (isShadowed(call, imported)) return void 0;
|
|
867
872
|
const last = host.getNamedImports().at(-1);
|
|
868
873
|
if (!last) return void 0;
|
|
869
874
|
return {
|
|
870
|
-
name:
|
|
875
|
+
name: imported,
|
|
871
876
|
insert: {
|
|
872
877
|
pos: last.getEnd(),
|
|
873
|
-
names: [
|
|
878
|
+
names: [imported]
|
|
874
879
|
}
|
|
875
880
|
};
|
|
876
881
|
};
|
|
@@ -881,7 +886,7 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
881
886
|
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
882
887
|
* recipe applies — so a partially-known selection is not foldable at all.
|
|
883
888
|
*/
|
|
884
|
-
const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
889
|
+
const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
885
890
|
if (!entry || entry === AMBIGUOUS) return {
|
|
886
891
|
kind: "decline",
|
|
887
892
|
reason: "unknown-recipe"
|
|
@@ -907,13 +912,37 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
907
912
|
const selection = {};
|
|
908
913
|
/** Variant → the source expression selecting it, for axes that stay runtime decisions. */
|
|
909
914
|
const dynamicAxes = /* @__PURE__ */ new Map();
|
|
915
|
+
/**
|
|
916
|
+
* Variants whose expression could run something, in the order the source evaluates them.
|
|
917
|
+
*
|
|
918
|
+
* The text is kept, not just the key: a later property writing the same key replaces the
|
|
919
|
+
* entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
|
|
920
|
+
*/
|
|
921
|
+
const effectful = [];
|
|
910
922
|
if (args.length === 1) {
|
|
911
923
|
const arg = args[0];
|
|
912
|
-
if (!arg
|
|
924
|
+
if (!arg) return {
|
|
913
925
|
kind: "decline",
|
|
914
926
|
reason: "dynamic"
|
|
915
927
|
};
|
|
916
|
-
|
|
928
|
+
/**
|
|
929
|
+
* `input(variantProps)` — a selection the build cannot see inside.
|
|
930
|
+
*
|
|
931
|
+
* The classes are still knowable: a recipe emits one per declared variant, so the call is
|
|
932
|
+
* one term per variant reading that binding. This is the shape a wrapper component takes,
|
|
933
|
+
* where the variants are the component's public API and cannot be literals by definition.
|
|
934
|
+
*
|
|
935
|
+
* An identifier only. Each variant reads the binding again, and re-reading anything else —
|
|
936
|
+
* a call, a property access — would evaluate it once per axis instead of once.
|
|
937
|
+
*/
|
|
938
|
+
if (ts_morph.Node.isIdentifier(arg)) {
|
|
939
|
+
const binding = arg.getText();
|
|
940
|
+
for (const key of Object.keys(config.variants ?? {})) dynamicAxes.set(key, `${binding}${propertyAccess(key)}`);
|
|
941
|
+
} else if (!ts_morph.Node.isObjectLiteralExpression(arg)) return {
|
|
942
|
+
kind: "decline",
|
|
943
|
+
reason: "dynamic"
|
|
944
|
+
};
|
|
945
|
+
else for (const property of arg.getProperties()) {
|
|
917
946
|
if (ts_morph.Node.isSpreadAssignment(property)) return {
|
|
918
947
|
kind: "decline",
|
|
919
948
|
reason: "dynamic"
|
|
@@ -937,14 +966,27 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
937
966
|
kind: "decline",
|
|
938
967
|
reason: "dynamic"
|
|
939
968
|
};
|
|
940
|
-
const
|
|
969
|
+
const initializer = property.getInitializer();
|
|
970
|
+
if (initializer && !isInert(initializer)) {
|
|
971
|
+
if (!Object.hasOwn(config.variants ?? {}, key)) return {
|
|
972
|
+
kind: "decline",
|
|
973
|
+
reason: "dynamic"
|
|
974
|
+
};
|
|
975
|
+
effectful.push({
|
|
976
|
+
key,
|
|
977
|
+
text: initializer.getText()
|
|
978
|
+
});
|
|
979
|
+
dynamicAxes.set(key, initializer.getText());
|
|
980
|
+
delete selection[key];
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
const literal = literalValue(initializer);
|
|
941
984
|
if (literal !== void 0) {
|
|
942
985
|
selection[key] = literal;
|
|
943
986
|
dynamicAxes.delete(key);
|
|
944
987
|
continue;
|
|
945
988
|
}
|
|
946
989
|
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
947
|
-
const initializer = property.getInitializer();
|
|
948
990
|
if (!initializer) return {
|
|
949
991
|
kind: "decline",
|
|
950
992
|
reason: "dynamic"
|
|
@@ -962,24 +1004,47 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
962
1004
|
dynamicAxes.delete(key);
|
|
963
1005
|
}
|
|
964
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Every expression that could run something has to reach the output carrying its own text.
|
|
1009
|
+
*
|
|
1010
|
+
* A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
|
|
1011
|
+
* tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
|
|
1012
|
+
* literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
|
|
1013
|
+
* typecheck and does transform `.js`, so this is reachable.
|
|
1014
|
+
*/
|
|
1015
|
+
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
965
1016
|
const merged = {
|
|
966
1017
|
...config.defaultVariants ?? {},
|
|
967
1018
|
...(0, _bamboocss_shared.compact)(selection)
|
|
968
1019
|
};
|
|
969
1020
|
const format = (0, _bamboocss_core.classFormatter)(ctx);
|
|
970
|
-
if (dynamicAxes.size === 0) return {
|
|
971
|
-
kind: "class",
|
|
972
|
-
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants, merged, ctx.utility.separator, format)
|
|
973
|
-
};
|
|
974
|
-
for (const key of [...dynamicAxes.keys()]) if (!config.variants?.[key]) dynamicAxes.delete(key);
|
|
975
1021
|
if (dynamicAxes.size === 0) {
|
|
976
|
-
|
|
977
|
-
|
|
1022
|
+
if (!everyEffectSurvives()) return {
|
|
1023
|
+
kind: "decline",
|
|
1024
|
+
reason: "dynamic"
|
|
1025
|
+
};
|
|
978
1026
|
return {
|
|
979
1027
|
kind: "class",
|
|
980
|
-
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants,
|
|
1028
|
+
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants, merged, ctx.utility.separator, format)
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (!everyEffectSurvives()) return {
|
|
1032
|
+
kind: "decline",
|
|
1033
|
+
reason: "dynamic"
|
|
1034
|
+
};
|
|
1035
|
+
if (effectful.length > 1) {
|
|
1036
|
+
const variantOrder = Object.keys(config.variants ?? {});
|
|
1037
|
+
const keys = effectful.map((entry) => entry.key);
|
|
1038
|
+
if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
|
|
1039
|
+
kind: "decline",
|
|
1040
|
+
reason: "dynamic"
|
|
981
1041
|
};
|
|
982
1042
|
}
|
|
1043
|
+
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
1044
|
+
if (dynamicAxes.size === 0) return {
|
|
1045
|
+
kind: "class",
|
|
1046
|
+
className: (0, _bamboocss_shared.getRecipeClassNames)(name, config.variants, merged, ctx.utility.separator, format)
|
|
1047
|
+
};
|
|
983
1048
|
const ownClass = format(name);
|
|
984
1049
|
const parts = [JSON.stringify(ownClass)];
|
|
985
1050
|
const classNames = [ownClass];
|
|
@@ -988,7 +1053,8 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
988
1053
|
if (expression === void 0) {
|
|
989
1054
|
const value = merged[key];
|
|
990
1055
|
if (value == null) continue;
|
|
991
|
-
|
|
1056
|
+
const declared = config.variants?.[key];
|
|
1057
|
+
if (!declared || !Object.hasOwn(declared, value) || declared[value] == null) continue;
|
|
992
1058
|
const className = format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(value)}`);
|
|
993
1059
|
parts.push(JSON.stringify(` ${className}`));
|
|
994
1060
|
classNames.push(className);
|
|
@@ -1371,7 +1437,7 @@ const hasStyles = (data) => data.length > 0 && data.every((entry) => entry != nu
|
|
|
1371
1437
|
const argumentsAccountedFor = (call, boxNode) => {
|
|
1372
1438
|
if (!ts_morph.Node.isCallExpression(call)) return false;
|
|
1373
1439
|
const args = call.getArguments();
|
|
1374
|
-
if (args.length === 0) return
|
|
1440
|
+
if (args.length === 0) return true;
|
|
1375
1441
|
if (_bamboocss_extractor.box.isArray(boxNode) && boxNode.getNode() === call) {
|
|
1376
1442
|
if (boxNode.value.length !== args.length) return false;
|
|
1377
1443
|
return args.every((arg, index) => accountsForSource(arg, boxNode.value[index]));
|
|
@@ -1496,6 +1562,8 @@ const foldSource = (options) => {
|
|
|
1496
1562
|
* saving real, and it is only correct to claim it once nothing reads the binding.
|
|
1497
1563
|
*/
|
|
1498
1564
|
const recipeCalls = /* @__PURE__ */ new Map();
|
|
1565
|
+
/** Bindings whose `splitVariantProps` was rewritten, so that access no longer reads them. */
|
|
1566
|
+
const loweredSplitProps = /* @__PURE__ */ new Set();
|
|
1499
1567
|
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1500
1568
|
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1501
1569
|
const importCache = /* @__PURE__ */ new Map();
|
|
@@ -1635,12 +1703,9 @@ const foldSource = (options) => {
|
|
|
1635
1703
|
recipeCalls.set(name, tally);
|
|
1636
1704
|
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1637
1705
|
const entry = recipeConfigs.get(name);
|
|
1638
|
-
const lowered =
|
|
1639
|
-
kind: "decline",
|
|
1640
|
-
reason: "dynamic"
|
|
1641
|
-
};
|
|
1706
|
+
const lowered = lowerRecipeCall(call, entry, ctx, isInertExpression, resolvedSelection);
|
|
1642
1707
|
if (lowered.kind === "expression") {
|
|
1643
|
-
const helper = ensureRecipeHelperImport(call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1708
|
+
const helper = ensureRecipeHelperImport(RECIPE_PICK_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1644
1709
|
if (helper) {
|
|
1645
1710
|
tally.lowered++;
|
|
1646
1711
|
candidates.push({
|
|
@@ -1751,7 +1816,7 @@ const foldSource = (options) => {
|
|
|
1751
1816
|
});
|
|
1752
1817
|
continue;
|
|
1753
1818
|
}
|
|
1754
|
-
if (!isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
1819
|
+
if (!(ts_morph.Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
1755
1820
|
const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
|
|
1756
1821
|
if (partial) {
|
|
1757
1822
|
candidates.push({
|
|
@@ -1891,6 +1956,30 @@ const foldSource = (options) => {
|
|
|
1891
1956
|
});
|
|
1892
1957
|
collectSourceFiles(item.box, dependencyScan);
|
|
1893
1958
|
}
|
|
1959
|
+
const recipeSourceFile = recipeConfigs?.size ? [...recipeConfigs.values()].find((entry) => entry.box)?.box?.getNode?.()?.getSourceFile() : void 0;
|
|
1960
|
+
if (recipeSourceFile) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
|
|
1961
|
+
if (access.getName() !== "splitVariantProps") continue;
|
|
1962
|
+
const target = access.getExpression();
|
|
1963
|
+
if (!ts_morph.Node.isIdentifier(target)) continue;
|
|
1964
|
+
const entry = recipeConfigs?.get(target.getText());
|
|
1965
|
+
if (!entry || entry === AMBIGUOUS) continue;
|
|
1966
|
+
if (isShadowed(access, target.getText())) continue;
|
|
1967
|
+
const call = access.getParent();
|
|
1968
|
+
if (!ts_morph.Node.isCallExpression(call) || call.getExpression() !== access) continue;
|
|
1969
|
+
const args = call.getArguments();
|
|
1970
|
+
if (args.length !== 1) continue;
|
|
1971
|
+
const start = call.getStart();
|
|
1972
|
+
const end = call.getEnd();
|
|
1973
|
+
if (code.slice(start, end) !== call.getText()) continue;
|
|
1974
|
+
if (collides([[start, end]])) continue;
|
|
1975
|
+
const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1976
|
+
if (!helper) continue;
|
|
1977
|
+
const keys = Object.keys(entry.config.variants ?? {});
|
|
1978
|
+
magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
|
|
1979
|
+
applyInsert(helper.insert);
|
|
1980
|
+
applied.push([start, end]);
|
|
1981
|
+
loweredSplitProps.add(target.getText());
|
|
1982
|
+
}
|
|
1894
1983
|
for (const [binding, tally] of recipeCalls) {
|
|
1895
1984
|
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
1896
1985
|
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -768,6 +768,8 @@ const collectRecipeConfigs = (parserResult) => {
|
|
|
768
768
|
};
|
|
769
769
|
/** The generated binding a lowered dynamic axis calls. Lives in `cx`, which pulls no engine. */
|
|
770
770
|
const RECIPE_PICK_HELPER = "cvaPick";
|
|
771
|
+
/** What `recipe.splitVariantProps` calls, reached directly once the call is lowered. */
|
|
772
|
+
const SPLIT_PROPS_HELPER = "splitProps";
|
|
771
773
|
const HELPER = RECIPE_PICK_HELPER;
|
|
772
774
|
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
773
775
|
const AMBIGUOUS = Object.freeze({
|
|
@@ -775,6 +777,9 @@ const AMBIGUOUS = Object.freeze({
|
|
|
775
777
|
name: "",
|
|
776
778
|
box: void 0
|
|
777
779
|
});
|
|
780
|
+
/** `.size`, or `["x-large"]` when the variant is not a valid identifier. */
|
|
781
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
782
|
+
const propertyAccess = (key) => IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
778
783
|
const LITERAL_KINDS = new Set([
|
|
779
784
|
SyntaxKind.StringLiteral,
|
|
780
785
|
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
@@ -818,7 +823,7 @@ const propertyKey = (nameNode) => {
|
|
|
818
823
|
* there is nothing to match — the host here is any import of the generated css module, which
|
|
819
824
|
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
820
825
|
*/
|
|
821
|
-
const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
826
|
+
const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
822
827
|
const sourceFile = call.getSourceFile();
|
|
823
828
|
let host;
|
|
824
829
|
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
@@ -826,7 +831,7 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
826
831
|
if (declaration.isTypeOnly()) continue;
|
|
827
832
|
for (const named of declaration.getNamedImports()) {
|
|
828
833
|
if (named.isTypeOnly()) continue;
|
|
829
|
-
if (named.getNameNode().getText() ===
|
|
834
|
+
if (named.getNameNode().getText() === imported) {
|
|
830
835
|
if (!isBambooCssModule(mod)) return void 0;
|
|
831
836
|
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
832
837
|
return isShadowed(call, local) ? void 0 : { name: local };
|
|
@@ -835,15 +840,15 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
835
840
|
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
836
841
|
}
|
|
837
842
|
if (!host) return void 0;
|
|
838
|
-
if (declaredAtModuleScope(sourceFile).has(
|
|
839
|
-
if (isShadowed(call,
|
|
843
|
+
if (declaredAtModuleScope(sourceFile).has(imported)) return void 0;
|
|
844
|
+
if (isShadowed(call, imported)) return void 0;
|
|
840
845
|
const last = host.getNamedImports().at(-1);
|
|
841
846
|
if (!last) return void 0;
|
|
842
847
|
return {
|
|
843
|
-
name:
|
|
848
|
+
name: imported,
|
|
844
849
|
insert: {
|
|
845
850
|
pos: last.getEnd(),
|
|
846
|
-
names: [
|
|
851
|
+
names: [imported]
|
|
847
852
|
}
|
|
848
853
|
};
|
|
849
854
|
};
|
|
@@ -854,7 +859,7 @@ const ensureRecipeHelperImport = (call, isBambooCssModule, isGeneratedCssModule,
|
|
|
854
859
|
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
855
860
|
* recipe applies — so a partially-known selection is not foldable at all.
|
|
856
861
|
*/
|
|
857
|
-
const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
862
|
+
const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
858
863
|
if (!entry || entry === AMBIGUOUS) return {
|
|
859
864
|
kind: "decline",
|
|
860
865
|
reason: "unknown-recipe"
|
|
@@ -880,13 +885,37 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
880
885
|
const selection = {};
|
|
881
886
|
/** Variant → the source expression selecting it, for axes that stay runtime decisions. */
|
|
882
887
|
const dynamicAxes = /* @__PURE__ */ new Map();
|
|
888
|
+
/**
|
|
889
|
+
* Variants whose expression could run something, in the order the source evaluates them.
|
|
890
|
+
*
|
|
891
|
+
* The text is kept, not just the key: a later property writing the same key replaces the
|
|
892
|
+
* entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
|
|
893
|
+
*/
|
|
894
|
+
const effectful = [];
|
|
883
895
|
if (args.length === 1) {
|
|
884
896
|
const arg = args[0];
|
|
885
|
-
if (!arg
|
|
897
|
+
if (!arg) return {
|
|
886
898
|
kind: "decline",
|
|
887
899
|
reason: "dynamic"
|
|
888
900
|
};
|
|
889
|
-
|
|
901
|
+
/**
|
|
902
|
+
* `input(variantProps)` — a selection the build cannot see inside.
|
|
903
|
+
*
|
|
904
|
+
* The classes are still knowable: a recipe emits one per declared variant, so the call is
|
|
905
|
+
* one term per variant reading that binding. This is the shape a wrapper component takes,
|
|
906
|
+
* where the variants are the component's public API and cannot be literals by definition.
|
|
907
|
+
*
|
|
908
|
+
* An identifier only. Each variant reads the binding again, and re-reading anything else —
|
|
909
|
+
* a call, a property access — would evaluate it once per axis instead of once.
|
|
910
|
+
*/
|
|
911
|
+
if (Node.isIdentifier(arg)) {
|
|
912
|
+
const binding = arg.getText();
|
|
913
|
+
for (const key of Object.keys(config.variants ?? {})) dynamicAxes.set(key, `${binding}${propertyAccess(key)}`);
|
|
914
|
+
} else if (!Node.isObjectLiteralExpression(arg)) return {
|
|
915
|
+
kind: "decline",
|
|
916
|
+
reason: "dynamic"
|
|
917
|
+
};
|
|
918
|
+
else for (const property of arg.getProperties()) {
|
|
890
919
|
if (Node.isSpreadAssignment(property)) return {
|
|
891
920
|
kind: "decline",
|
|
892
921
|
reason: "dynamic"
|
|
@@ -910,14 +939,27 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
910
939
|
kind: "decline",
|
|
911
940
|
reason: "dynamic"
|
|
912
941
|
};
|
|
913
|
-
const
|
|
942
|
+
const initializer = property.getInitializer();
|
|
943
|
+
if (initializer && !isInert(initializer)) {
|
|
944
|
+
if (!Object.hasOwn(config.variants ?? {}, key)) return {
|
|
945
|
+
kind: "decline",
|
|
946
|
+
reason: "dynamic"
|
|
947
|
+
};
|
|
948
|
+
effectful.push({
|
|
949
|
+
key,
|
|
950
|
+
text: initializer.getText()
|
|
951
|
+
});
|
|
952
|
+
dynamicAxes.set(key, initializer.getText());
|
|
953
|
+
delete selection[key];
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const literal = literalValue(initializer);
|
|
914
957
|
if (literal !== void 0) {
|
|
915
958
|
selection[key] = literal;
|
|
916
959
|
dynamicAxes.delete(key);
|
|
917
960
|
continue;
|
|
918
961
|
}
|
|
919
962
|
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
920
|
-
const initializer = property.getInitializer();
|
|
921
963
|
if (!initializer) return {
|
|
922
964
|
kind: "decline",
|
|
923
965
|
reason: "dynamic"
|
|
@@ -935,24 +977,47 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
935
977
|
dynamicAxes.delete(key);
|
|
936
978
|
}
|
|
937
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Every expression that could run something has to reach the output carrying its own text.
|
|
982
|
+
*
|
|
983
|
+
* A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
|
|
984
|
+
* tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
|
|
985
|
+
* literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
|
|
986
|
+
* typecheck and does transform `.js`, so this is reachable.
|
|
987
|
+
*/
|
|
988
|
+
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
938
989
|
const merged = {
|
|
939
990
|
...config.defaultVariants ?? {},
|
|
940
991
|
...compact(selection)
|
|
941
992
|
};
|
|
942
993
|
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
994
|
if (dynamicAxes.size === 0) {
|
|
949
|
-
|
|
950
|
-
|
|
995
|
+
if (!everyEffectSurvives()) return {
|
|
996
|
+
kind: "decline",
|
|
997
|
+
reason: "dynamic"
|
|
998
|
+
};
|
|
951
999
|
return {
|
|
952
1000
|
kind: "class",
|
|
953
|
-
className: getRecipeClassNames(name, config.variants,
|
|
1001
|
+
className: getRecipeClassNames(name, config.variants, merged, ctx.utility.separator, format)
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (!everyEffectSurvives()) return {
|
|
1005
|
+
kind: "decline",
|
|
1006
|
+
reason: "dynamic"
|
|
1007
|
+
};
|
|
1008
|
+
if (effectful.length > 1) {
|
|
1009
|
+
const variantOrder = Object.keys(config.variants ?? {});
|
|
1010
|
+
const keys = effectful.map((entry) => entry.key);
|
|
1011
|
+
if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
|
|
1012
|
+
kind: "decline",
|
|
1013
|
+
reason: "dynamic"
|
|
954
1014
|
};
|
|
955
1015
|
}
|
|
1016
|
+
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
1017
|
+
if (dynamicAxes.size === 0) return {
|
|
1018
|
+
kind: "class",
|
|
1019
|
+
className: getRecipeClassNames(name, config.variants, merged, ctx.utility.separator, format)
|
|
1020
|
+
};
|
|
956
1021
|
const ownClass = format(name);
|
|
957
1022
|
const parts = [JSON.stringify(ownClass)];
|
|
958
1023
|
const classNames = [ownClass];
|
|
@@ -961,7 +1026,8 @@ const lowerRecipeCall = (call, entry, ctx, resolvedSelection) => {
|
|
|
961
1026
|
if (expression === void 0) {
|
|
962
1027
|
const value = merged[key];
|
|
963
1028
|
if (value == null) continue;
|
|
964
|
-
|
|
1029
|
+
const declared = config.variants?.[key];
|
|
1030
|
+
if (!declared || !Object.hasOwn(declared, value) || declared[value] == null) continue;
|
|
965
1031
|
const className = format(`${name}--${key}${ctx.utility.separator}${withoutSpace(value)}`);
|
|
966
1032
|
parts.push(JSON.stringify(` ${className}`));
|
|
967
1033
|
classNames.push(className);
|
|
@@ -1344,7 +1410,7 @@ const hasStyles = (data) => data.length > 0 && data.every((entry) => entry != nu
|
|
|
1344
1410
|
const argumentsAccountedFor = (call, boxNode) => {
|
|
1345
1411
|
if (!Node.isCallExpression(call)) return false;
|
|
1346
1412
|
const args = call.getArguments();
|
|
1347
|
-
if (args.length === 0) return
|
|
1413
|
+
if (args.length === 0) return true;
|
|
1348
1414
|
if (box.isArray(boxNode) && boxNode.getNode() === call) {
|
|
1349
1415
|
if (boxNode.value.length !== args.length) return false;
|
|
1350
1416
|
return args.every((arg, index) => accountsForSource(arg, boxNode.value[index]));
|
|
@@ -1469,6 +1535,8 @@ const foldSource = (options) => {
|
|
|
1469
1535
|
* saving real, and it is only correct to claim it once nothing reads the binding.
|
|
1470
1536
|
*/
|
|
1471
1537
|
const recipeCalls = /* @__PURE__ */ new Map();
|
|
1538
|
+
/** Bindings whose `splitVariantProps` was rewritten, so that access no longer reads them. */
|
|
1539
|
+
const loweredSplitProps = /* @__PURE__ */ new Set();
|
|
1472
1540
|
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1473
1541
|
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1474
1542
|
const importCache = /* @__PURE__ */ new Map();
|
|
@@ -1608,12 +1676,9 @@ const foldSource = (options) => {
|
|
|
1608
1676
|
recipeCalls.set(name, tally);
|
|
1609
1677
|
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1610
1678
|
const entry = recipeConfigs.get(name);
|
|
1611
|
-
const lowered =
|
|
1612
|
-
kind: "decline",
|
|
1613
|
-
reason: "dynamic"
|
|
1614
|
-
};
|
|
1679
|
+
const lowered = lowerRecipeCall(call, entry, ctx, isInertExpression, resolvedSelection);
|
|
1615
1680
|
if (lowered.kind === "expression") {
|
|
1616
|
-
const helper = ensureRecipeHelperImport(call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1681
|
+
const helper = ensureRecipeHelperImport(RECIPE_PICK_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1617
1682
|
if (helper) {
|
|
1618
1683
|
tally.lowered++;
|
|
1619
1684
|
candidates.push({
|
|
@@ -1724,7 +1789,7 @@ const foldSource = (options) => {
|
|
|
1724
1789
|
});
|
|
1725
1790
|
continue;
|
|
1726
1791
|
}
|
|
1727
|
-
if (!isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
1792
|
+
if (!(Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
1728
1793
|
const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
|
|
1729
1794
|
if (partial) {
|
|
1730
1795
|
candidates.push({
|
|
@@ -1864,6 +1929,30 @@ const foldSource = (options) => {
|
|
|
1864
1929
|
});
|
|
1865
1930
|
collectSourceFiles(item.box, dependencyScan);
|
|
1866
1931
|
}
|
|
1932
|
+
const recipeSourceFile = recipeConfigs?.size ? [...recipeConfigs.values()].find((entry) => entry.box)?.box?.getNode?.()?.getSourceFile() : void 0;
|
|
1933
|
+
if (recipeSourceFile) for (const access of recipeSourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
1934
|
+
if (access.getName() !== "splitVariantProps") continue;
|
|
1935
|
+
const target = access.getExpression();
|
|
1936
|
+
if (!Node.isIdentifier(target)) continue;
|
|
1937
|
+
const entry = recipeConfigs?.get(target.getText());
|
|
1938
|
+
if (!entry || entry === AMBIGUOUS) continue;
|
|
1939
|
+
if (isShadowed(access, target.getText())) continue;
|
|
1940
|
+
const call = access.getParent();
|
|
1941
|
+
if (!Node.isCallExpression(call) || call.getExpression() !== access) continue;
|
|
1942
|
+
const args = call.getArguments();
|
|
1943
|
+
if (args.length !== 1) continue;
|
|
1944
|
+
const start = call.getStart();
|
|
1945
|
+
const end = call.getEnd();
|
|
1946
|
+
if (code.slice(start, end) !== call.getText()) continue;
|
|
1947
|
+
if (collides([[start, end]])) continue;
|
|
1948
|
+
const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1949
|
+
if (!helper) continue;
|
|
1950
|
+
const keys = Object.keys(entry.config.variants ?? {});
|
|
1951
|
+
magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
|
|
1952
|
+
applyInsert(helper.insert);
|
|
1953
|
+
applied.push([start, end]);
|
|
1954
|
+
loweredSplitProps.add(target.getText());
|
|
1955
|
+
}
|
|
1867
1956
|
for (const [binding, tally] of recipeCalls) {
|
|
1868
1957
|
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
1869
1958
|
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.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/
|
|
41
|
-
"@bamboocss/
|
|
42
|
-
"@bamboocss/
|
|
43
|
-
"@bamboocss/
|
|
44
|
-
"@bamboocss/
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/
|
|
40
|
+
"@bamboocss/config": "1.25.0",
|
|
41
|
+
"@bamboocss/core": "1.25.0",
|
|
42
|
+
"@bamboocss/extractor": "1.25.0",
|
|
43
|
+
"@bamboocss/logger": "1.25.0",
|
|
44
|
+
"@bamboocss/node": "1.25.0",
|
|
45
|
+
"@bamboocss/types": "1.25.0",
|
|
46
|
+
"@bamboocss/shared": "1.25.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.25.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"vite": ">=5"
|