@bettercms-ai/convert 0.15.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 +19 -0
- package/README.md +7 -0
- package/dist/cli.js +68 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +49 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.16.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
3
22
|
## 0.15.0
|
|
4
23
|
|
|
5
24
|
### 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
|
@@ -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,34 @@ 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 samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6524
6525
|
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6526
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
6527
|
+
for (const [ref, file] of Object.entries(options_.pins ?? {})) {
|
|
6528
|
+
const form = forms.find((f) => f.id === ref || f.name === ref);
|
|
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
|
+
pinned.set(form.id, file);
|
|
6532
|
+
}
|
|
6533
|
+
for (const [id, file] of pinned) {
|
|
6534
|
+
options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
|
|
6535
|
+
for (const other of forms) {
|
|
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)));
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
const beat = /* @__PURE__ */ new Map();
|
|
6542
|
+
for (const c of candidates) {
|
|
6543
|
+
const claimants = forms.filter((f) => options.get(f.id).includes(c));
|
|
6544
|
+
if (claimants.length < 2) continue;
|
|
6545
|
+
const top = Math.max(...claimants.map((f) => holds(c, f)));
|
|
6546
|
+
const winners = claimants.filter((f) => holds(c, f) === top);
|
|
6547
|
+
if (winners.length !== 1) continue;
|
|
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 });
|
|
6551
|
+
}
|
|
6525
6552
|
const wanted = /* @__PURE__ */ new Map();
|
|
6526
6553
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6527
6554
|
const matched = [];
|
|
@@ -6532,7 +6559,12 @@ async function wireForms(briefIn, sources) {
|
|
|
6532
6559
|
for (const form of forms) {
|
|
6533
6560
|
const list = options.get(form.id) ?? [];
|
|
6534
6561
|
if (list.length > 1) {
|
|
6535
|
-
pend2(
|
|
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
|
+
);
|
|
6536
6568
|
continue;
|
|
6537
6569
|
}
|
|
6538
6570
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6541,7 +6573,13 @@ async function wireForms(briefIn, sources) {
|
|
|
6541
6573
|
}
|
|
6542
6574
|
if (list.length === 0) {
|
|
6543
6575
|
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6544
|
-
|
|
6576
|
+
const pin = pinned.get(form.id);
|
|
6577
|
+
if (pin) {
|
|
6578
|
+
pend2(form, "FORM_NOT_IN_SOURCE", pin, `the pinned file has no <form> posting ${form.fields.map((f) => f.key).join(", ")}.`);
|
|
6579
|
+
} else if (best && best.n === form.fields.length) {
|
|
6580
|
+
const owner = forms.find((f) => f !== form && options.get(f.id).includes(best.c));
|
|
6581
|
+
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.`);
|
|
6582
|
+
} else if (best) {
|
|
6545
6583
|
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6546
6584
|
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6547
6585
|
} else if (unreadable[0]) {
|
|
@@ -6589,6 +6627,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6589
6627
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6590
6628
|
}
|
|
6591
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
|
+
}
|
|
6592
6638
|
for (const { form, candidate } of matched) {
|
|
6593
6639
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6594
6640
|
if (keys.length > 0) {
|
|
@@ -9689,7 +9735,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9689
9735
|
// package.json
|
|
9690
9736
|
var package_default = {
|
|
9691
9737
|
name: "@bettercms-ai/convert",
|
|
9692
|
-
version: "0.
|
|
9738
|
+
version: "0.16.1",
|
|
9693
9739
|
type: "module",
|
|
9694
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.",
|
|
9695
9741
|
bin: {
|
|
@@ -9750,6 +9796,7 @@ function parseArgs(argv) {
|
|
|
9750
9796
|
brief: void 0,
|
|
9751
9797
|
componentize: false,
|
|
9752
9798
|
forms: false,
|
|
9799
|
+
pins: {},
|
|
9753
9800
|
xray: void 0,
|
|
9754
9801
|
plan: void 0,
|
|
9755
9802
|
copy: "bind",
|
|
@@ -9766,7 +9813,16 @@ function parseArgs(argv) {
|
|
|
9766
9813
|
if (arg === "--brief") args.brief = argv[++i];
|
|
9767
9814
|
else if (arg === "--componentize") args.componentize = true;
|
|
9768
9815
|
else if (arg === "--forms") args.forms = true;
|
|
9769
|
-
else if (arg === "--
|
|
9816
|
+
else if (arg === "--pin") {
|
|
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);
|
|
9825
|
+
} else if (arg === "--xray") args.xray = argv[++i];
|
|
9770
9826
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
9771
9827
|
else if (arg === "--copy") {
|
|
9772
9828
|
const mode = argv[++i];
|
|
@@ -9802,6 +9858,9 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
9802
9858
|
endpoint, the id, a marker per field and the submit script. A form
|
|
9803
9859
|
the brief lists as a DRAFT is wired and receipted: publish it in the
|
|
9804
9860
|
Forms tab, or it rejects every submission with a 403.
|
|
9861
|
+
--pin <form>=<file> With --forms: the file a form's <form> is in, when the run reports
|
|
9862
|
+
FORM_AMBIGUOUS (two files post exactly its fields). Form by id or
|
|
9863
|
+
name. Repeatable.
|
|
9805
9864
|
--xray <dist-dir> Measure a LOCAL build instead of converting: how much of each page's
|
|
9806
9865
|
visible copy no binding owns, and the biggest buckets of it. Run it
|
|
9807
9866
|
after a build to see what the next conversion still has to reach,
|
|
@@ -9984,6 +10043,9 @@ async function main() {
|
|
|
9984
10043
|
if (args.forms && args.componentize) {
|
|
9985
10044
|
throw new CliError("--forms and --componentize are separate runs. Do one, then the other. See --help.");
|
|
9986
10045
|
}
|
|
10046
|
+
if (Object.keys(args.pins).length > 0 && !args.forms) {
|
|
10047
|
+
throw new CliError("--pin only means something with --forms. See --help.");
|
|
10048
|
+
}
|
|
9987
10049
|
const root = resolve(args.root);
|
|
9988
10050
|
const brief = JSON.parse(await readFile(resolve(args.brief), "utf-8"));
|
|
9989
10051
|
if (!Array.isArray(brief.pages)) {
|
|
@@ -10025,7 +10087,7 @@ async function main() {
|
|
|
10025
10087
|
return args.strict && receipt2.sections.pending.length > 0 ? 2 : 0;
|
|
10026
10088
|
}
|
|
10027
10089
|
if (args.forms) {
|
|
10028
|
-
const { files: files2, receipt: receipt2 } = await wireForms(brief, sources);
|
|
10090
|
+
const { files: files2, receipt: receipt2 } = await wireForms(brief, sources, { pins: args.pins });
|
|
10029
10091
|
await written(files2);
|
|
10030
10092
|
await saveReceipt(receipt2);
|
|
10031
10093
|
reportForms(files2, receipt2, args.dryRun);
|