@bettercms-ai/convert 0.13.0 → 0.15.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 +15 -0
- package/dist/cli.js +90 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +89 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0ddfbdd: `--forms` now sees a form built from components. A component tag handed a `name` or `id` (`<TextInput name="firstName" />`) is the control; a tag handed neither (`<EmailFooterInput />`) is matched by the controls in its own imported file, which are never written. Before, such a form was reported `FORM_NOT_IN_SOURCE` although its fields were all there. A component's file is found by RESOLVING its import — a relative path, or one of the project's own tsconfig `paths` aliases — so a bare package specifier reads no local file that merely shares its last segment, and the receipt notes any field whose marker went unwritten because its control lives in an imported component.
|
|
8
|
+
|
|
9
|
+
## 0.14.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- df570ef: Receipt and brief shapes a consumer reads:
|
|
14
|
+
- `BriefPath.kind` is typed `BindingKind` rather than `BindingKind | ""` — the brief always names a kind, and the empty case only forced callers to handle a value it never sends.
|
|
15
|
+
- New `PendingReason` `"DOCUMENT_DECLARE_ONLY"`: a `document` path is never located and never rewritten, because its `original` is only the body's FIRST block and declaring the paragraph that renders it is what publish refuses. The row's fix names the wrapper to declare instead.
|
|
16
|
+
- `PendingFix.kind` (`"document" | "richtext"`, optional) accompanies a `declare-richtext` action to say WHICH container is meant.
|
|
17
|
+
|
|
3
18
|
## 0.13.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/cli.js
CHANGED
|
@@ -2562,7 +2562,11 @@ var IMAGE_SOURCES = {
|
|
|
2562
2562
|
video: ["poster"]
|
|
2563
2563
|
};
|
|
2564
2564
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2565
|
-
var ownsSubtree = (kind, site, inline) =>
|
|
2565
|
+
var ownsSubtree = (kind, site, inline) => (
|
|
2566
|
+
// A `document` is the same claim at article scale: the element renders EVERY block of the Body,
|
|
2567
|
+
// so its children come from the stored value and nothing else may be bound inside it.
|
|
2568
|
+
(kind === "richtext" || kind === "document") && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null))
|
|
2569
|
+
);
|
|
2566
2570
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2567
2571
|
var BCMS_ATTRS = [
|
|
2568
2572
|
"data-bcms-field",
|
|
@@ -5363,7 +5367,7 @@ function actionFor(reason, input, file, inData) {
|
|
|
5363
5367
|
return chosen;
|
|
5364
5368
|
}
|
|
5365
5369
|
function reasonAction(reason, input, file, inData) {
|
|
5366
|
-
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5370
|
+
const richtext = input.kind === "richtext" || input.kind === "document" || input.inline === true;
|
|
5367
5371
|
switch (reason) {
|
|
5368
5372
|
case "AMBIGUOUS_LITERAL":
|
|
5369
5373
|
if (input.routes > 1) {
|
|
@@ -5414,6 +5418,11 @@ function reasonAction(reason, input, file, inData) {
|
|
|
5414
5418
|
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5415
5419
|
case "SUBTREE_NOT_HTML":
|
|
5416
5420
|
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5421
|
+
case "DOCUMENT_DECLARE_ONLY":
|
|
5422
|
+
return {
|
|
5423
|
+
action: "declare-richtext",
|
|
5424
|
+
why: 'This is a Body: bind the ONE element that renders every block of it \u2014 an article, a main or a div \u2014 with the richtext helper and data-bcms-kind="document". Never the paragraph holding its first block: that is where the whole article would be published.'
|
|
5425
|
+
};
|
|
5417
5426
|
default:
|
|
5418
5427
|
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5419
5428
|
}
|
|
@@ -5430,7 +5439,10 @@ function fixFor(row, input) {
|
|
|
5430
5439
|
...place.col !== void 0 ? { col: place.col } : {},
|
|
5431
5440
|
snippet: place.snippet,
|
|
5432
5441
|
action,
|
|
5433
|
-
...why ? { why } : {}
|
|
5442
|
+
...why ? { why } : {},
|
|
5443
|
+
// Only `declare-richtext` carries it, and only to say WHICH container is meant: one element
|
|
5444
|
+
// for a rich-text value, the whole Body's wrapper for a document.
|
|
5445
|
+
...action === "declare-richtext" && (input.kind === "document" || input.kind === "richtext") ? { kind: input.kind } : {}
|
|
5434
5446
|
};
|
|
5435
5447
|
}
|
|
5436
5448
|
function routeCounts(pending) {
|
|
@@ -6379,9 +6391,11 @@ function* elementsIn(nodes) {
|
|
|
6379
6391
|
var childElements = (node) => node.children.flatMap(
|
|
6380
6392
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6381
6393
|
);
|
|
6382
|
-
|
|
6394
|
+
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6395
|
+
function candidateOf(file, dialect, form, imported) {
|
|
6383
6396
|
const controls = /* @__PURE__ */ new Map();
|
|
6384
6397
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6398
|
+
const foreign = /* @__PURE__ */ new Set();
|
|
6385
6399
|
const all = [];
|
|
6386
6400
|
const walk = (parent) => {
|
|
6387
6401
|
const own = [];
|
|
@@ -6389,6 +6403,17 @@ function candidateOf(file, dialect, form) {
|
|
|
6389
6403
|
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6390
6404
|
own.push(child);
|
|
6391
6405
|
all.push(child);
|
|
6406
|
+
} else if (isComponentTag(dialect, child.tag)) {
|
|
6407
|
+
const inFile = attrOf(child, "name") ? [] : imported.get(child.tag) ?? [];
|
|
6408
|
+
if (inFile.length) {
|
|
6409
|
+
for (const control of inFile) {
|
|
6410
|
+
all.push(control);
|
|
6411
|
+
foreign.add(control);
|
|
6412
|
+
}
|
|
6413
|
+
} else if (attrOf(child, "name") ?? attrOf(child, "id")) {
|
|
6414
|
+
own.push(child);
|
|
6415
|
+
all.push(child);
|
|
6416
|
+
}
|
|
6392
6417
|
}
|
|
6393
6418
|
walk(child);
|
|
6394
6419
|
}
|
|
@@ -6400,10 +6425,32 @@ function candidateOf(file, dialect, form) {
|
|
|
6400
6425
|
if (name && !controls.has(name)) controls.set(name, control);
|
|
6401
6426
|
}
|
|
6402
6427
|
for (const control of all) {
|
|
6428
|
+
if (foreign.has(control)) continue;
|
|
6403
6429
|
const id = attrOf(control, "id")?.value;
|
|
6404
6430
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6405
6431
|
}
|
|
6406
|
-
return { file, dialect, el: form, controls, wrapper };
|
|
6432
|
+
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6433
|
+
}
|
|
6434
|
+
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6435
|
+
const out = /* @__PURE__ */ new Map();
|
|
6436
|
+
for (const [local, { specifier }] of importsIn(parsed, dialect)) {
|
|
6437
|
+
const path = resolveSpecifier(source.path, specifier, project.files, project.aliases);
|
|
6438
|
+
const child = path === null || path === source.path ? void 0 : project.byPath.get(path);
|
|
6439
|
+
if (!child) continue;
|
|
6440
|
+
let controls = cache.get(child.path);
|
|
6441
|
+
if (!controls) {
|
|
6442
|
+
controls = (async () => {
|
|
6443
|
+
const own = dialectOf(child.path, child.content);
|
|
6444
|
+
if (!own || !WIRABLE.includes(own)) return [];
|
|
6445
|
+
const file = await parseFile4(own, child.content);
|
|
6446
|
+
return file.error ? [] : [...elementsIn(file.roots)].filter((el) => CONTROLS.has(el.tag.toLowerCase()));
|
|
6447
|
+
})();
|
|
6448
|
+
cache.set(child.path, controls);
|
|
6449
|
+
}
|
|
6450
|
+
const found = await controls;
|
|
6451
|
+
if (found.length) out.set(local, found);
|
|
6452
|
+
}
|
|
6453
|
+
return out;
|
|
6407
6454
|
}
|
|
6408
6455
|
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6409
6456
|
function wire(source, candidate, form) {
|
|
@@ -6422,6 +6469,7 @@ function wire(source, candidate, form) {
|
|
|
6422
6469
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6423
6470
|
for (const field of form.fields) {
|
|
6424
6471
|
const control = candidate.controls.get(field.key);
|
|
6472
|
+
if (candidate.foreign.has(control)) continue;
|
|
6425
6473
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6426
6474
|
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6427
6475
|
const name = attrOf(control, "name");
|
|
@@ -6440,6 +6488,12 @@ async function wireForms(briefIn, sources) {
|
|
|
6440
6488
|
const forms = brief.forms ?? [];
|
|
6441
6489
|
const candidates = [];
|
|
6442
6490
|
const unreadable = [];
|
|
6491
|
+
const componentFiles = /* @__PURE__ */ new Map();
|
|
6492
|
+
const project = {
|
|
6493
|
+
byPath: new Map(sources.map((s) => [s.path, s])),
|
|
6494
|
+
files: new Set(sources.map((s) => s.path)),
|
|
6495
|
+
aliases: aliasesFrom(sources)
|
|
6496
|
+
};
|
|
6443
6497
|
for (const source of sources) {
|
|
6444
6498
|
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6445
6499
|
const dialect = dialectOf(source.path, source.content);
|
|
@@ -6460,8 +6514,9 @@ async function wireForms(briefIn, sources) {
|
|
|
6460
6514
|
});
|
|
6461
6515
|
continue;
|
|
6462
6516
|
}
|
|
6517
|
+
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6463
6518
|
for (const el of elementsIn(parsed.roots)) {
|
|
6464
|
-
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6519
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
|
|
6465
6520
|
}
|
|
6466
6521
|
}
|
|
6467
6522
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
@@ -6534,6 +6589,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6534
6589
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6535
6590
|
}
|
|
6536
6591
|
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.`);
|
|
6592
|
+
for (const { form, candidate } of matched) {
|
|
6593
|
+
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6594
|
+
if (keys.length > 0) {
|
|
6595
|
+
notes.push(
|
|
6596
|
+
`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"}.`
|
|
6597
|
+
);
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6537
6600
|
return {
|
|
6538
6601
|
files,
|
|
6539
6602
|
receipt: {
|
|
@@ -8771,10 +8834,28 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8771
8834
|
}
|
|
8772
8835
|
pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
|
|
8773
8836
|
}
|
|
8837
|
+
const documents = /* @__PURE__ */ new Set();
|
|
8838
|
+
for (const [key, hit] of locatedByKey) {
|
|
8839
|
+
const target = targets.get(key);
|
|
8840
|
+
if (!target || target.kind !== "document") continue;
|
|
8841
|
+
documents.add(key);
|
|
8842
|
+
const byHand = declaredByHand(target, hit.files);
|
|
8843
|
+
if (byHand !== null) {
|
|
8844
|
+
declaredOnly++;
|
|
8845
|
+
declaredRow(target, false, byHand);
|
|
8846
|
+
continue;
|
|
8847
|
+
}
|
|
8848
|
+
pend2(
|
|
8849
|
+
target,
|
|
8850
|
+
"DOCUMENT_DECLARE_ONLY",
|
|
8851
|
+
hit.files[0],
|
|
8852
|
+
"A Body is bound by declaring the one element that renders every block of it, never the paragraph that happens to hold its first block."
|
|
8853
|
+
);
|
|
8854
|
+
}
|
|
8774
8855
|
const byFile = /* @__PURE__ */ new Map();
|
|
8775
8856
|
for (const [key, hit] of locatedByKey) {
|
|
8776
8857
|
const target = targets.get(key);
|
|
8777
|
-
if (!target) continue;
|
|
8858
|
+
if (!target || documents.has(key)) continue;
|
|
8778
8859
|
for (const file of hit.files) byFile.set(file, [...byFile.get(file) ?? [], target]);
|
|
8779
8860
|
}
|
|
8780
8861
|
const drill = {
|
|
@@ -8823,6 +8904,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8823
8904
|
let rewritten = 0;
|
|
8824
8905
|
let alreadyDeclared = declaredOnly;
|
|
8825
8906
|
for (const [key, hit] of locatedByKey) {
|
|
8907
|
+
if (documents.has(key)) continue;
|
|
8826
8908
|
const target = targets.get(key);
|
|
8827
8909
|
const outcomes = hit.files.map((file) => results.get(file)?.get(target.key) ?? "NOT_IN_SOURCE");
|
|
8828
8910
|
const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
|
|
@@ -9607,7 +9689,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9607
9689
|
// package.json
|
|
9608
9690
|
var package_default = {
|
|
9609
9691
|
name: "@bettercms-ai/convert",
|
|
9610
|
-
version: "0.
|
|
9692
|
+
version: "0.15.0",
|
|
9611
9693
|
type: "module",
|
|
9612
9694
|
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.",
|
|
9613
9695
|
bin: {
|