@bamboocss/parser 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -44,7 +44,7 @@ function classifyProject(ctx, resultMap) {
44
44
  const processPattern = (opts) => {
45
45
  const { boxNode, data, item, filepath, localMaps } = opts;
46
46
  const name = item.componentName;
47
- const pattern = ctx.patterns.details.find((p) => p.match.test(name) || p.baseName === name);
47
+ const pattern = ctx.patterns.details.find((p) => p.baseName === name);
48
48
  if (!pattern) return;
49
49
  const cssObj = pattern.config.transform?.(data || {}, _bamboocss_shared.patternFns) ?? {};
50
50
  const newItem = {
@@ -76,7 +76,7 @@ function classifyProject(ctx, resultMap) {
76
76
  contains: [],
77
77
  debug: Reflect.has(item, "debug")
78
78
  };
79
- if (item.type === "pattern" || item.type === "jsx-pattern") return processPattern({
79
+ if (item.type === "pattern") return processPattern({
80
80
  boxNode: item.box,
81
81
  data: item.data[0],
82
82
  item: componentReportItem,
@@ -201,14 +201,6 @@ function classifyProject(ctx, resultMap) {
201
201
  resultMap.forEach((parserResult, filepath) => {
202
202
  if (parserResult.isEmpty()) return;
203
203
  const localMaps = createReportMaps();
204
- const componentFn = (item) => {
205
- processResultItemFn({
206
- item,
207
- filepath,
208
- localMaps,
209
- type: "component"
210
- });
211
- };
212
204
  const functionFn = (item) => {
213
205
  processResultItemFn({
214
206
  item,
@@ -217,7 +209,6 @@ function classifyProject(ctx, resultMap) {
217
209
  type: "function"
218
210
  });
219
211
  };
220
- parserResult.jsx.forEach(componentFn);
221
212
  parserResult.css.forEach(functionFn);
222
213
  parserResult.cva.forEach(functionFn);
223
214
  parserResult.pattern.forEach((itemList) => {
@@ -466,6 +457,120 @@ function getImportDeclarations(context, sourceFile) {
466
457
  return importDeclarations;
467
458
  }
468
459
  //#endregion
460
+ //#region src/unresolved-styles.ts
461
+ /**
462
+ * Whether every box under this one carries a value the build can actually see.
463
+ *
464
+ * Mirrors `isStaticBox` in `@bamboocss/vite`, which asks the same question to decide
465
+ * whether a call is safe to fold. Duplicated rather than shared for now because the fold's
466
+ * copy also answers questions about ternaries that a diagnostic does not care about;
467
+ * unifying them is worth doing when the fold's detection moves out of the vite package.
468
+ */
469
+ const findUnresolvable = (node, path, out, seen = /* @__PURE__ */ new Set()) => {
470
+ if (!node || seen.has(node)) return;
471
+ seen.add(node);
472
+ if (_bamboocss_extractor.box.isUnresolvable(node)) {
473
+ out.push(path.join("."));
474
+ return;
475
+ }
476
+ if (_bamboocss_extractor.box.isConditional(node)) return;
477
+ if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
478
+ if (ts_morph.Node.isTemplateExpression(node.getNode())) out.push(path.join("."));
479
+ return;
480
+ }
481
+ if (_bamboocss_extractor.box.isMap(node)) {
482
+ for (const [key, child] of node.value) findUnresolvable(child, [...path, key], out, seen);
483
+ return;
484
+ }
485
+ if (_bamboocss_extractor.box.isArray(node)) node.value.forEach((child, index) => findUnresolvable(child, [...path, String(index)], out, seen));
486
+ };
487
+ /**
488
+ * Property names written at the top level of the call's own object literal.
489
+ *
490
+ * A key whose value the extractor could not evaluate at all is not boxed as
491
+ * `unresolvable` — `maybeBoxNode` returns nothing and the pair is never recorded
492
+ * (`get-object-literal-expression-prop-pairs.ts` has no fallback), so the property
493
+ * disappears with no trace in the box tree. Reading the source back is the only way to
494
+ * notice, and it is why `css({ color: getColor() })` needs this and not just the walk above.
495
+ *
496
+ * Returns `undefined` only when the argument is not a single object literal at all, so the
497
+ * caller reports nothing rather than something wrong.
498
+ */
499
+ const writtenProps = (node) => {
500
+ let literal = node;
501
+ if (literal && ts_morph.Node.isCallExpression(literal)) {
502
+ const args = literal.getArguments();
503
+ if (args.length !== 1) return void 0;
504
+ literal = args[0];
505
+ }
506
+ if (!literal || !ts_morph.Node.isObjectLiteralExpression(literal)) return void 0;
507
+ const names = [];
508
+ let uncertain = false;
509
+ for (const property of literal.getProperties()) {
510
+ if (ts_morph.Node.isPropertyAssignment(property) || ts_morph.Node.isShorthandPropertyAssignment(property)) {
511
+ const name = property.getName();
512
+ if (name.startsWith("[")) {
513
+ uncertain = true;
514
+ continue;
515
+ }
516
+ names.push(name.replace(/^['"]|['"]$/g, ""));
517
+ continue;
518
+ }
519
+ uncertain = true;
520
+ }
521
+ return {
522
+ names,
523
+ uncertain
524
+ };
525
+ };
526
+ /**
527
+ * Every property of a `css()` call that will not reach the stylesheet.
528
+ *
529
+ * Only meaningful under `cssMode: 'grouped'`, where one class names the whole call: a
530
+ * property the build cannot resolve does not merely go missing, it changes the class, and
531
+ * the element renders with no styles at all. Under `atomic` the same call loses one
532
+ * declaration and keeps the rest, which is why this is not reported there.
533
+ */
534
+ const findUnresolvedStyles = (item) => {
535
+ const boxNode = item.box;
536
+ if (!boxNode) return [];
537
+ const node = boxNode.getNode();
538
+ const sourceFile = node?.getSourceFile();
539
+ if (!node || !sourceFile) return [];
540
+ const found = [];
541
+ findUnresolvable(boxNode, [], found);
542
+ const losses = found.map((prop) => ({
543
+ prop: prop || void 0,
544
+ reason: "unresolvable-value"
545
+ }));
546
+ const written = writtenProps(node);
547
+ if (written) {
548
+ const resolved = /* @__PURE__ */ new Set();
549
+ for (const entry of item.data) if (entry && typeof entry === "object") for (const key of Object.keys(entry)) resolved.add(key);
550
+ if (_bamboocss_extractor.box.isMap(boxNode)) for (const key of boxNode.value.keys()) resolved.add(key);
551
+ for (const prop of written.names) if (!resolved.has(prop)) losses.push({
552
+ prop,
553
+ reason: "missing-property"
554
+ });
555
+ if (written.uncertain && !hasKeyOutside(resolved, written.names)) losses.push({ reason: "unenumerable-keys" });
556
+ }
557
+ if (!losses.length) return [];
558
+ const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
559
+ const at = {
560
+ filePath: sourceFile.getFilePath(),
561
+ line,
562
+ column
563
+ };
564
+ return losses.map((loss) => ({
565
+ ...at,
566
+ ...loss
567
+ }));
568
+ };
569
+ const hasKeyOutside = (resolved, names) => {
570
+ for (const key of resolved) if (!names.includes(key)) return true;
571
+ return false;
572
+ };
573
+ //#endregion
469
574
  //#region src/parser-result.ts
470
575
  function cartesian(arrays) {
471
576
  if (arrays.length === 0) return [[]];
@@ -477,7 +582,6 @@ var ParserResult = class {
477
582
  context;
478
583
  /** Ordered list of all ResultItem */
479
584
  all = [];
480
- jsx = /* @__PURE__ */ new Set();
481
585
  css = /* @__PURE__ */ new Set();
482
586
  cva = /* @__PURE__ */ new Set();
483
587
  sva = /* @__PURE__ */ new Set();
@@ -487,10 +591,37 @@ var ParserResult = class {
487
591
  pattern = /* @__PURE__ */ new Map();
488
592
  filePath;
489
593
  encoder;
594
+ /**
595
+ * `css()` calls whose styles the build could not fully see.
596
+ *
597
+ * Only collected under `cssMode: 'grouped'`, where one class names the whole call, so a
598
+ * property the build cannot resolve changes the class rather than dropping a declaration
599
+ * from it — and the element renders with no styles at all. Under `atomic` the same call
600
+ * keeps everything the build did resolve, which is not worth interrupting a build over.
601
+ */
602
+ unresolved = [];
490
603
  constructor(context, encoder) {
491
604
  this.context = context;
492
605
  this.encoder = encoder ?? context.encoder;
493
606
  }
607
+ /**
608
+ * Record a call whose styles the build could not fully see, at the call's own position.
609
+ *
610
+ * Used for losses the box tree cannot show — a ternary past the combination cap emits
611
+ * fragments rather than whole objects, and every individual box in it resolved fine.
612
+ */
613
+ reportUnresolved(result, reason) {
614
+ const node = result.box?.getNode();
615
+ const sourceFile = node?.getSourceFile();
616
+ if (!node || !sourceFile) return;
617
+ const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
618
+ this.unresolved.push({
619
+ filePath: sourceFile.getFilePath(),
620
+ line,
621
+ column,
622
+ reason
623
+ });
624
+ }
494
625
  append(result) {
495
626
  this.all.push(result);
496
627
  return result;
@@ -516,24 +647,36 @@ var ParserResult = class {
516
647
  this.css.add(this.append(Object.assign({ type: "css" }, result)));
517
648
  const encoder = this.encoder;
518
649
  const grouped = this.context.config.cssMode === "grouped";
519
- if (!grouped || result.data.length <= 1) {
520
- result.data.forEach((obj) => grouped ? encoder.processGrouped(obj) : encoder.processAtomic(obj));
650
+ const data = result.data.some(Array.isArray) ? result.data.flatMap((obj) => Array.isArray(obj) ? obj : [obj]) : result.data;
651
+ if (grouped) {
652
+ const unresolved = findUnresolvedStyles(result);
653
+ if (unresolved.length) {
654
+ this.unresolved.push(...unresolved);
655
+ data.forEach((obj) => encoder.processAtomic(obj));
656
+ }
657
+ }
658
+ if (!grouped || data.length <= 1) {
659
+ data.forEach((obj) => grouped ? encoder.processGrouped(obj) : encoder.processAtomic(obj));
521
660
  return;
522
661
  }
523
662
  const keyCounts = /* @__PURE__ */ new Map();
524
- for (const obj of result.data) for (const key of Object.keys(obj)) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
663
+ for (const obj of data) for (const key of Object.keys(obj)) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
525
664
  if (!Array.from(keyCounts.values()).some((c) => c > 1)) {
526
- encoder.processGrouped(Object.assign({}, ...result.data));
665
+ encoder.processGroupedMerge(data);
527
666
  return;
528
667
  }
668
+ if (this.callArgumentCount(result) > 1) {
669
+ this.reportUnresolved(result, "ambiguous-merge");
670
+ data.forEach((obj) => encoder.processAtomic(obj));
671
+ }
529
672
  const overlappingKeys = /* @__PURE__ */ new Set();
530
673
  keyCounts.forEach((count, key) => {
531
674
  if (count > 1) overlappingKeys.add(key);
532
675
  });
533
- const base = {};
676
+ const baseEntries = [];
534
677
  const branchEntries = [];
535
- for (const obj of result.data) if (Object.keys(obj).some((k) => overlappingKeys.has(k))) branchEntries.push(obj);
536
- else Object.assign(base, obj);
678
+ for (const obj of data) if (Object.keys(obj).some((k) => overlappingKeys.has(k))) branchEntries.push(obj);
679
+ else baseEntries.push(obj);
537
680
  const branchGroups = /* @__PURE__ */ new Map();
538
681
  for (const entry of branchEntries) {
539
682
  const keySet = Object.keys(entry).sort().join("\0");
@@ -543,10 +686,24 @@ var ParserResult = class {
543
686
  }
544
687
  const groupArrays = Array.from(branchGroups.values());
545
688
  if (groupArrays.reduce((acc, g) => acc * g.length, 1) > 32) {
546
- result.data.forEach((obj) => encoder.processGrouped(obj));
689
+ this.reportUnresolved(result, "too-many-combinations");
690
+ data.forEach((obj) => {
691
+ encoder.processGrouped(obj);
692
+ encoder.processAtomic(obj);
693
+ });
547
694
  return;
548
695
  }
549
- for (const combo of cartesian(groupArrays)) encoder.processGrouped(Object.assign({}, base, ...combo));
696
+ for (const combo of cartesian(groupArrays)) encoder.processGroupedMerge([...baseEntries, ...combo]);
697
+ }
698
+ /**
699
+ * How many arguments the call this result came from was written with.
700
+ *
701
+ * Returns 1 for anything that is not a call — a JSX element, or a box that lost its node —
702
+ * since the question only separates operands from branches and neither has operands.
703
+ */
704
+ callArgumentCount(result) {
705
+ const node = result.box?.getNode();
706
+ return node && ts_morph.Node.isCallExpression(node) ? node.getArguments().length : 1;
550
707
  }
551
708
  setCva(result) {
552
709
  this.cva.add(this.append(Object.assign({ type: "cva" }, result)));
@@ -566,19 +723,31 @@ var ParserResult = class {
566
723
  const encoder = this.encoder;
567
724
  result.data.forEach((obj) => encoder.processViewTransition(obj));
568
725
  }
569
- setJsx(result) {
570
- this.jsx.add(this.append(Object.assign({ type: "jsx" }, result)));
571
- const encoder = this.encoder;
572
- const grouped = this.context.config.cssMode === "grouped";
573
- result.data.forEach((obj) => encoder.processStyleProps(obj, grouped));
574
- }
575
726
  setPattern(name, result) {
576
727
  (0, _bamboocss_shared.getOrCreateSet)(this.pattern, name).add(this.append(Object.assign({
577
728
  type: "pattern",
578
729
  name
579
730
  }, result)));
580
731
  const encoder = this.encoder;
581
- result.data.forEach((obj) => encoder.processPattern(name, obj, result.type ?? "pattern", result.name));
732
+ const grouped = this.context.config.cssMode === "grouped";
733
+ result.data.forEach((obj) => encoder.processPattern(name, obj, grouped));
734
+ if (grouped && !this.groupIsExact(result)) result.data.forEach((obj) => encoder.processPattern(name, obj, false));
735
+ }
736
+ /**
737
+ * Whether the group encoded for this result is the one the runtime will ask for.
738
+ *
739
+ * True only when the build saw the whole thing at once: one style object, with every
740
+ * value in it resolved. Several objects means the runtime merges them into a call this
741
+ * never encoded — `setCss` reconstructs those combinations, and nothing else does — and
742
+ * an unresolved value means the merge would not have matched anyway.
743
+ *
744
+ * Answering "no" costs a call site its atomic rules, which is CSS that duplicates the
745
+ * group. Answering a wrong "yes" costs the element every style it has, so this is
746
+ * deliberately conservative.
747
+ */
748
+ groupIsExact(result) {
749
+ if (result.data.length !== 1) return false;
750
+ return findUnresolvedStyles(result).length === 0;
582
751
  }
583
752
  setRecipe(recipeName, result) {
584
753
  (0, _bamboocss_shared.getOrCreateSet)(this.recipe, recipeName).add(this.append(Object.assign({ type: "recipe" }, result)));
@@ -608,7 +777,6 @@ var ParserResult = class {
608
777
  result.sva.forEach((item) => this.sva.add(this.append(item)));
609
778
  result.token.forEach((item) => this.token.add(this.append(item)));
610
779
  result.viewTransition.forEach((item) => this.viewTransition.add(this.append(item)));
611
- result.jsx.forEach((item) => this.jsx.add(this.append(item)));
612
780
  result.recipe.forEach((items, name) => {
613
781
  const set = (0, _bamboocss_shared.getOrCreateSet)(this.recipe, name);
614
782
  items.forEach((item) => set.add(this.append(item)));
@@ -617,6 +785,7 @@ var ParserResult = class {
617
785
  const set = (0, _bamboocss_shared.getOrCreateSet)(this.pattern, name);
618
786
  items.forEach((item) => set.add(this.append(item)));
619
787
  });
788
+ if (result.unresolved.length) this.unresolved.push(...result.unresolved);
620
789
  return this;
621
790
  }
622
791
  toArray() {
@@ -629,7 +798,6 @@ var ParserResult = class {
629
798
  sva: Array.from(this.sva),
630
799
  token: Array.from(this.token),
631
800
  viewTransition: Array.from(this.viewTransition),
632
- jsx: Array.from(this.jsx),
633
801
  recipe: Object.fromEntries(Array.from(this.recipe.entries()).map(([key, value]) => [key, Array.from(value)])),
634
802
  pattern: Object.fromEntries(Array.from(this.pattern.entries()).map(([key, value]) => [key, Array.from(value)]))
635
803
  };
@@ -654,8 +822,7 @@ const defaultEnv = { preset: "ECMA" };
654
822
  const fallbackImpl = (...values) => values.some((value) => value === void 0) ? void 0 : `fallback(${values.join(", ")})`;
655
823
  const evaluateOptions = { environment: defaultEnv };
656
824
  function createParser(context) {
657
- const { jsx, imports, recipes, config } = context;
658
- const syntax = config.syntax;
825
+ const { jsx, imports, recipes } = context;
659
826
  return function parse(sourceFile, encoder, options) {
660
827
  if (!sourceFile) return;
661
828
  const importDeclarations = getImportDeclarations(context, sourceFile);
@@ -690,12 +857,8 @@ function createParser(context) {
690
857
  functions: {
691
858
  matchFn: (prop) => file.matchFn(prop.fnName),
692
859
  matchProp: () => true,
693
- matchArg: (prop) => {
694
- if (file.isJsxFactory(prop.fnName) && prop.index === 1 && ts_morph.Node.isIdentifier(prop.argNode)) return false;
695
- return true;
696
- }
860
+ matchArg: () => true
697
861
  },
698
- taggedTemplates: syntax === "template-literal" ? { matchTaggedTemplate: (tag) => file.matchFn(tag.fnName) } : void 0,
699
862
  getEvaluateOptions: (node) => {
700
863
  if (!ts_morph.Node.isCallExpression(node)) return evaluateOptions;
701
864
  const propAccessExpr = node.getExpression();
@@ -729,14 +892,6 @@ function createParser(context) {
729
892
  box: query.box.value[0] ?? _bamboocss_extractor.box.fallback(query.box),
730
893
  data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
731
894
  });
732
- else if (query.kind === "tagged-template") {
733
- const obj = (0, _bamboocss_shared.astish)(query.box.value);
734
- parserResult.set(name, {
735
- name,
736
- box: query.box ?? _bamboocss_extractor.box.fallback(query.box),
737
- data: [obj]
738
- });
739
- }
740
895
  });
741
896
  }).when(imports.matchers.tokens.match, (name) => {
742
897
  result.queryList.forEach((query) => {
@@ -762,7 +917,7 @@ function createParser(context) {
762
917
  data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
763
918
  });
764
919
  });
765
- }).when((name) => syntax !== "template-literal" && file.isViewTransitionFn(name), (name) => {
920
+ }).when(file.isViewTransitionFn, (name) => {
766
921
  result.queryList.forEach((query) => {
767
922
  if (query.kind === "call-expression") parserResult.setViewTransition({
768
923
  name,
@@ -770,116 +925,20 @@ function createParser(context) {
770
925
  data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
771
926
  });
772
927
  });
773
- }).when(jsx.isJsxFactory, () => {
774
- result.queryList.forEach((query) => {
775
- if (query.kind === "call-expression" && query.box.value[1]) {
776
- const map = query.box.value[1];
777
- const boxNode = _bamboocss_extractor.box.isMap(map) ? map : _bamboocss_extractor.box.fallback(query.box);
778
- const combined = combineResult((0, _bamboocss_extractor.unbox)(boxNode));
779
- const result = {
780
- name,
781
- box: boxNode,
782
- data: options?.transform?.({
783
- type: "jsx-factory",
784
- data: combined
785
- }) ?? combined
786
- };
787
- if (_bamboocss_extractor.box.isRecipe(map)) parserResult.setCva(result);
788
- else parserResult.set("css", result);
789
- const recipeOptions = query.box.value[2];
790
- if (_bamboocss_extractor.box.isUnresolvable(map) && recipeOptions && _bamboocss_extractor.box.isMap(recipeOptions) && recipeOptions.value.has("defaultProps")) {
791
- const maybeIdentifier = map.getNode();
792
- if (ts_morph.Node.isIdentifier(maybeIdentifier)) {
793
- const name = maybeIdentifier.getText();
794
- const recipeName = file.getName(name);
795
- parserResult.setRecipe(recipeName, {
796
- type: "jsx-recipe",
797
- name: recipeName,
798
- box: recipeOptions,
799
- data: combineResult((0, _bamboocss_extractor.unbox)(recipeOptions.value.get("defaultProps")))
800
- });
801
- }
802
- }
803
- } else if (query.kind === "tagged-template") {
804
- const obj = (0, _bamboocss_shared.astish)(query.box.value);
805
- parserResult.set("css", {
806
- name,
807
- box: query.box ?? _bamboocss_extractor.box.fallback(query.box),
808
- data: [obj]
809
- });
810
- }
811
- });
812
- }).when(file.isJsxFactory, (name) => {
813
- result.queryList.forEach((query) => {
814
- if (query.kind === "call-expression") {
815
- const map = query.box.value[0];
816
- const boxNode = _bamboocss_extractor.box.isMap(map) ? map : _bamboocss_extractor.box.fallback(query.box);
817
- const combined = combineResult((0, _bamboocss_extractor.unbox)(boxNode));
818
- const result = {
819
- name,
820
- box: boxNode,
821
- data: options?.transform?.({
822
- type: "jsx-factory",
823
- data: combined
824
- }) ?? combined
825
- };
826
- if (_bamboocss_extractor.box.isRecipe(map)) parserResult.setCva(result);
827
- else parserResult.set("css", result);
828
- } else if (query.kind === "tagged-template") {
829
- const obj = (0, _bamboocss_shared.astish)(query.box.value);
830
- parserResult.set("css", {
831
- name,
832
- box: query.box ?? _bamboocss_extractor.box.fallback(query.box),
833
- data: [obj]
834
- });
835
- }
836
- });
837
928
  }).otherwise(() => {});
838
929
  else if (jsx.isEnabled && result.kind === "component") result.queryList.forEach((query) => {
839
930
  const data = combineResult((0, _bamboocss_extractor.unbox)(query.box));
840
- switch (true) {
841
- case file.isJsxFactory(name) || file.isJsxFactory(alias):
842
- parserResult.setJsx({
843
- type: "jsx-factory",
844
- name,
845
- box: query.box,
846
- data
847
- });
848
- break;
849
- case jsx.isJsxTagPattern(name) || jsx.isJsxTagPattern(alias):
850
- parserResult.setPattern(name, {
851
- type: "jsx-pattern",
852
- name,
931
+ for (const tag of [name, alias]) {
932
+ if (!jsx.isJsxTagRecipe(tag)) continue;
933
+ recipes.filter(tag).forEach((recipe) => {
934
+ parserResult.setRecipe(recipe.baseName, {
935
+ type: "jsx-recipe",
936
+ name: tag,
853
937
  box: query.box,
854
938
  data
855
939
  });
856
- break;
857
- case jsx.isJsxTagRecipe(name):
858
- recipes.filter(name).map((recipe) => {
859
- parserResult.setRecipe(recipe.baseName, {
860
- type: "jsx-recipe",
861
- name,
862
- box: query.box,
863
- data
864
- });
865
- });
866
- break;
867
- case jsx.isJsxTagRecipe(alias):
868
- recipes.filter(alias).map((recipe) => {
869
- parserResult.setRecipe(recipe.baseName, {
870
- type: "jsx-recipe",
871
- name: alias,
872
- box: query.box,
873
- data
874
- });
875
- });
876
- break;
877
- default: parserResult.setJsx({
878
- type: "jsx",
879
- name,
880
- box: query.box,
881
- data
882
940
  });
941
+ break;
883
942
  }
884
943
  });
885
944
  });
@@ -1090,7 +1149,6 @@ var Project = class {
1090
1149
  }
1091
1150
  }) ?? this.transformFile(filePath, original);
1092
1151
  if (original !== transformed) sourceFile.replaceWithText(transformed);
1093
- if (hooks["parser:preprocess"]) options.transform = hooks["parser:preprocess"];
1094
1152
  const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
1095
1153
  hooks["parser:after"]?.({
1096
1154
  filePath,
@@ -1109,3 +1167,4 @@ var Project = class {
1109
1167
  //#endregion
1110
1168
  exports.ParserResult = ParserResult;
1111
1169
  exports.Project = Project;
1170
+ exports.findUnresolvedStyles = findUnresolvedStyles;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, ParserOptions, StyleDecoder, Stylesheet } from "@bamboocss/core";
2
- import { ArtifactId, BambooHooks, ConfigTsOptions, CssArtifactType, JsxFactoryResultTransform, LoadConfigResult, ParserResultConfigureOptions, ParserResultInterface, ResultItem, Runtime, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
2
+ import { ArtifactId, BambooHooks, ConfigTsOptions, CssArtifactType, LoadConfigResult, ParserResultConfigureOptions, ParserResultInterface, ResultItem, Runtime, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
3
3
  import { FileSystemRefreshResult, Project as Project$1, ProjectOptions as ProjectOptions$1, SourceFile } from "ts-morph";
4
4
 
5
5
  //#region ../generator/dist/index.d.cts
@@ -93,6 +93,20 @@ declare class Generator extends Context {
93
93
  */
94
94
  private getAlwaysKeptTokenVars;
95
95
  getParserCss: (decoder: StyleDecoder) => string;
96
+ /**
97
+ * The grouped class names this build emitted a rule for.
98
+ *
99
+ * Derived from the encoder rather than the decoder, so it is available as soon as
100
+ * extraction finishes and before a stylesheet exists. Both sides go through
101
+ * `groupClassName`, which is the same function the browser runtime calls — a registry
102
+ * built any other way would be a third spelling of a name that already has two.
103
+ *
104
+ * Unescaped, unlike `StyleDecoder`'s class names: this is compared against what `css()`
105
+ * returns into a `class` attribute, not against a selector. A grouped class is an opaque
106
+ * hash, so the two only differ in principle, but the principle is the one that matters
107
+ * here — the registry is only useful if it holds exactly what the runtime will ask about.
108
+ */
109
+ getGroupRegistry: () => string[];
96
110
  getCss: (stylesheet?: Stylesheet) => string;
97
111
  /**
98
112
  * Get CSS for a specific layer from the stylesheet
@@ -119,12 +133,40 @@ declare class Generator extends Context {
119
133
  * Get CSS for a specific theme
120
134
  */
121
135
  //#endregion
136
+ //#region src/unresolved-styles.d.ts
137
+ interface UnresolvedStyle {
138
+ /** The property the build could not resolve, or `undefined` when only the count differs. */
139
+ prop?: string;
140
+ filePath: string;
141
+ line: number;
142
+ column: number;
143
+ /**
144
+ * How the build lost the call:
145
+ *
146
+ * - `unresolvable-value` — a value it could not evaluate.
147
+ * - `missing-property` — a key that never arrived in the box tree at all.
148
+ * - `unenumerable-keys` — a spread or computed key, so it cannot say what the call sets.
149
+ * - `ambiguous-merge` — two arguments setting one property, which it cannot tell from a
150
+ * pair of alternatives.
151
+ * - `too-many-combinations` — more ternary branches than it will enumerate.
152
+ */
153
+ reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys' | 'ambiguous-merge' | 'too-many-combinations';
154
+ }
155
+ /**
156
+ * Every property of a `css()` call that will not reach the stylesheet.
157
+ *
158
+ * Only meaningful under `cssMode: 'grouped'`, where one class names the whole call: a
159
+ * property the build cannot resolve does not merely go missing, it changes the class, and
160
+ * the element renders with no styles at all. Under `atomic` the same call loses one
161
+ * declaration and keeps the rest, which is why this is not reported there.
162
+ */
163
+ declare const findUnresolvedStyles: (item: ResultItem) => UnresolvedStyle[];
164
+ //#endregion
122
165
  //#region src/parser-result.d.ts
123
166
  declare class ParserResult implements ParserResultInterface {
124
167
  private context;
125
168
  /** Ordered list of all ResultItem */
126
169
  all: ResultItem[];
127
- jsx: Set<ResultItem>;
128
170
  css: Set<ResultItem>;
129
171
  cva: Set<ResultItem>;
130
172
  sva: Set<ResultItem>;
@@ -134,16 +176,51 @@ declare class ParserResult implements ParserResultInterface {
134
176
  pattern: Map<string, Set<ResultItem>>;
135
177
  filePath: string | undefined;
136
178
  encoder: ParserOptions['encoder'];
179
+ /**
180
+ * `css()` calls whose styles the build could not fully see.
181
+ *
182
+ * Only collected under `cssMode: 'grouped'`, where one class names the whole call, so a
183
+ * property the build cannot resolve changes the class rather than dropping a declaration
184
+ * from it — and the element renders with no styles at all. Under `atomic` the same call
185
+ * keeps everything the build did resolve, which is not worth interrupting a build over.
186
+ */
187
+ unresolved: UnresolvedStyle[];
137
188
  constructor(context: ParserOptions, encoder?: ParserOptions['encoder']);
189
+ /**
190
+ * Record a call whose styles the build could not fully see, at the call's own position.
191
+ *
192
+ * Used for losses the box tree cannot show — a ternary past the combination cap emits
193
+ * fragments rather than whole objects, and every individual box in it resolved fine.
194
+ */
195
+ private reportUnresolved;
138
196
  append(result: ResultItem): ResultItem;
139
197
  set(name: 'cva' | 'css' | 'sva' | 'token', result: ResultItem): void;
140
198
  setCss(result: ResultItem): void;
199
+ /**
200
+ * How many arguments the call this result came from was written with.
201
+ *
202
+ * Returns 1 for anything that is not a call — a JSX element, or a box that lost its node —
203
+ * since the question only separates operands from branches and neither has operands.
204
+ */
205
+ private callArgumentCount;
141
206
  setCva(result: ResultItem): void;
142
207
  setSva(result: ResultItem): void;
143
208
  setToken(result: ResultItem): void;
144
209
  setViewTransition(result: ResultItem): void;
145
- setJsx(result: ResultItem): void;
146
210
  setPattern(name: string, result: ResultItem): void;
211
+ /**
212
+ * Whether the group encoded for this result is the one the runtime will ask for.
213
+ *
214
+ * True only when the build saw the whole thing at once: one style object, with every
215
+ * value in it resolved. Several objects means the runtime merges them into a call this
216
+ * never encoded — `setCss` reconstructs those combinations, and nothing else does — and
217
+ * an unresolved value means the merge would not have matched anyway.
218
+ *
219
+ * Answering "no" costs a call site its atomic rules, which is CSS that duplicates the
220
+ * group. Answering a wrong "yes" costs the element every style it has, so this is
221
+ * deliberately conservative.
222
+ */
223
+ private groupIsExact;
147
224
  setRecipe(recipeName: string, result: ResultItem): void;
148
225
  isEmpty(): boolean;
149
226
  setFilePath(filePath: string): this;
@@ -155,7 +232,6 @@ declare class ParserResult implements ParserResultInterface {
155
232
  sva: ResultItem[];
156
233
  token: ResultItem[];
157
234
  viewTransition: ResultItem[];
158
- jsx: ResultItem[];
159
235
  recipe: {
160
236
  [k: string]: ResultItem[];
161
237
  };
@@ -166,7 +242,7 @@ declare class ParserResult implements ParserResultInterface {
166
242
  }
167
243
  //#endregion
168
244
  //#region src/parser.d.ts
169
- declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions & Partial<JsxFactoryResultTransform>) => ParserResult | undefined;
245
+ declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions) => ParserResult | undefined;
170
246
  //#endregion
171
247
  //#region src/project.d.ts
172
248
  interface ProjectOptions extends ProjectOptions$1 {
@@ -251,4 +327,4 @@ declare class Project {
251
327
  classify: (fileMap: Map<string, ParserResultInterface>) => import("@bamboocss/types").ClassifyReport;
252
328
  }
253
329
  //#endregion
254
- export { ParserResult, Project, ProjectOptions };
330
+ export { ParserResult, Project, ProjectOptions, type UnresolvedStyle, findUnresolvedStyles };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FileSystemRefreshResult, Project as Project$1, ProjectOptions as ProjectOptions$1, SourceFile } from "ts-morph";
2
2
  import { Context, ParserOptions, StyleDecoder, Stylesheet } from "@bamboocss/core";
3
- import { ArtifactId, BambooHooks, ConfigTsOptions, CssArtifactType, JsxFactoryResultTransform, LoadConfigResult, ParserResultConfigureOptions, ParserResultInterface, ResultItem, Runtime, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
3
+ import { ArtifactId, BambooHooks, ConfigTsOptions, CssArtifactType, LoadConfigResult, ParserResultConfigureOptions, ParserResultInterface, ResultItem, Runtime, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
4
4
 
5
5
  //#region ../generator/dist/index.d.cts
6
6
  //#region src/generator.d.ts
@@ -93,6 +93,20 @@ declare class Generator extends Context {
93
93
  */
94
94
  private getAlwaysKeptTokenVars;
95
95
  getParserCss: (decoder: StyleDecoder) => string;
96
+ /**
97
+ * The grouped class names this build emitted a rule for.
98
+ *
99
+ * Derived from the encoder rather than the decoder, so it is available as soon as
100
+ * extraction finishes and before a stylesheet exists. Both sides go through
101
+ * `groupClassName`, which is the same function the browser runtime calls — a registry
102
+ * built any other way would be a third spelling of a name that already has two.
103
+ *
104
+ * Unescaped, unlike `StyleDecoder`'s class names: this is compared against what `css()`
105
+ * returns into a `class` attribute, not against a selector. A grouped class is an opaque
106
+ * hash, so the two only differ in principle, but the principle is the one that matters
107
+ * here — the registry is only useful if it holds exactly what the runtime will ask about.
108
+ */
109
+ getGroupRegistry: () => string[];
96
110
  getCss: (stylesheet?: Stylesheet) => string;
97
111
  /**
98
112
  * Get CSS for a specific layer from the stylesheet
@@ -119,12 +133,40 @@ declare class Generator extends Context {
119
133
  * Get CSS for a specific theme
120
134
  */
121
135
  //#endregion
136
+ //#region src/unresolved-styles.d.ts
137
+ interface UnresolvedStyle {
138
+ /** The property the build could not resolve, or `undefined` when only the count differs. */
139
+ prop?: string;
140
+ filePath: string;
141
+ line: number;
142
+ column: number;
143
+ /**
144
+ * How the build lost the call:
145
+ *
146
+ * - `unresolvable-value` — a value it could not evaluate.
147
+ * - `missing-property` — a key that never arrived in the box tree at all.
148
+ * - `unenumerable-keys` — a spread or computed key, so it cannot say what the call sets.
149
+ * - `ambiguous-merge` — two arguments setting one property, which it cannot tell from a
150
+ * pair of alternatives.
151
+ * - `too-many-combinations` — more ternary branches than it will enumerate.
152
+ */
153
+ reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys' | 'ambiguous-merge' | 'too-many-combinations';
154
+ }
155
+ /**
156
+ * Every property of a `css()` call that will not reach the stylesheet.
157
+ *
158
+ * Only meaningful under `cssMode: 'grouped'`, where one class names the whole call: a
159
+ * property the build cannot resolve does not merely go missing, it changes the class, and
160
+ * the element renders with no styles at all. Under `atomic` the same call loses one
161
+ * declaration and keeps the rest, which is why this is not reported there.
162
+ */
163
+ declare const findUnresolvedStyles: (item: ResultItem) => UnresolvedStyle[];
164
+ //#endregion
122
165
  //#region src/parser-result.d.ts
123
166
  declare class ParserResult implements ParserResultInterface {
124
167
  private context;
125
168
  /** Ordered list of all ResultItem */
126
169
  all: ResultItem[];
127
- jsx: Set<ResultItem>;
128
170
  css: Set<ResultItem>;
129
171
  cva: Set<ResultItem>;
130
172
  sva: Set<ResultItem>;
@@ -134,16 +176,51 @@ declare class ParserResult implements ParserResultInterface {
134
176
  pattern: Map<string, Set<ResultItem>>;
135
177
  filePath: string | undefined;
136
178
  encoder: ParserOptions['encoder'];
179
+ /**
180
+ * `css()` calls whose styles the build could not fully see.
181
+ *
182
+ * Only collected under `cssMode: 'grouped'`, where one class names the whole call, so a
183
+ * property the build cannot resolve changes the class rather than dropping a declaration
184
+ * from it — and the element renders with no styles at all. Under `atomic` the same call
185
+ * keeps everything the build did resolve, which is not worth interrupting a build over.
186
+ */
187
+ unresolved: UnresolvedStyle[];
137
188
  constructor(context: ParserOptions, encoder?: ParserOptions['encoder']);
189
+ /**
190
+ * Record a call whose styles the build could not fully see, at the call's own position.
191
+ *
192
+ * Used for losses the box tree cannot show — a ternary past the combination cap emits
193
+ * fragments rather than whole objects, and every individual box in it resolved fine.
194
+ */
195
+ private reportUnresolved;
138
196
  append(result: ResultItem): ResultItem;
139
197
  set(name: 'cva' | 'css' | 'sva' | 'token', result: ResultItem): void;
140
198
  setCss(result: ResultItem): void;
199
+ /**
200
+ * How many arguments the call this result came from was written with.
201
+ *
202
+ * Returns 1 for anything that is not a call — a JSX element, or a box that lost its node —
203
+ * since the question only separates operands from branches and neither has operands.
204
+ */
205
+ private callArgumentCount;
141
206
  setCva(result: ResultItem): void;
142
207
  setSva(result: ResultItem): void;
143
208
  setToken(result: ResultItem): void;
144
209
  setViewTransition(result: ResultItem): void;
145
- setJsx(result: ResultItem): void;
146
210
  setPattern(name: string, result: ResultItem): void;
211
+ /**
212
+ * Whether the group encoded for this result is the one the runtime will ask for.
213
+ *
214
+ * True only when the build saw the whole thing at once: one style object, with every
215
+ * value in it resolved. Several objects means the runtime merges them into a call this
216
+ * never encoded — `setCss` reconstructs those combinations, and nothing else does — and
217
+ * an unresolved value means the merge would not have matched anyway.
218
+ *
219
+ * Answering "no" costs a call site its atomic rules, which is CSS that duplicates the
220
+ * group. Answering a wrong "yes" costs the element every style it has, so this is
221
+ * deliberately conservative.
222
+ */
223
+ private groupIsExact;
147
224
  setRecipe(recipeName: string, result: ResultItem): void;
148
225
  isEmpty(): boolean;
149
226
  setFilePath(filePath: string): this;
@@ -155,7 +232,6 @@ declare class ParserResult implements ParserResultInterface {
155
232
  sva: ResultItem[];
156
233
  token: ResultItem[];
157
234
  viewTransition: ResultItem[];
158
- jsx: ResultItem[];
159
235
  recipe: {
160
236
  [k: string]: ResultItem[];
161
237
  };
@@ -166,7 +242,7 @@ declare class ParserResult implements ParserResultInterface {
166
242
  }
167
243
  //#endregion
168
244
  //#region src/parser.d.ts
169
- declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions & Partial<JsxFactoryResultTransform>) => ParserResult | undefined;
245
+ declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions) => ParserResult | undefined;
170
246
  //#endregion
171
247
  //#region src/project.d.ts
172
248
  interface ProjectOptions extends ProjectOptions$1 {
@@ -251,4 +327,4 @@ declare class Project {
251
327
  classify: (fileMap: Map<string, ParserResultInterface>) => import("@bamboocss/types").ClassifyReport;
252
328
  }
253
329
  //#endregion
254
- export { ParserResult, Project, ProjectOptions };
330
+ export { ParserResult, Project, ProjectOptions, type UnresolvedStyle, findUnresolvedStyles };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Node, Project as Project$1, ScriptKind, ts } from "ts-morph";
2
2
  import { box, clearBoxNodeCache, extract, unbox } from "@bamboocss/extractor";
3
- import { BambooError, astish, compact, getOrCreateSet, patternFns } from "@bamboocss/shared";
3
+ import { BambooError, compact, getOrCreateSet, patternFns } from "@bamboocss/shared";
4
4
  import { logger } from "@bamboocss/logger";
5
5
  import { match } from "ts-pattern";
6
6
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
@@ -43,7 +43,7 @@ function classifyProject(ctx, resultMap) {
43
43
  const processPattern = (opts) => {
44
44
  const { boxNode, data, item, filepath, localMaps } = opts;
45
45
  const name = item.componentName;
46
- const pattern = ctx.patterns.details.find((p) => p.match.test(name) || p.baseName === name);
46
+ const pattern = ctx.patterns.details.find((p) => p.baseName === name);
47
47
  if (!pattern) return;
48
48
  const cssObj = pattern.config.transform?.(data || {}, patternFns) ?? {};
49
49
  const newItem = {
@@ -75,7 +75,7 @@ function classifyProject(ctx, resultMap) {
75
75
  contains: [],
76
76
  debug: Reflect.has(item, "debug")
77
77
  };
78
- if (item.type === "pattern" || item.type === "jsx-pattern") return processPattern({
78
+ if (item.type === "pattern") return processPattern({
79
79
  boxNode: item.box,
80
80
  data: item.data[0],
81
81
  item: componentReportItem,
@@ -200,14 +200,6 @@ function classifyProject(ctx, resultMap) {
200
200
  resultMap.forEach((parserResult, filepath) => {
201
201
  if (parserResult.isEmpty()) return;
202
202
  const localMaps = createReportMaps();
203
- const componentFn = (item) => {
204
- processResultItemFn({
205
- item,
206
- filepath,
207
- localMaps,
208
- type: "component"
209
- });
210
- };
211
203
  const functionFn = (item) => {
212
204
  processResultItemFn({
213
205
  item,
@@ -216,7 +208,6 @@ function classifyProject(ctx, resultMap) {
216
208
  type: "function"
217
209
  });
218
210
  };
219
- parserResult.jsx.forEach(componentFn);
220
211
  parserResult.css.forEach(functionFn);
221
212
  parserResult.cva.forEach(functionFn);
222
213
  parserResult.pattern.forEach((itemList) => {
@@ -465,6 +456,120 @@ function getImportDeclarations(context, sourceFile) {
465
456
  return importDeclarations;
466
457
  }
467
458
  //#endregion
459
+ //#region src/unresolved-styles.ts
460
+ /**
461
+ * Whether every box under this one carries a value the build can actually see.
462
+ *
463
+ * Mirrors `isStaticBox` in `@bamboocss/vite`, which asks the same question to decide
464
+ * whether a call is safe to fold. Duplicated rather than shared for now because the fold's
465
+ * copy also answers questions about ternaries that a diagnostic does not care about;
466
+ * unifying them is worth doing when the fold's detection moves out of the vite package.
467
+ */
468
+ const findUnresolvable = (node, path, out, seen = /* @__PURE__ */ new Set()) => {
469
+ if (!node || seen.has(node)) return;
470
+ seen.add(node);
471
+ if (box.isUnresolvable(node)) {
472
+ out.push(path.join("."));
473
+ return;
474
+ }
475
+ if (box.isConditional(node)) return;
476
+ if (box.isLiteral(node) && node.value === void 0) {
477
+ if (Node.isTemplateExpression(node.getNode())) out.push(path.join("."));
478
+ return;
479
+ }
480
+ if (box.isMap(node)) {
481
+ for (const [key, child] of node.value) findUnresolvable(child, [...path, key], out, seen);
482
+ return;
483
+ }
484
+ if (box.isArray(node)) node.value.forEach((child, index) => findUnresolvable(child, [...path, String(index)], out, seen));
485
+ };
486
+ /**
487
+ * Property names written at the top level of the call's own object literal.
488
+ *
489
+ * A key whose value the extractor could not evaluate at all is not boxed as
490
+ * `unresolvable` — `maybeBoxNode` returns nothing and the pair is never recorded
491
+ * (`get-object-literal-expression-prop-pairs.ts` has no fallback), so the property
492
+ * disappears with no trace in the box tree. Reading the source back is the only way to
493
+ * notice, and it is why `css({ color: getColor() })` needs this and not just the walk above.
494
+ *
495
+ * Returns `undefined` only when the argument is not a single object literal at all, so the
496
+ * caller reports nothing rather than something wrong.
497
+ */
498
+ const writtenProps = (node) => {
499
+ let literal = node;
500
+ if (literal && Node.isCallExpression(literal)) {
501
+ const args = literal.getArguments();
502
+ if (args.length !== 1) return void 0;
503
+ literal = args[0];
504
+ }
505
+ if (!literal || !Node.isObjectLiteralExpression(literal)) return void 0;
506
+ const names = [];
507
+ let uncertain = false;
508
+ for (const property of literal.getProperties()) {
509
+ if (Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property)) {
510
+ const name = property.getName();
511
+ if (name.startsWith("[")) {
512
+ uncertain = true;
513
+ continue;
514
+ }
515
+ names.push(name.replace(/^['"]|['"]$/g, ""));
516
+ continue;
517
+ }
518
+ uncertain = true;
519
+ }
520
+ return {
521
+ names,
522
+ uncertain
523
+ };
524
+ };
525
+ /**
526
+ * Every property of a `css()` call that will not reach the stylesheet.
527
+ *
528
+ * Only meaningful under `cssMode: 'grouped'`, where one class names the whole call: a
529
+ * property the build cannot resolve does not merely go missing, it changes the class, and
530
+ * the element renders with no styles at all. Under `atomic` the same call loses one
531
+ * declaration and keeps the rest, which is why this is not reported there.
532
+ */
533
+ const findUnresolvedStyles = (item) => {
534
+ const boxNode = item.box;
535
+ if (!boxNode) return [];
536
+ const node = boxNode.getNode();
537
+ const sourceFile = node?.getSourceFile();
538
+ if (!node || !sourceFile) return [];
539
+ const found = [];
540
+ findUnresolvable(boxNode, [], found);
541
+ const losses = found.map((prop) => ({
542
+ prop: prop || void 0,
543
+ reason: "unresolvable-value"
544
+ }));
545
+ const written = writtenProps(node);
546
+ if (written) {
547
+ const resolved = /* @__PURE__ */ new Set();
548
+ for (const entry of item.data) if (entry && typeof entry === "object") for (const key of Object.keys(entry)) resolved.add(key);
549
+ if (box.isMap(boxNode)) for (const key of boxNode.value.keys()) resolved.add(key);
550
+ for (const prop of written.names) if (!resolved.has(prop)) losses.push({
551
+ prop,
552
+ reason: "missing-property"
553
+ });
554
+ if (written.uncertain && !hasKeyOutside(resolved, written.names)) losses.push({ reason: "unenumerable-keys" });
555
+ }
556
+ if (!losses.length) return [];
557
+ const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
558
+ const at = {
559
+ filePath: sourceFile.getFilePath(),
560
+ line,
561
+ column
562
+ };
563
+ return losses.map((loss) => ({
564
+ ...at,
565
+ ...loss
566
+ }));
567
+ };
568
+ const hasKeyOutside = (resolved, names) => {
569
+ for (const key of resolved) if (!names.includes(key)) return true;
570
+ return false;
571
+ };
572
+ //#endregion
468
573
  //#region src/parser-result.ts
469
574
  function cartesian(arrays) {
470
575
  if (arrays.length === 0) return [[]];
@@ -476,7 +581,6 @@ var ParserResult = class {
476
581
  context;
477
582
  /** Ordered list of all ResultItem */
478
583
  all = [];
479
- jsx = /* @__PURE__ */ new Set();
480
584
  css = /* @__PURE__ */ new Set();
481
585
  cva = /* @__PURE__ */ new Set();
482
586
  sva = /* @__PURE__ */ new Set();
@@ -486,10 +590,37 @@ var ParserResult = class {
486
590
  pattern = /* @__PURE__ */ new Map();
487
591
  filePath;
488
592
  encoder;
593
+ /**
594
+ * `css()` calls whose styles the build could not fully see.
595
+ *
596
+ * Only collected under `cssMode: 'grouped'`, where one class names the whole call, so a
597
+ * property the build cannot resolve changes the class rather than dropping a declaration
598
+ * from it — and the element renders with no styles at all. Under `atomic` the same call
599
+ * keeps everything the build did resolve, which is not worth interrupting a build over.
600
+ */
601
+ unresolved = [];
489
602
  constructor(context, encoder) {
490
603
  this.context = context;
491
604
  this.encoder = encoder ?? context.encoder;
492
605
  }
606
+ /**
607
+ * Record a call whose styles the build could not fully see, at the call's own position.
608
+ *
609
+ * Used for losses the box tree cannot show — a ternary past the combination cap emits
610
+ * fragments rather than whole objects, and every individual box in it resolved fine.
611
+ */
612
+ reportUnresolved(result, reason) {
613
+ const node = result.box?.getNode();
614
+ const sourceFile = node?.getSourceFile();
615
+ if (!node || !sourceFile) return;
616
+ const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
617
+ this.unresolved.push({
618
+ filePath: sourceFile.getFilePath(),
619
+ line,
620
+ column,
621
+ reason
622
+ });
623
+ }
493
624
  append(result) {
494
625
  this.all.push(result);
495
626
  return result;
@@ -515,24 +646,36 @@ var ParserResult = class {
515
646
  this.css.add(this.append(Object.assign({ type: "css" }, result)));
516
647
  const encoder = this.encoder;
517
648
  const grouped = this.context.config.cssMode === "grouped";
518
- if (!grouped || result.data.length <= 1) {
519
- result.data.forEach((obj) => grouped ? encoder.processGrouped(obj) : encoder.processAtomic(obj));
649
+ const data = result.data.some(Array.isArray) ? result.data.flatMap((obj) => Array.isArray(obj) ? obj : [obj]) : result.data;
650
+ if (grouped) {
651
+ const unresolved = findUnresolvedStyles(result);
652
+ if (unresolved.length) {
653
+ this.unresolved.push(...unresolved);
654
+ data.forEach((obj) => encoder.processAtomic(obj));
655
+ }
656
+ }
657
+ if (!grouped || data.length <= 1) {
658
+ data.forEach((obj) => grouped ? encoder.processGrouped(obj) : encoder.processAtomic(obj));
520
659
  return;
521
660
  }
522
661
  const keyCounts = /* @__PURE__ */ new Map();
523
- for (const obj of result.data) for (const key of Object.keys(obj)) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
662
+ for (const obj of data) for (const key of Object.keys(obj)) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
524
663
  if (!Array.from(keyCounts.values()).some((c) => c > 1)) {
525
- encoder.processGrouped(Object.assign({}, ...result.data));
664
+ encoder.processGroupedMerge(data);
526
665
  return;
527
666
  }
667
+ if (this.callArgumentCount(result) > 1) {
668
+ this.reportUnresolved(result, "ambiguous-merge");
669
+ data.forEach((obj) => encoder.processAtomic(obj));
670
+ }
528
671
  const overlappingKeys = /* @__PURE__ */ new Set();
529
672
  keyCounts.forEach((count, key) => {
530
673
  if (count > 1) overlappingKeys.add(key);
531
674
  });
532
- const base = {};
675
+ const baseEntries = [];
533
676
  const branchEntries = [];
534
- for (const obj of result.data) if (Object.keys(obj).some((k) => overlappingKeys.has(k))) branchEntries.push(obj);
535
- else Object.assign(base, obj);
677
+ for (const obj of data) if (Object.keys(obj).some((k) => overlappingKeys.has(k))) branchEntries.push(obj);
678
+ else baseEntries.push(obj);
536
679
  const branchGroups = /* @__PURE__ */ new Map();
537
680
  for (const entry of branchEntries) {
538
681
  const keySet = Object.keys(entry).sort().join("\0");
@@ -542,10 +685,24 @@ var ParserResult = class {
542
685
  }
543
686
  const groupArrays = Array.from(branchGroups.values());
544
687
  if (groupArrays.reduce((acc, g) => acc * g.length, 1) > 32) {
545
- result.data.forEach((obj) => encoder.processGrouped(obj));
688
+ this.reportUnresolved(result, "too-many-combinations");
689
+ data.forEach((obj) => {
690
+ encoder.processGrouped(obj);
691
+ encoder.processAtomic(obj);
692
+ });
546
693
  return;
547
694
  }
548
- for (const combo of cartesian(groupArrays)) encoder.processGrouped(Object.assign({}, base, ...combo));
695
+ for (const combo of cartesian(groupArrays)) encoder.processGroupedMerge([...baseEntries, ...combo]);
696
+ }
697
+ /**
698
+ * How many arguments the call this result came from was written with.
699
+ *
700
+ * Returns 1 for anything that is not a call — a JSX element, or a box that lost its node —
701
+ * since the question only separates operands from branches and neither has operands.
702
+ */
703
+ callArgumentCount(result) {
704
+ const node = result.box?.getNode();
705
+ return node && Node.isCallExpression(node) ? node.getArguments().length : 1;
549
706
  }
550
707
  setCva(result) {
551
708
  this.cva.add(this.append(Object.assign({ type: "cva" }, result)));
@@ -565,19 +722,31 @@ var ParserResult = class {
565
722
  const encoder = this.encoder;
566
723
  result.data.forEach((obj) => encoder.processViewTransition(obj));
567
724
  }
568
- setJsx(result) {
569
- this.jsx.add(this.append(Object.assign({ type: "jsx" }, result)));
570
- const encoder = this.encoder;
571
- const grouped = this.context.config.cssMode === "grouped";
572
- result.data.forEach((obj) => encoder.processStyleProps(obj, grouped));
573
- }
574
725
  setPattern(name, result) {
575
726
  getOrCreateSet(this.pattern, name).add(this.append(Object.assign({
576
727
  type: "pattern",
577
728
  name
578
729
  }, result)));
579
730
  const encoder = this.encoder;
580
- result.data.forEach((obj) => encoder.processPattern(name, obj, result.type ?? "pattern", result.name));
731
+ const grouped = this.context.config.cssMode === "grouped";
732
+ result.data.forEach((obj) => encoder.processPattern(name, obj, grouped));
733
+ if (grouped && !this.groupIsExact(result)) result.data.forEach((obj) => encoder.processPattern(name, obj, false));
734
+ }
735
+ /**
736
+ * Whether the group encoded for this result is the one the runtime will ask for.
737
+ *
738
+ * True only when the build saw the whole thing at once: one style object, with every
739
+ * value in it resolved. Several objects means the runtime merges them into a call this
740
+ * never encoded — `setCss` reconstructs those combinations, and nothing else does — and
741
+ * an unresolved value means the merge would not have matched anyway.
742
+ *
743
+ * Answering "no" costs a call site its atomic rules, which is CSS that duplicates the
744
+ * group. Answering a wrong "yes" costs the element every style it has, so this is
745
+ * deliberately conservative.
746
+ */
747
+ groupIsExact(result) {
748
+ if (result.data.length !== 1) return false;
749
+ return findUnresolvedStyles(result).length === 0;
581
750
  }
582
751
  setRecipe(recipeName, result) {
583
752
  getOrCreateSet(this.recipe, recipeName).add(this.append(Object.assign({ type: "recipe" }, result)));
@@ -607,7 +776,6 @@ var ParserResult = class {
607
776
  result.sva.forEach((item) => this.sva.add(this.append(item)));
608
777
  result.token.forEach((item) => this.token.add(this.append(item)));
609
778
  result.viewTransition.forEach((item) => this.viewTransition.add(this.append(item)));
610
- result.jsx.forEach((item) => this.jsx.add(this.append(item)));
611
779
  result.recipe.forEach((items, name) => {
612
780
  const set = getOrCreateSet(this.recipe, name);
613
781
  items.forEach((item) => set.add(this.append(item)));
@@ -616,6 +784,7 @@ var ParserResult = class {
616
784
  const set = getOrCreateSet(this.pattern, name);
617
785
  items.forEach((item) => set.add(this.append(item)));
618
786
  });
787
+ if (result.unresolved.length) this.unresolved.push(...result.unresolved);
619
788
  return this;
620
789
  }
621
790
  toArray() {
@@ -628,7 +797,6 @@ var ParserResult = class {
628
797
  sva: Array.from(this.sva),
629
798
  token: Array.from(this.token),
630
799
  viewTransition: Array.from(this.viewTransition),
631
- jsx: Array.from(this.jsx),
632
800
  recipe: Object.fromEntries(Array.from(this.recipe.entries()).map(([key, value]) => [key, Array.from(value)])),
633
801
  pattern: Object.fromEntries(Array.from(this.pattern.entries()).map(([key, value]) => [key, Array.from(value)]))
634
802
  };
@@ -653,8 +821,7 @@ const defaultEnv = { preset: "ECMA" };
653
821
  const fallbackImpl = (...values) => values.some((value) => value === void 0) ? void 0 : `fallback(${values.join(", ")})`;
654
822
  const evaluateOptions = { environment: defaultEnv };
655
823
  function createParser(context) {
656
- const { jsx, imports, recipes, config } = context;
657
- const syntax = config.syntax;
824
+ const { jsx, imports, recipes } = context;
658
825
  return function parse(sourceFile, encoder, options) {
659
826
  if (!sourceFile) return;
660
827
  const importDeclarations = getImportDeclarations(context, sourceFile);
@@ -689,12 +856,8 @@ function createParser(context) {
689
856
  functions: {
690
857
  matchFn: (prop) => file.matchFn(prop.fnName),
691
858
  matchProp: () => true,
692
- matchArg: (prop) => {
693
- if (file.isJsxFactory(prop.fnName) && prop.index === 1 && Node.isIdentifier(prop.argNode)) return false;
694
- return true;
695
- }
859
+ matchArg: () => true
696
860
  },
697
- taggedTemplates: syntax === "template-literal" ? { matchTaggedTemplate: (tag) => file.matchFn(tag.fnName) } : void 0,
698
861
  getEvaluateOptions: (node) => {
699
862
  if (!Node.isCallExpression(node)) return evaluateOptions;
700
863
  const propAccessExpr = node.getExpression();
@@ -728,14 +891,6 @@ function createParser(context) {
728
891
  box: query.box.value[0] ?? box.fallback(query.box),
729
892
  data: combineResult(unbox(query.box.value[0]))
730
893
  });
731
- else if (query.kind === "tagged-template") {
732
- const obj = astish(query.box.value);
733
- parserResult.set(name, {
734
- name,
735
- box: query.box ?? box.fallback(query.box),
736
- data: [obj]
737
- });
738
- }
739
894
  });
740
895
  }).when(imports.matchers.tokens.match, (name) => {
741
896
  result.queryList.forEach((query) => {
@@ -761,7 +916,7 @@ function createParser(context) {
761
916
  data: combineResult(unbox(query.box.value[0]))
762
917
  });
763
918
  });
764
- }).when((name) => syntax !== "template-literal" && file.isViewTransitionFn(name), (name) => {
919
+ }).when(file.isViewTransitionFn, (name) => {
765
920
  result.queryList.forEach((query) => {
766
921
  if (query.kind === "call-expression") parserResult.setViewTransition({
767
922
  name,
@@ -769,116 +924,20 @@ function createParser(context) {
769
924
  data: combineResult(unbox(query.box.value[0]))
770
925
  });
771
926
  });
772
- }).when(jsx.isJsxFactory, () => {
773
- result.queryList.forEach((query) => {
774
- if (query.kind === "call-expression" && query.box.value[1]) {
775
- const map = query.box.value[1];
776
- const boxNode = box.isMap(map) ? map : box.fallback(query.box);
777
- const combined = combineResult(unbox(boxNode));
778
- const result = {
779
- name,
780
- box: boxNode,
781
- data: options?.transform?.({
782
- type: "jsx-factory",
783
- data: combined
784
- }) ?? combined
785
- };
786
- if (box.isRecipe(map)) parserResult.setCva(result);
787
- else parserResult.set("css", result);
788
- const recipeOptions = query.box.value[2];
789
- if (box.isUnresolvable(map) && recipeOptions && box.isMap(recipeOptions) && recipeOptions.value.has("defaultProps")) {
790
- const maybeIdentifier = map.getNode();
791
- if (Node.isIdentifier(maybeIdentifier)) {
792
- const name = maybeIdentifier.getText();
793
- const recipeName = file.getName(name);
794
- parserResult.setRecipe(recipeName, {
795
- type: "jsx-recipe",
796
- name: recipeName,
797
- box: recipeOptions,
798
- data: combineResult(unbox(recipeOptions.value.get("defaultProps")))
799
- });
800
- }
801
- }
802
- } else if (query.kind === "tagged-template") {
803
- const obj = astish(query.box.value);
804
- parserResult.set("css", {
805
- name,
806
- box: query.box ?? box.fallback(query.box),
807
- data: [obj]
808
- });
809
- }
810
- });
811
- }).when(file.isJsxFactory, (name) => {
812
- result.queryList.forEach((query) => {
813
- if (query.kind === "call-expression") {
814
- const map = query.box.value[0];
815
- const boxNode = box.isMap(map) ? map : box.fallback(query.box);
816
- const combined = combineResult(unbox(boxNode));
817
- const result = {
818
- name,
819
- box: boxNode,
820
- data: options?.transform?.({
821
- type: "jsx-factory",
822
- data: combined
823
- }) ?? combined
824
- };
825
- if (box.isRecipe(map)) parserResult.setCva(result);
826
- else parserResult.set("css", result);
827
- } else if (query.kind === "tagged-template") {
828
- const obj = astish(query.box.value);
829
- parserResult.set("css", {
830
- name,
831
- box: query.box ?? box.fallback(query.box),
832
- data: [obj]
833
- });
834
- }
835
- });
836
927
  }).otherwise(() => {});
837
928
  else if (jsx.isEnabled && result.kind === "component") result.queryList.forEach((query) => {
838
929
  const data = combineResult(unbox(query.box));
839
- switch (true) {
840
- case file.isJsxFactory(name) || file.isJsxFactory(alias):
841
- parserResult.setJsx({
842
- type: "jsx-factory",
843
- name,
844
- box: query.box,
845
- data
846
- });
847
- break;
848
- case jsx.isJsxTagPattern(name) || jsx.isJsxTagPattern(alias):
849
- parserResult.setPattern(name, {
850
- type: "jsx-pattern",
851
- name,
930
+ for (const tag of [name, alias]) {
931
+ if (!jsx.isJsxTagRecipe(tag)) continue;
932
+ recipes.filter(tag).forEach((recipe) => {
933
+ parserResult.setRecipe(recipe.baseName, {
934
+ type: "jsx-recipe",
935
+ name: tag,
852
936
  box: query.box,
853
937
  data
854
938
  });
855
- break;
856
- case jsx.isJsxTagRecipe(name):
857
- recipes.filter(name).map((recipe) => {
858
- parserResult.setRecipe(recipe.baseName, {
859
- type: "jsx-recipe",
860
- name,
861
- box: query.box,
862
- data
863
- });
864
- });
865
- break;
866
- case jsx.isJsxTagRecipe(alias):
867
- recipes.filter(alias).map((recipe) => {
868
- parserResult.setRecipe(recipe.baseName, {
869
- type: "jsx-recipe",
870
- name: alias,
871
- box: query.box,
872
- data
873
- });
874
- });
875
- break;
876
- default: parserResult.setJsx({
877
- type: "jsx",
878
- name,
879
- box: query.box,
880
- data
881
939
  });
940
+ break;
882
941
  }
883
942
  });
884
943
  });
@@ -1089,7 +1148,6 @@ var Project = class {
1089
1148
  }
1090
1149
  }) ?? this.transformFile(filePath, original);
1091
1150
  if (original !== transformed) sourceFile.replaceWithText(transformed);
1092
- if (hooks["parser:preprocess"]) options.transform = hooks["parser:preprocess"];
1093
1151
  const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
1094
1152
  hooks["parser:after"]?.({
1095
1153
  filePath,
@@ -1106,4 +1164,4 @@ var Project = class {
1106
1164
  };
1107
1165
  };
1108
1166
  //#endregion
1109
- export { ParserResult, Project };
1167
+ export { ParserResult, Project, findUnresolvedStyles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/parser",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "The static parser for bamboo css",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -34,17 +34,17 @@
34
34
  "dependencies": {
35
35
  "ts-morph": "28.0.0",
36
36
  "ts-pattern": "5.9.0",
37
- "@bamboocss/config": "^1.15.0",
38
- "@bamboocss/core": "^1.15.0",
39
- "@bamboocss/extractor": "1.15.0",
40
- "@bamboocss/shared": "1.15.0",
41
- "@bamboocss/logger": "1.15.0",
42
- "@bamboocss/types": "1.15.0"
37
+ "@bamboocss/config": "^1.16.1",
38
+ "@bamboocss/core": "^1.16.1",
39
+ "@bamboocss/extractor": "1.16.1",
40
+ "@bamboocss/logger": "1.16.1",
41
+ "@bamboocss/shared": "1.16.1",
42
+ "@bamboocss/types": "1.16.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@bamboocss/generator": "1.15.0",
46
- "@bamboocss/plugin-svelte": "1.15.0",
47
- "@bamboocss/plugin-vue": "1.15.0"
45
+ "@bamboocss/generator": "1.16.1",
46
+ "@bamboocss/plugin-svelte": "1.16.1",
47
+ "@bamboocss/plugin-vue": "1.16.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsdown src/index.ts --format=esm,cjs --dts",