@bettercms-ai/convert 0.18.2 → 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 +23 -0
- package/README.md +33 -3
- package/dist/cli.js +258 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +256 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -428,8 +428,12 @@ interface ConversionReceipt {
|
|
|
428
428
|
* `ACTION_IS_EXPRESSION` the `<form>`'s `action` is code; replacing it would rewrite a program.
|
|
429
429
|
* `DIALECT_UNSUPPORTED` the `<form>` is in a svelte/vue file, which this lane does not write.
|
|
430
430
|
* `PARSE_ERROR` the file holding it did not parse.
|
|
431
|
+
* `FORM_NAME_AMBIGUOUS` an Astro form is wired by name, and two CMS forms share this one's.
|
|
432
|
+
* `HELPER_CONFLICT` `src/bcms-forms.ts` exists with bytes this package did not write.
|
|
433
|
+
* `FORM_WIRED_TO_OTHER_NAME` an Astro `<form>` is already wired by another name. Never rewritten:
|
|
434
|
+
* the name is what every environment building the repo shares.
|
|
431
435
|
*/
|
|
432
|
-
type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR";
|
|
436
|
+
type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR" | "FORM_NAME_AMBIGUOUS" | "HELPER_CONFLICT" | "FORM_WIRED_TO_OTHER_NAME";
|
|
433
437
|
interface PendingForm {
|
|
434
438
|
id: string;
|
|
435
439
|
name: string;
|
|
@@ -951,7 +955,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
|
|
|
951
955
|
declare function propsAttribute(props: AttrBinding[]): string;
|
|
952
956
|
|
|
953
957
|
/** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
|
|
954
|
-
declare const HELPER_VERSION =
|
|
958
|
+
declare const HELPER_VERSION = 9;
|
|
955
959
|
/** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
|
|
956
960
|
declare const HELPER_PATH: Record<Dialect, string | null>;
|
|
957
961
|
/** The directory the snapshots live in, at the repository root. */
|
package/dist/index.js
CHANGED
|
@@ -4187,7 +4187,7 @@ async function bindImages(ctx, targets) {
|
|
|
4187
4187
|
|
|
4188
4188
|
// src/helper.ts
|
|
4189
4189
|
import { createHash as createHash2 } from "crypto";
|
|
4190
|
-
var HELPER_VERSION =
|
|
4190
|
+
var HELPER_VERSION = 9;
|
|
4191
4191
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4192
4192
|
var HELPER_PATH = Object.fromEntries(
|
|
4193
4193
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4352,9 +4352,19 @@ export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: st
|
|
|
4352
4352
|
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
4353
4353
|
* committed \`{}\` stub.
|
|
4354
4354
|
*/
|
|
4355
|
-
export function bcmsSection(
|
|
4355
|
+
export function bcmsSection(
|
|
4356
|
+
page: unknown,
|
|
4357
|
+
bind?: string,
|
|
4358
|
+
overrides?: unknown,
|
|
4359
|
+
bindPaths?: Record<string, string>,
|
|
4360
|
+
): Record<string, unknown> {
|
|
4356
4361
|
const group = bind ? readPath(page, bind) : undefined;
|
|
4357
4362
|
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
4363
|
+
// A locale copy on its twin's component spells its fields its own way: each prop's copy is
|
|
4364
|
+
// read from the path the placement names for it.
|
|
4365
|
+
if (bind && isRecord(bindPaths)) {
|
|
4366
|
+
for (const [key, path] of Object.entries(bindPaths)) base[key] = readPath(page, path);
|
|
4367
|
+
}
|
|
4358
4368
|
if (!isRecord(overrides)) return base;
|
|
4359
4369
|
for (const key of Object.keys(overrides)) {
|
|
4360
4370
|
const value = overrides[key];
|
|
@@ -4417,21 +4427,25 @@ export function bcmsSectionPaths(
|
|
|
4417
4427
|
overrides: unknown,
|
|
4418
4428
|
prefix: string,
|
|
4419
4429
|
map: BcmsPathMap,
|
|
4430
|
+
bindPaths?: Record<string, string>,
|
|
4420
4431
|
): Record<string, unknown> {
|
|
4421
4432
|
const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
|
|
4422
4433
|
const own = isRecord(overrides) ? overrides : {};
|
|
4423
4434
|
const separator = prefix.slice(-1);
|
|
4424
|
-
|
|
4425
|
-
|
|
4435
|
+
// A re-keyed locale copy names each prop's path on its own page; any other placement re-bases.
|
|
4436
|
+
const fromPage = (path: string, key: string): unknown =>
|
|
4437
|
+
!bind ? undefined
|
|
4438
|
+
: isRecord(bindPaths) && typeof bindPaths[key] === "string" ? readPath(page, bindPaths[key])
|
|
4439
|
+
: readPath(page, bind + separator + path.slice(prefix.length));
|
|
4426
4440
|
for (const [path, key] of Object.entries(map.scalars ?? {})) {
|
|
4427
4441
|
const value = own[key];
|
|
4428
|
-
out[path] = value !== undefined && value !== null ? value : fromPage(path);
|
|
4442
|
+
out[path] = value !== undefined && value !== null ? value : fromPage(path, key);
|
|
4429
4443
|
}
|
|
4430
4444
|
for (const [path, table] of Object.entries(map.tables ?? {})) {
|
|
4431
4445
|
const held = own[table.key];
|
|
4432
4446
|
const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
|
|
4433
4447
|
if (!Array.isArray(rows)) {
|
|
4434
|
-
out[path] = fromPage(path);
|
|
4448
|
+
out[path] = fromPage(path, table.key);
|
|
4435
4449
|
continue;
|
|
4436
4450
|
}
|
|
4437
4451
|
const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
|
|
@@ -4446,12 +4460,17 @@ export function bcmsSectionPaths(
|
|
|
4446
4460
|
}
|
|
4447
4461
|
|
|
4448
4462
|
export function bcmsFields(
|
|
4449
|
-
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
4463
|
+
placement: { blockId?: string; bind?: string; overrides?: unknown; bindPaths?: Record<string, string> },
|
|
4450
4464
|
pathPrefix: string,
|
|
4451
4465
|
map?: BcmsPathMap,
|
|
4452
4466
|
): (path: string) => string {
|
|
4453
4467
|
const id = placement.blockId;
|
|
4454
4468
|
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
4469
|
+
/** A re-keyed locale copy's own path for a prop, with whatever follows the prop kept. */
|
|
4470
|
+
const moved = (key: string | undefined, rest: string): string | undefined => {
|
|
4471
|
+
const paths = placement.bind && isRecord(placement.bindPaths) ? placement.bindPaths : undefined;
|
|
4472
|
+
return key !== undefined && paths && typeof paths[key] === "string" ? paths[key] + rest : undefined;
|
|
4473
|
+
};
|
|
4455
4474
|
/**
|
|
4456
4475
|
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
4457
4476
|
*
|
|
@@ -4470,11 +4489,14 @@ export function bcmsFields(
|
|
|
4470
4489
|
const separator = pathPrefix.slice(-1);
|
|
4471
4490
|
return (path: string): string => {
|
|
4472
4491
|
if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
|
|
4473
|
-
if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
|
|
4474
4492
|
const scalar = map.scalars?.[path];
|
|
4475
|
-
if (scalar !== undefined) return id + "__overrides." + scalar;
|
|
4476
4493
|
const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
|
|
4477
4494
|
const table = row ? map.tables?.[row[1]] : undefined;
|
|
4495
|
+
if (!own) {
|
|
4496
|
+
return moved(scalar, "") ?? moved(table?.key, row ? "[" + row[2] + "]." + row[3] : "")
|
|
4497
|
+
?? placement.bind + separator + path.slice(pathPrefix.length);
|
|
4498
|
+
}
|
|
4499
|
+
if (scalar !== undefined) return id + "__overrides." + scalar;
|
|
4478
4500
|
const column = row && table ? table.columns[row[3]] : undefined;
|
|
4479
4501
|
return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
|
|
4480
4502
|
};
|
|
@@ -4484,7 +4506,9 @@ export function bcmsFields(
|
|
|
4484
4506
|
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
4485
4507
|
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
4486
4508
|
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
4487
|
-
|
|
4509
|
+
const rest = path.slice(pathPrefix.length);
|
|
4510
|
+
const key = /^[^.[]+/.exec(rest)?.[0];
|
|
4511
|
+
return (own ? undefined : moved(key, rest.slice(key?.length ?? 0))) ?? lane + rest;
|
|
4488
4512
|
};
|
|
4489
4513
|
}
|
|
4490
4514
|
`;
|
|
@@ -4497,7 +4521,8 @@ var HISTORICAL = {
|
|
|
4497
4521
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4498
4522
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4499
4523
|
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4500
|
-
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4524
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
|
|
4525
|
+
8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9"
|
|
4501
4526
|
},
|
|
4502
4527
|
"src/lib/bcms-content.ts": {
|
|
4503
4528
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
@@ -4506,7 +4531,8 @@ var HISTORICAL = {
|
|
|
4506
4531
|
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4507
4532
|
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
|
|
4508
4533
|
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5",
|
|
4509
|
-
7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7"
|
|
4534
|
+
7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7",
|
|
4535
|
+
8: "1a6f669d76d42effe77f082436e9d21c0efa6fc499e919e3d116bc3a27a7f3d5"
|
|
4510
4536
|
},
|
|
4511
4537
|
"composables/bcms-content.ts": {
|
|
4512
4538
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
@@ -4515,7 +4541,8 @@ var HISTORICAL = {
|
|
|
4515
4541
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4516
4542
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4517
4543
|
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4518
|
-
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4544
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
|
|
4545
|
+
8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9"
|
|
4519
4546
|
}
|
|
4520
4547
|
};
|
|
4521
4548
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -6416,6 +6443,114 @@ function readPlan(value) {
|
|
|
6416
6443
|
// src/forms.ts
|
|
6417
6444
|
import MagicString4 from "magic-string";
|
|
6418
6445
|
var WIRABLE = ["html", "astro", "jsx"];
|
|
6446
|
+
var BY_NAME = ["astro"];
|
|
6447
|
+
var FORMS_HELPER_PATH = "src/bcms-forms.ts";
|
|
6448
|
+
var FORMS_HELPER_VERSION = 2;
|
|
6449
|
+
var FORMS_HELPER_HEADER = /^\/\/ @bettercms-ai\/convert forms-helper v(\d+)\r?\n/;
|
|
6450
|
+
var formsHelperVersion = (content) => {
|
|
6451
|
+
const m = FORMS_HELPER_HEADER.exec(content);
|
|
6452
|
+
return m ? Number(m[1]) : null;
|
|
6453
|
+
};
|
|
6454
|
+
var FORMS_HELPER_SOURCE = `// @bettercms-ai/convert forms-helper v${FORMS_HELPER_VERSION}
|
|
6455
|
+
// Written by @bettercms-ai/convert; a re-run rewrites it, so do not edit it.
|
|
6456
|
+
// Resolves a BetterCMS form by NAME at build, for the environment that is building this site: the id
|
|
6457
|
+
// from bcms-content.json (written into the tree before the build) and the endpoint from BCMS_API_URL.
|
|
6458
|
+
// The source names no host and no id, so every BetterCMS environment builds its own wiring.
|
|
6459
|
+
// A form it cannot resolve renders unwired (no action, no method, no id) with one warning line, and
|
|
6460
|
+
// the build goes on. Form NAMES are the contract between environments: rename a form on every one.
|
|
6461
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6462
|
+
|
|
6463
|
+
type SnapshotForm = { id: string; name: string; projectId?: string | null; ignoredAt?: string | null };
|
|
6464
|
+
type Snapshot = { projectId: string | null; forms: SnapshotForm[] };
|
|
6465
|
+
type Wiring = { id: string; action: string };
|
|
6466
|
+
|
|
6467
|
+
let snapshot: Snapshot | null | undefined;
|
|
6468
|
+
const resolved = new Map<string, Wiring | undefined>();
|
|
6469
|
+
|
|
6470
|
+
const env = (key: string): string | undefined => process.env[key] || process.env[\`PUBLIC_\${key}\`] || undefined;
|
|
6471
|
+
|
|
6472
|
+
const isForm = (value: unknown): value is SnapshotForm =>
|
|
6473
|
+
typeof value === "object" &&
|
|
6474
|
+
value !== null &&
|
|
6475
|
+
typeof (value as { id?: unknown }).id === "string" &&
|
|
6476
|
+
typeof (value as { name?: unknown }).name === "string";
|
|
6477
|
+
|
|
6478
|
+
function readSnapshot(): Snapshot | null {
|
|
6479
|
+
for (const file of ["bcms-content.json", "bcms-content/forms.json"]) {
|
|
6480
|
+
if (!existsSync(file)) continue;
|
|
6481
|
+
try {
|
|
6482
|
+
const data: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
6483
|
+
const record = (Array.isArray(data) ? { forms: data } : (data ?? {})) as { forms?: unknown; projectId?: unknown };
|
|
6484
|
+
const forms: unknown[] = Array.isArray(record.forms) ? record.forms : [];
|
|
6485
|
+
if (!forms.every(isForm)) throw new Error("a form entry has no id or name");
|
|
6486
|
+
return { projectId: typeof record.projectId === "string" ? record.projectId : null, forms };
|
|
6487
|
+
} catch (error) {
|
|
6488
|
+
// Read once per build, so this is the one line however many forms the site has.
|
|
6489
|
+
console.warn(\`[bcms-forms] FORMS_SNAPSHOT_UNREADABLE: \${file} could not be read (\${error instanceof Error ? error.message : String(error)}); every form renders unwired.\`);
|
|
6490
|
+
return null;
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
return null;
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
function unwired(code: string, name: string, why: string): undefined {
|
|
6497
|
+
console.warn(\`[bcms-forms] \${code}: form "\${name}" renders unwired; \${why}.\`);
|
|
6498
|
+
return undefined;
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6501
|
+
function resolve(name: string): Wiring | undefined {
|
|
6502
|
+
if (snapshot === undefined) snapshot = readSnapshot();
|
|
6503
|
+
// No snapshot (a local dev build), or one it could not read: the form renders unwired, as before.
|
|
6504
|
+
if (snapshot === null) return undefined;
|
|
6505
|
+
const project = env("BCMS_PROJECT_ID") ?? snapshot.projectId;
|
|
6506
|
+
// The forms the conversion brief names from: this project's own, ignored ones left out.
|
|
6507
|
+
const own = snapshot.forms.filter((f) => !f.ignoredAt && (!project || f.projectId === project));
|
|
6508
|
+
const hits = own.filter((f) => f.name === name);
|
|
6509
|
+
const form = hits.length === 1 ? hits[0] : undefined;
|
|
6510
|
+
if (!form && hits.length > 1) {
|
|
6511
|
+
return unwired("BCMS_FORM_NAME_AMBIGUOUS", name, \`\${hits.length} forms in this project share that name; rename one in the Forms tab\`);
|
|
6512
|
+
}
|
|
6513
|
+
if (!form) {
|
|
6514
|
+
return unwired(
|
|
6515
|
+
"BCMS_FORM_NOT_FOUND",
|
|
6516
|
+
name,
|
|
6517
|
+
own.length === 0
|
|
6518
|
+
? "this project has no forms yet; the release of this build derives them"
|
|
6519
|
+
: "no form in this project has that name; a name is shared by every environment, so rename the form on each one",
|
|
6520
|
+
);
|
|
6521
|
+
}
|
|
6522
|
+
const api = env("BCMS_API_URL");
|
|
6523
|
+
if (!api) return unwired("BCMS_FORM_API_URL_MISSING", name, "BCMS_API_URL is not set, so it has nowhere to post");
|
|
6524
|
+
return { id: form.id, action: \`\${api.replace(/\\/$/, "")}/api/v1/forms/public/\${form.id}/submissions\` };
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
/** Resolved once per name, so a form's three attributes print one warning, not three. */
|
|
6528
|
+
function wiring(name: string): Wiring | undefined {
|
|
6529
|
+
if (!resolved.has(name)) resolved.set(name, resolve(name));
|
|
6530
|
+
return resolved.get(name);
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6533
|
+
export const bcmsFormId = (name: string): string | undefined => wiring(name)?.id;
|
|
6534
|
+
|
|
6535
|
+
export const bcmsFormAction = (name: string): string | undefined => wiring(name)?.action;
|
|
6536
|
+
|
|
6537
|
+
/** "post" only for a form that resolved: an unwired form keeps no method, like its action and id. */
|
|
6538
|
+
export const bcmsFormMethod = (name: string): "post" | undefined => (wiring(name) ? "post" : undefined);
|
|
6539
|
+
`;
|
|
6540
|
+
var idCall = (form) => `bcmsFormId(${JSON.stringify(form.name)})`;
|
|
6541
|
+
var actionCall = (form) => `bcmsFormAction(${JSON.stringify(form.name)})`;
|
|
6542
|
+
var methodCall = (form) => `bcmsFormMethod(${JSON.stringify(form.name)})`;
|
|
6543
|
+
function calledName(code, fn) {
|
|
6544
|
+
const m = code === null ? null : new RegExp(`^${fn}\\s*\\(\\s*(["'\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1\\s*,?\\s*\\)$`).exec(code);
|
|
6545
|
+
if (!m) return null;
|
|
6546
|
+
const [, quote, raw] = m;
|
|
6547
|
+
if (quote === "`" && raw.includes("${")) return null;
|
|
6548
|
+
try {
|
|
6549
|
+
return JSON.parse(`"${raw.replace(/\\(['`])/g, "$1").replace(/(?<!\\)"/g, '\\"')}"`);
|
|
6550
|
+
} catch {
|
|
6551
|
+
return null;
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6419
6554
|
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6420
6555
|
var FORM_SCRIPT = `<script>
|
|
6421
6556
|
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:'';}
|
|
@@ -6477,6 +6612,10 @@ function* elementsIn(nodes) {
|
|
|
6477
6612
|
var childElements = (node) => node.children.flatMap(
|
|
6478
6613
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6479
6614
|
);
|
|
6615
|
+
function exprOf(c, name) {
|
|
6616
|
+
const range = attrOf(c.el, name)?.expressionRange;
|
|
6617
|
+
return range ? c.content.slice(range.start + 1, range.end - 1).trim() : null;
|
|
6618
|
+
}
|
|
6480
6619
|
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
|
|
6481
6620
|
function landmarkOf(chain) {
|
|
6482
6621
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
@@ -6498,7 +6637,7 @@ function* formsIn(nodes, chain = []) {
|
|
|
6498
6637
|
}
|
|
6499
6638
|
}
|
|
6500
6639
|
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6501
|
-
function candidateOf(file, dialect, form, imported, landmark, content) {
|
|
6640
|
+
function candidateOf(file, dialect, form, imported, landmark, content, importAt) {
|
|
6502
6641
|
const controls = /* @__PURE__ */ new Map();
|
|
6503
6642
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6504
6643
|
const foreign = /* @__PURE__ */ new Set();
|
|
@@ -6539,7 +6678,7 @@ function candidateOf(file, dialect, form, imported, landmark, content) {
|
|
|
6539
6678
|
content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`/g, "")
|
|
6540
6679
|
);
|
|
6541
6680
|
const dynamic = all.some((control) => !foreign.has(control) && computed(attrOf(control, "name")));
|
|
6542
|
-
return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic };
|
|
6681
|
+
return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic, content, importAt };
|
|
6543
6682
|
}
|
|
6544
6683
|
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6545
6684
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -6572,11 +6711,20 @@ function wire(source, candidate, form) {
|
|
|
6572
6711
|
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6573
6712
|
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6574
6713
|
};
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6714
|
+
const setExpr = (name, code) => {
|
|
6715
|
+
const attr = attrOf(el, name);
|
|
6716
|
+
if (!attr) inserts.push(`${name}={${code}}`);
|
|
6717
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}={${code}}`);
|
|
6718
|
+
};
|
|
6719
|
+
const byName = BY_NAME.includes(dialect);
|
|
6720
|
+
if (byName) setExpr("action", actionCall(form));
|
|
6721
|
+
else set("action", form.submitUrl);
|
|
6722
|
+
if (byName) setExpr("method", methodCall(form));
|
|
6723
|
+
else set("method", "post");
|
|
6724
|
+
if (byName) setExpr("data-bcms-form", idCall(form));
|
|
6725
|
+
else set("data-bcms-form", form.id);
|
|
6578
6726
|
if (form.successMessage) set("data-bcms-form-success", form.successMessage);
|
|
6579
|
-
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6727
|
+
if (inserts.length > 0) source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6580
6728
|
for (const inner of elementsIn(el.children)) {
|
|
6581
6729
|
for (const attr of inner.attrs) {
|
|
6582
6730
|
if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
|
|
@@ -6633,12 +6781,20 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6633
6781
|
}
|
|
6634
6782
|
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6635
6783
|
for (const { el, landmark } of formsIn(parsed.roots)) {
|
|
6636
|
-
candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content));
|
|
6784
|
+
candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
|
|
6637
6785
|
}
|
|
6638
6786
|
}
|
|
6639
6787
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6640
|
-
const
|
|
6641
|
-
const
|
|
6788
|
+
const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
|
|
6789
|
+
const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
|
|
6790
|
+
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);
|
|
6791
|
+
const wiredName = (c) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") : null;
|
|
6792
|
+
const staticPost = (c) => {
|
|
6793
|
+
const method = attrOf(c.el, "method");
|
|
6794
|
+
return !!method && !method.expression && method.value.toLowerCase() === "post";
|
|
6795
|
+
};
|
|
6796
|
+
const claims = (c, f) => wiredTo(c, f) || byId(c, f);
|
|
6797
|
+
const covers2 = (c, f) => c.dynamic && claims(c, f) || f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6642
6798
|
const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6643
6799
|
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6644
6800
|
const pinned = /* @__PURE__ */ new Map();
|
|
@@ -6682,7 +6838,14 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6682
6838
|
const wanted = /* @__PURE__ */ new Map();
|
|
6683
6839
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6684
6840
|
const matched = [];
|
|
6841
|
+
const methodUpgrades = [];
|
|
6842
|
+
const pendedByName = /* @__PURE__ */ new Set();
|
|
6685
6843
|
const pending = [];
|
|
6844
|
+
const existingHelper = sources.find((s) => samePath(s.path, FORMS_HELPER_PATH))?.content;
|
|
6845
|
+
const helperVersion = existingHelper === void 0 ? null : formsHelperVersion(existingHelper);
|
|
6846
|
+
const helperOk = existingHelper === void 0 || helperVersion !== null;
|
|
6847
|
+
const helperStale = existingHelper !== void 0 && helperVersion !== null && helperVersion <= FORMS_HELPER_VERSION && existingHelper !== FORMS_HELPER_SOURCE;
|
|
6848
|
+
let byNameOnSite = false;
|
|
6686
6849
|
const onSite = [];
|
|
6687
6850
|
let alreadyWired = 0;
|
|
6688
6851
|
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
@@ -6721,12 +6884,25 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6721
6884
|
continue;
|
|
6722
6885
|
}
|
|
6723
6886
|
const candidate = list[0];
|
|
6887
|
+
const byName = BY_NAME.includes(candidate.dialect);
|
|
6888
|
+
if (byName && forms.filter((f) => f.name === form.name).length > 1) {
|
|
6889
|
+
pend2(form, "FORM_NAME_AMBIGUOUS", candidate.file, `more than one CMS form is named "${form.name}"; rename one in the Forms tab.`);
|
|
6890
|
+
continue;
|
|
6891
|
+
}
|
|
6724
6892
|
if (wiredTo(candidate, form)) {
|
|
6725
6893
|
alreadyWired++;
|
|
6726
6894
|
onSite.push({ form, file: candidate.file });
|
|
6895
|
+
if (byName) byNameOnSite = true;
|
|
6896
|
+
if (wiredName(candidate) === form.name && staticPost(candidate) && helperOk) methodUpgrades.push({ form, candidate });
|
|
6897
|
+
continue;
|
|
6898
|
+
}
|
|
6899
|
+
const other = wiredName(candidate);
|
|
6900
|
+
if (other !== null) {
|
|
6901
|
+
pendedByName.add(candidate);
|
|
6902
|
+
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.`);
|
|
6727
6903
|
continue;
|
|
6728
6904
|
}
|
|
6729
|
-
if (
|
|
6905
|
+
if (programAction(candidate)) {
|
|
6730
6906
|
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6731
6907
|
continue;
|
|
6732
6908
|
}
|
|
@@ -6738,18 +6914,46 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6738
6914
|
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.`);
|
|
6739
6915
|
continue;
|
|
6740
6916
|
}
|
|
6917
|
+
if (byName && !helperOk) {
|
|
6918
|
+
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.`);
|
|
6919
|
+
continue;
|
|
6920
|
+
}
|
|
6741
6921
|
matched.push({ form, candidate });
|
|
6742
6922
|
onSite.push({ form, file: candidate.file });
|
|
6923
|
+
if (byName) byNameOnSite = true;
|
|
6743
6924
|
}
|
|
6744
6925
|
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6745
6926
|
const byFile = /* @__PURE__ */ new Map();
|
|
6746
|
-
|
|
6927
|
+
const editsOf = (path) => {
|
|
6928
|
+
if (!byFile.has(path)) byFile.set(path, { wire: [], upgrade: [] });
|
|
6929
|
+
return byFile.get(path);
|
|
6930
|
+
};
|
|
6931
|
+
for (const m of matched) editsOf(m.candidate.file).wire.push(m);
|
|
6932
|
+
for (const m of methodUpgrades) editsOf(m.candidate.file).upgrade.push(m);
|
|
6747
6933
|
const files = [];
|
|
6748
|
-
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6934
|
+
for (const [path, { wire: list, upgrade }] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6749
6935
|
const content = contents.get(path);
|
|
6750
6936
|
const source = new MagicString4(content);
|
|
6751
6937
|
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6752
|
-
|
|
6938
|
+
for (const { form, candidate } of upgrade) {
|
|
6939
|
+
const method = attrOf(candidate.el, "method");
|
|
6940
|
+
source.overwrite(method.range.start, method.range.end, `method={${methodCall(form)}}`);
|
|
6941
|
+
}
|
|
6942
|
+
const first = (list[0] ?? upgrade[0]).candidate;
|
|
6943
|
+
if (BY_NAME.includes(first.dialect) && first.importAt) {
|
|
6944
|
+
const at = first.importAt;
|
|
6945
|
+
const helper = relativeImport(path, FORMS_HELPER_PATH.replace(/\.ts$/, ""));
|
|
6946
|
+
const names = !/\bbcmsFormId\b/.test(content) ? "bcmsFormAction, bcmsFormId, bcmsFormMethod" : !/\bbcmsFormMethod\b/.test(content) ? "bcmsFormMethod" : null;
|
|
6947
|
+
if (names) {
|
|
6948
|
+
const line = `import { ${names} } from "${helper}";`;
|
|
6949
|
+
if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${line}${at.wrap.after}`);
|
|
6950
|
+
else if (at.at === 0) source.appendLeft(0, `${line}
|
|
6951
|
+
`);
|
|
6952
|
+
else source.appendLeft(at.at, `
|
|
6953
|
+
${line}`);
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
if (list.length > 0 && !content.includes("form[data-bcms-form]")) {
|
|
6753
6957
|
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6754
6958
|
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6755
6959
|
source.appendRight(last.candidate.el.end, `
|
|
@@ -6757,6 +6961,10 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6757
6961
|
}
|
|
6758
6962
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6759
6963
|
}
|
|
6964
|
+
const helperAdded = existingHelper === void 0 && (matched.some((m) => BY_NAME.includes(m.candidate.dialect)) || methodUpgrades.length > 0);
|
|
6965
|
+
if (helperAdded) files.push({ path: FORMS_HELPER_PATH, operation: "add", content: FORMS_HELPER_SOURCE });
|
|
6966
|
+
const helperUpgraded = helperStale && byNameOnSite;
|
|
6967
|
+
if (helperUpgraded) files.push({ path: FORMS_HELPER_PATH, operation: "modify", content: FORMS_HELPER_SOURCE });
|
|
6760
6968
|
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.`);
|
|
6761
6969
|
for (const { form, candidate } of matched) {
|
|
6762
6970
|
const tie = beat.get(candidate);
|
|
@@ -6772,6 +6980,13 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6772
6980
|
);
|
|
6773
6981
|
}
|
|
6774
6982
|
}
|
|
6983
|
+
for (const c of candidates) {
|
|
6984
|
+
const name = wiredName(c);
|
|
6985
|
+
if (name === null || pendedByName.has(c) || forms.some((f) => f.name === name)) continue;
|
|
6986
|
+
notes.push(
|
|
6987
|
+
`${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.`
|
|
6988
|
+
);
|
|
6989
|
+
}
|
|
6775
6990
|
for (const { form, candidate } of matched) {
|
|
6776
6991
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6777
6992
|
if (keys.length > 0) {
|
|
@@ -6789,8 +7004,8 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6789
7004
|
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6790
7005
|
occurrences: { located: 0, rewritten: 0 },
|
|
6791
7006
|
files: [],
|
|
6792
|
-
helpers: [],
|
|
6793
|
-
helperUpgraded
|
|
7007
|
+
helpers: helperAdded || helperUpgraded ? [FORMS_HELPER_PATH] : [],
|
|
7008
|
+
helperUpgraded,
|
|
6794
7009
|
dynamicBindings: [],
|
|
6795
7010
|
forms: assertForms(forms.length, {
|
|
6796
7011
|
wired: matched.length,
|
|
@@ -8131,6 +8346,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
8131
8346
|
provided: [
|
|
8132
8347
|
"blockId",
|
|
8133
8348
|
"bind",
|
|
8349
|
+
"bindPaths",
|
|
8134
8350
|
"overrides",
|
|
8135
8351
|
"page",
|
|
8136
8352
|
fieldName,
|
|
@@ -8145,8 +8361,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
8145
8361
|
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
8146
8362
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
8147
8363
|
` : "";
|
|
8148
|
-
const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
|
|
8149
|
-
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
|
|
8364
|
+
const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)}, bindPaths);` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides, bindPaths) };`;
|
|
8365
|
+
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
|
|
8150
8366
|
const declared = [copy, field].filter(Boolean).join("\n");
|
|
8151
8367
|
const styles = dialect === "astro" ? carriedStyles(content) : "";
|
|
8152
8368
|
if (dialect === "astro") {
|
|
@@ -8159,12 +8375,14 @@ interface Props {
|
|
|
8159
8375
|
blockId?: string;
|
|
8160
8376
|
/** The page field group this placement renders \u2014 Phase A keeps the copy where P2 put it. */
|
|
8161
8377
|
bind?: string;
|
|
8378
|
+
/** A locale copy's own field path for each prop, when its keys differ from this component's. */
|
|
8379
|
+
bindPaths?: Record<string, string>;
|
|
8162
8380
|
/** The instance's own copy, when an editor gave it any. Wins over the bound group. */
|
|
8163
8381
|
overrides?: Record<string, unknown>;
|
|
8164
8382
|
/** The page snapshot, so one component can serve every page whose group has this shape. */
|
|
8165
8383
|
page?: unknown;
|
|
8166
8384
|
}
|
|
8167
|
-
const { blockId, bind, overrides, page } = Astro.props;
|
|
8385
|
+
const { blockId, bind, bindPaths, overrides, page } = Astro.props;
|
|
8168
8386
|
${carriedDeclarations}${declared}
|
|
8169
8387
|
---
|
|
8170
8388
|
${markup}
|
|
@@ -8176,7 +8394,7 @@ ${styles}
|
|
|
8176
8394
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
8177
8395
|
${carriedDeclarations === "" ? "" : `
|
|
8178
8396
|
${carriedDeclarations}`}
|
|
8179
|
-
export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
|
|
8397
|
+
export default function ${resolved.name}({ blockId, bind, bindPaths, overrides, page }: BcmsSectionProps) {
|
|
8180
8398
|
${declared.split("\n").join("\n ")}
|
|
8181
8399
|
return (
|
|
8182
8400
|
${markup}
|
|
@@ -8538,6 +8756,7 @@ const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
|
8538
8756
|
export interface BcmsSectionProps {
|
|
8539
8757
|
blockId?: string;
|
|
8540
8758
|
bind?: string;
|
|
8759
|
+
bindPaths?: Record<string, string>;
|
|
8541
8760
|
overrides?: Record<string, unknown>;
|
|
8542
8761
|
page?: unknown;
|
|
8543
8762
|
}
|
|
@@ -8547,6 +8766,8 @@ export interface BcmsPlacement {
|
|
|
8547
8766
|
blockId?: string;
|
|
8548
8767
|
slug: string;
|
|
8549
8768
|
bind?: string;
|
|
8769
|
+
/** A locale copy's own field path for each prop, when its keys differ from the component's. */
|
|
8770
|
+
bindPaths?: Record<string, string>;
|
|
8550
8771
|
overrides?: Record<string, unknown>;
|
|
8551
8772
|
}
|
|
8552
8773
|
|
|
@@ -8607,6 +8828,7 @@ export function pageSections(pageSlug: string): BcmsPlacement[] {
|
|
|
8607
8828
|
blockId: typeof block.id === "string" ? block.id : undefined,
|
|
8608
8829
|
slug,
|
|
8609
8830
|
bind: typeof props.bind === "string" ? props.bind : undefined,
|
|
8831
|
+
bindPaths: isRecord(props.bindPaths) ? (props.bindPaths as Record<string, string>) : undefined,
|
|
8610
8832
|
overrides: isRecord(props.overrides) ? props.overrides : undefined,
|
|
8611
8833
|
});
|
|
8612
8834
|
}
|
|
@@ -8672,6 +8894,7 @@ const snapshot = pageSnapshot(page);
|
|
|
8672
8894
|
<Component
|
|
8673
8895
|
blockId={placement.blockId}
|
|
8674
8896
|
bind={placement.bind}
|
|
8897
|
+
bindPaths={placement.bindPaths}
|
|
8675
8898
|
overrides={placement.overrides}
|
|
8676
8899
|
page={snapshot}
|
|
8677
8900
|
/>
|
|
@@ -8694,6 +8917,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
8694
8917
|
key={placement.blockId ?? \`\${placement.slug}-\${i}\`}
|
|
8695
8918
|
blockId={placement.blockId}
|
|
8696
8919
|
bind={placement.bind}
|
|
8920
|
+
bindPaths={placement.bindPaths}
|
|
8697
8921
|
overrides={placement.overrides}
|
|
8698
8922
|
page={snapshot}
|
|
8699
8923
|
/>
|