@bamboocss/vite 1.14.0 → 1.16.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 +172 -548
- package/dist/index.d.cts +14 -18
- package/dist/index.d.mts +14 -18
- package/dist/index.mjs +172 -548
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -199,8 +199,11 @@ const accountsForSource = (node, boxNode) => {
|
|
|
199
199
|
if (!_bamboocss_extractor.box.isMap(boxNode)) return false;
|
|
200
200
|
for (const property of unwrapped.getProperties()) {
|
|
201
201
|
if (ts_morph.Node.isSpreadAssignment(property)) {
|
|
202
|
-
const expression = property.getExpression();
|
|
203
|
-
if (
|
|
202
|
+
const expression = unwrapExpression(property.getExpression());
|
|
203
|
+
if (ts_morph.Node.isObjectLiteralExpression(expression)) continue;
|
|
204
|
+
const walked = boxNode.resolvedSpreads?.find((entry) => entry.node === expression);
|
|
205
|
+
if (!walked) return false;
|
|
206
|
+
if (!accountsForSource(walked.box.getNode(), walked.box)) return false;
|
|
204
207
|
continue;
|
|
205
208
|
}
|
|
206
209
|
if (ts_morph.Node.isMethodDeclaration(property) || ts_morph.Node.isGetAccessorDeclaration(property) || ts_morph.Node.isSetAccessorDeclaration(property)) return false;
|
|
@@ -437,7 +440,6 @@ const LEAF_SENTINEL = "bamboo0leaf0sentinel0";
|
|
|
437
440
|
* carry the same path or the declined shape resolves without its condition.
|
|
438
441
|
*/
|
|
439
442
|
const leafPrefix = (key, ctx, runtimeCss) => {
|
|
440
|
-
if (ctx.isTemplateLiteralSyntax) return void 0;
|
|
441
443
|
if (ctx.conditions.isCondition(key)) return void 0;
|
|
442
444
|
let resolved;
|
|
443
445
|
try {
|
|
@@ -477,6 +479,9 @@ const planPartialFold = (argument, boxNode, styles, deps) => {
|
|
|
477
479
|
if (!partition) return void 0;
|
|
478
480
|
const className = deps.runtimeCss(partition.staticStyles);
|
|
479
481
|
if (!className && !partition.finite.length) return void 0;
|
|
482
|
+
if (deps.ctx.config.cssMode === "grouped") {
|
|
483
|
+
if ((className ? 1 : 0) + partition.finite.length + (partition.dynamicText.length ? 1 : 0) > 1) return void 0;
|
|
484
|
+
}
|
|
480
485
|
return {
|
|
481
486
|
className,
|
|
482
487
|
dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
|
|
@@ -666,464 +671,6 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
666
671
|
}
|
|
667
672
|
};
|
|
668
673
|
};
|
|
669
|
-
/**
|
|
670
|
-
* The `css` and `cx` bindings a partially folded JSX element needs.
|
|
671
|
-
*
|
|
672
|
-
* Splitting an element sends its dynamic style props to a `css()` call, so unlike the
|
|
673
|
-
* call-site split this needs *two* bindings rather than one. Both are taken from an
|
|
674
|
-
* existing bamboo `css` import: writing a new import declaration would mean guessing a
|
|
675
|
-
* module specifier, and the spelling varies with `importMap`, path aliases and how the
|
|
676
|
-
* project reaches its outdir. An element in a file that does not already import `css` is
|
|
677
|
-
* left alone instead.
|
|
678
|
-
*/
|
|
679
|
-
const resolveCssHelpers = (node, isBambooCssModule, isGeneratedCssModule, isShadowed, wantLeaf = false) => {
|
|
680
|
-
const sourceFile = node.getSourceFile();
|
|
681
|
-
if (!importsAnything(sourceFile, isBambooCssModule)) return void 0;
|
|
682
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
683
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
684
|
-
if (declaration.isTypeOnly() || !isBambooCssModule(mod)) continue;
|
|
685
|
-
const named = declaration.getNamedImports();
|
|
686
|
-
const cssImport = named.find((entry) => entry.getNameNode().getText() === "css" && !entry.isTypeOnly());
|
|
687
|
-
if (!cssImport) continue;
|
|
688
|
-
const cssName = (cssImport.getAliasNode() ?? cssImport.getNameNode()).getText();
|
|
689
|
-
if (isShadowed(node, cssName)) return void 0;
|
|
690
|
-
const wanted = wantLeaf ? ["cx", LEAF_HELPER] : ["cx"];
|
|
691
|
-
const resolved = {};
|
|
692
|
-
const missing = [];
|
|
693
|
-
for (const want of wanted) {
|
|
694
|
-
const existing = named.find((entry) => entry.getNameNode().getText() === want && !entry.isTypeOnly());
|
|
695
|
-
if (existing) {
|
|
696
|
-
const local = (existing.getAliasNode() ?? existing.getNameNode()).getText();
|
|
697
|
-
if (isShadowed(node, local)) return void 0;
|
|
698
|
-
resolved[want] = local;
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
missing.push(want);
|
|
702
|
-
}
|
|
703
|
-
if (!missing.length) return {
|
|
704
|
-
css: cssName,
|
|
705
|
-
cx: resolved.cx,
|
|
706
|
-
leaf: resolved[LEAF_HELPER]
|
|
707
|
-
};
|
|
708
|
-
if (!isGeneratedCssModule(mod)) return void 0;
|
|
709
|
-
const declared = declaredAtModuleScope(sourceFile);
|
|
710
|
-
for (const name of missing) {
|
|
711
|
-
if (isShadowed(node, name) || declared.has(name)) return void 0;
|
|
712
|
-
resolved[name] = name;
|
|
713
|
-
}
|
|
714
|
-
const last = named.at(-1);
|
|
715
|
-
if (!last) return void 0;
|
|
716
|
-
return {
|
|
717
|
-
css: cssName,
|
|
718
|
-
cx: resolved.cx,
|
|
719
|
-
leaf: resolved[LEAF_HELPER],
|
|
720
|
-
insert: {
|
|
721
|
-
pos: last.getEnd(),
|
|
722
|
-
names: missing
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
//#endregion
|
|
728
|
-
//#region src/fold-jsx.ts
|
|
729
|
-
const RANK = {
|
|
730
|
-
constant: 0,
|
|
731
|
-
reads: 1,
|
|
732
|
-
unknown: 2
|
|
733
|
-
};
|
|
734
|
-
const worst = (a, b) => RANK[a] >= RANK[b] ? a : b;
|
|
735
|
-
const purityOf = (node) => {
|
|
736
|
-
if (ts_morph.Node.isIdentifier(node)) return "reads";
|
|
737
|
-
if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNumericLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node) || ts_morph.Node.isTrueLiteral(node) || ts_morph.Node.isFalseLiteral(node) || node.getKind() === ts_morph.SyntaxKind.NullKeyword) return "constant";
|
|
738
|
-
if (ts_morph.Node.isPrefixUnaryExpression(node) && ts_morph.Node.isNumericLiteral(node.getOperand())) return "constant";
|
|
739
|
-
if (ts_morph.Node.isObjectLiteralExpression(node)) return node.getProperties().reduce((acc, property) => {
|
|
740
|
-
if (!ts_morph.Node.isPropertyAssignment(property)) return "unknown";
|
|
741
|
-
if (ts_morph.Node.isComputedPropertyName(property.getNameNode())) return "unknown";
|
|
742
|
-
const value = property.getInitializer();
|
|
743
|
-
return worst(acc, value ? purityOf(value) : "unknown");
|
|
744
|
-
}, "constant");
|
|
745
|
-
if (ts_morph.Node.isArrayLiteralExpression(node)) return node.getElements().reduce((acc, element) => worst(acc, purityOf(element)), "constant");
|
|
746
|
-
return "unknown";
|
|
747
|
-
};
|
|
748
|
-
/**
|
|
749
|
-
* The same question for a whole attribute.
|
|
750
|
-
*
|
|
751
|
-
* A `JsxElement` or `JsxFragment` initializer — `title=<Tag x={f()} />` — is legal, holds
|
|
752
|
-
* arbitrary expressions, and is not a `JsxExpression`, so this asks what an initializer
|
|
753
|
-
* *is* rather than listing the kinds that carry code.
|
|
754
|
-
*/
|
|
755
|
-
const attributePurity = (attribute) => {
|
|
756
|
-
const initializer = attribute.getInitializer();
|
|
757
|
-
if (!initializer) return "constant";
|
|
758
|
-
if (ts_morph.Node.isStringLiteral(initializer)) return "constant";
|
|
759
|
-
if (!ts_morph.Node.isJsxExpression(initializer)) return "unknown";
|
|
760
|
-
const expression = initializer.getExpression();
|
|
761
|
-
return expression ? purityOf(expression) : "constant";
|
|
762
|
-
};
|
|
763
|
-
/**
|
|
764
|
-
* Props the factory gives extra *styling* meaning to, which is why they block a fold.
|
|
765
|
-
*
|
|
766
|
-
* `unstyled` skips the recipe and `css` merges at a higher precedence than the style
|
|
767
|
-
* props, so neither is expressible as a class the fold can compute alongside the rest.
|
|
768
|
-
*
|
|
769
|
-
* `ref`, `key` and `children` used to be here too, and do not belong: none of them
|
|
770
|
-
* changes what the element is styled with. The factory takes `ref` through `forwardRef`
|
|
771
|
-
* and hands it straight to `createElement`, so an intrinsic tag — or whatever `as` names
|
|
772
|
-
* — receives the identical prop. `key` never reaches the component at all; React consumes
|
|
773
|
-
* it. And `children ?? combinedProps.children` mirrors `createElement`'s own rule that
|
|
774
|
-
* the third argument beats `props.children`. Each is a passthrough, and travels as one.
|
|
775
|
-
*/
|
|
776
|
-
const RESERVED_PROPS = new Set(["unstyled", "css"]);
|
|
777
|
-
/**
|
|
778
|
-
* Props that carry no styling, and so block a fold only where the framework gives them a
|
|
779
|
-
* meaning the rewrite would change.
|
|
780
|
-
*/
|
|
781
|
-
const MECHANICAL_PROPS = new Set([
|
|
782
|
-
"ref",
|
|
783
|
-
"key",
|
|
784
|
-
"children"
|
|
785
|
-
]);
|
|
786
|
-
/**
|
|
787
|
-
* Where a mechanical prop may travel as an ordinary passthrough.
|
|
788
|
-
*
|
|
789
|
-
* React only, and measured rather than reasoned about. Its factory forwards the ref to
|
|
790
|
-
* the element it renders, so moving the ref onto that element changes nothing.
|
|
791
|
-
*
|
|
792
|
-
* Preact was in this list on the strength of `forwardRef` appearing in its factory too,
|
|
793
|
-
* and that inference was wrong: under this repo's compat setup an unfolded
|
|
794
|
-
* `<styled.div ref={r}>` binds the *component instance* and the folded `<div ref={r}>`
|
|
795
|
-
* binds the DOM node. A hand-written `forwardRef` behaves the same way, so it is Preact's
|
|
796
|
-
* own handling rather than anything the factory does. Vue diverges for the plain reason —
|
|
797
|
-
* a ref on a component is the instance, on an element it is the node.
|
|
798
|
-
*
|
|
799
|
-
* An allowlist rather than a denylist, because the failure is silent and the list of
|
|
800
|
-
* runtimes is open. Every other framework keeps the behaviour it had before.
|
|
801
|
-
*/
|
|
802
|
-
const MECHANICAL_FRAMEWORKS = new Set(["react"]);
|
|
803
|
-
/**
|
|
804
|
-
* Is this a tag JSX reads as an intrinsic element?
|
|
805
|
-
*
|
|
806
|
-
* Anchored and dot-free, because both halves matter. `Section` is a variable reference,
|
|
807
|
-
* and `foo.bar` is a member expression — a property read off something in scope — where
|
|
808
|
-
* the runtime would have created an element named literally that.
|
|
809
|
-
*/
|
|
810
|
-
const isIntrinsicTag = (tag) => /^[a-z][\w-]*$/.test(tag);
|
|
811
|
-
/**
|
|
812
|
-
* The tag an `as` prop names, when it names one statically.
|
|
813
|
-
*
|
|
814
|
-
* The factory destructures `{ as: Element = __base__ }` and hands `Element` to
|
|
815
|
-
* `createElement`, so a static `as` is simply a different tag with the same class and
|
|
816
|
-
* the same forwarded props — `splitProps` keys off the factory's own config, not off
|
|
817
|
-
* what `as` points at, so the split is unchanged.
|
|
818
|
-
*
|
|
819
|
-
* Casing is load-bearing, because JSX and `createElement` disagree about it. JSX reads a
|
|
820
|
-
* lowercase tag as an intrinsic element and a capitalised one as a variable, while
|
|
821
|
-
* `createElement` takes a string as intrinsic and anything else as a component. So the
|
|
822
|
-
* two forms only survive the rewrite when their casing already agrees:
|
|
823
|
-
*
|
|
824
|
-
* - `as="section"` -> `<section>`, intrinsic both ways.
|
|
825
|
-
* - `as={Link}` -> `<Link>`, a component reference both ways.
|
|
826
|
-
*
|
|
827
|
-
* The mismatched pair render something else entirely. `as={thing}` would fold to
|
|
828
|
-
* `<thing>`, a DOM element named `thing` rather than the component; `as="Section"` would
|
|
829
|
-
* fold to `<Section>`, a variable reference rather than the intrinsic the factory would
|
|
830
|
-
* have created. Both bail.
|
|
831
|
-
*
|
|
832
|
-
* A dot is the same hazard spelled differently, and it survives lowercasing. `<foo.bar>`
|
|
833
|
-
* is a JSX member expression — `createElement(foo.bar)`, a property read off a variable
|
|
834
|
-
* in scope — where the factory would have created an intrinsic element named literally
|
|
835
|
-
* `foo.bar`. So a dotted value bails even though its casing agrees.
|
|
836
|
-
*/
|
|
837
|
-
const asTag = (attribute) => {
|
|
838
|
-
const initializer = attribute.getInitializer();
|
|
839
|
-
if (!initializer) return void 0;
|
|
840
|
-
if (ts_morph.Node.isStringLiteral(initializer)) {
|
|
841
|
-
const value = initializer.getLiteralValue();
|
|
842
|
-
return /^[a-z][\w-]*$/.test(value) ? value : void 0;
|
|
843
|
-
}
|
|
844
|
-
if (!ts_morph.Node.isJsxExpression(initializer)) return void 0;
|
|
845
|
-
const expression = initializer.getExpression();
|
|
846
|
-
if (!expression || !ts_morph.Node.isIdentifier(expression)) return void 0;
|
|
847
|
-
const name = expression.getText();
|
|
848
|
-
return /^[A-Z]/.test(name) ? name : void 0;
|
|
849
|
-
};
|
|
850
|
-
/**
|
|
851
|
-
* `normalizeHTMLProps` renames these on the way to the DOM (`htmlSize` -> `size`).
|
|
852
|
-
* Reproducing the rename is easy; noticing that it exists is the hard part, so they bail.
|
|
853
|
-
*/
|
|
854
|
-
const HTML_PROPS = new Set([
|
|
855
|
-
"htmlSize",
|
|
856
|
-
"htmlTranslate",
|
|
857
|
-
"htmlWidth",
|
|
858
|
-
"htmlHeight"
|
|
859
|
-
]);
|
|
860
|
-
/**
|
|
861
|
-
* The intrinsic tag a factory expression names, if it names one statically.
|
|
862
|
-
*
|
|
863
|
-
* Only `styled.div` and friends fold. `styled(Component)` and `styled('div')` are call
|
|
864
|
-
* expressions whose result is bound elsewhere, and a capitalised tag is a component
|
|
865
|
-
* rather than an intrinsic element.
|
|
866
|
-
*/
|
|
867
|
-
const intrinsicTag = (tagName, factoryName) => {
|
|
868
|
-
const prefix = `${factoryName}.`;
|
|
869
|
-
if (!tagName.startsWith(prefix)) return void 0;
|
|
870
|
-
const tag = tagName.slice(prefix.length);
|
|
871
|
-
if (!/^[a-z][a-z0-9-]*$/.test(tag)) return void 0;
|
|
872
|
-
return tag;
|
|
873
|
-
};
|
|
874
|
-
/**
|
|
875
|
-
* Collapse a pattern element (`<Stack gap="4">`) to the tag it renders.
|
|
876
|
-
*
|
|
877
|
-
* A pattern component is a second layer on top of the factory: it splits its own props
|
|
878
|
-
* out, runs them through the pattern's transform, and hands the result to
|
|
879
|
-
* `styled.<jsxElement>`, which then does everything described above. Folding one removes
|
|
880
|
-
* both layers.
|
|
881
|
-
*
|
|
882
|
-
* The class is computed through `patterns.transform`, the same call the encoder makes
|
|
883
|
-
* when it decides what css to emit — so a folded pattern class is backed by a rule by
|
|
884
|
-
* construction, and the render-parity test is what confirms it also matches the runtime.
|
|
885
|
-
*
|
|
886
|
-
* Only the default `jsxStyleProps: 'all'` folds. Under `minimal` and `none` the pattern's
|
|
887
|
-
* styles reach the factory through the `css` prop instead of being spread, which reverses
|
|
888
|
-
* which side wins when a prop is set in both places.
|
|
889
|
-
*/
|
|
890
|
-
const planPatternFold = (item, ctx, runtimeCss) => {
|
|
891
|
-
const node = item.box?.getNode?.();
|
|
892
|
-
if (!node || !ts_morph.Node.isJsxOpeningElement(node) && !ts_morph.Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
|
|
893
|
-
if (ctx.jsx.styleProps !== "all") return { reason: "unsupported-kind" };
|
|
894
|
-
const jsxName = node.getTagNameNode().getText();
|
|
895
|
-
const detail = ctx.patterns.details.find((entry) => entry.jsxName === jsxName);
|
|
896
|
-
if (!detail) return { reason: "unsupported-kind" };
|
|
897
|
-
const styles = item.data?.[0] ?? {};
|
|
898
|
-
const passthrough = [];
|
|
899
|
-
let staticClassName = "";
|
|
900
|
-
let sawChildrenProp = false;
|
|
901
|
-
let tag = detail.config.jsxElement ?? "div";
|
|
902
|
-
let tagFromAs = false;
|
|
903
|
-
for (const attribute of node.getAttributes()) {
|
|
904
|
-
if (!ts_morph.Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
|
|
905
|
-
const name = attribute.getNameNode().getText();
|
|
906
|
-
if (name === "className") {
|
|
907
|
-
const initializer = attribute.getInitializer();
|
|
908
|
-
if (!initializer || !ts_morph.Node.isStringLiteral(initializer)) return { reason: "dynamic" };
|
|
909
|
-
staticClassName = initializer.getLiteralValue();
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
if (name === "as") {
|
|
913
|
-
const resolved = asTag(attribute);
|
|
914
|
-
if (!resolved) return { reason: "dynamic" };
|
|
915
|
-
tag = resolved;
|
|
916
|
-
tagFromAs = true;
|
|
917
|
-
continue;
|
|
918
|
-
}
|
|
919
|
-
if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
|
|
920
|
-
if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
|
|
921
|
-
if (name === "children") sawChildrenProp = true;
|
|
922
|
-
if (detail.props.includes(name) || ctx.isValidProperty(name)) {
|
|
923
|
-
if (!(name in styles)) return { reason: "dynamic" };
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
passthrough.push(attribute.getText());
|
|
927
|
-
}
|
|
928
|
-
if (!tagFromAs && !isIntrinsicTag(tag)) return { reason: "unsupported-kind" };
|
|
929
|
-
if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
|
|
930
|
-
let resolved;
|
|
931
|
-
try {
|
|
932
|
-
resolved = runtimeCss(ctx.patterns.transform(detail.baseName, styles));
|
|
933
|
-
} catch {
|
|
934
|
-
return { reason: "dynamic" };
|
|
935
|
-
}
|
|
936
|
-
const className = [resolved, staticClassName].filter(Boolean).join(" ");
|
|
937
|
-
if (!className) return { reason: "dynamic" };
|
|
938
|
-
return buildEdits(node, tag, passthrough, className);
|
|
939
|
-
};
|
|
940
|
-
const planJsxFold = (item, ctx, runtimeCss, deps) => {
|
|
941
|
-
const node = item.box?.getNode?.();
|
|
942
|
-
if (!node || !ts_morph.Node.isJsxOpeningElement(node) && !ts_morph.Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
|
|
943
|
-
const baseTag = intrinsicTag(node.getTagNameNode().getText(), ctx.jsx.factoryName);
|
|
944
|
-
if (!baseTag) return { reason: "unsupported-kind" };
|
|
945
|
-
let tag = baseTag;
|
|
946
|
-
const styles = item.data?.[0] ?? {};
|
|
947
|
-
const propBoxes = _bamboocss_extractor.box.isMap(item.box) ? item.box.value : void 0;
|
|
948
|
-
const passthrough = [];
|
|
949
|
-
const staticProps = [];
|
|
950
|
-
const dynamicProps = [];
|
|
951
|
-
let staticClassName = "";
|
|
952
|
-
let dynamicClassName = "";
|
|
953
|
-
let sawClassName = false;
|
|
954
|
-
let sawChildrenProp = false;
|
|
955
|
-
/**
|
|
956
|
-
* Where a dynamic `className` was written, and where the attributes that outlive the
|
|
957
|
-
* fold were.
|
|
958
|
-
*
|
|
959
|
-
* The factory appends `className` after the styles, so a folded one is emitted last and
|
|
960
|
-
* anything written after it runs before it instead. That covers more than the style
|
|
961
|
-
* props — a passthrough keeps its own place among the attributes — but only where the
|
|
962
|
-
* expression survives at all. A static style prop is not among them: it is *deleted*,
|
|
963
|
-
* its value having been resolved at build time. That is a larger change than reordering
|
|
964
|
-
* and a separate pre-existing gap — `<styled.div color={counted()} />` folds and never
|
|
965
|
-
* calls it, with or without this — rather than a reason the reordering is moot.
|
|
966
|
-
*/
|
|
967
|
-
let classNameIndex = -1;
|
|
968
|
-
let classNamePurity = "constant";
|
|
969
|
-
const survivors = [];
|
|
970
|
-
let index = -1;
|
|
971
|
-
for (const attribute of node.getAttributes()) {
|
|
972
|
-
if (!ts_morph.Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
|
|
973
|
-
index += 1;
|
|
974
|
-
const name = attribute.getNameNode().getText();
|
|
975
|
-
if (name === "className") {
|
|
976
|
-
const initializer = attribute.getInitializer();
|
|
977
|
-
if (!initializer) return { reason: "dynamic" };
|
|
978
|
-
if (sawClassName && (dynamicClassName || !ts_morph.Node.isStringLiteral(initializer))) return { reason: "dynamic" };
|
|
979
|
-
sawClassName = true;
|
|
980
|
-
if (ts_morph.Node.isStringLiteral(initializer)) {
|
|
981
|
-
staticClassName = initializer.getLiteralValue();
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
const expression = ts_morph.Node.isJsxExpression(initializer) ? initializer.getExpression() : void 0;
|
|
985
|
-
if (!expression) return { reason: "dynamic" };
|
|
986
|
-
dynamicClassName = expression.getText();
|
|
987
|
-
classNameIndex = index;
|
|
988
|
-
classNamePurity = purityOf(expression);
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
if (name === "as") {
|
|
992
|
-
const resolved = asTag(attribute);
|
|
993
|
-
if (!resolved) return { reason: "dynamic" };
|
|
994
|
-
survivors.push({
|
|
995
|
-
index,
|
|
996
|
-
purity: attributePurity(attribute)
|
|
997
|
-
});
|
|
998
|
-
tag = resolved;
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
|
|
1002
|
-
if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
|
|
1003
|
-
if (name === "children") sawChildrenProp = true;
|
|
1004
|
-
if (ctx.isValidProperty(name)) {
|
|
1005
|
-
const attributeValue = attribute.getInitializer();
|
|
1006
|
-
const valueExpression = attributeValue && ts_morph.Node.isJsxExpression(attributeValue) ? attributeValue.getExpression() : void 0;
|
|
1007
|
-
const propBox = propBoxes?.get(name);
|
|
1008
|
-
if (name in styles && isStaticBox(propBox) && accountsForSource(valueExpression, propBox)) {
|
|
1009
|
-
staticProps.push(name);
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
if (!valueExpression) return { reason: "dynamic" };
|
|
1013
|
-
const expression = valueExpression;
|
|
1014
|
-
survivors.push({
|
|
1015
|
-
index,
|
|
1016
|
-
purity: attributePurity(attribute)
|
|
1017
|
-
});
|
|
1018
|
-
dynamicProps.push({
|
|
1019
|
-
name,
|
|
1020
|
-
text: expression.getText(),
|
|
1021
|
-
expression
|
|
1022
|
-
});
|
|
1023
|
-
continue;
|
|
1024
|
-
}
|
|
1025
|
-
survivors.push({
|
|
1026
|
-
index,
|
|
1027
|
-
purity: attributePurity(attribute)
|
|
1028
|
-
});
|
|
1029
|
-
passthrough.push(attribute.getText());
|
|
1030
|
-
}
|
|
1031
|
-
if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
|
|
1032
|
-
/**
|
|
1033
|
-
* Would emitting the className last move it past something that can observe it?
|
|
1034
|
-
*
|
|
1035
|
-
* A constant survivor commutes with anything. One that only reads commutes only while
|
|
1036
|
-
* the className expression cannot write — `className={cn} onClick={h}` is safe, and
|
|
1037
|
-
* `className={assigns()} bg={tone}` is not, because moving the read after the write
|
|
1038
|
-
* hands it the other value.
|
|
1039
|
-
*
|
|
1040
|
-
* This answers for the className and nothing else. `buildEdits` emits
|
|
1041
|
-
* `[...passthrough, className={cx(…)}]`, so a passthrough is also hoisted ahead of every
|
|
1042
|
-
* dynamic style prop's expression — `<styled.div bg={writes()} data-x={reads} />`
|
|
1043
|
-
* reorders those two with no className present at all. That is pre-existing and
|
|
1044
|
-
* reproduces on an unchanged tree; folding a dynamic className only makes it reachable
|
|
1045
|
-
* for more elements. Closing it means comparing every survivor against everything it
|
|
1046
|
-
* crosses rather than against one attribute, which is a different change.
|
|
1047
|
-
*/
|
|
1048
|
-
const reordered = () => classNameIndex >= 0 && survivors.some((entry) => entry.index > classNameIndex && entry.purity !== "constant" && !(entry.purity === "reads" && classNamePurity !== "unknown"));
|
|
1049
|
-
if (dynamicProps.length) {
|
|
1050
|
-
if (!deps || item.data.length !== 1) return { reason: "dynamic" };
|
|
1051
|
-
if (collides(staticProps, dynamicProps.map((prop) => prop.name), ctx)) return { reason: "dynamic" };
|
|
1052
|
-
const resolveProp = (name) => ctx.utility.hasShorthand ? ctx.utility.resolveShorthand(name) : name;
|
|
1053
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
1054
|
-
for (const prop of dynamicProps) claimed.set(resolveProp(prop.name), (claimed.get(resolveProp(prop.name)) ?? 0) + 1);
|
|
1055
|
-
const prefixes = /* @__PURE__ */ new Map();
|
|
1056
|
-
for (const prop of dynamicProps) {
|
|
1057
|
-
if (claimed.get(resolveProp(prop.name)) !== 1) continue;
|
|
1058
|
-
if (isWrittenAsCollection(prop.expression)) continue;
|
|
1059
|
-
const prefix = leafPrefix(prop.name, ctx, runtimeCss);
|
|
1060
|
-
if (prefix !== void 0) prefixes.set(prop.name, prefix);
|
|
1061
|
-
}
|
|
1062
|
-
const kinds = dynamicProps.map((prop) => prefixes.has(prop.name) ? "l" : "r").join("");
|
|
1063
|
-
if (!/^l*r*$/.test(kinds) && !/^r*l*$/.test(kinds)) prefixes.clear();
|
|
1064
|
-
if (reordered()) return { reason: "dynamic" };
|
|
1065
|
-
const helpers = resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed, prefixes.size > 0);
|
|
1066
|
-
if (!helpers) return { reason: "dynamic" };
|
|
1067
|
-
if (!helpers.leaf) prefixes.clear();
|
|
1068
|
-
const staticStyles = {};
|
|
1069
|
-
for (const name of staticProps) staticStyles[name] = styles[name];
|
|
1070
|
-
const resolved = [runtimeCss(staticStyles), staticClassName].filter(Boolean).join(" ");
|
|
1071
|
-
const lowered = dynamicProps.filter((prop) => prefixes.has(prop.name)).map((prop) => leafCall(prefixes.get(prop.name), prop.name, prop.text, helpers.leaf));
|
|
1072
|
-
const residue = dynamicProps.filter((prop) => !prefixes.has(prop.name));
|
|
1073
|
-
const runtime = residue.length ? [`${helpers.css}({ ${residue.map((prop) => `${prop.name}: ${prop.text}`).join(", ")} })`] : [];
|
|
1074
|
-
if (!resolved && !lowered.length) return { reason: "dynamic" };
|
|
1075
|
-
const ordered = kinds.startsWith("r") ? [...runtime, ...lowered] : [...lowered, ...runtime];
|
|
1076
|
-
const parts = dynamicClassName ? [...ordered, dynamicClassName] : ordered;
|
|
1077
|
-
const plan = buildEdits(node, tag, passthrough, resolved, `${helpers.cx}(${[...resolved ? [JSON.stringify(resolved)] : [], ...parts].join(", ")})`);
|
|
1078
|
-
return "reason" in plan ? plan : {
|
|
1079
|
-
...plan,
|
|
1080
|
-
insert: helpers.insert
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
if (item.data.length !== 1) return { reason: "dynamic" };
|
|
1084
|
-
const className = [runtimeCss(styles), staticClassName].filter(Boolean).join(" ");
|
|
1085
|
-
if (dynamicClassName) {
|
|
1086
|
-
if (reordered()) return { reason: "dynamic" };
|
|
1087
|
-
const helpers = deps && resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed);
|
|
1088
|
-
if (!helpers) return { reason: "dynamic" };
|
|
1089
|
-
const args = [...className ? [JSON.stringify(className)] : [], dynamicClassName];
|
|
1090
|
-
const plan = buildEdits(node, tag, passthrough, className, `${helpers.cx}(${args.join(", ")})`);
|
|
1091
|
-
return "reason" in plan ? plan : {
|
|
1092
|
-
...plan,
|
|
1093
|
-
insert: helpers.insert
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
if (!className) return { reason: "dynamic" };
|
|
1097
|
-
return buildEdits(node, tag, passthrough, className);
|
|
1098
|
-
};
|
|
1099
|
-
/** Rewrite the opening element, and the closing one when there is a pair. */
|
|
1100
|
-
const buildEdits = (node, tag, passthrough, className, classExpression) => {
|
|
1101
|
-
const attributes = [...passthrough, `className={${classExpression ?? JSON.stringify(className)}}`].join(" ");
|
|
1102
|
-
const selfClosing = ts_morph.Node.isJsxSelfClosingElement(node);
|
|
1103
|
-
const edits = [{
|
|
1104
|
-
start: node.getStart(),
|
|
1105
|
-
end: node.getEnd(),
|
|
1106
|
-
text: `<${tag} ${attributes}${selfClosing ? " />" : ">"}`
|
|
1107
|
-
}];
|
|
1108
|
-
let end = node.getEnd();
|
|
1109
|
-
if (!selfClosing) {
|
|
1110
|
-
const parent = node.getParent();
|
|
1111
|
-
if (!ts_morph.Node.isJsxElement(parent)) return { reason: "unsupported-kind" };
|
|
1112
|
-
const closing = parent.getClosingElement();
|
|
1113
|
-
edits.push({
|
|
1114
|
-
start: closing.getStart(),
|
|
1115
|
-
end: closing.getEnd(),
|
|
1116
|
-
text: `</${tag}>`
|
|
1117
|
-
});
|
|
1118
|
-
end = closing.getEnd();
|
|
1119
|
-
}
|
|
1120
|
-
return {
|
|
1121
|
-
edits,
|
|
1122
|
-
className,
|
|
1123
|
-
start: node.getStart(),
|
|
1124
|
-
end
|
|
1125
|
-
};
|
|
1126
|
-
};
|
|
1127
674
|
//#endregion
|
|
1128
675
|
//#region src/runtime-css.ts
|
|
1129
676
|
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
@@ -1149,9 +696,30 @@ const createRuntimeCss = (ctx) => {
|
|
|
1149
696
|
const { mergeCss } = (0, _bamboocss_shared.createMergeCss)(cssContext);
|
|
1150
697
|
return (...styles) => cssFn(mergeCss(...styles));
|
|
1151
698
|
};
|
|
1152
|
-
|
|
699
|
+
/**
|
|
700
|
+
* The map is every token in the project, so it is built once per context and shared by
|
|
701
|
+
* every module in the build — not once per `foldSource`, which would price a whole token
|
|
702
|
+
* table into each of the overwhelming majority of modules that call `token()` zero times.
|
|
703
|
+
* Keyed weakly so a context that goes out of scope takes its table with it.
|
|
704
|
+
*/
|
|
705
|
+
const tokenValues = /* @__PURE__ */ new WeakMap();
|
|
706
|
+
const tokenValuesFor = (ctx) => {
|
|
707
|
+
let values = tokenValues.get(ctx);
|
|
708
|
+
if (values) return values;
|
|
709
|
+
values = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const token of ctx.tokens.allTokens) {
|
|
711
|
+
const { varRef, isVirtual, condition } = token.extensions;
|
|
712
|
+
values.set(token.name, isVirtual || condition !== "base" ? varRef : token.value);
|
|
713
|
+
}
|
|
714
|
+
tokenValues.set(ctx, values);
|
|
715
|
+
return values;
|
|
716
|
+
};
|
|
717
|
+
const createRuntimeToken = (ctx) => (path) => {
|
|
718
|
+
const value = tokenValuesFor(ctx).get(path);
|
|
719
|
+
return typeof value === "string" ? value : void 0;
|
|
720
|
+
};
|
|
721
|
+
const createRuntimeRecipe = (ctx) => {
|
|
1153
722
|
const separator = ctx.utility.separator;
|
|
1154
|
-
const { mergeCss } = (0, _bamboocss_shared.createMergeCss)(createCssContext(ctx));
|
|
1155
723
|
return (name, variants) => {
|
|
1156
724
|
const config = ctx.recipes.getConfig(name);
|
|
1157
725
|
const node = ctx.recipes.getRecipe(name);
|
|
@@ -1183,40 +751,28 @@ const createRuntimeRecipe = (ctx, runtimeCss) => {
|
|
|
1183
751
|
...(0, _bamboocss_shared.compact)(variants)
|
|
1184
752
|
};
|
|
1185
753
|
if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
|
|
1186
|
-
|
|
1187
|
-
return [recipeCss(recipeStyles), runtimeCss(compoundStyles)].filter(Boolean).join(" ");
|
|
754
|
+
return recipeCss(recipeStyles);
|
|
1188
755
|
};
|
|
1189
756
|
};
|
|
1190
|
-
/**
|
|
1191
|
-
* Mirrors the function of the same name in the generated `cva` artifact, down to the
|
|
1192
|
-
* `mergeCss` it accumulates with.
|
|
1193
|
-
*
|
|
1194
|
-
* That merge has to be the deep one. More than one compound variant can match a single
|
|
1195
|
-
* selection, and their `css` objects then combine rather than replace: `_hover` set by
|
|
1196
|
-
* one and `_hover` set by another have to end up as a single condition holding both
|
|
1197
|
-
* declarations. `Object.assign` drops everything the earlier match contributed under a
|
|
1198
|
-
* shared key, which produces a shorter class list and no error at all.
|
|
1199
|
-
*
|
|
1200
|
-
* Taking `mergeCss` as an argument rather than importing one keeps it the same instance
|
|
1201
|
-
* the rest of the fold resolves through, built from the same context.
|
|
1202
|
-
*/
|
|
1203
|
-
const getCompoundVariantCss = (compoundVariants, variantMap, mergeCss) => {
|
|
1204
|
-
let result = {};
|
|
1205
|
-
for (const compoundVariant of compoundVariants) {
|
|
1206
|
-
if (!compoundVariant) continue;
|
|
1207
|
-
if (Object.entries(compoundVariant).every(([key, value]) => {
|
|
1208
|
-
if (key === "css") return true;
|
|
1209
|
-
return (Array.isArray(value) ? value : [value]).some((entry) => variantMap[key] === entry);
|
|
1210
|
-
})) result = mergeCss(result, compoundVariant.css);
|
|
1211
|
-
}
|
|
1212
|
-
return result;
|
|
1213
|
-
};
|
|
1214
757
|
//#endregion
|
|
1215
758
|
//#region src/fold.ts
|
|
1216
759
|
/**
|
|
1217
|
-
* `cva`/`sva` return a function
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
760
|
+
* `cva`/`sva` return a function, so neither can collapse to a class string. Their
|
|
761
|
+
* *invocations* could, but those are separate call sites the parser does not record as
|
|
762
|
+
* such. `token` also resolves to no class, but it does resolve to a literal, so it folds
|
|
763
|
+
* through its own path rather than being declined outright.
|
|
764
|
+
*
|
|
765
|
+
* Folding an invocation is now *possible* in a way it was not: a recipe's classes are named
|
|
766
|
+
* semantically, so the build knows every class a call can produce from the config alone.
|
|
767
|
+
* What is missing is upstream — the parser matches calls by imported name, so a local
|
|
768
|
+
* `button()` from `const button = cva(...)` is never recorded, and tracking those bindings
|
|
769
|
+
* is a change to the extractor rather than to this set.
|
|
770
|
+
*
|
|
771
|
+
* Worth knowing before taking that on: semantic naming already took most of the prize.
|
|
772
|
+
* `cvaFn` used to run `mergeCss` and name a class per property on every call; it is now a
|
|
773
|
+
* memoized loop over `variantKeys` doing string concatenation. That is an inspection of the
|
|
774
|
+
* two implementations, not a measurement — benchmark it before deciding it is worth the
|
|
775
|
+
* extractor work.
|
|
1220
776
|
*/
|
|
1221
777
|
const FOLDABLE_TYPES = new Set([
|
|
1222
778
|
"css",
|
|
@@ -1236,13 +792,16 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
1236
792
|
* phase — hence separate from `unsupported-kind`, where a slot recipe lands because it
|
|
1237
793
|
* resolves to one class per slot rather than to a single string.
|
|
1238
794
|
*/
|
|
1239
|
-
const UNFOLDABLE_TYPES = new Set([
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
795
|
+
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
796
|
+
/**
|
|
797
|
+
* An argument that cannot run anything when it is evaluated.
|
|
798
|
+
*
|
|
799
|
+
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
800
|
+
* the fallback also drops whatever evaluating it would have done. `token('x', compute())`
|
|
801
|
+
* is pathological, but the fold's contract is behaviour preservation and a literal is the
|
|
802
|
+
* cheap way to prove it: no call, no property read, no getter.
|
|
803
|
+
*/
|
|
804
|
+
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";
|
|
1246
805
|
/**
|
|
1247
806
|
* Source files a box tree reaches, other than the one being folded.
|
|
1248
807
|
*
|
|
@@ -1316,17 +875,6 @@ const calleeRootName = (call) => {
|
|
|
1316
875
|
return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
|
|
1317
876
|
};
|
|
1318
877
|
/**
|
|
1319
|
-
* The identifier a JSX tag is rooted at: `styled` for `<styled.div>`, `bamboo` for
|
|
1320
|
-
* `<bamboo.styled.div>`. The same question `calleeRootName` answers for a call, and it
|
|
1321
|
-
* feeds the same import and shadowing checks.
|
|
1322
|
-
*/
|
|
1323
|
-
const tagRootName = (element) => {
|
|
1324
|
-
if (!ts_morph.Node.isJsxOpeningElement(element) && !ts_morph.Node.isJsxSelfClosingElement(element)) return void 0;
|
|
1325
|
-
let current = element.getTagNameNode();
|
|
1326
|
-
while (ts_morph.Node.isPropertyAccessExpression(current)) current = current.getExpression();
|
|
1327
|
-
return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
|
|
1328
|
-
};
|
|
1329
|
-
/**
|
|
1330
878
|
* Local names a module binds to an import of bamboo's own generated system.
|
|
1331
879
|
*
|
|
1332
880
|
* The parser matches by name and asks neither question this does — deliberately, since
|
|
@@ -1428,7 +976,7 @@ const argumentsAccountedFor = (call, boxNode) => {
|
|
|
1428
976
|
return accountsForSource(args[0], boxNode);
|
|
1429
977
|
};
|
|
1430
978
|
const foldSource = (options) => {
|
|
1431
|
-
const { ctx, code, parserResult,
|
|
979
|
+
const { ctx, code, parserResult, partial: partial_ = true, runtimeCss = createRuntimeCss(ctx) } = options;
|
|
1432
980
|
/**
|
|
1433
981
|
* Recover the static half of a call the whole-call path gave up on. Only a
|
|
1434
982
|
* single-argument `css()` qualifies: a pattern or recipe call takes props rather than a
|
|
@@ -1483,7 +1031,8 @@ const foldSource = (options) => {
|
|
|
1483
1031
|
insert: cx.insert
|
|
1484
1032
|
};
|
|
1485
1033
|
};
|
|
1486
|
-
const runtimeRecipe = createRuntimeRecipe(ctx
|
|
1034
|
+
const runtimeRecipe = createRuntimeRecipe(ctx);
|
|
1035
|
+
const runtimeToken = createRuntimeToken(ctx);
|
|
1487
1036
|
/**
|
|
1488
1037
|
* Does this specifier name a module that exports the css API, exactly?
|
|
1489
1038
|
*
|
|
@@ -1525,11 +1074,6 @@ const foldSource = (options) => {
|
|
|
1525
1074
|
};
|
|
1526
1075
|
const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
|
|
1527
1076
|
const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
|
|
1528
|
-
const jsxDeps = {
|
|
1529
|
-
isBambooCssModule,
|
|
1530
|
-
isGeneratedCssModule,
|
|
1531
|
-
isShadowed
|
|
1532
|
-
};
|
|
1533
1077
|
const folded = [];
|
|
1534
1078
|
const skipped = [];
|
|
1535
1079
|
const candidates = [];
|
|
@@ -1548,12 +1092,8 @@ const foldSource = (options) => {
|
|
|
1548
1092
|
const name = item.name ?? type;
|
|
1549
1093
|
if (!item.box) continue;
|
|
1550
1094
|
const call = findCallExpression(item.box);
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
1553
|
-
if (!element) continue;
|
|
1554
|
-
const elementStart = element.getStart();
|
|
1555
|
-
const elementEnd = element.getEnd();
|
|
1556
|
-
if (code.slice(elementStart, elementEnd) !== element.getText()) {
|
|
1095
|
+
if (type === "token") {
|
|
1096
|
+
if (!call) {
|
|
1557
1097
|
skipped.push({
|
|
1558
1098
|
name,
|
|
1559
1099
|
reason: "no-call-expression",
|
|
@@ -1562,34 +1102,85 @@ const foldSource = (options) => {
|
|
|
1562
1102
|
});
|
|
1563
1103
|
continue;
|
|
1564
1104
|
}
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1105
|
+
const start = call.getStart();
|
|
1106
|
+
const end = call.getEnd();
|
|
1107
|
+
if (code.slice(start, end) !== call.getText()) {
|
|
1108
|
+
skipped.push({
|
|
1109
|
+
name,
|
|
1110
|
+
reason: "no-call-expression",
|
|
1111
|
+
start: 0,
|
|
1112
|
+
end: 0
|
|
1113
|
+
});
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
const rangeKey = `${start}:${end}`;
|
|
1117
|
+
if (seenRanges.has(rangeKey)) continue;
|
|
1118
|
+
seenRanges.add(rangeKey);
|
|
1119
|
+
const rootName = calleeRootName(call);
|
|
1120
|
+
if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
|
|
1567
1121
|
skipped.push({
|
|
1568
1122
|
name,
|
|
1569
1123
|
reason: "not-imported",
|
|
1570
|
-
start
|
|
1571
|
-
end
|
|
1124
|
+
start,
|
|
1125
|
+
end
|
|
1126
|
+
});
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const callee = ts_morph.Node.isCallExpression(call) ? call.getExpression() : void 0;
|
|
1130
|
+
if (ts_morph.Node.isPropertyAccessExpression(callee) && !ctx.imports.matchers.tokens.match(callee.getNameNode().getText())) {
|
|
1131
|
+
skipped.push({
|
|
1132
|
+
name,
|
|
1133
|
+
reason: "unsupported-kind",
|
|
1134
|
+
start,
|
|
1135
|
+
end
|
|
1572
1136
|
});
|
|
1573
1137
|
continue;
|
|
1574
1138
|
}
|
|
1575
|
-
|
|
1576
|
-
if ("reason" in plan) {
|
|
1139
|
+
if (!isStaticBox(item.box) || item.data.length !== 1) {
|
|
1577
1140
|
skipped.push({
|
|
1578
1141
|
name,
|
|
1579
|
-
reason:
|
|
1580
|
-
start
|
|
1581
|
-
end
|
|
1142
|
+
reason: "dynamic",
|
|
1143
|
+
start,
|
|
1144
|
+
end
|
|
1145
|
+
});
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const path = item.data[0];
|
|
1149
|
+
if (typeof path !== "string") {
|
|
1150
|
+
skipped.push({
|
|
1151
|
+
name,
|
|
1152
|
+
reason: "dynamic",
|
|
1153
|
+
start,
|
|
1154
|
+
end
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (!(ts_morph.Node.isCallExpression(call) ? call.getArguments().slice(1) : []).every(isInertArgument)) {
|
|
1159
|
+
skipped.push({
|
|
1160
|
+
name,
|
|
1161
|
+
reason: "dynamic",
|
|
1162
|
+
start,
|
|
1163
|
+
end
|
|
1164
|
+
});
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const value = runtimeToken(path);
|
|
1168
|
+
if (!value) {
|
|
1169
|
+
skipped.push({
|
|
1170
|
+
name,
|
|
1171
|
+
reason: "unresolved-token",
|
|
1172
|
+
start,
|
|
1173
|
+
end
|
|
1582
1174
|
});
|
|
1583
1175
|
continue;
|
|
1584
1176
|
}
|
|
1585
1177
|
candidates.push({
|
|
1586
1178
|
item,
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
end: plan.end
|
|
1179
|
+
call,
|
|
1180
|
+
node: call,
|
|
1181
|
+
start,
|
|
1182
|
+
end,
|
|
1183
|
+
value
|
|
1593
1184
|
});
|
|
1594
1185
|
continue;
|
|
1595
1186
|
}
|
|
@@ -1696,7 +1287,7 @@ const foldSource = (options) => {
|
|
|
1696
1287
|
for (const candidate of candidates) {
|
|
1697
1288
|
const { item, start, end } = candidate;
|
|
1698
1289
|
const name = item.name ?? item.type ?? "";
|
|
1699
|
-
const ranges =
|
|
1290
|
+
const ranges = [[start, end]];
|
|
1700
1291
|
if (collides(ranges)) {
|
|
1701
1292
|
skipped.push({
|
|
1702
1293
|
name,
|
|
@@ -1706,26 +1297,28 @@ const foldSource = (options) => {
|
|
|
1706
1297
|
});
|
|
1707
1298
|
continue;
|
|
1708
1299
|
}
|
|
1709
|
-
if (candidate.
|
|
1710
|
-
magic.overwrite(start, end, candidate.
|
|
1711
|
-
applyInsert(candidate.insert);
|
|
1300
|
+
if (candidate.value !== void 0) {
|
|
1301
|
+
magic.overwrite(start, end, JSON.stringify(candidate.value));
|
|
1712
1302
|
applied.push(...ranges);
|
|
1713
1303
|
folded.push({
|
|
1714
1304
|
name,
|
|
1715
|
-
|
|
1716
|
-
|
|
1305
|
+
kind: "value",
|
|
1306
|
+
className: "",
|
|
1307
|
+
classNames: [],
|
|
1308
|
+
value: candidate.value,
|
|
1717
1309
|
start,
|
|
1718
1310
|
end
|
|
1719
1311
|
});
|
|
1720
1312
|
collectSourceFiles(item.box, dependencyScan);
|
|
1721
1313
|
continue;
|
|
1722
1314
|
}
|
|
1723
|
-
if (candidate.
|
|
1724
|
-
|
|
1315
|
+
if (candidate.replacement) {
|
|
1316
|
+
magic.overwrite(start, end, candidate.replacement);
|
|
1725
1317
|
applyInsert(candidate.insert);
|
|
1726
1318
|
applied.push(...ranges);
|
|
1727
1319
|
folded.push({
|
|
1728
1320
|
name,
|
|
1321
|
+
kind: "class",
|
|
1729
1322
|
className: candidate.className,
|
|
1730
1323
|
classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
|
|
1731
1324
|
start,
|
|
@@ -1772,6 +1365,7 @@ const foldSource = (options) => {
|
|
|
1772
1365
|
applied.push(...ranges);
|
|
1773
1366
|
folded.push({
|
|
1774
1367
|
name,
|
|
1368
|
+
kind: "class",
|
|
1775
1369
|
className,
|
|
1776
1370
|
classNames: [className],
|
|
1777
1371
|
start,
|
|
@@ -1846,7 +1440,7 @@ const formatSkipped = (id, skipped) => {
|
|
|
1846
1440
|
* with no matching rule.
|
|
1847
1441
|
*/
|
|
1848
1442
|
const bamboocss = (options = {}) => {
|
|
1849
|
-
const { transform = false,
|
|
1443
|
+
const { transform = false, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
|
|
1850
1444
|
/** Totals across the build, for the summary. */
|
|
1851
1445
|
const totals = {
|
|
1852
1446
|
folded: 0,
|
|
@@ -1879,6 +1473,37 @@ const bamboocss = (options = {}) => {
|
|
|
1879
1473
|
totals.skipped.clear();
|
|
1880
1474
|
await ensureContext();
|
|
1881
1475
|
},
|
|
1476
|
+
/**
|
|
1477
|
+
* Take a changed module out of the parser's hands before the rebuild reads it.
|
|
1478
|
+
*
|
|
1479
|
+
* `addWatchFile` below registers the modules a fold read, so editing one re-transforms
|
|
1480
|
+
* its consumers. That is only half of it. The consumer is transformed *before* the
|
|
1481
|
+
* module it imports — that is how a bundler discovers imports at all — so by the time
|
|
1482
|
+
* the changed module's own `transform` refreshes it in the ts-morph project, the fold
|
|
1483
|
+
* that reads it has already run against the previous contents and baked a stale class
|
|
1484
|
+
* into the bundle. Rollup calls this hook before any of that, which is the only point
|
|
1485
|
+
* where refreshing is early enough.
|
|
1486
|
+
*
|
|
1487
|
+
* Both entry points clear the box-node cache, which is the part that matters: a
|
|
1488
|
+
* resolution memoized against the old contents outlives the file itself.
|
|
1489
|
+
*
|
|
1490
|
+
* A created file is handled as an edit. `reloadSourceFile` cannot re-read one the
|
|
1491
|
+
* parser has never held, and does not need to — it clears the cache, and the extractor
|
|
1492
|
+
* adds a newly-reachable module from disk on next use. What the shared path *is* needed
|
|
1493
|
+
* for is an editor's atomic save, which arrives as a delete followed by a create while
|
|
1494
|
+
* the parser still holds the file.
|
|
1495
|
+
*/
|
|
1496
|
+
watchChange(id, change) {
|
|
1497
|
+
if (!transform || !ctx) return;
|
|
1498
|
+
if (!shouldTransform(id)) return;
|
|
1499
|
+
const [filePath] = id.split("?");
|
|
1500
|
+
if (!filePath) return;
|
|
1501
|
+
if (change.event === "delete") {
|
|
1502
|
+
ctx.project.removeSourceFile(filePath);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
ctx.project.reloadSourceFile(filePath);
|
|
1506
|
+
},
|
|
1882
1507
|
async transform(code, id) {
|
|
1883
1508
|
if (!transform) return null;
|
|
1884
1509
|
if (!shouldTransform(id)) return null;
|
|
@@ -1897,7 +1522,6 @@ const bamboocss = (options = {}) => {
|
|
|
1897
1522
|
parserResult,
|
|
1898
1523
|
filePath,
|
|
1899
1524
|
runtimeCss,
|
|
1900
|
-
jsx,
|
|
1901
1525
|
partial
|
|
1902
1526
|
});
|
|
1903
1527
|
} catch (error) {
|