@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/cli.js
CHANGED
|
@@ -151,7 +151,7 @@ function propsAttribute(props) {
|
|
|
151
151
|
var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
|
|
152
152
|
var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
|
|
153
153
|
var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
|
|
154
|
-
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
|
|
154
|
+
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{(?:`[^`]*`|[^}])*\}\}/g;
|
|
155
155
|
var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
|
|
156
156
|
const [address, kind] = entry.split("|");
|
|
157
157
|
if (!address) return [];
|
|
@@ -192,7 +192,10 @@ function declarationsIn(roots, content) {
|
|
|
192
192
|
for (const root of roots) visit(root);
|
|
193
193
|
return out;
|
|
194
194
|
}
|
|
195
|
-
var bindingsPaths = (text) => [
|
|
195
|
+
var bindingsPaths = (text) => [
|
|
196
|
+
...[...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
|
|
197
|
+
...[...text.matchAll(/`([^`]+)`/g)].map((match) => shapeOfPath(match[1]))
|
|
198
|
+
];
|
|
196
199
|
var scopedPath = (scope, path) => `${scope}::${path}`;
|
|
197
200
|
var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
|
|
198
201
|
var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
|
|
@@ -3029,7 +3032,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3029
3032
|
let receiver = null;
|
|
3030
3033
|
let rest = null;
|
|
3031
3034
|
let hasBindings = false;
|
|
3032
|
-
let
|
|
3035
|
+
let destructured2 = false;
|
|
3033
3036
|
for (const node of component ? [component] : []) {
|
|
3034
3037
|
const params = node.params ?? [];
|
|
3035
3038
|
const first = params[0];
|
|
@@ -3055,7 +3058,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3055
3058
|
if (key !== propName) continue;
|
|
3056
3059
|
const value = property.value;
|
|
3057
3060
|
if (value?.type === "Identifier") names.add(String(value.name));
|
|
3058
|
-
|
|
3061
|
+
destructured2 = true;
|
|
3059
3062
|
receivable = true;
|
|
3060
3063
|
receive = patternClose(content, first, offset);
|
|
3061
3064
|
}
|
|
@@ -3065,7 +3068,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3065
3068
|
viaProps = true;
|
|
3066
3069
|
}
|
|
3067
3070
|
}
|
|
3068
|
-
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
|
|
3071
|
+
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
|
|
3069
3072
|
}
|
|
3070
3073
|
function astroReception(ast, propName, offset, content) {
|
|
3071
3074
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3248,8 +3251,7 @@ function splitGroup(path) {
|
|
|
3248
3251
|
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
3249
3252
|
}
|
|
3250
3253
|
function moduleOf(parsed) {
|
|
3251
|
-
|
|
3252
|
-
return { ast: parsed.ast, offset: parsed.script?.offset ?? 0 };
|
|
3254
|
+
return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
|
|
3253
3255
|
}
|
|
3254
3256
|
function arrayLiterals(ast) {
|
|
3255
3257
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3262,6 +3264,19 @@ function arrayLiterals(ast) {
|
|
|
3262
3264
|
});
|
|
3263
3265
|
return out;
|
|
3264
3266
|
}
|
|
3267
|
+
function exportedNames(ast) {
|
|
3268
|
+
const out = /* @__PURE__ */ new Set();
|
|
3269
|
+
walkAst(ast, (node) => {
|
|
3270
|
+
if (node.type !== "ExportNamedDeclaration") return;
|
|
3271
|
+
const declaration2 = node.declaration;
|
|
3272
|
+
if (declaration2?.type !== "VariableDeclaration") return;
|
|
3273
|
+
for (const declarator of declaration2.declarations ?? []) {
|
|
3274
|
+
const id = declarator.id;
|
|
3275
|
+
if (id?.type === "Identifier") out.add(String(id.name));
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3265
3280
|
function propertyFor(row, literal) {
|
|
3266
3281
|
if (row?.type !== "ObjectExpression") return null;
|
|
3267
3282
|
for (const property of row.properties ?? []) {
|
|
@@ -3286,21 +3301,36 @@ function iterations(roots, identifier2) {
|
|
|
3286
3301
|
for (const root of roots) visit(root);
|
|
3287
3302
|
return out;
|
|
3288
3303
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
);
|
|
3293
|
-
const match = expression.value.match(pattern);
|
|
3304
|
+
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*=>`;
|
|
3305
|
+
function paramsIn(text, pattern, base) {
|
|
3306
|
+
const match = text.match(pattern);
|
|
3294
3307
|
if (!match || match.index === void 0) return null;
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
const within = match[0].lastIndexOf(
|
|
3308
|
+
if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
|
|
3309
|
+
const bound = match[1] ?? match[2];
|
|
3310
|
+
const within = match[0].lastIndexOf(bound);
|
|
3298
3311
|
return {
|
|
3299
|
-
row
|
|
3300
|
-
|
|
3301
|
-
|
|
3312
|
+
// One of the two, never both: a row is either named or taken apart. @see bindLists for the
|
|
3313
|
+
// second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
|
|
3314
|
+
row: match[1] ?? null,
|
|
3315
|
+
names: match[2] === void 0 ? null : destructured(match[2]),
|
|
3316
|
+
index: match[4] ?? null,
|
|
3317
|
+
addIndexAt: base + match.index + within + bound.length
|
|
3302
3318
|
};
|
|
3303
3319
|
}
|
|
3320
|
+
function callbackParams(expression, identifier2) {
|
|
3321
|
+
return paramsIn(
|
|
3322
|
+
expression.value,
|
|
3323
|
+
new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
|
|
3324
|
+
expression.range.start
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
function mapParams(content, at, until) {
|
|
3328
|
+
return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
|
|
3329
|
+
}
|
|
3330
|
+
function destructured(pattern) {
|
|
3331
|
+
const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
|
|
3332
|
+
return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
|
|
3333
|
+
}
|
|
3304
3334
|
function renders(content, node, row) {
|
|
3305
3335
|
if (!node.inner) return null;
|
|
3306
3336
|
const inner = content.slice(node.inner.start, node.inner.end).trim();
|
|
@@ -3388,11 +3418,11 @@ function bindScalar(ctx, member, literals, offset) {
|
|
|
3388
3418
|
}
|
|
3389
3419
|
return { splices, refusal: null };
|
|
3390
3420
|
}
|
|
3391
|
-
function bindDataLiterals(ctx, members) {
|
|
3392
|
-
const out = { splices: [], usesRead: false, usesRows: false, slugs: [], bound: [], declarations: [], refused: [] };
|
|
3421
|
+
async function bindDataLiterals(ctx, members) {
|
|
3422
|
+
const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
|
|
3393
3423
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
3394
3424
|
const module = moduleOf(ctx.parsed);
|
|
3395
|
-
if (!
|
|
3425
|
+
if (!rule.text) return out;
|
|
3396
3426
|
const groups = /* @__PURE__ */ new Map();
|
|
3397
3427
|
const scalars = [];
|
|
3398
3428
|
for (const member of members) {
|
|
@@ -3405,6 +3435,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3405
3435
|
const key = `${member.slug}\0${split.array}`;
|
|
3406
3436
|
groups.set(key, [...groups.get(key) ?? [], held]);
|
|
3407
3437
|
}
|
|
3438
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
3408
3439
|
const strings = stringLiterals(module.ast);
|
|
3409
3440
|
for (const member of scalars) {
|
|
3410
3441
|
const scalar = bindScalar(ctx, member, strings, module.offset);
|
|
@@ -3415,7 +3446,6 @@ function bindDataLiterals(ctx, members) {
|
|
|
3415
3446
|
out.slugs.push(member.slug);
|
|
3416
3447
|
out.bound.push(member.key);
|
|
3417
3448
|
}
|
|
3418
|
-
if (groups.size === 0) return out;
|
|
3419
3449
|
const literals = arrayLiterals(module.ast);
|
|
3420
3450
|
for (const all of groups.values()) {
|
|
3421
3451
|
const array = all[0].array;
|
|
@@ -3456,23 +3486,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3456
3486
|
}
|
|
3457
3487
|
const loop = loops[0];
|
|
3458
3488
|
const params = callbackParams(loop, identifier2);
|
|
3459
|
-
if (!params) {
|
|
3489
|
+
if (!params || params.row === null) {
|
|
3460
3490
|
refuse("IN_EXPRESSION");
|
|
3461
3491
|
continue;
|
|
3462
3492
|
}
|
|
3463
|
-
const
|
|
3493
|
+
const rowName = params.row;
|
|
3494
|
+
const elements = leafElements(ctx.content, loop, rowName);
|
|
3464
3495
|
const placed = /* @__PURE__ */ new Map();
|
|
3496
|
+
const passed = [];
|
|
3465
3497
|
let refusal = null;
|
|
3466
3498
|
for (const property of keys.keys()) {
|
|
3467
3499
|
const found = elements.get(property) ?? [];
|
|
3468
|
-
if (found.length === 1)
|
|
3469
|
-
|
|
3500
|
+
if (found.length === 1) {
|
|
3501
|
+
placed.set(property, found[0]);
|
|
3502
|
+
continue;
|
|
3503
|
+
}
|
|
3504
|
+
if (found.length > 1) {
|
|
3505
|
+
refusal = "REPEATER_AMBIGUOUS";
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
const calls = propCalls(ctx.content, loop, rowName, property);
|
|
3509
|
+
if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
|
|
3510
|
+
else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
|
|
3511
|
+
else refusal = leafRefusal(loop.value, rowName, property);
|
|
3470
3512
|
}
|
|
3471
3513
|
if (refusal !== null) {
|
|
3472
3514
|
refuse(refusal);
|
|
3473
3515
|
continue;
|
|
3474
3516
|
}
|
|
3475
|
-
const index = params.index ?? allocateIndex(loop.value);
|
|
3517
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3518
|
+
indexes.add(index);
|
|
3476
3519
|
const splices = [];
|
|
3477
3520
|
if (rule.loopKey) {
|
|
3478
3521
|
const rows = loop.children.filter((child) => child.type === "element");
|
|
@@ -3488,6 +3531,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3488
3531
|
}
|
|
3489
3532
|
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3490
3533
|
}
|
|
3534
|
+
const drilled = [];
|
|
3535
|
+
const edits = [];
|
|
3536
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3537
|
+
for (const entry of passed) {
|
|
3538
|
+
const leaf = keys.get(entry.property);
|
|
3539
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3540
|
+
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);
|
|
3541
|
+
if (!outcome.ok) {
|
|
3542
|
+
refusal = outcome.reason;
|
|
3543
|
+
break;
|
|
3544
|
+
}
|
|
3545
|
+
edits.push(...outcome.edits);
|
|
3546
|
+
bindings.set(entry.call.attrInsertAt, [
|
|
3547
|
+
...bindings.get(entry.call.attrInsertAt) ?? [],
|
|
3548
|
+
`${entry.prop}: \`${literal}\``
|
|
3549
|
+
]);
|
|
3550
|
+
drilled.push({
|
|
3551
|
+
file: outcome.edits[0].file,
|
|
3552
|
+
prop: entry.prop,
|
|
3553
|
+
path: shapeOfPath(literal),
|
|
3554
|
+
literal: `${entry.prop}: \`${literal}\``
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
if (refusal !== null) {
|
|
3558
|
+
refuse(refusal);
|
|
3559
|
+
continue;
|
|
3560
|
+
}
|
|
3561
|
+
for (const [at2, entries] of bindings) {
|
|
3562
|
+
splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
|
|
3563
|
+
}
|
|
3491
3564
|
for (const [property, element] of placed) {
|
|
3492
3565
|
const leaf = keys.get(property);
|
|
3493
3566
|
const literal = `${array}[\${${index}}].${leaf}`;
|
|
@@ -3497,7 +3570,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3497
3570
|
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3498
3571
|
// A richtext row value is html, so it goes into the dialect's html sink rather than being
|
|
3499
3572
|
// printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
|
|
3500
|
-
...kind === "richtext" && rule.richtext ? [rule.richtext(`${
|
|
3573
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
|
|
3501
3574
|
];
|
|
3502
3575
|
splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3503
3576
|
if (kind === "richtext" && rule.richtext && element.inner) {
|
|
@@ -3517,19 +3590,319 @@ function bindDataLiterals(ctx, members) {
|
|
|
3517
3590
|
splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
|
|
3518
3591
|
out.splices.push(...splices);
|
|
3519
3592
|
out.usesRows = true;
|
|
3593
|
+
out.componentEdits.push(...edits);
|
|
3594
|
+
out.props.push(...drilled);
|
|
3520
3595
|
out.slugs.push(...group.map((member) => member.slug));
|
|
3521
3596
|
out.bound.push(...group.map((member) => member.key));
|
|
3522
3597
|
}
|
|
3598
|
+
const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
|
|
3599
|
+
bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
|
|
3600
|
+
return out;
|
|
3601
|
+
}
|
|
3602
|
+
function literalTable(init) {
|
|
3603
|
+
const elements = (init.elements ?? []).filter((e) => e !== null);
|
|
3604
|
+
const first = elements[0];
|
|
3605
|
+
if (!first || elements.length !== (init.elements ?? []).length) return null;
|
|
3606
|
+
if (elements.some((element) => element.type !== first.type)) return null;
|
|
3607
|
+
if (first.type === "StringLiteral") {
|
|
3608
|
+
return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
|
|
3609
|
+
}
|
|
3610
|
+
if (first.type !== "ArrayExpression") return null;
|
|
3611
|
+
const rows = [];
|
|
3612
|
+
for (const element of elements) {
|
|
3613
|
+
const inner = (element.elements ?? []).filter((x) => x !== null);
|
|
3614
|
+
if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
|
|
3615
|
+
if (inner.length !== (element.elements ?? []).length) return null;
|
|
3616
|
+
rows.push(inner.map((x) => norm(String(x.value))));
|
|
3617
|
+
}
|
|
3618
|
+
if (new Set(rows.map((row) => row.length)).size !== 1) return null;
|
|
3619
|
+
return { tuple: true, rows };
|
|
3620
|
+
}
|
|
3621
|
+
function openingBracket(text, closeAt) {
|
|
3622
|
+
let depth = 0;
|
|
3623
|
+
for (let at = closeAt; at >= 0; at--) {
|
|
3624
|
+
if (text[at] === "]") depth++;
|
|
3625
|
+
else if (text[at] === "[") {
|
|
3626
|
+
depth--;
|
|
3627
|
+
if (depth === 0) return at;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
return -1;
|
|
3631
|
+
}
|
|
3632
|
+
function arrayLiteral(source) {
|
|
3633
|
+
try {
|
|
3634
|
+
const ast = parseProgram(`(${source})`);
|
|
3635
|
+
const expression = ast.program.body[0]?.expression;
|
|
3636
|
+
if (!expression || expression.type !== "ArrayExpression") return null;
|
|
3637
|
+
return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
|
|
3638
|
+
} catch {
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
function inlineTables(ctx) {
|
|
3643
|
+
const out = /* @__PURE__ */ new Map();
|
|
3644
|
+
const visit = (node) => {
|
|
3645
|
+
if (node.type === "element") {
|
|
3646
|
+
if (!node.raw) for (const child of node.children) visit(child);
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (node.type !== "expr") return;
|
|
3650
|
+
for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
|
|
3651
|
+
const closeAt = match.index;
|
|
3652
|
+
const openAt = openingBracket(node.value, closeAt);
|
|
3653
|
+
if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
|
|
3654
|
+
const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
|
|
3655
|
+
const table = init ? literalTable(init) : null;
|
|
3656
|
+
if (!table) continue;
|
|
3657
|
+
const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
|
|
3658
|
+
out.set(at.start, {
|
|
3659
|
+
...table,
|
|
3660
|
+
at,
|
|
3661
|
+
label: "an inline array literal",
|
|
3662
|
+
render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
for (const child of node.children) visit(child);
|
|
3666
|
+
};
|
|
3667
|
+
for (const root of ctx.parsed.roots) visit(root);
|
|
3668
|
+
return [...out.values()];
|
|
3669
|
+
}
|
|
3670
|
+
function readerElements(content, expression, reads) {
|
|
3671
|
+
const wanted = new Set(reads);
|
|
3672
|
+
const out = /* @__PURE__ */ new Map();
|
|
3673
|
+
const visit = (node) => {
|
|
3674
|
+
if (node.type === "expr") {
|
|
3675
|
+
for (const child of node.children) visit(child);
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
if (node.type !== "element" || node.raw) return;
|
|
3679
|
+
const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
|
|
3680
|
+
const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3681
|
+
if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
|
|
3682
|
+
for (const child of node.children) visit(child);
|
|
3683
|
+
};
|
|
3684
|
+
for (const child of expression.children) visit(child);
|
|
3685
|
+
return out;
|
|
3686
|
+
}
|
|
3687
|
+
function readerWraps(expression, reads) {
|
|
3688
|
+
const wanted = new Set(reads);
|
|
3689
|
+
const out = /* @__PURE__ */ new Map();
|
|
3690
|
+
const visit = (node, parent) => {
|
|
3691
|
+
if (node.type === "expr") {
|
|
3692
|
+
const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3693
|
+
const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
|
|
3694
|
+
if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
|
|
3695
|
+
for (const child of node.children) visit(child, parent);
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (node.type !== "element" || node.raw) return;
|
|
3699
|
+
for (const child of node.children) visit(child, node);
|
|
3700
|
+
};
|
|
3701
|
+
for (const child of expression.children) visit(child, null);
|
|
3702
|
+
return out;
|
|
3703
|
+
}
|
|
3704
|
+
function pathTemplates(paths, index) {
|
|
3705
|
+
const out = [];
|
|
3706
|
+
for (let position = 0; position < paths[0].length; position++) {
|
|
3707
|
+
const split = paths.map((row) => splitGroup(row[position]));
|
|
3708
|
+
const first = split[0];
|
|
3709
|
+
if (!first) return null;
|
|
3710
|
+
if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
|
|
3711
|
+
return null;
|
|
3712
|
+
}
|
|
3713
|
+
out.push(`${first.array}[\${${index}}].${first.leaf}`);
|
|
3714
|
+
}
|
|
3715
|
+
return out;
|
|
3716
|
+
}
|
|
3717
|
+
function bindLists(ctx, members, module, out, indexes) {
|
|
3718
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3719
|
+
const escapes = exportedNames(module.ast);
|
|
3720
|
+
const candidates = [];
|
|
3721
|
+
for (const [name, init] of arrayLiterals(module.ast)) {
|
|
3722
|
+
const table = literalTable(init);
|
|
3723
|
+
if (!table) continue;
|
|
3724
|
+
candidates.push({
|
|
3725
|
+
...table,
|
|
3726
|
+
at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
|
|
3727
|
+
label: `\`${name}\``,
|
|
3728
|
+
render: () => {
|
|
3729
|
+
const loops = iterations(ctx.parsed.roots, name);
|
|
3730
|
+
if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
|
|
3731
|
+
if (loops.length === 0) return null;
|
|
3732
|
+
const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
|
|
3733
|
+
if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
|
|
3734
|
+
return { loop: loops[0], params: callbackParams(loops[0], name) };
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
candidates.push(...inlineTables(ctx));
|
|
3739
|
+
const siblings = /* @__PURE__ */ new Map();
|
|
3740
|
+
const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
|
|
3741
|
+
for (const member of members) {
|
|
3742
|
+
siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
|
|
3743
|
+
}
|
|
3744
|
+
const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
|
|
3745
|
+
(leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
|
|
3746
|
+
).length;
|
|
3747
|
+
const best = /* @__PURE__ */ new Map();
|
|
3748
|
+
const bestFit = (member) => {
|
|
3749
|
+
const key = groupOf2(member);
|
|
3750
|
+
if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
|
|
3751
|
+
return best.get(key);
|
|
3752
|
+
};
|
|
3753
|
+
const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
|
|
3754
|
+
const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
|
|
3755
|
+
for (const candidate of candidates) {
|
|
3756
|
+
const mine = members.filter((member) => claims(candidate, member));
|
|
3757
|
+
if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
|
|
3758
|
+
if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
|
|
3759
|
+
const refuse = (reason) => {
|
|
3760
|
+
for (const member of mine) out.refused.push({ key: member.key, reason });
|
|
3761
|
+
};
|
|
3762
|
+
const paths = candidate.rows.map((row) => row.map(() => ""));
|
|
3763
|
+
const kinds = candidate.rows.map((row) => row.map(() => ""));
|
|
3764
|
+
let clash = false;
|
|
3765
|
+
for (const member of mine) {
|
|
3766
|
+
const spots = candidate.rows.flatMap(
|
|
3767
|
+
(row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
|
|
3768
|
+
);
|
|
3769
|
+
if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
|
|
3770
|
+
else {
|
|
3771
|
+
paths[spots[0][0]][spots[0][1]] = member.path;
|
|
3772
|
+
kinds[spots[0][0]][spots[0][1]] = member.kind;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
if (clash || paths.some((row) => row.some((path) => path === ""))) {
|
|
3776
|
+
refuse("IN_EXPRESSION");
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
const kindAt = kinds[0];
|
|
3780
|
+
if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
|
|
3781
|
+
refuse("IN_EXPRESSION");
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3784
|
+
const rendered = candidate.render();
|
|
3785
|
+
if (rendered === null) continue;
|
|
3786
|
+
if ("refuse" in rendered) {
|
|
3787
|
+
refuse(rendered.refuse);
|
|
3788
|
+
continue;
|
|
3789
|
+
}
|
|
3790
|
+
const { loop, params } = rendered;
|
|
3791
|
+
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];
|
|
3792
|
+
if (!params || !reads || reads.length !== candidate.rows[0].length) {
|
|
3793
|
+
refuse("IN_EXPRESSION");
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
const elements = readerElements(ctx.content, loop, reads);
|
|
3797
|
+
const wraps = readerWraps(loop, reads);
|
|
3798
|
+
const hosts = [];
|
|
3799
|
+
let refusal = null;
|
|
3800
|
+
for (const read of reads) {
|
|
3801
|
+
const found = elements.get(read) ?? [];
|
|
3802
|
+
const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
|
|
3803
|
+
if (found.length === 1) hosts.push({ element: found[0] });
|
|
3804
|
+
else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
|
|
3805
|
+
else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
|
|
3806
|
+
}
|
|
3807
|
+
if (refusal !== null) {
|
|
3808
|
+
refuse(refusal);
|
|
3809
|
+
continue;
|
|
3810
|
+
}
|
|
3811
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3812
|
+
indexes.add(index);
|
|
3813
|
+
const splices = [];
|
|
3814
|
+
if (rule.loopKey) {
|
|
3815
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3816
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3817
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3818
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (params.index === null) {
|
|
3822
|
+
if (params.addIndexAt < 0) {
|
|
3823
|
+
refuse("IN_EXPRESSION");
|
|
3824
|
+
continue;
|
|
3825
|
+
}
|
|
3826
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3827
|
+
}
|
|
3828
|
+
const templates = pathTemplates(paths, index);
|
|
3829
|
+
if (!templates) {
|
|
3830
|
+
refuse("IN_EXPRESSION");
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
|
|
3834
|
+
for (const [position, host] of hosts.entries()) {
|
|
3835
|
+
const kind = kindAt[position];
|
|
3836
|
+
const attributes = [
|
|
3837
|
+
rule.attr("data-bcms-field", `\`${templates[position]}\``),
|
|
3838
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3839
|
+
// A richtext value is html, so it goes into the dialect's html sink rather than being
|
|
3840
|
+
// printed as characters — the same rule one level up. @see bindDataLiterals
|
|
3841
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
|
|
3842
|
+
];
|
|
3843
|
+
if ("element" in host) {
|
|
3844
|
+
splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3845
|
+
if (kind === "richtext" && rule.richtext && host.element.inner) {
|
|
3846
|
+
splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
|
|
3847
|
+
}
|
|
3848
|
+
} else {
|
|
3849
|
+
const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
|
|
3850
|
+
splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
|
|
3851
|
+
}
|
|
3852
|
+
out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
|
|
3853
|
+
}
|
|
3854
|
+
const args = [
|
|
3855
|
+
ctx.snapshotName(mine[0].slug),
|
|
3856
|
+
list,
|
|
3857
|
+
ctx.content.slice(candidate.at.start, candidate.at.end),
|
|
3858
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3859
|
+
];
|
|
3860
|
+
splices.push({
|
|
3861
|
+
start: candidate.at.start,
|
|
3862
|
+
end: candidate.at.end,
|
|
3863
|
+
text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
|
|
3864
|
+
});
|
|
3865
|
+
out.splices.push(...splices);
|
|
3866
|
+
if (candidate.tuple) out.usesTuples = true;
|
|
3867
|
+
else out.usesList = true;
|
|
3868
|
+
out.slugs.push(...mine.map((member) => member.slug));
|
|
3869
|
+
out.bound.push(...mine.map((member) => member.key));
|
|
3870
|
+
out.notes.push(
|
|
3871
|
+
`${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.`
|
|
3872
|
+
);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
function propCalls(content, expression, row, property) {
|
|
3876
|
+
const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
|
|
3877
|
+
const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
|
|
3878
|
+
const out = [];
|
|
3879
|
+
const visit = (node) => {
|
|
3880
|
+
if (node.type === "expr") {
|
|
3881
|
+
for (const child of node.children) visit(child);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
if (node.type !== "element" || node.raw) return;
|
|
3885
|
+
if (node.tag !== "" && !isIntrinsic(node.tag)) {
|
|
3886
|
+
for (const attr of node.attrs) {
|
|
3887
|
+
if (!attr.expressionRange) continue;
|
|
3888
|
+
const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
|
|
3889
|
+
if (spread.test(text)) out.push({ call: node, prop: property });
|
|
3890
|
+
else if (named.test(text)) out.push({ call: node, prop: attr.name });
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
for (const child of node.children) visit(child);
|
|
3894
|
+
};
|
|
3895
|
+
for (const child of expression.children) visit(child);
|
|
3523
3896
|
return out;
|
|
3524
3897
|
}
|
|
3525
3898
|
function leafRefusal(loop, row, property) {
|
|
3526
3899
|
const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
|
|
3527
3900
|
return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
|
|
3528
3901
|
}
|
|
3529
|
-
function allocateIndex(loop) {
|
|
3902
|
+
function allocateIndex(loop, taken) {
|
|
3530
3903
|
const used = new Set(loop.match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
3531
|
-
if (!used.has("i")) return "i";
|
|
3532
|
-
for (let n = 1; ; n++) if (!used.has(`i_${n}`)) return `i_${n}`;
|
|
3904
|
+
if (!used.has("i") && !taken.has("i")) return "i";
|
|
3905
|
+
for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
|
|
3533
3906
|
}
|
|
3534
3907
|
|
|
3535
3908
|
// src/resolve.ts
|
|
@@ -3611,6 +3984,13 @@ function isAliasShaped(value) {
|
|
|
3611
3984
|
function unreadableConfigs(sources) {
|
|
3612
3985
|
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3613
3986
|
}
|
|
3987
|
+
function extendedConfigs(sources) {
|
|
3988
|
+
return sources.filter((source) => {
|
|
3989
|
+
if (!isProjectConfig(source.path)) return false;
|
|
3990
|
+
const parsed = readJsonc(source.content);
|
|
3991
|
+
return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
|
|
3992
|
+
}).map((source) => source.path);
|
|
3993
|
+
}
|
|
3614
3994
|
function expand(specifier, aliases) {
|
|
3615
3995
|
const out = [];
|
|
3616
3996
|
for (const [pattern, targets] of aliases) {
|
|
@@ -3740,7 +4120,7 @@ function bindImages(ctx, targets) {
|
|
|
3740
4120
|
|
|
3741
4121
|
// src/helper.ts
|
|
3742
4122
|
import { createHash as createHash2 } from "crypto";
|
|
3743
|
-
var HELPER_VERSION =
|
|
4123
|
+
var HELPER_VERSION = 6;
|
|
3744
4124
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
3745
4125
|
var HELPER_PATH = Object.fromEntries(
|
|
3746
4126
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -3848,6 +4228,30 @@ export function bcmsRowsAs<T>(
|
|
|
3848
4228
|
});
|
|
3849
4229
|
}
|
|
3850
4230
|
|
|
4231
|
+
/**
|
|
4232
|
+
* A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
|
|
4233
|
+
*
|
|
4234
|
+
* \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
|
|
4235
|
+
* renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
|
|
4236
|
+
* an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
|
|
4237
|
+
* resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
|
|
4238
|
+
* editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
|
|
4239
|
+
* this out loud rather than leaving an editor to discover it.
|
|
4240
|
+
*/
|
|
4241
|
+
export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
|
|
4242
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4243
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4244
|
+
return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
/**
|
|
4248
|
+
* The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
|
|
4249
|
+
* same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
|
|
4250
|
+
*/
|
|
4251
|
+
export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
|
|
4252
|
+
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4253
|
+
}
|
|
4254
|
+
|
|
3851
4255
|
/** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
|
|
3852
4256
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
3853
4257
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
@@ -3939,19 +4343,22 @@ var HISTORICAL = {
|
|
|
3939
4343
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3940
4344
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3941
4345
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3942
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4346
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4347
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3943
4348
|
},
|
|
3944
4349
|
"src/lib/bcms-content.ts": {
|
|
3945
4350
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
3946
4351
|
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
3947
4352
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
3948
|
-
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af"
|
|
4353
|
+
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4354
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
3949
4355
|
},
|
|
3950
4356
|
"composables/bcms-content.ts": {
|
|
3951
4357
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3952
4358
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3953
4359
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3954
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4360
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4361
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3955
4362
|
}
|
|
3956
4363
|
};
|
|
3957
4364
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4486,8 +4893,8 @@ function declaringElements(roots, content) {
|
|
|
4486
4893
|
const name = attr.name.replace(/^:/, "");
|
|
4487
4894
|
if (name === "bcmsBindings") {
|
|
4488
4895
|
const own = ownBytes(node, content);
|
|
4489
|
-
for (const
|
|
4490
|
-
out.push({ scope: "page", path
|
|
4896
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
4897
|
+
out.push({ scope: "page", path, kind, own });
|
|
4491
4898
|
}
|
|
4492
4899
|
continue;
|
|
4493
4900
|
}
|
|
@@ -4517,7 +4924,7 @@ function declaringElements(roots, content) {
|
|
|
4517
4924
|
return out;
|
|
4518
4925
|
}
|
|
4519
4926
|
function helperLocals(parsed, dialect, file) {
|
|
4520
|
-
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [] };
|
|
4927
|
+
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
|
|
4521
4928
|
const helper = HELPER_PATH[dialect];
|
|
4522
4929
|
if (!helper) return out;
|
|
4523
4930
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
@@ -4527,6 +4934,8 @@ function helperLocals(parsed, dialect, file) {
|
|
|
4527
4934
|
if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
|
|
4528
4935
|
if (from.imported === "bcmsImage") out.bcmsImage.push(local);
|
|
4529
4936
|
if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
|
|
4937
|
+
if (from.imported === "bcmsList") out.bcmsList.push(local);
|
|
4938
|
+
if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
|
|
4530
4939
|
}
|
|
4531
4940
|
return out;
|
|
4532
4941
|
}
|
|
@@ -4599,6 +5008,9 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4599
5008
|
keyed = reads(locals.bcmsRowsAs);
|
|
4600
5009
|
rows = keyed || reads(locals.bcmsRows);
|
|
4601
5010
|
}
|
|
5011
|
+
const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
|
|
5012
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
|
|
5013
|
+
);
|
|
4602
5014
|
let cached = null;
|
|
4603
5015
|
const moduleRead = () => {
|
|
4604
5016
|
if (indexed || snapshot === null) return false;
|
|
@@ -4614,7 +5026,7 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4614
5026
|
return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
|
|
4615
5027
|
}
|
|
4616
5028
|
if (element.path !== shape) return false;
|
|
4617
|
-
return rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
5029
|
+
return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
4618
5030
|
});
|
|
4619
5031
|
}
|
|
4620
5032
|
|
|
@@ -5692,6 +6104,7 @@ function isSourceCandidate(path) {
|
|
|
5692
6104
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
5693
6105
|
return false;
|
|
5694
6106
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6107
|
+
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
5695
6108
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
5696
6109
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
5697
6110
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
@@ -7740,6 +8153,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7740
8153
|
const receiptFiles = [];
|
|
7741
8154
|
const dialects = /* @__PURE__ */ new Set();
|
|
7742
8155
|
const dynamicBindings = [];
|
|
8156
|
+
const laneNotes = [];
|
|
7743
8157
|
const snapshotSlugs = /* @__PURE__ */ new Set();
|
|
7744
8158
|
let occurrencesLocated = 0;
|
|
7745
8159
|
let occurrencesRewritten = 0;
|
|
@@ -7765,6 +8179,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7765
8179
|
outFiles.push({ path: file, operation: "modify", content: result.content });
|
|
7766
8180
|
receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
|
|
7767
8181
|
dynamicBindings.push(...result.dynamic);
|
|
8182
|
+
laneNotes.push(...result.notes);
|
|
7768
8183
|
for (const slug of result.snapshots) snapshotSlugs.add(slug);
|
|
7769
8184
|
}
|
|
7770
8185
|
}
|
|
@@ -7861,13 +8276,25 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7861
8276
|
helperUpgraded,
|
|
7862
8277
|
dynamicBindings,
|
|
7863
8278
|
canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
|
|
7864
|
-
notes:
|
|
7865
|
-
(
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
8279
|
+
notes: [
|
|
8280
|
+
...unreadableConfigs(sources).map(
|
|
8281
|
+
(file) => ({
|
|
8282
|
+
code: "TSCONFIG_UNREADABLE",
|
|
8283
|
+
file,
|
|
8284
|
+
message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
8285
|
+
})
|
|
8286
|
+
),
|
|
8287
|
+
// An absent alias table looks exactly like a project that has none, and the two have
|
|
8288
|
+
// opposite consequences for every component reached through one. @see extendedConfigs
|
|
8289
|
+
...extendedConfigs(sources).map(
|
|
8290
|
+
(file) => ({
|
|
8291
|
+
code: "TSCONFIG_EXTENDS_ONLY",
|
|
8292
|
+
file,
|
|
8293
|
+
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.`
|
|
8294
|
+
})
|
|
8295
|
+
),
|
|
8296
|
+
...laneNotes
|
|
8297
|
+
]
|
|
7871
8298
|
});
|
|
7872
8299
|
return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
|
|
7873
8300
|
}
|
|
@@ -7889,7 +8316,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7889
8316
|
dialect: dialectOf(file, content),
|
|
7890
8317
|
dynamic: [],
|
|
7891
8318
|
componentEdits: [],
|
|
7892
|
-
snapshots: []
|
|
8319
|
+
snapshots: [],
|
|
8320
|
+
notes: []
|
|
7893
8321
|
};
|
|
7894
8322
|
if (pendingTargets.length === 0) return empty;
|
|
7895
8323
|
if (!dialect) {
|
|
@@ -7950,7 +8378,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7950
8378
|
dialect: null,
|
|
7951
8379
|
dynamic: [],
|
|
7952
8380
|
componentEdits: [],
|
|
7953
|
-
snapshots: []
|
|
8381
|
+
snapshots: [],
|
|
8382
|
+
notes: []
|
|
7954
8383
|
};
|
|
7955
8384
|
}
|
|
7956
8385
|
if (rescued.reason === "unverifiable") {
|
|
@@ -8020,6 +8449,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8020
8449
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
8021
8450
|
{ key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
|
|
8022
8451
|
{ key: "helper:bcmsImage", name: "bcmsImage" },
|
|
8452
|
+
{ key: "helper:bcmsList", name: "bcmsList" },
|
|
8453
|
+
{ key: "helper:bcmsTuples", name: "bcmsTuples" },
|
|
8023
8454
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
8024
8455
|
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
8025
8456
|
].filter((entry) => !preset.has(entry.key));
|
|
@@ -8211,15 +8642,23 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8211
8642
|
const collided = new Set(overlapping(kept).map((r) => r.key));
|
|
8212
8643
|
const applied = kept.filter((r) => !collided.has(r.key));
|
|
8213
8644
|
for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
|
|
8214
|
-
const data = bindDataLiterals(
|
|
8645
|
+
const data = await bindDataLiterals(
|
|
8215
8646
|
{
|
|
8216
8647
|
content,
|
|
8217
8648
|
parsed,
|
|
8218
8649
|
dialect: writing,
|
|
8219
8650
|
rowsAs: helperName("bcmsRowsAs"),
|
|
8651
|
+
list: helperName("bcmsList"),
|
|
8652
|
+
tuples: helperName("bcmsTuples"),
|
|
8220
8653
|
read: helperName("bcms"),
|
|
8221
8654
|
snapshotName,
|
|
8222
|
-
slug: params?.slug ?? null
|
|
8655
|
+
slug: params?.slug ?? null,
|
|
8656
|
+
// The row's leaf travels to another file exactly as a scalar prop does, and by the same
|
|
8657
|
+
// machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
|
|
8658
|
+
drill: async (component, prop, kind) => {
|
|
8659
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
8660
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
8661
|
+
}
|
|
8223
8662
|
},
|
|
8224
8663
|
targets.filter((t) => {
|
|
8225
8664
|
const outcome = outcomes.get(t.key);
|
|
@@ -8237,6 +8676,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8237
8676
|
for (const declaration2 of data.declarations) {
|
|
8238
8677
|
dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
|
|
8239
8678
|
}
|
|
8679
|
+
componentEdits.push(...data.componentEdits);
|
|
8680
|
+
for (const drilledProp of data.props) {
|
|
8681
|
+
dynamic.push({
|
|
8682
|
+
file: drilledProp.file,
|
|
8683
|
+
prop: drilledProp.prop,
|
|
8684
|
+
path: drilledProp.path,
|
|
8685
|
+
callSites: [{ file, literal: drilledProp.literal }]
|
|
8686
|
+
});
|
|
8687
|
+
}
|
|
8240
8688
|
const images = bindImages(
|
|
8241
8689
|
{ content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
|
|
8242
8690
|
targets.filter((t) => {
|
|
@@ -8248,7 +8696,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8248
8696
|
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8249
8697
|
const lanes = [...data.splices, ...images.splices];
|
|
8250
8698
|
if (applied.length === 0 && lanes.length === 0) {
|
|
8251
|
-
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
|
|
8699
|
+
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|
|
8252
8700
|
}
|
|
8253
8701
|
const readSlugs = [
|
|
8254
8702
|
.../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
|
|
@@ -8275,6 +8723,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8275
8723
|
] : [],
|
|
8276
8724
|
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
|
|
8277
8725
|
...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
|
|
8726
|
+
...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
|
|
8727
|
+
// `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
|
|
8728
|
+
// page imports only the one it calls — an unused import is dead code in someone else's repo.
|
|
8729
|
+
...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
|
|
8278
8730
|
...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
|
|
8279
8731
|
...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
|
|
8280
8732
|
].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
|
|
@@ -8289,7 +8741,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8289
8741
|
dialect: writing,
|
|
8290
8742
|
dynamic,
|
|
8291
8743
|
componentEdits,
|
|
8292
|
-
snapshots: rule.text ? readSlugs : []
|
|
8744
|
+
snapshots: rule.text ? readSlugs : [],
|
|
8745
|
+
// The lane reports one sentence per list it bound; the FILE is added here, where it is known.
|
|
8746
|
+
notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
|
|
8293
8747
|
};
|
|
8294
8748
|
}
|
|
8295
8749
|
function mergeInto(content, site) {
|