@bamboocss/vite 1.25.0 → 1.28.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 +382 -42
- package/dist/index.d.cts +102 -2
- package/dist/index.d.mts +101 -3
- package/dist/index.mjs +382 -42
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,10 @@ let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
|
|
|
30
30
|
let _bamboocss_extractor = require("@bamboocss/extractor");
|
|
31
31
|
let magic_string = require("magic-string");
|
|
32
32
|
magic_string = __toESM(magic_string);
|
|
33
|
+
let node_path = require("node:path");
|
|
33
34
|
let ts_morph = require("ts-morph");
|
|
34
35
|
let _bamboocss_core = require("@bamboocss/core");
|
|
35
36
|
let _bamboocss_shared = require("@bamboocss/shared");
|
|
36
|
-
let node_path = require("node:path");
|
|
37
37
|
//#region src/css.ts
|
|
38
38
|
/**
|
|
39
39
|
* What a project imports to get the stylesheet.
|
|
@@ -850,7 +850,7 @@ const propertyKey = (nameNode) => {
|
|
|
850
850
|
* there is nothing to match — the host here is any import of the generated css module, which
|
|
851
851
|
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
852
852
|
*/
|
|
853
|
-
const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed) => {
|
|
853
|
+
const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed, newImportModule) => {
|
|
854
854
|
const sourceFile = call.getSourceFile();
|
|
855
855
|
let host;
|
|
856
856
|
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
@@ -866,9 +866,21 @@ const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGenerated
|
|
|
866
866
|
}
|
|
867
867
|
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
868
868
|
}
|
|
869
|
-
if (!host) return void 0;
|
|
869
|
+
if (!host && !newImportModule) return void 0;
|
|
870
870
|
if (declaredAtModuleScope(sourceFile).has(imported)) return void 0;
|
|
871
871
|
if (isShadowed(call, imported)) return void 0;
|
|
872
|
+
if (!host) {
|
|
873
|
+
const anchor = sourceFile.getImportDeclarations().at(-1);
|
|
874
|
+
if (!anchor) return void 0;
|
|
875
|
+
return {
|
|
876
|
+
name: imported,
|
|
877
|
+
insert: {
|
|
878
|
+
pos: anchor.getEnd(),
|
|
879
|
+
names: [imported],
|
|
880
|
+
module: newImportModule
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
872
884
|
const last = host.getNamedImports().at(-1);
|
|
873
885
|
if (!last) return void 0;
|
|
874
886
|
return {
|
|
@@ -928,6 +940,13 @@ const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
|
928
940
|
/**
|
|
929
941
|
* `input(variantProps)` — a selection the build cannot see inside.
|
|
930
942
|
*
|
|
943
|
+
* Inline recipes only. `cva` resolves a selection with `getRecipeClassNames`, which reads
|
|
944
|
+
* a variant value as a key and so cannot take a conditional — a `{ base, md }` object finds
|
|
945
|
+
* no entry and names no class, exactly as `cvaPick` does. A **config** recipe routes its
|
|
946
|
+
* selection through `createCss`, which *expands* conditions into one class per breakpoint,
|
|
947
|
+
* so a scalar lookup silently drops them. That is why this lowering is not applied to
|
|
948
|
+
* config recipes: for a dynamic axis the build cannot know which kind of value arrives.
|
|
949
|
+
*
|
|
931
950
|
* The classes are still knowable: a recipe emits one per declared variant, so the call is
|
|
932
951
|
* one term per variant reading that binding. This is the shape a wrapper component takes,
|
|
933
952
|
* where the variants are the component's public API and cannot be literals by definition.
|
|
@@ -1175,11 +1194,24 @@ const createRuntimeRecipe = (ctx) => {
|
|
|
1175
1194
|
}
|
|
1176
1195
|
}
|
|
1177
1196
|
});
|
|
1178
|
-
const
|
|
1197
|
+
const declaredValues = config.variants ?? {};
|
|
1198
|
+
/**
|
|
1199
|
+
* The same filter the generated `createRecipe` applies: only a value the config declares
|
|
1200
|
+
* names a class.
|
|
1201
|
+
*
|
|
1202
|
+
* Scalars only — a conditional or responsive value is an object of leaves, and the leaves
|
|
1203
|
+
* are what name classes when `createCss` walks them.
|
|
1204
|
+
*/
|
|
1205
|
+
const onlyDeclared = (styles) => Object.fromEntries(Object.entries(styles).filter(([prop, value]) => {
|
|
1206
|
+
if (prop === className) return true;
|
|
1207
|
+
if (value === null || typeof value === "object") return true;
|
|
1208
|
+
return Object.hasOwn(declaredValues, prop) && Object.hasOwn(declaredValues[prop] ?? {}, String(value));
|
|
1209
|
+
}));
|
|
1210
|
+
const recipeStyles = isConstantSlot ? { [className]: "__ignore__" } : onlyDeclared({
|
|
1179
1211
|
[className]: "__ignore__",
|
|
1180
1212
|
...defaultVariants,
|
|
1181
1213
|
...(0, _bamboocss_shared.compact)(variants)
|
|
1182
|
-
};
|
|
1214
|
+
});
|
|
1183
1215
|
if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
|
|
1184
1216
|
if (isSlotRecipe) {
|
|
1185
1217
|
const evaluated = anchors.length > 0 ? anchors : config.slots;
|
|
@@ -1219,6 +1251,22 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
1219
1251
|
*/
|
|
1220
1252
|
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
1221
1253
|
/**
|
|
1254
|
+
* The skip reasons that leave a `css()`-family call in the output.
|
|
1255
|
+
*
|
|
1256
|
+
* `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
|
|
1257
|
+
* function of the same name — neither leaves a call of ours. `not-foldable` is a `cva`/`sva`
|
|
1258
|
+
* definition, which keeps the recipe runtime rather than the css engine; see `strict`.
|
|
1259
|
+
*/
|
|
1260
|
+
const SURVIVES_TO_RUNTIME = new Set([
|
|
1261
|
+
"dynamic",
|
|
1262
|
+
"runtime-binding",
|
|
1263
|
+
"raw-call",
|
|
1264
|
+
"unsupported-kind",
|
|
1265
|
+
"no-call-expression",
|
|
1266
|
+
"empty",
|
|
1267
|
+
"unresolved-token"
|
|
1268
|
+
]);
|
|
1269
|
+
/**
|
|
1222
1270
|
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
1223
1271
|
*
|
|
1224
1272
|
* Folded when the whole selection resolves, reported under this reason when it does not.
|
|
@@ -1231,6 +1279,65 @@ const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
|
1231
1279
|
*/
|
|
1232
1280
|
const RECIPE_CALL_TYPE = "cva-call";
|
|
1233
1281
|
/**
|
|
1282
|
+
* An identifier that actually reads the binding.
|
|
1283
|
+
*
|
|
1284
|
+
* `getDescendantsOfKind(Identifier)` yields every name in the file, and most of them bind or
|
|
1285
|
+
* label rather than read: a JSX tag (`<button/>` against a recipe called `button`), an object
|
|
1286
|
+
* key, a property name, a declaration. Counting those failed builds on modules that had
|
|
1287
|
+
* folded completely — and `button`, `input`, `label`, `select`, `table`, `dialog` and `form`
|
|
1288
|
+
* are all ordinary recipe names as well as intrinsic elements.
|
|
1289
|
+
*
|
|
1290
|
+
* A type position is excluded for a different reason: it is erased, and with it the import.
|
|
1291
|
+
*/
|
|
1292
|
+
const isValueReference = (identifier) => {
|
|
1293
|
+
const parent = identifier.getParent();
|
|
1294
|
+
if (!parent) return false;
|
|
1295
|
+
if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) return false;
|
|
1296
|
+
if (ts_morph.Node.isImportClause(parent) || ts_morph.Node.isNamespaceImport(parent)) return false;
|
|
1297
|
+
if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) return false;
|
|
1298
|
+
if (ts_morph.Node.isQualifiedName(parent) && parent.getRight() === identifier) return false;
|
|
1299
|
+
if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) return false;
|
|
1300
|
+
if (ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) {
|
|
1301
|
+
if (parent.getNameNode() === identifier) return false;
|
|
1302
|
+
}
|
|
1303
|
+
if (ts_morph.Node.isLabeledStatement(parent) || ts_morph.Node.isBreakStatement(parent) || ts_morph.Node.isContinueStatement(parent)) return false;
|
|
1304
|
+
if (ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) {
|
|
1305
|
+
if (parent.getTagNameNode() === identifier) return identifier.getText()[0] === identifier.getText()[0]?.toUpperCase();
|
|
1306
|
+
}
|
|
1307
|
+
if (ts_morph.Node.isJsxAttribute(parent)) return false;
|
|
1308
|
+
if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) return false;
|
|
1309
|
+
if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isBindingElement(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent)) && parent.getNameNode() === identifier) return false;
|
|
1310
|
+
return !identifier.getFirstAncestor((ancestor) => ts_morph.Node.isTypeNode(ancestor) || ts_morph.Node.isTypeAliasDeclaration(ancestor) || ts_morph.Node.isInterfaceDeclaration(ancestor));
|
|
1311
|
+
};
|
|
1312
|
+
/**
|
|
1313
|
+
* Imports a surviving reference to is not a failure.
|
|
1314
|
+
*
|
|
1315
|
+
* The first four are what the fold itself writes; all live in `cx` and pull no engine, so a
|
|
1316
|
+
* reference to one is the fold having worked.
|
|
1317
|
+
*
|
|
1318
|
+
* `cva` and `sva` are there for the reason `SURVIVES_TO_RUNTIME` omits `not-foldable`: a
|
|
1319
|
+
* recipe *definition* cannot fold to a class string and never could, and what it keeps is the
|
|
1320
|
+
* recipe runtime rather than the css engine — which `strict` accepts. Their unfoldable
|
|
1321
|
+
* invocations are reported separately, as `recipe-call`.
|
|
1322
|
+
*/
|
|
1323
|
+
const PERMITTED_BINDINGS = new Set([
|
|
1324
|
+
"cx",
|
|
1325
|
+
"cva",
|
|
1326
|
+
"sva",
|
|
1327
|
+
RECIPE_PICK_HELPER,
|
|
1328
|
+
SPLIT_PROPS_HELPER,
|
|
1329
|
+
LEAF_HELPER
|
|
1330
|
+
]);
|
|
1331
|
+
/**
|
|
1332
|
+
* The pieces `trim` reduces a module specifier by, hoisted because a regex literal
|
|
1333
|
+
* constructs a new object every time it is evaluated and `trim` runs per specifier per
|
|
1334
|
+
* import declaration per module.
|
|
1335
|
+
*/
|
|
1336
|
+
const LEADING_RELATIVE = /^(?:\.\.?\/)+/;
|
|
1337
|
+
const TRAILING_SLASH = /\/$/;
|
|
1338
|
+
const MODULE_EXTENSION = /\.[mc]?[jt]sx?$/;
|
|
1339
|
+
const TRAILING_INDEX = /\/index$/;
|
|
1340
|
+
/**
|
|
1234
1341
|
* An argument that cannot run anything when it is evaluated.
|
|
1235
1342
|
*
|
|
1236
1343
|
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
@@ -1446,7 +1553,7 @@ const argumentsAccountedFor = (call, boxNode) => {
|
|
|
1446
1553
|
return accountsForSource(args[0], boxNode);
|
|
1447
1554
|
};
|
|
1448
1555
|
const foldSource = (options) => {
|
|
1449
|
-
const { ctx, code, parserResult, partial: partial_ = true, runtimeCss = createRuntimeCss(ctx) } = options;
|
|
1556
|
+
const { ctx, code, parserResult, partial: partial_ = true, runtimeCss = createRuntimeCss(ctx), parseModule, recipeConfigCache, reportSurvivors, sourceFile: ownSourceFile } = options;
|
|
1450
1557
|
/**
|
|
1451
1558
|
* Recover the static half of a call the whole-call path gave up on. Only a
|
|
1452
1559
|
* single-argument `css()` qualifies: a pattern or recipe call takes props rather than a
|
|
@@ -1498,7 +1605,8 @@ const foldSource = (options) => {
|
|
|
1498
1605
|
className: plan.className,
|
|
1499
1606
|
classNames: [plan.className, ...plan.finite.filter((entry) => entry.emitsLiterals).flatMap((entry) => literalsIn(entry.expression))].filter(Boolean),
|
|
1500
1607
|
replacement: `${cx.name}(${parts.join(", ")})`,
|
|
1501
|
-
insert: cx.insert
|
|
1608
|
+
insert: cx.insert,
|
|
1609
|
+
runtimeCallee: runtimePart ? callee : void 0
|
|
1502
1610
|
};
|
|
1503
1611
|
};
|
|
1504
1612
|
const runtimeRecipe = createRuntimeRecipe(ctx);
|
|
@@ -1528,7 +1636,25 @@ const foldSource = (options) => {
|
|
|
1528
1636
|
*/
|
|
1529
1637
|
const generatedCssModule = [ctx.imports.outdir, "css"].join("/");
|
|
1530
1638
|
const pathMappings = ctx.conf.tsOptions?.pathMappings;
|
|
1531
|
-
|
|
1639
|
+
/**
|
|
1640
|
+
* The spelling reduced to the module it names.
|
|
1641
|
+
*
|
|
1642
|
+
* The extension and `/index` are stripped because bamboo's own output makes a file
|
|
1643
|
+
* import them: `outExtension: 'js'` under NodeNext resolution is written
|
|
1644
|
+
* `styled-system/css/index.js`, which is neither equal to `styled-system/css` nor a
|
|
1645
|
+
* tail of it. Extraction admitted such a file anyway — `ImportMap.match` is
|
|
1646
|
+
* substring-based — so the call was folded while the *insert* was refused, and the
|
|
1647
|
+
* result was reported as `dynamic`: the same silent downgrade the alias case above
|
|
1648
|
+
* describes, reached through the extension instead.
|
|
1649
|
+
*
|
|
1650
|
+
* This does not weaken the equality the comment above insists on. `styled-system/css/css`
|
|
1651
|
+
* still names neither, because only a trailing `/index` is a module's own directory.
|
|
1652
|
+
*
|
|
1653
|
+
* `.d.ts` is deliberately not stripped. A declaration file exports no runtime binding, so
|
|
1654
|
+
* matching one would authorise inserting an import that resolves to nothing — and a value
|
|
1655
|
+
* import cannot name one anyway, which is what makes leaving it out free.
|
|
1656
|
+
*/
|
|
1657
|
+
const trim = (value) => value.replaceAll("\\", "/").replace(LEADING_RELATIVE, "").replace(TRAILING_SLASH, "").replace(MODULE_EXTENSION, "").replace(TRAILING_INDEX, "");
|
|
1532
1658
|
const matchesModule = (mod, entries) => {
|
|
1533
1659
|
const candidates = [mod];
|
|
1534
1660
|
if (pathMappings) {
|
|
@@ -1545,6 +1671,100 @@ const foldSource = (options) => {
|
|
|
1545
1671
|
};
|
|
1546
1672
|
const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
|
|
1547
1673
|
const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
|
|
1674
|
+
/**
|
|
1675
|
+
* How *this* module would have to spell the css module, learnt from one that already does.
|
|
1676
|
+
*
|
|
1677
|
+
* A file calling an imported recipe need not import the css module at all, so when the
|
|
1678
|
+
* lowering needs `cvaPick` there is no spelling in the file to copy. The declaring module
|
|
1679
|
+
* necessarily has one — `cva` came from it — and that is the spelling reused here.
|
|
1680
|
+
*
|
|
1681
|
+
* A bare or aliased specifier resolves identically from any file, so it is taken as
|
|
1682
|
+
* written. A relative one is re-based: resolved against the module that wrote it, then
|
|
1683
|
+
* expressed from the module being folded.
|
|
1684
|
+
*/
|
|
1685
|
+
const cssModuleSpecifierFrom = (declaring) => {
|
|
1686
|
+
for (const declaration of declaring.getImportDeclarations()) {
|
|
1687
|
+
if (declaration.isTypeOnly()) continue;
|
|
1688
|
+
const mod = declaration.getModuleSpecifierValue();
|
|
1689
|
+
if (isGeneratedCssModule(mod)) return mod;
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
/**
|
|
1693
|
+
* That spelling, said from the module being folded.
|
|
1694
|
+
*
|
|
1695
|
+
* A bare or aliased specifier resolves identically from any file, so it is taken as
|
|
1696
|
+
* written. A relative one is re-based: resolved against the module that wrote it, then
|
|
1697
|
+
* expressed from the module being folded. Pure path arithmetic, so it holds a string
|
|
1698
|
+
* rather than a node — a cached node does not survive the next `addSourceFile`, which
|
|
1699
|
+
* ts-morph implements by forgetting the file's whole tree.
|
|
1700
|
+
*/
|
|
1701
|
+
const rebaseSpecifier = (specifier, declaringPath, consumingPath) => {
|
|
1702
|
+
if (!specifier.startsWith(".")) return specifier;
|
|
1703
|
+
const absolute = (0, node_path.resolve)((0, node_path.dirname)(declaringPath), specifier);
|
|
1704
|
+
const rebased = (0, node_path.relative)((0, node_path.dirname)(consumingPath), absolute).replaceAll("\\", "/");
|
|
1705
|
+
if (!rebased) return void 0;
|
|
1706
|
+
return rebased.startsWith(".") ? rebased : `./${rebased}`;
|
|
1707
|
+
};
|
|
1708
|
+
/**
|
|
1709
|
+
* Configs of one foreign module, parsed once however many of its recipes are called.
|
|
1710
|
+
*
|
|
1711
|
+
* Falls back to a per-call map when the caller supplies none, so the fold stays correct
|
|
1712
|
+
* standalone — only repeated, which is what the shared cache exists to avoid.
|
|
1713
|
+
*/
|
|
1714
|
+
const configsByModule = recipeConfigCache ?? /* @__PURE__ */ new Map();
|
|
1715
|
+
/** The specifier each imported recipe's module used for the css module, when it needs one. */
|
|
1716
|
+
const helperModules = /* @__PURE__ */ new Map();
|
|
1717
|
+
/** Declaring modules a fold read, recorded as paths because their nodes do not persist. */
|
|
1718
|
+
const foreignDependencies = /* @__PURE__ */ new Set();
|
|
1719
|
+
/** Resolutions for this module's own call sites, keyed by the name the call site writes. */
|
|
1720
|
+
const importedRecipes = /* @__PURE__ */ new Map();
|
|
1721
|
+
/**
|
|
1722
|
+
* The config of a recipe this module imports.
|
|
1723
|
+
*
|
|
1724
|
+
* The binding is followed with ts-morph's symbol aliasing rather than by re-reading import
|
|
1725
|
+
* declarations, because that is what already understands the shapes these are reached
|
|
1726
|
+
* through: `export { badge } from './styles'`, `export * from './styles'`, and an alias at
|
|
1727
|
+
* either end. Each hop is an alias symbol, so following them to a non-alias lands on the
|
|
1728
|
+
* declaration wherever it lives.
|
|
1729
|
+
*
|
|
1730
|
+
* The class names do not depend on which module the call is in — `getRecipeIdentity` hashes
|
|
1731
|
+
* the config — so a recipe lowered here produces exactly the string its own module's call
|
|
1732
|
+
* sites produce, and exactly the one the runtime would have.
|
|
1733
|
+
*/
|
|
1734
|
+
const resolveImportedRecipe = (call, name, origin) => {
|
|
1735
|
+
if (importedRecipes.has(name)) return importedRecipes.get(name);
|
|
1736
|
+
const resolve = () => {
|
|
1737
|
+
if (!parseModule) return void 0;
|
|
1738
|
+
const consuming = call.getSourceFile();
|
|
1739
|
+
if (origin.filePath === consuming.getFilePath()) return void 0;
|
|
1740
|
+
let foreign = configsByModule.get(origin.filePath);
|
|
1741
|
+
if (!foreign) {
|
|
1742
|
+
const result = parseModule(origin.filePath);
|
|
1743
|
+
if (!result) return void 0;
|
|
1744
|
+
const collected = collectRecipeConfigs(result);
|
|
1745
|
+
const declaring = [...collected.values()].find((entry) => entry.box)?.box?.getNode?.()?.getSourceFile();
|
|
1746
|
+
const configs = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const [key, entry] of collected) configs.set(key, entry === AMBIGUOUS ? entry : {
|
|
1748
|
+
config: entry.config,
|
|
1749
|
+
name: entry.name,
|
|
1750
|
+
box: void 0
|
|
1751
|
+
});
|
|
1752
|
+
foreign = {
|
|
1753
|
+
configs,
|
|
1754
|
+
cssSpecifier: declaring ? cssModuleSpecifierFrom(declaring) : void 0
|
|
1755
|
+
};
|
|
1756
|
+
configsByModule.set(origin.filePath, foreign);
|
|
1757
|
+
}
|
|
1758
|
+
const entry = foreign.configs.get(origin.name);
|
|
1759
|
+
if (!entry || entry === AMBIGUOUS) return void 0;
|
|
1760
|
+
foreignDependencies.add(origin.filePath);
|
|
1761
|
+
helperModules.set(name, foreign.cssSpecifier ? rebaseSpecifier(foreign.cssSpecifier, origin.filePath, consuming.getFilePath()) : void 0);
|
|
1762
|
+
return entry;
|
|
1763
|
+
};
|
|
1764
|
+
const resolved = resolve();
|
|
1765
|
+
importedRecipes.set(name, resolved);
|
|
1766
|
+
return resolved;
|
|
1767
|
+
};
|
|
1548
1768
|
const folded = [];
|
|
1549
1769
|
const skipped = [];
|
|
1550
1770
|
const candidates = [];
|
|
@@ -1695,6 +1915,10 @@ const foldSource = (options) => {
|
|
|
1695
1915
|
continue;
|
|
1696
1916
|
}
|
|
1697
1917
|
recipeConfigs ??= collectRecipeConfigs(parserResult);
|
|
1918
|
+
if (!recipeConfigs.has(name) && item.origin) {
|
|
1919
|
+
const imported = resolveImportedRecipe(call, name, item.origin);
|
|
1920
|
+
if (imported) recipeConfigs.set(name, imported);
|
|
1921
|
+
}
|
|
1698
1922
|
const tally = recipeCalls.get(name) ?? {
|
|
1699
1923
|
seen: 0,
|
|
1700
1924
|
lowered: 0
|
|
@@ -1705,7 +1929,7 @@ const foldSource = (options) => {
|
|
|
1705
1929
|
const entry = recipeConfigs.get(name);
|
|
1706
1930
|
const lowered = lowerRecipeCall(call, entry, ctx, isInertExpression, resolvedSelection);
|
|
1707
1931
|
if (lowered.kind === "expression") {
|
|
1708
|
-
const helper = ensureRecipeHelperImport(RECIPE_PICK_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1932
|
+
const helper = ensureRecipeHelperImport(RECIPE_PICK_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name));
|
|
1709
1933
|
if (helper) {
|
|
1710
1934
|
tally.lowered++;
|
|
1711
1935
|
candidates.push({
|
|
@@ -1819,6 +2043,12 @@ const foldSource = (options) => {
|
|
|
1819
2043
|
if (!(ts_morph.Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
1820
2044
|
const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
|
|
1821
2045
|
if (partial) {
|
|
2046
|
+
if (reportSurvivors && partial.runtimeCallee) skipped.push({
|
|
2047
|
+
name,
|
|
2048
|
+
reason: "runtime-binding",
|
|
2049
|
+
start,
|
|
2050
|
+
end
|
|
2051
|
+
});
|
|
1822
2052
|
candidates.push({
|
|
1823
2053
|
item,
|
|
1824
2054
|
call,
|
|
@@ -1846,13 +2076,22 @@ const foldSource = (options) => {
|
|
|
1846
2076
|
slot
|
|
1847
2077
|
});
|
|
1848
2078
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2079
|
+
/**
|
|
2080
|
+
* Ranges the rewrite actually replaced. Declared before the early return below, because
|
|
2081
|
+
* that return is now also a reporting point: a module with nothing to fold is exactly the
|
|
2082
|
+
* shape `reportSurvivors` exists to catch.
|
|
2083
|
+
*/
|
|
2084
|
+
const applied = [];
|
|
2085
|
+
if (candidates.length === 0) {
|
|
2086
|
+
if (reportSurvivors) reportRuntimeBindings();
|
|
2087
|
+
return {
|
|
2088
|
+
code,
|
|
2089
|
+
map: null,
|
|
2090
|
+
folded,
|
|
2091
|
+
skipped,
|
|
2092
|
+
dependencies: []
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
1856
2095
|
const dependencyScan = createDependencyScan(candidates[0].node.getSourceFile());
|
|
1857
2096
|
candidates.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1858
2097
|
const magic = new magic_string.default(code);
|
|
@@ -1861,10 +2100,9 @@ const foldSource = (options) => {
|
|
|
1861
2100
|
if (!insert) return;
|
|
1862
2101
|
const missing = insert.names.filter((name) => !insertedNames.has(name));
|
|
1863
2102
|
if (!missing.length) return;
|
|
1864
|
-
magic.appendLeft(insert.pos, missing.map((name) => `, ${name}`).join(""));
|
|
2103
|
+
magic.appendLeft(insert.pos, insert.module ? `\nimport { ${missing.join(", ")} } from '${insert.module}'` : missing.map((name) => `, ${name}`).join(""));
|
|
1865
2104
|
for (const name of missing) insertedNames.add(name);
|
|
1866
2105
|
};
|
|
1867
|
-
const applied = [];
|
|
1868
2106
|
const collides = (edits) => edits.some(([start, end]) => applied.some(([from, to]) => start < to && from < end));
|
|
1869
2107
|
for (const candidate of candidates) {
|
|
1870
2108
|
const { item, start, end } = candidate;
|
|
@@ -1956,14 +2194,20 @@ const foldSource = (options) => {
|
|
|
1956
2194
|
});
|
|
1957
2195
|
collectSourceFiles(item.box, dependencyScan);
|
|
1958
2196
|
}
|
|
1959
|
-
const recipeSourceFile =
|
|
2197
|
+
const recipeSourceFile = candidates[0]?.node.getSourceFile();
|
|
1960
2198
|
if (recipeSourceFile) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
|
|
1961
2199
|
if (access.getName() !== "splitVariantProps") continue;
|
|
1962
2200
|
const target = access.getExpression();
|
|
1963
2201
|
if (!ts_morph.Node.isIdentifier(target)) continue;
|
|
1964
|
-
const entry = recipeConfigs?.get(target.getText());
|
|
1965
|
-
if (!entry || entry === AMBIGUOUS) continue;
|
|
1966
2202
|
if (isShadowed(access, target.getText())) continue;
|
|
2203
|
+
const local = recipeConfigs?.get(target.getText());
|
|
2204
|
+
const importedConfig = !local && importsFor(recipeSourceFile).has(target.getText()) ? ctx.recipes.getConfig(target.getText()) : void 0;
|
|
2205
|
+
const entry = local && local !== AMBIGUOUS ? local : importedConfig ? {
|
|
2206
|
+
config: importedConfig,
|
|
2207
|
+
name: "",
|
|
2208
|
+
box: void 0
|
|
2209
|
+
} : void 0;
|
|
2210
|
+
if (!entry) continue;
|
|
1967
2211
|
const call = access.getParent();
|
|
1968
2212
|
if (!ts_morph.Node.isCallExpression(call) || call.getExpression() !== access) continue;
|
|
1969
2213
|
const args = call.getArguments();
|
|
@@ -1972,7 +2216,7 @@ const foldSource = (options) => {
|
|
|
1972
2216
|
const end = call.getEnd();
|
|
1973
2217
|
if (code.slice(start, end) !== call.getText()) continue;
|
|
1974
2218
|
if (collides([[start, end]])) continue;
|
|
1975
|
-
const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
2219
|
+
const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(target.getText()));
|
|
1976
2220
|
if (!helper) continue;
|
|
1977
2221
|
const keys = Object.keys(entry.config.variants ?? {});
|
|
1978
2222
|
magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
|
|
@@ -1990,6 +2234,104 @@ const foldSource = (options) => {
|
|
|
1990
2234
|
if (code.slice(start, call.getEnd()) !== call.getText()) continue;
|
|
1991
2235
|
magic.appendLeft(start, "/*#__PURE__*/");
|
|
1992
2236
|
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Bindings from a bamboo module still referenced once every rewrite is applied.
|
|
2239
|
+
*
|
|
2240
|
+
* Deliberately not driven by `parserResult`: that is the recogniser, and the point here is
|
|
2241
|
+
* to catch what it did not see. A namespace import called as `s.cva(...)`, a default
|
|
2242
|
+
* import, a specifier that resolved to nothing — each leaves a live reference and no ledger
|
|
2243
|
+
* entry at all, which is how `strict` came to pass a build that still shipped the engine.
|
|
2244
|
+
*
|
|
2245
|
+
* The helpers the fold itself writes are excluded: `cx`, `cvaPick`, `splitProps` and the
|
|
2246
|
+
* leaf helper live in `cx` and pull no engine, so a reference to one is the fold working
|
|
2247
|
+
* rather than failing.
|
|
2248
|
+
*/
|
|
2249
|
+
function reportRuntimeBindings() {
|
|
2250
|
+
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile();
|
|
2251
|
+
if (!sourceFile) return;
|
|
2252
|
+
const bambooModules = [
|
|
2253
|
+
...cssModules,
|
|
2254
|
+
...ctx.imports.matchers.recipe?.mods ?? [],
|
|
2255
|
+
...ctx.imports.matchers.pattern?.mods ?? [],
|
|
2256
|
+
...ctx.imports.matchers.tokens?.mods ?? []
|
|
2257
|
+
];
|
|
2258
|
+
/** Local name -> what to call it in the report. */
|
|
2259
|
+
const watched = /* @__PURE__ */ new Map();
|
|
2260
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
2261
|
+
if (declaration.isTypeOnly()) continue;
|
|
2262
|
+
if (!matchesModule(declaration.getModuleSpecifierValue(), bambooModules)) continue;
|
|
2263
|
+
for (const named of declaration.getNamedImports()) {
|
|
2264
|
+
if (named.isTypeOnly()) continue;
|
|
2265
|
+
const imported = named.getNameNode().getText();
|
|
2266
|
+
if (PERMITTED_BINDINGS.has(imported)) continue;
|
|
2267
|
+
watched.set((named.getAliasNode() ?? named.getNameNode()).getText(), imported);
|
|
2268
|
+
}
|
|
2269
|
+
const namespace = declaration.getNamespaceImport();
|
|
2270
|
+
if (namespace) watched.set(namespace.getText(), `${namespace.getText()}.*`);
|
|
2271
|
+
const defaultImport = declaration.getDefaultImport();
|
|
2272
|
+
if (defaultImport) watched.set(defaultImport.getText(), defaultImport.getText());
|
|
2273
|
+
}
|
|
2274
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
2275
|
+
if (declaration.isTypeOnly()) continue;
|
|
2276
|
+
if (!matchesModule(declaration.getModuleSpecifierValue() ?? "", bambooModules)) continue;
|
|
2277
|
+
if (declaration.isNamespaceExport()) {
|
|
2278
|
+
skipped.push({
|
|
2279
|
+
name: declaration.getNamespaceExport()?.getName() ?? "*",
|
|
2280
|
+
reason: "runtime-binding",
|
|
2281
|
+
start: declaration.getStart(),
|
|
2282
|
+
end: declaration.getEnd()
|
|
2283
|
+
});
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
for (const named of declaration.getNamedExports()) {
|
|
2287
|
+
if (named.isTypeOnly()) continue;
|
|
2288
|
+
const imported = named.getNameNode().getText();
|
|
2289
|
+
if (PERMITTED_BINDINGS.has(imported)) continue;
|
|
2290
|
+
skipped.push({
|
|
2291
|
+
name: imported,
|
|
2292
|
+
reason: "runtime-binding",
|
|
2293
|
+
start: named.getStart(),
|
|
2294
|
+
end: named.getEnd()
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
2299
|
+
if (declaration.isTypeOnly() || declaration.getModuleSpecifier()) continue;
|
|
2300
|
+
for (const named of declaration.getNamedExports()) {
|
|
2301
|
+
if (named.isTypeOnly()) continue;
|
|
2302
|
+
const imported = watched.get(named.getNameNode().getText());
|
|
2303
|
+
if (imported === void 0) continue;
|
|
2304
|
+
skipped.push({
|
|
2305
|
+
name: imported,
|
|
2306
|
+
reason: "runtime-binding",
|
|
2307
|
+
start: named.getStart(),
|
|
2308
|
+
end: named.getEnd()
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (watched.size === 0) return;
|
|
2313
|
+
const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
|
|
2314
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2315
|
+
for (const identifier of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.Identifier)) {
|
|
2316
|
+
const local = identifier.getText();
|
|
2317
|
+
const imported = watched.get(local);
|
|
2318
|
+
if (imported === void 0 || reported.has(local)) continue;
|
|
2319
|
+
const start = identifier.getStart();
|
|
2320
|
+
if (identifier.getFirstAncestorByKind(ts_morph.SyntaxKind.ImportDeclaration)) continue;
|
|
2321
|
+
if (applied.some(([from, to]) => start >= from && start < to)) continue;
|
|
2322
|
+
if (declined.some(([from, to]) => start >= from && start < to)) continue;
|
|
2323
|
+
if (!isValueReference(identifier)) continue;
|
|
2324
|
+
if (isShadowed(identifier, local)) continue;
|
|
2325
|
+
reported.add(local);
|
|
2326
|
+
skipped.push({
|
|
2327
|
+
name: imported,
|
|
2328
|
+
reason: "runtime-binding",
|
|
2329
|
+
start,
|
|
2330
|
+
end: identifier.getEnd()
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
if (reportSurvivors) reportRuntimeBindings();
|
|
1993
2335
|
if (folded.length === 0) return {
|
|
1994
2336
|
code,
|
|
1995
2337
|
map: null,
|
|
@@ -2006,7 +2348,7 @@ const foldSource = (options) => {
|
|
|
2006
2348
|
}),
|
|
2007
2349
|
folded,
|
|
2008
2350
|
skipped,
|
|
2009
|
-
dependencies:
|
|
2351
|
+
dependencies: [...dependencyScan.results, ...foreignDependencies]
|
|
2010
2352
|
};
|
|
2011
2353
|
};
|
|
2012
2354
|
//#endregion
|
|
@@ -2040,21 +2382,6 @@ const isGeneratedOutput = (filePath, ctx) => {
|
|
|
2040
2382
|
const file = slashed(filePath);
|
|
2041
2383
|
return file === root || file.startsWith(`${root}/`);
|
|
2042
2384
|
};
|
|
2043
|
-
/**
|
|
2044
|
-
* The skip reasons that leave a `css()`-family call in the output.
|
|
2045
|
-
*
|
|
2046
|
-
* `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
|
|
2047
|
-
* function of the same name — neither leaves a call of ours. `not-foldable` is a `cva`/`sva`
|
|
2048
|
-
* definition, which keeps the recipe runtime rather than the css engine; see `strict`.
|
|
2049
|
-
*/
|
|
2050
|
-
const SURVIVES_TO_RUNTIME = new Set([
|
|
2051
|
-
"dynamic",
|
|
2052
|
-
"raw-call",
|
|
2053
|
-
"unsupported-kind",
|
|
2054
|
-
"no-call-expression",
|
|
2055
|
-
"empty",
|
|
2056
|
-
"unresolved-token"
|
|
2057
|
-
]);
|
|
2058
2385
|
/** 1-indexed line of a source offset, for an error a user can navigate to. */
|
|
2059
2386
|
const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
|
|
2060
2387
|
const formatSkipped = (id, skipped) => {
|
|
@@ -2085,6 +2412,13 @@ const bamboocss = (options = {}) => {
|
|
|
2085
2412
|
};
|
|
2086
2413
|
/** Under `strict`, every call that would still reach the runtime. */
|
|
2087
2414
|
const survivors = [];
|
|
2415
|
+
/**
|
|
2416
|
+
* Recipe configs read out of modules other than the one being transformed.
|
|
2417
|
+
*
|
|
2418
|
+
* Per build rather than per module: a recipe declared once and imported by fifty components
|
|
2419
|
+
* would otherwise re-parse its module fifty times, which is the transform path.
|
|
2420
|
+
*/
|
|
2421
|
+
const recipeConfigCache = /* @__PURE__ */ new Map();
|
|
2088
2422
|
let ctx;
|
|
2089
2423
|
let runtimeCss;
|
|
2090
2424
|
let setup;
|
|
@@ -2112,6 +2446,7 @@ const bamboocss = (options = {}) => {
|
|
|
2112
2446
|
totals.filesWithFolds = 0;
|
|
2113
2447
|
totals.skipped.clear();
|
|
2114
2448
|
survivors.length = 0;
|
|
2449
|
+
recipeConfigCache.clear();
|
|
2115
2450
|
await ensureContext();
|
|
2116
2451
|
},
|
|
2117
2452
|
/**
|
|
@@ -2139,6 +2474,7 @@ const bamboocss = (options = {}) => {
|
|
|
2139
2474
|
if (!shouldTransform(id)) return;
|
|
2140
2475
|
const [filePath] = id.split("?");
|
|
2141
2476
|
if (!filePath) return;
|
|
2477
|
+
recipeConfigCache.clear();
|
|
2142
2478
|
if (change.event === "delete") {
|
|
2143
2479
|
ctx.project.removeSourceFile(filePath);
|
|
2144
2480
|
return;
|
|
@@ -2154,16 +2490,20 @@ const bamboocss = (options = {}) => {
|
|
|
2154
2490
|
if (isGeneratedOutput(filePath, ctx)) return null;
|
|
2155
2491
|
let result;
|
|
2156
2492
|
try {
|
|
2157
|
-
ctx.project.addSourceFile(filePath, code);
|
|
2493
|
+
const sourceFile = ctx.project.addSourceFile(filePath, code);
|
|
2158
2494
|
const parserResult = ctx.project.parseSourceFile(filePath);
|
|
2159
|
-
if (!parserResult || parserResult.isEmpty()) return null;
|
|
2495
|
+
if (!parserResult || parserResult.isEmpty() && !strict) return null;
|
|
2160
2496
|
result = foldSource({
|
|
2161
2497
|
ctx,
|
|
2162
2498
|
code,
|
|
2163
2499
|
parserResult,
|
|
2164
2500
|
filePath,
|
|
2165
2501
|
runtimeCss,
|
|
2166
|
-
partial
|
|
2502
|
+
partial,
|
|
2503
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
2504
|
+
recipeConfigCache,
|
|
2505
|
+
reportSurvivors: strict,
|
|
2506
|
+
sourceFile
|
|
2167
2507
|
});
|
|
2168
2508
|
} catch (error) {
|
|
2169
2509
|
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to fold ${filePath}`, error);
|
|
@@ -2207,7 +2547,7 @@ const bamboocss = (options = {}) => {
|
|
|
2207
2547
|
list.push(entry);
|
|
2208
2548
|
byFile.set(entry.file, list);
|
|
2209
2549
|
}
|
|
2210
|
-
const detail = Array.from(byFile.entries()).map(([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${e.name}() — ${e.reason}`)].join("\n")).join("\n");
|
|
2550
|
+
const detail = Array.from(byFile.entries()).map(([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${e.name}${e.reason === "runtime-binding" ? "" : "()"} — ${e.reason}`)].join("\n")).join("\n");
|
|
2211
2551
|
throw new Error(`bamboocss: ${survivors.length} call(s) could not be folded, and \`strict\` is on.\n\n${detail}\n\nEach one keeps \`styled-system/css\` in the bundle, so the engine cannot be dropped however many other calls folded. Make the values static, move the variation into a \`cva\` variant, or generate them with \`staticCss\` — or set \`strict: false\` to accept the runtime.`);
|
|
2212
2552
|
}
|
|
2213
2553
|
if (!transform || !reportSummary) return;
|