@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.mjs
CHANGED
|
@@ -172,8 +172,11 @@ const accountsForSource = (node, boxNode) => {
|
|
|
172
172
|
if (!box.isMap(boxNode)) return false;
|
|
173
173
|
for (const property of unwrapped.getProperties()) {
|
|
174
174
|
if (Node.isSpreadAssignment(property)) {
|
|
175
|
-
const expression = property.getExpression();
|
|
176
|
-
if (
|
|
175
|
+
const expression = unwrapExpression(property.getExpression());
|
|
176
|
+
if (Node.isObjectLiteralExpression(expression)) continue;
|
|
177
|
+
const walked = boxNode.resolvedSpreads?.find((entry) => entry.node === expression);
|
|
178
|
+
if (!walked) return false;
|
|
179
|
+
if (!accountsForSource(walked.box.getNode(), walked.box)) return false;
|
|
177
180
|
continue;
|
|
178
181
|
}
|
|
179
182
|
if (Node.isMethodDeclaration(property) || Node.isGetAccessorDeclaration(property) || Node.isSetAccessorDeclaration(property)) return false;
|
|
@@ -410,7 +413,6 @@ const LEAF_SENTINEL = "bamboo0leaf0sentinel0";
|
|
|
410
413
|
* carry the same path or the declined shape resolves without its condition.
|
|
411
414
|
*/
|
|
412
415
|
const leafPrefix = (key, ctx, runtimeCss) => {
|
|
413
|
-
if (ctx.isTemplateLiteralSyntax) return void 0;
|
|
414
416
|
if (ctx.conditions.isCondition(key)) return void 0;
|
|
415
417
|
let resolved;
|
|
416
418
|
try {
|
|
@@ -450,6 +452,9 @@ const planPartialFold = (argument, boxNode, styles, deps) => {
|
|
|
450
452
|
if (!partition) return void 0;
|
|
451
453
|
const className = deps.runtimeCss(partition.staticStyles);
|
|
452
454
|
if (!className && !partition.finite.length) return void 0;
|
|
455
|
+
if (deps.ctx.config.cssMode === "grouped") {
|
|
456
|
+
if ((className ? 1 : 0) + partition.finite.length + (partition.dynamicText.length ? 1 : 0) > 1) return void 0;
|
|
457
|
+
}
|
|
453
458
|
return {
|
|
454
459
|
className,
|
|
455
460
|
dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
|
|
@@ -639,464 +644,6 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
639
644
|
}
|
|
640
645
|
};
|
|
641
646
|
};
|
|
642
|
-
/**
|
|
643
|
-
* The `css` and `cx` bindings a partially folded JSX element needs.
|
|
644
|
-
*
|
|
645
|
-
* Splitting an element sends its dynamic style props to a `css()` call, so unlike the
|
|
646
|
-
* call-site split this needs *two* bindings rather than one. Both are taken from an
|
|
647
|
-
* existing bamboo `css` import: writing a new import declaration would mean guessing a
|
|
648
|
-
* module specifier, and the spelling varies with `importMap`, path aliases and how the
|
|
649
|
-
* project reaches its outdir. An element in a file that does not already import `css` is
|
|
650
|
-
* left alone instead.
|
|
651
|
-
*/
|
|
652
|
-
const resolveCssHelpers = (node, isBambooCssModule, isGeneratedCssModule, isShadowed, wantLeaf = false) => {
|
|
653
|
-
const sourceFile = node.getSourceFile();
|
|
654
|
-
if (!importsAnything(sourceFile, isBambooCssModule)) return void 0;
|
|
655
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
656
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
657
|
-
if (declaration.isTypeOnly() || !isBambooCssModule(mod)) continue;
|
|
658
|
-
const named = declaration.getNamedImports();
|
|
659
|
-
const cssImport = named.find((entry) => entry.getNameNode().getText() === "css" && !entry.isTypeOnly());
|
|
660
|
-
if (!cssImport) continue;
|
|
661
|
-
const cssName = (cssImport.getAliasNode() ?? cssImport.getNameNode()).getText();
|
|
662
|
-
if (isShadowed(node, cssName)) return void 0;
|
|
663
|
-
const wanted = wantLeaf ? ["cx", LEAF_HELPER] : ["cx"];
|
|
664
|
-
const resolved = {};
|
|
665
|
-
const missing = [];
|
|
666
|
-
for (const want of wanted) {
|
|
667
|
-
const existing = named.find((entry) => entry.getNameNode().getText() === want && !entry.isTypeOnly());
|
|
668
|
-
if (existing) {
|
|
669
|
-
const local = (existing.getAliasNode() ?? existing.getNameNode()).getText();
|
|
670
|
-
if (isShadowed(node, local)) return void 0;
|
|
671
|
-
resolved[want] = local;
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
missing.push(want);
|
|
675
|
-
}
|
|
676
|
-
if (!missing.length) return {
|
|
677
|
-
css: cssName,
|
|
678
|
-
cx: resolved.cx,
|
|
679
|
-
leaf: resolved[LEAF_HELPER]
|
|
680
|
-
};
|
|
681
|
-
if (!isGeneratedCssModule(mod)) return void 0;
|
|
682
|
-
const declared = declaredAtModuleScope(sourceFile);
|
|
683
|
-
for (const name of missing) {
|
|
684
|
-
if (isShadowed(node, name) || declared.has(name)) return void 0;
|
|
685
|
-
resolved[name] = name;
|
|
686
|
-
}
|
|
687
|
-
const last = named.at(-1);
|
|
688
|
-
if (!last) return void 0;
|
|
689
|
-
return {
|
|
690
|
-
css: cssName,
|
|
691
|
-
cx: resolved.cx,
|
|
692
|
-
leaf: resolved[LEAF_HELPER],
|
|
693
|
-
insert: {
|
|
694
|
-
pos: last.getEnd(),
|
|
695
|
-
names: missing
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
//#endregion
|
|
701
|
-
//#region src/fold-jsx.ts
|
|
702
|
-
const RANK = {
|
|
703
|
-
constant: 0,
|
|
704
|
-
reads: 1,
|
|
705
|
-
unknown: 2
|
|
706
|
-
};
|
|
707
|
-
const worst = (a, b) => RANK[a] >= RANK[b] ? a : b;
|
|
708
|
-
const purityOf = (node) => {
|
|
709
|
-
if (Node.isIdentifier(node)) return "reads";
|
|
710
|
-
if (Node.isStringLiteral(node) || Node.isNumericLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) || node.getKind() === SyntaxKind.NullKeyword) return "constant";
|
|
711
|
-
if (Node.isPrefixUnaryExpression(node) && Node.isNumericLiteral(node.getOperand())) return "constant";
|
|
712
|
-
if (Node.isObjectLiteralExpression(node)) return node.getProperties().reduce((acc, property) => {
|
|
713
|
-
if (!Node.isPropertyAssignment(property)) return "unknown";
|
|
714
|
-
if (Node.isComputedPropertyName(property.getNameNode())) return "unknown";
|
|
715
|
-
const value = property.getInitializer();
|
|
716
|
-
return worst(acc, value ? purityOf(value) : "unknown");
|
|
717
|
-
}, "constant");
|
|
718
|
-
if (Node.isArrayLiteralExpression(node)) return node.getElements().reduce((acc, element) => worst(acc, purityOf(element)), "constant");
|
|
719
|
-
return "unknown";
|
|
720
|
-
};
|
|
721
|
-
/**
|
|
722
|
-
* The same question for a whole attribute.
|
|
723
|
-
*
|
|
724
|
-
* A `JsxElement` or `JsxFragment` initializer — `title=<Tag x={f()} />` — is legal, holds
|
|
725
|
-
* arbitrary expressions, and is not a `JsxExpression`, so this asks what an initializer
|
|
726
|
-
* *is* rather than listing the kinds that carry code.
|
|
727
|
-
*/
|
|
728
|
-
const attributePurity = (attribute) => {
|
|
729
|
-
const initializer = attribute.getInitializer();
|
|
730
|
-
if (!initializer) return "constant";
|
|
731
|
-
if (Node.isStringLiteral(initializer)) return "constant";
|
|
732
|
-
if (!Node.isJsxExpression(initializer)) return "unknown";
|
|
733
|
-
const expression = initializer.getExpression();
|
|
734
|
-
return expression ? purityOf(expression) : "constant";
|
|
735
|
-
};
|
|
736
|
-
/**
|
|
737
|
-
* Props the factory gives extra *styling* meaning to, which is why they block a fold.
|
|
738
|
-
*
|
|
739
|
-
* `unstyled` skips the recipe and `css` merges at a higher precedence than the style
|
|
740
|
-
* props, so neither is expressible as a class the fold can compute alongside the rest.
|
|
741
|
-
*
|
|
742
|
-
* `ref`, `key` and `children` used to be here too, and do not belong: none of them
|
|
743
|
-
* changes what the element is styled with. The factory takes `ref` through `forwardRef`
|
|
744
|
-
* and hands it straight to `createElement`, so an intrinsic tag — or whatever `as` names
|
|
745
|
-
* — receives the identical prop. `key` never reaches the component at all; React consumes
|
|
746
|
-
* it. And `children ?? combinedProps.children` mirrors `createElement`'s own rule that
|
|
747
|
-
* the third argument beats `props.children`. Each is a passthrough, and travels as one.
|
|
748
|
-
*/
|
|
749
|
-
const RESERVED_PROPS = new Set(["unstyled", "css"]);
|
|
750
|
-
/**
|
|
751
|
-
* Props that carry no styling, and so block a fold only where the framework gives them a
|
|
752
|
-
* meaning the rewrite would change.
|
|
753
|
-
*/
|
|
754
|
-
const MECHANICAL_PROPS = new Set([
|
|
755
|
-
"ref",
|
|
756
|
-
"key",
|
|
757
|
-
"children"
|
|
758
|
-
]);
|
|
759
|
-
/**
|
|
760
|
-
* Where a mechanical prop may travel as an ordinary passthrough.
|
|
761
|
-
*
|
|
762
|
-
* React only, and measured rather than reasoned about. Its factory forwards the ref to
|
|
763
|
-
* the element it renders, so moving the ref onto that element changes nothing.
|
|
764
|
-
*
|
|
765
|
-
* Preact was in this list on the strength of `forwardRef` appearing in its factory too,
|
|
766
|
-
* and that inference was wrong: under this repo's compat setup an unfolded
|
|
767
|
-
* `<styled.div ref={r}>` binds the *component instance* and the folded `<div ref={r}>`
|
|
768
|
-
* binds the DOM node. A hand-written `forwardRef` behaves the same way, so it is Preact's
|
|
769
|
-
* own handling rather than anything the factory does. Vue diverges for the plain reason —
|
|
770
|
-
* a ref on a component is the instance, on an element it is the node.
|
|
771
|
-
*
|
|
772
|
-
* An allowlist rather than a denylist, because the failure is silent and the list of
|
|
773
|
-
* runtimes is open. Every other framework keeps the behaviour it had before.
|
|
774
|
-
*/
|
|
775
|
-
const MECHANICAL_FRAMEWORKS = new Set(["react"]);
|
|
776
|
-
/**
|
|
777
|
-
* Is this a tag JSX reads as an intrinsic element?
|
|
778
|
-
*
|
|
779
|
-
* Anchored and dot-free, because both halves matter. `Section` is a variable reference,
|
|
780
|
-
* and `foo.bar` is a member expression — a property read off something in scope — where
|
|
781
|
-
* the runtime would have created an element named literally that.
|
|
782
|
-
*/
|
|
783
|
-
const isIntrinsicTag = (tag) => /^[a-z][\w-]*$/.test(tag);
|
|
784
|
-
/**
|
|
785
|
-
* The tag an `as` prop names, when it names one statically.
|
|
786
|
-
*
|
|
787
|
-
* The factory destructures `{ as: Element = __base__ }` and hands `Element` to
|
|
788
|
-
* `createElement`, so a static `as` is simply a different tag with the same class and
|
|
789
|
-
* the same forwarded props — `splitProps` keys off the factory's own config, not off
|
|
790
|
-
* what `as` points at, so the split is unchanged.
|
|
791
|
-
*
|
|
792
|
-
* Casing is load-bearing, because JSX and `createElement` disagree about it. JSX reads a
|
|
793
|
-
* lowercase tag as an intrinsic element and a capitalised one as a variable, while
|
|
794
|
-
* `createElement` takes a string as intrinsic and anything else as a component. So the
|
|
795
|
-
* two forms only survive the rewrite when their casing already agrees:
|
|
796
|
-
*
|
|
797
|
-
* - `as="section"` -> `<section>`, intrinsic both ways.
|
|
798
|
-
* - `as={Link}` -> `<Link>`, a component reference both ways.
|
|
799
|
-
*
|
|
800
|
-
* The mismatched pair render something else entirely. `as={thing}` would fold to
|
|
801
|
-
* `<thing>`, a DOM element named `thing` rather than the component; `as="Section"` would
|
|
802
|
-
* fold to `<Section>`, a variable reference rather than the intrinsic the factory would
|
|
803
|
-
* have created. Both bail.
|
|
804
|
-
*
|
|
805
|
-
* A dot is the same hazard spelled differently, and it survives lowercasing. `<foo.bar>`
|
|
806
|
-
* is a JSX member expression — `createElement(foo.bar)`, a property read off a variable
|
|
807
|
-
* in scope — where the factory would have created an intrinsic element named literally
|
|
808
|
-
* `foo.bar`. So a dotted value bails even though its casing agrees.
|
|
809
|
-
*/
|
|
810
|
-
const asTag = (attribute) => {
|
|
811
|
-
const initializer = attribute.getInitializer();
|
|
812
|
-
if (!initializer) return void 0;
|
|
813
|
-
if (Node.isStringLiteral(initializer)) {
|
|
814
|
-
const value = initializer.getLiteralValue();
|
|
815
|
-
return /^[a-z][\w-]*$/.test(value) ? value : void 0;
|
|
816
|
-
}
|
|
817
|
-
if (!Node.isJsxExpression(initializer)) return void 0;
|
|
818
|
-
const expression = initializer.getExpression();
|
|
819
|
-
if (!expression || !Node.isIdentifier(expression)) return void 0;
|
|
820
|
-
const name = expression.getText();
|
|
821
|
-
return /^[A-Z]/.test(name) ? name : void 0;
|
|
822
|
-
};
|
|
823
|
-
/**
|
|
824
|
-
* `normalizeHTMLProps` renames these on the way to the DOM (`htmlSize` -> `size`).
|
|
825
|
-
* Reproducing the rename is easy; noticing that it exists is the hard part, so they bail.
|
|
826
|
-
*/
|
|
827
|
-
const HTML_PROPS = new Set([
|
|
828
|
-
"htmlSize",
|
|
829
|
-
"htmlTranslate",
|
|
830
|
-
"htmlWidth",
|
|
831
|
-
"htmlHeight"
|
|
832
|
-
]);
|
|
833
|
-
/**
|
|
834
|
-
* The intrinsic tag a factory expression names, if it names one statically.
|
|
835
|
-
*
|
|
836
|
-
* Only `styled.div` and friends fold. `styled(Component)` and `styled('div')` are call
|
|
837
|
-
* expressions whose result is bound elsewhere, and a capitalised tag is a component
|
|
838
|
-
* rather than an intrinsic element.
|
|
839
|
-
*/
|
|
840
|
-
const intrinsicTag = (tagName, factoryName) => {
|
|
841
|
-
const prefix = `${factoryName}.`;
|
|
842
|
-
if (!tagName.startsWith(prefix)) return void 0;
|
|
843
|
-
const tag = tagName.slice(prefix.length);
|
|
844
|
-
if (!/^[a-z][a-z0-9-]*$/.test(tag)) return void 0;
|
|
845
|
-
return tag;
|
|
846
|
-
};
|
|
847
|
-
/**
|
|
848
|
-
* Collapse a pattern element (`<Stack gap="4">`) to the tag it renders.
|
|
849
|
-
*
|
|
850
|
-
* A pattern component is a second layer on top of the factory: it splits its own props
|
|
851
|
-
* out, runs them through the pattern's transform, and hands the result to
|
|
852
|
-
* `styled.<jsxElement>`, which then does everything described above. Folding one removes
|
|
853
|
-
* both layers.
|
|
854
|
-
*
|
|
855
|
-
* The class is computed through `patterns.transform`, the same call the encoder makes
|
|
856
|
-
* when it decides what css to emit — so a folded pattern class is backed by a rule by
|
|
857
|
-
* construction, and the render-parity test is what confirms it also matches the runtime.
|
|
858
|
-
*
|
|
859
|
-
* Only the default `jsxStyleProps: 'all'` folds. Under `minimal` and `none` the pattern's
|
|
860
|
-
* styles reach the factory through the `css` prop instead of being spread, which reverses
|
|
861
|
-
* which side wins when a prop is set in both places.
|
|
862
|
-
*/
|
|
863
|
-
const planPatternFold = (item, ctx, runtimeCss) => {
|
|
864
|
-
const node = item.box?.getNode?.();
|
|
865
|
-
if (!node || !Node.isJsxOpeningElement(node) && !Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
|
|
866
|
-
if (ctx.jsx.styleProps !== "all") return { reason: "unsupported-kind" };
|
|
867
|
-
const jsxName = node.getTagNameNode().getText();
|
|
868
|
-
const detail = ctx.patterns.details.find((entry) => entry.jsxName === jsxName);
|
|
869
|
-
if (!detail) return { reason: "unsupported-kind" };
|
|
870
|
-
const styles = item.data?.[0] ?? {};
|
|
871
|
-
const passthrough = [];
|
|
872
|
-
let staticClassName = "";
|
|
873
|
-
let sawChildrenProp = false;
|
|
874
|
-
let tag = detail.config.jsxElement ?? "div";
|
|
875
|
-
let tagFromAs = false;
|
|
876
|
-
for (const attribute of node.getAttributes()) {
|
|
877
|
-
if (!Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
|
|
878
|
-
const name = attribute.getNameNode().getText();
|
|
879
|
-
if (name === "className") {
|
|
880
|
-
const initializer = attribute.getInitializer();
|
|
881
|
-
if (!initializer || !Node.isStringLiteral(initializer)) return { reason: "dynamic" };
|
|
882
|
-
staticClassName = initializer.getLiteralValue();
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
if (name === "as") {
|
|
886
|
-
const resolved = asTag(attribute);
|
|
887
|
-
if (!resolved) return { reason: "dynamic" };
|
|
888
|
-
tag = resolved;
|
|
889
|
-
tagFromAs = true;
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
|
|
893
|
-
if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
|
|
894
|
-
if (name === "children") sawChildrenProp = true;
|
|
895
|
-
if (detail.props.includes(name) || ctx.isValidProperty(name)) {
|
|
896
|
-
if (!(name in styles)) return { reason: "dynamic" };
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
passthrough.push(attribute.getText());
|
|
900
|
-
}
|
|
901
|
-
if (!tagFromAs && !isIntrinsicTag(tag)) return { reason: "unsupported-kind" };
|
|
902
|
-
if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
|
|
903
|
-
let resolved;
|
|
904
|
-
try {
|
|
905
|
-
resolved = runtimeCss(ctx.patterns.transform(detail.baseName, styles));
|
|
906
|
-
} catch {
|
|
907
|
-
return { reason: "dynamic" };
|
|
908
|
-
}
|
|
909
|
-
const className = [resolved, staticClassName].filter(Boolean).join(" ");
|
|
910
|
-
if (!className) return { reason: "dynamic" };
|
|
911
|
-
return buildEdits(node, tag, passthrough, className);
|
|
912
|
-
};
|
|
913
|
-
const planJsxFold = (item, ctx, runtimeCss, deps) => {
|
|
914
|
-
const node = item.box?.getNode?.();
|
|
915
|
-
if (!node || !Node.isJsxOpeningElement(node) && !Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
|
|
916
|
-
const baseTag = intrinsicTag(node.getTagNameNode().getText(), ctx.jsx.factoryName);
|
|
917
|
-
if (!baseTag) return { reason: "unsupported-kind" };
|
|
918
|
-
let tag = baseTag;
|
|
919
|
-
const styles = item.data?.[0] ?? {};
|
|
920
|
-
const propBoxes = box.isMap(item.box) ? item.box.value : void 0;
|
|
921
|
-
const passthrough = [];
|
|
922
|
-
const staticProps = [];
|
|
923
|
-
const dynamicProps = [];
|
|
924
|
-
let staticClassName = "";
|
|
925
|
-
let dynamicClassName = "";
|
|
926
|
-
let sawClassName = false;
|
|
927
|
-
let sawChildrenProp = false;
|
|
928
|
-
/**
|
|
929
|
-
* Where a dynamic `className` was written, and where the attributes that outlive the
|
|
930
|
-
* fold were.
|
|
931
|
-
*
|
|
932
|
-
* The factory appends `className` after the styles, so a folded one is emitted last and
|
|
933
|
-
* anything written after it runs before it instead. That covers more than the style
|
|
934
|
-
* props — a passthrough keeps its own place among the attributes — but only where the
|
|
935
|
-
* expression survives at all. A static style prop is not among them: it is *deleted*,
|
|
936
|
-
* its value having been resolved at build time. That is a larger change than reordering
|
|
937
|
-
* and a separate pre-existing gap — `<styled.div color={counted()} />` folds and never
|
|
938
|
-
* calls it, with or without this — rather than a reason the reordering is moot.
|
|
939
|
-
*/
|
|
940
|
-
let classNameIndex = -1;
|
|
941
|
-
let classNamePurity = "constant";
|
|
942
|
-
const survivors = [];
|
|
943
|
-
let index = -1;
|
|
944
|
-
for (const attribute of node.getAttributes()) {
|
|
945
|
-
if (!Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
|
|
946
|
-
index += 1;
|
|
947
|
-
const name = attribute.getNameNode().getText();
|
|
948
|
-
if (name === "className") {
|
|
949
|
-
const initializer = attribute.getInitializer();
|
|
950
|
-
if (!initializer) return { reason: "dynamic" };
|
|
951
|
-
if (sawClassName && (dynamicClassName || !Node.isStringLiteral(initializer))) return { reason: "dynamic" };
|
|
952
|
-
sawClassName = true;
|
|
953
|
-
if (Node.isStringLiteral(initializer)) {
|
|
954
|
-
staticClassName = initializer.getLiteralValue();
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
const expression = Node.isJsxExpression(initializer) ? initializer.getExpression() : void 0;
|
|
958
|
-
if (!expression) return { reason: "dynamic" };
|
|
959
|
-
dynamicClassName = expression.getText();
|
|
960
|
-
classNameIndex = index;
|
|
961
|
-
classNamePurity = purityOf(expression);
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
if (name === "as") {
|
|
965
|
-
const resolved = asTag(attribute);
|
|
966
|
-
if (!resolved) return { reason: "dynamic" };
|
|
967
|
-
survivors.push({
|
|
968
|
-
index,
|
|
969
|
-
purity: attributePurity(attribute)
|
|
970
|
-
});
|
|
971
|
-
tag = resolved;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
|
|
975
|
-
if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
|
|
976
|
-
if (name === "children") sawChildrenProp = true;
|
|
977
|
-
if (ctx.isValidProperty(name)) {
|
|
978
|
-
const attributeValue = attribute.getInitializer();
|
|
979
|
-
const valueExpression = attributeValue && Node.isJsxExpression(attributeValue) ? attributeValue.getExpression() : void 0;
|
|
980
|
-
const propBox = propBoxes?.get(name);
|
|
981
|
-
if (name in styles && isStaticBox(propBox) && accountsForSource(valueExpression, propBox)) {
|
|
982
|
-
staticProps.push(name);
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
if (!valueExpression) return { reason: "dynamic" };
|
|
986
|
-
const expression = valueExpression;
|
|
987
|
-
survivors.push({
|
|
988
|
-
index,
|
|
989
|
-
purity: attributePurity(attribute)
|
|
990
|
-
});
|
|
991
|
-
dynamicProps.push({
|
|
992
|
-
name,
|
|
993
|
-
text: expression.getText(),
|
|
994
|
-
expression
|
|
995
|
-
});
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
survivors.push({
|
|
999
|
-
index,
|
|
1000
|
-
purity: attributePurity(attribute)
|
|
1001
|
-
});
|
|
1002
|
-
passthrough.push(attribute.getText());
|
|
1003
|
-
}
|
|
1004
|
-
if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
|
|
1005
|
-
/**
|
|
1006
|
-
* Would emitting the className last move it past something that can observe it?
|
|
1007
|
-
*
|
|
1008
|
-
* A constant survivor commutes with anything. One that only reads commutes only while
|
|
1009
|
-
* the className expression cannot write — `className={cn} onClick={h}` is safe, and
|
|
1010
|
-
* `className={assigns()} bg={tone}` is not, because moving the read after the write
|
|
1011
|
-
* hands it the other value.
|
|
1012
|
-
*
|
|
1013
|
-
* This answers for the className and nothing else. `buildEdits` emits
|
|
1014
|
-
* `[...passthrough, className={cx(…)}]`, so a passthrough is also hoisted ahead of every
|
|
1015
|
-
* dynamic style prop's expression — `<styled.div bg={writes()} data-x={reads} />`
|
|
1016
|
-
* reorders those two with no className present at all. That is pre-existing and
|
|
1017
|
-
* reproduces on an unchanged tree; folding a dynamic className only makes it reachable
|
|
1018
|
-
* for more elements. Closing it means comparing every survivor against everything it
|
|
1019
|
-
* crosses rather than against one attribute, which is a different change.
|
|
1020
|
-
*/
|
|
1021
|
-
const reordered = () => classNameIndex >= 0 && survivors.some((entry) => entry.index > classNameIndex && entry.purity !== "constant" && !(entry.purity === "reads" && classNamePurity !== "unknown"));
|
|
1022
|
-
if (dynamicProps.length) {
|
|
1023
|
-
if (!deps || item.data.length !== 1) return { reason: "dynamic" };
|
|
1024
|
-
if (collides(staticProps, dynamicProps.map((prop) => prop.name), ctx)) return { reason: "dynamic" };
|
|
1025
|
-
const resolveProp = (name) => ctx.utility.hasShorthand ? ctx.utility.resolveShorthand(name) : name;
|
|
1026
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
1027
|
-
for (const prop of dynamicProps) claimed.set(resolveProp(prop.name), (claimed.get(resolveProp(prop.name)) ?? 0) + 1);
|
|
1028
|
-
const prefixes = /* @__PURE__ */ new Map();
|
|
1029
|
-
for (const prop of dynamicProps) {
|
|
1030
|
-
if (claimed.get(resolveProp(prop.name)) !== 1) continue;
|
|
1031
|
-
if (isWrittenAsCollection(prop.expression)) continue;
|
|
1032
|
-
const prefix = leafPrefix(prop.name, ctx, runtimeCss);
|
|
1033
|
-
if (prefix !== void 0) prefixes.set(prop.name, prefix);
|
|
1034
|
-
}
|
|
1035
|
-
const kinds = dynamicProps.map((prop) => prefixes.has(prop.name) ? "l" : "r").join("");
|
|
1036
|
-
if (!/^l*r*$/.test(kinds) && !/^r*l*$/.test(kinds)) prefixes.clear();
|
|
1037
|
-
if (reordered()) return { reason: "dynamic" };
|
|
1038
|
-
const helpers = resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed, prefixes.size > 0);
|
|
1039
|
-
if (!helpers) return { reason: "dynamic" };
|
|
1040
|
-
if (!helpers.leaf) prefixes.clear();
|
|
1041
|
-
const staticStyles = {};
|
|
1042
|
-
for (const name of staticProps) staticStyles[name] = styles[name];
|
|
1043
|
-
const resolved = [runtimeCss(staticStyles), staticClassName].filter(Boolean).join(" ");
|
|
1044
|
-
const lowered = dynamicProps.filter((prop) => prefixes.has(prop.name)).map((prop) => leafCall(prefixes.get(prop.name), prop.name, prop.text, helpers.leaf));
|
|
1045
|
-
const residue = dynamicProps.filter((prop) => !prefixes.has(prop.name));
|
|
1046
|
-
const runtime = residue.length ? [`${helpers.css}({ ${residue.map((prop) => `${prop.name}: ${prop.text}`).join(", ")} })`] : [];
|
|
1047
|
-
if (!resolved && !lowered.length) return { reason: "dynamic" };
|
|
1048
|
-
const ordered = kinds.startsWith("r") ? [...runtime, ...lowered] : [...lowered, ...runtime];
|
|
1049
|
-
const parts = dynamicClassName ? [...ordered, dynamicClassName] : ordered;
|
|
1050
|
-
const plan = buildEdits(node, tag, passthrough, resolved, `${helpers.cx}(${[...resolved ? [JSON.stringify(resolved)] : [], ...parts].join(", ")})`);
|
|
1051
|
-
return "reason" in plan ? plan : {
|
|
1052
|
-
...plan,
|
|
1053
|
-
insert: helpers.insert
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
if (item.data.length !== 1) return { reason: "dynamic" };
|
|
1057
|
-
const className = [runtimeCss(styles), staticClassName].filter(Boolean).join(" ");
|
|
1058
|
-
if (dynamicClassName) {
|
|
1059
|
-
if (reordered()) return { reason: "dynamic" };
|
|
1060
|
-
const helpers = deps && resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed);
|
|
1061
|
-
if (!helpers) return { reason: "dynamic" };
|
|
1062
|
-
const args = [...className ? [JSON.stringify(className)] : [], dynamicClassName];
|
|
1063
|
-
const plan = buildEdits(node, tag, passthrough, className, `${helpers.cx}(${args.join(", ")})`);
|
|
1064
|
-
return "reason" in plan ? plan : {
|
|
1065
|
-
...plan,
|
|
1066
|
-
insert: helpers.insert
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
if (!className) return { reason: "dynamic" };
|
|
1070
|
-
return buildEdits(node, tag, passthrough, className);
|
|
1071
|
-
};
|
|
1072
|
-
/** Rewrite the opening element, and the closing one when there is a pair. */
|
|
1073
|
-
const buildEdits = (node, tag, passthrough, className, classExpression) => {
|
|
1074
|
-
const attributes = [...passthrough, `className={${classExpression ?? JSON.stringify(className)}}`].join(" ");
|
|
1075
|
-
const selfClosing = Node.isJsxSelfClosingElement(node);
|
|
1076
|
-
const edits = [{
|
|
1077
|
-
start: node.getStart(),
|
|
1078
|
-
end: node.getEnd(),
|
|
1079
|
-
text: `<${tag} ${attributes}${selfClosing ? " />" : ">"}`
|
|
1080
|
-
}];
|
|
1081
|
-
let end = node.getEnd();
|
|
1082
|
-
if (!selfClosing) {
|
|
1083
|
-
const parent = node.getParent();
|
|
1084
|
-
if (!Node.isJsxElement(parent)) return { reason: "unsupported-kind" };
|
|
1085
|
-
const closing = parent.getClosingElement();
|
|
1086
|
-
edits.push({
|
|
1087
|
-
start: closing.getStart(),
|
|
1088
|
-
end: closing.getEnd(),
|
|
1089
|
-
text: `</${tag}>`
|
|
1090
|
-
});
|
|
1091
|
-
end = closing.getEnd();
|
|
1092
|
-
}
|
|
1093
|
-
return {
|
|
1094
|
-
edits,
|
|
1095
|
-
className,
|
|
1096
|
-
start: node.getStart(),
|
|
1097
|
-
end
|
|
1098
|
-
};
|
|
1099
|
-
};
|
|
1100
647
|
//#endregion
|
|
1101
648
|
//#region src/runtime-css.ts
|
|
1102
649
|
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
@@ -1122,9 +669,30 @@ const createRuntimeCss = (ctx) => {
|
|
|
1122
669
|
const { mergeCss } = createMergeCss(cssContext);
|
|
1123
670
|
return (...styles) => cssFn(mergeCss(...styles));
|
|
1124
671
|
};
|
|
1125
|
-
|
|
672
|
+
/**
|
|
673
|
+
* The map is every token in the project, so it is built once per context and shared by
|
|
674
|
+
* every module in the build — not once per `foldSource`, which would price a whole token
|
|
675
|
+
* table into each of the overwhelming majority of modules that call `token()` zero times.
|
|
676
|
+
* Keyed weakly so a context that goes out of scope takes its table with it.
|
|
677
|
+
*/
|
|
678
|
+
const tokenValues = /* @__PURE__ */ new WeakMap();
|
|
679
|
+
const tokenValuesFor = (ctx) => {
|
|
680
|
+
let values = tokenValues.get(ctx);
|
|
681
|
+
if (values) return values;
|
|
682
|
+
values = /* @__PURE__ */ new Map();
|
|
683
|
+
for (const token of ctx.tokens.allTokens) {
|
|
684
|
+
const { varRef, isVirtual, condition } = token.extensions;
|
|
685
|
+
values.set(token.name, isVirtual || condition !== "base" ? varRef : token.value);
|
|
686
|
+
}
|
|
687
|
+
tokenValues.set(ctx, values);
|
|
688
|
+
return values;
|
|
689
|
+
};
|
|
690
|
+
const createRuntimeToken = (ctx) => (path) => {
|
|
691
|
+
const value = tokenValuesFor(ctx).get(path);
|
|
692
|
+
return typeof value === "string" ? value : void 0;
|
|
693
|
+
};
|
|
694
|
+
const createRuntimeRecipe = (ctx) => {
|
|
1126
695
|
const separator = ctx.utility.separator;
|
|
1127
|
-
const { mergeCss } = createMergeCss(createCssContext(ctx));
|
|
1128
696
|
return (name, variants) => {
|
|
1129
697
|
const config = ctx.recipes.getConfig(name);
|
|
1130
698
|
const node = ctx.recipes.getRecipe(name);
|
|
@@ -1156,40 +724,28 @@ const createRuntimeRecipe = (ctx, runtimeCss) => {
|
|
|
1156
724
|
...compact(variants)
|
|
1157
725
|
};
|
|
1158
726
|
if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
|
|
1159
|
-
|
|
1160
|
-
return [recipeCss(recipeStyles), runtimeCss(compoundStyles)].filter(Boolean).join(" ");
|
|
727
|
+
return recipeCss(recipeStyles);
|
|
1161
728
|
};
|
|
1162
729
|
};
|
|
1163
|
-
/**
|
|
1164
|
-
* Mirrors the function of the same name in the generated `cva` artifact, down to the
|
|
1165
|
-
* `mergeCss` it accumulates with.
|
|
1166
|
-
*
|
|
1167
|
-
* That merge has to be the deep one. More than one compound variant can match a single
|
|
1168
|
-
* selection, and their `css` objects then combine rather than replace: `_hover` set by
|
|
1169
|
-
* one and `_hover` set by another have to end up as a single condition holding both
|
|
1170
|
-
* declarations. `Object.assign` drops everything the earlier match contributed under a
|
|
1171
|
-
* shared key, which produces a shorter class list and no error at all.
|
|
1172
|
-
*
|
|
1173
|
-
* Taking `mergeCss` as an argument rather than importing one keeps it the same instance
|
|
1174
|
-
* the rest of the fold resolves through, built from the same context.
|
|
1175
|
-
*/
|
|
1176
|
-
const getCompoundVariantCss = (compoundVariants, variantMap, mergeCss) => {
|
|
1177
|
-
let result = {};
|
|
1178
|
-
for (const compoundVariant of compoundVariants) {
|
|
1179
|
-
if (!compoundVariant) continue;
|
|
1180
|
-
if (Object.entries(compoundVariant).every(([key, value]) => {
|
|
1181
|
-
if (key === "css") return true;
|
|
1182
|
-
return (Array.isArray(value) ? value : [value]).some((entry) => variantMap[key] === entry);
|
|
1183
|
-
})) result = mergeCss(result, compoundVariant.css);
|
|
1184
|
-
}
|
|
1185
|
-
return result;
|
|
1186
|
-
};
|
|
1187
730
|
//#endregion
|
|
1188
731
|
//#region src/fold.ts
|
|
1189
732
|
/**
|
|
1190
|
-
* `cva`/`sva` return a function
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
733
|
+
* `cva`/`sva` return a function, so neither can collapse to a class string. Their
|
|
734
|
+
* *invocations* could, but those are separate call sites the parser does not record as
|
|
735
|
+
* such. `token` also resolves to no class, but it does resolve to a literal, so it folds
|
|
736
|
+
* through its own path rather than being declined outright.
|
|
737
|
+
*
|
|
738
|
+
* Folding an invocation is now *possible* in a way it was not: a recipe's classes are named
|
|
739
|
+
* semantically, so the build knows every class a call can produce from the config alone.
|
|
740
|
+
* What is missing is upstream — the parser matches calls by imported name, so a local
|
|
741
|
+
* `button()` from `const button = cva(...)` is never recorded, and tracking those bindings
|
|
742
|
+
* is a change to the extractor rather than to this set.
|
|
743
|
+
*
|
|
744
|
+
* Worth knowing before taking that on: semantic naming already took most of the prize.
|
|
745
|
+
* `cvaFn` used to run `mergeCss` and name a class per property on every call; it is now a
|
|
746
|
+
* memoized loop over `variantKeys` doing string concatenation. That is an inspection of the
|
|
747
|
+
* two implementations, not a measurement — benchmark it before deciding it is worth the
|
|
748
|
+
* extractor work.
|
|
1193
749
|
*/
|
|
1194
750
|
const FOLDABLE_TYPES = new Set([
|
|
1195
751
|
"css",
|
|
@@ -1209,13 +765,16 @@ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/
|
|
|
1209
765
|
* phase — hence separate from `unsupported-kind`, where a slot recipe lands because it
|
|
1210
766
|
* resolves to one class per slot rather than to a single string.
|
|
1211
767
|
*/
|
|
1212
|
-
const UNFOLDABLE_TYPES = new Set([
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
768
|
+
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
769
|
+
/**
|
|
770
|
+
* An argument that cannot run anything when it is evaluated.
|
|
771
|
+
*
|
|
772
|
+
* `token(path, fallback)` evaluates both arguments before the call, so a fold that drops
|
|
773
|
+
* the fallback also drops whatever evaluating it would have done. `token('x', compute())`
|
|
774
|
+
* is pathological, but the fold's contract is behaviour preservation and a literal is the
|
|
775
|
+
* cheap way to prove it: no call, no property read, no getter.
|
|
776
|
+
*/
|
|
777
|
+
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";
|
|
1219
778
|
/**
|
|
1220
779
|
* Source files a box tree reaches, other than the one being folded.
|
|
1221
780
|
*
|
|
@@ -1289,17 +848,6 @@ const calleeRootName = (call) => {
|
|
|
1289
848
|
return Node.isIdentifier(current) ? current.getText() : void 0;
|
|
1290
849
|
};
|
|
1291
850
|
/**
|
|
1292
|
-
* The identifier a JSX tag is rooted at: `styled` for `<styled.div>`, `bamboo` for
|
|
1293
|
-
* `<bamboo.styled.div>`. The same question `calleeRootName` answers for a call, and it
|
|
1294
|
-
* feeds the same import and shadowing checks.
|
|
1295
|
-
*/
|
|
1296
|
-
const tagRootName = (element) => {
|
|
1297
|
-
if (!Node.isJsxOpeningElement(element) && !Node.isJsxSelfClosingElement(element)) return void 0;
|
|
1298
|
-
let current = element.getTagNameNode();
|
|
1299
|
-
while (Node.isPropertyAccessExpression(current)) current = current.getExpression();
|
|
1300
|
-
return Node.isIdentifier(current) ? current.getText() : void 0;
|
|
1301
|
-
};
|
|
1302
|
-
/**
|
|
1303
851
|
* Local names a module binds to an import of bamboo's own generated system.
|
|
1304
852
|
*
|
|
1305
853
|
* The parser matches by name and asks neither question this does — deliberately, since
|
|
@@ -1401,7 +949,7 @@ const argumentsAccountedFor = (call, boxNode) => {
|
|
|
1401
949
|
return accountsForSource(args[0], boxNode);
|
|
1402
950
|
};
|
|
1403
951
|
const foldSource = (options) => {
|
|
1404
|
-
const { ctx, code, parserResult,
|
|
952
|
+
const { ctx, code, parserResult, partial: partial_ = true, runtimeCss = createRuntimeCss(ctx) } = options;
|
|
1405
953
|
/**
|
|
1406
954
|
* Recover the static half of a call the whole-call path gave up on. Only a
|
|
1407
955
|
* single-argument `css()` qualifies: a pattern or recipe call takes props rather than a
|
|
@@ -1456,7 +1004,8 @@ const foldSource = (options) => {
|
|
|
1456
1004
|
insert: cx.insert
|
|
1457
1005
|
};
|
|
1458
1006
|
};
|
|
1459
|
-
const runtimeRecipe = createRuntimeRecipe(ctx
|
|
1007
|
+
const runtimeRecipe = createRuntimeRecipe(ctx);
|
|
1008
|
+
const runtimeToken = createRuntimeToken(ctx);
|
|
1460
1009
|
/**
|
|
1461
1010
|
* Does this specifier name a module that exports the css API, exactly?
|
|
1462
1011
|
*
|
|
@@ -1498,11 +1047,6 @@ const foldSource = (options) => {
|
|
|
1498
1047
|
};
|
|
1499
1048
|
const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
|
|
1500
1049
|
const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
|
|
1501
|
-
const jsxDeps = {
|
|
1502
|
-
isBambooCssModule,
|
|
1503
|
-
isGeneratedCssModule,
|
|
1504
|
-
isShadowed
|
|
1505
|
-
};
|
|
1506
1050
|
const folded = [];
|
|
1507
1051
|
const skipped = [];
|
|
1508
1052
|
const candidates = [];
|
|
@@ -1521,12 +1065,8 @@ const foldSource = (options) => {
|
|
|
1521
1065
|
const name = item.name ?? type;
|
|
1522
1066
|
if (!item.box) continue;
|
|
1523
1067
|
const call = findCallExpression(item.box);
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
1526
|
-
if (!element) continue;
|
|
1527
|
-
const elementStart = element.getStart();
|
|
1528
|
-
const elementEnd = element.getEnd();
|
|
1529
|
-
if (code.slice(elementStart, elementEnd) !== element.getText()) {
|
|
1068
|
+
if (type === "token") {
|
|
1069
|
+
if (!call) {
|
|
1530
1070
|
skipped.push({
|
|
1531
1071
|
name,
|
|
1532
1072
|
reason: "no-call-expression",
|
|
@@ -1535,34 +1075,85 @@ const foldSource = (options) => {
|
|
|
1535
1075
|
});
|
|
1536
1076
|
continue;
|
|
1537
1077
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1078
|
+
const start = call.getStart();
|
|
1079
|
+
const end = call.getEnd();
|
|
1080
|
+
if (code.slice(start, end) !== call.getText()) {
|
|
1081
|
+
skipped.push({
|
|
1082
|
+
name,
|
|
1083
|
+
reason: "no-call-expression",
|
|
1084
|
+
start: 0,
|
|
1085
|
+
end: 0
|
|
1086
|
+
});
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const rangeKey = `${start}:${end}`;
|
|
1090
|
+
if (seenRanges.has(rangeKey)) continue;
|
|
1091
|
+
seenRanges.add(rangeKey);
|
|
1092
|
+
const rootName = calleeRootName(call);
|
|
1093
|
+
if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
|
|
1540
1094
|
skipped.push({
|
|
1541
1095
|
name,
|
|
1542
1096
|
reason: "not-imported",
|
|
1543
|
-
start
|
|
1544
|
-
end
|
|
1097
|
+
start,
|
|
1098
|
+
end
|
|
1099
|
+
});
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const callee = Node.isCallExpression(call) ? call.getExpression() : void 0;
|
|
1103
|
+
if (Node.isPropertyAccessExpression(callee) && !ctx.imports.matchers.tokens.match(callee.getNameNode().getText())) {
|
|
1104
|
+
skipped.push({
|
|
1105
|
+
name,
|
|
1106
|
+
reason: "unsupported-kind",
|
|
1107
|
+
start,
|
|
1108
|
+
end
|
|
1545
1109
|
});
|
|
1546
1110
|
continue;
|
|
1547
1111
|
}
|
|
1548
|
-
|
|
1549
|
-
if ("reason" in plan) {
|
|
1112
|
+
if (!isStaticBox(item.box) || item.data.length !== 1) {
|
|
1550
1113
|
skipped.push({
|
|
1551
1114
|
name,
|
|
1552
|
-
reason:
|
|
1553
|
-
start
|
|
1554
|
-
end
|
|
1115
|
+
reason: "dynamic",
|
|
1116
|
+
start,
|
|
1117
|
+
end
|
|
1118
|
+
});
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const path = item.data[0];
|
|
1122
|
+
if (typeof path !== "string") {
|
|
1123
|
+
skipped.push({
|
|
1124
|
+
name,
|
|
1125
|
+
reason: "dynamic",
|
|
1126
|
+
start,
|
|
1127
|
+
end
|
|
1128
|
+
});
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (!(Node.isCallExpression(call) ? call.getArguments().slice(1) : []).every(isInertArgument)) {
|
|
1132
|
+
skipped.push({
|
|
1133
|
+
name,
|
|
1134
|
+
reason: "dynamic",
|
|
1135
|
+
start,
|
|
1136
|
+
end
|
|
1137
|
+
});
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const value = runtimeToken(path);
|
|
1141
|
+
if (!value) {
|
|
1142
|
+
skipped.push({
|
|
1143
|
+
name,
|
|
1144
|
+
reason: "unresolved-token",
|
|
1145
|
+
start,
|
|
1146
|
+
end
|
|
1555
1147
|
});
|
|
1556
1148
|
continue;
|
|
1557
1149
|
}
|
|
1558
1150
|
candidates.push({
|
|
1559
1151
|
item,
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
end: plan.end
|
|
1152
|
+
call,
|
|
1153
|
+
node: call,
|
|
1154
|
+
start,
|
|
1155
|
+
end,
|
|
1156
|
+
value
|
|
1566
1157
|
});
|
|
1567
1158
|
continue;
|
|
1568
1159
|
}
|
|
@@ -1669,7 +1260,7 @@ const foldSource = (options) => {
|
|
|
1669
1260
|
for (const candidate of candidates) {
|
|
1670
1261
|
const { item, start, end } = candidate;
|
|
1671
1262
|
const name = item.name ?? item.type ?? "";
|
|
1672
|
-
const ranges =
|
|
1263
|
+
const ranges = [[start, end]];
|
|
1673
1264
|
if (collides(ranges)) {
|
|
1674
1265
|
skipped.push({
|
|
1675
1266
|
name,
|
|
@@ -1679,26 +1270,28 @@ const foldSource = (options) => {
|
|
|
1679
1270
|
});
|
|
1680
1271
|
continue;
|
|
1681
1272
|
}
|
|
1682
|
-
if (candidate.
|
|
1683
|
-
magic.overwrite(start, end, candidate.
|
|
1684
|
-
applyInsert(candidate.insert);
|
|
1273
|
+
if (candidate.value !== void 0) {
|
|
1274
|
+
magic.overwrite(start, end, JSON.stringify(candidate.value));
|
|
1685
1275
|
applied.push(...ranges);
|
|
1686
1276
|
folded.push({
|
|
1687
1277
|
name,
|
|
1688
|
-
|
|
1689
|
-
|
|
1278
|
+
kind: "value",
|
|
1279
|
+
className: "",
|
|
1280
|
+
classNames: [],
|
|
1281
|
+
value: candidate.value,
|
|
1690
1282
|
start,
|
|
1691
1283
|
end
|
|
1692
1284
|
});
|
|
1693
1285
|
collectSourceFiles(item.box, dependencyScan);
|
|
1694
1286
|
continue;
|
|
1695
1287
|
}
|
|
1696
|
-
if (candidate.
|
|
1697
|
-
|
|
1288
|
+
if (candidate.replacement) {
|
|
1289
|
+
magic.overwrite(start, end, candidate.replacement);
|
|
1698
1290
|
applyInsert(candidate.insert);
|
|
1699
1291
|
applied.push(...ranges);
|
|
1700
1292
|
folded.push({
|
|
1701
1293
|
name,
|
|
1294
|
+
kind: "class",
|
|
1702
1295
|
className: candidate.className,
|
|
1703
1296
|
classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
|
|
1704
1297
|
start,
|
|
@@ -1745,6 +1338,7 @@ const foldSource = (options) => {
|
|
|
1745
1338
|
applied.push(...ranges);
|
|
1746
1339
|
folded.push({
|
|
1747
1340
|
name,
|
|
1341
|
+
kind: "class",
|
|
1748
1342
|
className,
|
|
1749
1343
|
classNames: [className],
|
|
1750
1344
|
start,
|
|
@@ -1819,7 +1413,7 @@ const formatSkipped = (id, skipped) => {
|
|
|
1819
1413
|
* with no matching rule.
|
|
1820
1414
|
*/
|
|
1821
1415
|
const bamboocss = (options = {}) => {
|
|
1822
|
-
const { transform = false,
|
|
1416
|
+
const { transform = false, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
|
|
1823
1417
|
/** Totals across the build, for the summary. */
|
|
1824
1418
|
const totals = {
|
|
1825
1419
|
folded: 0,
|
|
@@ -1852,6 +1446,37 @@ const bamboocss = (options = {}) => {
|
|
|
1852
1446
|
totals.skipped.clear();
|
|
1853
1447
|
await ensureContext();
|
|
1854
1448
|
},
|
|
1449
|
+
/**
|
|
1450
|
+
* Take a changed module out of the parser's hands before the rebuild reads it.
|
|
1451
|
+
*
|
|
1452
|
+
* `addWatchFile` below registers the modules a fold read, so editing one re-transforms
|
|
1453
|
+
* its consumers. That is only half of it. The consumer is transformed *before* the
|
|
1454
|
+
* module it imports — that is how a bundler discovers imports at all — so by the time
|
|
1455
|
+
* the changed module's own `transform` refreshes it in the ts-morph project, the fold
|
|
1456
|
+
* that reads it has already run against the previous contents and baked a stale class
|
|
1457
|
+
* into the bundle. Rollup calls this hook before any of that, which is the only point
|
|
1458
|
+
* where refreshing is early enough.
|
|
1459
|
+
*
|
|
1460
|
+
* Both entry points clear the box-node cache, which is the part that matters: a
|
|
1461
|
+
* resolution memoized against the old contents outlives the file itself.
|
|
1462
|
+
*
|
|
1463
|
+
* A created file is handled as an edit. `reloadSourceFile` cannot re-read one the
|
|
1464
|
+
* parser has never held, and does not need to — it clears the cache, and the extractor
|
|
1465
|
+
* adds a newly-reachable module from disk on next use. What the shared path *is* needed
|
|
1466
|
+
* for is an editor's atomic save, which arrives as a delete followed by a create while
|
|
1467
|
+
* the parser still holds the file.
|
|
1468
|
+
*/
|
|
1469
|
+
watchChange(id, change) {
|
|
1470
|
+
if (!transform || !ctx) return;
|
|
1471
|
+
if (!shouldTransform(id)) return;
|
|
1472
|
+
const [filePath] = id.split("?");
|
|
1473
|
+
if (!filePath) return;
|
|
1474
|
+
if (change.event === "delete") {
|
|
1475
|
+
ctx.project.removeSourceFile(filePath);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
ctx.project.reloadSourceFile(filePath);
|
|
1479
|
+
},
|
|
1855
1480
|
async transform(code, id) {
|
|
1856
1481
|
if (!transform) return null;
|
|
1857
1482
|
if (!shouldTransform(id)) return null;
|
|
@@ -1870,7 +1495,6 @@ const bamboocss = (options = {}) => {
|
|
|
1870
1495
|
parserResult,
|
|
1871
1496
|
filePath,
|
|
1872
1497
|
runtimeCss,
|
|
1873
|
-
jsx,
|
|
1874
1498
|
partial
|
|
1875
1499
|
});
|
|
1876
1500
|
} catch (error) {
|