@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 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
- return { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
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
- inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
6468
- if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(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 elementsIn(parsed.roots)) {
6519
- if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
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
- 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
- );
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.16.1",
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: {