@bettercms-ai/convert 0.16.1 → 0.17.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 +11 -0
- package/dist/cli.js +62 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +61 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 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".
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
3
14
|
## 0.16.1
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -6377,6 +6377,7 @@ function scriptFor(dialect) {
|
|
|
6377
6377
|
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6378
6378
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6379
6379
|
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6380
|
+
var DECLARED_BINDING_ATTR = /^data-bcms-(field|kind|props)$/i;
|
|
6380
6381
|
function* elementsIn(nodes) {
|
|
6381
6382
|
for (const node of nodes) {
|
|
6382
6383
|
if (node.type === "expr") {
|
|
@@ -6391,8 +6392,28 @@ function* elementsIn(nodes) {
|
|
|
6391
6392
|
var childElements = (node) => node.children.flatMap(
|
|
6392
6393
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6393
6394
|
);
|
|
6395
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
|
|
6396
|
+
function landmarkOf(chain) {
|
|
6397
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
6398
|
+
const el = chain[i];
|
|
6399
|
+
if (attrOf(el, "role")?.value === "dialog") return "dialog";
|
|
6400
|
+
if (LANDMARK_TAGS.has(el.tag.toLowerCase())) return el.tag.toLowerCase();
|
|
6401
|
+
}
|
|
6402
|
+
return "body";
|
|
6403
|
+
}
|
|
6404
|
+
function* formsIn(nodes, chain = []) {
|
|
6405
|
+
for (const node of nodes) {
|
|
6406
|
+
if (node.type === "expr") {
|
|
6407
|
+
yield* formsIn(node.children, chain);
|
|
6408
|
+
continue;
|
|
6409
|
+
}
|
|
6410
|
+
if (node.type !== "element") continue;
|
|
6411
|
+
if (node.tag.toLowerCase() === "form") yield { el: node, landmark: landmarkOf(chain) };
|
|
6412
|
+
yield* formsIn(node.children, [...chain, node]);
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6394
6415
|
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6395
|
-
function candidateOf(file, dialect, form, imported) {
|
|
6416
|
+
function candidateOf(file, dialect, form, imported, landmark) {
|
|
6396
6417
|
const controls = /* @__PURE__ */ new Map();
|
|
6397
6418
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6398
6419
|
const foreign = /* @__PURE__ */ new Set();
|
|
@@ -6429,7 +6450,7 @@ function candidateOf(file, dialect, form, imported) {
|
|
|
6429
6450
|
const id = attrOf(control, "id")?.value;
|
|
6430
6451
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6431
6452
|
}
|
|
6432
|
-
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6453
|
+
return { file, dialect, el: form, controls, wrapper, foreign, landmark };
|
|
6433
6454
|
}
|
|
6434
6455
|
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6435
6456
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -6464,14 +6485,21 @@ function wire(source, candidate, form) {
|
|
|
6464
6485
|
};
|
|
6465
6486
|
set("action", form.submitUrl);
|
|
6466
6487
|
set("method", "post");
|
|
6467
|
-
|
|
6468
|
-
if (form.successMessage)
|
|
6488
|
+
set("data-bcms-form", form.id);
|
|
6489
|
+
if (form.successMessage) set("data-bcms-form-success", form.successMessage);
|
|
6469
6490
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6491
|
+
for (const inner of elementsIn(el.children)) {
|
|
6492
|
+
for (const attr of inner.attrs) {
|
|
6493
|
+
if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
|
|
6494
|
+
const start = source.original[attr.range.start - 1] === " " ? attr.range.start - 1 : attr.range.start;
|
|
6495
|
+
source.remove(start, attr.range.end);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6470
6498
|
for (const field of form.fields) {
|
|
6471
6499
|
const control = candidate.controls.get(field.key);
|
|
6472
6500
|
if (candidate.foreign.has(control)) continue;
|
|
6473
6501
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6474
|
-
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6502
|
+
if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6475
6503
|
const name = attrOf(control, "name");
|
|
6476
6504
|
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6477
6505
|
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
@@ -6515,8 +6543,8 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6515
6543
|
continue;
|
|
6516
6544
|
}
|
|
6517
6545
|
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6518
|
-
for (const el of
|
|
6519
|
-
|
|
6546
|
+
for (const { el, landmark } of formsIn(parsed.roots)) {
|
|
6547
|
+
candidates.push(candidateOf(source.path, dialect, el, imported, landmark));
|
|
6520
6548
|
}
|
|
6521
6549
|
}
|
|
6522
6550
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
@@ -6530,6 +6558,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6530
6558
|
if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
|
|
6531
6559
|
pinned.set(form.id, file);
|
|
6532
6560
|
}
|
|
6561
|
+
for (const form of forms) {
|
|
6562
|
+
if (form.pin && form.pin.trim() !== "" && !pinned.has(form.id)) pinned.set(form.id, form.pin);
|
|
6563
|
+
}
|
|
6533
6564
|
for (const [id, file] of pinned) {
|
|
6534
6565
|
options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
|
|
6535
6566
|
for (const other of forms) {
|
|
@@ -6549,6 +6580,15 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6549
6580
|
for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
|
|
6550
6581
|
beat.set(c, { winner: winners[0], rivals });
|
|
6551
6582
|
}
|
|
6583
|
+
const placed = /* @__PURE__ */ new Map();
|
|
6584
|
+
for (const form of forms) {
|
|
6585
|
+
const list = options.get(form.id);
|
|
6586
|
+
if (list.length < 2 || !form.landmark || form.landmark === "body") continue;
|
|
6587
|
+
const kept = list.filter((c) => c.landmark === form.landmark);
|
|
6588
|
+
if (kept.length === 0 || kept.length === list.length) continue;
|
|
6589
|
+
options.set(form.id, kept);
|
|
6590
|
+
placed.set(form.id, { kept, dropped: list.filter((c) => !kept.includes(c)) });
|
|
6591
|
+
}
|
|
6552
6592
|
const wanted = /* @__PURE__ */ new Map();
|
|
6553
6593
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6554
6594
|
const matched = [];
|
|
@@ -6559,12 +6599,13 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6559
6599
|
for (const form of forms) {
|
|
6560
6600
|
const list = options.get(form.id) ?? [];
|
|
6561
6601
|
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
|
-
|
|
6602
|
+
pending.push({
|
|
6603
|
+
id: form.id,
|
|
6604
|
+
name: form.name,
|
|
6605
|
+
reason: "FORM_AMBIGUOUS",
|
|
6606
|
+
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>".`}`,
|
|
6607
|
+
candidates: [...new Set(list.map((c) => c.file))]
|
|
6608
|
+
});
|
|
6568
6609
|
continue;
|
|
6569
6610
|
}
|
|
6570
6611
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6602,7 +6643,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6602
6643
|
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
6644
|
continue;
|
|
6604
6645
|
}
|
|
6605
|
-
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6646
|
+
if (attrOf(candidate.el, "data-bcms-form")?.value === form.id) {
|
|
6606
6647
|
alreadyWired++;
|
|
6607
6648
|
onSite.push({ form, file: candidate.file });
|
|
6608
6649
|
continue;
|
|
@@ -6634,6 +6675,12 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6634
6675
|
`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
6676
|
);
|
|
6636
6677
|
}
|
|
6678
|
+
const where = placed.get(form.id);
|
|
6679
|
+
if (where) {
|
|
6680
|
+
notes.push(
|
|
6681
|
+
`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.`
|
|
6682
|
+
);
|
|
6683
|
+
}
|
|
6637
6684
|
}
|
|
6638
6685
|
for (const { form, candidate } of matched) {
|
|
6639
6686
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
@@ -9735,7 +9782,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9735
9782
|
// package.json
|
|
9736
9783
|
var package_default = {
|
|
9737
9784
|
name: "@bettercms-ai/convert",
|
|
9738
|
-
version: "0.
|
|
9785
|
+
version: "0.17.0",
|
|
9739
9786
|
type: "module",
|
|
9740
9787
|
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
9788
|
bin: {
|