@bettercms-ai/convert 0.2.0 → 0.3.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/cli.js CHANGED
@@ -2628,6 +2628,71 @@ function locate(brief, sources, options) {
2628
2628
  // src/loops.ts
2629
2629
  import MagicString from "magic-string";
2630
2630
 
2631
+ // src/declarations.ts
2632
+ var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
2633
+ var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
2634
+ var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
2635
+ var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
2636
+ var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
2637
+ const [address, kind] = entry.split("|");
2638
+ if (!address) return [];
2639
+ const scope = address.startsWith("layout:") ? "layout" : "page";
2640
+ return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
2641
+ });
2642
+ var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
2643
+ var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
2644
+ var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
2645
+ function declarationsIn(roots, content) {
2646
+ const out = [];
2647
+ const visit = (node) => {
2648
+ if (node.type === "expr") {
2649
+ for (const child of node.children) visit(child);
2650
+ return;
2651
+ }
2652
+ if (node.type !== "element") return;
2653
+ const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
2654
+ const at = node.attrInsertAt;
2655
+ for (const attr of node.attrs) {
2656
+ const name = attr.name.replace(/^:/, "");
2657
+ if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
2658
+ const scope = name === "data-bcms-layout-field" ? "layout" : "page";
2659
+ const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
2660
+ const templated = template?.[1] ?? template?.[2];
2661
+ if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
2662
+ else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
2663
+ } else if (attr.name === "data-bcms-props") {
2664
+ out.push(...propsDeclarations(attr.value, at));
2665
+ } else if (attr.name === "bcmsBindings") {
2666
+ for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
2667
+ out.push({ scope: "page", path, kind: "", at, via: "prop" });
2668
+ }
2669
+ }
2670
+ }
2671
+ for (const child of node.children) visit(child);
2672
+ };
2673
+ for (const root of roots) visit(root);
2674
+ return out;
2675
+ }
2676
+ var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
2677
+ var scopedPath = (scope, path) => `${scope}::${path}`;
2678
+ var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
2679
+ var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
2680
+ function declaredByScan(content) {
2681
+ const out = /* @__PURE__ */ new Set();
2682
+ const add = (scope, path) => void out.add(scopedPath(scope, path));
2683
+ for (const match of content.matchAll(BINDING_ATTR)) {
2684
+ add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
2685
+ }
2686
+ for (const match of content.matchAll(TEMPLATE_SCAN)) {
2687
+ add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
2688
+ }
2689
+ for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
2690
+ for (const match of content.matchAll(PROPS_ATTR))
2691
+ for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
2692
+ return out;
2693
+ }
2694
+ var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
2695
+
2631
2696
  // src/scope.ts
