@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 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) => kind === "richtext" && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null));
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
- function candidateOf(file, dialect, form) {
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.13.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: {