@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 +13 -0
- package/README.md +7 -0
- package/dist/cli.js +32 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.js +20 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
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.
|
|
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
|
|
9809
|
-
|
|
9810
|
-
|
|
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);
|