@bettercms-ai/convert 0.2.1 → 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/index.js CHANGED
@@ -2622,6 +2622,75 @@ function locate(brief, sources, options) {
2622
2622
  // src/loops.ts
2623
2623
  import MagicString from "magic-string";
2624
2624
 
2625
+ // src/declarations.ts
2626
+ var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
2627
+ var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
2628
+ var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
2629
+ var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
2630
+ var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
2631
+ const [address, kind] = entry.split("|");
2632
+ if (!address) return [];
2633
+ const scope = address.startsWith("layout:") ? "layout" : "page";
2634
+ return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
2635
+ });
2636
+ var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
2637
+ var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
2638
+ var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
2639
+ function declarationsIn(roots, content) {
2640
+ const out = [];
2641
+ const visit = (node) => {
2642
+ if (node.type === "expr") {
2643
+ for (const child of node.children) visit(child);
2644
+ return;
2645
+ }
2646
+ if (node.type !== "element") return;
2647
+ const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
2648
+ const at = node.attrInsertAt;
2649
+ for (const attr of node.attrs) {
2650
+ const name = attr.name.replace(/^:/, "");
2651
+ if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
2652
+ const scope = name === "data-bcms-layout-field" ? "layout" : "page";
2653
+ const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
2654
+ const templated = template?.[1] ?? template?.[2];
2655
+ if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
2656
+ else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
2657
+ } else if (attr.name === "data-bcms-props") {
2658
+ out.push(...propsDeclarations(attr.value, at));
2659
+ } else if (attr.name === "bcmsBindings") {
2660
+ for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
2661
+ out.push({ scope: "page", path, kind: "", at, via: "prop" });
2662
+ }
2663
+ }
2664
+ }
2665
+ for (const child of node.children) visit(child);
2666
+ };
2667
+ for (const root of roots) visit(root);
2668
+ return out;
2669
+ }
2670
+ var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
2671
+ var scopedPath = (scope, path) => `${scope}::${path}`;
2672
+ var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
2673
+ var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
2674
+ var scannedDeclarations = (content) => [...declaredByScan(content)].map((entry) => {
2675
+ const [scope, ...rest] = entry.split("::");
2676
+ return { scope, path: rest.join("::") };
2677
+ });
2678
+ function declaredByScan(content) {
2679
+ const out = /* @__PURE__ */ new Set();
2680
+ const add = (scope, path) => void out.add(scopedPath(scope, path));
2681
+ for (const match of content.matchAll(BINDING_ATTR)) {
2682
+ add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
2683
+ }
2684
+ for (const match of content.matchAll(TEMPLATE_SCAN)) {
2685
+ add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
2686
+ }
2687
+ for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
2688
+ for (const match of content.matchAll(PROPS_ATTR))
2689
+ for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
2690
+ return out;
2691
+ }
2692
+ var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
2693
+
2625
2694
  // src/scope.ts
