@bettercms-ai/convert 0.24.1 → 0.25.1

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,23 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.25.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b1e5eb0: Prices and other numbers are now editable: a value written as `49` or `price={49}` in a data object, a `const`, or a component prop is bound like any other copy, and still renders the same text. `--brief` no longer writes a second `data-bcms-field` on an element, and two paths that want the same element, or a rewrite inside text another rewrite replaces, are reported as ambiguous instead of corrupting the file; a second run over converted source changes nothing.
8
+ - Updated dependencies [56335ff]
9
+ - @bettercms-ai/types@1.32.0
10
+
11
+ ## 0.25.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 832e08a: A converted repository whose `build` runs checks before the build (`astro check && astro build`, `tsc --noEmit && vite build`, `npm run lint && next build`) gets a `build:bcms` script beside it with the same build minus those checks, `prebuild`/`postbuild` folded in. BetterCMS builds with `build:bcms` when a project declares one, so a deploy no longer pays for the project's CI checks; `build` is left unchanged. Every writing run adds it once (a repository that already has one is never touched), and only for a plain `&&` chain. Exported as `bcmsBuildScript`.
16
+
17
+ ### Patch Changes
18
+
19
+ - c6fd790: The codemod never walks into a directory holding its own `.git` (a git worktree, submodule or vendored clone), so a tool's worktree copy of the repo is no longer converted alongside the real source.
20
+
3
21
  ## 0.24.1
4
22
 
5
23
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execFileSync } from "child_process";
5
+ import { existsSync } from "fs";
5
6
  import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
6
7
  import { dirname, join as join2, relative, resolve, sep } from "path";
7
8
 
@@ -2714,8 +2715,9 @@ function collectSites(roots, literals) {
2714
2715
  }
2715
2716
  const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
