@bettercms-ai/convert 0.16.1 → 0.17.1
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 +17 -0
- package/dist/cli.js +83 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +82 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,13 @@ interface BriefForm {
|
|
|
114
114
|
key: string;
|
|
115
115
|
type: string;
|
|
116
116
|
}[];
|
|
117
|
+
/**
|
|
118
|
+
* Where the built form sat — footer, header, nav, main, aside, dialog or body. Breaks the tie
|
|
119
|
+
* between several source `<form>`s posting exactly these fields. @see forms.ts
|
|
120
|
+
*/
|
|
121
|
+
landmark?: string;
|
|
122
|
+
/** The owner's answer to FORM_AMBIGUOUS: the file this form is in. Applied like `--pin`. */
|
|
123
|
+
pin?: string;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
@@ -402,7 +409,7 @@ interface ConversionReceipt {
|
|
|
402
409
|
* reader must treat absent as unknown rather than as `"pending"`.
|
|
403
410
|
*/
|
|
404
411
|
canvas?: {
|
|
405
|
-
lane: "bridge" | "pending";
|
|
412
|
+
lane: "bridge" | "draft-route" | "pending";
|
|
406
413
|
files?: string[];
|
|
407
414
|
};
|
|
408
415
|
/**
|
|
@@ -431,6 +438,8 @@ interface PendingForm {
|
|
|
431
438
|
file?: string;
|
|
432
439
|
/** What exactly could not be done. Informative; the reason is the contract. */
|
|
433
440
|
message?: string;
|
|
441
|
+
/** FORM_AMBIGUOUS only: the files that each post every field — what a person picks from. */
|
|
442
|
+
candidates?: string[];
|
|
434
443
|
}
|
|
435
444
|
interface FormsReceipt {
|
|
436
445
|
/** A `<form>` this run pointed at the CMS: action, method, id, field markers and the script. */
|
|
@@ -1233,7 +1242,7 @@ interface CanvasSource {
|
|
|
1233
1242
|
ast?: unknown;
|
|
1234
1243
|
}
|
|
1235
1244
|
interface CanvasResult {
|
|
1236
|
-
lane: "bridge" | "pending";
|
|
1245
|
+
lane: "bridge" | "draft-route" | "pending";
|
|
1237
1246
|
/** Files the bridge was installed in. */
|
|
1238
1247
|
files: string[];
|
|
1239
1248
|
/** Rewritten contents, by path. Empty when the lane is pending. */
|
|
@@ -1248,8 +1257,8 @@ declare const ASTRO_LANE_PENDING: PendingPath;
|
|
|
1248
1257
|
* Install the draft bridge, or say why not.
|
|
1249
1258
|
*
|
|
1250
1259
|
* `routes` maps a brief route (`/about`) to its slug, so a route written as a literal in the page
|
|
1251
|
-
* can name itself. An Astro project is
|
|
1252
|
-
*
|
|
1260
|
+
* can name itself. An Astro project is never bridged here: it either wired a lane itself (see
|
|
1261
|
+
* {@link astroLane}) or it carries the SSR recipe.
|
|
1253
1262
|
*/
|
|
1254
1263
|
declare function canvasBridge(sources: CanvasSource[], routes: Map<string, string>): CanvasResult;
|
|
1255
1264
|
|
package/dist/index.js
CHANGED
|
@@ -6090,9 +6090,28 @@ var pend = (reason, hint, file, line) => ({
|
|
|
6090
6090
|
hint
|
|
6091
6091
|
});
|
|
6092
6092
|
var ASTRO_LANE_PENDING = pend("SSR_DRAFT_ROUTE", SSR_DRAFT_RECIPE);
|
|
6093
|
+
function mounts(content, component) {
|
|
6094
|
+
const from = new RegExp(
|
|
6095
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+["']@bettercms-ai/astro/components/${component}\\.astro["']`
|
|
6096
|
+
);
|
|
6097
|
+
const local = content.match(from)?.[1];
|
|
6098
|
+
return local !== void 0 && new RegExp(`<${local}[\\s/>]`).test(content);
|
|
6099
|
+
}
|
|
6100
|
+
var ASTRO_CONFIG = /(^|\/)astro\.config\.(mjs|cjs|js|ts|mts)$/;
|
|
6101
|
+
function astroLane(sources) {
|
|
6102
|
+
const astro = sources.filter((s) => s.path.toLowerCase().endsWith(".astro"));
|
|
6103
|
+
const server = sources.some(
|
|
6104
|
+
(s) => ASTRO_CONFIG.test(s.path) && /\boutput\s*:\s*["']server["']/.test(s.content)
|
|
6105
|
+
);
|
|
6106
|
+
if (server && astro.some((s) => mounts(s.content, "BcmsLive"))) return "draft-route";
|
|
6107
|
+
if (astro.some((s) => mounts(s.content, "BcmsLiveBlocks"))) return "bridge";
|
|
6108
|
+
return null;
|
|
6109
|
+
}
|
|
6093
6110
|
function canvasBridge(sources, routes) {
|
|
6094
|
-
if (sources.some((s) => s.path.toLowerCase().endsWith(".astro")))
|
|
6095
|
-
|
|
6111
|
+
if (sources.some((s) => s.path.toLowerCase().endsWith(".astro"))) {
|
|
6112
|
+
const lane = astroLane(sources);
|
|
6113
|
+
return lane ? { lane, files: [], edits: /* @__PURE__ */ new Map(), pending: [] } : { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
|
|
6114
|
+
}
|
|
6096
6115
|
const files = [];
|
|
6097
6116
|
const edits = /* @__PURE__ */ new Map();
|
|
6098
6117
|
const pending = [];
|
|
@@ -6387,6 +6406,7 @@ function scriptFor(dialect) {
|
|
|
6387
6406
|
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6388
6407
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6389
6408
|
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6409
|
+
var DECLARED_BINDING_ATTR = /^data-bcms-(field|kind|props)$/i;
|
|
6390
6410
|
function* elementsIn(nodes) {
|
|
6391
6411
|
for (const node of nodes) {
|
|
6392
6412
|
if (node.type === "expr") {
|
|
@@ -6401,8 +6421,28 @@ function* elementsIn(nodes) {
|
|
|
6401
6421
|
var childElements = (node) => node.children.flatMap(
|
|
6402
6422
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6403
6423
|
);
|
|
6424
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
|
|
6425
|
+
function landmarkOf(chain) {
|
|
6426
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
6427
|
+
const el = chain[i];
|
|
6428
|
+
if (attrOf(el, "role")?.value === "dialog") return "dialog";
|
|
6429
|
+
if (LANDMARK_TAGS.has(el.tag.toLowerCase())) return el.tag.toLowerCase();
|
|
6430
|
+
}
|
|
6431
|
+
return "body";
|
|
6432
|
+
}
|
|
6433
|
+
function* formsIn(nodes, chain = []) {
|
|
6434
|
+
for (const node of nodes) {
|
|
6435
|
+
if (node.type === "expr") {
|
|
6436
|
+
yield* formsIn(node.children, chain);
|
|
6437
|
+
continue;
|
|
6438
|
+
}
|
|
6439
|
+
if (node.type !== "element") continue;
|
|
6440
|
+
if (node.tag.toLowerCase() === "form") yield { el: node, landmark: landmarkOf(chain) };
|
|
6441
|
+
yield* formsIn(node.children, [...chain, node]);
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6404
6444
|
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6405
|
-
function candidateOf(file, dialect, form, imported) {
|
|
6445
|
+
function candidateOf(file, dialect, form, imported, landmark) {
|
|
6406
6446
|
const controls = /* @__PURE__ */ new Map();
|
|
6407
6447
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6408
6448
|
const foreign = /* @__PURE__ */ new Set();
|
|
@@ -6439,7 +6479,7 @@ function candidateOf(file, dialect, form, imported) {
|
|
|
6439
6479
|
const id = attrOf(control, "id")?.value;
|
|
6440
6480
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6441
6481
|
}
|
|
6442
|
-
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6482
|
+
return { file, dialect, el: form, controls, wrapper, foreign, landmark };
|
|
6443
6483
|
}
|
|
6444
6484
|
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6445
6485
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -6474,14 +6514,21 @@ function wire(source, candidate, form) {
|
|
|
6474
6514
|
};
|
|
6475
6515
|
set("action", form.submitUrl);
|
|
6476
6516
|
set("method", "post");
|
|
6477
|
-
|
|
6478
|
-
if (form.successMessage)
|
|
6517
|
+
set("data-bcms-form", form.id);
|
|
6518
|
+
if (form.successMessage) set("data-bcms-form-success", form.successMessage);
|
|
6479
6519
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6520
|
+
for (const inner of elementsIn(el.children)) {
|
|
6521
|
+
for (const attr of inner.attrs) {
|
|
6522
|
+
if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
|
|
6523
|
+
const start = source.original[attr.range.start - 1] === " " ? attr.range.start - 1 : attr.range.start;
|
|
6524
|
+
source.remove(start, attr.range.end);
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6480
6527
|
for (const field of form.fields) {
|
|
6481
6528
|
const control = candidate.controls.get(field.key);
|
|
6482
6529
|
if (candidate.foreign.has(control)) continue;
|
|
6483
6530
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6484
|
-
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6531
|
+
if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6485
6532
|
const name = attrOf(control, "name");
|
|
6486
6533
|
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6487
6534
|
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
@@ -6525,8 +6572,8 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6525
6572
|
continue;
|
|
6526
6573
|
}
|
|
6527
6574
|
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6528
|
-
for (const el of
|
|
6529
|
-
|
|
6575
|
+
for (const { el, landmark } of formsIn(parsed.roots)) {
|
|
6576
|
+
candidates.push(candidateOf(source.path, dialect, el, imported, landmark));
|
|
6530
6577
|
}
|
|
6531
6578
|
}
|
|
6532
6579
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
@@ -6540,6 +6587,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6540
6587
|
if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
|
|
6541
6588
|
pinned.set(form.id, file);
|
|
6542
6589
|
}
|
|
6590
|
+
for (const form of forms) {
|
|
6591
|
+
if (form.pin && form.pin.trim() !== "" && !pinned.has(form.id)) pinned.set(form.id, form.pin);
|
|
6592
|
+
}
|
|
6543
6593
|
for (const [id, file] of pinned) {
|
|
6544
6594
|
options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
|
|
6545
6595
|
for (const other of forms) {
|
|
@@ -6559,6 +6609,15 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6559
6609
|
for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
|
|
6560
6610
|
beat.set(c, { winner: winners[0], rivals });
|
|
6561
6611
|
}
|
|
6612
|
+
const placed = /* @__PURE__ */ new Map();
|
|
6613
|
+
for (const form of forms) {
|
|
6614
|
+
const list = options.get(form.id);
|
|
6615
|
+
if (list.length < 2 || !form.landmark || form.landmark === "body") continue;
|
|
6616
|
+
const kept = list.filter((c) => c.landmark === form.landmark);
|
|
6617
|
+
if (kept.length === 0 || kept.length === list.length) continue;
|
|
6618
|
+
options.set(form.id, kept);
|
|
6619
|
+
placed.set(form.id, { kept, dropped: list.filter((c) => !kept.includes(c)) });
|
|
6620
|
+
}
|
|
6562
6621
|
const wanted = /* @__PURE__ */ new Map();
|
|
6563
6622
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6564
6623
|
const matched = [];
|
|
@@ -6569,12 +6628,13 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6569
6628
|
for (const form of forms) {
|
|
6570
6629
|
const list = options.get(form.id) ?? [];
|
|
6571
6630
|
if (list.length > 1) {
|
|
6572
|
-
|
|
6573
|
-
form,
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
`${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.${pinned.has(form.id) ? " The pin names that file, and it holds more than one." : ` Name the right one with --pin "${form.name}=<file>".`}
|
|
6577
|
-
|
|
6631
|
+
pending.push({
|
|
6632
|
+
id: form.id,
|
|
6633
|
+
name: form.name,
|
|
6634
|
+
reason: "FORM_AMBIGUOUS",
|
|
6635
|
+
message: `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.${pinned.has(form.id) ? " The pin names that file, and it holds more than one." : ` Name the right one with --pin "${form.name}=<file>".`}`,
|
|
6636
|
+
candidates: [...new Set(list.map((c) => c.file))]
|
|
6637
|
+
});
|
|
6578
6638
|
continue;
|
|
6579
6639
|
}
|
|
6580
6640
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6612,7 +6672,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6612
6672
|
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.`);
|
|
6613
6673
|
continue;
|
|
6614
6674
|
}
|
|
6615
|
-
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6675
|
+
if (attrOf(candidate.el, "data-bcms-form")?.value === form.id) {
|
|
6616
6676
|
alreadyWired++;
|
|
6617
6677
|
onSite.push({ form, file: candidate.file });
|
|
6618
6678
|
continue;
|
|
@@ -6644,6 +6704,12 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6644
6704
|
`form ${form.name}: ${candidate.file} posts every field of ${tie.rivals.map((f) => f.name).join(", ")} too, and more of ${form.name}'s, so it went to ${form.name} \u2014 name the file yourself with --pin "<form>=<file>" if that is wrong.`
|
|
6645
6705
|
);
|
|
6646
6706
|
}
|
|
6707
|
+
const where = placed.get(form.id);
|
|
6708
|
+
if (where) {
|
|
6709
|
+
notes.push(
|
|
6710
|
+
`form ${form.name}: ${where.dropped.map((c) => c.file).join(", ")} post${where.dropped.length === 1 ? "s" : ""} every field of it too, but the built form sat in the ${form.landmark} and only ${candidate.file}'s does \u2014 name the file yourself with --pin "<form>=<file>" if that is wrong.`
|
|
6711
|
+
);
|
|
6712
|
+
}
|
|
6647
6713
|
}
|
|
6648
6714
|
for (const { form, candidate } of matched) {
|
|
6649
6715
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|