@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/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
|
5
5
|
import { dirname, join as join2, relative, resolve, sep } from "path";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
import
|
|
8
|
+
import MagicString6 from "magic-string";
|
|
9
9
|
|
|
10
10
|
// src/brief.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -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)));
|
|
@@ -2560,7 +2563,16 @@ var IMAGE_SOURCES = {
|
|
|
2560
2563
|
};
|
|
2561
2564
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2562
2565
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2563
|
-
var BCMS_ATTRS = [
|
|
2566
|
+
var BCMS_ATTRS = [
|
|
2567
|
+
"data-bcms-field",
|
|
2568
|
+
"data-bcms-kind",
|
|
2569
|
+
"data-bcms-props",
|
|
2570
|
+
"data-bcms-layout-field",
|
|
2571
|
+
// The forms lane's two. Listed here so `collectSites` never matches a brief literal against a
|
|
2572
|
+
// marker this package itself wrote — a form id or a field key is not the page's copy.
|
|
2573
|
+
"data-bcms-form",
|
|
2574
|
+
"data-bcms-form-field"
|
|
2575
|
+
];
|
|
2564
2576
|
var attrInsertAfter = (attrs, start, tag) => attrs.length > 0 ? Math.max(...attrs.map((a) => a.range.end)) : start + 1 + tag.length;
|
|
2565
2577
|
function openTagEnd(content, start) {
|
|
2566
2578
|
let quote = null;
|
|
@@ -3029,7 +3041,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3029
3041
|
let receiver = null;
|
|
3030
3042
|
let rest = null;
|
|
3031
3043
|
let hasBindings = false;
|
|
3032
|
-
let
|
|
3044
|
+
let destructured2 = false;
|
|
3033
3045
|
for (const node of component ? [component] : []) {
|
|
3034
3046
|
const params = node.params ?? [];
|
|
3035
3047
|
const first = params[0];
|
|
@@ -3055,7 +3067,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3055
3067
|
if (key !== propName) continue;
|
|
3056
3068
|
const value = property.value;
|
|
3057
3069
|
if (value?.type === "Identifier") names.add(String(value.name));
|
|
3058
|
-
|
|
3070
|
+
destructured2 = true;
|
|
3059
3071
|
receivable = true;
|
|
3060
3072
|
receive = patternClose(content, first, offset);
|
|
3061
3073
|
}
|
|
@@ -3065,7 +3077,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3065
3077
|
viaProps = true;
|
|
3066
3078
|
}
|
|
3067
3079
|
}
|
|
3068
|
-
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
|
|
3080
|
+
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
|
|
3069
3081
|
}
|
|
3070
3082
|
function astroReception(ast, propName, offset, content) {
|
|
3071
3083
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3248,8 +3260,7 @@ function splitGroup(path) {
|
|
|
3248
3260
|
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
3249
3261
|
}
|
|
3250
3262
|
function moduleOf(parsed) {
|
|
3251
|
-
|
|
3252
|
-
return { ast: parsed.ast, offset: parsed.script?.offset ?? 0 };
|
|
3263
|
+
return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
|
|
3253
3264
|
}
|
|
3254
3265
|
function arrayLiterals(ast) {
|
|
3255
3266
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3262,6 +3273,19 @@ function arrayLiterals(ast) {
|
|
|
3262
3273
|
});
|
|
3263
3274
|
return out;
|
|
3264
3275
|
}
|
|
3276
|
+
function exportedNames(ast) {
|
|
3277
|
+
const out = /* @__PURE__ */ new Set();
|
|
3278
|
+
walkAst(ast, (node) => {
|
|
3279
|
+
if (node.type !== "ExportNamedDeclaration") return;
|
|
3280
|
+
const declaration2 = node.declaration;
|
|
3281
|
+
if (declaration2?.type !== "VariableDeclaration") return;
|
|
3282
|
+
for (const declarator of declaration2.declarations ?? []) {
|
|
3283
|
+
const id = declarator.id;
|
|
3284
|
+
if (id?.type === "Identifier") out.add(String(id.name));
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
3287
|
+
return out;
|
|
3288
|
+
}
|
|
3265
3289
|
function propertyFor(row, literal) {
|
|
3266
3290
|
if (row?.type !== "ObjectExpression") return null;
|
|
3267
3291
|
for (const property of row.properties ?? []) {
|
|
@@ -3286,21 +3310,36 @@ function iterations(roots, identifier2) {
|
|
|
3286
3310
|
for (const root of roots) visit(root);
|
|
3287
3311
|
return out;
|
|
3288
3312
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
);
|
|
3293
|
-
const match = expression.value.match(pattern);
|
|
3313
|
+
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*=>`;
|
|
3314
|
+
function paramsIn(text, pattern, base) {
|
|
3315
|
+
const match = text.match(pattern);
|
|
3294
3316
|
if (!match || match.index === void 0) return null;
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
const within = match[0].lastIndexOf(
|
|
3317
|
+
if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
|
|
3318
|
+
const bound = match[1] ?? match[2];
|
|
3319
|
+
const within = match[0].lastIndexOf(bound);
|
|
3298
3320
|
return {
|
|
3299
|
-
row
|
|
3300
|
-
|
|
3301
|
-
|
|
3321
|
+
// One of the two, never both: a row is either named or taken apart. @see bindLists for the
|
|
3322
|
+
// second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
|
|
3323
|
+
row: match[1] ?? null,
|
|
3324
|
+
names: match[2] === void 0 ? null : destructured(match[2]),
|
|
3325
|
+
index: match[4] ?? null,
|
|
3326
|
+
addIndexAt: base + match.index + within + bound.length
|
|
3302
3327
|
};
|
|
3303
3328
|
}
|
|
3329
|
+
function callbackParams(expression, identifier2) {
|
|
3330
|
+
return paramsIn(
|
|
3331
|
+
expression.value,
|
|
3332
|
+
new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
|
|
3333
|
+
expression.range.start
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
function mapParams(content, at, until) {
|
|
3337
|
+
return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
|
|
3338
|
+
}
|
|
3339
|
+
function destructured(pattern) {
|
|
3340
|
+
const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
|
|
3341
|
+
return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
|
|
3342
|
+
}
|
|
3304
3343
|
function renders(content, node, row) {
|
|
3305
3344
|
if (!node.inner) return null;
|
|
3306
3345
|
const inner = content.slice(node.inner.start, node.inner.end).trim();
|
|
@@ -3388,11 +3427,11 @@ function bindScalar(ctx, member, literals, offset) {
|
|
|
3388
3427
|
}
|
|
3389
3428
|
return { splices, refusal: null };
|
|
3390
3429
|
}
|
|
3391
|
-
function bindDataLiterals(ctx, members) {
|
|
3392
|
-
const out = { splices: [], usesRead: false, usesRows: false, slugs: [], bound: [], declarations: [], refused: [] };
|
|
3430
|
+
async function bindDataLiterals(ctx, members) {
|
|
3431
|
+
const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
|
|
3393
3432
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
3394
3433
|
const module = moduleOf(ctx.parsed);
|
|
3395
|
-
if (!
|
|
3434
|
+
if (!rule.text) return out;
|
|
3396
3435
|
const groups = /* @__PURE__ */ new Map();
|
|
3397
3436
|
const scalars = [];
|
|
3398
3437
|
for (const member of members) {
|
|
@@ -3405,6 +3444,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3405
3444
|
const key = `${member.slug}\0${split.array}`;
|
|
3406
3445
|
groups.set(key, [...groups.get(key) ?? [], held]);
|
|
3407
3446
|
}
|
|
3447
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
3408
3448
|
const strings = stringLiterals(module.ast);
|
|
3409
3449
|
for (const member of scalars) {
|
|
3410
3450
|
const scalar = bindScalar(ctx, member, strings, module.offset);
|
|
@@ -3415,7 +3455,6 @@ function bindDataLiterals(ctx, members) {
|
|
|
3415
3455
|
out.slugs.push(member.slug);
|
|
3416
3456
|
out.bound.push(member.key);
|
|
3417
3457
|
}
|
|
3418
|
-
if (groups.size === 0) return out;
|
|
3419
3458
|
const literals = arrayLiterals(module.ast);
|
|
3420
3459
|
for (const all of groups.values()) {
|
|
3421
3460
|
const array = all[0].array;
|
|
@@ -3456,23 +3495,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3456
3495
|
}
|
|
3457
3496
|
const loop = loops[0];
|
|
3458
3497
|
const params = callbackParams(loop, identifier2);
|
|
3459
|
-
if (!params) {
|
|
3498
|
+
if (!params || params.row === null) {
|
|
3460
3499
|
refuse("IN_EXPRESSION");
|
|
3461
3500
|
continue;
|
|
3462
3501
|
}
|
|
3463
|
-
const
|
|
3502
|
+
const rowName = params.row;
|
|
3503
|
+
const elements = leafElements(ctx.content, loop, rowName);
|
|
3464
3504
|
const placed = /* @__PURE__ */ new Map();
|
|
3505
|
+
const passed = [];
|
|
3465
3506
|
let refusal = null;
|
|
3466
3507
|
for (const property of keys.keys()) {
|
|
3467
3508
|
const found = elements.get(property) ?? [];
|
|
3468
|
-
if (found.length === 1)
|
|
3469
|
-
|
|
3509
|
+
if (found.length === 1) {
|
|
3510
|
+
placed.set(property, found[0]);
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
if (found.length > 1) {
|
|
3514
|
+
refusal = "REPEATER_AMBIGUOUS";
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
const calls = propCalls(ctx.content, loop, rowName, property);
|
|
3518
|
+
if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
|
|
3519
|
+
else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
|
|
3520
|
+
else refusal = leafRefusal(loop.value, rowName, property);
|
|
3470
3521
|
}
|
|
3471
3522
|
if (refusal !== null) {
|
|
3472
3523
|
refuse(refusal);
|
|
3473
3524
|
continue;
|
|
3474
3525
|
}
|
|
3475
|
-
const index = params.index ?? allocateIndex(loop.value);
|
|
3526
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3527
|
+
indexes.add(index);
|
|
3476
3528
|
const splices = [];
|
|
3477
3529
|
if (rule.loopKey) {
|
|
3478
3530
|
const rows = loop.children.filter((child) => child.type === "element");
|
|
@@ -3488,6 +3540,36 @@ function bindDataLiterals(ctx, members) {
|
|
|
3488
3540
|
}
|
|
3489
3541
|
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3490
3542
|
}
|
|
3543
|
+
const drilled = [];
|
|
3544
|
+
const edits = [];
|
|
3545
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3546
|
+
for (const entry of passed) {
|
|
3547
|
+
const leaf = keys.get(entry.property);
|
|
3548
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3549
|
+
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);
|
|
3550
|
+
if (!outcome.ok) {
|
|
3551
|
+
refusal = outcome.reason;
|
|
3552
|
+
break;
|
|
3553
|
+
}
|
|
3554
|
+
edits.push(...outcome.edits);
|
|
3555
|
+
bindings.set(entry.call.attrInsertAt, [
|
|
3556
|
+
...bindings.get(entry.call.attrInsertAt) ?? [],
|
|
3557
|
+
`${entry.prop}: \`${literal}\``
|
|
3558
|
+
]);
|
|
3559
|
+
drilled.push({
|
|
3560
|
+
file: outcome.edits[0].file,
|
|
3561
|
+
prop: entry.prop,
|
|
3562
|
+
path: shapeOfPath(literal),
|
|
3563
|
+
literal: `${entry.prop}: \`${literal}\``
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
if (refusal !== null) {
|
|
3567
|
+
refuse(refusal);
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
for (const [at2, entries] of bindings) {
|
|
3571
|
+
splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
|
|
3572
|
+
}
|
|
3491
3573
|
for (const [property, element] of placed) {
|
|
3492
3574
|
const leaf = keys.get(property);
|
|
3493
3575
|
const literal = `${array}[\${${index}}].${leaf}`;
|
|
@@ -3497,7 +3579,7 @@ function bindDataLiterals(ctx, members) {
|
|
|
3497
3579
|
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3498
3580
|
// A richtext row value is html, so it goes into the dialect's html sink rather than being
|
|
3499
3581
|
// printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
|
|
3500
|
-
...kind === "richtext" && rule.richtext ? [rule.richtext(`${
|
|
3582
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
|
|
3501
3583
|
];
|
|
3502
3584
|
splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3503
3585
|
if (kind === "richtext" && rule.richtext && element.inner) {
|
|
@@ -3517,19 +3599,319 @@ function bindDataLiterals(ctx, members) {
|
|
|
3517
3599
|
splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
|
|
3518
3600
|
out.splices.push(...splices);
|
|
3519
3601
|
out.usesRows = true;
|
|
3602
|
+
out.componentEdits.push(...edits);
|
|
3603
|
+
out.props.push(...drilled);
|
|
3520
3604
|
out.slugs.push(...group.map((member) => member.slug));
|
|
3521
3605
|
out.bound.push(...group.map((member) => member.key));
|
|
3522
3606
|
}
|
|
3607
|
+
const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
|
|
3608
|
+
bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
|
|
3609
|
+
return out;
|
|
3610
|
+
}
|
|
3611
|
+
function literalTable(init) {
|
|
3612
|
+
const elements = (init.elements ?? []).filter((e) => e !== null);
|
|
3613
|
+
const first = elements[0];
|
|
3614
|
+
if (!first || elements.length !== (init.elements ?? []).length) return null;
|
|
3615
|
+
if (elements.some((element) => element.type !== first.type)) return null;
|
|
3616
|
+
if (first.type === "StringLiteral") {
|
|
3617
|
+
return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
|
|
3618
|
+
}
|
|
3619
|
+
if (first.type !== "ArrayExpression") return null;
|
|
3620
|
+
const rows = [];
|
|
3621
|
+
for (const element of elements) {
|
|
3622
|
+
const inner = (element.elements ?? []).filter((x) => x !== null);
|
|
3623
|
+
if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
|
|
3624
|
+
if (inner.length !== (element.elements ?? []).length) return null;
|
|
3625
|
+
rows.push(inner.map((x) => norm(String(x.value))));
|
|
3626
|
+
}
|
|
3627
|
+
if (new Set(rows.map((row) => row.length)).size !== 1) return null;
|
|
3628
|
+
return { tuple: true, rows };
|
|
3629
|
+
}
|
|
3630
|
+
function openingBracket(text, closeAt) {
|
|
3631
|
+
let depth = 0;
|
|
3632
|
+
for (let at = closeAt; at >= 0; at--) {
|
|
3633
|
+
if (text[at] === "]") depth++;
|
|
3634
|
+
else if (text[at] === "[") {
|
|
3635
|
+
depth--;
|
|
3636
|
+
if (depth === 0) return at;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return -1;
|
|
3640
|
+
}
|
|
3641
|
+
function arrayLiteral(source) {
|
|
3642
|
+
try {
|
|
3643
|
+
const ast = parseProgram(`(${source})`);
|
|
3644
|
+
const expression = ast.program.body[0]?.expression;
|
|
3645
|
+
if (!expression || expression.type !== "ArrayExpression") return null;
|
|
3646
|
+
return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
|
|
3647
|
+
} catch {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
function inlineTables(ctx) {
|
|
3652
|
+
const out = /* @__PURE__ */ new Map();
|
|
3653
|
+
const visit = (node) => {
|
|
3654
|
+
if (node.type === "element") {
|
|
3655
|
+
if (!node.raw) for (const child of node.children) visit(child);
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
if (node.type !== "expr") return;
|
|
3659
|
+
for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
|
|
3660
|
+
const closeAt = match.index;
|
|
3661
|
+
const openAt = openingBracket(node.value, closeAt);
|
|
3662
|
+
if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
|
|
3663
|
+
const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
|
|
3664
|
+
const table = init ? literalTable(init) : null;
|
|
3665
|
+
if (!table) continue;
|
|
3666
|
+
const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
|
|
3667
|
+
out.set(at.start, {
|
|
3668
|
+
...table,
|
|
3669
|
+
at,
|
|
3670
|
+
label: "an inline array literal",
|
|
3671
|
+
render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
for (const child of node.children) visit(child);
|
|
3675
|
+
};
|
|
3676
|
+
for (const root of ctx.parsed.roots) visit(root);
|
|
3677
|
+
return [...out.values()];
|
|
3678
|
+
}
|
|
3679
|
+
function readerElements(content, expression, reads) {
|
|
3680
|
+
const wanted = new Set(reads);
|
|
3681
|
+
const out = /* @__PURE__ */ new Map();
|
|
3682
|
+
const visit = (node) => {
|
|
3683
|
+
if (node.type === "expr") {
|
|
3684
|
+
for (const child of node.children) visit(child);
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (node.type !== "element" || node.raw) return;
|
|
3688
|
+
const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
|
|
3689
|
+
const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3690
|
+
if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
|
|
3691
|
+
for (const child of node.children) visit(child);
|
|
3692
|
+
};
|
|
3693
|
+
for (const child of expression.children) visit(child);
|
|
3694
|
+
return out;
|
|
3695
|
+
}
|
|
3696
|
+
function readerWraps(expression, reads) {
|
|
3697
|
+
const wanted = new Set(reads);
|
|
3698
|
+
const out = /* @__PURE__ */ new Map();
|
|
3699
|
+
const visit = (node, parent) => {
|
|
3700
|
+
if (node.type === "expr") {
|
|
3701
|
+
const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3702
|
+
const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
|
|
3703
|
+
if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
|
|
3704
|
+
for (const child of node.children) visit(child, parent);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (node.type !== "element" || node.raw) return;
|
|
3708
|
+
for (const child of node.children) visit(child, node);
|
|
3709
|
+
};
|
|
3710
|
+
for (const child of expression.children) visit(child, null);
|
|
3711
|
+
return out;
|
|
3712
|
+
}
|
|
3713
|
+
function pathTemplates(paths, index) {
|
|
3714
|
+
const out = [];
|
|
3715
|
+
for (let position = 0; position < paths[0].length; position++) {
|
|
3716
|
+
const split = paths.map((row) => splitGroup(row[position]));
|
|
3717
|
+
const first = split[0];
|
|
3718
|
+
if (!first) return null;
|
|
3719
|
+
if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3722
|
+
out.push(`${first.array}[\${${index}}].${first.leaf}`);
|
|
3723
|
+
}
|
|
3724
|
+
return out;
|
|
3725
|
+
}
|
|
3726
|
+
function bindLists(ctx, members, module, out, indexes) {
|
|
3727
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3728
|
+
const escapes = exportedNames(module.ast);
|
|
3729
|
+
const candidates = [];
|
|
3730
|
+
for (const [name, init] of arrayLiterals(module.ast)) {
|
|
3731
|
+
const table = literalTable(init);
|
|
3732
|
+
if (!table) continue;
|
|
3733
|
+
candidates.push({
|
|
3734
|
+
...table,
|
|
3735
|
+
at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
|
|
3736
|
+
label: `\`${name}\``,
|
|
3737
|
+
render: () => {
|
|
3738
|
+
const loops = iterations(ctx.parsed.roots, name);
|
|
3739
|
+
if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
|
|
3740
|
+
if (loops.length === 0) return null;
|
|
3741
|
+
const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
|
|
3742
|
+
if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
|
|
3743
|
+
return { loop: loops[0], params: callbackParams(loops[0], name) };
|
|
3744
|
+
}
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
candidates.push(...inlineTables(ctx));
|
|
3748
|
+
const siblings = /* @__PURE__ */ new Map();
|
|
3749
|
+
const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
|
|
3750
|
+
for (const member of members) {
|
|
3751
|
+
siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
|
|
3752
|
+
}
|
|
3753
|
+
const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
|
|
3754
|
+
(leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
|
|
3755
|
+
).length;
|
|
3756
|
+
const best = /* @__PURE__ */ new Map();
|
|
3757
|
+
const bestFit = (member) => {
|
|
3758
|
+
const key = groupOf2(member);
|
|
3759
|
+
if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
|
|
3760
|
+
return best.get(key);
|
|
3761
|
+
};
|
|
3762
|
+
const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
|
|
3763
|
+
const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
|
|
3764
|
+
for (const candidate of candidates) {
|
|
3765
|
+
const mine = members.filter((member) => claims(candidate, member));
|
|
3766
|
+
if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
|
|
3767
|
+
if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
|
|
3768
|
+
const refuse = (reason) => {
|
|
3769
|
+
for (const member of mine) out.refused.push({ key: member.key, reason });
|
|
3770
|
+
};
|
|
3771
|
+
const paths = candidate.rows.map((row) => row.map(() => ""));
|
|
3772
|
+
const kinds = candidate.rows.map((row) => row.map(() => ""));
|
|
3773
|
+
let clash = false;
|
|
3774
|
+
for (const member of mine) {
|
|
3775
|
+
const spots = candidate.rows.flatMap(
|
|
3776
|
+
(row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
|
|
3777
|
+
);
|
|
3778
|
+
if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
|
|
3779
|
+
else {
|
|
3780
|
+
paths[spots[0][0]][spots[0][1]] = member.path;
|
|
3781
|
+
kinds[spots[0][0]][spots[0][1]] = member.kind;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
if (clash || paths.some((row) => row.some((path) => path === ""))) {
|
|
3785
|
+
refuse("IN_EXPRESSION");
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
const kindAt = kinds[0];
|
|
3789
|
+
if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
|
|
3790
|
+
refuse("IN_EXPRESSION");
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
const rendered = candidate.render();
|
|
3794
|
+
if (rendered === null) continue;
|
|
3795
|
+
if ("refuse" in rendered) {
|
|
3796
|
+
refuse(rendered.refuse);
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
const { loop, params } = rendered;
|
|
3800
|
+
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];
|
|
3801
|
+
if (!params || !reads || reads.length !== candidate.rows[0].length) {
|
|
3802
|
+
refuse("IN_EXPRESSION");
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3805
|
+
const elements = readerElements(ctx.content, loop, reads);
|
|
3806
|
+
const wraps = readerWraps(loop, reads);
|
|
3807
|
+
const hosts = [];
|
|
3808
|
+
let refusal = null;
|
|
3809
|
+
for (const read of reads) {
|
|
3810
|
+
const found = elements.get(read) ?? [];
|
|
3811
|
+
const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
|
|
3812
|
+
if (found.length === 1) hosts.push({ element: found[0] });
|
|
3813
|
+
else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
|
|
3814
|
+
else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
|
|
3815
|
+
}
|
|
3816
|
+
if (refusal !== null) {
|
|
3817
|
+
refuse(refusal);
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3821
|
+
indexes.add(index);
|
|
3822
|
+
const splices = [];
|
|
3823
|
+
if (rule.loopKey) {
|
|
3824
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3825
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3826
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3827
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
if (params.index === null) {
|
|
3831
|
+
if (params.addIndexAt < 0) {
|
|
3832
|
+
refuse("IN_EXPRESSION");
|
|
3833
|
+
continue;
|
|
3834
|
+
}
|
|
3835
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3836
|
+
}
|
|
3837
|
+
const templates = pathTemplates(paths, index);
|
|
3838
|
+
if (!templates) {
|
|
3839
|
+
refuse("IN_EXPRESSION");
|
|
3840
|
+
continue;
|
|
3841
|
+
}
|
|
3842
|
+
const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
|
|
3843
|
+
for (const [position, host] of hosts.entries()) {
|
|
3844
|
+
const kind = kindAt[position];
|
|
3845
|
+
const attributes = [
|
|
3846
|
+
rule.attr("data-bcms-field", `\`${templates[position]}\``),
|
|
3847
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3848
|
+
// A richtext value is html, so it goes into the dialect's html sink rather than being
|
|
3849
|
+
// printed as characters — the same rule one level up. @see bindDataLiterals
|
|
3850
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
|
|
3851
|
+
];
|
|
3852
|
+
if ("element" in host) {
|
|
3853
|
+
splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3854
|
+
if (kind === "richtext" && rule.richtext && host.element.inner) {
|
|
3855
|
+
splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
|
|
3856
|
+
}
|
|
3857
|
+
} else {
|
|
3858
|
+
const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
|
|
3859
|
+
splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
|
|
3860
|
+
}
|
|
3861
|
+
out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
|
|
3862
|
+
}
|
|
3863
|
+
const args = [
|
|
3864
|
+
ctx.snapshotName(mine[0].slug),
|
|
3865
|
+
list,
|
|
3866
|
+
ctx.content.slice(candidate.at.start, candidate.at.end),
|
|
3867
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3868
|
+
];
|
|
3869
|
+
splices.push({
|
|
3870
|
+
start: candidate.at.start,
|
|
3871
|
+
end: candidate.at.end,
|
|
3872
|
+
text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
|
|
3873
|
+
});
|
|
3874
|
+
out.splices.push(...splices);
|
|
3875
|
+
if (candidate.tuple) out.usesTuples = true;
|
|
3876
|
+
else out.usesList = true;
|
|
3877
|
+
out.slugs.push(...mine.map((member) => member.slug));
|
|
3878
|
+
out.bound.push(...mine.map((member) => member.key));
|
|
3879
|
+
out.notes.push(
|
|
3880
|
+
`${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.`
|
|
3881
|
+
);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
function propCalls(content, expression, row, property) {
|
|
3885
|
+
const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
|
|
3886
|
+
const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
|
|
3887
|
+
const out = [];
|
|
3888
|
+
const visit = (node) => {
|
|
3889
|
+
if (node.type === "expr") {
|
|
3890
|
+
for (const child of node.children) visit(child);
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
if (node.type !== "element" || node.raw) return;
|
|
3894
|
+
if (node.tag !== "" && !isIntrinsic(node.tag)) {
|
|
3895
|
+
for (const attr of node.attrs) {
|
|
3896
|
+
if (!attr.expressionRange) continue;
|
|
3897
|
+
const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
|
|
3898
|
+
if (spread.test(text)) out.push({ call: node, prop: property });
|
|
3899
|
+
else if (named.test(text)) out.push({ call: node, prop: attr.name });
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
for (const child of node.children) visit(child);
|
|
3903
|
+
};
|
|
3904
|
+
for (const child of expression.children) visit(child);
|
|
3523
3905
|
return out;
|
|
3524
3906
|
}
|
|
3525
3907
|
function leafRefusal(loop, row, property) {
|
|
3526
3908
|
const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
|
|
3527
3909
|
return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
|
|
3528
3910
|
}
|
|
3529
|
-
function allocateIndex(loop) {
|
|
3911
|
+
function allocateIndex(loop, taken) {
|
|
3530
3912
|
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}`;
|
|
3913
|
+
if (!used.has("i") && !taken.has("i")) return "i";
|
|
3914
|
+
for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
|
|
3533
3915
|
}
|
|
3534
3916
|
|
|
3535
3917
|
// src/resolve.ts
|
|
@@ -3611,6 +3993,13 @@ function isAliasShaped(value) {
|
|
|
3611
3993
|
function unreadableConfigs(sources) {
|
|
3612
3994
|
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3613
3995
|
}
|
|
3996
|
+
function extendedConfigs(sources) {
|
|
3997
|
+
return sources.filter((source) => {
|
|
3998
|
+
if (!isProjectConfig(source.path)) return false;
|
|
3999
|
+
const parsed = readJsonc(source.content);
|
|
4000
|
+
return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
|
|
4001
|
+
}).map((source) => source.path);
|
|
4002
|
+
}
|
|
3614
4003
|
function expand(specifier, aliases) {
|
|
3615
4004
|
const out = [];
|
|
3616
4005
|
for (const [pattern, targets] of aliases) {
|
|
@@ -3740,7 +4129,7 @@ function bindImages(ctx, targets) {
|
|
|
3740
4129
|
|
|
3741
4130
|
// src/helper.ts
|
|
3742
4131
|
import { createHash as createHash2 } from "crypto";
|
|
3743
|
-
var HELPER_VERSION =
|
|
4132
|
+
var HELPER_VERSION = 6;
|
|
3744
4133
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
3745
4134
|
var HELPER_PATH = Object.fromEntries(
|
|
3746
4135
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -3848,6 +4237,30 @@ export function bcmsRowsAs<T>(
|
|
|
3848
4237
|
});
|
|
3849
4238
|
}
|
|
3850
4239
|
|
|
4240
|
+
/**
|
|
4241
|
+
* A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
|
|
4242
|
+
*
|
|
4243
|
+
* \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
|
|
4244
|
+
* renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
|
|
4245
|
+
* an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
|
|
4246
|
+
* resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
|
|
4247
|
+
* editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
|
|
4248
|
+
* this out loud rather than leaving an editor to discover it.
|
|
4249
|
+
*/
|
|
4250
|
+
export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
|
|
4251
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4252
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4253
|
+
return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
/**
|
|
4257
|
+
* The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
|
|
4258
|
+
* same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
|
|
4259
|
+
*/
|
|
4260
|
+
export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
|
|
4261
|
+
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4262
|
+
}
|
|
4263
|
+
|
|
3851
4264
|
/** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
|
|
3852
4265
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
3853
4266
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
@@ -3939,19 +4352,22 @@ var HISTORICAL = {
|
|
|
3939
4352
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3940
4353
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3941
4354
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3942
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4355
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4356
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3943
4357
|
},
|
|
3944
4358
|
"src/lib/bcms-content.ts": {
|
|
3945
4359
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
3946
4360
|
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
3947
4361
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
3948
|
-
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af"
|
|
4362
|
+
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4363
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
3949
4364
|
},
|
|
3950
4365
|
"composables/bcms-content.ts": {
|
|
3951
4366
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3952
4367
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3953
4368
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3954
|
-
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
4369
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4370
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
3955
4371
|
}
|
|
3956
4372
|
};
|
|
3957
4373
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4486,8 +4902,8 @@ function declaringElements(roots, content) {
|
|
|
4486
4902
|
const name = attr.name.replace(/^:/, "");
|
|
4487
4903
|
if (name === "bcmsBindings") {
|
|
4488
4904
|
const own = ownBytes(node, content);
|
|
4489
|
-
for (const
|
|
4490
|
-
out.push({ scope: "page", path
|
|
4905
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
4906
|
+
out.push({ scope: "page", path, kind, own });
|
|
4491
4907
|
}
|
|
4492
4908
|
continue;
|
|
4493
4909
|
}
|
|
@@ -4517,7 +4933,7 @@ function declaringElements(roots, content) {
|
|
|
4517
4933
|
return out;
|
|
4518
4934
|
}
|
|
4519
4935
|
function helperLocals(parsed, dialect, file) {
|
|
4520
|
-
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [] };
|
|
4936
|
+
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
|
|
4521
4937
|
const helper = HELPER_PATH[dialect];
|
|
4522
4938
|
if (!helper) return out;
|
|
4523
4939
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
@@ -4527,6 +4943,8 @@ function helperLocals(parsed, dialect, file) {
|
|
|
4527
4943
|
if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
|
|
4528
4944
|
if (from.imported === "bcmsImage") out.bcmsImage.push(local);
|
|
4529
4945
|
if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
|
|
4946
|
+
if (from.imported === "bcmsList") out.bcmsList.push(local);
|
|
4947
|
+
if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
|
|
4530
4948
|
}
|
|
4531
4949
|
return out;
|
|
4532
4950
|
}
|
|
@@ -4599,6 +5017,9 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4599
5017
|
keyed = reads(locals.bcmsRowsAs);
|
|
4600
5018
|
rows = keyed || reads(locals.bcmsRows);
|
|
4601
5019
|
}
|
|
5020
|
+
const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
|
|
5021
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
|
|
5022
|
+
);
|
|
4602
5023
|
let cached = null;
|
|
4603
5024
|
const moduleRead = () => {
|
|
4604
5025
|
if (indexed || snapshot === null) return false;
|
|
@@ -4614,7 +5035,7 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4614
5035
|
return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
|
|
4615
5036
|
}
|
|
4616
5037
|
if (element.path !== shape) return false;
|
|
4617
|
-
return rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
5038
|
+
return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
4618
5039
|
});
|
|
4619
5040
|
}
|
|
4620
5041
|
|
|
@@ -4750,6 +5171,22 @@ function importLines(imports) {
|
|
|
4750
5171
|
|
|
4751
5172
|
// src/receipt.ts
|
|
4752
5173
|
var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
|
|
5174
|
+
var FormInvariantError = class extends Error {
|
|
5175
|
+
code = "FORM_INVARIANT";
|
|
5176
|
+
constructor(message) {
|
|
5177
|
+
super(message);
|
|
5178
|
+
this.name = "FormInvariantError";
|
|
5179
|
+
}
|
|
5180
|
+
};
|
|
5181
|
+
function assertForms(total, forms) {
|
|
5182
|
+
const sum = forms.wired + forms.alreadyWired + forms.pending.length;
|
|
5183
|
+
if (sum !== total) {
|
|
5184
|
+
throw new FormInvariantError(
|
|
5185
|
+
`The receipt does not account for every form: ${total} listed but ${forms.wired} wired + ${forms.alreadyWired} already wired + ${forms.pending.length} pending = ${sum}.`
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
5188
|
+
return forms;
|
|
5189
|
+
}
|
|
4753
5190
|
var ReceiptInvariantError = class extends Error {
|
|
4754
5191
|
code = "RECEIPT_INVARIANT";
|
|
4755
5192
|
constructor(message) {
|
|
@@ -5589,6 +6026,17 @@ function readBrief(value) {
|
|
|
5589
6026
|
}
|
|
5590
6027
|
}
|
|
5591
6028
|
}
|
|
6029
|
+
for (const [at, entry] of check.array(brief.forms ?? [], `the brief's "forms"`).entries()) {
|
|
6030
|
+
const where = `brief.forms[${at}]`;
|
|
6031
|
+
const form = check.record(entry, where);
|
|
6032
|
+
check.segment(form.id, `${where}.id`);
|
|
6033
|
+
check.string(form.name, `${where}.name`);
|
|
6034
|
+
check.string(form.submitUrl, `${where}.submitUrl`);
|
|
6035
|
+
check.optionalString(form.successMessage, `${where}.successMessage`);
|
|
6036
|
+
for (const [n, held] of check.array(form.fields, `${where}.fields`).entries()) {
|
|
6037
|
+
check.segment(check.record(held, `${where}.fields[${n}]`).key, `${where}.fields[${n}].key`);
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
5592
6040
|
return brief;
|
|
5593
6041
|
}
|
|
5594
6042
|
function readPlan(value) {
|
|
@@ -5635,6 +6083,236 @@ function readPlan(value) {
|
|
|
5635
6083
|
return plan2;
|
|
5636
6084
|
}
|
|
5637
6085
|
|
|
6086
|
+
// src/forms.ts
|
|
6087
|
+
import MagicString4 from "magic-string";
|
|
6088
|
+
var WIRABLE = ["html", "astro", "jsx"];
|
|
6089
|
+
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6090
|
+
var FORM_SCRIPT = `<script>
|
|
6091
|
+
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6092
|
+
f.addEventListener('submit',function(e){
|
|
6093
|
+
e.preventDefault();
|
|
6094
|
+
var msg=f.querySelector('.bcms-form-msg');
|
|
6095
|
+
var data={};
|
|
6096
|
+
// A checkboxes group posts one entry per checked box under the SAME name; the old
|
|
6097
|
+
// one-at-a-time assignment kept only the last, so "pick many" delivered one answer.
|
|
6098
|
+
// One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a
|
|
6099
|
+
// lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts
|
|
6100
|
+
// both an array and a comma string.
|
|
6101
|
+
new FormData(f).forEach(function(v,k){
|
|
6102
|
+
if(k==='cf-turnstile-response')return;
|
|
6103
|
+
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6104
|
+
else data[k]=v;
|
|
6105
|
+
});
|
|
6106
|
+
var body={data:data};
|
|
6107
|
+
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6108
|
+
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
6109
|
+
fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
6110
|
+
.then(function(r){return r.ok;})
|
|
6111
|
+
.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();})
|
|
6112
|
+
.catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});
|
|
6113
|
+
});
|
|
6114
|
+
});
|
|
6115
|
+
</script>`;
|
|
6116
|
+
var SCRIPT_BODY = FORM_SCRIPT.slice("<script>".length, -"</script>".length);
|
|
6117
|
+
function scriptFor(dialect) {
|
|
6118
|
+
if (dialect === "jsx") return `<script dangerouslySetInnerHTML={{ __html: \`${SCRIPT_BODY}\` }} />`;
|
|
6119
|
+
if (dialect === "astro") return `<script is:inline>${SCRIPT_BODY}</script>`;
|
|
6120
|
+
return FORM_SCRIPT;
|
|
6121
|
+
}
|
|
6122
|
+
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6123
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6124
|
+
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6125
|
+
function* elementsIn(nodes) {
|
|
6126
|
+
for (const node of nodes) {
|
|
6127
|
+
if (node.type === "expr") {
|
|
6128
|
+
yield* elementsIn(node.children);
|
|
6129
|
+
continue;
|
|
6130
|
+
}
|
|
6131
|
+
if (node.type !== "element") continue;
|
|
6132
|
+
yield node;
|
|
6133
|
+
yield* elementsIn(node.children);
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
var childElements = (node) => node.children.flatMap(
|
|
6137
|
+
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6138
|
+
);
|
|
6139
|
+
function candidateOf(file, dialect, form) {
|
|
6140
|
+
const controls = /* @__PURE__ */ new Map();
|
|
6141
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
6142
|
+
const all = [];
|
|
6143
|
+
const walk = (parent) => {
|
|
6144
|
+
const own = [];
|
|
6145
|
+
for (const child of childElements(parent)) {
|
|
6146
|
+
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6147
|
+
own.push(child);
|
|
6148
|
+
all.push(child);
|
|
6149
|
+
}
|
|
6150
|
+
walk(child);
|
|
6151
|
+
}
|
|
6152
|
+
if (own.length === 1 && parent !== form) wrapper.set(own[0], parent);
|
|
6153
|
+
};
|
|
6154
|
+
walk(form);
|
|
6155
|
+
for (const control of all) {
|
|
6156
|
+
const name = attrOf(control, "name")?.value;
|
|
6157
|
+
if (name && !controls.has(name)) controls.set(name, control);
|
|
6158
|
+
}
|
|
6159
|
+
for (const control of all) {
|
|
6160
|
+
const id = attrOf(control, "id")?.value;
|
|
6161
|
+
if (id && !controls.has(id)) controls.set(id, control);
|
|
6162
|
+
}
|
|
6163
|
+
return { file, dialect, el: form, controls, wrapper };
|
|
6164
|
+
}
|
|
6165
|
+
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6166
|
+
function wire(source, candidate, form) {
|
|
6167
|
+
const { el, dialect } = candidate;
|
|
6168
|
+
const inserts = [];
|
|
6169
|
+
const set = (name, value) => {
|
|
6170
|
+
const attr = attrOf(el, name);
|
|
6171
|
+
if (!attr) inserts.push(`${name}="${escapeAttr(value)}"`);
|
|
6172
|
+
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6173
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6174
|
+
};
|
|
6175
|
+
set("action", form.submitUrl);
|
|
6176
|
+
set("method", "post");
|
|
6177
|
+
inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
|
|
6178
|
+
if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(form.successMessage)}"`);
|
|
6179
|
+
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6180
|
+
for (const field of form.fields) {
|
|
6181
|
+
const control = candidate.controls.get(field.key);
|
|
6182
|
+
const host = candidate.wrapper.get(control) ?? control;
|
|
6183
|
+
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6184
|
+
const name = attrOf(control, "name");
|
|
6185
|
+
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6186
|
+
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
6187
|
+
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
if (el.inner && !hasMessage(el)) {
|
|
6191
|
+
const tail = /\s*$/.exec(source.original.slice(el.inner.start, el.inner.end))[0];
|
|
6192
|
+
source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
async function wireForms(briefIn, sources) {
|
|
6196
|
+
const brief = readBrief(briefIn);
|
|
6197
|
+
const forms = brief.forms ?? [];
|
|
6198
|
+
const candidates = [];
|
|
6199
|
+
const unreadable = [];
|
|
6200
|
+
for (const source of sources) {
|
|
6201
|
+
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6202
|
+
const dialect = dialectOf(source.path, source.content);
|
|
6203
|
+
if (!dialect || !WIRABLE.includes(dialect)) {
|
|
6204
|
+
unreadable.push({
|
|
6205
|
+
file: source.path,
|
|
6206
|
+
reason: "DIALECT_UNSUPPORTED",
|
|
6207
|
+
message: `${source.path} is ${dialect ?? "not a template"}.`
|
|
6208
|
+
});
|
|
6209
|
+
continue;
|
|
6210
|
+
}
|
|
6211
|
+
const parsed = await parseFile4(dialect, source.content);
|
|
6212
|
+
if (parsed.error) {
|
|
6213
|
+
unreadable.push({
|
|
6214
|
+
file: source.path,
|
|
6215
|
+
reason: parsed.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR",
|
|
6216
|
+
message: parsed.error.message
|
|
6217
|
+
});
|
|
6218
|
+
continue;
|
|
6219
|
+
}
|
|
6220
|
+
for (const el of elementsIn(parsed.roots)) {
|
|
6221
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6225
|
+
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6226
|
+
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6227
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
6228
|
+
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6229
|
+
const matched = [];
|
|
6230
|
+
const pending = [];
|
|
6231
|
+
const onSite = [];
|
|
6232
|
+
let alreadyWired = 0;
|
|
6233
|
+
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
6234
|
+
for (const form of forms) {
|
|
6235
|
+
const list = options.get(form.id) ?? [];
|
|
6236
|
+
if (list.length > 1) {
|
|
6237
|
+
pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
|
|
6238
|
+
continue;
|
|
6239
|
+
}
|
|
6240
|
+
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
6241
|
+
pend2(form, "FORM_AMBIGUOUS", list[0].file, "more than one CMS form's fields are posted by this <form>.");
|
|
6242
|
+
continue;
|
|
6243
|
+
}
|
|
6244
|
+
if (list.length === 0) {
|
|
6245
|
+
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6246
|
+
if (best) {
|
|
6247
|
+
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6248
|
+
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6249
|
+
} else if (unreadable[0]) {
|
|
6250
|
+
pend2(form, unreadable[0].reason, unreadable[0].file, unreadable[0].message);
|
|
6251
|
+
} else {
|
|
6252
|
+
pend2(form, "FORM_NOT_IN_SOURCE", void 0, `no <form> posts ${form.fields.map((f) => f.key).join(", ") || "any field of this form"}.`);
|
|
6253
|
+
}
|
|
6254
|
+
continue;
|
|
6255
|
+
}
|
|
6256
|
+
const candidate = list[0];
|
|
6257
|
+
if (attrOf(candidate.el, "action")?.expression) {
|
|
6258
|
+
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6259
|
+
continue;
|
|
6260
|
+
}
|
|
6261
|
+
const misnamed = form.fields.find((field) => {
|
|
6262
|
+
const attr = attrOf(candidate.controls.get(field.key), "name");
|
|
6263
|
+
return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
|
|
6264
|
+
});
|
|
6265
|
+
if (misnamed) {
|
|
6266
|
+
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.`);
|
|
6267
|
+
continue;
|
|
6268
|
+
}
|
|
6269
|
+
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6270
|
+
alreadyWired++;
|
|
6271
|
+
onSite.push(form);
|
|
6272
|
+
continue;
|
|
6273
|
+
}
|
|
6274
|
+
matched.push({ form, candidate });
|
|
6275
|
+
onSite.push(form);
|
|
6276
|
+
}
|
|
6277
|
+
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6278
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6279
|
+
for (const m of matched) byFile.set(m.candidate.file, [...byFile.get(m.candidate.file) ?? [], m]);
|
|
6280
|
+
const files = [];
|
|
6281
|
+
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6282
|
+
const content = contents.get(path);
|
|
6283
|
+
const source = new MagicString4(content);
|
|
6284
|
+
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6285
|
+
if (!content.includes("form[data-bcms-form]")) {
|
|
6286
|
+
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6287
|
+
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6288
|
+
source.appendRight(last.candidate.el.end, `
|
|
6289
|
+
${indent}${scriptFor(last.candidate.dialect)}`);
|
|
6290
|
+
}
|
|
6291
|
+
files.push({ path, operation: "modify", content: source.toString() });
|
|
6292
|
+
}
|
|
6293
|
+
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.`);
|
|
6294
|
+
return {
|
|
6295
|
+
files,
|
|
6296
|
+
receipt: {
|
|
6297
|
+
briefDigest: briefDigest(brief),
|
|
6298
|
+
// This lane binds no brief PATH, and says so rather than leaving the arithmetic to a reader:
|
|
6299
|
+
// a receipt claiming zero of zero paths is exactly what a forms-only run did.
|
|
6300
|
+
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6301
|
+
occurrences: { located: 0, rewritten: 0 },
|
|
6302
|
+
files: [],
|
|
6303
|
+
helpers: [],
|
|
6304
|
+
helperUpgraded: false,
|
|
6305
|
+
dynamicBindings: [],
|
|
6306
|
+
forms: assertForms(forms.length, {
|
|
6307
|
+
wired: matched.length,
|
|
6308
|
+
alreadyWired,
|
|
6309
|
+
pending,
|
|
6310
|
+
...notes.length > 0 ? { notes } : {}
|
|
6311
|
+
})
|
|
6312
|
+
}
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
6315
|
+
|
|
5638
6316
|
// src/scan.ts
|
|
5639
6317
|
var SKIP_DIRS = [
|
|
5640
6318
|
"node_modules/",
|
|
@@ -5692,6 +6370,7 @@ function isSourceCandidate(path) {
|
|
|
5692
6370
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
5693
6371
|
return false;
|
|
5694
6372
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6373
|
+
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
5695
6374
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
5696
6375
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
5697
6376
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
@@ -5994,7 +6673,7 @@ function moduleScope(parsed, content) {
|
|
|
5994
6673
|
}
|
|
5995
6674
|
|
|
5996
6675
|
// src/componentize.ts
|
|
5997
|
-
import
|
|
6676
|
+
import MagicString5 from "magic-string";
|
|
5998
6677
|
var SectionInvariantError = class extends Error {
|
|
5999
6678
|
code = "SECTION_INVARIANT";
|
|
6000
6679
|
constructor(message) {
|
|
@@ -6920,7 +7599,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
|
|
|
6920
7599
|
` };
|
|
6921
7600
|
}
|
|
6922
7601
|
function sectionMarkup(content, root, fieldName) {
|
|
6923
|
-
const slice = new
|
|
7602
|
+
const slice = new MagicString5(content.slice(root.start, root.end));
|
|
6924
7603
|
const at = (offset) => offset - root.start;
|
|
6925
7604
|
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
6926
7605
|
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
@@ -7119,7 +7798,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
|
7119
7798
|
);
|
|
7120
7799
|
continue;
|
|
7121
7800
|
}
|
|
7122
|
-
const source = new
|
|
7801
|
+
const source = new MagicString5(content);
|
|
7123
7802
|
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
7124
7803
|
if (duplicates.length > 0) {
|
|
7125
7804
|
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.`);
|
|
@@ -7396,7 +8075,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
7396
8075
|
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
7397
8076
|
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
7398
8077
|
const local = names.get("Sections");
|
|
7399
|
-
const source = new
|
|
8078
|
+
const source = new MagicString5(content);
|
|
7400
8079
|
const first = placeable[0].root;
|
|
7401
8080
|
const last = placeable[placeable.length - 1].root;
|
|
7402
8081
|
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
@@ -7416,7 +8095,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
|
|
|
7416
8095
|
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
7417
8096
|
]);
|
|
7418
8097
|
const snapshot = names.get("pageSnapshot");
|
|
7419
|
-
const source = new
|
|
8098
|
+
const source = new MagicString5(content);
|
|
7420
8099
|
for (const resolved of placeable) {
|
|
7421
8100
|
const component = written.get(resolved.slug);
|
|
7422
8101
|
const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
|
|
@@ -7740,6 +8419,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7740
8419
|
const receiptFiles = [];
|
|
7741
8420
|
const dialects = /* @__PURE__ */ new Set();
|
|
7742
8421
|
const dynamicBindings = [];
|
|
8422
|
+
const laneNotes = [];
|
|
7743
8423
|
const snapshotSlugs = /* @__PURE__ */ new Set();
|
|
7744
8424
|
let occurrencesLocated = 0;
|
|
7745
8425
|
let occurrencesRewritten = 0;
|
|
@@ -7765,6 +8445,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7765
8445
|
outFiles.push({ path: file, operation: "modify", content: result.content });
|
|
7766
8446
|
receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
|
|
7767
8447
|
dynamicBindings.push(...result.dynamic);
|
|
8448
|
+
laneNotes.push(...result.notes);
|
|
7768
8449
|
for (const slug of result.snapshots) snapshotSlugs.add(slug);
|
|
7769
8450
|
}
|
|
7770
8451
|
}
|
|
@@ -7801,7 +8482,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7801
8482
|
} else rewritten++;
|
|
7802
8483
|
}
|
|
7803
8484
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7804
|
-
const source = new
|
|
8485
|
+
const source = new MagicString6(contents.get(file));
|
|
7805
8486
|
const seen = /* @__PURE__ */ new Set();
|
|
7806
8487
|
for (const splice of splices) {
|
|
7807
8488
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -7861,13 +8542,25 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7861
8542
|
helperUpgraded,
|
|
7862
8543
|
dynamicBindings,
|
|
7863
8544
|
canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
|
|
7864
|
-
notes:
|
|
7865
|
-
(
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
8545
|
+
notes: [
|
|
8546
|
+
...unreadableConfigs(sources).map(
|
|
8547
|
+
(file) => ({
|
|
8548
|
+
code: "TSCONFIG_UNREADABLE",
|
|
8549
|
+
file,
|
|
8550
|
+
message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
8551
|
+
})
|
|
8552
|
+
),
|
|
8553
|
+
// An absent alias table looks exactly like a project that has none, and the two have
|
|
8554
|
+
// opposite consequences for every component reached through one. @see extendedConfigs
|
|
8555
|
+
...extendedConfigs(sources).map(
|
|
8556
|
+
(file) => ({
|
|
8557
|
+
code: "TSCONFIG_EXTENDS_ONLY",
|
|
8558
|
+
file,
|
|
8559
|
+
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.`
|
|
8560
|
+
})
|
|
8561
|
+
),
|
|
8562
|
+
...laneNotes
|
|
8563
|
+
]
|
|
7871
8564
|
});
|
|
7872
8565
|
return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
|
|
7873
8566
|
}
|
|
@@ -7889,7 +8582,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7889
8582
|
dialect: dialectOf(file, content),
|
|
7890
8583
|
dynamic: [],
|
|
7891
8584
|
componentEdits: [],
|
|
7892
|
-
snapshots: []
|
|
8585
|
+
snapshots: [],
|
|
8586
|
+
notes: []
|
|
7893
8587
|
};
|
|
7894
8588
|
if (pendingTargets.length === 0) return empty;
|
|
7895
8589
|
if (!dialect) {
|
|
@@ -7950,7 +8644,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7950
8644
|
dialect: null,
|
|
7951
8645
|
dynamic: [],
|
|
7952
8646
|
componentEdits: [],
|
|
7953
|
-
snapshots: []
|
|
8647
|
+
snapshots: [],
|
|
8648
|
+
notes: []
|
|
7954
8649
|
};
|
|
7955
8650
|
}
|
|
7956
8651
|
if (rescued.reason === "unverifiable") {
|
|
@@ -8020,6 +8715,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8020
8715
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
8021
8716
|
{ key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
|
|
8022
8717
|
{ key: "helper:bcmsImage", name: "bcmsImage" },
|
|
8718
|
+
{ key: "helper:bcmsList", name: "bcmsList" },
|
|
8719
|
+
{ key: "helper:bcmsTuples", name: "bcmsTuples" },
|
|
8023
8720
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
8024
8721
|
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
8025
8722
|
].filter((entry) => !preset.has(entry.key));
|
|
@@ -8211,15 +8908,23 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8211
8908
|
const collided = new Set(overlapping(kept).map((r) => r.key));
|
|
8212
8909
|
const applied = kept.filter((r) => !collided.has(r.key));
|
|
8213
8910
|
for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
|
|
8214
|
-
const data = bindDataLiterals(
|
|
8911
|
+
const data = await bindDataLiterals(
|
|
8215
8912
|
{
|
|
8216
8913
|
content,
|
|
8217
8914
|
parsed,
|
|
8218
8915
|
dialect: writing,
|
|
8219
8916
|
rowsAs: helperName("bcmsRowsAs"),
|
|
8917
|
+
list: helperName("bcmsList"),
|
|
8918
|
+
tuples: helperName("bcmsTuples"),
|
|
8220
8919
|
read: helperName("bcms"),
|
|
8221
8920
|
snapshotName,
|
|
8222
|
-
slug: params?.slug ?? null
|
|
8921
|
+
slug: params?.slug ?? null,
|
|
8922
|
+
// The row's leaf travels to another file exactly as a scalar prop does, and by the same
|
|
8923
|
+
// machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
|
|
8924
|
+
drill: async (component, prop, kind) => {
|
|
8925
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
8926
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
8927
|
+
}
|
|
8223
8928
|
},
|
|
8224
8929
|
targets.filter((t) => {
|
|
8225
8930
|
const outcome = outcomes.get(t.key);
|
|
@@ -8237,6 +8942,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8237
8942
|
for (const declaration2 of data.declarations) {
|
|
8238
8943
|
dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
|
|
8239
8944
|
}
|
|
8945
|
+
componentEdits.push(...data.componentEdits);
|
|
8946
|
+
for (const drilledProp of data.props) {
|
|
8947
|
+
dynamic.push({
|
|
8948
|
+
file: drilledProp.file,
|
|
8949
|
+
prop: drilledProp.prop,
|
|
8950
|
+
path: drilledProp.path,
|
|
8951
|
+
callSites: [{ file, literal: drilledProp.literal }]
|
|
8952
|
+
});
|
|
8953
|
+
}
|
|
8240
8954
|
const images = bindImages(
|
|
8241
8955
|
{ content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
|
|
8242
8956
|
targets.filter((t) => {
|
|
@@ -8248,7 +8962,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8248
8962
|
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8249
8963
|
const lanes = [...data.splices, ...images.splices];
|
|
8250
8964
|
if (applied.length === 0 && lanes.length === 0) {
|
|
8251
|
-
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
|
|
8965
|
+
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|
|
8252
8966
|
}
|
|
8253
8967
|
const readSlugs = [
|
|
8254
8968
|
.../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
|
|
@@ -8275,6 +8989,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8275
8989
|
] : [],
|
|
8276
8990
|
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
|
|
8277
8991
|
...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
|
|
8992
|
+
...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
|
|
8993
|
+
// `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
|
|
8994
|
+
// page imports only the one it calls — an unused import is dead code in someone else's repo.
|
|
8995
|
+
...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
|
|
8278
8996
|
...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
|
|
8279
8997
|
...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
|
|
8280
8998
|
].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
|
|
@@ -8289,7 +9007,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8289
9007
|
dialect: writing,
|
|
8290
9008
|
dynamic,
|
|
8291
9009
|
componentEdits,
|
|
8292
|
-
snapshots: rule.text ? readSlugs : []
|
|
9010
|
+
snapshots: rule.text ? readSlugs : [],
|
|
9011
|
+
// The lane reports one sentence per list it bound; the FILE is added here, where it is known.
|
|
9012
|
+
notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
|
|
8293
9013
|
};
|
|
8294
9014
|
}
|
|
8295
9015
|
function mergeInto(content, site) {
|
|
@@ -8465,8 +9185,62 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
8465
9185
|
}
|
|
8466
9186
|
}
|
|
8467
9187
|
|
|
9188
|
+
// package.json
|
|
9189
|
+
var package_default = {
|
|
9190
|
+
name: "@bettercms-ai/convert",
|
|
9191
|
+
version: "0.9.0",
|
|
9192
|
+
type: "module",
|
|
9193
|
+
description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
|
|
9194
|
+
bin: {
|
|
9195
|
+
"bettercms-convert": "dist/cli.js"
|
|
9196
|
+
},
|
|
9197
|
+
main: "./dist/index.js",
|
|
9198
|
+
module: "./dist/index.js",
|
|
9199
|
+
types: "./dist/index.d.ts",
|
|
9200
|
+
sideEffects: false,
|
|
9201
|
+
exports: {
|
|
9202
|
+
".": {
|
|
9203
|
+
types: "./dist/index.d.ts",
|
|
9204
|
+
import: "./dist/index.js",
|
|
9205
|
+
default: "./dist/index.js"
|
|
9206
|
+
}
|
|
9207
|
+
},
|
|
9208
|
+
files: [
|
|
9209
|
+
"dist",
|
|
9210
|
+
"README.md",
|
|
9211
|
+
"CHANGELOG.md"
|
|
9212
|
+
],
|
|
9213
|
+
scripts: {
|
|
9214
|
+
typecheck: "tsc --noEmit",
|
|
9215
|
+
build: "tsup",
|
|
9216
|
+
test: "vitest run",
|
|
9217
|
+
"test:watch": "vitest",
|
|
9218
|
+
briefs: "bun --conditions=development scripts/build-briefs.ts"
|
|
9219
|
+
},
|
|
9220
|
+
dependencies: {
|
|
9221
|
+
"@astrojs/compiler": "^2.13.1",
|
|
9222
|
+
"@babel/parser": "^7.29.2",
|
|
9223
|
+
"@bettercms-ai/types": "^1.12.0",
|
|
9224
|
+
"@vue/compiler-sfc": "^3.5.0",
|
|
9225
|
+
"magic-string": "^0.30.21",
|
|
9226
|
+
parse5: "^7.3.0",
|
|
9227
|
+
svelte: "^5.0.0"
|
|
9228
|
+
},
|
|
9229
|
+
devDependencies: {
|
|
9230
|
+
"@types/node": "^20",
|
|
9231
|
+
esbuild: "^0.27.7",
|
|
9232
|
+
tsup: "^8.5.1",
|
|
9233
|
+
typescript: "^5",
|
|
9234
|
+
vitest: "^4.1.4"
|
|
9235
|
+
},
|
|
9236
|
+
publishConfig: {
|
|
9237
|
+
access: "public",
|
|
9238
|
+
registry: "https://registry.npmjs.org/"
|
|
9239
|
+
}
|
|
9240
|
+
};
|
|
9241
|
+
|
|
8468
9242
|
// src/cli.ts
|
|
8469
|
-
var VERSION =
|
|
9243
|
+
var { version: VERSION } = package_default;
|
|
8470
9244
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
8471
9245
|
var CliError = class extends Error {
|
|
8472
9246
|
};
|
|
@@ -8474,6 +9248,7 @@ function parseArgs(argv) {
|
|
|
8474
9248
|
const args = {
|
|
8475
9249
|
brief: void 0,
|
|
8476
9250
|
componentize: false,
|
|
9251
|
+
forms: false,
|
|
8477
9252
|
plan: void 0,
|
|
8478
9253
|
copy: "bind",
|
|
8479
9254
|
root: ".",
|
|
@@ -8488,6 +9263,7 @@ function parseArgs(argv) {
|
|
|
8488
9263
|
const arg = argv[i];
|
|
8489
9264
|
if (arg === "--brief") args.brief = argv[++i];
|
|
8490
9265
|
else if (arg === "--componentize") args.componentize = true;
|
|
9266
|
+
else if (arg === "--forms") args.forms = true;
|
|
8491
9267
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
8492
9268
|
else if (arg === "--copy") {
|
|
8493
9269
|
const mode = argv[++i];
|
|
@@ -8514,9 +9290,14 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
8514
9290
|
|
|
8515
9291
|
bettercms-convert --brief <json> --root . [options]
|
|
8516
9292
|
bettercms-convert --componentize --brief <json> --plan <json> --root . [options]
|
|
9293
|
+
bettercms-convert --forms --brief <json> --root . [options]
|
|
8517
9294
|
|
|
8518
9295
|
--brief <file> The conversion brief, as get_conversion_brief returns it. Required.
|
|
8519
9296
|
--componentize Lift each planned SECTION into a component the CMS can reorder.
|
|
9297
|
+
--forms Point each <form> in the repository at the CMS form behind it \u2014 the
|
|
9298
|
+
endpoint, the id, a marker per field and the submit script. A form
|
|
9299
|
+
the brief lists as a DRAFT is wired and receipted: publish it in the
|
|
9300
|
+
Forms tab, or it rejects every submission with a 403.
|
|
8520
9301
|
--plan <file> The componentize plan, as get_componentize_plan returns it.
|
|
8521
9302
|
Required with --componentize.
|
|
8522
9303
|
--copy bind|instance Which copy model the plan is expected to describe. Default: bind
|
|
@@ -8599,6 +9380,26 @@ ${rewritten} of ${declared} paths bound (${alreadyDeclared} already declared, ${
|
|
|
8599
9380
|
`);
|
|
8600
9381
|
}
|
|
8601
9382
|
}
|
|
9383
|
+
function reportForms(files, receipt, dryRun) {
|
|
9384
|
+
const { wired, alreadyWired, pending, notes } = receipt.forms;
|
|
9385
|
+
for (const file of files) {
|
|
9386
|
+
process.stdout.write(`${dryRun ? "would " : ""}${file.operation} ${file.path}
|
|
9387
|
+
`);
|
|
9388
|
+
}
|
|
9389
|
+
process.stdout.write(
|
|
9390
|
+
`
|
|
9391
|
+
${wired} forms wired (${alreadyWired} already wired, ${pending.length} pending)
|
|
9392
|
+
`
|
|
9393
|
+
);
|
|
9394
|
+
for (const form of pending) {
|
|
9395
|
+
process.stdout.write(
|
|
9396
|
+
` pending ${form.name} \u2014 ${form.reason}${form.message ? ` (${form.message})` : ""}${form.file ? ` [${form.file}]` : ""}
|
|
9397
|
+
`
|
|
9398
|
+
);
|
|
9399
|
+
}
|
|
9400
|
+
for (const note of notes ?? []) process.stdout.write(` todo ${note}
|
|
9401
|
+
`);
|
|
9402
|
+
}
|
|
8602
9403
|
function reportSections(files, receipt, dryRun) {
|
|
8603
9404
|
const { extracted, inlineOnly, alreadyExtracted, registered, pending } = receipt.sections;
|
|
8604
9405
|
for (const file of files) {
|
|
@@ -8630,6 +9431,9 @@ async function main() {
|
|
|
8630
9431
|
if (args.copy === "instance" && !args.componentize) {
|
|
8631
9432
|
throw new CliError("--copy is only read with --componentize. See --help.");
|
|
8632
9433
|
}
|
|
9434
|
+
if (args.forms && args.componentize) {
|
|
9435
|
+
throw new CliError("--forms and --componentize are separate runs. Do one, then the other. See --help.");
|
|
9436
|
+
}
|
|
8633
9437
|
const root = resolve(args.root);
|
|
8634
9438
|
const brief = JSON.parse(await readFile(resolve(args.brief), "utf-8"));
|
|
8635
9439
|
if (!Array.isArray(brief.pages)) {
|
|
@@ -8670,6 +9474,13 @@ async function main() {
|
|
|
8670
9474
|
reportSections(files2, receipt2, args.dryRun);
|
|
8671
9475
|
return args.strict && receipt2.sections.pending.length > 0 ? 2 : 0;
|
|
8672
9476
|
}
|
|
9477
|
+
if (args.forms) {
|
|
9478
|
+
const { files: files2, receipt: receipt2 } = await wireForms(brief, sources);
|
|
9479
|
+
await written(files2);
|
|
9480
|
+
await saveReceipt(receipt2);
|
|
9481
|
+
reportForms(files2, receipt2, args.dryRun);
|
|
9482
|
+
return args.strict && receipt2.forms.pending.length > 0 ? 2 : 0;
|
|
9483
|
+
}
|
|
8673
9484
|
const { files, receipt } = await convertSources(brief, sources, {
|
|
8674
9485
|
overwriteHelper: args.overwriteHelper
|
|
8675
9486
|
});
|