@bettercms-ai/convert 0.7.0 → 0.8.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
@@ -145,7 +145,7 @@ function propsAttribute(props) {
145
145
  var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
146
146
  var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
147
147
  var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
148
- var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
148
+ var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{(?:`[^`]*`|[^}])*\}\}/g;
149
149
  var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
150
150
  const [address, kind] = entry.split("|");
151
151
  if (!address) return [];
@@ -186,7 +186,10 @@ function declarationsIn(roots, content) {
186
186
  for (const root of roots) visit(root);
187
187
  return out;
188
188
  }
189
- var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
189
+ var bindingsPaths = (text) => [
190
+ ...[...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
191
+ ...[...text.matchAll(/`([^`]+)`/g)].map((match) => shapeOfPath(match[1]))
192
+ ];
190
193
  var scopedPath = (scope, path) => `${scope}::${path}`;
191
194
  var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
192
195
  var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
@@ -3027,7 +3030,7 @@ function receptionIn(component, propName, offset, content) {
3027
3030
  let receiver = null;
3028
3031
  let rest = null;
3029
3032
  let hasBindings = false;
3030
- let destructured = false;
3033
+ let destructured2 = false;
3031
3034
  for (const node of component ? [component] : []) {
3032
3035
  const params = node.params ?? [];
3033
3036
  const first = params[0];
@@ -3053,7 +3056,7 @@ function receptionIn(component, propName, offset, content) {
3053
3056
  if (key !== propName) continue;
3054
3057
  const value = property.value;
3055
3058
  if (value?.type === "Identifier") names.add(String(value.name));
3056
- destructured = true;
3059
+ destructured2 = true;
3057
3060
  receivable = true;
3058
3061
  receive = patternClose(content, first, offset);
3059
3062
  }
@@ -3063,7 +3066,7 @@ function receptionIn(component, propName, offset, content) {
3063
3066
  viaProps = true;
3064
3067
  }
3065
3068
  }
3066
- return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
3069
+ return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
3067
3070
  }
3068
3071
  function astroReception(ast, propName, offset, content) {
3069
3072
  const names = /* @__PURE__ */ new Set();
@@ -3246,8 +3249,7 @@ function splitGroup(path) {
3246
3249
  return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
3247
3250
  }
3248
3251
  function moduleOf(parsed) {
3249
- if (!parsed.ast) return null;
3250
- return { ast: parsed.ast, offset: parsed.script?.offset ?? 0 };
3252
+ return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
3251
3253
  }
3252
3254
  function arrayLiterals(ast) {
3253
3255
  const out = /* @__PURE__ */ new Map();
@@ -3260,6 +3262,19 @@ function arrayLiterals(ast) {
3260
3262
  });
3261
3263
  return out;
3262
3264
  }
3265
+ function exportedNames(ast) {
3266
+ const out = /* @__PURE__ */ new Set();
3267
+ walkAst(ast, (node) => {
3268
+ if (node.type !== "ExportNamedDeclaration") return;
3269
+ const declaration2 = node.declaration;
3270
+ if (declaration2?.type !== "VariableDeclaration") return;
3271
+ for (const declarator of declaration2.declarations ?? []) {
3272
+ const id = declarator.id;
3273
+ if (id?.type === "Identifier") out.add(String(id.name));
3274
+ }
3275
+ });
3276
+ return out;
3277
+ }
3263
3278
  function propertyFor(row, literal) {
3264
3279
  if (row?.type !== "ObjectExpression") return null;
3265
3280
  for (const property of row.properties ?? []) {
@@ -3284,21 +3299,36 @@ function iterations(roots, identifier2) {
3284
3299
  for (const root of roots) visit(root);
3285
3300
  return out;
3286
3301
  }
3287
- function callbackParams(expression, identifier2) {
3288
- const pattern = new RegExp(
3289
- `(?<![\\w$])${identifier2}(?![\\w$])\\s*\\.\\s*map\\s*\\(\\s*(?:\\(\\s*([A-Za-z_$][\\w$]*)\\s*(,\\s*([A-Za-z_$][\\w$]*)\\s*)?\\)|([A-Za-z_$][\\w$]*))\\s*=>`
3290
- );
3291
- const match = expression.value.match(pattern);
3302
+ var CALLBACK = String.raw`\s*\.\s*map\s*\(\s*(?:\(\s*(?:([A-Za-z_$][\w$]*)|(\[[^\]]*\]))\s*(,\s*([A-Za-z_$][\w$]*)\s*)?\)|([A-Za-z_$][\w$]*))\s*=>`;
3303
+ function paramsIn(text, pattern, base) {
3304
+ const match = text.match(pattern);
3292
3305
  if (!match || match.index === void 0) return null;
3293
- const row = match[1] ?? match[4];
3294
- if (match[4] !== void 0) return { row, index: null, addIndexAt: -1 };
3295
- const within = match[0].lastIndexOf(row);
3306
+ if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
3307
+ const bound = match[1] ?? match[2];
3308
+ const within = match[0].lastIndexOf(bound);
3296
3309
  return {
3297
- row,
3298
- index: match[3] ?? null,
3299
- addIndexAt: expression.range.start + match.index + within + row.length
3310
+ // One of the two, never both: a row is either named or taken apart. @see bindLists for the
3311
+ // second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
3312
+ row: match[1] ?? null,
3313
+ names: match[2] === void 0 ? null : destructured(match[2]),
3314
+ index: match[4] ?? null,
3315
+ addIndexAt: base + match.index + within + bound.length
3300
3316
  };
3301
3317
  }
3318
+ function callbackParams(expression, identifier2) {
3319
+ return paramsIn(
3320
+ expression.value,
3321
+ new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
3322
+ expression.range.start
3323
+ );
3324
+ }
3325
+ function mapParams(content, at, until) {
3326
+ return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
3327
+ }
3328
+ function destructured(pattern) {
3329
+ const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
3330
+ return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
3331
+ }
3302
3332
  function renders(content, node, row) {
3303
3333
  if (!node.inner) return null;
3304
3334
  const inner = content.slice(node.inner.start, node.inner.end).trim();
@@ -3386,11 +3416,11 @@ function bindScalar(ctx, member, literals, offset) {
3386
3416
  }
3387
3417
  return { splices, refusal: null };
3388
3418
  }
3389
- function bindDataLiterals(ctx, members) {
3390
- const out = { splices: [], usesRead: false, usesRows: false, slugs: [], bound: [], declarations: [], refused: [] };
3419
+ async function bindDataLiterals(ctx, members) {
3420
+ const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
3391
3421
  const rule = DIALECT_RULES[ctx.dialect];
3392
3422
  const module = moduleOf(ctx.parsed);
3393
- if (!module || !rule.text) return out;
3423
+ if (!rule.text) return out;
3394
3424
  const groups = /* @__PURE__ */ new Map();
3395
3425
  const scalars = [];
3396
3426
  for (const member of members) {
@@ -3403,6 +3433,7 @@ function bindDataLiterals(ctx, members) {
3403
3433
  const key = `${member.slug}\0${split.array}`;
3404
3434
  groups.set(key, [...groups.get(key) ?? [], held]);
3405
3435
  }
3436
+ const indexes = /* @__PURE__ */ new Set();
3406
3437
  const strings = stringLiterals(module.ast);
3407
3438
  for (const member of scalars) {
3408
3439
  const scalar = bindScalar(ctx, member, strings, module.offset);
@@ -3413,7 +3444,6 @@ function bindDataLiterals(ctx, members) {
3413
3444
  out.slugs.push(member.slug);
3414
3445
  out.bound.push(member.key);
3415
3446
  }
3416
- if (groups.size === 0) return out;
3417
3447
  const literals = arrayLiterals(module.ast);
3418
3448
  for (const all of groups.values()) {
3419
3449
  const array = all[0].array;
@@ -3454,23 +3484,36 @@ function bindDataLiterals(ctx, members) {
3454
3484
  }
3455
3485
  const loop = loops[0];
3456
3486
  const params = callbackParams(loop, identifier2);
3457
- if (!params) {
3487
+ if (!params || params.row === null) {
3458
3488
  refuse("IN_EXPRESSION");
3459
3489
  continue;
3460
3490
  }
3461
- const elements = leafElements(ctx.content, loop, params.row);
3491
+ const rowName = params.row;
3492
+ const elements = leafElements(ctx.content, loop, rowName);
3462
3493
  const placed = /* @__PURE__ */ new Map();
3494
+ const passed = [];
3463
3495
  let refusal = null;
3464
3496
  for (const property of keys.keys()) {
3465
3497
  const found = elements.get(property) ?? [];
3466
- if (found.length === 1) placed.set(property, found[0]);
3467
- else refusal = found.length > 1 ? "REPEATER_AMBIGUOUS" : leafRefusal(loop.value, params.row, property);
3498
+ if (found.length === 1) {
3499
+ placed.set(property, found[0]);
3500
+ continue;
3501
+ }
3502
+ if (found.length > 1) {
3503
+ refusal = "REPEATER_AMBIGUOUS";
3504
+ continue;
3505
+ }
3506
+ const calls = propCalls(ctx.content, loop, rowName, property);
3507
+ if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
3508
+ else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
3509
+ else refusal = leafRefusal(loop.value, rowName, property);
3468
3510
  }
3469
3511
  if (refusal !== null) {
3470
3512
  refuse(refusal);
3471
3513
  continue;
3472
3514
  }
3473
- const index = params.index ?? allocateIndex(loop.value);
3515
+ const index = params.index ?? allocateIndex(loop.value, indexes);
3516
+ indexes.add(index);
3474
3517
  const splices = [];
3475
3518
  if (rule.loopKey) {
3476
3519
  const rows = loop.children.filter((child) => child.type === "element");
@@ -3486,6 +3529,36 @@ function bindDataLiterals(ctx, members) {
3486
3529
  }
3487
3530
  splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
3488
3531
  }
3532
+ const drilled = [];
3533
+ const edits = [];
3534
+ const bindings = /* @__PURE__ */ new Map();
3535
+ for (const entry of passed) {
3536
+ const leaf = keys.get(entry.property);
3537
+ const literal = `${array}[\${${index}}].${leaf}`;
3538
+ const outcome = entry.call.attrs.some((a) => a.name === "bcmsBindings") ? { ok: false, reason: "PROP_TARGET_NOT_FOUND" } : await ctx.drill(entry.call.tag, entry.prop, group.find((member) => member.leaf === leaf).kind);
3539
+ if (!outcome.ok) {
3540
+ refusal = outcome.reason;
3541
+ break;
3542
+ }
3543
+ edits.push(...outcome.edits);
3544
+ bindings.set(entry.call.attrInsertAt, [
3545
+ ...bindings.get(entry.call.attrInsertAt) ?? [],
3546
+ `${entry.prop}: \`${literal}\``
3547
+ ]);
3548
+ drilled.push({
3549
+ file: outcome.edits[0].file,
3550
+ prop: entry.prop,
3551
+ path: shapeOfPath(literal),
3552
+ literal: `${entry.prop}: \`${literal}\``
3553
+ });
3554
+ }
3555
+ if (refusal !== null) {
3556
+ refuse(refusal);
3557
+ continue;
3558
+ }
3559
+ for (const [at2, entries] of bindings) {
3560
+ splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
3561
+ }
3489
3562
  for (const [property, element] of placed) {
3490
3563
  const leaf = keys.get(property);
3491
3564
  const literal = `${array}[\${${index}}].${leaf}`;
@@ -3495,7 +3568,7 @@ function bindDataLiterals(ctx, members) {
3495
3568
  ...kind ? [`data-bcms-kind="${kind}"`] : [],
3496
3569
  // A richtext row value is html, so it goes into the dialect's html sink rather than being
3497
3570
  // printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
3498
- ...kind === "richtext" && rule.richtext ? [rule.richtext(`${params.row}.${property}`)] : []
3571
+ ...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
3499
3572
  ];
3500
3573
  splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
3501
3574
  if (kind === "richtext" && rule.richtext && element.inner) {
@@ -3515,19 +3588,319 @@ function bindDataLiterals(ctx, members) {
3515
3588
  splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
3516
3589
  out.splices.push(...splices);
3517
3590
  out.usesRows = true;
3591
+ out.componentEdits.push(...edits);
3592
+ out.props.push(...drilled);
3518
3593
  out.slugs.push(...group.map((member) => member.slug));
3519
3594
  out.bound.push(...group.map((member) => member.key));
3520
3595
  }
3596
+ const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
3597
+ bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
3598
+ return out;
3599
+ }
3600
+ function literalTable(init) {
3601
+ const elements = (init.elements ?? []).filter((e) => e !== null);
3602
+ const first = elements[0];
3603
+ if (!first || elements.length !== (init.elements ?? []).length) return null;
3604
+ if (elements.some((element) => element.type !== first.type)) return null;
3605
+ if (first.type === "StringLiteral") {
3606
+ return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
3607
+ }
3608
+ if (first.type !== "ArrayExpression") return null;
3609
+ const rows = [];
3610
+ for (const element of elements) {
3611
+ const inner = (element.elements ?? []).filter((x) => x !== null);
3612
+ if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
3613
+ if (inner.length !== (element.elements ?? []).length) return null;
3614
+ rows.push(inner.map((x) => norm(String(x.value))));
3615
+ }
3616
+ if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3617
+ return { tuple: true, rows };
3618
+ }
3619
+ function openingBracket(text, closeAt) {
3620
+ let depth = 0;
3621
+ for (let at = closeAt; at >= 0; at--) {
3622
+ if (text[at] === "]") depth++;
3623
+ else if (text[at] === "[") {
3624
+ depth--;
3625
+ if (depth === 0) return at;
3626
+ }
3627
+ }
3628
+ return -1;
3629
+ }
3630
+ function arrayLiteral(source) {
3631
+ try {
3632
+ const ast = parseProgram(`(${source})`);
3633
+ const expression = ast.program.body[0]?.expression;
3634
+ if (!expression || expression.type !== "ArrayExpression") return null;
3635
+ return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
3636
+ } catch {
3637
+ return null;
3638
+ }
3639
+ }
3640
+ function inlineTables(ctx) {
3641
+ const out = /* @__PURE__ */ new Map();
3642
+ const visit = (node) => {
3643
+ if (node.type === "element") {
3644
+ if (!node.raw) for (const child of node.children) visit(child);
3645
+ return;
3646
+ }
3647
+ if (node.type !== "expr") return;
3648
+ for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
3649
+ const closeAt = match.index;
3650
+ const openAt = openingBracket(node.value, closeAt);
3651
+ if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
3652
+ const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
3653
+ const table = init ? literalTable(init) : null;
3654
+ if (!table) continue;
3655
+ const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
3656
+ out.set(at.start, {
3657
+ ...table,
3658
+ at,
3659
+ label: "an inline array literal",
3660
+ render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
3661
+ });
3662
+ }
3663
+ for (const child of node.children) visit(child);
3664
+ };
3665
+ for (const root of ctx.parsed.roots) visit(root);
3666
+ return [...out.values()];
3667
+ }
3668
+ function readerElements(content, expression, reads) {
3669
+ const wanted = new Set(reads);
3670
+ const out = /* @__PURE__ */ new Map();
3671
+ const visit = (node) => {
3672
+ if (node.type === "expr") {
3673
+ for (const child of node.children) visit(child);
3674
+ return;
3675
+ }
3676
+ if (node.type !== "element" || node.raw) return;
3677
+ const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
3678
+ const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
3679
+ if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
3680
+ for (const child of node.children) visit(child);
3681
+ };
3682
+ for (const child of expression.children) visit(child);
3683
+ return out;
3684
+ }
3685
+ function readerWraps(expression, reads) {
3686
+ const wanted = new Set(reads);
3687
+ const out = /* @__PURE__ */ new Map();
3688
+ const visit = (node, parent) => {
3689
+ if (node.type === "expr") {
3690
+ const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
3691
+ const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
3692
+ if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
3693
+ for (const child of node.children) visit(child, parent);
3694
+ return;
3695
+ }
3696
+ if (node.type !== "element" || node.raw) return;
3697
+ for (const child of node.children) visit(child, node);
3698
+ };
3699
+ for (const child of expression.children) visit(child, null);
3700
+ return out;
3701
+ }
3702
+ function pathTemplates(paths, index) {
3703
+ const out = [];
3704
+ for (let position = 0; position < paths[0].length; position++) {
3705
+ const split = paths.map((row) => splitGroup(row[position]));
3706
+ const first = split[0];
3707
+ if (!first) return null;
3708
+ if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
3709
+ return null;
3710
+ }
3711
+ out.push(`${first.array}[\${${index}}].${first.leaf}`);
3712
+ }
3713
+ return out;
3714
+ }
3715
+ function bindLists(ctx, members, module, out, indexes) {
3716
+ const rule = DIALECT_RULES[ctx.dialect];
3717
+ const escapes = exportedNames(module.ast);
3718
+ const candidates = [];
3719
+ for (const [name, init] of arrayLiterals(module.ast)) {
3720
+ const table = literalTable(init);
3721
+ if (!table) continue;
3722
+ candidates.push({
3723
+ ...table,
3724
+ at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
3725
+ label: `\`${name}\``,
3726
+ render: () => {
3727
+ const loops = iterations(ctx.parsed.roots, name);
3728
+ if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
3729
+ if (loops.length === 0) return null;
3730
+ const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
3731
+ if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
3732
+ return { loop: loops[0], params: callbackParams(loops[0], name) };
3733
+ }
3734
+ });
3735
+ }
3736
+ candidates.push(...inlineTables(ctx));
3737
+ const siblings = /* @__PURE__ */ new Map();
3738
+ const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
3739
+ for (const member of members) {
3740
+ siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
3741
+ }
3742
+ const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
3743
+ (leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
3744
+ ).length;
3745
+ const best = /* @__PURE__ */ new Map();
3746
+ const bestFit = (member) => {
3747
+ const key = groupOf2(member);
3748
+ if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
3749
+ return best.get(key);
3750
+ };
3751
+ const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
3752
+ const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
3753
+ for (const candidate of candidates) {
3754
+ const mine = members.filter((member) => claims(candidate, member));
3755
+ if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
3756
+ if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
3757
+ const refuse = (reason) => {
3758
+ for (const member of mine) out.refused.push({ key: member.key, reason });
3759
+ };
3760
+ const paths = candidate.rows.map((row) => row.map(() => ""));
3761
+ const kinds = candidate.rows.map((row) => row.map(() => ""));
3762
+ let clash = false;
3763
+ for (const member of mine) {
3764
+ const spots = candidate.rows.flatMap(
3765
+ (row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
3766
+ );
3767
+ if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
3768
+ else {
3769
+ paths[spots[0][0]][spots[0][1]] = member.path;
3770
+ kinds[spots[0][0]][spots[0][1]] = member.kind;
3771
+ }
3772
+ }
3773
+ if (clash || paths.some((row) => row.some((path) => path === ""))) {
3774
+ refuse("IN_EXPRESSION");
3775
+ continue;
3776
+ }
3777
+ const kindAt = kinds[0];
3778
+ if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
3779
+ refuse("IN_EXPRESSION");
3780
+ continue;
3781
+ }
3782
+ const rendered = candidate.render();
3783
+ if (rendered === null) continue;
3784
+ if ("refuse" in rendered) {
3785
+ refuse(rendered.refuse);
3786
+ continue;
3787
+ }
3788
+ const { loop, params } = rendered;
3789
+ const reads = !params ? null : candidate.tuple ? params.names ?? (params.row === null ? null : candidate.rows[0].map((_, p) => `${params.row}[${p}]`)) : params.row === null ? null : [params.row];
3790
+ if (!params || !reads || reads.length !== candidate.rows[0].length) {
3791
+ refuse("IN_EXPRESSION");
3792
+ continue;
3793
+ }
3794
+ const elements = readerElements(ctx.content, loop, reads);
3795
+ const wraps = readerWraps(loop, reads);
3796
+ const hosts = [];
3797
+ let refusal = null;
3798
+ for (const read of reads) {
3799
+ const found = elements.get(read) ?? [];
3800
+ const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
3801
+ if (found.length === 1) hosts.push({ element: found[0] });
3802
+ else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
3803
+ else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
3804
+ }
3805
+ if (refusal !== null) {
3806
+ refuse(refusal);
3807
+ continue;
3808
+ }
3809
+ const index = params.index ?? allocateIndex(loop.value, indexes);
3810
+ indexes.add(index);
3811
+ const splices = [];
3812
+ if (rule.loopKey) {
3813
+ const rows = loop.children.filter((child) => child.type === "element");
3814
+ const row = rows.length === 1 ? rows[0] : null;
3815
+ if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
3816
+ splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
3817
+ }
3818
+ }
3819
+ if (params.index === null) {
3820
+ if (params.addIndexAt < 0) {
3821
+ refuse("IN_EXPRESSION");
3822
+ continue;
3823
+ }
3824
+ splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
3825
+ }
3826
+ const templates = pathTemplates(paths, index);
3827
+ if (!templates) {
3828
+ refuse("IN_EXPRESSION");
3829
+ continue;
3830
+ }
3831
+ const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
3832
+ for (const [position, host] of hosts.entries()) {
3833
+ const kind = kindAt[position];
3834
+ const attributes = [
3835
+ rule.attr("data-bcms-field", `\`${templates[position]}\``),
3836
+ ...kind ? [`data-bcms-kind="${kind}"`] : [],
3837
+ // A richtext value is html, so it goes into the dialect's html sink rather than being
3838
+ // printed as characters — the same rule one level up. @see bindDataLiterals
3839
+ ...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
3840
+ ];
3841
+ if ("element" in host) {
3842
+ splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
3843
+ if (kind === "richtext" && rule.richtext && host.element.inner) {
3844
+ splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
3845
+ }
3846
+ } else {
3847
+ const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
3848
+ splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
3849
+ }
3850
+ out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
3851
+ }
3852
+ const args = [
3853
+ ctx.snapshotName(mine[0].slug),
3854
+ list,
3855
+ ctx.content.slice(candidate.at.start, candidate.at.end),
3856
+ ...ctx.slug ? [ctx.slug] : []
3857
+ ];
3858
+ splices.push({
3859
+ start: candidate.at.start,
3860
+ end: candidate.at.end,
3861
+ text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
3862
+ });
3863
+ out.splices.push(...splices);
3864
+ if (candidate.tuple) out.usesTuples = true;
3865
+ else out.usesList = true;
3866
+ out.slugs.push(...mine.map((member) => member.slug));
3867
+ out.bound.push(...mine.map((member) => member.key));
3868
+ out.notes.push(
3869
+ `${candidate.label} renders exactly the ${paths.length} row(s) this run wrote paths for: a row ADDED in the CMS does not appear, and a row DELETED falls back to the copy the template shipped with.`
3870
+ );
3871
+ }
3872
+ }
3873
+ function propCalls(content, expression, row, property) {
3874
+ const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
3875
+ const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
3876
+ const out = [];
3877
+ const visit = (node) => {
3878
+ if (node.type === "expr") {
3879
+ for (const child of node.children) visit(child);
3880
+ return;
3881
+ }
3882
+ if (node.type !== "element" || node.raw) return;
3883
+ if (node.tag !== "" && !isIntrinsic(node.tag)) {
3884
+ for (const attr of node.attrs) {
3885
+ if (!attr.expressionRange) continue;
3886
+ const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
3887
+ if (spread.test(text)) out.push({ call: node, prop: property });
3888
+ else if (named.test(text)) out.push({ call: node, prop: attr.name });
3889
+ }
3890
+ }
3891
+ for (const child of node.children) visit(child);
3892
+ };
3893
+ for (const child of expression.children) visit(child);
3521
3894
  return out;
3522
3895
  }
3523
3896
  function leafRefusal(loop, row, property) {
3524
3897
  const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
3525
3898
  return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
3526
3899
  }
3527
- function allocateIndex(loop) {
3900
+ function allocateIndex(loop, taken) {
3528
3901
  const used = new Set(loop.match(/[A-Za-z_$][\w$]*/g) ?? []);
3529
- if (!used.has("i")) return "i";
3530
- for (let n = 1; ; n++) if (!used.has(`i_${n}`)) return `i_${n}`;
3902
+ if (!used.has("i") && !taken.has("i")) return "i";
3903
+ for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
3531
3904
  }
3532
3905
 
3533
3906
  // src/resolve.ts
@@ -3609,6 +3982,13 @@ function isAliasShaped(value) {
3609
3982
  function unreadableConfigs(sources) {
3610
3983
  return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
3611
3984
  }
3985
+ function extendedConfigs(sources) {
3986
+ return sources.filter((source) => {
3987
+ if (!isProjectConfig(source.path)) return false;
3988
+ const parsed = readJsonc(source.content);
3989
+ return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
3990
+ }).map((source) => source.path);
3991
+ }
3612
3992
  function expand(specifier, aliases) {
3613
3993
  const out = [];
3614
3994
  for (const [pattern, targets] of aliases) {
@@ -3738,7 +4118,7 @@ function bindImages(ctx, targets) {
3738
4118
 
3739
4119
  // src/helper.ts
3740
4120
  import { createHash as createHash2 } from "crypto";
3741
- var HELPER_VERSION = 5;
4121
+ var HELPER_VERSION = 6;
3742
4122
  var HEADER = "// @bettercms-ai/convert helper v";
3743
4123
  var HELPER_PATH = Object.fromEntries(
3744
4124
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -3846,6 +4226,30 @@ export function bcmsRowsAs<T>(
3846
4226
  });
3847
4227
  }
3848
4228
 
4229
+ /**
4230
+ * A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
4231
+ *
4232
+ * \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
4233
+ * renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
4234
+ * an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
4235
+ * resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
4236
+ * editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
4237
+ * this out loud rather than leaving an editor to discover it.
4238
+ */
4239
+ export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
4240
+ const entries = isRecord(page) ? page.entries : undefined;
4241
+ const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4242
+ return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
4243
+ }
4244
+
4245
+ /**
4246
+ * The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
4247
+ * same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
4248
+ */
4249
+ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
4250
+ return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
4251
+ }
4252
+
3849
4253
  /** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
3850
4254
  export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
3851
4255
  const entries = isRecord(page) ? page.entries : undefined;
@@ -3937,19 +4341,22 @@ var HISTORICAL = {
3937
4341
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
3938
4342
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
3939
4343
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
3940
- 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
4344
+ 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4345
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
3941
4346
  },
3942
4347
  "src/lib/bcms-content.ts": {
3943
4348
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
3944
4349
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
3945
4350
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
3946
- 4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af"
4351
+ 4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4352
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
3947
4353
  },
3948
4354
  "composables/bcms-content.ts": {
3949
4355
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
3950
4356
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
3951
4357
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
3952
- 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
4358
+ 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4359
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
3953
4360
  }
3954
4361
  };
3955
4362
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -4484,8 +4891,8 @@ function declaringElements(roots, content) {
4484
4891
  const name = attr.name.replace(/^:/, "");
4485
4892
  if (name === "bcmsBindings") {
4486
4893
  const own = ownBytes(node, content);
4487
- for (const quoted of content.slice(attr.range.start, attr.range.end).matchAll(/["']([^"']+)["']/g)) {
4488
- out.push({ scope: "page", path: quoted[1], kind, own });
4894
+ for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
4895
+ out.push({ scope: "page", path, kind, own });
4489
4896
  }
4490
4897
  continue;
4491
4898
  }
@@ -4515,7 +4922,7 @@ function declaringElements(roots, content) {
4515
4922
  return out;
4516
4923
  }
4517
4924
  function helperLocals(parsed, dialect, file) {
4518
- const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [] };
4925
+ const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
4519
4926
  const helper = HELPER_PATH[dialect];
4520
4927
  if (!helper) return out;
4521
4928
  for (const [local, from] of importsIn(parsed, dialect)) {
@@ -4525,6 +4932,8 @@ function helperLocals(parsed, dialect, file) {
4525
4932
  if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
4526
4933
  if (from.imported === "bcmsImage") out.bcmsImage.push(local);
4527
4934
  if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
4935
+ if (from.imported === "bcmsList") out.bcmsList.push(local);
4936
+ if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
4528
4937
  }
4529
4938
  return out;
4530
4939
  }
@@ -4597,6 +5006,9 @@ function convertedHere(content, parsed, dialect, target, file) {
4597
5006
  keyed = reads(locals.bcmsRowsAs);
4598
5007
  rows = keyed || reads(locals.bcmsRows);
4599
5008
  }
5009
+ const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
5010
+ (name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
5011
+ );
4600
5012
  let cached = null;
4601
5013
  const moduleRead = () => {
4602
5014
  if (indexed || snapshot === null) return false;
@@ -4612,7 +5024,7 @@ function convertedHere(content, parsed, dialect, target, file) {
4612
5024
  return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
4613
5025
  }
4614
5026
  if (element.path !== shape) return false;
4615
- return rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
5027
+ return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
4616
5028
  });
4617
5029
  }
4618
5030
 
@@ -5702,6 +6114,7 @@ function isSourceCandidate(path) {
5702
6114
  if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
5703
6115
  return false;
5704
6116
  if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
6117
+ if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
5705
6118
  if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
5706
6119
  if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
5707
6120
  return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
@@ -7758,6 +8171,7 @@ async function convertSources(briefIn, sources, options) {
7758
8171
  const receiptFiles = [];
7759
8172
  const dialects = /* @__PURE__ */ new Set();
7760
8173
  const dynamicBindings = [];
8174
+ const laneNotes = [];
7761
8175
  const snapshotSlugs = /* @__PURE__ */ new Set();
7762
8176
  let occurrencesLocated = 0;
7763
8177
  let occurrencesRewritten = 0;
@@ -7783,6 +8197,7 @@ async function convertSources(briefIn, sources, options) {
7783
8197
  outFiles.push({ path: file, operation: "modify", content: result.content });
7784
8198
  receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
7785
8199
  dynamicBindings.push(...result.dynamic);
8200
+ laneNotes.push(...result.notes);
7786
8201
  for (const slug of result.snapshots) snapshotSlugs.add(slug);
7787
8202
  }
7788
8203
  }
@@ -7879,13 +8294,25 @@ async function convertSources(briefIn, sources, options) {
7879
8294
  helperUpgraded,
7880
8295
  dynamicBindings,
7881
8296
  canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
7882
- notes: unreadableConfigs(sources).map(
7883
- (file) => ({
7884
- code: "TSCONFIG_UNREADABLE",
7885
- file,
7886
- message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
7887
- })
7888
- )
8297
+ notes: [
8298
+ ...unreadableConfigs(sources).map(
8299
+ (file) => ({
8300
+ code: "TSCONFIG_UNREADABLE",
8301
+ file,
8302
+ message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
8303
+ })
8304
+ ),
8305
+ // An absent alias table looks exactly like a project that has none, and the two have
8306
+ // opposite consequences for every component reached through one. @see extendedConfigs
8307
+ ...extendedConfigs(sources).map(
8308
+ (file) => ({
8309
+ code: "TSCONFIG_EXTENDS_ONLY",
8310
+ file,
8311
+ message: `${file} declares no \`compilerOptions.paths\` of its own and extends another config, which this package does not follow \u2014 so an import through a path alias resolved to nothing and any prop drilled through one is PROP_TARGET_NOT_FOUND.`
8312
+ })
8313
+ ),
8314
+ ...laneNotes
8315
+ ]
7889
8316
  });
7890
8317
  return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
7891
8318
  }
@@ -7907,7 +8334,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7907
8334
  dialect: dialectOf(file, content),
7908
8335
  dynamic: [],
7909
8336
  componentEdits: [],
7910
- snapshots: []
8337
+ snapshots: [],
8338
+ notes: []
7911
8339
  };
7912
8340
  if (pendingTargets.length === 0) return empty;
7913
8341
  if (!dialect) {
@@ -7968,7 +8396,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7968
8396
  dialect: null,
7969
8397
  dynamic: [],
7970
8398
  componentEdits: [],
7971
- snapshots: []
8399
+ snapshots: [],
8400
+ notes: []
7972
8401
  };
7973
8402
  }
7974
8403
  if (rescued.reason === "unverifiable") {
@@ -8038,6 +8467,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8038
8467
  { key: "helper:bcmsLayout", name: "bcmsLayout" },
8039
8468
  { key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
8040
8469
  { key: "helper:bcmsImage", name: "bcmsImage" },
8470
+ { key: "helper:bcmsList", name: "bcmsList" },
8471
+ { key: "helper:bcmsTuples", name: "bcmsTuples" },
8041
8472
  // Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
8042
8473
  ...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
8043
8474
  ].filter((entry) => !preset.has(entry.key));
@@ -8229,15 +8660,23 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8229
8660
  const collided = new Set(overlapping(kept).map((r) => r.key));
8230
8661
  const applied = kept.filter((r) => !collided.has(r.key));
8231
8662
  for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
8232
- const data = bindDataLiterals(
8663
+ const data = await bindDataLiterals(
8233
8664
  {
8234
8665
  content,
8235
8666
  parsed,
8236
8667
  dialect: writing,
8237
8668
  rowsAs: helperName("bcmsRowsAs"),
8669
+ list: helperName("bcmsList"),
8670
+ tuples: helperName("bcmsTuples"),
8238
8671
  read: helperName("bcms"),
8239
8672
  snapshotName,
8240
- slug: params?.slug ?? null
8673
+ slug: params?.slug ?? null,
8674
+ // The row's leaf travels to another file exactly as a scalar prop does, and by the same
8675
+ // machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
8676
+ drill: async (component, prop, kind) => {
8677
+ const drilled = await drillProp(drill, file, component, prop, kind, 1);
8678
+ return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
8679
+ }
8241
8680
  },
8242
8681
  targets.filter((t) => {
8243
8682
  const outcome = outcomes.get(t.key);
@@ -8255,6 +8694,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8255
8694
  for (const declaration2 of data.declarations) {
8256
8695
  dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
8257
8696
  }
8697
+ componentEdits.push(...data.componentEdits);
8698
+ for (const drilledProp of data.props) {
8699
+ dynamic.push({
8700
+ file: drilledProp.file,
8701
+ prop: drilledProp.prop,
8702
+ path: drilledProp.path,
8703
+ callSites: [{ file, literal: drilledProp.literal }]
8704
+ });
8705
+ }
8258
8706
  const images = bindImages(
8259
8707
  { content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
8260
8708
  targets.filter((t) => {
@@ -8266,7 +8714,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8266
8714
  for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
8267
8715
  const lanes = [...data.splices, ...images.splices];
8268
8716
  if (applied.length === 0 && lanes.length === 0) {
8269
- return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
8717
+ return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
8270
8718
  }
8271
8719
  const readSlugs = [
8272
8720
  .../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
@@ -8293,6 +8741,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8293
8741
  ] : [],
8294
8742
  ...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
8295
8743
  ...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
8744
+ ...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
8745
+ // `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
8746
+ // page imports only the one it calls — an unused import is dead code in someone else's repo.
8747
+ ...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
8296
8748
  ...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
8297
8749
  ...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
8298
8750
  ].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
@@ -8307,7 +8759,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8307
8759
  dialect: writing,
8308
8760
  dynamic,
8309
8761
  componentEdits,
8310
- snapshots: rule.text ? readSlugs : []
8762
+ snapshots: rule.text ? readSlugs : [],
8763
+ // The lane reports one sentence per list it bound; the FILE is added here, where it is known.
8764
+ notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
8311
8765
  };
8312
8766
  }
8313
8767
  function mergeInto(content, site) {
@@ -8520,6 +8974,7 @@ export {
8520
8974
  declaringElements,
8521
8975
  dialectOf,
8522
8976
  exportedFunction,
8977
+ extendedConfigs,
8523
8978
  findSites,
8524
8979
  findSitesTolerant,
8525
8980
  flat,