2626
2695
  function patternNames(node, out) {
2627
2696
  if (!node || typeof node !== "object") return;
@@ -2908,7 +2977,10 @@ function wordBounded(haystack, needle) {
2908
2977
  }
2909
2978
 
2910
2979
  // src/loops.ts
2911
- var shapeOf = (arrayPath, leaf) => `${arrayPath}[*].${leaf}`;
2980
+ function splitNested(leaf) {
2981
+ const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
2982
+ return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
2983
+ }
2912
2984
  var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
2913
2985
  function findRows(roots, members) {
2914
2986
  const wanted = new Set(members.map((m) => m.index));
@@ -2980,6 +3052,69 @@ function indentOf(content, at) {
2980
3052
  const prefix = content.slice(lineStart, at);
2981
3053
  return prefix.trim() === "" ? prefix : "";
2982
3054
  }
3055
+ function writeLeaf(source, shift, member, rule, read, row, literal) {
3056
+ const site = member.site;
3057
+ const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
3058
+ if (member.kind === "richtext" && rule.richtext) {
3059
+ source.remove(site.inner.start + shift, site.inner.end + shift);
3060
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
3061
+ } else {
3062
+ source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
3063
+ }
3064
+ const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
3065
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
3066
+ }
3067
+ function emitNested(rule, array, members, prefix, outer, parent, ctx) {
3068
+ if (!rule.loop || !rule.text) return null;
3069
+ const inner = [];
3070
+ for (const member of members) {
3071
+ const split = splitNested(member.leaf);
3072
+ if (!split || splitNested(split.leaf) !== null) return null;
3073
+ inner.push({ ...member, index: split.index, leaf: split.leaf });
3074
+ }
3075
+ const rows = findRows([parent.node], inner);
3076
+ if (!rows || rows.length === 0) return null;
3077
+ if (!consecutive(rows, ctx.content)) return null;
3078
+ if (!sameShape(rows, ctx.content)) return null;
3079
+ const template = rows[0];
3080
+ const used = /* @__PURE__ */ new Set([
3081
+ ...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
3082
+ outer.row,
3083
+ outer.index
3084
+ ]);
3085
+ const allocated = allocateNames(used, [
3086
+ { key: "row", name: "item" },
3087
+ { key: "index", name: "j" }
3088
+ ]);
3089
+ const names = { row: allocated.get("row"), index: allocated.get("index") };
3090
+ const fallback = [];
3091
+ const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
3092
+ if (!writable) return null;
3093
+ const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
3094
+ const shift = -template.range.start;
3095
+ const declarations = [];
3096
+ for (const member of template.members) {
3097
+ const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
3098
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3099
+ declarations.push({ path: shapeOfPath(literal), literal });
3100
+ }
3101
+ const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
3102
+ if (rule.loop.kind === "attr") {
3103
+ source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
3104
+ }
3105
+ if (rule.loopKey) {
3106
+ const key = rule.loopKey(names.index);
3107
+ if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
3108
+ else if (template.keyAttr.end > template.keyAttr.start)
3109
+ source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
3110
+ else return null;
3111
+ }
3112
+ const indent = indentOf(ctx.content, template.range.start);
3113
+ const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
3114
+ ${indented(source.toString(), indent)}
3115
+ ${indent}${rule.loop.close}` : source.toString();
3116
+ return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
3117
+ }
2983
3118
  function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
2984
3119
  const rule = DIALECT_RULES[dialect];
2985
3120
  if (!rule.loop || !rule.text || group.length === 0) return null;
@@ -2997,20 +3132,25 @@ function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
2997
3132
  const names = { row: allocated.get("row"), index: allocated.get("index") };
2998
3133
  const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
2999
3134
  const shift = -template.range.start;
3000
- const bindings = [];
3135
+ const declarations = [];
3136
+ const nested = /* @__PURE__ */ new Map();
3137
+ const direct = [];
3001
3138
  for (const member of template.members) {
3002
- const site = member.site;
3003
- const expression = `${ctx.read}(${names.row}, ${JSON.stringify(member.leaf)}, "")`;
3004
- if (member.kind === "richtext" && rule.richtext) {
3005
- source.remove(site.inner.start + shift, site.inner.end + shift);
3006
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
3007
- } else {
3008
- source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
3009
- }
3010
- const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
3011
- const path = `\`${arrayPath}[\${${names.index}}].${member.leaf}\``;
3012
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", path)}${kind}`);
3013
- bindings.push(shapeOf(arrayPath, member.leaf));
3139
+ const inner = splitNested(member.leaf);
3140
+ if (inner === null) direct.push(member);
3141
+ else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
3142
+ }
3143
+ for (const [array, members] of nested) {
3144
+ const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
3145
+ if (!plan2) return null;
3146
+ if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
3147
+ source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
3148
+ declarations.push(...plan2.declarations);
3149
+ }
3150
+ for (const member of direct) {
3151
+ const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
3152
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3153
+ declarations.push({ path: shapeOfPath(literal), literal });
3014
3154
  }
3015
3155
  const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
3016
3156
  const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
@@ -3031,7 +3171,8 @@ ${indent}${rule.loop.close}` : source.toString();
3031
3171
  return {
3032
3172
  range: { start: template.range.start, end: rows[rows.length - 1].range.end },
3033
3173
  text,
3034
- bindings: [...new Set(bindings)],
3174
+ declarations,
3175
+ bindings: [...new Set(declarations.map((d) => d.path))],
3035
3176
  names
3036
3177
  };
3037
3178
  }
@@ -3061,9 +3202,9 @@ function setLeaf(row, leaf, value) {
3061
3202
  var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3062
3203
  var emptyRow = () => /* @__PURE__ */ Object.create(null);
3063
3204
  function splitIndexed(arrayPath, path) {
3064
- const match = path.match(/^(.*)\[(\d+)\]\.(.+)$/);
3065
- if (!match || match[1] !== arrayPath) return null;
3066
- return { index: Number(match[2]), leaf: match[3] };
3205
+ if (!path.startsWith(`${arrayPath}[`)) return null;
3206
+ const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
3207
+ return match ? { index: Number(match[1]), leaf: match[2] } : null;
3067
3208
  }
3068
3209
 
3069
3210
  // src/sites/jsx.ts
@@ -3556,22 +3697,68 @@ var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${
3556
3697
  function aliasesFrom(sources) {
3557
3698
  const out = /* @__PURE__ */ new Map();
3558
3699
  for (const source of sources) {
3559
- if (!source.path.endsWith("tsconfig.json") && !source.path.endsWith("jsconfig.json")) continue;
3560
- try {
3561
- const parsed = JSON.parse(source.content.replace(/^\s*\/\/.*$/gm, ""));
3562
- const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3563
- const root = source.path.split("/").slice(0, -1).join("/");
3564
- for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3565
- out.set(
3566
- pattern,
3567
- targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3568
- );
3569
- }
3570
- } catch {
3700
+ if (!isProjectConfig(source.path)) continue;
3701
+ const parsed = readJsonc(source.content);
3702
+ if (parsed === null) continue;
3703
+ const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3704
+ const root = source.path.split("/").slice(0, -1).join("/");
3705
+ for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3706
+ out.set(
3707
+ pattern,
3708
+ targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3709
+ );
3571
3710
  }
3572
3711
  }
3573
3712
  return out;
3574
3713
  }
3714
+ var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
3715
+ function readJsonc(content) {
3716
+ let out = "";
3717
+ let at = 0;
3718
+ while (at < content.length) {
3719
+ const char = content[at];
3720
+ if (char === '"') {
3721
+ const start = at++;
3722
+ while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
3723
+ out += content.slice(start, Math.min(at + 1, content.length));
3724
+ at++;
3725
+ continue;
3726
+ }
3727
+ if (char === "/" && content[at + 1] === "/") {
3728
+ while (at < content.length && content[at] !== "\n") at++;
3729
+ continue;
3730
+ }
3731
+ if (char === "/" && content[at + 1] === "*") {
3732
+ const close = content.indexOf("*/", at + 2);
3733
+ at = close < 0 ? content.length : close + 2;
3734
+ continue;
3735
+ }
3736
+ out += char;
3737
+ at++;
3738
+ }
3739
+ try {
3740
+ const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
3741
+ return isAliasShaped(parsed) ? parsed : null;
3742
+ } catch {
3743
+ return null;
3744
+ }
3745
+ }
3746
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3747
+ function isAliasShaped(value) {
3748
+ if (!isRecord(value)) return false;
3749
+ const options = value.compilerOptions;
3750
+ if (options === void 0) return true;
3751
+ if (!isRecord(options)) return false;
3752
+ if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
3753
+ if (options.paths === void 0) return true;
3754
+ if (!isRecord(options.paths)) return false;
3755
+ return Object.values(options.paths).every(
3756
+ (targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
3757
+ );
3758
+ }
3759
+ function unreadableConfigs(sources) {
3760
+ return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
3761
+ }
3575
3762
  function expand(specifier, aliases) {
3576
3763
  const out = [];
3577
3764
  for (const [pattern, targets] of aliases) {
@@ -3740,74 +3927,6 @@ function typeMember(content, literal) {
3740
3927
  return { start: at, end: at, text: `${terminated ? "" : ";"} ${text}` };
3741
3928
  }
3742
3929
 
3743
- // src/declarations.ts
3744
- var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
3745
- var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
3746
- var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
3747
- var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
3748
- var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
3749
- const [address, kind] = entry.split("|");
3750
- if (!address) return [];
3751
- const scope = address.startsWith("layout:") ? "layout" : "page";
3752
- return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
3753
- });
3754
- var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
3755
- var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
3756
- var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
3757
- function declarationsIn(roots, content) {
3758
- const out = [];
3759
- const visit = (node) => {
3760
- if (node.type === "expr") {
3761
- for (const child of node.children) visit(child);
3762
- return;
3763
- }
3764
- if (node.type !== "element") return;
3765
- const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
3766
- const at = node.attrInsertAt;
3767
- for (const attr of node.attrs) {
3768
- const name = attr.name.replace(/^:/, "");
3769
- if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
3770
- const scope = name === "data-bcms-layout-field" ? "layout" : "page";
3771
- const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
3772
- const templated = template?.[1] ?? template?.[2];
3773
- if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
3774
- else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
3775
- } else if (attr.name === "data-bcms-props") {
3776
- out.push(...propsDeclarations(attr.value, at));
3777
- } else if (attr.name === "bcmsBindings") {
3778
- for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
3779
- out.push({ scope: "page", path, kind: "", at, via: "prop" });
3780
- }
3781
- }
3782
- }
3783
- for (const child of node.children) visit(child);
3784
- };
3785
- for (const root of roots) visit(root);
3786
- return out;
3787
- }
3788
- var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
3789
- var scopedPath = (scope, path) => `${scope}::${path}`;
3790
- var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
3791
- var scannedDeclarations = (content) => [...declaredByScan(content)].map((entry) => {
3792
- const [scope, ...rest] = entry.split("::");
3793
- return { scope, path: rest.join("::") };
3794
- });
3795
- function declaredByScan(content) {
3796
- const out = /* @__PURE__ */ new Set();
3797
- const add = (scope, path) => void out.add(scopedPath(scope, path));
3798
- for (const match of content.matchAll(BINDING_ATTR)) {
3799
- add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
3800
- }
3801
- for (const match of content.matchAll(TEMPLATE_SCAN)) {
3802
- add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
3803
- }
3804
- for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
3805
- for (const match of content.matchAll(PROPS_ATTR))
3806
- for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
3807
- return out;
3808
- }
3809
- var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
3810
-
3811
3930
  // src/converted.ts
3812
3931
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3813
3932
  function splitArrayPath(path) {
@@ -4653,7 +4772,7 @@ var ValidationError = class extends Error {
4653
4772
  }
4654
4773
  code;
4655
4774
  };
4656
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4775
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4657
4776
  var Check = class {
4658
4777
  constructor(code) {
4659
4778
  this.code = code;
@@ -4663,7 +4782,7 @@ var Check = class {
4663
4782
  throw new ValidationError(this.code, `${where} ${why}.`);
4664
4783
  }
4665
4784
  record(value, where) {
4666
- if (!isRecord(value)) this.fail(where, "is not an object");
4785
+ if (!isRecord2(value)) this.fail(where, "is not an object");
4667
4786
  return value;
4668
4787
  }
4669
4788
  array(value, where) {
@@ -5119,10 +5238,10 @@ var SectionInvariantError = class extends Error {
5119
5238
  }
5120
5239
  };
5121
5240
  function assertSections(total, sections) {
5122
- const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.pending.length;
5241
+ const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.registered + sections.pending.length;
5123
5242
  if (sum !== total) {
5124
5243
  throw new SectionInvariantError(
5125
- `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}.`
5244
+ `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}.`
5126
5245
  );
5127
5246
  }
5128
5247
  return sections;
@@ -5132,16 +5251,28 @@ var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
5132
5251
  var COMPONENT_DIR = "src/components/bcms";
5133
5252
  var SECTIONS_LIB = "src/lib/bcms-sections.ts";
5134
5253
  var EXTRACTABLE = ["astro", "jsx"];
5254
+ var registryValue = (entry) => entry.member ? `${entry.name}.${entry.member}` : entry.name;
5255
+ function importLine(name, specifier, entry) {
5256
+ if (entry.kind === "namespace") return `import * as ${name} from "${specifier}";`;
5257
+ if (entry.kind === "named") {
5258
+ const exported = entry.imported ?? name;
5259
+ return `import { ${exported === name ? name : `${exported} as ${name}`} } from "${specifier}";`;
5260
+ }
5261
+ return `import ${name} from "${specifier}";`;
5262
+ }
5135
5263
  async function componentizeSources(briefIn, planIn, sources, options) {
5136
5264
  const pending = [];
5137
5265
  let extracted = 0;
5138
5266
  let inlineOnly = 0;
5139
5267
  let alreadyExtracted = 0;
5268
+ let registered = 0;
5140
5269
  let planPending = 0;
5141
5270
  let total = 0;
5142
5271
  const brief = readBrief(briefIn);
5143
5272
  const plan2 = readPlan(planIn);
5144
5273
  const contents = new Map(sources.map((s) => [s.path, s.content]));
5274
+ const fileSet = new Set(contents.keys());
5275
+ const aliases = aliasesFrom(sources);
5145
5276
  const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5146
5277
  const trees = /* @__PURE__ */ new Map();
5147
5278
  const treeOf = async (file) => {
@@ -5167,6 +5298,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5167
5298
  };
5168
5299
  const fresh = /* @__PURE__ */ new Set();
5169
5300
  const registryConflicts = [];
5301
+ const registryUnreadable = [];
5170
5302
  for (const dialect of EXTRACTABLE) {
5171
5303
  const path = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
5172
5304
  const existing = contents.get(path);
@@ -5175,7 +5307,11 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5175
5307
  registryConflicts.push(path);
5176
5308
  continue;
5177
5309
  }
5178
- const held = readRegistry(existing, dialect);
5310
+ const held = await readRegistry(existing, dialect, path, fileSet, aliases);
5311
+ if (held === null) {
5312
+ registryUnreadable.push(path);
5313
+ continue;
5314
+ }
5179
5315
  componentsOf(dialect);
5180
5316
  for (const [slug, component] of held.components) written.get(dialect).set(slug, component);
5181
5317
  const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
@@ -5185,6 +5321,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5185
5321
  const lib = contents.get(SECTIONS_LIB);
5186
5322
  if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
5187
5323
  const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
5324
+ const registryUnread = registryUnreadable.length > 0;
5188
5325
  for (const page of plan2.pages ?? []) {
5189
5326
  const sections2 = (page.sections ?? []).filter((section) => {
5190
5327
  if (section.pending) {
@@ -5195,6 +5332,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5195
5332
  });
5196
5333
  total += sections2.length;
5197
5334
  if (sections2.length === 0) continue;
5335
+ const pendingBefore = pending.length;
5198
5336
  const briefPage = brief.pages.find((p) => p.route === page.route);
5199
5337
  const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5200
5338
  if (helperBlocked) {
@@ -5209,6 +5347,17 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5209
5347
  }
5210
5348
  continue;
5211
5349
  }
5350
+ if (registryUnread) {
5351
+ for (const section of sections2) {
5352
+ pend(
5353
+ page.route,
5354
+ section.groupKey,
5355
+ "REGISTRY_UNREADABLE",
5356
+ `${registryUnreadable.join(", ")} carries our marker but its REGISTRY/SHIPPED tables cannot be read back \u2014 rebuilding it would drop the components it names.`
5357
+ );
5358
+ }
5359
+ continue;
5360
+ }
5212
5361
  const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5213
5362
  if (!snapshotUsable(contents.get(snapshotPath))) {
5214
5363
  for (const section of sections2) {
@@ -5244,37 +5393,115 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5244
5393
  }
5245
5394
  const live = found.placed;
5246
5395
  if (live.length === 0) continue;
5396
+ const content = contents.get(file);
5397
+ const dialect = dialectOf(file, content);
5398
+ if (!dialect || !EXTRACTABLE.includes(dialect)) {
5399
+ for (const placed of live) {
5400
+ pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5401
+ }
5402
+ continue;
5403
+ }
5404
+ const tree = await treeOf(file);
5405
+ if (!tree) {
5406
+ for (const placed of live) {
5407
+ pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5408
+ }
5409
+ continue;
5410
+ }
5411
+ const parsed = tree.parsed;
5412
+ const importedFiles = /* @__PURE__ */ new Map();
5413
+ for (const [local, from] of importsIn(parsed, dialect)) {
5414
+ const resolved = resolveSpecifier(file, from.specifier, fileSet, aliases);
5415
+ if (resolved === null) continue;
5416
+ importedFiles.set(resolved, [
5417
+ ...importedFiles.get(resolved) ?? [],
5418
+ { local, kind: from.kind, imported: from.imported }
5419
+ ]);
5420
+ }
5421
+ const bindingOf = (held) => {
5422
+ const bindings = importedFiles.get(held) ?? [];
5423
+ return bindings.find((binding) => calledIn(tree, binding)) ?? bindings[0];
5424
+ };
5247
5425
  const placedHere = [];
5426
+ const importedSections = [];
5248
5427
  for (const placed of live) {
5249
5428
  const elements = placed.byFile.get(file);
5429
+ const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
5250
5430
  if (elements && elements.length > 0) {
5431
+ if (alsoRendered.length > 0) {
5432
+ pend(
5433
+ page.route,
5434
+ placed.section.groupKey,
5435
+ "SECTION_ROOT_AMBIGUOUS",
5436
+ `rendered inline by ${file} AND by ${alsoRendered.join(", ")}, which it imports.`
5437
+ );
5438
+ continue;
5439
+ }
5251
5440
  placedHere.push({ section: placed.section, elements });
5252
5441
  continue;
5253
5442
  }
5443
+ if (alsoRendered.length > 1) {
5444
+ pend(
5445
+ page.route,
5446
+ placed.section.groupKey,
5447
+ "SECTION_ROOT_AMBIGUOUS",
5448
+ `rendered by ${alsoRendered.join(" and ")}, both imported by ${file}.`
5449
+ );
5450
+ continue;
5451
+ }
5452
+ const only = alsoRendered[0] ?? [...placed.byFile.keys()][0];
5453
+ const held = bindingOf(only);
5454
+ if (held !== void 0 && dialectOf(only, contents.get(only) ?? "") === dialect) {
5455
+ importedSections.push({ section: placed.section, path: only, call: held.local });
5456
+ continue;
5457
+ }
5254
5458
  pend(
5255
5459
  page.route,
5256
5460
  placed.section.groupKey,
5257
5461
  "SECTION_ROOT_AMBIGUOUS",
5258
- `rendered by ${[...placed.byFile.keys()].join(", ")}, not ${file}.`
5462
+ 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}.`
5259
5463
  );
