@bettercms-ai/convert 0.14.0 → 0.16.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 +12 -0
- package/dist/cli.js +108 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +94 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -990,7 +990,11 @@ declare const FORM_SCRIPT = "<script>\nfunction bcmsPhone(cc,raw,trunk){var d=St
|
|
|
990
990
|
* `ConversionReceipt` with an empty path arithmetic and the lane's own account under `forms`, so
|
|
991
991
|
* `submit_conversion_receipt` takes it unchanged — the server's invariant reads `paths` only.
|
|
992
992
|
*/
|
|
993
|
-
|
|
993
|
+
interface WireFormsOptions {
|
|
994
|
+
/** Form id or name → the file its `<form>` is in. The operator's answer to FORM_AMBIGUOUS. */
|
|
995
|
+
pins?: Record<string, string>;
|
|
996
|
+
}
|
|
997
|
+
declare function wireForms(briefIn: Brief, sources: SourceFile[], options_?: WireFormsOptions): Promise<{
|
|
994
998
|
files: PlanFile[];
|
|
995
999
|
receipt: ConversionReceipt;
|
|
996
1000
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -6401,9 +6401,11 @@ function* elementsIn(nodes) {
|
|
|
6401
6401
|
var childElements = (node) => node.children.flatMap(
|
|
6402
6402
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6403
6403
|
);
|
|
6404
|
-
|
|
6404
|
+
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6405
|
+
function candidateOf(file, dialect, form, imported) {
|
|
6405
6406
|
const controls = /* @__PURE__ */ new Map();
|
|
6406
6407
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6408
|
+
const foreign = /* @__PURE__ */ new Set();
|
|
6407
6409
|
const all = [];
|
|
6408
6410
|
const walk = (parent) => {
|
|
6409
6411
|
const own = [];
|
|
@@ -6411,6 +6413,17 @@ function candidateOf(file, dialect, form) {
|
|
|
6411
6413
|
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6412
6414
|
own.push(child);
|
|
6413
6415
|
all.push(child);
|
|
6416
|
+
} else if (isComponentTag(dialect, child.tag)) {
|
|
6417
|
+
const inFile = attrOf(child, "name") ? [] : imported.get(child.tag) ?? [];
|
|
6418
|
+
if (inFile.length) {
|
|
6419
|
+
for (const control of inFile) {
|
|
6420
|
+
all.push(control);
|
|
6421
|
+
foreign.add(control);
|
|
6422
|
+
}
|
|
6423
|
+
} else if (attrOf(child, "name") ?? attrOf(child, "id")) {
|
|
6424
|
+
own.push(child);
|
|
6425
|
+
all.push(child);
|
|
6426
|
+
}
|
|
6414
6427
|
}
|
|
6415
6428
|
walk(child);
|
|
6416
6429
|
}
|
|
@@ -6422,10 +6435,32 @@ function candidateOf(file, dialect, form) {
|
|
|
6422
6435
|
if (name && !controls.has(name)) controls.set(name, control);
|
|
6423
6436
|
}
|
|
6424
6437
|
for (const control of all) {
|
|
6438
|
+
if (foreign.has(control)) continue;
|
|
6425
6439
|
const id = attrOf(control, "id")?.value;
|
|
6426
6440
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6427
6441
|
}
|
|
6428
|
-
return { file, dialect, el: form, controls, wrapper };
|
|
6442
|
+
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6443
|
+
}
|
|
6444
|
+
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6445
|
+
const out = /* @__PURE__ */ new Map();
|
|
6446
|
+
for (const [local, { specifier }] of importsIn(parsed, dialect)) {
|
|
6447
|
+
const path = resolveSpecifier(source.path, specifier, project.files, project.aliases);
|
|
6448
|
+
const child = path === null || path === source.path ? void 0 : project.byPath.get(path);
|
|
6449
|
+
if (!child) continue;
|
|
6450
|
+
let controls = cache.get(child.path);
|
|
6451
|
+
if (!controls) {
|
|
6452
|
+
controls = (async () => {
|
|
6453
|
+
const own = dialectOf(child.path, child.content);
|
|
6454
|
+
if (!own || !WIRABLE.includes(own)) return [];
|
|
6455
|
+
const file = await parseFile4(own, child.content);
|
|
6456
|
+
return file.error ? [] : [...elementsIn(file.roots)].filter((el) => CONTROLS.has(el.tag.toLowerCase()));
|
|
6457
|
+
})();
|
|
6458
|
+
cache.set(child.path, controls);
|
|
6459
|
+
}
|
|
6460
|
+
const found = await controls;
|
|
6461
|
+
if (found.length) out.set(local, found);
|
|
6462
|
+
}
|
|
6463
|
+
return out;
|
|
6429
6464
|
}
|
|
6430
6465
|
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6431
6466
|
function wire(source, candidate, form) {
|
|
@@ -6444,6 +6479,7 @@ function wire(source, candidate, form) {
|
|
|
6444
6479
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6445
6480
|
for (const field of form.fields) {
|
|
6446
6481
|
const control = candidate.controls.get(field.key);
|
|
6482
|
+
if (candidate.foreign.has(control)) continue;
|
|
6447
6483
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6448
6484
|
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6449
6485
|
const name = attrOf(control, "name");
|
|
@@ -6457,11 +6493,17 @@ function wire(source, candidate, form) {
|
|
|
6457
6493
|
source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
|
|
6458
6494
|
}
|
|
6459
6495
|
}
|
|
6460
|
-
async function wireForms(briefIn, sources) {
|
|
6496
|
+
async function wireForms(briefIn, sources, options_ = {}) {
|
|
6461
6497
|
const brief = readBrief(briefIn);
|
|
6462
6498
|
const forms = brief.forms ?? [];
|
|
6463
6499
|
const candidates = [];
|
|
6464
6500
|
const unreadable = [];
|
|
6501
|
+
const componentFiles = /* @__PURE__ */ new Map();
|
|
6502
|
+
const project = {
|
|
6503
|
+
byPath: new Map(sources.map((s) => [s.path, s])),
|
|
6504
|
+
files: new Set(sources.map((s) => s.path)),
|
|
6505
|
+
aliases: aliasesFrom(sources)
|
|
6506
|
+
};
|
|
6465
6507
|
for (const source of sources) {
|
|
6466
6508
|
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6467
6509
|
const dialect = dialectOf(source.path, source.content);
|
|
@@ -6482,13 +6524,40 @@ async function wireForms(briefIn, sources) {
|
|
|
6482
6524
|
});
|
|
6483
6525
|
continue;
|
|
6484
6526
|
}
|
|
6527
|
+
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6485
6528
|
for (const el of elementsIn(parsed.roots)) {
|
|
6486
|
-
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6529
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
|
|
6487
6530
|
}
|
|
6488
6531
|
}
|
|
6489
6532
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6490
6533
|
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6534
|
+
const posts = (c) => new Set(c.controls.values()).size;
|
|
6535
|
+
const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6491
6536
|
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6537
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
6538
|
+
for (const [ref, file] of Object.entries(options_.pins ?? {})) {
|
|
6539
|
+
const form = forms.find((f) => f.id === ref || f.name === ref);
|
|
6540
|
+
if (!form) throw new Error(`--pin ${ref}: the brief lists no form with that id or name.`);
|
|
6541
|
+
pinned.set(form.id, file);
|
|
6542
|
+
options.set(form.id, options.get(form.id).filter((c) => samePath(c.file, file)));
|
|
6543
|
+
for (const other of forms) {
|
|
6544
|
+
if (other.id !== form.id) options.set(other.id, options.get(other.id).filter((c) => !samePath(c.file, file)));
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
for (const c of candidates) {
|
|
6548
|
+
const claimants = forms.filter((f) => options.get(f.id).includes(c));
|
|
6549
|
+
if (claimants.length < 2) continue;
|
|
6550
|
+
const top = Math.max(...claimants.map((f) => holds(c, f)));
|
|
6551
|
+
const winners = claimants.filter((f) => holds(c, f) === top);
|
|
6552
|
+
if (winners.length !== 1) continue;
|
|
6553
|
+
for (const f of claimants) if (f !== winners[0]) options.set(f.id, options.get(f.id).filter((x) => x !== c));
|
|
6554
|
+
}
|
|
6555
|
+
for (const f of forms) {
|
|
6556
|
+
const list = options.get(f.id);
|
|
6557
|
+
if (list.length < 2) continue;
|
|
6558
|
+
const exact = list.filter((c) => posts(c) === f.fields.length);
|
|
6559
|
+
if (exact.length === 1) options.set(f.id, exact);
|
|
6560
|
+
}
|
|
6492
6561
|
const wanted = /* @__PURE__ */ new Map();
|
|
6493
6562
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6494
6563
|
const matched = [];
|
|
@@ -6499,7 +6568,12 @@ async function wireForms(briefIn, sources) {
|
|
|
6499
6568
|
for (const form of forms) {
|
|
6500
6569
|
const list = options.get(form.id) ?? [];
|
|
6501
6570
|
if (list.length > 1) {
|
|
6502
|
-
pend2(
|
|
6571
|
+
pend2(
|
|
6572
|
+
form,
|
|
6573
|
+
"FORM_AMBIGUOUS",
|
|
6574
|
+
void 0,
|
|
6575
|
+
`${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}. Name the right one with --pin "${form.name}=<file>".`
|
|
6576
|
+
);
|
|
6503
6577
|
continue;
|
|
6504
6578
|
}
|
|
6505
6579
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6508,7 +6582,13 @@ async function wireForms(briefIn, sources) {
|
|
|
6508
6582
|
}
|
|
6509
6583
|
if (list.length === 0) {
|
|
6510
6584
|
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6511
|
-
|
|
6585
|
+
const pin = pinned.get(form.id);
|
|
6586
|
+
if (pin) {
|
|
6587
|
+
pend2(form, "FORM_NOT_IN_SOURCE", pin, `the pinned file has no <form> posting ${form.fields.map((f) => f.key).join(", ")}.`);
|
|
6588
|
+
} else if (best && best.n === form.fields.length) {
|
|
6589
|
+
const owner = forms.find((f) => f !== form && options.get(f.id).includes(best.c));
|
|
6590
|
+
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.`);
|
|
6591
|
+
} else if (best) {
|
|
6512
6592
|
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6513
6593
|
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6514
6594
|
} else if (unreadable[0]) {
|
|
@@ -6556,6 +6636,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6556
6636
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6557
6637
|
}
|
|
6558
6638
|
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.`);
|
|
6639
|
+
for (const { form, candidate } of matched) {
|
|
6640
|
+
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6641
|
+
if (keys.length > 0) {
|
|
6642
|
+
notes.push(
|
|
6643
|
+
`form ${form.name}: ${keys.join(", ")} matched a control inside an imported component, so no data-bcms-form-field marker was written for ${keys.length === 1 ? "it" : "them"}.`
|
|
6644
|
+
);
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6559
6647
|
return {
|
|
6560
6648
|
files,
|
|
6561
6649
|
receipt: {
|