@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/CHANGELOG.md +48 -0
- package/README.md +61 -6
- package/dist/cli.js +506 -52
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +72 -4
- package/dist/index.js +507 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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) => [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3294
|
-
|
|
3295
|
-
const within = match[0].lastIndexOf(
|
|
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
|
-
|
|
3299
|
-
|
|
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 (!
|
|
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
|
|
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)
|
|
3467
|
-
|
|
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(`${
|
|
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 =
|
|
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
|
|
4488
|
-
out.push({ scope: "page", path
|
|
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:
|
|
7883
|
-
(
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
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,
|