5260
5464
  }
5261
- if (placedHere.length === 0) continue;
5262
- const content = contents.get(file);
5263
- const dialect = dialectOf(file, content);
5264
- if (!dialect || !EXTRACTABLE.includes(dialect)) {
5265
- for (const placed of placedHere) {
5266
- pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5465
+ if (placedHere.length === 0 && importedSections.length === 0) continue;
5466
+ const extractable = componentsOf(dialect);
5467
+ const registeredHere = [];
5468
+ for (const held of importedSections) {
5469
+ const component = resolveComponent(plan2, held.section);
5470
+ const already = extractable.get(component.slug);
5471
+ if (already !== void 0 && already.path !== held.path) {
5472
+ pend(
5473
+ page.route,
5474
+ held.section.groupKey,
5475
+ "COMPONENT_CONFLICT",
5476
+ `the slug "${component.slug}" already names ${already.path}, not ${held.path}.`
5477
+ );
5478
+ continue;
5267
5479
  }
5268
- continue;
5269
- }
5270
- const tree = await treeOf(file);
5271
- if (!tree) {
5272
- for (const placed of placedHere) {
5273
- pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5480
+ const binding = bindingOf(held.path);
5481
+ const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
5482
+ if (binding.kind === "namespace" && member === void 0) {
5483
+ pend(
5484
+ page.route,
5485
+ held.section.groupKey,
5486
+ "SECTION_ROOT_AMBIGUOUS",
5487
+ `${held.path} is imported as a namespace and ${file} never calls one of its components, so nothing names the export that renders it.`
5488
+ );
5489
+ continue;
5274
5490
  }
5275
- continue;
5491
+ extractable.set(component.slug, {
5492
+ name: component.name,
5493
+ path: held.path,
5494
+ kind: binding.kind,
5495
+ imported: binding.imported,
5496
+ ...member ? { member } : {}
5497
+ });
5498
+ registered++;
5499
+ registeredHere.push({
5500
+ slug: component.slug,
5501
+ bind: held.section.groupKey,
5502
+ call: callSiteOf(tree, member ? `${binding.local}.${member}` : held.call)
5503
+ });
5276
5504
  }
5277
- const parsed = tree.parsed;
5278
5505
  const roots = [];
5279
5506
  for (const placed of placedHere) {
5280
5507
  const root = lowestCommonAncestor(placed.elements);
@@ -5298,8 +5525,6 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5298
5525
  pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5299
5526
  }
5300
5527
  const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5301
- if (candidates.length === 0) continue;
5302
- const extractable = componentsOf(dialect);
5303
5528
  const placeable = [];
5304
5529
  for (const resolved of candidates) {
5305
5530
  const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
@@ -5322,7 +5547,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5322
5547
  );
5323
5548
  continue;
5324
5549
  }
5325
- extractable.set(resolved.slug, { name: resolved.name, path });
5550
+ extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
5326
5551
  fresh.add(resolved.slug);
5327
5552
  addFile(outFiles, contents, {
5328
5553
  path,
@@ -5331,24 +5556,42 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5331
5556
  });
5332
5557
  placeable.push(resolved);
5333
5558
  }
5334
- if (placeable.length === 0) continue;
5335
- const layout = runOf(placeable, parsed.roots);
5559
+ if (placeable.length === 0 && registeredHere.length === 0) continue;
5560
+ const spans = [
5561
+ ...placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey, root: r.root })),
5562
+ ...registeredHere.map((r) => ({ slug: r.slug, bind: r.bind, root: r.call }))
5563
+ ].sort((a, b) => (a.root?.start ?? Number.MAX_SAFE_INTEGER) - (b.root?.start ?? Number.MAX_SAFE_INTEGER));
5564
+ const known = spans.filter((s) => s.root !== null);
5565
+ const layout = runOf(known, parsed.roots, content);
5336
5566
  if (layout === null) {
5337
5567
  for (const resolved of placeable) {
5338
5568
  pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5339
5569
  }
5340
5570
  continue;
5341
5571
  }