2716
2717
  for (const attr of node.attrs) {
2717
- if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2718
- const value = norm(attr.value);
2718
+ const digits = attr.expression && attr.expressionRange ? attr.number : void 0;
2719
+ if (attr.expression && digits === void 0 || !digits && !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2720
+ const value = norm(digits ?? attr.value);
2719
2721
  for (const literal of wanted) {
2720
2722
  if (value !== literal) continue;
2721
2723
  matched.add(literal);
@@ -2723,7 +2725,7 @@ function collectSites(roots, literals) {
2723
2725
  file: "",
2724
2726
  tag: node.tag,
2725
2727
  attrInsertAt: node.attrInsertAt,
2726
- range: attr.valueRange,
2728
+ range: digits === void 0 ? attr.valueRange : attr.expressionRange,
2727
2729
  inner: node.inner,
2728
2730
  // 🔴 A COMPONENT'S ATTRIBUTE IS A PROP, and a prop is a different conversion: the value
2729
2731
  // is rendered in another file, by an element only that file knows. @see props.ts
@@ -2831,6 +2833,8 @@ function attrsOf2(open, content) {
2831
2833
  const value = attr.value;
2832
2834
  const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
2833
2835
  const literal = value?.type === "StringLiteral";
2836
+ const inside = value?.type === "JSXExpressionContainer" ? value.expression : null;
2837
+ const numeric = inside?.type === "NumericLiteral" ? String(inside.extra.raw) : "";
2834
2838
  return [
2835
2839
  {
2836
2840
  name: nameOf(attr.name),
@@ -2839,7 +2843,8 @@ function attrsOf2(open, content) {
2839
2843
  valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
2840
2844
  expression: !literal,
2841
2845
  range,
2842
- expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
2846
+ expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null,
2847
+ ...numeric ? { number: numeric } : {}
2843
2848
  }
2844
2849
  ];
2845
2850
  });
@@ -3315,14 +3320,21 @@ function exportedNames(ast) {
3315
3320
  });
3316
3321
  return out;
3317
3322
  }
3323
+ function printed(node) {
3324
+ if (node?.type === "StringLiteral") return [String(node.value)];
3325
+ if (node?.type === "NumericLiteral") return [String(node.value), String(node.extra?.raw ?? "")];
3326
+ const quasis = node?.type === "TemplateLiteral" ? node.quasis ?? [] : [];
3327
+ const expressions = node?.expressions ?? [];
3328
+ return quasis.length === 1 && expressions.length === 0 ? [String(quasis[0].value.cooked ?? "")] : [];
3329
+ }
3318
3330
  function propertyFor(row, literal) {
3319
3331
  if (row?.type !== "ObjectExpression") return null;
3320
3332
  for (const property of row.properties ?? []) {
3321
3333
  if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
3322
3334
  const key = property.key;
3323
3335
  const value = property.value;
3324
- if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
3325
- if (norm(String(value.value)) === literal) return String(key.name);
3336
+ if (key?.type !== "Identifier") continue;
3337
+ if (printed(value).some((text) => norm(text) === literal)) return String(key.name);
3326
3338
  }
3327
3339
  return null;
3328
3340
  }
@@ -3396,7 +3408,7 @@ function stringLiterals(ast) {
3396
3408
  if (node.type !== "VariableDeclarator") return;
3397
3409
  const id = node.id;
3398
3410
  const init = node.init;
3399
- if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
3411
+ if (id?.type !== "Identifier" || !init || printed(init).length === 0) return;
3400
3412
  out.set(String(id.name), init);
3401
3413
  });
3402
3414
  return out;
@@ -3427,7 +3439,7 @@ function scalarUses(content, roots, id) {
3427
3439
  }
3428
3440
  function bindScalar(ctx, member, literals, offset) {
3429
3441
  const rule = DIALECT_RULES[ctx.dialect];
3430
- const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
3442
+ const named = [...literals].filter(([, init2]) => printed(init2).some((text) => norm(text) === member.literal));
3431
3443
  if (named.length !== 1) return { splices: [], refusal: null };
3432
3444
  const [id, init] = named[0];
3433
3445
  const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
@@ -3642,16 +3654,17 @@ function literalTable(init) {
3642
3654
  const first = elements[0];
3643
3655
  if (!first || elements.length !== (init.elements ?? []).length) return null;
3644
3656
  if (elements.some((element) => element.type !== first.type)) return null;
3645
- if (first.type === "StringLiteral") {
3646
- return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
3657
+ if (printed(first).length > 0) {
3658
+ if (elements.some((element) => printed(element).length === 0)) return null;
3659
+ return { tuple: false, rows: elements.map((element) => [norm(printed(element)[0])]) };
3647
3660
  }
3648
3661
  if (first.type !== "ArrayExpression") return null;
3649
3662
  const rows = [];
3650
3663
  for (const element of elements) {
3651
3664
  const inner = (element.elements ?? []).filter((x) => x !== null);
3652
- if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
3665
+ if (inner.length === 0 || inner.some((x) => printed(x).length === 0)) return null;
3653
3666
  if (inner.length !== (element.elements ?? []).length) return null;
3654
- rows.push(inner.map((x) => norm(String(x.value))));
3667
+ rows.push(inner.map((x) => norm(printed(x)[0])));
3655
3668
  }
3656
3669
  if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3657
3670
  return { tuple: true, rows };
@@ -5459,7 +5472,8 @@ function overlapping(rewrites) {
5459
5472
  for (const a of rewrites) {
5460
5473
  for (const b of rewrites) {
5461
5474
  if (a.raw !== void 0 || b.raw !== void 0) continue;
5462
- if (a !== b && a.key !== b.key && a.site.where === "text" && b.site.where === "text" && overlaps(a.site.range, b.site.range)) {
5475
+ if (a === b || a.key === b.key) continue;
5476
+ if (collide(a, b)) {
5463
5477
  out.add(a);
5464
5478
  out.add(b);
5465
5479
  }
@@ -5467,6 +5481,16 @@ function overlapping(rewrites) {
5467
5481
  }
5468
5482
  return [...out];
5469
5483
  }
5484
+ var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
5485
+ function collide(a, b) {
5486
+ const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
5487
+ const [wa, wb] = [writes(a), writes(b)];
5488
+ if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
5489
+ if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
5490
+ const [fa, fb] = [field(a), field(b)];
5491
+ return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
5492
+ }
5493
+ var field = (r) => r.site.where === "prop" || r.site.where === "text" && !ownsSubtree(r.kind, r.site, r.inline) && r.site.mixed && r.site.textRange ? void 0 : declaration(r).plain[0];
5470
5494
  function declaration(rewrite) {
5471
5495
  const { site, path, kind, scope } = rewrite;
5472
5496
  if (site.where === "attr" && kind !== "image") {
@@ -5481,8 +5505,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5481
5505
  const bindings = /* @__PURE__ */ new Map();
5482
5506
  const addDeclaration = (at, add) => {
5483
5507
  const held = attributes.get(at) ?? { plain: [], props: [] };
5484
- held.plain.push(...add.plain);
5485
- held.props.push(...add.props);
5508
+ held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
5509
+ held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
5486
5510
  attributes.set(at, held);
5487
5511
  };
5488
5512
  for (const rewrite of rewrites) {
@@ -5905,7 +5929,8 @@ function attrsOf4(node, content) {
5905
5929
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5906
5930
  expression: !quoted,
5907
5931
  range,
5908
- expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
5932
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
5933
+ ...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
5909
5934
  }
5910
5935
  ];
5911
5936
  });
@@ -6024,6 +6049,9 @@ function attrsOf5(node) {
6024
6049
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6025
6050
  }
6026
6051
  const literal = literalValue(attr);
6052
+ const tag = attr.value;
6053
+ const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
6054
+ const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
6027
6055
  return [
6028
6056
  {
6029
6057
  name,
@@ -6031,7 +6059,8 @@ function attrsOf5(node) {
6031
6059
  valueRange: literal && literal.value !== "" ? literal.range : null,
6032
6060
  expression: literal === null,
6033
6061
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6034
- expressionRange: null,
6062
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6063
+ ...digits ? { number: digits } : {},
6035
6064
  range
6036
6065
  }
6037
6066
  ];
@@ -6136,13 +6165,15 @@ var spanOf = (loc) => ({
6136
6165
  function attrsOf6(node) {
6137
6166
  return (node.props ?? []).flatMap((prop) => {
6138
6167
  if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
6168
+ const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
6139
6169
  return [
6140
6170
  {
6141
6171
  name: prop.arg.content,
6142
6172
  value: "",
6143
6173
  valueRange: null,
6144
6174
  expression: true,
6145
- expressionRange: null,
6175
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6176
+ ...digits ? { number: digits } : {},
6146
6177
  range: spanOf(prop.loc)
6147
6178
  }
6148
6179
  ];
@@ -6626,8 +6657,8 @@ function readPlan(value) {
6626
6657
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6627
6658
  check.optionalString(section.copy, `${at2}.copy`);
6628
6659
  if (section.fields !== void 0) {
6629
- for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
6630
- const shape = check.record(field, `${at2}.fields[${f}]`);
6660
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6661
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6631
6662
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6632
6663
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6633
6664
  }
@@ -6946,15 +6977,15 @@ function wire(source, candidate, form) {
6946
6977
  source.remove(start, attr.range.end);
6947
6978
  }
6948
6979
  }
6949
- for (const field of form.fields) {
6950
- const control = candidate.controls.get(field.key);
6980
+ for (const field2 of form.fields) {
6981
+ const control = candidate.controls.get(field2.key);
6951
6982
  if (candidate.foreign.has(control)) continue;
6952
6983
  const host = candidate.wrapper.get(control) ?? control;
6953
- if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
6984
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6954
6985
  const name = attrOf(control, "name");
6955
- if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
6956
- else if (!name.expression && name.value !== field.key && name.valueRange) {
6957
- source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
6986
+ if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
6987
+ else if (!name.expression && name.value !== field2.key && name.valueRange) {
6988
+ source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
6958
6989
  }
6959
6990
  }
6960
6991
  if (el.inner && !hasMessage(el)) {
@@ -6998,7 +7029,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
6998
7029
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
6999
7030
  }
7000
7031
  }
7001
- const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
7032
+ const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
7002
7033
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7003
7034
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7004
7035
  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);
@@ -7120,9 +7151,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7120
7151
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7121
7152
  continue;
7122
7153
  }
7123
- const misnamed = form.fields.find((field) => {
7124
- const attr = attrOf(candidate.controls.get(field.key), "name");
7125
- return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
7154
+ const misnamed = form.fields.find((field2) => {
7155
+ const attr = attrOf(candidate.controls.get(field2.key), "name");
7156
+ return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
7126
7157
  });
7127
7158
  if (misnamed) {
7128
7159
  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.`);
@@ -7202,7 +7233,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7202
7233
  );
7203
7234
  }
7204
7235
  for (const { form, candidate } of matched) {
7205
- const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
7236
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7206
7237
  if (keys.length > 0) {
7207
7238
  notes.push(
7208
7239
  `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"}.`
@@ -7664,6 +7695,54 @@ function moduleScope(parsed, content) {
7664
7695
  return out;
7665
7696
  }
7666
7697
 
7698
+ // src/build-script.ts
7699
+ var CHECK = /^(?:npx\s+|pnpm\s+(?:exec\s+)?|yarn\s+|bunx\s+)?(?:astro\s+check|svelte-check|vue-tsc|tsc(?:\s|$)|eslint|next\s+lint|biome\s+(?:check|lint|ci)|prettier\s+(?:--check|-c)\b|stylelint|oxlint)/;
7700
+ var RUN_CHECK = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun\s+run)\s+(?:lint|typecheck|type-check|check|check:types|types|format:check|test)(?::[\w-]+)?\s*$/;
7701
+ var UNSAFE = /;|\|\||\||(?:^|[^&])&(?:[^&]|$)|\$\(|`|\n/;
7702
+ var isCheck = (step) => CHECK.test(step) || RUN_CHECK.test(step);
7703
+ function steps(script) {
7704
+ if (UNSAFE.test(script)) return null;
7705
+ const out = script.split("&&").map((s) => s.trim());
7706
+ return out.some((s) => s === "") ? null : out;
7707
+ }
7708
+ function bcmsBuildScript(packageJson) {
7709
+ let pkg;
7710
+ try {
7711
+ pkg = JSON.parse(packageJson);
7712
+ } catch {
7713
+ return null;
7714
+ }
7715
+ const scripts = pkg?.scripts;
7716
+ if (!scripts || typeof scripts !== "object" || "build:bcms" in scripts) return null;
7717
+ const build = scripts.build;
7718
+ if (typeof build !== "string") return null;
7719
+ const parts = [];
7720
+ for (const name of ["prebuild", "build", "postbuild"]) {
7721
+ const value = scripts[name];
7722
+ if (value === void 0) continue;
7723
+ if (typeof value !== "string") return null;
7724
+ const s = steps(value);
7725
+ if (!s) return null;
7726
+ parts.push(s);
7727
+ }
7728
+ const all = parts.flat();
7729
+ const dropped = all.filter(isCheck);
7730
+ const kept = all.filter((s) => !isCheck(s));
7731
+ if (dropped.length === 0 || kept.length === 0) return null;
7732
+ const script = kept.join(" && ");
7733
+ const next = {};
7734
+ for (const [key, value] of Object.entries(scripts)) {
7735
+ next[key] = value;
7736
+ if (key === "build") next["build:bcms"] = script;
7737
+ }
7738
+ const indent = /^[ \t]+(?=")/m.exec(packageJson)?.[0] ?? " ";
7739
+ const eol = packageJson.includes("\r\n") ? "\r\n" : "\n";
7740
+ let content = JSON.stringify({ ...pkg, scripts: next }, null, indent);
7741
+ if (eol !== "\n") content = content.replace(/\n/g, eol);
7742
+ if (/\r?\n$/.test(packageJson)) content += eol;
7743
+ return { content, script, dropped };
7744
+ }
7745
+
7667
7746
  // src/componentize.ts
7668
7747
  import MagicString5 from "magic-string";
7669
7748
  var SectionInvariantError = class extends Error {
@@ -8413,7 +8492,7 @@ var inSection = (section, path) => {
8413
8492
  const target = rowless(path);
8414
8493
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8415
8494
  };
8416
- var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
8495
+ var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
8417
8496
  function elementIndex(roots) {
8418
8497
  const out = /* @__PURE__ */ new Map();
8419
8498
  const visit = (node, chain) => {
@@ -8571,8 +8650,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8571
8650
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8572
8651
  ` : "";
8573
8652
  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) };`;
8574
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8575
- const declared = [copy, field].filter(Boolean).join("\n");
8653
+ const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8654
+ const declared = [copy, field2].filter(Boolean).join("\n");
8576
8655
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8577
8656
  if (dialect === "astro") {
8578
8657
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8885,8 +8964,8 @@ function propPrefix(section) {
8885
8964
  }
8886
8965
  function exactPrefix(section) {
8887
8966
  const prefixes = /* @__PURE__ */ new Set();
8888
- for (const field of section.fields ?? []) {
8889
- const { path, key } = field;
8967
+ for (const field2 of section.fields ?? []) {
8968
+ const { path, key } = field2;
8890
8969
  if (typeof path !== "string" || typeof key !== "string") continue;
8891
8970
  if (!path.endsWith(key)) return null;
8892
8971
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8898,11 +8977,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8898
8977
  function flatPrefix(section) {
8899
8978
  const prefixes = /* @__PURE__ */ new Set();
8900
8979
  let informative = false;
8901
- for (const field of section.fields ?? []) {
8902
- const { path, key } = field;
8980
+ for (const field2 of section.fields ?? []) {
8981
+ const { path, key } = field2;
8903
8982
  if (typeof path !== "string" || typeof key !== "string") continue;
8904
8983
  informative = true;
8905
- const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8984
+ const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8906
8985
  if (!prefix) return null;
8907
8986
  prefixes.add(prefix);
8908
8987
  }
@@ -8911,15 +8990,15 @@ function flatPrefix(section) {
8911
8990
  function flatPathMap(section, prefix) {
8912
8991
  const scalars = {};
8913
8992
  const tables = {};
8914
- for (const field of section.fields ?? []) {
8915
- const { path, key } = field;
8993
+ for (const field2 of section.fields ?? []) {
8994
+ const { path, key } = field2;
8916
8995
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8917
8996
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8918
8997
  if (!row) {
8919
- scalars[path] = field.overrideKey ?? key;
8998
+ scalars[path] = field2.overrideKey ?? key;
8920
8999
  continue;
8921
9000
  }
8922
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9001
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8923
9002
  if (!address) return null;
8924
9003
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8925
9004
  if (table.key !== address[1]) return null;
@@ -10784,7 +10863,7 @@ function styleEdits(before, after) {
10784
10863
  // package.json
10785
10864
  var package_default = {
10786
10865
  name: "@bettercms-ai/convert",
10787
- version: "0.24.1",
10866
+ version: "0.25.1",
10788
10867
  type: "module",
10789
10868
  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.",
10790
10869
  bin: {
@@ -10817,7 +10896,7 @@ var package_default = {
10817
10896
  dependencies: {
10818
10897
  "@astrojs/compiler": "^2.13.1",
10819
10898
  "@babel/parser": "^7.29.2",
10820
- "@bettercms-ai/types": "^1.30.0",
10899
+ "@bettercms-ai/types": "^1.32.0",
10821
10900
  "@vue/compiler-sfc": "^3.5.0",
10822
10901
  "magic-string": "^0.30.21",
10823
10902
  parse5: "^7.3.0",
@@ -10961,7 +11040,7 @@ async function readTree(root, verbose) {
10961
11040
  const full = join2(dir, entry.name);
10962
11041
  const rel = relative(root, full).split(sep).join("/");
10963
11042
  if (entry.isDirectory()) {
10964
- if (isSourceCandidate(`${rel}/x.html`)) await walk(full);
11043
+ if (isSourceCandidate(`${rel}/x.html`) && !existsSync(join2(full, ".git"))) await walk(full);
10965
11044
  continue;
10966
11045
  }
10967
11046
  if (!entry.isFile() || !isSourceCandidate(rel)) continue;
@@ -11142,6 +11221,21 @@ async function verifyStyles(root, base, verbose) {
11142
11221
  );
11143
11222
  return 1;
11144
11223
  }
11224
+ async function addBcmsBuild(root, dryRun) {
11225
+ let packageJson;
11226
+ try {
11227
+ packageJson = await readFile(join2(root, "package.json"), "utf-8");
11228
+ } catch {
11229
+ return;
11230
+ }
11231
+ const added = bcmsBuildScript(packageJson);
11232
+ if (!added) return;
11233
+ if (!dryRun) await writeFile(join2(root, "package.json"), added.content, "utf-8");
11234
+ process.stderr.write(
11235
+ `package.json: ${dryRun ? "would add" : "added"} "build:bcms": "${added.script}" \u2014 BetterCMS builds with it, without ${added.dropped.join(", ")}; "build" is unchanged.
11236
+ `
11237
+ );
11238
+ }
11145
11239
  async function main() {
11146
11240
  const args = parseArgs(process.argv.slice(2));
11147
11241
  if (args.help) {
@@ -11173,6 +11267,7 @@ async function main() {
11173
11267
  await writeFile(target, file.content, "utf-8");
11174
11268
  }
11175
11269
  }
11270
+ await addBcmsBuild(root2, args.dryRun);
11176
11271
  if (args.receipt) await writeFile(resolve(args.receipt), `${JSON.stringify(receipt2, null, 2)}
11177
11272
  `, "utf-8");
11178
11273
  reportUpgrade(files2, receipt2, args.dryRun);
@@ -11200,12 +11295,14 @@ async function main() {
11200
11295
  if (args.verbose) process.stderr.write(`read ${sources.length} candidate files under ${root}
11201
11296
  `);
11202
11297
  const written = async (files2) => {
11203
- if (args.dryRun) return;
11204
- for (const file of files2) {
11205
- const target = under(root, file.path);
11206
- await mkdir(dirname(target), { recursive: true });
11207
- await writeFile(target, file.content, "utf-8");
11298
+ if (!args.dryRun) {
11299
+ for (const file of files2) {
11300
+ const target = under(root, file.path);
11301
+ await mkdir(dirname(target), { recursive: true });
11302
+ await writeFile(target, file.content, "utf-8");
11303
+ }
11208
11304
  }
11305
+ await addBcmsBuild(root, args.dryRun);
11209
11306
  };
11210
11307
  const saveReceipt = async (receipt2) => {
11211
11308
  if (!args.receipt) return;