@bettercms-ai/convert 0.7.0 → 0.9.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 +81 -0
- package/README.md +103 -6
- package/dist/cli.js +872 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +178 -4
- package/dist/index.js +786 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import MagicString6 from "magic-string";
|
|
3
3
|
|
|
4
4
|
// src/brief.ts
|
|
5
5
|
import { createHash } from "crypto";
|
|
@@ -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)));
|
|
@@ -2558,7 +2561,16 @@ var IMAGE_SOURCES = {
|
|
|
2558
2561
|
};
|
|
2559
2562
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2560
2563
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2561
|
-
var BCMS_ATTRS = [
|
|
2564
|
+
var BCMS_ATTRS = [
|
|
2565
|
+
"data-bcms-field",
|
|
2566
|
+
"data-bcms-kind",
|
|
2567
|
+
"data-bcms-props",
|
|
2568
|
+
"data-bcms-layout-field",
|
|
2569
|
+
// The forms lane's two. Listed here so `collectSites` never matches a brief literal against a
|
|
2570
|
+
// marker this package itself wrote — a form id or a field key is not the page's copy.
|
|
2571
|
+
"data-bcms-form",
|
|
2572
|
+
"data-bcms-form-field"
|
|
2573
|
+
];
|
|
2562
2574
|
var attrInsertAfter = (attrs, start, tag) => attrs.length > 0 ? Math.max(...attrs.map((a) => a.range.end)) : start + 1 + tag.length;
|
|
2563
2575
|
function openTagEnd(content, start) {
|
|
2564
2576
|
let quote = null;
|
|
@@ -3027,7 +3039,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3027
3039
|
let receiver = null;
|
|
3028
3040
|
let rest = null;
|
|
3029
3041
|
let hasBindings = false;
|
|
3030
|
-
let
|
|
3042
|
+
let destructured2 = false;
|
|
3031
3043
|
for (const node of component ? [component] : []) {
|
|
3032
3044
|
const params = node.params ?? [];
|
|
3033
3045
|
const first = params[0];
|
|
@@ -3053,7 +3065,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3053
3065
|
if (key !== propName) continue;
|
|
3054
3066
|
const value = property.value;
|
|
3055
3067
|
if (value?.type === "Identifier") names.add(String(value.name));
|
|
3056
|
-
|
|
3068
|
+
destructured2 = true;
|
|
3057
3069
|
receivable = true;
|
|
3058
3070
|
receive = patternClose(content, first, offset);
|
|
3059
3071
|
}
|
|
@@ -3063,7 +3075,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3063
3075
|
viaProps = true;
|
|
3064
3076
|
}
|
|
3065
3077
|
}
|
|
3066
|
-
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
|
|
3078
|
+
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
|
|
3067
3079
|
}
|
|
3068
3080
|
function astroReception(ast, propName, offset, content) {
|
|
3069
3081
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3246,8 +3258,7 @@ function splitGroup(path) {
|
|
|
3246
3258
|
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
3247
3259
|
}
|
|
3248
3260
|
function moduleOf(parsed) {
|
|
3249
|
-
|
|
3250
|
-
return { ast: parsed.ast, offset: parsed.script?.offset ?? 0 };
|
|
3261
|
+
return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
|
|
3251
3262
|
}
|
|
3252
3263
|
function arrayLiterals(ast) {
|
|
3253
3264
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3260,6 +3271,19 @@ function arrayLiterals(ast) {
|
|
|
3260
3271
|
});
|
|
3261
3272
|
return out;
|
|
3262
3273
|
}
|
|
3274
|
+
function exportedNames(ast) {
|
|
3275
|
+
const out = /* @__PURE__ */ new Set();
|
|
3276
|
+
walkAst(ast, (node) => {
|
|
3277
|
+
if (node.type !== "ExportNamedDeclaration") return;
|
|
3278
|
+
const declaration2 = node.declaration;
|
|
3279
|
+
if (declaration2?.type !== "VariableDeclaration") return;
|
|
3280
|
+
for (const declarator of declaration2.declarations ?? []) {
|
|
3281
|
+
const id = declarator.id;
|
|
3282
|
+
if (id?.type === "Identifier") out.add(String(id.name));
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
return out;
|
|
3286
|
+
}
|
|
3263
3287
|
function propertyFor(row, literal) {
|
|
3264
3288
|
if (row?.type !== "ObjectExpression") return null;
|
|
3265
3289
|
for (const property of row.properties ?? []) {
|
|
@@ -3284,21 +3308,36 @@ function iterations(roots, identifier2) {
|
|
|
3284
3308
|
for (const root of roots) visit(root);
|
|
3285
3309
|
return out;
|
|
3286
3310
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
);
|
|
3291
|
-
const match = expression.value.match(pattern);
|
|
3311
|
+
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*=>`;
|
|
3312
|
+
function paramsIn(text, pattern, base) {
|
|
3313
|
+
const match = text.match(pattern);
|
|
3292
3314
|
if (!match || match.index === void 0) return null;
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
const within = match[0].lastIndexOf(
|
|
3315
|
+
if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
|
|
3316
|
+
const bound = match[1] ?? match[2];
|
|
3317
|
+
const within = match[0].lastIndexOf(bound);
|
|
3296
3318
|
return {
|
|
3297
|
-
row
|
|
3298
|
-
|
|
3299
|
-
|
|
3319
|
+
// One of the two, never both: a row is either named or taken apart. @see bindLists for the
|
|
3320
|
+
// second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
|
|
3321
|
+
row: match[1] ?? null,
|
|
3322
|
+
names: match[2] === void 0 ? null : destructured(match[2]),
|
|
3323
|
+
index: match[4] ?? null,
|
|
3324
|
+
addIndexAt: base + match.index + within + bound.length
|
|
3300
3325
|
};
|
|
3301
3326
|
}
|
|
3327
|
+
function callbackParams(expression, identifier2) {
|
|
3328
|
+
return paramsIn(
|
|
3329
|
+
expression.value,
|
|
3330
|
+
new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
|
|
3331
|
+
expression.range.start
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
function mapParams(content, at, until) {
|
|
3335
|
+
return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
|
|
3336
|
+
}
|
|
3337
|
+
function destructured(pattern) {
|
|
3338
|
+
const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
|
|
3339
|
+
return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
|
|
3340
|
+
}
|
|
3302
3341
|
function renders(content, node, row) {
|
|
3303
3342
|
if (!node.inner) return null;
|
|
3304
3343
|
const inner = content.slice(node.inner.start, node.inner.end).trim();
|
|
@@ -3386,11 +3425,11 @@ function bindScalar(ctx, member, literals, offset) {
|
|
|
3386
3425
|
}
|
|
3387
3426
|
return { splices, refusal: null };
|
|
3388
3427
|
}
|
|
3389
|
-
function bindDataLiterals(ctx, members) {
|
|
3390
|
-
const out = { splices: [], usesRead: false, usesRows: false, slugs: [], bound: [], declarations: [], refused: [] };
|
|
3428
|
+
async function bindDataLiterals(ctx, members) {
|
|
3429
|
+
const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
|
|
3391
3430
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
3392
3431
|
const module = moduleOf(ctx.parsed);
|
|
3393
|
-
if (!
|
|
3432
|
+
if (!rule.text) return out;
|
|
3394
3433
|
const groups = /* @__PURE__ */ new Map();
|
|
3395
3434
|
const scalars = [];
|
|
3396
3435
|
for (const member of members) {
|
|
@@ -3403,6 +3442,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3403
3442
|
const key = `${member.slug}\0${split.array}`;
|
|
3404
3443
|
groups.set(key, [...groups.get(key) ?? [], held]);
|
|
3405
3444
|
}
|
|
3445
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
3406
3446
|
const strings = stringLiterals(module.ast);
|
|
3407
3447
|
for (const member of scalars) {
|
|
3408
3448
|
const scalar = bindScalar(ctx, member, strings, module.offset);
|
|
@@ -3413,7 +3453,6 @@ function bindDataLiterals(ctx, members) {
|
|
|
3413
3453
|
out.slugs.push(member.slug);
|
|
3414
3454
|
out.bound.push(member.key);
|
|
3415
3455
|
}
|
|
3416
|
-
if (groups.size === 0) return out;
|
|
3417
3456
|
const literals = arrayLiterals(module.ast);
|
|
3418
3457
|
for (const all of groups.values()) {
|
|
3419
3458
|
const array = all[0].array;
|
|
@@ -3454,23 +3493,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3454
3493
|
}
|
|
3455
3494
|
const loop = loops[0];
|
|
3456
3495
|
const params = callbackParams(loop, identifier2);
|
|
3457
|
-
if (!params) {
|
|
3496
|
+
if (!params || params.row === null) {
|
|
3458
3497
|
refuse("IN_EXPRESSION");
|
|
3459
3498
|
continue;
|
|
3460
3499
|
}
|
|
3461
|
-
const
|
|
3500
|
+
const rowName = params.row;
|
|
3501
|
+
const elements = leafElements(ctx.content, loop, rowName);
|
|
3462
3502
|
const placed = /* @__PURE__ */ new Map();
|
|
3503
|
+
const passed = [];
|
|
3463
3504
|
let refusal = null;
|
|
3464
3505
|
for (const property of keys.keys()) {
|
|
3465
3506
|
const found = elements.get(property) ?? [];
|
|
3466
|
-
if (found.length === 1)
|
|
3467
|
-
|
|
3507
|
+
if (found.length === 1) {
|
|
3508
|
+
placed.set(property, found[0]);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
if (found.length > 1) {
|
|
3512
|
+
refusal = "REPEATER_AMBIGUOUS";
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3515
|
+
const calls = propCalls(ctx.content, loop, rowName, property);
|
|
3516
|
+
if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
|
|
3517
|
+
else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
|
|
3518
|
+
else refusal = leafRefusal(loop.value, rowName, property);
|
|
3468
3519
|
}
|
|
3469
3520
|
if (refusal !== null) {
|
|
3470
3521
|
refuse(refusal);
|
|
3471
3522
|
continue;
|
|
3472
3523
|
}
|
|
3473
|
-
const index = params.index ?? allocateIndex(loop.value);
|
|
3524
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3525
|
+
indexes.add(index);
|
|
3474
3526
|
const splices = [];
|
|
3475
3527
|
if (rule.loopKey) {
|
|
3476
3528
|
const rows = loop.children.filter((child) => child.type === "element");
|
|
@@ -3486,6 +3538,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3486
3538
|
}
|
|
3487
3539
|
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3488
3540
|
}
|
|
3541
|
+
const drilled = [];
|
|
3542
|
+
const edits = [];
|
|
3543
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3544
|
+
for (const entry of passed) {
|
|
3545
|
+
const leaf = keys.get(entry.property);
|
|
3546
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3547
|
+
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);
|
|
3548
|
+
if (!outcome.ok) {
|
|
3549
|
+
refusal = outcome.reason;
|
|
3550
|
+
break;
|
|
3551
|
+
}
|
|
3552
|
+
edits.push(...outcome.edits);
|
|
3553
|
+
bindings.set(entry.call.attrInsertAt, [
|
|
3554
|
+
...bindings.get(entry.call.attrInsertAt) ?? [],
|
|
3555
|
+
`${entry.prop}: \`${literal}\``
|
|
3556
|
+
]);
|
|
3557
|
+
drilled.push({
|
|
3558
|
+
file: outcome.edits[0].file,
|
|
3559
|
+
prop: entry.prop,
|
|
3560
|
+
path: shapeOfPath(literal),
|
|
3561
|
+
literal: `${entry.prop}: \`${literal}\``
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
if (refusal !== null) {
|
|
3565
|
+
refuse(refusal);
|
|
3566
|
+
continue;
|
|
3567
|
+
}
|
|
3568
|
+
for (const [at2, entries] of bindings) {
|
|
3569
|
+
splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
|
|
3570
|
+
}
|
|
3489
3571
|
for (const [property, element] of placed) {
|
|
3490
3572
|
const leaf = keys.get(property);
|
|
3491
3573
|
const literal = `${array}[\${${index}}].${leaf}`;
|
|
@@ -3495,7 +3577,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3495
3577
|
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3496
3578
|
// A richtext row value is html, so it goes into the dialect's html sink rather than being
|
|
3497
3579
|
// printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
|
|
3498
|
-
...kind === "richtext" && rule.richtext ? [rule.richtext(`${
|
|
3580
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
|
|
3499
3581
|
];
|
|
3500
3582
|
splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3501
3583
|
if (kind === "richtext" && rule.richtext && element.inner) {
|
|
@@ -3515,19 +3597,319 @@ function bindDataLiterals(ctx, members) {
|
|
|
3515
3597
|
splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
|
|
3516
3598
|
out.splices.push(...splices);
|
|
3517
3599
|
out.usesRows = true;
|
|
3600
|
+
out.componentEdits.push(...edits);
|
|
3601
|
+
out.props.push(...drilled);
|
|
3518
3602
|
out.slugs.push(...group.map((member) => member.slug));
|
|
3519
3603
|
out.bound.push(...group.map((member) => member.key));
|
|
3520
3604
|
}
|
|
3605
|
+
const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
|
|
3606
|
+
bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
|
|
3607
|
+
return out;
|
|
3608
|
+
}
|
|
3609
|
+
function literalTable(init) {
|
|
3610
|
+
const elements = (init.elements ?? []).filter((e) => e !== null);
|
|
3611
|
+
const first = elements[0];
|
|
3612
|
+
if (!first || elements.length !== (init.elements ?? []).length) return null;
|
|
3613
|
+
if (elements.some((element) => element.type !== first.type)) return null;
|
|
3614
|
+
if (first.type === "StringLiteral") {
|
|
3615
|
+
return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
|
|
3616
|
+
}
|
|
3617
|
+
if (first.type !== "ArrayExpression") return null;
|
|
3618
|
+
const rows = [];
|
|
3619
|
+
for (const element of elements) {
|
|
3620
|
+
const inner = (element.elements ?? []).filter((x) => x !== null);
|
|
3621
|
+
if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
|
|
3622
|
+
if (inner.length !== (element.elements ?? []).length) return null;
|
|
3623
|
+
rows.push(inner.map((x) => norm(String(x.value))));
|
|
3624
|
+
}
|
|
3625
|
+
if (new Set(rows.map((row) => row.length)).size !== 1) return null;
|
|
3626
|
+
return { tuple: true, rows };
|
|
3627
|
+
}
|
|
3628
|
+
function openingBracket(text, closeAt) {
|
|
3629
|
+
let depth = 0;
|
|
3630
|
+
for (let at = closeAt; at >= 0; at--) {
|
|
3631
|
+
if (text[at] === "]") depth++;
|
|
3632
|
+
else if (text[at] === "[") {
|
|
3633
|
+
depth--;
|
|
3634
|
+
if (depth === 0) return at;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return -1;
|
|
3638
|
+
}
|
|
3639
|
+
function arrayLiteral(source) {
|
|
3640
|
+
try {
|
|
3641
|
+
const ast = parseProgram(`(${source})`);
|
|
3642
|
+
const expression = ast.program.body[0]?.expression;
|
|
3643
|
+
if (!expression || expression.type !== "ArrayExpression") return null;
|
|
3644
|
+
return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
|
|
3645
|
+
} catch {
|
|
3646
|
+
return null;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
function inlineTables(ctx) {
|
|
3650
|
+
const out = /* @__PURE__ */ new Map();
|
|
3651
|
+
const visit = (node) => {
|
|
3652
|
+
if (node.type === "element") {
|
|
3653
|
+
if (!node.raw) for (const child of node.children) visit(child);
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (node.type !== "expr") return;
|
|
3657
|
+
for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
|
|
3658
|
+
const closeAt = match.index;
|
|
3659
|
+
const openAt = openingBracket(node.value, closeAt);
|
|
3660
|
+
if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
|
|
3661
|
+
const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
|
|
3662
|
+
const table = init ? literalTable(init) : null;
|
|
3663
|
+
if (!table) continue;
|
|
3664
|
+
const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
|
|
3665
|
+
out.set(at.start, {
|
|
3666
|
+
...table,
|
|
3667
|
+
at,
|
|
3668
|
+
label: "an inline array literal",
|
|
3669
|
+
render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
for (const child of node.children) visit(child);
|
|
3673
|
+
};
|
|
3674
|
+
for (const root of ctx.parsed.roots) visit(root);
|
|
3675
|
+
return [...out.values()];
|
|
3676
|
+
}
|
|
3677
|
+
function readerElements(content, expression, reads) {
|
|
3678
|
+
const wanted = new Set(reads);
|
|
3679
|
+
const out = /* @__PURE__ */ new Map();
|
|
3680
|
+
const visit = (node) => {
|
|
3681
|
+
if (node.type === "expr") {
|
|
3682
|
+
for (const child of node.children) visit(child);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
if (node.type !== "element" || node.raw) return;
|
|
3686
|
+
const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
|
|
3687
|
+
const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3688
|
+
if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
|
|
3689
|
+
for (const child of node.children) visit(child);
|
|
3690
|
+
};
|
|
3691
|
+
for (const child of expression.children) visit(child);
|
|
3692
|
+
return out;
|
|
3693
|
+
}
|
|
3694
|
+
function readerWraps(expression, reads) {
|
|
3695
|
+
const wanted = new Set(reads);
|
|
3696
|
+
const out = /* @__PURE__ */ new Map();
|
|
3697
|
+
const visit = (node, parent) => {
|
|
3698
|
+
if (node.type === "expr") {
|
|
3699
|
+
const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3700
|
+
const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
|
|
3701
|
+
if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
|
|
3702
|
+
for (const child of node.children) visit(child, parent);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (node.type !== "element" || node.raw) return;
|
|
3706
|
+
for (const child of node.children) visit(child, node);
|
|
3707
|
+
};
|
|
3708
|
+
for (const child of expression.children) visit(child, null);
|
|
3709
|
+
return out;
|
|
3710
|
+
}
|
|
3711
|
+
function pathTemplates(paths, index) {
|
|
3712
|
+
const out = [];
|
|
3713
|
+
for (let position = 0; position < paths[0].length; position++) {
|
|
3714
|
+
const split = paths.map((row) => splitGroup(row[position]));
|
|
3715
|
+
const first = split[0];
|
|
3716
|
+
if (!first) return null;
|
|
3717
|
+
if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
out.push(`${first.array}[\${${index}}].${first.leaf}`);
|
|
3721
|
+
}
|
|
3722
|
+
return out;
|
|
3723
|
+
}
|
|
3724
|
+
function bindLists(ctx, members, module, out, indexes) {
|
|
3725
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3726
|
+
const escapes = exportedNames(module.ast);
|
|
3727
|
+
const candidates = [];
|
|
3728
|
+
for (const [name, init] of arrayLiterals(module.ast)) {
|
|
3729
|
+
const table = literalTable(init);
|
|
3730
|
+
if (!table) continue;
|
|
3731
|
+
candidates.push({
|
|
3732
|
+
...table,
|
|
3733
|
+
at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
|
|
3734
|
+
label: `\`${name}\``,
|
|
3735
|
+
render: () => {
|
|
3736
|
+
const loops = iterations(ctx.parsed.roots, name);
|
|
3737
|
+
if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
|
|
3738
|
+
if (loops.length === 0) return null;
|
|
3739
|
+
const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
|
|
3740
|
+
if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
|
|
3741
|
+
return { loop: loops[0], params: callbackParams(loops[0], name) };
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
candidates.push(...inlineTables(ctx));
|
|
3746
|
+
const siblings = /* @__PURE__ */ new Map();
|
|
3747
|
+
const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
|
|
3748
|
+
for (const member of members) {
|
|
3749
|
+
siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
|
|
3750
|
+
}
|
|
3751
|
+
const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
|
|
3752
|
+
(leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
|
|
3753
|
+
).length;
|
|
3754
|
+
const best = /* @__PURE__ */ new Map();
|
|
3755
|
+
const bestFit = (member) => {
|
|
3756
|
+
const key = groupOf2(member);
|
|
3757
|
+
if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
|
|
3758
|
+
return best.get(key);
|
|
3759
|
+
};
|
|
3760
|
+
const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
|
|
3761
|
+
const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
|
|
3762
|
+
for (const candidate of candidates) {
|
|
3763
|
+
const mine = members.filter((member) => claims(candidate, member));
|
|
3764
|
+
if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
|
|
3765
|
+
if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
|
|
3766
|
+
const refuse = (reason) => {
|
|
3767
|
+
for (const member of mine) out.refused.push({ key: member.key, reason });
|
|
3768
|
+
};
|
|
3769
|
+
const paths = candidate.rows.map((row) => row.map(() => ""));
|
|
3770
|
+
const kinds = candidate.rows.map((row) => row.map(() => ""));
|
|
3771
|
+
let clash = false;
|
|
3772
|
+
for (const member of mine) {
|
|
3773
|
+
const spots = candidate.rows.flatMap(
|
|
3774
|
+
(row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
|
|
3775
|
+
);
|
|
3776
|
+
if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
|
|
3777
|
+
else {
|
|
3778
|
+
paths[spots[0][0]][spots[0][1]] = member.path;
|
|
3779
|
+
kinds[spots[0][0]][spots[0][1]] = member.kind;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
if (clash || paths.some((row) => row.some((path) => path === ""))) {
|
|
3783
|
+
refuse("IN_EXPRESSION");
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
const kindAt = kinds[0];
|
|
3787
|
+
if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
|
|
3788
|
+
refuse("IN_EXPRESSION");
|
|
3789
|
+
continue;
|
|
3790
|
+
}
|
|
3791
|
+
const rendered = candidate.render();
|
|
3792
|
+
if (rendered === null) continue;
|
|
3793
|
+
if ("refuse" in rendered) {
|
|
3794
|
+
refuse(rendered.refuse);
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const { loop, params } = rendered;
|
|
3798
|
+
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];
|
|
3799
|
+
if (!params || !reads || reads.length !== candidate.rows[0].length) {
|
|
3800
|
+
refuse("IN_EXPRESSION");
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
const elements = readerElements(ctx.content, loop, reads);
|
|
3804
|
+
const wraps = readerWraps(loop, reads);
|
|
3805
|
+
const hosts = [];
|
|
3806
|
+
let refusal = null;
|
|
3807
|
+
for (const read of reads) {
|
|
3808
|
+
const found = elements.get(read) ?? [];
|
|
3809
|
+
const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
|
|
3810
|
+
if (found.length === 1) hosts.push({ element: found[0] });
|
|
3811
|
+
else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
|
|
3812
|
+
else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
|
|
3813
|
+
}
|
|
3814
|
+
if (refusal !== null) {
|
|
3815
|
+
refuse(refusal);
|
|
3816
|
+
continue;
|
|
3817
|
+
}
|
|
3818
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3819
|
+
indexes.add(index);
|
|
3820
|
+
const splices = [];
|
|
3821
|
+
if (rule.loopKey) {
|
|
3822
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3823
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3824
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3825
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (params.index === null) {
|
|
3829
|
+
if (params.addIndexAt < 0) {
|
|
3830
|
+
refuse("IN_EXPRESSION");
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3834
|
+
}
|
|
3835
|
+
const templates = pathTemplates(paths, index);
|
|
3836
|
+
if (!templates) {
|
|
3837
|
+
refuse("IN_EXPRESSION");
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
|
|
3841
|
+
for (const [position, host] of hosts.entries()) {
|
|
3842
|
+
const kind = kindAt[position];
|
|
3843
|
+
const attributes = [
|
|
3844
|
+
rule.attr("data-bcms-field", `\`${templates[position]}\``),
|
|
3845
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3846
|
+
// A richtext value is html, so it goes into the dialect's html sink rather than being
|
|
3847
|
+
// printed as characters — the same rule one level up. @see bindDataLiterals
|
|
3848
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
|
|
3849
|
+
];
|
|
3850
|
+
if ("element" in host) {
|
|
3851
|
+
splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3852
|
+
if (kind === "richtext" && rule.richtext && host.element.inner) {
|
|
3853
|
+
splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
|
|
3854
|
+
}
|
|
3855
|
+
} else {
|
|
3856
|
+
const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
|
|
3857
|
+
splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
|
|
3858
|
+
}
|
|
3859
|
+
out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
|
|
3860
|
+
}
|
|
3861
|
+
const args = [
|
|
3862
|
+
ctx.snapshotName(mine[0].slug),
|
|
3863
|
+
list,
|
|
3864
|
+
ctx.content.slice(candidate.at.start, candidate.at.end),
|
|
3865
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3866
|
+
];
|
|
3867
|
+
splices.push({
|
|
3868
|
+
start: candidate.at.start,
|
|
3869
|
+
end: candidate.at.end,
|
|
3870
|
+
text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
|
|
3871
|
+
});
|
|
3872
|
+
out.splices.push(...splices);
|
|
3873
|
+
if (candidate.tuple) out.usesTuples = true;
|
|
3874
|
+
else out.usesList = true;
|
|
3875
|
+
out.slugs.push(...mine.map((member) => member.slug));
|
|
3876
|
+
out.bound.push(...mine.map((member) => member.key));
|
|
3877
|
+
out.notes.push(
|
|
3878
|
+
`${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.`
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function propCalls(content, expression, row, property) {
|
|
3883
|
+
const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
|
|
3884
|
+
const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
|
|
3885
|
+
const out = [];
|
|
3886
|
+
const visit = (node) => {
|
|
3887
|
+
if (node.type === "expr") {
|
|
3888
|
+
for (const child of node.children) visit(child);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
if (node.type !== "element" || node.raw) return;
|
|
3892
|
+
if (node.tag !== "" && !isIntrinsic(node.tag)) {
|
|
3893
|
+
for (const attr of node.attrs) {
|
|
3894
|
+
if (!attr.expressionRange) continue;
|
|
3895
|
+
const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
|
|
3896
|
+
if (spread.test(text)) out.push({ call: node, prop: property });
|
|
3897
|
+
else if (named.test(text)) out.push({ call: node, prop: attr.name });
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
for (const child of node.children) visit(child);
|
|
3901
|
+
};
|
|
3902
|
+
for (const child of expression.children) visit(child);
|
|
3521
3903
|
return out;
|
|
3522
3904
|
}
|
|
3523
3905
|
function leafRefusal(loop, row, property) {
|
|
3524
3906
|
const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
|
|
3525
3907
|
return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
|
|
3526
3908
|
}
|
|
3527
|
-
function allocateIndex(loop) {
|
|
3909
|
+
function allocateIndex(loop, taken) {
|
|
3528
3910
|
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}`;
|
|
3911
|
+
if (!used.has("i") && !taken.has("i")) return "i";
|
|
3912
|
+
for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
|
|
3531
3913
|
}
|
|
3532
3914
|
|
|
3533
3915
|
// src/resolve.ts
|
|
@@ -3609,6 +3991,13 @@ function isAliasShaped(value) {
|
|
|
3609
3991
|
function unreadableConfigs(sources) {
|
|
3610
3992
|
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3611
3993
|
}
|
|
3994
|
+
function extendedConfigs(sources) {
|
|
3995
|
+
return sources.filter((source) => {
|
|
3996
|
+
if (!isProjectConfig(source.path)) return false;
|
|
3997
|
+
const parsed = readJsonc(source.content);
|
|
3998
|
+
return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
|
|
3999
|
+
}).map((source) => source.path);
|
|
4000
|
+
}
|
|
3612
4001
|
function expand(specifier, aliases) {
|
|
3613
4002
|
const out = [];
|
|
3614
4003
|
for (const [pattern, targets] of aliases) {
|
|
@@ -3738,7 +4127,7 @@ function bindImages(ctx, targets) {
|
|
|
3738
4127
|
|
|
3739
4128
|
// src/helper.ts
|
|
3740
4129
|
import { createHash as createHash2 } from "crypto";
|
|
3741
|
-
var HELPER_VERSION =
|
|
4130
|
+
var HELPER_VERSION = 6;
|
|
3742
4131
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
3743
4132
|
var HELPER_PATH = Object.fromEntries(
|
|
3744
4133
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -3846,6 +4235,30 @@ export function bcmsRowsAs<T>(
|
|
|
3846
4235
|
});
|
|
3847
4236
|
}
|
|
3848
4237
|
|
|
4238
|
+
/**
|
|
4239
|
+
* A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
|
|
4240
|
+
*
|
|
4241
|
+
* \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
|
|
4242
|
+
* renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
|
|
4243
|
+
* an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
|
|
4244
|
+
* resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
|
|
4245
|
+
* editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
|
|
4246
|
+
* this out loud rather than leaving an editor to discover it.
|
|
4247
|
+
*/
|
|
4248
|
+
export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
|
|
4249
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4250
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4251
|
+
return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
/**
|
|
4255
|
+
* The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
|
|
4256
|
+
* same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
|
|
4257
|
+
*/
|
|
4258
|
+
export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
|
|
4259
|
+
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4260
|
+
}
|
|
4261
|
+
|
|
3849
4262
|
/** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
|
|
3850
4263
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
3851
4264
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
@@ -3937,19 +4350,22 @@ var HISTORICAL = {
|
|
|
3937
4350
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3938
4351
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3939
4352
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3940
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4353
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4354
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3941
4355
|
},
|
|
3942
4356
|
"src/lib/bcms-content.ts": {
|
|
3943
4357
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
3944
4358
|
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
3945
4359
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
3946
|
-
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af"
|
|
4360
|
+
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4361
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
3947
4362
|
},
|
|
3948
4363
|
"composables/bcms-content.ts": {
|
|
3949
4364
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3950
4365
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3951
4366
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3952
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4367
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4368
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3953
4369
|
}
|
|
3954
4370
|
};
|
|
3955
4371
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4484,8 +4900,8 @@ function declaringElements(roots, content) {
|
|
|
4484
4900
|
const name = attr.name.replace(/^:/, "");
|
|
4485
4901
|
if (name === "bcmsBindings") {
|
|
4486
4902
|
const own = ownBytes(node, content);
|
|
4487
|
-
for (const
|
|
4488
|
-
out.push({ scope: "page", path
|
|
4903
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
4904
|
+
out.push({ scope: "page", path, kind, own });
|
|
4489
4905
|
}
|
|
4490
4906
|
continue;
|
|
4491
4907
|
}
|
|
@@ -4515,7 +4931,7 @@ function declaringElements(roots, content) {
|
|
|
4515
4931
|
return out;
|
|
4516
4932
|
}
|
|
4517
4933
|
function helperLocals(parsed, dialect, file) {
|
|
4518
|
-
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [] };
|
|
4934
|
+
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
|
|
4519
4935
|
const helper = HELPER_PATH[dialect];
|
|
4520
4936
|
if (!helper) return out;
|
|
4521
4937
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
@@ -4525,6 +4941,8 @@ function helperLocals(parsed, dialect, file) {
|
|
|
4525
4941
|
if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
|
|
4526
4942
|
if (from.imported === "bcmsImage") out.bcmsImage.push(local);
|
|
4527
4943
|
if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
|
|
4944
|
+
if (from.imported === "bcmsList") out.bcmsList.push(local);
|
|
4945
|
+
if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
|
|
4528
4946
|
}
|
|
4529
4947
|
return out;
|
|
4530
4948
|
}
|
|
@@ -4597,6 +5015,9 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4597
5015
|
keyed = reads(locals.bcmsRowsAs);
|
|
4598
5016
|
rows = keyed || reads(locals.bcmsRows);
|
|
4599
5017
|
}
|
|
5018
|
+
const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
|
|
5019
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
|
|
5020
|
+
);
|
|
4600
5021
|
let cached = null;
|
|
4601
5022
|
const moduleRead = () => {
|
|
4602
5023
|
if (indexed || snapshot === null) return false;
|
|
@@ -4612,7 +5033,7 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4612
5033
|
return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
|
|
4613
5034
|
}
|
|
4614
5035
|
if (element.path !== shape) return false;
|
|
4615
|
-
return rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
5036
|
+
return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
4616
5037
|
});
|
|
4617
5038
|
}
|
|
4618
5039
|
|
|
@@ -4748,6 +5169,22 @@ function importLines(imports) {
|
|
|
4748
5169
|
|
|
4749
5170
|
// src/receipt.ts
|
|
4750
5171
|
var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
|
|
5172
|
+
var FormInvariantError = class extends Error {
|
|
5173
|
+
code = "FORM_INVARIANT";
|
|
5174
|
+
constructor(message) {
|
|
5175
|
+
super(message);
|
|
5176
|
+
this.name = "FormInvariantError";
|
|
5177
|
+
}
|
|
5178
|
+
};
|
|
5179
|
+
function assertForms(total, forms) {
|
|
5180
|
+
const sum = forms.wired + forms.alreadyWired + forms.pending.length;
|
|
5181
|
+
if (sum !== total) {
|
|
5182
|
+
throw new FormInvariantError(
|
|
5183
|
+
`The receipt does not account for every form: ${total} listed but ${forms.wired} wired + ${forms.alreadyWired} already wired + ${forms.pending.length} pending = ${sum}.`
|
|
5184
|
+
);
|
|
5185
|
+
}
|
|
5186
|
+
return forms;
|
|
5187
|
+
}
|
|
4751
5188
|
var ReceiptInvariantError = class extends Error {
|
|
4752
5189
|
code = "RECEIPT_INVARIANT";
|
|
4753
5190
|
constructor(message) {
|
|
@@ -5599,6 +6036,17 @@ function readBrief(value) {
|
|
|
5599
6036
|
}
|
|
5600
6037
|
}
|
|
5601
6038
|
}
|
|
6039
|
+
for (const [at, entry] of check.array(brief.forms ?? [], `the brief's "forms"`).entries()) {
|
|
6040
|
+
const where = `brief.forms[${at}]`;
|
|
6041
|
+
const form = check.record(entry, where);
|
|
6042
|
+
check.segment(form.id, `${where}.id`);
|
|
6043
|
+
check.string(form.name, `${where}.name`);
|
|
6044
|
+
check.string(form.submitUrl, `${where}.submitUrl`);
|
|
6045
|
+
check.optionalString(form.successMessage, `${where}.successMessage`);
|
|
6046
|
+
for (const [n, held] of check.array(form.fields, `${where}.fields`).entries()) {
|
|
6047
|
+
check.segment(check.record(held, `${where}.fields[${n}]`).key, `${where}.fields[${n}].key`);
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
5602
6050
|
return brief;
|
|
5603
6051
|
}
|
|
5604
6052
|
function readPlan(value) {
|
|
@@ -5645,6 +6093,236 @@ function readPlan(value) {
|
|
|
5645
6093
|
return plan2;
|
|
5646
6094
|
}
|
|
5647
6095
|
|
|
6096
|
+
// src/forms.ts
|
|
6097
|
+
import MagicString4 from "magic-string";
|
|
6098
|
+
var WIRABLE = ["html", "astro", "jsx"];
|
|
6099
|
+
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6100
|
+
var FORM_SCRIPT = `<script>
|
|
6101
|
+
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6102
|
+
f.addEventListener('submit',function(e){
|
|
6103
|
+
e.preventDefault();
|
|
6104
|
+
var msg=f.querySelector('.bcms-form-msg');
|
|
6105
|
+
var data={};
|
|
6106
|
+
// A checkboxes group posts one entry per checked box under the SAME name; the old
|
|
6107
|
+
// one-at-a-time assignment kept only the last, so "pick many" delivered one answer.
|
|
6108
|
+
// One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a
|
|
6109
|
+
// lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts
|
|
6110
|
+
// both an array and a comma string.
|
|
6111
|
+
new FormData(f).forEach(function(v,k){
|
|
6112
|
+
if(k==='cf-turnstile-response')return;
|
|
6113
|
+
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6114
|
+
else data[k]=v;
|
|
6115
|
+
});
|
|
6116
|
+
var body={data:data};
|
|
6117
|
+
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6118
|
+
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
6119
|
+
fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
6120
|
+
.then(function(r){return r.ok;})
|
|
6121
|
+
.then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})
|
|
6122
|
+
.catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});
|
|
6123
|
+
});
|
|
6124
|
+
});
|
|
6125
|
+
</script>`;
|
|
6126
|
+
var SCRIPT_BODY = FORM_SCRIPT.slice("<script>".length, -"</script>".length);
|
|
6127
|
+
function scriptFor(dialect) {
|
|
6128
|
+
if (dialect === "jsx") return `<script dangerouslySetInnerHTML={{ __html: \`${SCRIPT_BODY}\` }} />`;
|
|
6129
|
+
if (dialect === "astro") return `<script is:inline>${SCRIPT_BODY}</script>`;
|
|
6130
|
+
return FORM_SCRIPT;
|
|
6131
|
+
}
|
|
6132
|
+
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6133
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6134
|
+
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6135
|
+
function* elementsIn(nodes) {
|
|
6136
|
+
for (const node of nodes) {
|
|
6137
|
+
if (node.type === "expr") {
|
|
6138
|
+
yield* elementsIn(node.children);
|
|
6139
|
+
continue;
|
|
6140
|
+
}
|
|
6141
|
+
if (node.type !== "element") continue;
|
|
6142
|
+
yield node;
|
|
6143
|
+
yield* elementsIn(node.children);
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
var childElements = (node) => node.children.flatMap(
|
|
6147
|
+
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6148
|
+
);
|
|
6149
|
+
function candidateOf(file, dialect, form) {
|
|
6150
|
+
const controls = /* @__PURE__ */ new Map();
|
|
6151
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
6152
|
+
const all = [];
|
|
6153
|
+
const walk = (parent) => {
|
|
6154
|
+
const own = [];
|
|
6155
|
+
for (const child of childElements(parent)) {
|
|
6156
|
+
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6157
|
+
own.push(child);
|
|
6158
|
+
all.push(child);
|
|
6159
|
+
}
|
|
6160
|
+
walk(child);
|
|
6161
|
+
}
|
|
6162
|
+
if (own.length === 1 && parent !== form) wrapper.set(own[0], parent);
|
|
6163
|
+
};
|
|
6164
|
+
walk(form);
|
|
6165
|
+
for (const control of all) {
|
|
6166
|
+
const name = attrOf(control, "name")?.value;
|
|
6167
|
+
if (name && !controls.has(name)) controls.set(name, control);
|
|
6168
|
+
}
|
|
6169
|
+
for (const control of all) {
|
|
6170
|
+
const id = attrOf(control, "id")?.value;
|
|
6171
|
+
if (id && !controls.has(id)) controls.set(id, control);
|
|
6172
|
+
}
|
|
6173
|
+
return { file, dialect, el: form, controls, wrapper };
|
|
6174
|
+
}
|
|
6175
|
+
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6176
|
+
function wire(source, candidate, form) {
|
|
6177
|
+
const { el, dialect } = candidate;
|
|
6178
|
+
const inserts = [];
|
|
6179
|
+
const set = (name, value) => {
|
|
6180
|
+
const attr = attrOf(el, name);
|
|
6181
|
+
if (!attr) inserts.push(`${name}="${escapeAttr(value)}"`);
|
|
6182
|
+
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6183
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6184
|
+
};
|
|
6185
|
+
set("action", form.submitUrl);
|
|
6186
|
+
set("method", "post");
|
|
6187
|
+
inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
|
|
6188
|
+
if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(form.successMessage)}"`);
|
|
6189
|
+
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6190
|
+
for (const field of form.fields) {
|
|
6191
|
+
const control = candidate.controls.get(field.key);
|
|
6192
|
+
const host = candidate.wrapper.get(control) ?? control;
|
|
6193
|
+
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6194
|
+
const name = attrOf(control, "name");
|
|
6195
|
+
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6196
|
+
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
6197
|
+
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
if (el.inner && !hasMessage(el)) {
|
|
6201
|
+
const tail = /\s*$/.exec(source.original.slice(el.inner.start, el.inner.end))[0];
|
|
6202
|
+
source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
async function wireForms(briefIn, sources) {
|
|
6206
|
+
const brief = readBrief(briefIn);
|
|
6207
|
+
const forms = brief.forms ?? [];
|
|
6208
|
+
const candidates = [];
|
|
6209
|
+
const unreadable = [];
|
|
6210
|
+
for (const source of sources) {
|
|
6211
|
+
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6212
|
+
const dialect = dialectOf(source.path, source.content);
|
|
6213
|
+
if (!dialect || !WIRABLE.includes(dialect)) {
|
|
6214
|
+
unreadable.push({
|
|
6215
|
+
file: source.path,
|
|
6216
|
+
reason: "DIALECT_UNSUPPORTED",
|
|
6217
|
+
message: `${source.path} is ${dialect ?? "not a template"}.`
|
|
6218
|
+
});
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
const parsed = await parseFile4(dialect, source.content);
|
|
6222
|
+
if (parsed.error) {
|
|
6223
|
+
unreadable.push({
|
|
6224
|
+
file: source.path,
|
|
6225
|
+
reason: parsed.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR",
|
|
6226
|
+
message: parsed.error.message
|
|
6227
|
+
});
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
for (const el of elementsIn(parsed.roots)) {
|
|
6231
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6235
|
+
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6236
|
+
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6237
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
6238
|
+
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6239
|
+
const matched = [];
|
|
6240
|
+
const pending = [];
|
|
6241
|
+
const onSite = [];
|
|
6242
|
+
let alreadyWired = 0;
|
|
6243
|
+
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
6244
|
+
for (const form of forms) {
|
|
6245
|
+
const list = options.get(form.id) ?? [];
|
|
6246
|
+
if (list.length > 1) {
|
|
6247
|
+
pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
6251
|
+
pend2(form, "FORM_AMBIGUOUS", list[0].file, "more than one CMS form's fields are posted by this <form>.");
|
|
6252
|
+
continue;
|
|
6253
|
+
}
|
|
6254
|
+
if (list.length === 0) {
|
|
6255
|
+
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6256
|
+
if (best) {
|
|
6257
|
+
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6258
|
+
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6259
|
+
} else if (unreadable[0]) {
|
|
6260
|
+
pend2(form, unreadable[0].reason, unreadable[0].file, unreadable[0].message);
|
|
6261
|
+
} else {
|
|
6262
|
+
pend2(form, "FORM_NOT_IN_SOURCE", void 0, `no <form> posts ${form.fields.map((f) => f.key).join(", ") || "any field of this form"}.`);
|
|
6263
|
+
}
|
|
6264
|
+
continue;
|
|
6265
|
+
}
|
|
6266
|
+
const candidate = list[0];
|
|
6267
|
+
if (attrOf(candidate.el, "action")?.expression) {
|
|
6268
|
+
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6269
|
+
continue;
|
|
6270
|
+
}
|
|
6271
|
+
const misnamed = form.fields.find((field) => {
|
|
6272
|
+
const attr = attrOf(candidate.controls.get(field.key), "name");
|
|
6273
|
+
return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
|
|
6274
|
+
});
|
|
6275
|
+
if (misnamed) {
|
|
6276
|
+
pend2(form, "FIELD_UNMATCHED", candidate.file, `the control for ${misnamed.key} posts under an expression, so its name cannot be set to the CMS key.`);
|
|
6277
|
+
continue;
|
|
6278
|
+
}
|
|
6279
|
+
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6280
|
+
alreadyWired++;
|
|
6281
|
+
onSite.push(form);
|
|
6282
|
+
continue;
|
|
6283
|
+
}
|
|
6284
|
+
matched.push({ form, candidate });
|
|
6285
|
+
onSite.push(form);
|
|
6286
|
+
}
|
|
6287
|
+
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6288
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6289
|
+
for (const m of matched) byFile.set(m.candidate.file, [...byFile.get(m.candidate.file) ?? [], m]);
|
|
6290
|
+
const files = [];
|
|
6291
|
+
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6292
|
+
const content = contents.get(path);
|
|
6293
|
+
const source = new MagicString4(content);
|
|
6294
|
+
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6295
|
+
if (!content.includes("form[data-bcms-form]")) {
|
|
6296
|
+
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6297
|
+
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6298
|
+
source.appendRight(last.candidate.el.end, `
|
|
6299
|
+
${indent}${scriptFor(last.candidate.dialect)}`);
|
|
6300
|
+
}
|
|
6301
|
+
files.push({ path, operation: "modify", content: source.toString() });
|
|
6302
|
+
}
|
|
6303
|
+
const notes = onSite.filter((f) => f.status === "draft").map((f) => `publish form ${f.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
|
|
6304
|
+
return {
|
|
6305
|
+
files,
|
|
6306
|
+
receipt: {
|
|
6307
|
+
briefDigest: briefDigest(brief),
|
|
6308
|
+
// This lane binds no brief PATH, and says so rather than leaving the arithmetic to a reader:
|
|
6309
|
+
// a receipt claiming zero of zero paths is exactly what a forms-only run did.
|
|
6310
|
+
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6311
|
+
occurrences: { located: 0, rewritten: 0 },
|
|
6312
|
+
files: [],
|
|
6313
|
+
helpers: [],
|
|
6314
|
+
helperUpgraded: false,
|
|
6315
|
+
dynamicBindings: [],
|
|
6316
|
+
forms: assertForms(forms.length, {
|
|
6317
|
+
wired: matched.length,
|
|
6318
|
+
alreadyWired,
|
|
6319
|
+
pending,
|
|
6320
|
+
...notes.length > 0 ? { notes } : {}
|
|
6321
|
+
})
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
|
|
5648
6326
|
// src/scan.ts
|
|
5649
6327
|
var SKIP_DIRS = [
|
|
5650
6328
|
"node_modules/",
|
|
@@ -5702,6 +6380,7 @@ function isSourceCandidate(path) {
|
|
|
5702
6380
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
5703
6381
|
return false;
|
|
5704
6382
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6383
|
+
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
5705
6384
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
5706
6385
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
5707
6386
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
@@ -6012,7 +6691,7 @@ function moduleScope(parsed, content) {
|
|
|
6012
6691
|
}
|
|
6013
6692
|
|
|
6014
6693
|
// src/componentize.ts
|
|
6015
|
-
import
|
|
6694
|
+
import MagicString5 from "magic-string";
|
|
6016
6695
|
var SectionInvariantError = class extends Error {
|
|
6017
6696
|
code = "SECTION_INVARIANT";
|
|
6018
6697
|
constructor(message) {
|
|
@@ -6938,7 +7617,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
|
|
|
6938
7617
|
` };
|
|
6939
7618
|
}
|
|
6940
7619
|
function sectionMarkup(content, root, fieldName) {
|
|
6941
|
-
const slice = new
|
|
7620
|
+
const slice = new MagicString5(content.slice(root.start, root.end));
|
|
6942
7621
|
const at = (offset) => offset - root.start;
|
|
6943
7622
|
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
6944
7623
|
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
@@ -7137,7 +7816,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
|
7137
7816
|
);
|
|
7138
7817
|
continue;
|
|
7139
7818
|
}
|
|
7140
|
-
const source = new
|
|
7819
|
+
const source = new MagicString5(content);
|
|
7141
7820
|
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
7142
7821
|
if (duplicates.length > 0) {
|
|
7143
7822
|
refused.set(refusalKey(dialect, slug), `${entry.path} has ${duplicates.join(", ")} carrying more than one data-bcms-field, so there is no single address to rewrite.`);
|
|
@@ -7414,7 +8093,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
7414
8093
|
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
7415
8094
|
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
7416
8095
|
const local = names.get("Sections");
|
|
7417
|
-
const source = new
|
|
8096
|
+
const source = new MagicString5(content);
|
|
7418
8097
|
const first = placeable[0].root;
|
|
7419
8098
|
const last = placeable[placeable.length - 1].root;
|
|
7420
8099
|
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
@@ -7434,7 +8113,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
|
|
|
7434
8113
|
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
7435
8114
|
]);
|
|
7436
8115
|
const snapshot = names.get("pageSnapshot");
|
|
7437
|
-
const source = new
|
|
8116
|
+
const source = new MagicString5(content);
|
|
7438
8117
|
for (const resolved of placeable) {
|
|
7439
8118
|
const component = written.get(resolved.slug);
|
|
7440
8119
|
const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
|
|
@@ -7758,6 +8437,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7758
8437
|
const receiptFiles = [];
|
|
7759
8438
|
const dialects = /* @__PURE__ */ new Set();
|
|
7760
8439
|
const dynamicBindings = [];
|
|
8440
|
+
const laneNotes = [];
|
|
7761
8441
|
const snapshotSlugs = /* @__PURE__ */ new Set();
|
|
7762
8442
|
let occurrencesLocated = 0;
|
|
7763
8443
|
let occurrencesRewritten = 0;
|
|
@@ -7783,6 +8463,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7783
8463
|
outFiles.push({ path: file, operation: "modify", content: result.content });
|
|
7784
8464
|
receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
|
|
7785
8465
|
dynamicBindings.push(...result.dynamic);
|
|
8466
|
+
laneNotes.push(...result.notes);
|
|
7786
8467
|
for (const slug of result.snapshots) snapshotSlugs.add(slug);
|
|
7787
8468
|
}
|
|
7788
8469
|
}
|
|
@@ -7819,7 +8500,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7819
8500
|
} else rewritten++;
|
|
7820
8501
|
}
|
|
7821
8502
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7822
|
-
const source = new
|
|
8503
|
+
const source = new MagicString6(contents.get(file));
|
|
7823
8504
|
const seen = /* @__PURE__ */ new Set();
|
|
7824
8505
|
for (const splice of splices) {
|
|
7825
8506
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -7879,13 +8560,25 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7879
8560
|
helperUpgraded,
|
|
7880
8561
|
dynamicBindings,
|
|
7881
8562
|
canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
|
|
7882
|
-
notes:
|
|
7883
|
-
(
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
8563
|
+
notes: [
|
|
8564
|
+
...unreadableConfigs(sources).map(
|
|
8565
|
+
(file) => ({
|
|
8566
|
+
code: "TSCONFIG_UNREADABLE",
|
|
8567
|
+
file,
|
|
8568
|
+
message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
8569
|
+
})
|
|
8570
|
+
),
|
|
8571
|
+
// An absent alias table looks exactly like a project that has none, and the two have
|
|
8572
|
+
// opposite consequences for every component reached through one. @see extendedConfigs
|
|
8573
|
+
...extendedConfigs(sources).map(
|
|
8574
|
+
(file) => ({
|
|
8575
|
+
code: "TSCONFIG_EXTENDS_ONLY",
|
|
8576
|
+
file,
|
|
8577
|
+
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.`
|
|
8578
|
+
})
|
|
8579
|
+
),
|
|
8580
|
+
...laneNotes
|
|
8581
|
+
]
|
|
7889
8582
|
});
|
|
7890
8583
|
return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
|
|
7891
8584
|
}
|
|
@@ -7907,7 +8600,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7907
8600
|
dialect: dialectOf(file, content),
|
|
7908
8601
|
dynamic: [],
|
|
7909
8602
|
componentEdits: [],
|
|
7910
|
-
snapshots: []
|
|
8603
|
+
snapshots: [],
|
|
8604
|
+
notes: []
|
|
7911
8605
|
};
|
|
7912
8606
|
if (pendingTargets.length === 0) return empty;
|
|
7913
8607
|
if (!dialect) {
|
|
@@ -7968,7 +8662,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7968
8662
|
dialect: null,
|
|
7969
8663
|
dynamic: [],
|
|
7970
8664
|
componentEdits: [],
|
|
7971
|
-
snapshots: []
|
|
8665
|
+
snapshots: [],
|
|
8666
|
+
notes: []
|
|
7972
8667
|
};
|
|
7973
8668
|
}
|
|
7974
8669
|
if (rescued.reason === "unverifiable") {
|
|
@@ -8038,6 +8733,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8038
8733
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
8039
8734
|
{ key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
|
|
8040
8735
|
{ key: "helper:bcmsImage", name: "bcmsImage" },
|
|
8736
|
+
{ key: "helper:bcmsList", name: "bcmsList" },
|
|
8737
|
+
{ key: "helper:bcmsTuples", name: "bcmsTuples" },
|
|
8041
8738
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
8042
8739
|
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
8043
8740
|
].filter((entry) => !preset.has(entry.key));
|
|
@@ -8229,15 +8926,23 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8229
8926
|
const collided = new Set(overlapping(kept).map((r) => r.key));
|
|
8230
8927
|
const applied = kept.filter((r) => !collided.has(r.key));
|
|
8231
8928
|
for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
|
|
8232
|
-
const data = bindDataLiterals(
|
|
8929
|
+
const data = await bindDataLiterals(
|
|
8233
8930
|
{
|
|
8234
8931
|
content,
|
|
8235
8932
|
parsed,
|
|
8236
8933
|
dialect: writing,
|
|
8237
8934
|
rowsAs: helperName("bcmsRowsAs"),
|
|
8935
|
+
list: helperName("bcmsList"),
|
|
8936
|
+
tuples: helperName("bcmsTuples"),
|
|
8238
8937
|
read: helperName("bcms"),
|
|
8239
8938
|
snapshotName,
|
|
8240
|
-
slug: params?.slug ?? null
|
|
8939
|
+
slug: params?.slug ?? null,
|
|
8940
|
+
// The row's leaf travels to another file exactly as a scalar prop does, and by the same
|
|
8941
|
+
// machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
|
|
8942
|
+
drill: async (component, prop, kind) => {
|
|
8943
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
8944
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
8945
|
+
}
|
|
8241
8946
|
},
|
|
8242
8947
|
targets.filter((t) => {
|
|
8243
8948
|
const outcome = outcomes.get(t.key);
|
|
@@ -8255,6 +8960,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8255
8960
|
for (const declaration2 of data.declarations) {
|
|
8256
8961
|
dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
|
|
8257
8962
|
}
|
|
8963
|
+
componentEdits.push(...data.componentEdits);
|
|
8964
|
+
for (const drilledProp of data.props) {
|
|
8965
|
+
dynamic.push({
|
|
8966
|
+
file: drilledProp.file,
|
|
8967
|
+
prop: drilledProp.prop,
|
|
8968
|
+
path: drilledProp.path,
|
|
8969
|
+
callSites: [{ file, literal: drilledProp.literal }]
|
|
8970
|
+
});
|
|
8971
|
+
}
|
|
8258
8972
|
const images = bindImages(
|
|
8259
8973
|
{ content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
|
|
8260
8974
|
targets.filter((t) => {
|
|
@@ -8266,7 +8980,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8266
8980
|
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8267
8981
|
const lanes = [...data.splices, ...images.splices];
|
|
8268
8982
|
if (applied.length === 0 && lanes.length === 0) {
|
|
8269
|
-
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
|
|
8983
|
+
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|
|
8270
8984
|
}
|
|
8271
8985
|
const readSlugs = [
|
|
8272
8986
|
.../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
|
|
@@ -8293,6 +9007,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8293
9007
|
] : [],
|
|
8294
9008
|
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
|
|
8295
9009
|
...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
|
|
9010
|
+
...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
|
|
9011
|
+
// `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
|
|
9012
|
+
// page imports only the one it calls — an unused import is dead code in someone else's repo.
|
|
9013
|
+
...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
|
|
8296
9014
|
...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
|
|
8297
9015
|
...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
|
|
8298
9016
|
].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
|
|
@@ -8307,7 +9025,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8307
9025
|
dialect: writing,
|
|
8308
9026
|
dynamic,
|
|
8309
9027
|
componentEdits,
|
|
8310
|
-
snapshots: rule.text ? readSlugs : []
|
|
9028
|
+
snapshots: rule.text ? readSlugs : [],
|
|
9029
|
+
// The lane reports one sentence per list it bound; the FILE is added here, where it is known.
|
|
9030
|
+
notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
|
|
8311
9031
|
};
|
|
8312
9032
|
}
|
|
8313
9033
|
function mergeInto(content, site) {
|
|
@@ -8488,6 +9208,8 @@ export {
|
|
|
8488
9208
|
CONTENT_DIR,
|
|
8489
9209
|
ConvertError,
|
|
8490
9210
|
DIALECT_RULES,
|
|
9211
|
+
FORM_SCRIPT,
|
|
9212
|
+
FormInvariantError,
|
|
8491
9213
|
HELPER_PATH,
|
|
8492
9214
|
HELPER_VERSION,
|
|
8493
9215
|
PARSE_FILE,
|
|
@@ -8503,6 +9225,7 @@ export {
|
|
|
8503
9225
|
SectionInvariantError,
|
|
8504
9226
|
ValidationError,
|
|
8505
9227
|
aliasesFrom,
|
|
9228
|
+
assertForms,
|
|
8506
9229
|
assertReceipt,
|
|
8507
9230
|
assertSections,
|
|
8508
9231
|
assetCandidates,
|
|
@@ -8520,6 +9243,7 @@ export {
|
|
|
8520
9243
|
declaringElements,
|
|
8521
9244
|
dialectOf,
|
|
8522
9245
|
exportedFunction,
|
|
9246
|
+
extendedConfigs,
|
|
8523
9247
|
findSites,
|
|
8524
9248
|
findSitesTolerant,
|
|
8525
9249
|
flat,
|
|
@@ -8544,6 +9268,7 @@ export {
|
|
|
8544
9268
|
routeOfFile,
|
|
8545
9269
|
stripTags,
|
|
8546
9270
|
unreadableConfigs,
|
|
8547
|
-
walkAst
|
|
9271
|
+
walkAst,
|
|
9272
|
+
wireForms
|
|
8548
9273
|
};
|
|
8549
9274
|
//# sourceMappingURL=index.js.map
|