@bettercms-ai/convert 0.14.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,11 @@
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
+
3
9
  ## 0.14.0
4
10
 
5
11
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -6391,9 +6391,11 @@ function* elementsIn(nodes) {
6391
6391
  var childElements = (node) => node.children.flatMap(
6392
6392
  (child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
6393
6393
  );
6394
- function candidateOf(file, dialect, form) {
6394
+ var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
6395
+ function candidateOf(file, dialect, form, imported) {
6395
6396
  const controls = /* @__PURE__ */ new Map();
6396
6397
  const wrapper = /* @__PURE__ */ new Map();
6398
+ const foreign = /* @__PURE__ */ new Set();
6397
6399
  const all = [];
6398
6400
  const walk = (parent) => {
6399
6401
  const own = [];
@@ -6401,6 +6403,17 @@ function candidateOf(file, dialect, form) {
6401
6403
  if (CONTROLS.has(child.tag.toLowerCase())) {
6402
6404
  own.push(child);
6403
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
+ }
6404
6417
  }
6405
6418
  walk(child);
6406
6419
  }
@@ -6412,10 +6425,32 @@ function candidateOf(file, dialect, form) {
6412
6425
  if (name && !controls.has(name)) controls.set(name, control);
6413
6426
  }
6414
6427
  for (const control of all) {
6428
+ if (foreign.has(control)) continue;
6415
6429
  const id = attrOf(control, "id")?.value;
6416
6430
  if (id && !controls.has(id)) controls.set(id, control);
6417
6431
  }
6418
- 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;
6419
6454
  }
6420
6455
  var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
6421
6456
  function wire(source, candidate, form) {
@@ -6434,6 +6469,7 @@ function wire(source, candidate, form) {
6434
6469
  source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
6435
6470
  for (const field of form.fields) {
6436
6471
  const control = candidate.controls.get(field.key);
6472
+ if (candidate.foreign.has(control)) continue;
6437
6473
  const host = candidate.wrapper.get(control) ?? control;
6438
6474
  source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
6439
6475
  const name = attrOf(control, "name");
@@ -6452,6 +6488,12 @@ async function wireForms(briefIn, sources) {
6452
6488
  const forms = brief.forms ?? [];
6453
6489
  const candidates = [];
6454
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
+ };
6455
6497
  for (const source of sources) {
6456
6498
  if (!/<form[\s/>]/i.test(source.content)) continue;
6457
6499
  const dialect = dialectOf(source.path, source.content);
@@ -6472,8 +6514,9 @@ async function wireForms(briefIn, sources) {
6472
6514
  });
6473
6515
  continue;
6474
6516
  }
6517
+ const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
6475
6518
  for (const el of elementsIn(parsed.roots)) {
6476
- 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));
6477
6520
  }
6478
6521
  }
6479
6522
  const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
@@ -6546,6 +6589,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6546
6589
  files.push({ path, operation: "modify", content: source.toString() });
6547
6590
  }
6548
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
+ }
6549
6600
  return {
6550
6601
  files,
6551
6602
  receipt: {
@@ -9638,7 +9689,7 @@ function emitStubs2(dialects, slugs, contents, out) {
9638
9689
  // package.json
9639
9690
  var package_default = {
9640
9691
  name: "@bettercms-ai/convert",
9641
- version: "0.14.0",
9692
+ version: "0.15.0",
9642
9693
  type: "module",
9643
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.",
9644
9695
  bin: {