5572
+ const wholePage = registeredHere.length === 0 || known.length === spans.length && pending.length === pendingBefore;
5573
+ const useRun = layout.contiguous && wholePage;
5342
5574
  const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
5343
5575
  shipped.set(dialect, order);
5344
5576
  order.set(
5345
5577
  pageSlug,
5346
- placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey }))
5578
+ spans.map((s) => ({ slug: s.slug, bind: s.bind }))
5347
5579
  );
5348
- const rewritten = layout.contiguous ? splicePageRun(file, content, dialect, parsed, placeable, pageSlug) : splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable);
5349
- addFile(outFiles, contents, { path: file, operation: "modify", content: rewritten });
5350
- if (layout.contiguous) extracted += placeable.length;
5351
- else inlineOnly += placeable.length;
5580
+ if (useRun) {
5581
+ addFile(outFiles, contents, {
5582
+ path: file,
5583
+ operation: "modify",
5584
+ content: splicePageRun(file, content, dialect, parsed, known, pageSlug)
5585
+ });
5586
+ extracted += placeable.length;
5587
+ } else if (placeable.length > 0) {
5588
+ addFile(outFiles, contents, {
5589
+ path: file,
5590
+ operation: "modify",
5591
+ content: splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable)
5592
+ });
5593
+ inlineOnly += placeable.length;
5594
+ }
5352
5595
  }
