@bettercms-ai/convert 0.16.0 → 0.16.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,18 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix `--forms` tie-breaking. 0.16.0's second tie-break rule ("the form posting nothing beyond its
8
+ fields wins") could bind a look-alike form — for example a "forgot password" dialog posting only
9
+ `email` — to the newsletter without reporting it. 0.16.1 removes that rule: such cases stay
10
+ `FORM_AMBIGUOUS` until you name the file with `--pin "<form>=<file>"`. When the remaining rule
11
+ (the form posting more of a `<form>`'s fields wins) decides a tie, the receipt now carries a `notes`
12
+ line naming the file and the rival that lost it. `--pin` is also stricter: an empty value or a
13
+ repeated pin for the same form is refused, `--pin` cannot be combined with `--componentize`, and
14
+ two forms pinned to one file both wire.
15
+
3
16
  ## 0.16.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -630,6 +630,13 @@ a reason — `FORM_NOT_IN_SOURCE`, `FORM_AMBIGUOUS`, `FIELD_UNMATCHED`, `ACTION_
630
630
  leads into another form's inbox, silently, forever. A `<form>` that already carries
631
631
  `data-bcms-form` is `alreadyWired` and is left alone, so a second run is a no-op.
632
632
 
633
+ Two `<form>`s that both post exactly a form's fields are `FORM_AMBIGUOUS`, and no rule can break
634
+ that tie: `--pin "<form>=<file>"` (repeatable, form by id or name) names the file, and a pin is
635
+ exclusive only against forms that carry no pin of their own, so two forms living in one file can
636
+ both be pinned to it. **Keep passing a pin on every later run** — a pin is not written into the
637
+ tree, so an unpinned run over the same repository reports that form ambiguous again and its
638
+ receipt stops listing the file it is wired to, even though the markup still is.
639
+
633
640
  `--receipt <file>` writes this run's own receipt (`forms-receipt.json` — a separate file from the
634
641
  P2 run's, since the two runs describe different work), and `--strict` exits 2 on any pending form.
635
642
  `--forms` and `--componentize` are separate runs; doing both in one command is refused.
package/dist/cli.js CHANGED
@@ -6521,32 +6521,33 @@ async function wireForms(briefIn, sources, options_ = {}) {
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
6524
  const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
6526
6525
  const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
6527
6526
  const pinned = /* @__PURE__ */ new Map();
6528
6527
  for (const [ref, file] of Object.entries(options_.pins ?? {})) {
6529
6528
  const form = forms.find((f) => f.id === ref || f.name === ref);
6530
6529
  if (!form) throw new Error(`--pin ${ref}: the brief lists no form with that id or name.`);
6530
+ if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
6531
6531
  pinned.set(form.id, file);
6532
- options.set(form.id, options.get(form.id).filter((c) => samePath(c.file, file)));
6532
+ }
6533
+ for (const [id, file] of pinned) {
6534
+ options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
6533
6535
  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)));
6536
+ const its = pinned.get(other.id);
6537
+ if (other.id === id || its !== void 0 && samePath(its, file)) continue;
6538
+ options.set(other.id, options.get(other.id).filter((c) => !samePath(c.file, file)));
6535
6539
  }
6536
6540
  }
6541
+ const beat = /* @__PURE__ */ new Map();
6537
6542
  for (const c of candidates) {
6538
6543
  const claimants = forms.filter((f) => options.get(f.id).includes(c));
6539
6544
  if (claimants.length < 2) continue;
6540
6545
  const top = Math.max(...claimants.map((f) => holds(c, f)));
6541
6546
  const winners = claimants.filter((f) => holds(c, f) === top);
6542
6547
  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);
6548
+ const rivals = claimants.filter((f) => f !== winners[0]);
6549
+ for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
6550
+ beat.set(c, { winner: winners[0], rivals });
6550
6551
  }
6551
6552
  const wanted = /* @__PURE__ */ new Map();
6552
6553
  for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
@@ -6562,7 +6563,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
6562
6563
  form,
6563
6564
  "FORM_AMBIGUOUS",
6564
6565
  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
+ `${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>".`}`
6566
6567
  );
6567
6568
  continue;
6568
6569
  }
@@ -6626,6 +6627,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6626
6627
  files.push({ path, operation: "modify", content: source.toString() });
6627
6628
  }
6628
6629
  const notes = onSite.filter(({ form }) => form.status === "draft").map(({ form }) => `publish form ${form.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
6630
+ for (const { form, candidate } of matched) {
6631
+ const tie = beat.get(candidate);
6632
+ if (tie?.winner === form) {
6633
+ notes.push(
6634
+ `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
+ );
6636
+ }
6637
+ }
6629
6638
  for (const { form, candidate } of matched) {
6630
6639
  const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
6631
6640
  if (keys.length > 0) {
@@ -9726,7 +9735,7 @@ function emitStubs2(dialects, slugs, contents, out) {
9726
9735
  // package.json
9727
9736
  var package_default = {
9728
9737
  name: "@bettercms-ai/convert",
9729
- version: "0.16.0",
9738
+ version: "0.16.1",
9730
9739
  type: "module",
9731
9740
  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.",
9732
9741
  bin: {
@@ -9805,9 +9814,14 @@ function parseArgs(argv) {
9805
9814
  else if (arg === "--componentize") args.componentize = true;
9806
9815
  else if (arg === "--forms") args.forms = true;
9807
9816
  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);
9817
+ const raw = argv[++i] ?? "";
9818
+ const at = raw.indexOf("=");
9819
+ if (at < 1 || at === raw.length - 1) {
9820
+ throw new CliError(`--pin takes <form>=<file>, e.g. --pin "Newsletter=src/components/FooterSection.astro". See --help.`);
9821
+ }
9822
+ const form = raw.slice(0, at);
9823
+ if (Object.hasOwn(args.pins, form)) throw new CliError(`--pin ${form} twice: give one file. See --help.`);
9824
+ args.pins[form] = raw.slice(at + 1);
9811
9825
  } else if (arg === "--xray") args.xray = argv[++i];
9812
9826
  else if (arg === "--plan") args.plan = argv[++i];
9813
9827
  else if (arg === "--copy") {
@@ -10029,6 +10043,9 @@ async function main() {
10029
10043
  if (args.forms && args.componentize) {
10030
10044
  throw new CliError("--forms and --componentize are separate runs. Do one, then the other. See --help.");
10031
10045
  }
10046
+ if (Object.keys(args.pins).length > 0 && !args.forms) {
10047
+ throw new CliError("--pin only means something with --forms. See --help.");
10048
+ }
10032
10049
  const root = resolve(args.root);
10033
10050
  const brief = JSON.parse(await readFile(resolve(args.brief), "utf-8"));
10034
10051
  if (!Array.isArray(brief.pages)) {
@@ -10069,9 +10086,6 @@ async function main() {
10069
10086
  reportSections(files2, receipt2, args.dryRun);
10070
10087
  return args.strict && receipt2.sections.pending.length > 0 ? 2 : 0;
10071
10088
  }
10072
- if (Object.keys(args.pins).length > 0 && !args.forms) {
10073
- throw new CliError("--pin only means something with --forms. See --help.");
10074
- }
10075
10089
  if (args.forms) {
10076
10090
  const { files: files2, receipt: receipt2 } = await wireForms(brief, sources, { pins: args.pins });
10077
10091
  await written(files2);