@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5a2fcc5: An Astro project that already has a live-preview lane no longer gets the `SSR_DRAFT_ROUTE` pending row. A mounted `<BcmsLiveBlocks>` reads as `bridge`, and `output: "server"` plus a mounted `<BcmsLive>` reads as `draft-route`. Before this, every Astro receipt stayed incomplete no matter what was wired.
|
|
8
|
+
|
|
9
|
+
## 0.17.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 15ecfcb: `--forms` reads two new facts from the brief. `landmark` (where the built form sat: footer, header, nav, main, aside or dialog) breaks the tie between several source `<form>`s posting exactly a form's fields — a footer newsletter is not a "forgot password" dialog — and the choice is disclosed in `notes`. `pin` is the owner's answer from the Forms tab, applied like `--pin`; a flag typed at the keyboard still wins. `FORM_AMBIGUOUS` pending entries now carry `candidates`, the files a person picks from.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- c2cfcb7: `--forms` rewires a `<form>` that carries a stale `data-bcms-form` id — a repository re-imported after the project it was wired to was deleted. The attribute, the action and the success message are replaced in place (never doubled), existing field markers are kept, and the form counts as wired rather than already wired. Only a form carrying the brief form's own id is "already wired".
|
|
18
|
+
- 994a5e6: `--forms` strips page-copy bindings (`data-bcms-field`, `data-bcms-kind`, `data-bcms-props`) declared on elements inside a `<form>` it wires. A form's labels, placeholders and button copy are the CMS form's, rendered by the section's `form` block; a hand conversion that bound them as page copy made the platform derive them beside the form and put the label on the section component twice. Copy around the form — the heading that names it — is untouched.
|
|
19
|
+
|
|
3
20
|
## 0.16.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -6080,9 +6080,28 @@ var pend = (reason, hint, file, line) => ({
|
|
|
6080
6080
|
hint
|
|
6081
6081
|
});
|
|
6082
6082
|
var ASTRO_LANE_PENDING = pend("SSR_DRAFT_ROUTE", SSR_DRAFT_RECIPE);
|
|
6083
|
+
function mounts(content, component) {
|
|
6084
|
+
const from = new RegExp(
|
|
6085
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+["']@bettercms-ai/astro/components/${component}\\.astro["']`
|
|
6086
|
+
);
|
|
6087
|
+
const local = content.match(from)?.[1];
|
|
6088
|
+
return local !== void 0 && new RegExp(`<${local}[\\s/>]`).test(content);
|
|
6089
|
+
}
|
|
6090
|
+
var ASTRO_CONFIG = /(^|\/)astro\.config\.(mjs|cjs|js|ts|mts)$/;
|
|
6091
|
+
function astroLane(sources) {
|
|
6092
|
+
const astro = sources.filter((s) => s.path.toLowerCase().endsWith(".astro"));
|
|
6093
|
+
const server = sources.some(
|
|
6094
|
+
(s) => ASTRO_CONFIG.test(s.path) && /\boutput\s*:\s*["']server["']/.test(s.content)
|
|
6095
|
+
);
|
|
6096
|
+
if (server && astro.some((s) => mounts(s.content, "BcmsLive"))) return "draft-route";
|
|
6097
|
+
if (astro.some((s) => mounts(s.content, "BcmsLiveBlocks"))) return "bridge";
|
|
6098
|
+
return null;
|
|
6099
|
+
}
|
|
6083
6100
|
function canvasBridge(sources, routes) {
|
|
6084
|
-
if (sources.some((s) => s.path.toLowerCase().endsWith(".astro")))
|
|
6085
|
-
|
|
6101
|
+
if (sources.some((s) => s.path.toLowerCase().endsWith(".astro"))) {
|
|
6102
|
+
const lane = astroLane(sources);
|
|
6103
|
+
return lane ? { lane, files: [], edits: /* @__PURE__ */ new Map(), pending: [] } : { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
|
|
6104
|
+
}
|
|
6086
6105
|
const files = [];
|
|
6087
6106
|
const edits = /* @__PURE__ */ new Map();
|
|
6088
6107
|
const pending = [];
|
|
@@ -6377,6 +6396,7 @@ function scriptFor(dialect) {
|
|
|
6377
6396
|
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6378
6397
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6379
6398
|
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6399
|
+
var DECLARED_BINDING_ATTR = /^data-bcms-(field|kind|props)$/i;
|
|
6380
6400
|
function* elementsIn(nodes) {
|
|
6381
6401
|
for (const node of nodes) {
|
|
6382
6402
|
if (node.type === "expr") {
|
|
@@ -6391,8 +6411,28 @@ function* elementsIn(nodes) {
|
|
|
6391
6411
|
var childElements = (node) => node.children.flatMap(
|
|
6392
6412
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6393
6413
|
);
|
|
6414
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
|
|
6415
|
+
function landmarkOf(chain) {
|
|
6416
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
6417
|
+
const el = chain[i];
|
|
6418
|
+
if (attrOf(el, "role")?.value === "dialog") return "dialog";
|
|
6419
|
+
if (LANDMARK_TAGS.has(el.tag.toLowerCase())) return el.tag.toLowerCase();
|
|
6420
|
+
}
|
|
6421
|
+
return "body";
|
|
6422
|
+
}
|
|
6423
|
+
function* formsIn(nodes, chain = []) {
|
|
6424
|
+
for (const node of nodes) {
|
|
6425
|
+
if (node.type === "expr") {
|
|
6426
|
+
yield* formsIn(node.children, chain);
|
|
6427
|
+
continue;
|
|
6428
|
+
}
|
|
6429
|
+
if (node.type !== "element") continue;
|
|
6430
|
+
if (node.tag.toLowerCase() === "form") yield { el: node, landmark: landmarkOf(chain) };
|
|
6431
|
+
yield* formsIn(node.children, [...chain, node]);
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6394
6434
|
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6395
|
-
function candidateOf(file, dialect, form, imported) {
|
|
6435
|
+
function candidateOf(file, dialect, form, imported, landmark) {
|
|
6396
6436
|
const controls = /* @__PURE__ */ new Map();
|
|
6397
6437
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6398
6438
|
const foreign = /* @__PURE__ */ new Set();
|
|
@@ -6429,7 +6469,7 @@ function candidateOf(file, dialect, form, imported) {
|
|
|
6429
6469
|
const id = attrOf(control, "id")?.value;
|
|
6430
6470
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6431
6471
|
}
|
|
6432
|
-
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6472
|
+
return { file, dialect, el: form, controls, wrapper, foreign, landmark };
|
|
6433
6473
|
}
|
|
6434
6474
|
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6435
6475
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -6464,14 +6504,21 @@ function wire(source, candidate, form) {
|
|
|
6464
6504
|
};
|
|
6465
6505
|
set("action", form.submitUrl);
|
|
6466
6506
|
set("method", "post");
|
|
6467
|
-
|
|
6468
|
-
if (form.successMessage)
|
|
6507
|
+
set("data-bcms-form", form.id);
|
|
6508
|
+
if (form.successMessage) set("data-bcms-form-success", form.successMessage);
|
|
6469
6509
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6510
|
+
for (const inner of elementsIn(el.children)) {
|
|
6511
|
+
for (const attr of inner.attrs) {
|
|
6512
|
+
if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
|
|
6513
|
+
const start = source.original[attr.range.start - 1] === " " ? attr.range.start - 1 : attr.range.start;
|
|
6514
|
+
source.remove(start, attr.range.end);
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6470
6517
|
for (const field of form.fields) {
|
|
6471
6518
|
const control = candidate.controls.get(field.key);
|
|
6472
6519
|
if (candidate.foreign.has(control)) continue;
|
|
6473
6520
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6474
|
-
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6521
|
+
if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6475
6522
|
const name = attrOf(control, "name");
|
|
6476
6523
|
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6477
6524
|
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
@@ -6515,8 +6562,8 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6515
6562
|
continue;
|
|
6516
6563
|
}
|
|
6517
6564
|
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6518
|
-
for (const el of
|
|
6519
|
-
|
|
6565
|
+
for (const { el, landmark } of formsIn(parsed.roots)) {
|
|
6566
|
+
candidates.push(candidateOf(source.path, dialect, el, imported, landmark));
|
|
6520
6567
|
}
|
|
6521
6568
|
}
|
|
6522
6569
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
@@ -6530,6 +6577,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6530
6577
|
if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
|
|
6531
6578
|
pinned.set(form.id, file);
|
|
6532
6579
|
}
|
|
6580
|
+
for (const form of forms) {
|
|
6581
|
+
if (form.pin && form.pin.trim() !== "" && !pinned.has(form.id)) pinned.set(form.id, form.pin);
|
|
6582
|
+
}
|
|
6533
6583
|
for (const [id, file] of pinned) {
|
|
6534
6584
|
options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
|
|
6535
6585
|
for (const other of forms) {
|
|
@@ -6549,6 +6599,15 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6549
6599
|
for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
|
|
6550
6600
|
beat.set(c, { winner: winners[0], rivals });
|
|
6551
6601
|
}
|
|
6602
|
+
const placed = /* @__PURE__ */ new Map();
|
|
6603
|
+
for (const form of forms) {
|
|
6604
|
+
const list = options.get(form.id);
|
|
6605
|
+
if (list.length < 2 || !form.landmark || form.landmark === "body") continue;
|
|
6606
|
+
const kept = list.filter((c) => c.landmark === form.landmark);
|
|
6607
|
+
if (kept.length === 0 || kept.length === list.length) continue;
|
|
6608
|
+
options.set(form.id, kept);
|
|
6609
|
+
placed.set(form.id, { kept, dropped: list.filter((c) => !kept.includes(c)) });
|
|
6610
|
+
}
|
|
6552
6611
|
const wanted = /* @__PURE__ */ new Map();
|
|
6553
6612
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6554
6613
|
const matched = [];
|
|
@@ -6559,12 +6618,13 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6559
6618
|
for (const form of forms) {
|
|
6560
6619
|
const list = options.get(form.id) ?? [];
|
|
6561
6620
|
if (list.length > 1) {
|
|
6562
|
-
|
|
6563
|
-
form,
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
`${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>".`}
|
|
6567
|
-
|
|
6621
|
+
pending.push({
|
|
6622
|
+
id: form.id,
|
|
6623
|
+
name: form.name,
|
|
6624
|
+
reason: "FORM_AMBIGUOUS",
|
|
6625
|
+
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>".`}`,
|
|
6626
|
+
candidates: [...new Set(list.map((c) => c.file))]
|
|
6627
|
+
});
|
|
6568
6628
|
continue;
|
|
6569
6629
|
}
|
|
6570
6630
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6602,7 +6662,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6602
6662
|
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.`);
|
|
6603
6663
|
continue;
|
|
6604
6664
|
}
|
|
6605
|
-
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6665
|
+
if (attrOf(candidate.el, "data-bcms-form")?.value === form.id) {
|
|
6606
6666
|
alreadyWired++;
|
|
6607
6667
|
onSite.push({ form, file: candidate.file });
|
|
6608
6668
|
continue;
|
|
@@ -6634,6 +6694,12 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6634
6694
|
`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.`
|
|
6635
6695
|
);
|
|
6636
6696
|
}
|
|
6697
|
+
const where = placed.get(form.id);
|
|
6698
|
+
if (where) {
|
|
6699
|
+
notes.push(
|
|
6700
|
+
`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.`
|
|
6701
|
+
);
|
|
6702
|
+
}
|
|
6637
6703
|
}
|
|
6638
6704
|
for (const { form, candidate } of matched) {
|
|
6639
6705
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
@@ -9735,7 +9801,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9735
9801
|
// package.json
|
|
9736
9802
|
var package_default = {
|
|
9737
9803
|
name: "@bettercms-ai/convert",
|
|
9738
|
-
version: "0.
|
|
9804
|
+
version: "0.17.1",
|
|
9739
9805
|
type: "module",
|
|
9740
9806
|
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.",
|
|
9741
9807
|
bin: {
|