@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 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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
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
- inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
6468
- if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(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 elementsIn(parsed.roots)) {
6519
- if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
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
- pend2(
6563
- form,
6564
- "FORM_AMBIGUOUS",
6565
- void 0,
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.16.1",
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: {