2632
2697
  function patternNames(node, out) {
2633
2698
  if (!node || typeof node !== "object") return;
@@ -2914,7 +2979,10 @@ function wordBounded(haystack, needle) {
2914
2979
  }
2915
2980
 
2916
2981
  // src/loops.ts
2917
- var shapeOf = (arrayPath, leaf) => `${arrayPath}[*].${leaf}`;
2982
+ function splitNested(leaf) {
2983
+ const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
2984
+ return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
2985
+ }
2918
2986
  var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
2919
2987
  function findRows(roots, members) {
2920
2988
  const wanted = new Set(members.map((m) => m.index));
@@ -2986,6 +3054,69 @@ function indentOf(content, at) {
2986
3054
  const prefix = content.slice(lineStart, at);
2987
3055
  return prefix.trim() === "" ? prefix : "";
2988
3056
  }
3057
+ function writeLeaf(source, shift, member, rule, read, row, literal) {
3058
+ const site = member.site;
3059
+ const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
3060
+ if (member.kind === "richtext" && rule.richtext) {
3061
+ source.remove(site.inner.start + shift, site.inner.end + shift);
3062
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
3063
+ } else {
3064
+ source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
3065
+ }
3066
+ const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
3067
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
3068
+ }
3069
+ function emitNested(rule, array, members, prefix, outer, parent, ctx) {
3070
+ if (!rule.loop || !rule.text) return null;
3071
+ const inner = [];
3072
+ for (const member of members) {
3073
+ const split = splitNested(member.leaf);
3074
+ if (!split || splitNested(split.leaf) !== null) return null;
3075
+ inner.push({ ...member, index: split.index, leaf: split.leaf });
3076
+ }
3077
+ const rows = findRows([parent.node], inner);
3078
+ if (!rows || rows.length === 0) return null;
3079
+ if (!consecutive(rows, ctx.content)) return null;
3080
+ if (!sameShape(rows, ctx.content)) return null;
3081
+ const template = rows[0];
3082
+ const used = /* @__PURE__ */ new Set([
3083
+ ...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
3084
+ outer.row,
3085
+ outer.index
3086
+ ]);
3087
+ const allocated = allocateNames(used, [
3088
+ { key: "row", name: "item" },
3089
+ { key: "index", name: "j" }
3090
+ ]);
3091
+ const names = { row: allocated.get("row"), index: allocated.get("index") };
3092
+ const fallback = [];
3093
+ const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
3094
+ if (!writable) return null;
3095
+ const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
3096
+ const shift = -template.range.start;
3097
+ const declarations = [];
3098
+ for (const member of template.members) {
3099
+ const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
3100
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3101
+ declarations.push({ path: shapeOfPath(literal), literal });
3102
+ }
3103
+ const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
3104
+ if (rule.loop.kind === "attr") {
3105
+ source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
3106
+ }
3107
+ if (rule.loopKey) {
3108
+ const key = rule.loopKey(names.index);
3109
+ if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
3110
+ else if (template.keyAttr.end > template.keyAttr.start)
3111
+ source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
3112
+ else return null;
3113
+ }
3114
+ const indent = indentOf(ctx.content, template.range.start);
3115
+ const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
3116
+ ${indented(source.toString(), indent)}
3117
+ ${indent}${rule.loop.close}` : source.toString();
3118
+ return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
3119
+ }
2989
3120
  function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
2990
3121
  const rule = DIALECT_RULES[dialect];
2991
3122
  if (!rule.loop || !rule.text || group.length === 0) return null;
@@ -3003,20 +3134,25 @@ function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
3003
3134
  const names = { row: allocated.get("row"), index: allocated.get("index") };
3004
3135
  const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
3005
3136
  const shift = -template.range.start;
3006
- const bindings = [];
3137
+ const declarations = [];
3138
+ const nested = /* @__PURE__ */ new Map();
3139
+ const direct = [];
3007
3140
  for (const member of template.members) {
3008
- const site = member.site;
3009
- const expression = `${ctx.read}(${names.row}, ${JSON.stringify(member.leaf)}, "")`;
3010
- if (member.kind === "richtext" && rule.richtext) {
3011
- source.remove(site.inner.start + shift, site.inner.end + shift);
3012
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
3013
- } else {
3014
- source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
3015
- }
3016
- const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
3017
- const path = `\`${arrayPath}[\${${names.index}}].${member.leaf}\``;
3018
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", path)}${kind}`);
3019
- bindings.push(shapeOf(arrayPath, member.leaf));
3141
+ const inner = splitNested(member.leaf);
3142
+ if (inner === null) direct.push(member);
3143
+ else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
3144
+ }
3145
+ for (const [array, members] of nested) {
3146
+ const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
3147
+ if (!plan2) return null;
3148
+ if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
3149
+ source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
3150
+ declarations.push(...plan2.declarations);
3151
+ }
3152
+ for (const member of direct) {
3153
+ const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
3154
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3155
+ declarations.push({ path: shapeOfPath(literal), literal });
3020
3156
  }
3021
3157
  const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
3022
3158
  const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
@@ -3037,7 +3173,8 @@ ${indent}${rule.loop.close}` : source.toString();
3037
3173
  return {
3038
3174
  range: { start: template.range.start, end: rows[rows.length - 1].range.end },
3039
3175
  text,
3040
- bindings: [...new Set(bindings)],
3176
+ declarations,
3177
+ bindings: [...new Set(declarations.map((d) => d.path))],
3041
3178
  names
3042
3179
  };
3043
3180
  }
@@ -3067,9 +3204,9 @@ function setLeaf(row, leaf, value) {
3067
3204
  var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3068
3205
  var emptyRow = () => /* @__PURE__ */ Object.create(null);
3069
3206
  function splitIndexed(arrayPath, path) {
3070
- const match = path.match(/^(.*)\[(\d+)\]\.(.+)$/);
3071
- if (!match || match[1] !== arrayPath) return null;
3072
- return { index: Number(match[2]), leaf: match[3] };
3207
+ if (!path.startsWith(`${arrayPath}[`)) return null;
3208
+ const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
3209
+ return match ? { index: Number(match[1]), leaf: match[2] } : null;
3073
3210
  }
3074
3211
 
3075
3212
  // src/sites/jsx.ts
@@ -3562,22 +3699,68 @@ var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${
3562
3699
  function aliasesFrom(sources) {
3563
3700
  const out = /* @__PURE__ */ new Map();
3564
3701
  for (const source of sources) {
3565
- if (!source.path.endsWith("tsconfig.json") && !source.path.endsWith("jsconfig.json")) continue;
3566
- try {
3567
- const parsed = JSON.parse(source.content.replace(/^\s*\/\/.*$/gm, ""));
3568
- const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3569
- const root = source.path.split("/").slice(0, -1).join("/");
3570
- for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3571
- out.set(
3572
- pattern,
3573
- targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3574
- );
3575
- }
3576
- } catch {
3702
+ if (!isProjectConfig(source.path)) continue;
3703
+ const parsed = readJsonc(source.content);
3704
+ if (parsed === null) continue;
3705
+ const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3706
+ const root = source.path.split("/").slice(0, -1).join("/");
3707
+ for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3708
+ out.set(
3709
+ pattern,
3710
+ targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3711
+ );
3577
3712
  }
3578
3713
  }
3579
3714
  return out;
3580
3715
  }
3716
+ var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
3717
+ function readJsonc(content) {
3718
+ let out = "";
3719
+ let at = 0;
3720
+ while (at < content.length) {
3721
+ const char = content[at];
3722
+ if (char === '"') {
3723
+ const start = at++;
3724
+ while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
3725
+ out += content.slice(start, Math.min(at + 1, content.length));
3726
+ at++;
3727
+ continue;
3728
+ }
3729
+ if (char === "/" && content[at + 1] === "/") {
3730
+ while (at < content.length && content[at] !== "\n") at++;
3731
+ continue;
3732
+ }
3733
+ if (char === "/" && content[at + 1] === "*") {
3734
+ const close = content.indexOf("*/", at + 2);
3735
+ at = close < 0 ? content.length : close + 2;
3736
+ continue;
3737
+ }
3738
+ out += char;
3739
+ at++;
3740
+ }
3741
+ try {
3742
+ const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
3743
+ return isAliasShaped(parsed) ? parsed : null;
3744
+ } catch {
3745
+ return null;
3746
+ }
3747
+ }
3748
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3749
+ function isAliasShaped(value) {
3750
+ if (!isRecord(value)) return false;
3751
+ const options = value.compilerOptions;
3752
+ if (options === void 0) return true;
3753
+ if (!isRecord(options)) return false;
3754
+ if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
3755
+ if (options.paths === void 0) return true;
3756
+ if (!isRecord(options.paths)) return false;
3757
+ return Object.values(options.paths).every(
3758
+ (targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
3759
+ );
3760
+ }
3761
+ function unreadableConfigs(sources) {
3762
+ return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
3763
+ }
3581
3764
  function expand(specifier, aliases) {
3582
3765
  const out = [];
3583
3766
  for (const [pattern, targets] of aliases) {
@@ -3746,70 +3929,6 @@ function typeMember(content, literal) {
3746
3929
  return { start: at, end: at, text: `${terminated ? "" : ";"} ${text}` };
3747
3930
  }
3748
3931
 
3749
- // src/declarations.ts
3750
- var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
3751
- var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
3752
- var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
3753
- var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
3754
- var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
3755
- const [address, kind] = entry.split("|");
3756
- if (!address) return [];
3757
- const scope = address.startsWith("layout:") ? "layout" : "page";
3758
- return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
3759
- });
3760
- var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
3761
- var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
3762
- var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
3763
- function declarationsIn(roots, content) {
3764
- const out = [];
3765
- const visit = (node) => {
3766
- if (node.type === "expr") {
3767
- for (const child of node.children) visit(child);
3768
- return;
3769
- }
3770
- if (node.type !== "element") return;
3771
- const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
3772
- const at = node.attrInsertAt;
3773
- for (const attr of node.attrs) {
3774
- const name = attr.name.replace(/^:/, "");
3775
- if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
3776
- const scope = name === "data-bcms-layout-field" ? "layout" : "page";
3777
- const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
3778
- const templated = template?.[1] ?? template?.[2];
3779
- if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
3780
- else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
3781
- } else if (attr.name === "data-bcms-props") {
3782
- out.push(...propsDeclarations(attr.value, at));
3783
- } else if (attr.name === "bcmsBindings") {
3784
- for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
3785
- out.push({ scope: "page", path, kind: "", at, via: "prop" });
3786
- }
3787
- }
3788
- }
3789
- for (const child of node.children) visit(child);
3790
- };
3791
- for (const root of roots) visit(root);
3792
- return out;
3793
- }
3794
- var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
3795
- var scopedPath = (scope, path) => `${scope}::${path}`;
3796
- var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
3797
- function declaredByScan(content) {
3798
- const out = /* @__PURE__ */ new Set();
3799
- const add = (scope, path) => void out.add(scopedPath(scope, path));
3800
- for (const match of content.matchAll(BINDING_ATTR)) {
3801
- add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
3802
- }
3803
- for (const match of content.matchAll(TEMPLATE_SCAN)) {
3804
- add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
3805
- }
3806
- for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
3807
- for (const match of content.matchAll(PROPS_ATTR))
3808
- for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
3809
- return out;
3810
- }
3811
- var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
3812
-
3813
3932
  // src/converted.ts
3814
3933
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3815
3934
  function splitArrayPath(path) {
@@ -4643,7 +4762,7 @@ var ValidationError = class extends Error {
4643
4762
  }
4644
4763
  code;
4645
4764
  };
4646
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4765
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4647
4766
  var Check = class {
4648
4767
  constructor(code) {
4649
4768
  this.code = code;
@@ -4653,7 +4772,7 @@ var Check = class {
4653
4772
  throw new ValidationError(this.code, `${where} ${why}.`);
4654
4773
  }
4655
4774
  record(value, where) {
4656
- if (!isRecord(value)) this.fail(where, "is not an object");
4775
+ if (!isRecord2(value)) this.fail(where, "is not an object");
4657
4776
  return value;
4658
4777
  }
4659
4778
  array(value, where) {
@@ -5101,10 +5220,10 @@ var SectionInvariantError = class extends Error {
5101
5220
  }
5102
5221
  };
5103
5222
  function assertSections(total, sections) {
5104
- const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.pending.length;
5223
+ const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.registered + sections.pending.length;
5105
5224
  if (sum !== total) {
5106
5225
  throw new SectionInvariantError(
5107
- `The receipt does not account for every section: ${total} planned but ${sections.extracted} extracted + ${sections.inlineOnly} inline-only + ${sections.alreadyExtracted} already extracted + ${sections.pending.length} pending = ${sum}.`
5226
+ `The receipt does not account for every section: ${total} planned but ${sections.extracted} extracted + ${sections.inlineOnly} inline-only + ${sections.alreadyExtracted} already extracted + ${sections.registered} registered + ${sections.pending.length} pending = ${sum}.`
5108
5227
  );
5109
5228
  }
5110
5229
  return sections;
@@ -5114,16 +5233,28 @@ var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
5114
5233
  var COMPONENT_DIR = "src/components/bcms";
5115
5234
  var SECTIONS_LIB = "src/lib/bcms-sections.ts";
5116
5235
  var EXTRACTABLE = ["astro", "jsx"];
5236
+ var registryValue = (entry) => entry.member ? `${entry.name}.${entry.member}` : entry.name;
5237
+ function importLine(name, specifier, entry) {
5238
+ if (entry.kind === "namespace") return `import * as ${name} from "${specifier}";`;
5239
+ if (entry.kind === "named") {
5240
+ const exported = entry.imported ?? name;
5241
+ return `import { ${exported === name ? name : `${exported} as ${name}`} } from "${specifier}";`;
5242
+ }
5243
+ return `import ${name} from "${specifier}";`;
5244
+ }
5117
5245
  async function componentizeSources(briefIn, planIn, sources, options) {
5118
5246
  const pending = [];
5119
5247
  let extracted = 0;
5120
5248
  let inlineOnly = 0;
5121
5249
  let alreadyExtracted = 0;
5250
+ let registered = 0;
5122
5251
  let planPending = 0;
5123
5252
  let total = 0;
5124
5253
  const brief = readBrief(briefIn);
5125
5254
  const plan2 = readPlan(planIn);
5126
5255
  const contents = new Map(sources.map((s) => [s.path, s.content]));
5256
+ const fileSet = new Set(contents.keys());
5257
+ const aliases = aliasesFrom(sources);
5127
5258
  const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5128
5259
  const trees = /* @__PURE__ */ new Map();
5129
5260
  const treeOf = async (file) => {
@@ -5149,6 +5280,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5149
5280
  };
5150
5281
  const fresh = /* @__PURE__ */ new Set();
5151
5282
  const registryConflicts = [];
5283
+ const registryUnreadable = [];
5152
5284
  for (const dialect of EXTRACTABLE) {
5153
5285
  const path = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
5154
5286
  const existing = contents.get(path);
@@ -5157,7 +5289,11 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5157
5289
  registryConflicts.push(path);
5158
5290
  continue;
5159
5291
  }
5160
- const held = readRegistry(existing, dialect);
5292
+ const held = await readRegistry(existing, dialect, path, fileSet, aliases);
5293
+ if (held === null) {
5294
+ registryUnreadable.push(path);
5295
+ continue;
5296
+ }
5161
5297
  componentsOf(dialect);
5162
5298
  for (const [slug, component] of held.components) written.get(dialect).set(slug, component);
5163
5299
  const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
@@ -5167,6 +5303,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5167
5303
  const lib = contents.get(SECTIONS_LIB);
5168
5304
  if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
5169
5305
  const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
5306
+ const registryUnread = registryUnreadable.length > 0;
5170
5307
  for (const page of plan2.pages ?? []) {
5171
5308
  const sections2 = (page.sections ?? []).filter((section) => {
5172
5309
  if (section.pending) {
@@ -5177,6 +5314,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5177
5314
  });
5178
5315
  total += sections2.length;
5179
5316
  if (sections2.length === 0) continue;
5317
+ const pendingBefore = pending.length;
5180
5318
  const briefPage = brief.pages.find((p) => p.route === page.route);
5181
5319
  const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5182
5320
  if (helperBlocked) {
@@ -5191,6 +5329,17 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5191
5329
  }
5192
5330
  continue;
5193
5331
  }
5332
+ if (registryUnread) {
5333
+ for (const section of sections2) {
5334
+ pend(
5335
+ page.route,
5336
+ section.groupKey,
5337
+ "REGISTRY_UNREADABLE",
5338
+ `${registryUnreadable.join(", ")} carries our marker but its REGISTRY/SHIPPED tables cannot be read back \u2014 rebuilding it would drop the components it names.`
5339
+ );
5340
+ }
5341
+ continue;
5342
+ }
5194
5343
  const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5195
5344
  if (!snapshotUsable(contents.get(snapshotPath))) {
5196
5345
  for (const section of sections2) {
@@ -5226,37 +5375,115 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5226
5375
  }
5227
5376
  const live = found.placed;
5228
5377
  if (live.length === 0) continue;
5378
+ const content = contents.get(file);
5379
+ const dialect = dialectOf(file, content);
5380
+ if (!dialect || !EXTRACTABLE.includes(dialect)) {
5381
+ for (const placed of live) {
5382
+ pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5383
+ }
5384
+ continue;
5385
+ }
5386
+ const tree = await treeOf(file);
5387
+ if (!tree) {
5388
+ for (const placed of live) {
5389
+ pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5390
+ }
5391
+ continue;
5392
+ }
5393
+ const parsed = tree.parsed;
5394
+ const importedFiles = /* @__PURE__ */ new Map();
5395
+ for (const [local, from] of importsIn(parsed, dialect)) {
5396
+ const resolved = resolveSpecifier(file, from.specifier, fileSet, aliases);
5397
+ if (resolved === null) continue;
5398
+ importedFiles.set(resolved, [
5399
+ ...importedFiles.get(resolved) ?? [],
5400
+ { local, kind: from.kind, imported: from.imported }
5401
+ ]);
5402
+ }
5403
+ const bindingOf = (held) => {
5404
+ const bindings = importedFiles.get(held) ?? [];
5405
+ return bindings.find((binding) => calledIn(tree, binding)) ?? bindings[0];
5406
+ };
5229
5407
  const placedHere = [];
5408
+ const importedSections = [];
5230
5409
  for (const placed of live) {
5231
5410
  const elements = placed.byFile.get(file);
5411
+ const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
5232
5412
  if (elements && elements.length > 0) {
5413
+ if (alsoRendered.length > 0) {
5414
+ pend(
5415
+ page.route,
5416
+ placed.section.groupKey,
5417
+ "SECTION_ROOT_AMBIGUOUS",
5418
+ `rendered inline by ${file} AND by ${alsoRendered.join(", ")}, which it imports.`
5419
+ );
5420
+ continue;
5421
+ }
5233
5422
  placedHere.push({ section: placed.section, elements });
5234
5423
  continue;
5235
5424
  }
5425
+ if (alsoRendered.length > 1) {
5426
+ pend(
5427
+ page.route,
5428
+ placed.section.groupKey,
5429
+ "SECTION_ROOT_AMBIGUOUS",
5430
+ `rendered by ${alsoRendered.join(" and ")}, both imported by ${file}.`
5431
+ );
5432
+ continue;
5433
+ }
5434
+ const only = alsoRendered[0] ?? [...placed.byFile.keys()][0];
5435
+ const held = bindingOf(only);
5436
+ if (held !== void 0 && dialectOf(only, contents.get(only) ?? "") === dialect) {
5437
+ importedSections.push({ section: placed.section, path: only, call: held.local });
5438
+ continue;
5439
+ }
5236
5440
  pend(
5237
5441
  page.route,
5238
5442
  placed.section.groupKey,
5239
5443
  "SECTION_ROOT_AMBIGUOUS",
5240
- `rendered by ${[...placed.byFile.keys()].join(", ")}, not ${file}.`
5444
+ held === void 0 ? `rendered by ${[...placed.byFile.keys()].join(", ")}, which ${file} does not import.` : `rendered by ${only}, which is ${dialectOf(only, contents.get(only) ?? "") ?? "not a template"}, not ${dialect} like ${file}.`
5241
5445
  );
5242
5446
  }
5243
- if (placedHere.length === 0) continue;
5244
- const content = contents.get(file);
5245
- const dialect = dialectOf(file, content);
5246
- if (!dialect || !EXTRACTABLE.includes(dialect)) {
5247
- for (const placed of placedHere) {
5248
- pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5447
+ if (placedHere.length === 0 && importedSections.length === 0) continue;
5448
+ const extractable = componentsOf(dialect);
5449
+ const registeredHere = [];
5450
+ for (const held of importedSections) {
5451
+ const component = resolveComponent(plan2, held.section);
5452
+ const already = extractable.get(component.slug);
5453
+ if (already !== void 0 && already.path !== held.path) {
5454
+ pend(
5455
+ page.route,
5456
+ held.section.groupKey,
5457
+ "COMPONENT_CONFLICT",
5458
+ `the slug "${component.slug}" already names ${already.path}, not ${held.path}.`
5459
+ );
5460
+ continue;
5249
5461
  }
5250
- continue;
5251
- }
5252
- const tree = await treeOf(file);
5253
- if (!tree) {
5254
- for (const placed of placedHere) {
5255
- pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5462
+ const binding = bindingOf(held.path);
5463
+ const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
5464
+ if (binding.kind === "namespace" && member === void 0) {
5465
+ pend(
5466
+ page.route,
5467
+ held.section.groupKey,
5468
+ "SECTION_ROOT_AMBIGUOUS",
5469
+ `${held.path} is imported as a namespace and ${file} never calls one of its components, so nothing names the export that renders it.`
5470
+ );
5471
+ continue;
5256
5472
  }
5257
- continue;
5473
+ extractable.set(component.slug, {
5474
+ name: component.name,
5475
+ path: held.path,
5476
+ kind: binding.kind,
5477
+ imported: binding.imported,
5478
+ ...member ? { member } : {}
5479
+ });
5480
+ registered++;
5481
+ registeredHere.push({
5482
+ slug: component.slug,
5483
+ bind: held.section.groupKey,
5484
+ call: callSiteOf(tree, member ? `${binding.local}.${member}` : held.call)
5485
+ });
5258
5486
  }
5259
- const parsed = tree.parsed;
5260
5487
  const roots = [];
5261
5488
  for (const placed of placedHere) {
5262
5489
  const root = lowestCommonAncestor(placed.elements);
@@ -5280,8 +5507,6 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5280
5507
  pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5281
5508
  }
5282
5509
  const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5283
- if (candidates.length === 0) continue;
5284
- const extractable = componentsOf(dialect);
5285
5510
  const placeable = [];
5286
5511
  for (const resolved of candidates) {
5287
5512
  const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
@@ -5304,7 +5529,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5304
5529
  );
5305
5530
  continue;
5306
5531
  }
5307
- extractable.set(resolved.slug, { name: resolved.name, path });
5532
+ extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
5308
5533
  fresh.add(resolved.slug);
5309
5534
  addFile(outFiles, contents, {
5310
5535
  path,
@@ -5313,24 +5538,42 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5313
5538
  });
5314
5539
  placeable.push(resolved);
5315
5540
  }
5316
- if (placeable.length === 0) continue;
5317
- const layout = runOf(placeable, parsed.roots);
5541
+ if (placeable.length === 0 && registeredHere.length === 0) continue;
5542
+ const spans = [
5543
+ ...placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey, root: r.root })),
5544
+ ...registeredHere.map((r) => ({ slug: r.slug, bind: r.bind, root: r.call }))
5545
+ ].sort((a, b) => (a.root?.start ?? Number.MAX_SAFE_INTEGER) - (b.root?.start ?? Number.MAX_SAFE_INTEGER));
5546
+ const known = spans.filter((s) => s.root !== null);
5547
+ const layout = runOf(known, parsed.roots, content);
5318
5548
  if (layout === null) {
5319
5549
  for (const resolved of placeable) {
5320
5550
  pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5321
5551
  }
5322
5552
  continue;
5323
5553
  }
5554
+ const wholePage = registeredHere.length === 0 || known.length === spans.length && pending.length === pendingBefore;
5555
+ const useRun = layout.contiguous && wholePage;
5324
5556
  const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
5325
5557
  shipped.set(dialect, order);
5326
5558
  order.set(
5327
5559
  pageSlug,
5328
- placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey }))
5560
+ spans.map((s) => ({ slug: s.slug, bind: s.bind }))
5329
5561
  );
5330
- const rewritten = layout.contiguous ? splicePageRun(file, content, dialect, parsed, placeable, pageSlug) : splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable);
5331
- addFile(outFiles, contents, { path: file, operation: "modify", content: rewritten });
5332
- if (layout.contiguous) extracted += placeable.length;
5333
- else inlineOnly += placeable.length;
5562
+ if (useRun) {
5563
+ addFile(outFiles, contents, {
5564
+ path: file,
5565
+ operation: "modify",
5566
+ content: splicePageRun(file, content, dialect, parsed, known, pageSlug)
5567
+ });
5568
+ extracted += placeable.length;
5569
+ } else if (placeable.length > 0) {
5570
+ addFile(outFiles, contents, {
5571
+ path: file,
5572
+ operation: "modify",
5573
+ content: splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable)
5574
+ });
5575
+ inlineOnly += placeable.length;
5576
+ }
5334
5577
  }
5335
5578
  const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
5336
5579
  if (componentSlugs.length > 0) {
@@ -5352,7 +5595,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5352
5595
  emitHelper(outFiles, contents, helperVersion, options?.overwriteHelper);
5353
5596
  emitStubs(outFiles, contents, pageSlugs);
5354
5597
  }
5355
- const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, pending });
5598
+ const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, registered, pending });
5356
5599
  return {
5357
5600
  files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
5358
5601
  receipt: {
@@ -5360,7 +5603,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5360
5603
  sections,
5361
5604
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
5362
5605
  components: [...fresh].sort(),
5363
- planPending
5606
+ planPending,
5607
+ notes: unreadableConfigs(sources).map(
5608
+ (path) => ({
5609
+ code: "TSCONFIG_UNREADABLE",
5610
+ file: path,
5611
+ message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
5612
+ })
5613
+ )
5364
5614
  }
5365
5615
  };
5366
5616
  }
@@ -5373,29 +5623,138 @@ function alreadyOn(content, pageSlug, sections) {
5373
5623
  }
5374
5624
  return sections.map((section) => section.groupKey).filter((groupKey) => new RegExp(`bind=\\s*["']${groupKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content));
5375
5625
  }
5376
- function readRegistry(content, dialect) {
5626
+ async function readRegistry(content, dialect, path, files, aliases) {
5377
5627
  const components = /* @__PURE__ */ new Map();
5378
5628
  const shipped = /* @__PURE__ */ new Map();
5379
- const paths = /* @__PURE__ */ new Map();
5380
- for (const match of content.matchAll(/^import\s+(\w+)\s+from\s+"\.\/([^"]+)";$/gm)) {
5381
- const [, name, file] = match;
5382
- paths.set(name, `${COMPONENT_DIR}/${file}${dialect === "astro" ? "" : ".tsx"}`);
5383
- }
5384
- const registry = content.match(/const REGISTRY[^=]*=\s*\{([\s\S]*?)\n\};/);
5385
- for (const match of (registry?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\w+),$/gm)) {
5386
- const [, slug, name] = match;
5387
- const path = paths.get(name);
5388
- if (path) components.set(slug, { name, path });
5389
- }
5390
- const table = content.match(/const SHIPPED[^=]*=\s*\{([\s\S]*?)\n\};/);
5391
- for (const match of (table?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\[.*\]),$/gm)) {
5629
+ const parsed = await parseFile4(dialect, content);
5630
+ if (parsed.error) return null;
5631
+ const imports = /* @__PURE__ */ new Map();
5632
+ for (const [local, from] of importsIn(parsed, dialect)) {
5633
+ if (!from.specifier.startsWith(".")) continue;
5634
+ const resolved = resolveSpecifier(path, from.specifier, files, aliases);
5635
+ if (resolved !== null) {
5636
+ imports.set(local, { name: local, path: resolved, kind: from.kind, imported: from.imported });
5637
+ }
5638
+ }
5639
+ const registry = blockOf(content, "REGISTRY");
5640
+ const table = blockOf(content, "SHIPPED");
5641
+ if (registry === null || table === null) return null;
5642
+ const registryRows = propertiesOf(registry);
5643
+ const shippedRows = propertiesOf(table);
5644
+ if (registryRows === null || shippedRows === null) return null;
5645
+ for (const row of registryRows) {
5646
+ const named = row.value.match(/^([A-Za-z_$][\w$]*)(?:\.([A-Za-z_$][\w$]*))?$/);
5647
+ const entry = named ? imports.get(named[1]) : void 0;
5648
+ if (!named || !entry) return null;
5649
+ components.set(row.key, named[2] === void 0 ? entry : { ...entry, member: named[2] });
5650
+ }
5651
+ for (const row of shippedRows) {
5392
5652
  try {
5393
- shipped.set(match[1], JSON.parse(match[2]));
5653
+ const placements = JSON.parse(row.value);
5654
+ if (!Array.isArray(placements)) return null;
5655
+ shipped.set(row.key, placements);
5394
5656
  } catch {
5657
+ return null;
5395
5658
  }
5396
5659
  }
5397
5660
  return { components, shipped };
5398
5661
  }
5662
+ function blockOf(content, name) {
5663
+ const declaration2 = content.indexOf(`const ${name}`);
5664
+ if (declaration2 < 0) return null;
5665
+ const open = content.indexOf("{", declaration2);
5666
+ if (open < 0) return null;
5667
+ let depth = 0;
5668
+ let at = open;
5669
+ while (at < content.length) {
5670
+ const skip = skipTrivia(content, at);
5671
+ if (skip >= 0) {
5672
+ at = skip;
5673
+ continue;
5674
+ }
5675
+ if (content[at] === "{") depth++;
5676
+ else if (content[at] === "}" && --depth === 0) return content.slice(open + 1, at);
5677
+ at++;
5678
+ }
5679
+ return null;
5680
+ }
5681
+ function skipTrivia(source, at) {
5682
+ const char = source[at];
5683
+ if (char === '"' || char === "'" || char === "`") {
5684
+ let cursor = at + 1;
5685
+ while (cursor < source.length && source[cursor] !== char) cursor += source[cursor] === "\\" ? 2 : 1;
5686
+ return Math.min(cursor + 1, source.length);
5687
+ }
5688
+ if (char === "/" && source[at + 1] === "/") {
5689
+ const line = source.indexOf("\n", at);
5690
+ return line < 0 ? source.length : line;
5691
+ }
5692
+ if (char === "/" && source[at + 1] === "*") {
5693
+ const close = source.indexOf("*/", at + 2);
5694
+ return close < 0 ? source.length : close + 2;
5695
+ }
5696
+ return -1;
5697
+ }
5698
+ function withoutComments(text) {
5699
+ let out = "";
5700
+ let at = 0;
5701
+ while (at < text.length) {
5702
+ const skip = skipTrivia(text, at);
5703
+ if (skip < 0) {
5704
+ out += text[at];
5705
+ at++;
5706
+ continue;
5707
+ }
5708
+ out += text[at] === '"' || text[at] === "'" || text[at] === "`" ? text.slice(at, skip) : "";
5709
+ at = skip;
5710
+ }
5711
+ return out;
5712
+ }
5713
+ function propertiesOf(block) {
5714
+ const out = [];
5715
+ const push = (text) => {
5716
+ const held = text.trim();
5717
+ if (held === "") return true;
5718
+ let depth2 = 0;
5719
+ for (let at = 0; at < held.length; ) {
5720
+ const skip = skipTrivia(held, at);
5721
+ if (skip >= 0) {
5722
+ at = skip;
5723
+ continue;
5724
+ }
5725
+ const char = held[at];
5726
+ if ("{[(".includes(char)) depth2++;
5727
+ else if ("}])".includes(char)) depth2--;
5728
+ else if (char === ":" && depth2 === 0) {
5729
+ const key = withoutComments(held.slice(0, at)).trim().replace(/^["'`]|["'`]$/g, "");
5730
+ const value = withoutComments(held.slice(at + 1)).trim();
5731
+ if (key === "" || value === "") return false;
5732
+ out.push({ key, value });
5733
+ return true;
5734
+ }
5735
+ at++;
5736
+ }
5737
+ return false;
5738
+ };
5739
+ let depth = 0;
5740
+ let start = 0;
5741
+ for (let at = 0; at < block.length; ) {
5742
+ const skip = skipTrivia(block, at);
5743
+ if (skip >= 0) {
5744
+ at = skip;
5745
+ continue;
5746
+ }
5747
+ const char = block[at];
5748
+ if ("{[(".includes(char)) depth++;
5749
+ else if ("}])".includes(char)) depth--;
5750
+ else if (char === "," && depth === 0) {
5751
+ if (!push(block.slice(start, at))) return null;
5752
+ start = at + 1;
5753
+ }
5754
+ at++;
5755
+ }
5756
+ return push(block.slice(start)) ? out : null;
5757
+ }
5399
5758
  function snapshotUsable(existing) {
5400
5759
  if (existing === void 0) return true;
5401
5760
  try {
@@ -5480,7 +5839,22 @@ function lowestCommonAncestor(elements) {
5480
5839
  }
5481
5840
  return common[common.length - 1] ?? null;
5482
5841
  }
5483
- function runOf(placeable, roots) {
5842
+ var calledIn = (tree, binding) => [...tree.elements.values()].some(
5843
+ (placed) => binding.kind === "namespace" ? placed.node.tag.startsWith(`${binding.local}.`) : placed.node.tag === binding.local
5844
+ );
5845
+ function namespaceMember(tree, local) {
5846
+ const members = new Set(
5847
+ [...tree.elements.values()].map((placed) => placed.node.tag).filter((tag) => tag.startsWith(`${local}.`)).map((tag) => tag.slice(local.length + 1))
5848
+ );
5849
+ return members.size === 1 ? [...members][0] : void 0;
5850
+ }
5851
+ function callSiteOf(tree, local) {
5852
+ const calls = [...tree.elements.values()].map((placed) => placed.node).filter((node) => node.tag === local);
5853
+ const only = calls.length === 1 ? calls[0] : null;
5854
+ return only !== null && only.attrs.length === 0 ? only : null;
5855
+ }
5856
+ function runOf(placeable, roots, content) {
5857
+ if (placeable.length === 0) return null;
5484
5858
  const parents = placeable.map((r) => parentOf(r.root, roots));
5485
5859
  if (parents.some((p) => p === void 0)) return null;
5486
5860
  const [first, ...rest] = parents;
@@ -5490,9 +5864,14 @@ function runOf(placeable, roots) {
5490
5864
  );
5491
5865
  const indices = placeable.map((r) => siblings.indexOf(r.root)).sort((a, b) => a - b);
5492
5866
  if (indices.some((at) => at < 0)) return null;
5493
- const contiguous = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
5494
- return { contiguous };
5867
+ const consecutive2 = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
5868
+ const ordered = [...placeable].sort((a, b) => a.root.start - b.root.start);
5869
+ const clean = ordered.every(
5870
+ (span, at) => at === 0 || isTrivia(content.slice(ordered[at - 1].root.end, span.root.start))
5871
+ );
5872
+ return { contiguous: consecutive2 && clean };
5495
5873
  }
5874
+ var isTrivia = (between) => between.replace(/<!--[\s\S]*?-->/g, "").replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, "").trim() === "";
5496
5875
  function parentOf(target, roots) {
5497
5876
  if (roots.some((node) => node === target)) return null;
5498
5877
  const search = (nodes, parent) => {
@@ -5712,9 +6091,9 @@ function registryFile(dialect, path, written, shipped) {
5712
6091
  const entries = [...written].sort(([a], [b]) => a.localeCompare(b));
5713
6092
  const imports = entries.map(([, component]) => {
5714
6093
  const specifier = relativeImport(path, component.path);
5715
- return `import ${component.name} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
6094
+ return importLine(component.name, dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, ""), component);
5716
6095
  }).join("\n");
5717
- const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${component.name},`).join("\n");
6096
+ const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${registryValue(component)},`).join("\n");
5718
6097
  const shippedTable = [...shipped].sort(([a], [b]) => a.localeCompare(b)).map(([page, list]) => ` ${JSON.stringify(page)}: ${JSON.stringify(list)},`).join("\n");
5719
6098
  const lib = relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "");
5720
6099
  const registryType = dialect === "astro" ? "Record<string, unknown>" : "Record<string, (props: BcmsSectionProps) => JSX.Element | null>";
@@ -5822,17 +6201,24 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
5822
6201
  const snapshot = names.get("pageSnapshot");
5823
6202
  const source = new MagicString3(content);
5824
6203
  for (const resolved of placeable) {
6204
+ const component = written.get(resolved.slug);
6205
+ const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
5825
6206
  source.overwrite(
5826
6207
  resolved.root.start,
5827
6208
  resolved.root.end,
5828
- `<${names.get(resolved.slug)} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
6209
+ `<${tag} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
5829
6210
  );
5830
6211
  }
5831
6212
  const lib = relativeImport(file, SECTIONS_LIB).replace(/\.tsx?$/, "");
5832
6213
  const imports = [
5833
6214
  ...wanted.map(({ key }) => {
5834
- const specifier = relativeImport(file, written.get(key).path);
5835
- return `import ${names.get(key)} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
6215
+ const component = written.get(key);
6216
+ const specifier = relativeImport(file, component.path);
6217
+ return importLine(
6218
+ names.get(key),
6219
+ dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, ""),
6220
+ component
6221
+ );
5836
6222
  }),
5837
6223
  `import { pageSnapshot${snapshot === "pageSnapshot" ? "" : ` as ${snapshot}`} } from "${lib}";`
5838
6224
  ];
@@ -6005,28 +6391,78 @@ async function convertSources(briefIn, sources, options) {
6005
6391
  );
6006
6392
  }
6007
6393
  let declaredOnly = 0;
6394
+ const declaredRows = [];
6395
+ const briefRoutes = new Set(brief.pages.map((page) => page.route));
6396
+ const declaredRow = (target, readsFromCms, file) => {
6397
+ declaredRows.push({
6398
+ route: target.route,
6399
+ scope: target.scope,
6400
+ path: target.path,
6401
+ kind: target.kind,
6402
+ ...file ? { file } : {},
6403
+ readsFromCms
6404
+ });
6405
+ };
6008
6406
  const witnessed = async (target) => {
6009
6407
  for (const source of sources) {
6010
6408
  const parsed = await parseOf(source.path);
6011
6409
  const dialect = dialectOf(source.path, source.content);
6012
6410
  if (!parsed || !dialect) continue;
6013
- if (convertedHere(source.content, parsed, dialect, target, source.path)) return true;
6411
+ if (convertedHere(source.content, parsed, dialect, target, source.path) && attributable(target, source.path)) {
6412
+ return source.path;
6413
+ }
6014
6414
  }
6015
- return false;
6415
+ return null;
6016
6416
  };
6017
6417
  const locatedByKey = /* @__PURE__ */ new Map();
6018
6418
  for (const hit of located) {
6019
6419
  const key = keyOf(hit.route, hit.scope ?? "page", hit.path);
6020
6420
  if (!locatedByKey.has(key)) locatedByKey.set(key, hit);
6021
6421
  }
6422
+ const sharingPath = /* @__PURE__ */ new Map();
6423
+ for (const [key, hit] of locatedByKey) {
6424
+ const held = targets.get(key);
6425
+ if (!held) continue;
6426
+ const id = scopedPath(held.scope, held.path);
6427
+ sharingPath.set(id, [...sharingPath.get(id) ?? [], { key, files: hit.files }]);
6428
+ }
6429
+ const claimedByAnother = (target, file) => (sharingPath.get(scopedPath(target.scope, target.path)) ?? []).some(
6430
+ (other) => other.key !== target.key && other.files.includes(file)
6431
+ );
6432
+ const attributable = (target, file, claimed) => {
6433
+ if (target.scope === "layout") return true;
6434
+ const serves = routeOfFile(file);
6435
+ if (serves !== null && serves !== target.route && briefRoutes.has(serves)) return false;
6436
+ if (serves === target.route && pageFilesFor(target.route, contents.keys()).length !== 1) return false;
6437
+ return claimed !== true;
6438
+ };
6439
+ const declaredByHand = (target, within) => {
6440
+ const candidates = within ?? (target.scope === "layout" ? sources.map((s) => s.path) : pageFilesFor(target.route, contents.keys()));
6441
+ for (const file of candidates) {
6442
+ if (!covers(declarations.get(file), target)) continue;
6443
+ if (attributable(target, file, within !== void 0 && claimedByAnother(target, file))) return file;
6444
+ }
6445
+ return null;
6446
+ };
6022
6447
  const missed = /* @__PURE__ */ new Set();
6023
6448
  for (const miss of unlocated) {
6024
6449
  const key = keyOf(miss.route, miss.scope ?? "page", miss.path);
6025
6450
  const target = targets.get(key);
6026
6451
  if (!target || locatedByKey.has(key) || missed.has(key)) continue;
6027
6452
  missed.add(key);
6028
- if (await witnessed(target)) declaredOnly++;
6029
- else pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6453
+ const witness = await witnessed(target);
6454
+ if (witness !== null) {
6455
+ declaredOnly++;
6456
+ declaredRow(target, true, witness);
6457
+ continue;
6458
+ }
6459
+ const byHand = declaredByHand(target);
6460
+ if (byHand !== null) {
6461
+ declaredOnly++;
6462
+ declaredRow(target, false, byHand);
6463
+ continue;
6464
+ }
6465
+ pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6030
6466
  }
6031
6467
  const byFile = /* @__PURE__ */ new Map();
6032
6468
  for (const [key, hit] of locatedByKey) {
@@ -6084,8 +6520,21 @@ async function convertSources(briefIn, sources, options) {
6084
6520
  if (worst) {
6085
6521
  const file = hit.files[outcomes.indexOf(worst)];
6086
6522
  pend(target, worst, file);
6087
- } else if (outcomes.every((o) => o === "declared")) alreadyDeclared++;
6088
- else rewritten++;
6523
+ } else if (outcomes.every((o) => o === "declared")) {
6524
+ const witness = await witnessed(target);
6525
+ const byHand = witness === null ? declaredByHand(target, hit.files) : null;
6526
+ if (witness === null && byHand === null) {
6527
+ pend(
6528
+ target,
6529
+ "AMBIGUOUS_LITERAL",
6530
+ hit.files[0],
6531
+ `${hit.files.join(", ")} already declares ${target.path}, and nothing there says it is ${target.route}'s.`
6532
+ );
6533
+ continue;
6534
+ }
6535
+ alreadyDeclared++;
6536
+ declaredRow(target, witness !== null, witness ?? byHand);
6537
+ } else rewritten++;
6089
6538
  }
6090
6539
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
6091
6540
  const source = new MagicString4(contents.get(file));
@@ -6111,12 +6560,27 @@ async function convertSources(briefIn, sources, options) {
6111
6560
  emitStubs2(dialects, snapshotSlugs, contents, outFiles);
6112
6561
  const receipt = assertReceipt({
6113
6562
  briefDigest: digest,
6114
- paths: { declared: targets.size, rewritten, alreadyDeclared, pending },
6563
+ paths: {
6564
+ declared: targets.size,
6565
+ rewritten,
6566
+ alreadyDeclared,
6567
+ declarations: declaredRows.sort(
6568
+ (a, b) => a.route.localeCompare(b.route) || a.scope.localeCompare(b.scope) || a.path.localeCompare(b.path)
6569
+ ),
6570
+ pending
6571
+ },
6115
6572
  occurrences: { located: occurrencesLocated, rewritten: occurrencesRewritten },
6116
6573
  files: receiptFiles,
6117
6574
  helpers,
6118
6575
  helperUpgraded,
6119
- dynamicBindings
6576
+ dynamicBindings,
6577
+ notes: unreadableConfigs(sources).map(
6578
+ (file) => ({
6579
+ code: "TSCONFIG_UNREADABLE",
6580
+ file,
6581
+ message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
6582
+ })
6583
+ )
6120
6584
  });
6121
6585
  return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
6122
6586
  }
@@ -6384,14 +6848,12 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
6384
6848
  looped.add(member.key);
6385
6849
  outcomes.set(member.key, "rewritten");
6386
6850
  }
6387
- for (const leaf of [...new Set(members.map((m) => m.leaf))]) {
6851
+ for (const declaration2 of plan2.declarations) {
6388
6852
  dynamic.push({
6389
6853
  file,
6390
6854
  prop: null,
6391
- path: shapeOf(arrayPath, leaf),
6392
- // The loop's OWN index name — `i_1` when the row already bound `i`. Hardcoding `i` here
6393
- // described a binding the file does not contain, which is what the proposer looks for.
6394
- callSites: [{ file, literal: `${arrayPath}[\${${plan2.names.index}}].${leaf}` }]
6855
+ path: declaration2.path,
6856
+ callSites: [{ file, literal: declaration2.literal }]
6395
6857
  });
6396
6858
  }
6397
6859
  rewrites.push({
@@ -6748,14 +7210,14 @@ ${rewritten} of ${declared} paths bound (${alreadyDeclared} already declared, ${
6748
7210
  }
6749
7211
  }
6750
7212
  function reportSections(files, receipt, dryRun) {
6751
- const { extracted, inlineOnly, alreadyExtracted, pending } = receipt.sections;
7213
+ const { extracted, inlineOnly, alreadyExtracted, registered, pending } = receipt.sections;
6752
7214
  for (const file of files) {
6753
7215
  process.stdout.write(`${dryRun ? "would " : ""}${file.operation} ${file.path}
6754
7216
  `);
6755
7217
  }
6756
7218
  process.stdout.write(
6757
7219
  `
6758
- ${extracted} sections extracted (${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending)
7220
+ ${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending)
6759
7221
  `
6760
7222
  );
6761
7223
  for (const section of pending) {