5353
5596
  const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
5354
5597
  if (componentSlugs.length > 0) {
@@ -5370,7 +5613,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5370
5613
  emitHelper(outFiles, contents, helperVersion, options?.overwriteHelper);
5371
5614
  emitStubs(outFiles, contents, pageSlugs);
5372
5615
  }
5373
- const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, pending });
5616
+ const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, registered, pending });
5374
5617
  return {
5375
5618
  files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
5376
5619
  receipt: {
@@ -5378,7 +5621,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5378
5621
  sections,
5379
5622
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
5380
5623
  components: [...fresh].sort(),
5381
- planPending
5624
+ planPending,
5625
+ notes: unreadableConfigs(sources).map(
5626
+ (path) => ({
5627
+ code: "TSCONFIG_UNREADABLE",
5628
+ file: path,
5629
+ message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
5630
+ })
5631
+ )
5382
5632
  }
5383
5633
  };
5384
5634
  }
@@ -5391,29 +5641,138 @@ function alreadyOn(content, pageSlug, sections) {
5391
5641
  }
5392
5642
  return sections.map((section) => section.groupKey).filter((groupKey) => new RegExp(`bind=\\s*["']${groupKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content));
5393
5643
  }
5394
- function readRegistry(content, dialect) {
5644
+ async function readRegistry(content, dialect, path, files, aliases) {
5395
5645
  const components = /* @__PURE__ */ new Map();
5396
5646
  const shipped = /* @__PURE__ */ new Map();
5397
- const paths = /* @__PURE__ */ new Map();
5398
- for (const match of content.matchAll(/^import\s+(\w+)\s+from\s+"\.\/([^"]+)";$/gm)) {
5399
- const [, name, file] = match;
5400
- paths.set(name, `${COMPONENT_DIR}/${file}${dialect === "astro" ? "" : ".tsx"}`);
5401
- }
5402
- const registry = content.match(/const REGISTRY[^=]*=\s*\{([\s\S]*?)\n\};/);
5403
- for (const match of (registry?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\w+),$/gm)) {
5404
- const [, slug, name] = match;
5405
- const path = paths.get(name);
5406
- if (path) components.set(slug, { name, path });
5407
- }
5408
- const table = content.match(/const SHIPPED[^=]*=\s*\{([\s\S]*?)\n\};/);
5409
- for (const match of (table?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\[.*\]),$/gm)) {
5647
+ const parsed = await parseFile4(dialect, content);
5648
+ if (parsed.error) return null;
5649
+ const imports = /* @__PURE__ */ new Map();
5650
+ for (const [local, from] of importsIn(parsed, dialect)) {
5651
+ if (!from.specifier.startsWith(".")) continue;
5652
+ const resolved = resolveSpecifier(path, from.specifier, files, aliases);
5653
+ if (resolved !== null) {
5654
+ imports.set(local, { name: local, path: resolved, kind: from.kind, imported: from.imported });
5655
+ }
5656
+ }
5657
+ const registry = blockOf(content, "REGISTRY");
5658
+ const table = blockOf(content, "SHIPPED");
5659
+ if (registry === null || table === null) return null;
5660
+ const registryRows = propertiesOf(registry);
5661
+ const shippedRows = propertiesOf(table);
5662
+ if (registryRows === null || shippedRows === null) return null;
5663
+ for (const row of registryRows) {
5664
+ const named = row.value.match(/^([A-Za-z_$][\w$]*)(?:\.([A-Za-z_$][\w$]*))?$/);
5665
+ const entry = named ? imports.get(named[1]) : void 0;
5666
+ if (!named || !entry) return null;
5667
+ components.set(row.key, named[2] === void 0 ? entry : { ...entry, member: named[2] });
5668
+ }
5669
+ for (const row of shippedRows) {
5410
5670
  try {
5411
- shipped.set(match[1], JSON.parse(match[2]));
5671
+ const placements = JSON.parse(row.value);
5672
+ if (!Array.isArray(placements)) return null;
5673
+ shipped.set(row.key, placements);
5412
5674
  } catch {
5675
+ return null;
5413
5676
  }
5414
5677
  }
5415
5678
  return { components, shipped };
5416
5679
  }
5680
+ function blockOf(content, name) {
5681
+ const declaration2 = content.indexOf(`const ${name}`);
5682
+ if (declaration2 < 0) return null;
5683
+ const open = content.indexOf("{", declaration2);
5684
+ if (open < 0) return null;
5685
+ let depth = 0;
5686
+ let at = open;
5687
+ while (at < content.length) {
5688
+ const skip = skipTrivia(content, at);
5689
+ if (skip >= 0) {
5690
+ at = skip;
5691
+ continue;
5692
+ }
5693
+ if (content[at] === "{") depth++;
5694
+ else if (content[at] === "}" && --depth === 0) return content.slice(open + 1, at);
5695
+ at++;
5696
+ }
5697
+ return null;
5698
+ }
5699
+ function skipTrivia(source, at) {
5700
+ const char = source[at];
5701
+ if (char === '"' || char === "'" || char === "`") {
5702
+ let cursor = at + 1;
5703
+ while (cursor < source.length && source[cursor] !== char) cursor += source[cursor] === "\\" ? 2 : 1;
5704
+ return Math.min(cursor + 1, source.length);
5705
+ }
5706
+ if (char === "/" && source[at + 1] === "/") {
5707
+ const line = source.indexOf("\n", at);
5708
+ return line < 0 ? source.length : line;
5709
+ }
5710
+ if (char === "/" && source[at + 1] === "*") {
5711
+ const close = source.indexOf("*/", at + 2);
5712
+ return close < 0 ? source.length : close + 2;
5713
+ }
5714
+ return -1;
5715
+ }
5716
+ function withoutComments(text) {
5717
+ let out = "";
5718
+ let at = 0;
5719
+ while (at < text.length) {
5720
+ const skip = skipTrivia(text, at);
5721
+ if (skip < 0) {
5722
+ out += text[at];
5723
+ at++;
5724
+ continue;
5725
+ }
5726
+ out += text[at] === '"' || text[at] === "'" || text[at] === "`" ? text.slice(at, skip) : "";
5727
+ at = skip;
5728
+ }
5729
+ return out;
5730
+ }
5731
+ function propertiesOf(block) {
5732
+ const out = [];
5733
+ const push = (text) => {
5734
+ const held = text.trim();
5735
+ if (held === "") return true;
5736
+ let depth2 = 0;
5737
+ for (let at = 0; at < held.length; ) {
5738
+ const skip = skipTrivia(held, at);
5739
+ if (skip >= 0) {
5740
+ at = skip;
5741
+ continue;
5742
+ }
5743
+ const char = held[at];
5744
+ if ("{[(".includes(char)) depth2++;
5745
+ else if ("}])".includes(char)) depth2--;
5746
+ else if (char === ":" && depth2 === 0) {
5747
+ const key = withoutComments(held.slice(0, at)).trim().replace(/^["'`]|["'`]$/g, "");
5748
+ const value = withoutComments(held.slice(at + 1)).trim();
5749
+ if (key === "" || value === "") return false;
5750
+ out.push({ key, value });
5751
+ return true;
5752
+ }
5753
+ at++;
5754
+ }
5755
+ return false;
5756
+ };
5757
+ let depth = 0;
5758
+ let start = 0;
5759
+ for (let at = 0; at < block.length; ) {
5760
+ const skip = skipTrivia(block, at);
5761
+ if (skip >= 0) {
5762
+ at = skip;
5763
+ continue;
5764
+ }
5765
+ const char = block[at];
5766
+ if ("{[(".includes(char)) depth++;
5767
+ else if ("}])".includes(char)) depth--;
5768
+ else if (char === "," && depth === 0) {
5769
+ if (!push(block.slice(start, at))) return null;
5770
+ start = at + 1;
5771
+ }
5772
+ at++;
5773
+ }
5774
+ return push(block.slice(start)) ? out : null;
5775
+ }
5417
5776
  function snapshotUsable(existing) {
5418
5777
  if (existing === void 0) return true;
5419
5778
  try {
@@ -5498,7 +5857,22 @@ function lowestCommonAncestor(elements) {
5498
5857
  }
5499
5858
  return common[common.length - 1] ?? null;
5500
5859
  }
5501
- function runOf(placeable, roots) {
5860
+ var calledIn = (tree, binding) => [...tree.elements.values()].some(
5861
+ (placed) => binding.kind === "namespace" ? placed.node.tag.startsWith(`${binding.local}.`) : placed.node.tag === binding.local
5862
+ );
5863
+ function namespaceMember(tree, local) {
5864
+ const members = new Set(
5865
+ [...tree.elements.values()].map((placed) => placed.node.tag).filter((tag) => tag.startsWith(`${local}.`)).map((tag) => tag.slice(local.length + 1))
5866
+ );
5867
+ return members.size === 1 ? [...members][0] : void 0;
5868
+ }
5869
+ function callSiteOf(tree, local) {
5870
+ const calls = [...tree.elements.values()].map((placed) => placed.node).filter((node) => node.tag === local);
5871
+ const only = calls.length === 1 ? calls[0] : null;
5872
+ return only !== null && only.attrs.length === 0 ? only : null;
5873
+ }
5874
+ function runOf(placeable, roots, content) {
5875
+ if (placeable.length === 0) return null;
5502
5876
  const parents = placeable.map((r) => parentOf(r.root, roots));
5503
5877
  if (parents.some((p) => p === void 0)) return null;
5504
5878
  const [first, ...rest] = parents;
@@ -5508,9 +5882,14 @@ function runOf(placeable, roots) {
5508
5882
  );
5509
5883
  const indices = placeable.map((r) => siblings.indexOf(r.root)).sort((a, b) => a - b);
5510
5884
  if (indices.some((at) => at < 0)) return null;
5511
- const contiguous = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
5512
- return { contiguous };
5885
+ const consecutive2 = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
5886
+ const ordered = [...placeable].sort((a, b) => a.root.start - b.root.start);
5887
+ const clean = ordered.every(
5888
+ (span, at) => at === 0 || isTrivia(content.slice(ordered[at - 1].root.end, span.root.start))
5889
+ );
5890
+ return { contiguous: consecutive2 && clean };
5513
5891
  }
5892
+ var isTrivia = (between) => between.replace(/<!--[\s\S]*?-->/g, "").replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, "").trim() === "";
5514
5893
  function parentOf(target, roots) {
5515
5894
  if (roots.some((node) => node === target)) return null;
5516
5895
  const search = (nodes, parent) => {
@@ -5730,9 +6109,9 @@ function registryFile(dialect, path, written, shipped) {
5730
6109
  const entries = [...written].sort(([a], [b]) => a.localeCompare(b));
5731
6110
  const imports = entries.map(([, component]) => {
5732
6111
  const specifier = relativeImport(path, component.path);
5733
- return `import ${component.name} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
6112
+ return importLine(component.name, dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, ""), component);
5734
6113
  }).join("\n");
5735
- const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${component.name},`).join("\n");
6114
+ const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${registryValue(component)},`).join("\n");
5736
6115
  const shippedTable = [...shipped].sort(([a], [b]) => a.localeCompare(b)).map(([page, list]) => ` ${JSON.stringify(page)}: ${JSON.stringify(list)},`).join("\n");
5737
6116
  const lib = relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "");
5738
6117
  const registryType = dialect === "astro" ? "Record<string, unknown>" : "Record<string, (props: BcmsSectionProps) => JSX.Element | null>";
@@ -5840,17 +6219,24 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
5840
6219
  const snapshot = names.get("pageSnapshot");
5841
6220
  const source = new MagicString3(content);
5842
6221
  for (const resolved of placeable) {
6222
+ const component = written.get(resolved.slug);
6223
+ const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
5843
6224
  source.overwrite(
5844
6225
  resolved.root.start,
5845
6226
  resolved.root.end,
5846
- `<${names.get(resolved.slug)} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
6227
+ `<${tag} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
5847
6228
  );
5848
6229
  }
5849
6230
  const lib = relativeImport(file, SECTIONS_LIB).replace(/\.tsx?$/, "");
5850
6231
  const imports = [
5851
6232
  ...wanted.map(({ key }) => {
5852
- const specifier = relativeImport(file, written.get(key).path);
5853
- return `import ${names.get(key)} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
6233
+ const component = written.get(key);
6234
+ const specifier = relativeImport(file, component.path);
6235
+ return importLine(
6236
+ names.get(key),
6237
+ dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, ""),
6238
+ component
6239
+ );
5854
6240
  }),
5855
6241
  `import { pageSnapshot${snapshot === "pageSnapshot" ? "" : ` as ${snapshot}`} } from "${lib}";`
5856
6242
  ];
@@ -6023,28 +6409,78 @@ async function convertSources(briefIn, sources, options) {
6023
6409
  );
6024
6410
  }
6025
6411
  let declaredOnly = 0;
6412
+ const declaredRows = [];
6413
+ const briefRoutes = new Set(brief.pages.map((page) => page.route));
6414
+ const declaredRow = (target, readsFromCms, file) => {
6415
+ declaredRows.push({
6416
+ route: target.route,
6417
+ scope: target.scope,
6418
+ path: target.path,
6419
+ kind: target.kind,
6420
+ ...file ? { file } : {},
6421
+ readsFromCms
6422
+ });
6423
+ };
6026
6424
  const witnessed = async (target) => {
6027
6425
  for (const source of sources) {
6028
6426
  const parsed = await parseOf(source.path);
6029
6427
  const dialect = dialectOf(source.path, source.content);
6030
6428
  if (!parsed || !dialect) continue;
6031
- if (convertedHere(source.content, parsed, dialect, target, source.path)) return true;
6429
+ if (convertedHere(source.content, parsed, dialect, target, source.path) && attributable(target, source.path)) {
6430
+ return source.path;
6431
+ }
6032
6432
  }
6033
- return false;
6433
+ return null;
6034
6434
  };
6035
6435
  const locatedByKey = /* @__PURE__ */ new Map();
6036
6436
  for (const hit of located) {
6037
6437
  const key = keyOf(hit.route, hit.scope ?? "page", hit.path);
6038
6438
  if (!locatedByKey.has(key)) locatedByKey.set(key, hit);
6039
6439
  }
6440
+ const sharingPath = /* @__PURE__ */ new Map();
6441
+ for (const [key, hit] of locatedByKey) {
6442
+ const held = targets.get(key);
6443
+ if (!held) continue;
6444
+ const id = scopedPath(held.scope, held.path);
6445
+ sharingPath.set(id, [...sharingPath.get(id) ?? [], { key, files: hit.files }]);
6446
+ }
6447
+ const claimedByAnother = (target, file) => (sharingPath.get(scopedPath(target.scope, target.path)) ?? []).some(
6448
+ (other) => other.key !== target.key && other.files.includes(file)
6449
+ );
6450
+ const attributable = (target, file, claimed) => {
6451
+ if (target.scope === "layout") return true;
6452
+ const serves = routeOfFile(file);
6453
+ if (serves !== null && serves !== target.route && briefRoutes.has(serves)) return false;
6454
+ if (serves === target.route && pageFilesFor(target.route, contents.keys()).length !== 1) return false;
6455
+ return claimed !== true;
6456
+ };
6457
+ const declaredByHand = (target, within) => {
6458
+ const candidates = within ?? (target.scope === "layout" ? sources.map((s) => s.path) : pageFilesFor(target.route, contents.keys()));
6459
+ for (const file of candidates) {
6460
+ if (!covers(declarations.get(file), target)) continue;
6461
+ if (attributable(target, file, within !== void 0 && claimedByAnother(target, file))) return file;
6462
+ }
6463
+ return null;
6464
+ };
6040
6465
  const missed = /* @__PURE__ */ new Set();
6041
6466
  for (const miss of unlocated) {
6042
6467
  const key = keyOf(miss.route, miss.scope ?? "page", miss.path);
6043
6468
  const target = targets.get(key);
6044
6469
  if (!target || locatedByKey.has(key) || missed.has(key)) continue;
6045
6470
  missed.add(key);
6046
- if (await witnessed(target)) declaredOnly++;
6047
- else pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6471
+ const witness = await witnessed(target);
6472
+ if (witness !== null) {
6473
+ declaredOnly++;
6474
+ declaredRow(target, true, witness);
6475
+ continue;
6476
+ }
6477
+ const byHand = declaredByHand(target);
6478
+ if (byHand !== null) {
6479
+ declaredOnly++;
6480
+ declaredRow(target, false, byHand);
6481
+ continue;
6482
+ }
6483
+ pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6048
6484
  }
6049
6485
  const byFile = /* @__PURE__ */ new Map();
6050
6486
  for (const [key, hit] of locatedByKey) {
@@ -6102,8 +6538,21 @@ async function convertSources(briefIn, sources, options) {
6102
6538
  if (worst) {
6103
6539
  const file = hit.files[outcomes.indexOf(worst)];
6104
6540
  pend(target, worst, file);
6105
- } else if (outcomes.every((o) => o === "declared")) alreadyDeclared++;
6106
- else rewritten++;
6541
+ } else if (outcomes.every((o) => o === "declared")) {
6542
+ const witness = await witnessed(target);
6543
+ const byHand = witness === null ? declaredByHand(target, hit.files) : null;
6544
+ if (witness === null && byHand === null) {
6545
+ pend(
6546
+ target,
6547
+ "AMBIGUOUS_LITERAL",
6548
+ hit.files[0],
6549
+ `${hit.files.join(", ")} already declares ${target.path}, and nothing there says it is ${target.route}'s.`
6550
+ );
6551
+ continue;
6552
+ }
6553
+ alreadyDeclared++;
6554
+ declaredRow(target, witness !== null, witness ?? byHand);
6555
+ } else rewritten++;
6107
6556
  }
6108
6557
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
6109
6558
  const source = new MagicString4(contents.get(file));
@@ -6129,12 +6578,27 @@ async function convertSources(briefIn, sources, options) {
6129
6578
  emitStubs2(dialects, snapshotSlugs, contents, outFiles);
6130
6579
  const receipt = assertReceipt({
6131
6580
  briefDigest: digest,
6132
- paths: { declared: targets.size, rewritten, alreadyDeclared, pending },
6581
+ paths: {
6582
+ declared: targets.size,
6583
+ rewritten,
6584
+ alreadyDeclared,
6585
+ declarations: declaredRows.sort(
6586
+ (a, b) => a.route.localeCompare(b.route) || a.scope.localeCompare(b.scope) || a.path.localeCompare(b.path)
6587
+ ),
6588
+ pending
6589
+ },
6133
6590
  occurrences: { located: occurrencesLocated, rewritten: occurrencesRewritten },
6134
6591
  files: receiptFiles,
6135
6592
  helpers,
6136
6593
  helperUpgraded,
6137
- dynamicBindings
6594
+ dynamicBindings,
6595
+ notes: unreadableConfigs(sources).map(
6596
+ (file) => ({
6597
+ code: "TSCONFIG_UNREADABLE",
6598
+ file,
6599
+ message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
6600
+ })
6601
+ )
6138
6602
  });
6139
6603
  return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
6140
6604
  }
@@ -6402,14 +6866,12 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
6402
6866
  looped.add(member.key);
6403
6867
  outcomes.set(member.key, "rewritten");
6404
6868
  }
6405
- for (const leaf of [...new Set(members.map((m) => m.leaf))]) {
6869
+ for (const declaration2 of plan2.declarations) {
6406
6870
  dynamic.push({
6407
6871
  file,
6408
6872
  prop: null,
6409
- path: shapeOf(arrayPath, leaf),
6410
- // The loop's OWN index name — `i_1` when the row already bound `i`. Hardcoding `i` here
6411
- // described a binding the file does not contain, which is what the proposer looks for.
6412
- callSites: [{ file, literal: `${arrayPath}[\${${plan2.names.index}}].${leaf}` }]
6873
+ path: declaration2.path,
6874
+ callSites: [{ file, literal: declaration2.literal }]
6413
6875
  });
6414
6876
  }
6415
6877
  rewrites.push({
@@ -6695,6 +7157,7 @@ export {
6695
7157
  rewriteFile,
6696
7158
  routeOfFile,
6697
7159
  stripTags,
7160
+ unreadableConfigs,
6698
7161
  walkAst
6699
7162
  };
6700
7163
  //# sourceMappingURL=index.js.map