@bettercms-ai/convert 0.19.0 → 0.20.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 +8 -0
- package/README.md +33 -3
- package/dist/cli.js +204 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +203 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c4bd14b: `--forms` wires an Astro `<form>` by the CMS form's NAME: `data-bcms-form={bcmsFormId("Contact")}`, `action={bcmsFormAction("Contact")}` and `method={bcmsFormMethod("Contact")}`, resolved at build by a committed `src/bcms-forms.ts` from `bcms-content.json` and `BCMS_API_URL`. The source carries no host and no form id, so a repository built by more than one BetterCMS environment wires each build to the environment that built it. An Astro form wired by id is rewired by name once, one wired by name with a static `method="post"` gets `method={bcmsFormMethod(…)}` once, and one already wired by name counts as wired whatever quotes or spacing a formatter gave the calls; a shared name pends `FORM_NAME_AMBIGUOUS`. Form NAMES are the cross-rung contract; rename on both rungs: a wiring by name is never rewritten, so a form this project calls something else pends `FORM_WIRED_TO_OTHER_NAME` with nothing written, and a wiring by a name no form of the brief covers gets one note. At build the helper never fails: a name it cannot resolve among the project's own non-ignored forms, a shared name or a missing `BCMS_API_URL` renders that form unwired (no `action`, no `method`, no `data-bcms-form`) with one warning line naming `BCMS_FORM_NOT_FOUND`, `BCMS_FORM_NAME_AMBIGUOUS` or `BCMS_FORM_API_URL_MISSING` and the form, and a `bcms-content.json` it cannot parse, or one listing a form with no id or name, renders every form unwired with one `FORMS_SNAPSHOT_UNREADABLE` line, because forms are derived only by the release of a successful build. The helper passes a strict site tsconfig (`astro check`). Its first line, `// @bettercms-ai/convert forms-helper v2`, marks it as ours: a copy at that version or older (v1 included) is rewritten to the current bytes, and a `src/bcms-forms.ts` without that line pends `HELPER_CONFLICT`. A matched `<form>` that already carries every attribute no longer gains a stray space.
|
|
8
|
+
|
|
9
|
+
Not changed: html and JSX forms stay wired by id (html has no build, and JSX may render in a client bundle that cannot use `node:fs`). The backend's orchestration-validation workflow is left unsuffixed; only the convert, section-validation and component-validation workflows are per rung.
|
|
10
|
+
|
|
3
11
|
## 0.19.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -624,13 +624,43 @@ It writes six things onto a matched `<form>` and nothing else: `action="<submitU
|
|
|
624
624
|
`method="post"`, `data-bcms-form="<id>"` (plus `data-bcms-form-success` when the CMS form has a
|
|
625
625
|
success message), `data-bcms-form-field="<key>"` on each matched control, a
|
|
626
626
|
`<p class="bcms-form-msg" hidden>` before `</form>` when there is none, and the submit script once
|
|
627
|
-
per file — the same bytes the hosted renderer ships, pinned by a test.
|
|
628
|
-
|
|
627
|
+
per file — the same bytes the hosted renderer ships, pinned by a test.
|
|
628
|
+
|
|
629
|
+
In an Astro file the form is named, not addressed: `data-bcms-form={bcmsFormId("Contact")}`,
|
|
630
|
+
`action={bcmsFormAction("Contact")}` and `method={bcmsFormMethod("Contact")}`, imported from
|
|
631
|
+
`src/bcms-forms.ts`, which this run adds. At build that helper reads the form's id from
|
|
632
|
+
`bcms-content.json` and the API from `BCMS_API_URL`, so the source holds no host and no id and every
|
|
633
|
+
BetterCMS environment that builds the repository wires it to itself. The built page is unchanged: an
|
|
634
|
+
id, an absolute action and `post`. The helper reads the forms the brief names from: the building
|
|
635
|
+
project's own, ignored ones left out. With no snapshot (a local build) the form renders unwired. The
|
|
636
|
+
helper never fails a build: a name it cannot resolve, a name two forms share, or a build with no
|
|
637
|
+
`BCMS_API_URL` renders that form unwired too (no `action`, no `method`, no `data-bcms-form`, so
|
|
638
|
+
Astro drops all three) and prints one warning line naming the code (`BCMS_FORM_NOT_FOUND`,
|
|
639
|
+
`BCMS_FORM_NAME_AMBIGUOUS`, `BCMS_FORM_API_URL_MISSING`) and the form; a `bcms-content.json` it
|
|
640
|
+
cannot parse, or one listing a form with no id or name, renders every form unwired with one
|
|
641
|
+
`FORMS_SNAPSHOT_UNREADABLE` line. Forms are derived by the release of a successful build and convert
|
|
642
|
+
runs after it, so a helper that threw would leave a first import or a renamed form undeployable for
|
|
643
|
+
good. The helper type-checks under a strict site tsconfig, because `astro check` lints it. Its first
|
|
644
|
+
line, `// @bettercms-ai/convert forms-helper v2`, is what makes the file ours: a copy at that version
|
|
645
|
+
or older is rewritten to the current bytes, a newer one is left alone, and a file without the line is
|
|
646
|
+
`HELPER_CONFLICT`. An Astro form an earlier run wired by id is rewired by name once, a form wired by
|
|
647
|
+
name with v1's static `method="post"` gets `method={bcmsFormMethod(…)}` once, and a form wired by
|
|
648
|
+
name counts as wired whatever quotes or spacing a formatter gave the calls. html and JSX still get
|
|
649
|
+
the literal id and endpoint of the environment that ran the codemod — html has no build, and a JSX
|
|
650
|
+
form may render in a client bundle that cannot read a file.
|
|
651
|
+
|
|
652
|
+
**Form NAMES are the cross-rung contract; rename on both rungs.** A wiring by name is never
|
|
653
|
+
rewritten, whether or not the name resolves on the environment running the codemod: prod and dev
|
|
654
|
+
share the name and nothing else, so rewriting it for one unwires the form on the other. A form this
|
|
655
|
+
project calls something else is pending `FORM_WIRED_TO_OTHER_NAME` and nothing is written; a wiring by
|
|
656
|
+
a name no form of the brief even covers gets one line in `notes`.
|
|
629
657
|
|
|
630
658
|
The match is the FIELD KEYS: a `<form>` is a CMS form's only when the names its controls post are a
|
|
631
659
|
superset of that form's field keys and exactly one `<form>` answers. Everything else is pending with
|
|
632
660
|
a reason — `FORM_NOT_IN_SOURCE`, `FORM_AMBIGUOUS`, `FIELD_UNMATCHED`, `ACTION_IS_EXPRESSION`,
|
|
633
|
-
`DIALECT_UNSUPPORTED`, `PARSE_ERROR
|
|
661
|
+
`DIALECT_UNSUPPORTED`, `PARSE_ERROR`, and for Astro `FORM_NAME_AMBIGUOUS` (two CMS forms share the
|
|
662
|
+
name), `HELPER_CONFLICT` (`src/bcms-forms.ts` is somebody else's file) and
|
|
663
|
+
`FORM_WIRED_TO_OTHER_NAME` (the `<form>` is wired by another name) — because a form wired to the wrong id sends the customer's
|
|
634
664
|
leads into another form's inbox, silently, forever. A `<form>` that already carries
|
|
635
665
|
`data-bcms-form` is `alreadyWired` and is left alone, so a second run is a no-op.
|
|
636
666
|
|
package/dist/cli.js
CHANGED
|
@@ -6434,6 +6434,114 @@ function readPlan(value) {
|
|
|
6434
6434
|
// src/forms.ts
|
|
6435
6435
|
import MagicString4 from "magic-string";
|
|
6436
6436
|
var WIRABLE = ["html", "astro", "jsx"];
|
|
6437
|
+
var BY_NAME = ["astro"];
|
|
6438
|
+
var FORMS_HELPER_PATH = "src/bcms-forms.ts";
|
|
6439
|
+
var FORMS_HELPER_VERSION = 2;
|
|
6440
|
+
var FORMS_HELPER_HEADER = /^\/\/ @bettercms-ai\/convert forms-helper v(\d+)\r?\n/;
|
|
6441
|
+
var formsHelperVersion = (content) => {
|
|
6442
|
+
const m = FORMS_HELPER_HEADER.exec(content);
|
|
6443
|
+
return m ? Number(m[1]) : null;
|
|
6444
|
+
};
|
|
6445
|
+
var FORMS_HELPER_SOURCE = `// @bettercms-ai/convert forms-helper v${FORMS_HELPER_VERSION}
|
|
6446
|
+
// Written by @bettercms-ai/convert; a re-run rewrites it, so do not edit it.
|
|
6447
|
+
// Resolves a BetterCMS form by NAME at build, for the environment that is building this site: the id
|
|
6448
|
+
// from bcms-content.json (written into the tree before the build) and the endpoint from BCMS_API_URL.
|
|
6449
|
+
// The source names no host and no id, so every BetterCMS environment builds its own wiring.
|
|
6450
|
+
// A form it cannot resolve renders unwired (no action, no method, no id) with one warning line, and
|
|
6451
|
+
// the build goes on. Form NAMES are the contract between environments: rename a form on every one.
|
|
6452
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6453
|
+
|
|
6454
|
+
type SnapshotForm = { id: string; name: string; projectId?: string | null; ignoredAt?: string | null };
|
|
6455
|
+
type Snapshot = { projectId: string | null; forms: SnapshotForm[] };
|
|
6456
|
+
type Wiring = { id: string; action: string };
|
|
6457
|
+
|
|
6458
|
+
let snapshot: Snapshot | null | undefined;
|
|
6459
|
+
const resolved = new Map<string, Wiring | undefined>();
|
|
6460
|
+
|
|
6461
|
+
const env = (key: string): string | undefined => process.env[key] || process.env[\`PUBLIC_\${key}\`] || undefined;
|
|
6462
|
+
|
|
6463
|
+
const isForm = (value: unknown): value is SnapshotForm =>
|
|
6464
|
+
typeof value === "object" &&
|
|
6465
|
+
value !== null &&
|
|
6466
|
+
typeof (value as { id?: unknown }).id === "string" &&
|
|
6467
|
+
typeof (value as { name?: unknown }).name === "string";
|
|
6468
|
+
|
|
6469
|
+
function readSnapshot(): Snapshot | null {
|
|
6470
|
+
for (const file of ["bcms-content.json", "bcms-content/forms.json"]) {
|
|
6471
|
+
if (!existsSync(file)) continue;
|
|
6472
|
+
try {
|
|
6473
|
+
const data: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
6474
|
+
const record = (Array.isArray(data) ? { forms: data } : (data ?? {})) as { forms?: unknown; projectId?: unknown };
|
|
6475
|
+
const forms: unknown[] = Array.isArray(record.forms) ? record.forms : [];
|
|
6476
|
+
if (!forms.every(isForm)) throw new Error("a form entry has no id or name");
|
|
6477
|
+
return { projectId: typeof record.projectId === "string" ? record.projectId : null, forms };
|
|
6478
|
+
} catch (error) {
|
|
6479
|
+
// Read once per build, so this is the one line however many forms the site has.
|
|
6480
|
+
console.warn(\`[bcms-forms] FORMS_SNAPSHOT_UNREADABLE: \${file} could not be read (\${error instanceof Error ? error.message : String(error)}); every form renders unwired.\`);
|
|
6481
|
+
return null;
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
return null;
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
function unwired(code: string, name: string, why: string): undefined {
|
|
6488
|
+
console.warn(\`[bcms-forms] \${code}: form "\${name}" renders unwired; \${why}.\`);
|
|
6489
|
+
return undefined;
|
|
6490
|
+
}
|
|
6491
|
+
|
|
6492
|
+
function resolve(name: string): Wiring | undefined {
|
|
6493
|
+
if (snapshot === undefined) snapshot = readSnapshot();
|
|
6494
|
+
// No snapshot (a local dev build), or one it could not read: the form renders unwired, as before.
|
|
6495
|
+
if (snapshot === null) return undefined;
|
|
6496
|
+
const project = env("BCMS_PROJECT_ID") ?? snapshot.projectId;
|
|
6497
|
+
// The forms the conversion brief names from: this project's own, ignored ones left out.
|
|
6498
|
+
const own = snapshot.forms.filter((f) => !f.ignoredAt && (!project || f.projectId === project));
|
|
6499
|
+
const hits = own.filter((f) => f.name === name);
|
|
6500
|
+
const form = hits.length === 1 ? hits[0] : undefined;
|
|
6501
|
+
if (!form && hits.length > 1) {
|
|
6502
|
+
return unwired("BCMS_FORM_NAME_AMBIGUOUS", name, \`\${hits.length} forms in this project share that name; rename one in the Forms tab\`);
|
|
6503
|
+
}
|
|
6504
|
+
if (!form) {
|
|
6505
|
+
return unwired(
|
|
6506
|
+
"BCMS_FORM_NOT_FOUND",
|
|
6507
|
+
name,
|
|
6508
|
+
own.length === 0
|
|
6509
|
+
? "this project has no forms yet; the release of this build derives them"
|
|
6510
|
+
: "no form in this project has that name; a name is shared by every environment, so rename the form on each one",
|
|
6511
|
+
);
|
|
6512
|
+
}
|
|
6513
|
+
const api = env("BCMS_API_URL");
|
|
6514
|
+
if (!api) return unwired("BCMS_FORM_API_URL_MISSING", name, "BCMS_API_URL is not set, so it has nowhere to post");
|
|
6515
|
+
return { id: form.id, action: \`\${api.replace(/\\/$/, "")}/api/v1/forms/public/\${form.id}/submissions\` };
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6518
|
+
/** Resolved once per name, so a form's three attributes print one warning, not three. */
|
|
6519
|
+
function wiring(name: string): Wiring | undefined {
|
|
6520
|
+
if (!resolved.has(name)) resolved.set(name, resolve(name));
|
|
6521
|
+
return resolved.get(name);
|
|
6522
|
+
}
|
|
6523
|
+
|
|
6524
|
+
export const bcmsFormId = (name: string): string | undefined => wiring(name)?.id;
|
|
6525
|
+
|
|
6526
|
+
export const bcmsFormAction = (name: string): string | undefined => wiring(name)?.action;
|
|
6527
|
+
|
|
6528
|
+
/** "post" only for a form that resolved: an unwired form keeps no method, like its action and id. */
|
|
6529
|
+
export const bcmsFormMethod = (name: string): "post" | undefined => (wiring(name) ? "post" : undefined);
|
|
6530
|
+
`;
|
|
6531
|
+
var idCall = (form) => `bcmsFormId(${JSON.stringify(form.name)})`;
|
|
6532
|
+
var actionCall = (form) => `bcmsFormAction(${JSON.stringify(form.name)})`;
|
|
6533
|
+
var methodCall = (form) => `bcmsFormMethod(${JSON.stringify(form.name)})`;
|
|
6534
|
+
function calledName(code, fn) {
|
|
6535
|
+
const m = code === null ? null : new RegExp(`^${fn}\\s*\\(\\s*(["'\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1\\s*,?\\s*\\)$`).exec(code);
|
|
6536
|
+
if (!m) return null;
|
|
6537
|
+
const [, quote, raw] = m;
|
|
6538
|
+
if (quote === "`" && raw.includes("${")) return null;
|
|
6539
|
+
try {
|
|
6540
|
+
return JSON.parse(`"${raw.replace(/\\(['`])/g, "$1").replace(/(?<!\\)"/g, '\\"')}"`);
|
|
6541
|
+
} catch {
|
|
6542
|
+
return null;
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6437
6545
|
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6438
6546
|
var FORM_SCRIPT = `<script>
|
|
6439
6547
|
function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
|
|
@@ -6495,6 +6603,10 @@ function* elementsIn(nodes) {
|
|
|
6495
6603
|
var childElements = (node) => node.children.flatMap(
|
|
6496
6604
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6497
6605
|
);
|
|
6606
|
+
function exprOf(c, name) {
|
|
6607
|
+
const range = attrOf(c.el, name)?.expressionRange;
|
|
6608
|
+
return range ? c.content.slice(range.start + 1, range.end - 1).trim() : null;
|
|
6609
|
+
}
|
|
6498
6610
|
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
|
|
6499
6611
|
function landmarkOf(chain) {
|
|
6500
6612
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
@@ -6516,7 +6628,7 @@ function* formsIn(nodes, chain = []) {
|
|
|
6516
6628
|
}
|
|
6517
6629
|
}
|
|
6518
6630
|
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6519
|
-
function candidateOf(file, dialect, form, imported, landmark, content) {
|
|
6631
|
+
function candidateOf(file, dialect, form, imported, landmark, content, importAt) {
|
|
6520
6632
|
const controls = /* @__PURE__ */ new Map();
|
|
6521
6633
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6522
6634
|
const foreign = /* @__PURE__ */ new Set();
|
|
@@ -6557,7 +6669,7 @@ function candidateOf(file, dialect, form, imported, landmark, content) {
|
|
|
6557
6669
|
content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`/g, "")
|
|
6558
6670
|
);
|
|
6559
6671
|
const dynamic = all.some((control) => !foreign.has(control) && computed(attrOf(control, "name")));
|
|
6560
|
-
return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic };
|
|
6672
|
+
return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic, content, importAt };
|
|
6561
6673
|
}
|
|
6562
6674
|
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6563
6675
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -6590,11 +6702,20 @@ function wire(source, candidate, form) {
|
|
|
6590
6702
|
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6591
6703
|
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6592
6704
|
};
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6705
|
+
const setExpr = (name, code) => {
|
|
6706
|
+
const attr = attrOf(el, name);
|
|
6707
|
+
if (!attr) inserts.push(`${name}={${code}}`);
|
|
6708
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}={${code}}`);
|
|
6709
|
+
};
|
|
6710
|
+
const byName = BY_NAME.includes(dialect);
|
|
6711
|
+
if (byName) setExpr("action", actionCall(form));
|
|
6712
|
+
else set("action", form.submitUrl);
|
|
6713
|
+
if (byName) setExpr("method", methodCall(form));
|
|
6714
|
+
else set("method", "post");
|
|
6715
|
+
if (byName) setExpr("data-bcms-form", idCall(form));
|
|
6716
|
+
else set("data-bcms-form", form.id);
|
|
6596
6717
|
if (form.successMessage) set("data-bcms-form-success", form.successMessage);
|
|
6597
|
-
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6718
|
+
if (inserts.length > 0) source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6598
6719
|
for (const inner of elementsIn(el.children)) {
|
|
6599
6720
|
for (const attr of inner.attrs) {
|
|
6600
6721
|
if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
|
|
@@ -6651,12 +6772,20 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6651
6772
|
}
|
|
6652
6773
|
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6653
6774
|
for (const { el, landmark } of formsIn(parsed.roots)) {
|
|
6654
|
-
candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content));
|
|
6775
|
+
candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
|
|
6655
6776
|
}
|
|
6656
6777
|
}
|
|
6657
6778
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6658
|
-
const
|
|
6659
|
-
const
|
|
6779
|
+
const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
|
|
6780
|
+
const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
|
|
6781
|
+
const wiredTo = (c, f) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") === f.name || byId(c, f) && programAction(c) : byId(c, f);
|
|
6782
|
+
const wiredName = (c) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") : null;
|
|
6783
|
+
const staticPost = (c) => {
|
|
6784
|
+
const method = attrOf(c.el, "method");
|
|
6785
|
+
return !!method && !method.expression && method.value.toLowerCase() === "post";
|
|
6786
|
+
};
|
|
6787
|
+
const claims = (c, f) => wiredTo(c, f) || byId(c, f);
|
|
6788
|
+
const covers2 = (c, f) => c.dynamic && claims(c, f) || f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6660
6789
|
const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6661
6790
|
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6662
6791
|
const pinned = /* @__PURE__ */ new Map();
|
|
@@ -6700,7 +6829,14 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6700
6829
|
const wanted = /* @__PURE__ */ new Map();
|
|
6701
6830
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6702
6831
|
const matched = [];
|
|
6832
|
+
const methodUpgrades = [];
|
|
6833
|
+
const pendedByName = /* @__PURE__ */ new Set();
|
|
6703
6834
|
const pending = [];
|
|
6835
|
+
const existingHelper = sources.find((s) => samePath(s.path, FORMS_HELPER_PATH))?.content;
|
|
6836
|
+
const helperVersion = existingHelper === void 0 ? null : formsHelperVersion(existingHelper);
|
|
6837
|
+
const helperOk = existingHelper === void 0 || helperVersion !== null;
|
|
6838
|
+
const helperStale = existingHelper !== void 0 && helperVersion !== null && helperVersion <= FORMS_HELPER_VERSION && existingHelper !== FORMS_HELPER_SOURCE;
|
|
6839
|
+
let byNameOnSite = false;
|
|
6704
6840
|
const onSite = [];
|
|
6705
6841
|
let alreadyWired = 0;
|
|
6706
6842
|
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
@@ -6739,12 +6875,25 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6739
6875
|
continue;
|
|
6740
6876
|
}
|
|
6741
6877
|
const candidate = list[0];
|
|
6878
|
+
const byName = BY_NAME.includes(candidate.dialect);
|
|
6879
|
+
if (byName && forms.filter((f) => f.name === form.name).length > 1) {
|
|
6880
|
+
pend2(form, "FORM_NAME_AMBIGUOUS", candidate.file, `more than one CMS form is named "${form.name}"; rename one in the Forms tab.`);
|
|
6881
|
+
continue;
|
|
6882
|
+
}
|
|
6742
6883
|
if (wiredTo(candidate, form)) {
|
|
6743
6884
|
alreadyWired++;
|
|
6744
6885
|
onSite.push({ form, file: candidate.file });
|
|
6886
|
+
if (byName) byNameOnSite = true;
|
|
6887
|
+
if (wiredName(candidate) === form.name && staticPost(candidate) && helperOk) methodUpgrades.push({ form, candidate });
|
|
6745
6888
|
continue;
|
|
6746
6889
|
}
|
|
6747
|
-
|
|
6890
|
+
const other = wiredName(candidate);
|
|
6891
|
+
if (other !== null) {
|
|
6892
|
+
pendedByName.add(candidate);
|
|
6893
|
+
pend2(form, "FORM_WIRED_TO_OTHER_NAME", candidate.file, `its <form> is wired by name to "${other}", and a wiring by name is never rewritten: form names are shared by every environment, so give this form the same name on each one.`);
|
|
6894
|
+
continue;
|
|
6895
|
+
}
|
|
6896
|
+
if (programAction(candidate)) {
|
|
6748
6897
|
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6749
6898
|
continue;
|
|
6750
6899
|
}
|
|
@@ -6756,18 +6905,46 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6756
6905
|
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.`);
|
|
6757
6906
|
continue;
|
|
6758
6907
|
}
|
|
6908
|
+
if (byName && !helperOk) {
|
|
6909
|
+
pend2(form, "HELPER_CONFLICT", FORMS_HELPER_PATH, `${FORMS_HELPER_PATH} exists and is not the file @bettercms-ai/convert writes, so nothing may import it. Move it and re-run.`);
|
|
6910
|
+
continue;
|
|
6911
|
+
}
|
|
6759
6912
|
matched.push({ form, candidate });
|
|
6760
6913
|
onSite.push({ form, file: candidate.file });
|
|
6914
|
+
if (byName) byNameOnSite = true;
|
|
6761
6915
|
}
|
|
6762
6916
|
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6763
6917
|
const byFile = /* @__PURE__ */ new Map();
|
|
6764
|
-
|
|
6918
|
+
const editsOf = (path) => {
|
|
6919
|
+
if (!byFile.has(path)) byFile.set(path, { wire: [], upgrade: [] });
|
|
6920
|
+
return byFile.get(path);
|
|
6921
|
+
};
|
|
6922
|
+
for (const m of matched) editsOf(m.candidate.file).wire.push(m);
|
|
6923
|
+
for (const m of methodUpgrades) editsOf(m.candidate.file).upgrade.push(m);
|
|
6765
6924
|
const files = [];
|
|
6766
|
-
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6925
|
+
for (const [path, { wire: list, upgrade }] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6767
6926
|
const content = contents.get(path);
|
|
6768
6927
|
const source = new MagicString4(content);
|
|
6769
6928
|
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6770
|
-
|
|
6929
|
+
for (const { form, candidate } of upgrade) {
|
|
6930
|
+
const method = attrOf(candidate.el, "method");
|
|
6931
|
+
source.overwrite(method.range.start, method.range.end, `method={${methodCall(form)}}`);
|
|
6932
|
+
}
|
|
6933
|
+
const first = (list[0] ?? upgrade[0]).candidate;
|
|
6934
|
+
if (BY_NAME.includes(first.dialect) && first.importAt) {
|
|
6935
|
+
const at = first.importAt;
|
|
6936
|
+
const helper = relativeImport(path, FORMS_HELPER_PATH.replace(/\.ts$/, ""));
|
|
6937
|
+
const names = !/\bbcmsFormId\b/.test(content) ? "bcmsFormAction, bcmsFormId, bcmsFormMethod" : !/\bbcmsFormMethod\b/.test(content) ? "bcmsFormMethod" : null;
|
|
6938
|
+
if (names) {
|
|
6939
|
+
const line = `import { ${names} } from "${helper}";`;
|
|
6940
|
+
if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${line}${at.wrap.after}`);
|
|
6941
|
+
else if (at.at === 0) source.appendLeft(0, `${line}
|
|
6942
|
+
`);
|
|
6943
|
+
else source.appendLeft(at.at, `
|
|
6944
|
+
${line}`);
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
if (list.length > 0 && !content.includes("form[data-bcms-form]")) {
|
|
6771
6948
|
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6772
6949
|
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6773
6950
|
source.appendRight(last.candidate.el.end, `
|
|
@@ -6775,6 +6952,10 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6775
6952
|
}
|
|
6776
6953
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6777
6954
|
}
|
|
6955
|
+
const helperAdded = existingHelper === void 0 && (matched.some((m) => BY_NAME.includes(m.candidate.dialect)) || methodUpgrades.length > 0);
|
|
6956
|
+
if (helperAdded) files.push({ path: FORMS_HELPER_PATH, operation: "add", content: FORMS_HELPER_SOURCE });
|
|
6957
|
+
const helperUpgraded = helperStale && byNameOnSite;
|
|
6958
|
+
if (helperUpgraded) files.push({ path: FORMS_HELPER_PATH, operation: "modify", content: FORMS_HELPER_SOURCE });
|
|
6778
6959
|
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.`);
|
|
6779
6960
|
for (const { form, candidate } of matched) {
|
|
6780
6961
|
const tie = beat.get(candidate);
|
|
@@ -6790,6 +6971,13 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6790
6971
|
);
|
|
6791
6972
|
}
|
|
6792
6973
|
}
|
|
6974
|
+
for (const c of candidates) {
|
|
6975
|
+
const name = wiredName(c);
|
|
6976
|
+
if (name === null || pendedByName.has(c) || forms.some((f) => f.name === name)) continue;
|
|
6977
|
+
notes.push(
|
|
6978
|
+
`${c.file}: its <form> is wired by name to "${name}", which no form of this project has, so this environment's build renders it unwired; it was not rewritten \u2014 form names are shared by every environment, so give the form that name here too.`
|
|
6979
|
+
);
|
|
6980
|
+
}
|
|
6793
6981
|
for (const { form, candidate } of matched) {
|
|
6794
6982
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6795
6983
|
if (keys.length > 0) {
|
|
@@ -6807,8 +6995,8 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6807
6995
|
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6808
6996
|
occurrences: { located: 0, rewritten: 0 },
|
|
6809
6997
|
files: [],
|
|
6810
|
-
helpers: [],
|
|
6811
|
-
helperUpgraded
|
|
6998
|
+
helpers: helperAdded || helperUpgraded ? [FORMS_HELPER_PATH] : [],
|
|
6999
|
+
helperUpgraded,
|
|
6812
7000
|
dynamicBindings: [],
|
|
6813
7001
|
forms: assertForms(forms.length, {
|
|
6814
7002
|
wired: matched.length,
|
|
@@ -9988,7 +10176,7 @@ function styleEdits(before, after) {
|
|
|
9988
10176
|
// package.json
|
|
9989
10177
|
var package_default = {
|
|
9990
10178
|
name: "@bettercms-ai/convert",
|
|
9991
|
-
version: "0.
|
|
10179
|
+
version: "0.20.0",
|
|
9992
10180
|
type: "module",
|
|
9993
10181
|
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.",
|
|
9994
10182
|
bin: {
|