@bettercms-ai/convert 0.15.0 → 0.16.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,11 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 17aaccb: `--forms` breaks the ties that left ScrewFast's two forms `FORM_AMBIGUOUS`: a `<form>` two CMS forms both cover goes to the one it posts more fields of (a contact form is not the newsletter because it also posts `email`), and among several `<form>`s covering one CMS form the one posting nothing beyond its fields wins. For the tie nothing can break — two email-only forms, a footer newsletter and a "forgot password" dialog — the new `--pin "<form>=<file>"` names the file; the pending message says so.
8
+
3
9
  ## 0.15.0
4
10
 
5
11
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -6483,7 +6483,7 @@ function wire(source, candidate, form) {
6483
6483
  source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
6484
6484
  }
6485
6485
  }
6486
- async function wireForms(briefIn, sources) {
6486
+ async function wireForms(briefIn, sources, options_ = {}) {
6487
6487
  const brief = readBrief(briefIn);
6488
6488
  const forms = brief.forms ?? [];
6489
6489
  const candidates = [];
@@ -6521,7 +6521,33 @@ async function wireForms(briefIn, sources) {
6521
6521
  }
6522
6522
  const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
6523
6523
  const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
6524
+ const posts = (c) => new Set(c.controls.values()).size;
6525
+ const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
6524
6526
  const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
6527
+ const pinned = /* @__PURE__ */ new Map();
6528
+ for (const [ref, file] of Object.entries(options_.pins ?? {})) {
6529
+ const form = forms.find((f) => f.id === ref || f.name === ref);
6530
+ if (!form) throw new Error(`--pin ${ref}: the brief lists no form with that id or name.`);
6531
+ pinned.set(form.id, file);
6532
+ options.set(form.id, options.get(form.id).filter((c) => samePath(c.file, file)));
6533
+ for (const other of forms) {
6534
+ if (other.id !== form.id) options.set(other.id, options.get(other.id).filter((c) => !samePath(c.file, file)));
6535
+ }
6536
+ }
6537
+ for (const c of candidates) {
6538
+ const claimants = forms.filter((f) => options.get(f.id).includes(c));
6539
+ if (claimants.length < 2) continue;
6540
+ const top = Math.max(...claimants.map((f) => holds(c, f)));
6541
+ const winners = claimants.filter((f) => holds(c, f) === top);
6542
+ if (winners.length !== 1) continue;
6543
+ for (const f of claimants) if (f !== winners[0]) options.set(f.id, options.get(f.id).filter((x) => x !== c));
6544
+ }
6545
+ for (const f of forms) {
6546
+ const list = options.get(f.id);
6547
+ if (list.length < 2) continue;
6548
+ const exact = list.filter((c) => posts(c) === f.fields.length);
6549
+ if (exact.length === 1) options.set(f.id, exact);
6550
+ }
6525
6551
  const wanted = /* @__PURE__ */ new Map();
6526
6552
  for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
6527
6553
  const matched = [];
@@ -6532,7 +6558,12 @@ async function wireForms(briefIn, sources) {
6532
6558
  for (const form of forms) {
6533
6559
  const list = options.get(form.id) ?? [];
6534
6560
  if (list.length > 1) {
6535
- pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
6561
+ pend2(
6562
+ form,
6563
+ "FORM_AMBIGUOUS",
6564
+ void 0,
6565
+ `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}. Name the right one with --pin "${form.name}=<file>".`
6566
+ );
6536
6567
  continue;
6537
6568
  }
6538
6569
  if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
@@ -6541,7 +6572,13 @@ async function wireForms(briefIn, sources) {
6541
6572
  }
6542
6573
  if (list.length === 0) {
6543
6574
  const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
6544
- if (best) {
6575
+ const pin = pinned.get(form.id);
6576
+ if (pin) {
6577
+ pend2(form, "FORM_NOT_IN_SOURCE", pin, `the pinned file has no <form> posting ${form.fields.map((f) => f.key).join(", ")}.`);
6578
+ } else if (best && best.n === form.fields.length) {
6579
+ const owner = forms.find((f) => f !== form && options.get(f.id).includes(best.c));
6580
+ pend2(form, "FORM_NOT_IN_SOURCE", best.c.file, `the only <form> posting ${form.fields.map((f) => f.key).join(", ")} is "${owner?.name ?? "another form"}"'s.`);
6581
+ } else if (best) {
6545
6582
  const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
6546
6583
  pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
6547
6584
  } else if (unreadable[0]) {
@@ -9689,7 +9726,7 @@ function emitStubs2(dialects, slugs, contents, out) {
9689
9726
  // package.json
9690
9727
  var package_default = {
9691
9728
  name: "@bettercms-ai/convert",
9692
- version: "0.15.0",
9729
+ version: "0.16.0",
9693
9730
  type: "module",
9694
9731
  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.",
9695
9732
  bin: {
@@ -9750,6 +9787,7 @@ function parseArgs(argv) {
9750
9787
  brief: void 0,
9751
9788
  componentize: false,
9752
9789
  forms: false,
9790
+ pins: {},
9753
9791
  xray: void 0,
9754
9792
  plan: void 0,
9755
9793
  copy: "bind",
@@ -9766,7 +9804,11 @@ function parseArgs(argv) {
9766
9804
  if (arg === "--brief") args.brief = argv[++i];
9767
9805
  else if (arg === "--componentize") args.componentize = true;
9768
9806
  else if (arg === "--forms") args.forms = true;
9769
- else if (arg === "--xray") args.xray = argv[++i];
9807
+ else if (arg === "--pin") {
9808
+ const at = (argv[++i] ?? "").indexOf("=");
9809
+ if (at < 1) throw new CliError(`--pin takes <form>=<file>, e.g. --pin "Newsletter=src/components/FooterSection.astro". See --help.`);
9810
+ args.pins[argv[i].slice(0, at)] = argv[i].slice(at + 1);
9811
+ } else if (arg === "--xray") args.xray = argv[++i];
9770
9812
  else if (arg === "--plan") args.plan = argv[++i];
9771
9813
  else if (arg === "--copy") {
9772
9814
  const mode = argv[++i];
@@ -9802,6 +9844,9 @@ var HELP = `bettercms-convert ${VERSION}
9802
9844
  endpoint, the id, a marker per field and the submit script. A form
9803
9845
  the brief lists as a DRAFT is wired and receipted: publish it in the
9804
9846
  Forms tab, or it rejects every submission with a 403.
9847
+ --pin <form>=<file> With --forms: the file a form's <form> is in, when the run reports
9848
+ FORM_AMBIGUOUS (two files post exactly its fields). Form by id or
9849
+ name. Repeatable.
9805
9850
  --xray <dist-dir> Measure a LOCAL build instead of converting: how much of each page's
9806
9851
  visible copy no binding owns, and the biggest buckets of it. Run it
9807
9852
  after a build to see what the next conversion still has to reach,
@@ -10024,8 +10069,11 @@ async function main() {
10024
10069
  reportSections(files2, receipt2, args.dryRun);
10025
10070
  return args.strict && receipt2.sections.pending.length > 0 ? 2 : 0;
10026
10071
  }
10072
+ if (Object.keys(args.pins).length > 0 && !args.forms) {
10073
+ throw new CliError("--pin only means something with --forms. See --help.");
10074
+ }
10027
10075
  if (args.forms) {
10028
- const { files: files2, receipt: receipt2 } = await wireForms(brief, sources);
10076
+ const { files: files2, receipt: receipt2 } = await wireForms(brief, sources, { pins: args.pins });
10029
10077
  await written(files2);
10030
10078
  await saveReceipt(receipt2);
10031
10079
  reportForms(files2, receipt2, args.dryRun);