@bamboocss/vite 1.16.1 → 1.17.1

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 CHANGED
@@ -29,6 +29,7 @@ let _bamboocss_extractor = require("@bamboocss/extractor");
29
29
  let magic_string = require("magic-string");
30
30
  magic_string = __toESM(magic_string);
31
31
  let ts_morph = require("ts-morph");
32
+ let _bamboocss_core = require("@bamboocss/core");
32
33
  let _bamboocss_shared = require("@bamboocss/shared");
33
34
  let node_path = require("node:path");
34
35
  let _bamboocss_logger = require("@bamboocss/logger");
@@ -718,15 +719,35 @@ const createRuntimeToken = (ctx) => (path) => {
718
719
  const value = tokenValuesFor(ctx).get(path);
719
720
  return typeof value === "string" ? value : void 0;
720
721
  };
722
+ /**
723
+ * Whether a slot's class is independent of the variant props.
724
+ *
725
+ * A scoped slot recipe delivers variants through `@scope` rules anchored on an enclosing
726
+ * slot, so every *other* slot carries a constant class — the same string whatever the props
727
+ * are. That is what makes `recipe(anything).slot` foldable even when the variant is fully
728
+ * dynamic, which was not true before scoping existed.
729
+ */
730
+ const createConstantSlotCheck = (ctx) => (name, slot) => {
731
+ const config = ctx.recipes.getConfig(name);
732
+ if (!config || !("slots" in config)) return false;
733
+ if (!config.slots.includes(slot)) return false;
734
+ const anchors = _bamboocss_core.Recipes.getScopeRoots(config);
735
+ return anchors.length > 0 && !anchors.includes(slot);
736
+ };
721
737
  const createRuntimeRecipe = (ctx) => {
722
738
  const separator = ctx.utility.separator;
723
- return (name, variants) => {
739
+ return (name, variants, slot) => {
724
740
  const config = ctx.recipes.getConfig(name);
725
741
  const node = ctx.recipes.getRecipe(name);
726
742
  if (!config || !node) return void 0;
727
- if ("slots" in config) return void 0;
728
- const className = node.className;
729
- const { defaultVariants = {}, compoundVariants = [] } = config;
743
+ const isSlotRecipe = "slots" in config;
744
+ if (isSlotRecipe !== Boolean(slot)) return void 0;
745
+ if (slot && !config.slots.includes(slot)) return void 0;
746
+ const anchors = isSlotRecipe ? _bamboocss_core.Recipes.getScopeRoots(config) : [];
747
+ const isConstantSlot = Boolean(slot) && anchors.length > 0 && !anchors.includes(slot);
748
+ const className = slot ? ctx.recipes.getSlotKey(node.className, slot) : node.className;
749
+ const { defaultVariants = {} } = config;
750
+ const compoundVariants = slot ? (0, _bamboocss_shared.getSlotCompoundVariant)(config.compoundVariants ?? [], slot) : config.compoundVariants ?? [];
730
751
  const recipeCss = (0, _bamboocss_shared.createCss)({
731
752
  hash: Boolean(ctx.hash.className),
732
753
  conditions: {
@@ -745,12 +766,16 @@ const createRuntimeRecipe = (ctx) => {
745
766
  }
746
767
  }
747
768
  });
748
- const recipeStyles = {
769
+ const recipeStyles = isConstantSlot ? { [className]: "__ignore__" } : {
749
770
  [className]: "__ignore__",
750
771
  ...defaultVariants,
751
772
  ...(0, _bamboocss_shared.compact)(variants)
752
773
  };
753
774
  if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
775
+ if (isSlotRecipe) {
776
+ const evaluated = anchors.length > 0 ? anchors : config.slots;
777
+ if (Object.values(variants).some((value) => typeof value === "object" && value !== null) && evaluated.some((slotName) => (0, _bamboocss_shared.getSlotCompoundVariant)(config.compoundVariants ?? [], slotName).length > 0)) return void 0;
778
+ }
754
779
  return recipeCss(recipeStyles);
755
780
  };
756
781
  };
@@ -803,6 +828,39 @@ const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
803
828
  */
804
829
  const isInertArgument = (node) => ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNumericLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node) || node.getKind() === ts_morph.SyntaxKind.TrueKeyword || node.getKind() === ts_morph.SyntaxKind.FalseKeyword || node.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isIdentifier(node) && node.getText() === "undefined";
805
830
  /**
831
+ * An expression whose evaluation cannot do anything observable, so deleting it preserves
832
+ * behaviour.
833
+ *
834
+ * `isInertArgument` covers the leaves; this walks the object and array literals a recipe
835
+ * call is actually written with. A spread runs the source's getters, a computed key runs an
836
+ * expression, a getter or method definition is a function — none of those are safe to
837
+ * delete, so they are declined rather than enumerated.
838
+ */
839
+ const isInertExpression = (node) => {
840
+ if (isInertArgument(node)) return true;
841
+ if (ts_morph.Node.isIdentifier(node)) return true;
842
+ if (ts_morph.Node.isAsExpression(node) || ts_morph.Node.isSatisfiesExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isParenthesizedExpression(node)) return isInertExpression(node.getExpression());
843
+ if (ts_morph.Node.isArrowFunction(node) || ts_morph.Node.isFunctionExpression(node)) return true;
844
+ if (ts_morph.Node.isRegularExpressionLiteral(node) || ts_morph.Node.isBigIntLiteral(node)) return true;
845
+ if (ts_morph.Node.isPrefixUnaryExpression(node)) {
846
+ const operator = node.getOperatorToken();
847
+ return (operator === ts_morph.SyntaxKind.MinusToken || operator === ts_morph.SyntaxKind.PlusToken || operator === ts_morph.SyntaxKind.ExclamationToken || operator === ts_morph.SyntaxKind.TildeToken) && isInertExpression(node.getOperand());
848
+ }
849
+ if (ts_morph.Node.isBinaryExpression(node)) {
850
+ const operator = node.getOperatorToken().getKind();
851
+ return (operator === ts_morph.SyntaxKind.QuestionQuestionToken || operator === ts_morph.SyntaxKind.BarBarToken || operator === ts_morph.SyntaxKind.AmpersandAmpersandToken) && isInertExpression(node.getLeft()) && isInertExpression(node.getRight());
852
+ }
853
+ if (ts_morph.Node.isObjectLiteralExpression(node)) return node.getProperties().every((property) => {
854
+ if (ts_morph.Node.isShorthandPropertyAssignment(property)) return true;
855
+ if (!ts_morph.Node.isPropertyAssignment(property)) return false;
856
+ if (ts_morph.Node.isComputedPropertyName(property.getNameNode())) return false;
857
+ const initializer = property.getInitializer();
858
+ return initializer !== void 0 && isInertExpression(initializer);
859
+ });
860
+ if (ts_morph.Node.isArrayLiteralExpression(node)) return node.getElements().every(isInertExpression);
861
+ return false;
862
+ };
863
+ /**
806
864
  * Source files a box tree reaches, other than the one being folded.
807
865
  *
808
866
  * When the extractor resolves an imported identifier it boxes the *declaration's*
@@ -1032,6 +1090,7 @@ const foldSource = (options) => {
1032
1090
  };
1033
1091
  };
1034
1092
  const runtimeRecipe = createRuntimeRecipe(ctx);
1093
+ const isConstantSlot = createConstantSlotCheck(ctx);
1035
1094
  const runtimeToken = createRuntimeToken(ctx);
1036
1095
  /**
1037
1096
  * Does this specifier name a module that exports the css API, exactly?
@@ -1204,6 +1263,13 @@ const foldSource = (options) => {
1204
1263
  }
1205
1264
  const start = call.getStart();
1206
1265
  const end = call.getEnd();
1266
+ const memberParent = call.getParent();
1267
+ const accessed = type === "recipe" && ts_morph.Node.isPropertyAccessExpression(memberParent) && memberParent.getExpression() === call ? memberParent : void 0;
1268
+ const accessedName = accessed?.getNameNode().getText();
1269
+ const declaredSlots = accessed ? ctx.recipes.getConfig(name)?.slots ?? [] : [];
1270
+ const memberAccess = accessedName && declaredSlots.includes(accessedName) ? accessed : void 0;
1271
+ const slot = memberAccess ? accessedName : void 0;
1272
+ const foldEnd = memberAccess ? memberAccess.getEnd() : end;
1207
1273
  if (code.slice(start, end) !== call.getText()) {
1208
1274
  skipped.push({
1209
1275
  name,
@@ -1213,7 +1279,7 @@ const foldSource = (options) => {
1213
1279
  });
1214
1280
  continue;
1215
1281
  }
1216
- const rangeKey = `${start}:${end}`;
1282
+ const rangeKey = `${start}:${foldEnd}`;
1217
1283
  if (seenRanges.has(rangeKey)) continue;
1218
1284
  seenRanges.add(rangeKey);
1219
1285
  if (isRawCall(call)) {
@@ -1235,6 +1301,18 @@ const foldSource = (options) => {
1235
1301
  });
1236
1302
  continue;
1237
1303
  }
1304
+ if (slot && isConstantSlot(name, slot) && ts_morph.Node.isCallExpression(call) && call.getArguments().every(isInertExpression)) {
1305
+ candidates.push({
1306
+ item,
1307
+ call,
1308
+ node: call,
1309
+ start,
1310
+ end: foldEnd,
1311
+ slot,
1312
+ constantSlot: true
1313
+ });
1314
+ continue;
1315
+ }
1238
1316
  if (!isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
1239
1317
  const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
1240
1318
  if (partial) {
@@ -1261,7 +1339,8 @@ const foldSource = (options) => {
1261
1339
  call,
1262
1340
  node: call,
1263
1341
  start,
1264
- end
1342
+ end: foldEnd,
1343
+ slot
1265
1344
  });
1266
1345
  }
1267
1346
  if (candidates.length === 0) return {
@@ -1331,7 +1410,7 @@ const foldSource = (options) => {
1331
1410
  try {
1332
1411
  if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
1333
1412
  else if (item.type === "recipe") {
1334
- const resolved = item.data.length === 1 ? runtimeRecipe(name, item.data[0]) : void 0;
1413
+ const resolved = candidate.constantSlot ? runtimeRecipe(name, {}, candidate.slot) : item.data.length === 1 ? runtimeRecipe(name, item.data[0], candidate.slot) : void 0;
1335
1414
  if (resolved == null) {
1336
1415
  skipped.push({
1337
1416
  name,
package/dist/index.mjs CHANGED
@@ -2,7 +2,8 @@ import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
2
2
  import { box, maybeBoxNode, unbox } from "@bamboocss/extractor";
3
3
  import MagicString from "magic-string";
4
4
  import { Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
5
- import { compact, createCss, createMergeCss, withoutSpace } from "@bamboocss/shared";
5
+ import { Recipes } from "@bamboocss/core";
6
+ import { compact, createCss, createMergeCss, getSlotCompoundVariant, withoutSpace } from "@bamboocss/shared";
6
7
  import { resolve } from "node:path";
7
8
  import { logger } from "@bamboocss/logger";
8
9
  import { loadConfigAndCreateContext } from "@bamboocss/node";
@@ -691,15 +692,35 @@ const createRuntimeToken = (ctx) => (path) => {
691
692
  const value = tokenValuesFor(ctx).get(path);
692
693
  return typeof value === "string" ? value : void 0;
693
694
  };
695
+ /**
696
+ * Whether a slot's class is independent of the variant props.
697
+ *
698
+ * A scoped slot recipe delivers variants through `@scope` rules anchored on an enclosing
699
+ * slot, so every *other* slot carries a constant class — the same string whatever the props
700
+ * are. That is what makes `recipe(anything).slot` foldable even when the variant is fully
701
+ * dynamic, which was not true before scoping existed.
702
+ */
703
+ const createConstantSlotCheck = (ctx) => (name, slot) => {
704
+ const config = ctx.recipes.getConfig(name);
705
+ if (!config || !("slots" in config)) return false;
706
+ if (!config.slots.includes(slot)) return false;
707
+ const anchors = Recipes.getScopeRoots(config);
708
+ return anchors.length > 0 && !anchors.includes(slot);
709
+ };
694
710
  const createRuntimeRecipe = (ctx) => {
695
711
  const separator = ctx.utility.separator;
696
- return (name, variants) => {
712
+ return (name, variants, slot) => {
697
713
  const config = ctx.recipes.getConfig(name);
698
714
  const node = ctx.recipes.getRecipe(name);
699
715
  if (!config || !node) return void 0;
700
- if ("slots" in config) return void 0;
701
- const className = node.className;
702
- const { defaultVariants = {}, compoundVariants = [] } = config;
716
+ const isSlotRecipe = "slots" in config;
717
+ if (isSlotRecipe !== Boolean(slot)) return void 0;
718
+ if (slot && !config.slots.includes(slot)) return void 0;
719
+ const anchors = isSlotRecipe ? Recipes.getScopeRoots(config) : [];
720
+ const isConstantSlot = Boolean(slot) && anchors.length > 0 && !anchors.includes(slot);
721
+ const className = slot ? ctx.recipes.getSlotKey(node.className, slot) : node.className;
722
+ const { defaultVariants = {} } = config;
723
+ const compoundVariants = slot ? getSlotCompoundVariant(config.compoundVariants ?? [], slot) : config.compoundVariants ?? [];
703
724
  const recipeCss = createCss({
704
725
  hash: Boolean(ctx.hash.className),
705
726
  conditions: {
@@ -718,12 +739,16 @@ const createRuntimeRecipe = (ctx) => {
718
739
  }
719
740
  }
720
741
  });
721
- const recipeStyles = {
742
+ const recipeStyles = isConstantSlot ? { [className]: "__ignore__" } : {
722
743
  [className]: "__ignore__",
723
744
  ...defaultVariants,
724
745
  ...compact(variants)
725
746
  };
726
747
  if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
748
+ if (isSlotRecipe) {
749
+ const evaluated = anchors.length > 0 ? anchors : config.slots;
750
+ if (Object.values(variants).some((value) => typeof value === "object" && value !== null) && evaluated.some((slotName) => getSlotCompoundVariant(config.compoundVariants ?? [], slotName).length > 0)) return void 0;
751
+ }
727
752
  return recipeCss(recipeStyles);
728
753
  };
729
754
  };
@@ -776,6 +801,39 @@ const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
776
801
  */
777
802
  const isInertArgument = (node) => Node.isStringLiteral(node) || Node.isNumericLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node) || node.getKind() === SyntaxKind.TrueKeyword || node.getKind() === SyntaxKind.FalseKeyword || node.getKind() === SyntaxKind.NullKeyword || Node.isIdentifier(node) && node.getText() === "undefined";
778
803
  /**
804
+ * An expression whose evaluation cannot do anything observable, so deleting it preserves
805
+ * behaviour.
806
+ *
807
+ * `isInertArgument` covers the leaves; this walks the object and array literals a recipe
808
+ * call is actually written with. A spread runs the source's getters, a computed key runs an
809
+ * expression, a getter or method definition is a function — none of those are safe to
810
+ * delete, so they are declined rather than enumerated.
811
+ */
812
+ const isInertExpression = (node) => {
813
+ if (isInertArgument(node)) return true;
814
+ if (Node.isIdentifier(node)) return true;
815
+ if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node) || Node.isNonNullExpression(node) || Node.isTypeAssertion(node) || Node.isParenthesizedExpression(node)) return isInertExpression(node.getExpression());
816
+ if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) return true;
817
+ if (Node.isRegularExpressionLiteral(node) || Node.isBigIntLiteral(node)) return true;
818
+ if (Node.isPrefixUnaryExpression(node)) {
819
+ const operator = node.getOperatorToken();
820
+ return (operator === SyntaxKind.MinusToken || operator === SyntaxKind.PlusToken || operator === SyntaxKind.ExclamationToken || operator === SyntaxKind.TildeToken) && isInertExpression(node.getOperand());
821
+ }
822
+ if (Node.isBinaryExpression(node)) {
823
+ const operator = node.getOperatorToken().getKind();
824
+ return (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.AmpersandAmpersandToken) && isInertExpression(node.getLeft()) && isInertExpression(node.getRight());
825
+ }
826
+ if (Node.isObjectLiteralExpression(node)) return node.getProperties().every((property) => {
827
+ if (Node.isShorthandPropertyAssignment(property)) return true;
828
+ if (!Node.isPropertyAssignment(property)) return false;
829
+ if (Node.isComputedPropertyName(property.getNameNode())) return false;
830
+ const initializer = property.getInitializer();
831
+ return initializer !== void 0 && isInertExpression(initializer);
832
+ });
833
+ if (Node.isArrayLiteralExpression(node)) return node.getElements().every(isInertExpression);
834
+ return false;
835
+ };
836
+ /**
779
837
  * Source files a box tree reaches, other than the one being folded.
780
838
  *
781
839
  * When the extractor resolves an imported identifier it boxes the *declaration's*
@@ -1005,6 +1063,7 @@ const foldSource = (options) => {
1005
1063
  };
1006
1064
  };
1007
1065
  const runtimeRecipe = createRuntimeRecipe(ctx);
1066
+ const isConstantSlot = createConstantSlotCheck(ctx);
1008
1067
  const runtimeToken = createRuntimeToken(ctx);
1009
1068
  /**
1010
1069
  * Does this specifier name a module that exports the css API, exactly?
@@ -1177,6 +1236,13 @@ const foldSource = (options) => {
1177
1236
  }
1178
1237
  const start = call.getStart();
1179
1238
  const end = call.getEnd();
1239
+ const memberParent = call.getParent();
1240
+ const accessed = type === "recipe" && Node.isPropertyAccessExpression(memberParent) && memberParent.getExpression() === call ? memberParent : void 0;
1241
+ const accessedName = accessed?.getNameNode().getText();
1242
+ const declaredSlots = accessed ? ctx.recipes.getConfig(name)?.slots ?? [] : [];
1243
+ const memberAccess = accessedName && declaredSlots.includes(accessedName) ? accessed : void 0;
1244
+ const slot = memberAccess ? accessedName : void 0;
1245
+ const foldEnd = memberAccess ? memberAccess.getEnd() : end;
1180
1246
  if (code.slice(start, end) !== call.getText()) {
1181
1247
  skipped.push({
1182
1248
  name,
@@ -1186,7 +1252,7 @@ const foldSource = (options) => {
1186
1252
  });
1187
1253
  continue;
1188
1254
  }
1189
- const rangeKey = `${start}:${end}`;
1255
+ const rangeKey = `${start}:${foldEnd}`;
1190
1256
  if (seenRanges.has(rangeKey)) continue;
1191
1257
  seenRanges.add(rangeKey);
1192
1258
  if (isRawCall(call)) {
@@ -1208,6 +1274,18 @@ const foldSource = (options) => {
1208
1274
  });
1209
1275
  continue;
1210
1276
  }
1277
+ if (slot && isConstantSlot(name, slot) && Node.isCallExpression(call) && call.getArguments().every(isInertExpression)) {
1278
+ candidates.push({
1279
+ item,
1280
+ call,
1281
+ node: call,
1282
+ start,
1283
+ end: foldEnd,
1284
+ slot,
1285
+ constantSlot: true
1286
+ });
1287
+ continue;
1288
+ }
1211
1289
  if (!isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
1212
1290
  const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
1213
1291
  if (partial) {
@@ -1234,7 +1312,8 @@ const foldSource = (options) => {
1234
1312
  call,
1235
1313
  node: call,
1236
1314
  start,
1237
- end
1315
+ end: foldEnd,
1316
+ slot
1238
1317
  });
1239
1318
  }
1240
1319
  if (candidates.length === 0) return {
@@ -1304,7 +1383,7 @@ const foldSource = (options) => {
1304
1383
  try {
1305
1384
  if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
1306
1385
  else if (item.type === "recipe") {
1307
- const resolved = item.data.length === 1 ? runtimeRecipe(name, item.data[0]) : void 0;
1386
+ const resolved = candidate.constantSlot ? runtimeRecipe(name, {}, candidate.slot) : item.data.length === 1 ? runtimeRecipe(name, item.data[0], candidate.slot) : void 0;
1308
1387
  if (resolved == null) {
1309
1388
  skipped.push({
1310
1389
  name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.16.1",
3
+ "version": "1.17.1",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -35,18 +35,18 @@
35
35
  "dependencies": {
36
36
  "magic-string": "0.30.21",
37
37
  "ts-morph": "28.0.0",
38
- "@bamboocss/config": "1.16.1",
39
- "@bamboocss/extractor": "1.16.1",
40
- "@bamboocss/logger": "1.16.1",
41
- "@bamboocss/node": "1.16.1",
42
- "@bamboocss/shared": "1.16.1",
43
- "@bamboocss/types": "1.16.1",
44
- "@bamboocss/core": "1.16.1"
38
+ "@bamboocss/config": "1.17.1",
39
+ "@bamboocss/core": "1.17.1",
40
+ "@bamboocss/extractor": "1.17.1",
41
+ "@bamboocss/logger": "1.17.1",
42
+ "@bamboocss/node": "1.17.1",
43
+ "@bamboocss/shared": "1.17.1",
44
+ "@bamboocss/types": "1.17.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@jridgewell/trace-mapping": "^0.3.31",
48
48
  "vite": "7.2.6",
49
- "@bamboocss/fixture": "1.16.1"
49
+ "@bamboocss/fixture": "1.17.1"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "vite": ">=5"