@bettercms-ai/convert 0.25.0 → 0.26.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,19 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 69dfcfa: A server app hosted on BetterCMS shows a publish on its very next request: the converted-site helper (v14) and `readPublishedContent` ask the content file's time on every read instead of once a second. Helper v14 also renders an image an editor replaced on a page the app renders per request: `bcmsImage` returns the local copy BetterCMS puts beside the app, in the shape of an import, where it used to keep the build's picture. A site built with an older helper gets v14 on its next BetterCMS build, with no commit.
8
+
9
+ ## 0.25.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+ - Updated dependencies [56335ff]
15
+ - @bettercms-ai/types@1.32.0
16
+
3
17
  ## 0.25.0
4
18
 
5
19
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -2715,8 +2715,9 @@ function collectSites(roots, literals) {
2715
2715
  }
2716
2716
  const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
2717
2717
  for (const attr of node.attrs) {
2718
- if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2719
- 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);
2720
2721
  for (const literal of wanted) {
2721
2722
  if (value !== literal) continue;
2722
2723
  matched.add(literal);
@@ -2724,7 +2725,7 @@ function collectSites(roots, literals) {
2724
2725
  file: "",
2725
2726
  tag: node.tag,
2726
2727
  attrInsertAt: node.attrInsertAt,
2727
- range: attr.valueRange,
2728
+ range: digits === void 0 ? attr.valueRange : attr.expressionRange,
2728
2729
  inner: node.inner,
2729
2730
  // 🔴 A COMPONENT'S ATTRIBUTE IS A PROP, and a prop is a different conversion: the value
2730
2731
  // is rendered in another file, by an element only that file knows. @see props.ts
@@ -2832,6 +2833,8 @@ function attrsOf2(open, content) {
2832
2833
  const value = attr.value;
2833
2834
  const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
2834
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) : "";
2835
2838
  return [
2836
2839
  {
2837
2840
  name: nameOf(attr.name),
@@ -2840,7 +2843,8 @@ function attrsOf2(open, content) {
2840
2843
  valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
2841
2844
  expression: !literal,
2842
2845
  range,
2843
- 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 } : {}
2844
2848
  }
2845
2849
  ];
2846
2850
  });
@@ -3316,14 +3320,21 @@ function exportedNames(ast) {
3316
3320
  });
3317
3321
  return out;
3318
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
+ }
3319
3330
  function propertyFor(row, literal) {
3320
3331
  if (row?.type !== "ObjectExpression") return null;
3321
3332
  for (const property of row.properties ?? []) {
3322
3333
  if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
3323
3334
  const key = property.key;
3324
3335
  const value = property.value;
3325
- if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
3326
- 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);
3327
3338
  }
3328
3339
  return null;
3329
3340
  }
@@ -3397,7 +3408,7 @@ function stringLiterals(ast) {
3397
3408
  if (node.type !== "VariableDeclarator") return;
3398
3409
  const id = node.id;
3399
3410
  const init = node.init;
3400
- if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
3411
+ if (id?.type !== "Identifier" || !init || printed(init).length === 0) return;
3401
3412
  out.set(String(id.name), init);
3402
3413
  });
3403
3414
  return out;
@@ -3428,7 +3439,7 @@ function scalarUses(content, roots, id) {
3428
3439
  }
3429
3440
  function bindScalar(ctx, member, literals, offset) {
3430
3441
  const rule = DIALECT_RULES[ctx.dialect];
3431
- 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));
3432
3443
  if (named.length !== 1) return { splices: [], refusal: null };
3433
3444
  const [id, init] = named[0];
3434
3445
  const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
@@ -3643,16 +3654,17 @@ function literalTable(init) {
3643
3654
  const first = elements[0];
3644
3655
  if (!first || elements.length !== (init.elements ?? []).length) return null;
3645
3656
  if (elements.some((element) => element.type !== first.type)) return null;
3646
- if (first.type === "StringLiteral") {
3647
- 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])]) };
3648
3660
  }
3649
3661
  if (first.type !== "ArrayExpression") return null;
3650
3662
  const rows = [];
3651
3663
  for (const element of elements) {
3652
3664
  const inner = (element.elements ?? []).filter((x) => x !== null);
3653
- 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;
3654
3666
  if (inner.length !== (element.elements ?? []).length) return null;
3655
- rows.push(inner.map((x) => norm(String(x.value))));
3667
+ rows.push(inner.map((x) => norm(printed(x)[0])));
3656
3668
  }
3657
3669
  if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3658
3670
  return { tuple: true, rows };
@@ -4191,7 +4203,7 @@ async function bindImages(ctx, targets) {
4191
4203
 
4192
4204
  // src/helper.ts
4193
4205
  import { createHash as createHash2 } from "crypto";
4194
- var HELPER_VERSION = 13;
4206
+ var HELPER_VERSION = 14;
4195
4207
  var HEADER = "// @bettercms-ai/convert helper v";
4196
4208
  var HELPER_PATH = Object.fromEntries(
4197
4209
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4265,11 +4277,11 @@ function render(value: unknown): string | undefined {
4265
4277
  *
4266
4278
  * Nowhere else does anything change: with no variable (a build, \`astro dev\`, a browser, an edge
4267
4279
  * runtime) every snapshot is the one the bundle imported, exactly as before. A file that cannot be
4268
- * read keeps the last good copy; it is checked at most once a second.
4280
+ * read keeps the last good copy. Its modification time is asked on EVERY read (a stat, microseconds;
4281
+ * the file is parsed again only when it changed), so a publish shows on the very next request.
4269
4282
  */
4270
- type BcmsLiveContent = { stamp: number; bySlug: Map<string, unknown>; layout: unknown };
4283
+ type BcmsLiveContent = { helper: string; stamp: number; bySlug: Map<string, unknown>; entries: Record<string, unknown>; layout: unknown };
4271
4284
  let bcmsLive: BcmsLiveContent | null = null;
4272
- let bcmsLiveCheckedAt = 0;
4273
4285
 
4274
4286
  function liveContent(): BcmsLiveContent | null {
4275
4287
  const proc = (globalThis as {
@@ -4277,9 +4289,6 @@ function liveContent(): BcmsLiveContent | null {
4277
4289
  }).process;
4278
4290
  const file = proc?.env?.BCMS_CONTENT_FILE;
4279
4291
  if (!file || typeof proc?.getBuiltinModule !== "function") return null;
4280
- const now = Date.now();
4281
- if (now - bcmsLiveCheckedAt < 1000) return bcmsLive;
4282
- bcmsLiveCheckedAt = now;
4283
4292
  try {
4284
4293
  const fs = proc.getBuiltinModule("node:fs") as {
4285
4294
  statSync(path: string): { mtimeMs: number };
@@ -4293,13 +4302,30 @@ function liveContent(): BcmsLiveContent | null {
4293
4302
  for (const rows of Object.values(collections)) {
4294
4303
  if (!Array.isArray(rows)) continue;
4295
4304
  for (const row of rows) {
4296
- if (isRecord(row) && typeof row.slug === "string" && !bySlug.has(row.slug)) bySlug.set(row.slug, row);
4305
+ // A published row carries its values under \`data\` (the delivery API's shape); the snapshot a
4306
+ // template reads is the values themselves. Read flat, as before, where a row has none.
4307
+ if (isRecord(row) && typeof row.slug === "string" && !bySlug.has(row.slug)) {
4308
+ bySlug.set(row.slug, isRecord(row.data) ? { ...row.data, slug: row.slug } : row);
4309
+ }
4297
4310
  }
4298
4311
  }
4312
+ // A dynamic route reads \`page.entries[slug]\`: every published row, by its slug.
4313
+ const entries: Record<string, unknown> = Object.fromEntries(bySlug);
4314
+ for (const row of bySlug.values()) if (isRecord(row) && !("entries" in row)) row.entries = entries;
4299
4315
  const pages = isRecord(snapshot) && Array.isArray(snapshot.pages) ? snapshot.pages : [];
4300
4316
  const withLayout = pages.find((page: unknown) => isRecord(page) && isRecord(page.layout));
4301
- const layout = isRecord(withLayout) ? withLayout.layout : isRecord(snapshot) ? snapshot.layout : undefined;
4302
- bcmsLive = { stamp, bySlug, layout };
4317
+ // The site's Layout first; a page's own (its overrides resolved in) only where there is no other.
4318
+ const delivered = isRecord(snapshot) && isRecord(snapshot.layout) ? snapshot.layout : isRecord(withLayout) ? withLayout.layout : undefined;
4319
+ // A template addresses a Layout value as \`<section id>:<field id>\`; the published Layout holds
4320
+ // each Section's values under \`sections.<slug>.fields\`. Both spellings are answered.
4321
+ const layout: Record<string, unknown> | undefined = isRecord(delivered) ? { ...delivered } : undefined;
4322
+ for (const section of layout && isRecord(delivered) && isRecord(delivered.sections) ? Object.values(delivered.sections) : []) {
4323
+ if (!isRecord(section) || typeof section.id !== "string" || !isRecord(section.fields)) continue;
4324
+ for (const [field, value] of Object.entries(section.fields)) layout![section.id + ":" + field] = value;
4325
+ }
4326
+ // \`helper\` names this reader in the built bundle: the platform reads it to know the app shows a
4327
+ // publish on its next request, and an image BetterCMS copied beside it (@see bcmsImage).
4328
+ bcmsLive = { helper: "bcms-helper@${HELPER_VERSION}", stamp, bySlug, entries, layout };
4303
4329
  } catch {
4304
4330
  // Unreadable, or caught mid-write: keep serving the last good copy.
4305
4331
  }
@@ -4311,7 +4337,10 @@ function live(page: unknown): unknown {
4311
4337
  const content = liveContent();
4312
4338
  if (!content) return page;
4313
4339
  if (page === layoutSnapshot) return isRecord(content.layout) ? content.layout : page;
4314
- if (isRecord(page) && typeof page.slug === "string") return content.bySlug.get(page.slug) ?? page;
4340
+ if (isRecord(page) && typeof page.slug === "string") {
4341
+ // A collection's page has no row of its own: its entries are the published ones all the same.
4342
+ return content.bySlug.get(page.slug) ?? ("entries" in page ? page : { ...page, entries: content.entries });
4343
+ }
4315
4344
  return page;
4316
4345
  }
4317
4346
 
@@ -4325,7 +4354,10 @@ export function bcms(page: unknown, path: string, fallback: string, slug?: strin
4325
4354
 
4326
4355
  /** One field of the shared layout \u2014 the header and footer every route renders. */
4327
4356
  export function bcmsLayout(path: string, fallback: string): string {
4328
- return render(readPath(live(layoutSnapshot), path)) ?? fallback;
4357
+ const layout = live(layoutSnapshot);
4358
+ // The whole address first: a field id may carry a dot of its own (\`derived.nav-cta\`).
4359
+ const whole = isRecord(layout) ? layout[path] : undefined;
4360
+ return render(whole !== undefined ? whole : readPath(layout, path)) ?? fallback;
4329
4361
  }
4330
4362
 
4331
4363
  /**
@@ -4402,13 +4434,24 @@ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][
4402
4434
  * release write (\`writeImage\`) puts the editor's image on the \`data-bcms-field\` element, which
4403
4435
  * is where every published value lands. A plain string fallback (\`<img src="/hero.png">\`) has
4404
4436
  * no such rule and takes the CMS url directly.
4437
+ *
4438
+ * \u{1F534} ON A HOSTED SERVER THE EDITOR'S IMAGE IS A LOCAL FILE. A page the app renders per request has no
4439
+ * release write: what this returns is the page. BetterCMS copies a replaced image beside the app and
4440
+ * names the copy on the published value (\`local\`, with its size), and it is returned in the shape
4441
+ * of an import (\`{ src, width, height, format }\`), so \`<Image>\` optimises it like the site's own.
4442
+ * Absent (a build, or the site's own picture) the import stands.
4405
4443
  */
4406
4444
  export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
4407
4445
  page = live(page);
4408
- if (fallback !== null && typeof fallback === "object") return fallback;
4409
4446
  const entries = isRecord(page) ? page.entries : undefined;
4410
4447
  const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4411
- return render(readPath(root, path)) ?? fallback;
4448
+ const value = readPath(root, path);
4449
+ if (fallback !== null && typeof fallback === "object") {
4450
+ if (!isRecord(value) || typeof value.local !== "string" || typeof value.width !== "number" || typeof value.height !== "number") return fallback;
4451
+ const format = /\\.([a-z0-9]+)$/i.exec(value.local)?.[1]?.toLowerCase() ?? "";
4452
+ return { src: value.local, width: value.width, height: value.height, format: format === "jpg" ? "jpeg" : format } as unknown as T;
4453
+ }
4454
+ return render(value) ?? fallback;
4412
4455
  }
4413
4456
 
4414
4457
  /**
@@ -4741,7 +4784,8 @@ var HISTORICAL = {
4741
4784
  9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4742
4785
  10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
4743
4786
  11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
4744
- 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
4787
+ 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
4788
+ 13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
4745
4789
  },
4746
4790
  "src/lib/bcms-content.ts": {
4747
4791
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
@@ -4755,7 +4799,8 @@ var HISTORICAL = {
4755
4799
  9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494",
4756
4800
  10: "35bb7c0e30f5c41fe68e52259ec4764de6de2734064fa0e1521f3338eef0a09e",
4757
4801
  11: "818784f2088150d64d91b42eac944dc9503f104d32a0e7a18c753d2982a5adc0",
4758
- 12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208"
4802
+ 12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208",
4803
+ 13: "06ee70c9f0bb25e03f3930d2250da8dae172ade457f27ebba20382b6482ad3f4"
4759
4804
  },
4760
4805
  "composables/bcms-content.ts": {
4761
4806
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
@@ -4769,7 +4814,8 @@ var HISTORICAL = {
4769
4814
  9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4770
4815
  10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
4771
4816
  11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
4772
- 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
4817
+ 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
4818
+ 13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
4773
4819
  }
4774
4820
  };
4775
4821
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -5460,7 +5506,8 @@ function overlapping(rewrites) {
5460
5506
  for (const a of rewrites) {
5461
5507
  for (const b of rewrites) {
5462
5508
  if (a.raw !== void 0 || b.raw !== void 0) continue;
5463
- if (a !== b && a.key !== b.key && a.site.where === "text" && b.site.where === "text" && overlaps(a.site.range, b.site.range)) {
5509
+ if (a === b || a.key === b.key) continue;
5510
+ if (collide(a, b)) {
5464
5511
  out.add(a);
5465
5512
  out.add(b);
5466
5513
  }
@@ -5468,6 +5515,16 @@ function overlapping(rewrites) {
5468
5515
  }
5469
5516
  return [...out];
5470
5517
  }
5518
+ var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
5519
+ function collide(a, b) {
5520
+ const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
5521
+ const [wa, wb] = [writes(a), writes(b)];
5522
+ if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
5523
+ if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
5524
+ const [fa, fb] = [field(a), field(b)];
5525
+ return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
5526
+ }
5527
+ 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];
5471
5528
  function declaration(rewrite) {
5472
5529
  const { site, path, kind, scope } = rewrite;
5473
5530
  if (site.where === "attr" && kind !== "image") {
@@ -5482,8 +5539,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5482
5539
  const bindings = /* @__PURE__ */ new Map();
5483
5540
  const addDeclaration = (at, add) => {
5484
5541
  const held = attributes.get(at) ?? { plain: [], props: [] };
5485
- held.plain.push(...add.plain);
5486
- held.props.push(...add.props);
5542
+ held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
5543
+ held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
5487
5544
  attributes.set(at, held);
5488
5545
  };
5489
5546
  for (const rewrite of rewrites) {
@@ -5906,7 +5963,8 @@ function attrsOf4(node, content) {
5906
5963
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5907
5964
  expression: !quoted,
5908
5965
  range,
5909
- expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
5966
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
5967
+ ...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
5910
5968
  }
5911
5969
  ];
5912
5970
  });
@@ -6025,6 +6083,9 @@ function attrsOf5(node) {
6025
6083
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6026
6084
  }
6027
6085
  const literal = literalValue(attr);
6086
+ const tag = attr.value;
6087
+ const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
6088
+ const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
6028
6089
  return [
6029
6090
  {
6030
6091
  name,
@@ -6032,7 +6093,8 @@ function attrsOf5(node) {
6032
6093
  valueRange: literal && literal.value !== "" ? literal.range : null,
6033
6094
  expression: literal === null,
6034
6095
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6035
- expressionRange: null,
6096
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6097
+ ...digits ? { number: digits } : {},
6036
6098
  range
6037
6099
  }
6038
6100
  ];
@@ -6137,13 +6199,15 @@ var spanOf = (loc) => ({
6137
6199
  function attrsOf6(node) {
6138
6200
  return (node.props ?? []).flatMap((prop) => {
6139
6201
  if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
6202
+ const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
6140
6203
  return [
6141
6204
  {
6142
6205
  name: prop.arg.content,
6143
6206
  value: "",
6144
6207
  valueRange: null,
6145
6208
  expression: true,
6146
- expressionRange: null,
6209
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6210
+ ...digits ? { number: digits } : {},
6147
6211
  range: spanOf(prop.loc)
6148
6212
  }
6149
6213
  ];
@@ -6627,8 +6691,8 @@ function readPlan(value) {
6627
6691
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6628
6692
  check.optionalString(section.copy, `${at2}.copy`);
6629
6693
  if (section.fields !== void 0) {
6630
- for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
6631
- const shape = check.record(field, `${at2}.fields[${f}]`);
6694
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6695
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6632
6696
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6633
6697
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6634
6698
  }
@@ -6947,15 +7011,15 @@ function wire(source, candidate, form) {
6947
7011
  source.remove(start, attr.range.end);
6948
7012
  }
6949
7013
  }
6950
- for (const field of form.fields) {
6951
- const control = candidate.controls.get(field.key);
7014
+ for (const field2 of form.fields) {
7015
+ const control = candidate.controls.get(field2.key);
6952
7016
  if (candidate.foreign.has(control)) continue;
6953
7017
  const host = candidate.wrapper.get(control) ?? control;
6954
- if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
7018
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6955
7019
  const name = attrOf(control, "name");
6956
- if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
6957
- else if (!name.expression && name.value !== field.key && name.valueRange) {
6958
- source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
7020
+ if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
7021
+ else if (!name.expression && name.value !== field2.key && name.valueRange) {
7022
+ source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
6959
7023
  }
6960
7024
  }
6961
7025
  if (el.inner && !hasMessage(el)) {
@@ -6999,7 +7063,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
6999
7063
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
7000
7064
  }
7001
7065
  }
7002
- const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
7066
+ const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
7003
7067
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7004
7068
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7005
7069
  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);
@@ -7121,9 +7185,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7121
7185
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7122
7186
  continue;
7123
7187
  }
7124
- const misnamed = form.fields.find((field) => {
7125
- const attr = attrOf(candidate.controls.get(field.key), "name");
7126
- return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
7188
+ const misnamed = form.fields.find((field2) => {
7189
+ const attr = attrOf(candidate.controls.get(field2.key), "name");
7190
+ return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
7127
7191
  });
7128
7192
  if (misnamed) {
7129
7193
  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.`);
@@ -7203,7 +7267,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7203
7267
  );
7204
7268
  }
7205
7269
  for (const { form, candidate } of matched) {
7206
- const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
7270
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7207
7271
  if (keys.length > 0) {
7208
7272
  notes.push(
7209
7273
  `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"}.`
@@ -8462,7 +8526,7 @@ var inSection = (section, path) => {
8462
8526
  const target = rowless(path);
8463
8527
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8464
8528
  };
8465
- var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
8529
+ var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
8466
8530
  function elementIndex(roots) {
8467
8531
  const out = /* @__PURE__ */ new Map();
8468
8532
  const visit = (node, chain) => {
@@ -8620,8 +8684,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8620
8684
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8621
8685
  ` : "";
8622
8686
  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) };`;
8623
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8624
- const declared = [copy, field].filter(Boolean).join("\n");
8687
+ const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8688
+ const declared = [copy, field2].filter(Boolean).join("\n");
8625
8689
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8626
8690
  if (dialect === "astro") {
8627
8691
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8934,8 +8998,8 @@ function propPrefix(section) {
8934
8998
  }
8935
8999
  function exactPrefix(section) {
8936
9000
  const prefixes = /* @__PURE__ */ new Set();
8937
- for (const field of section.fields ?? []) {
8938
- const { path, key } = field;
9001
+ for (const field2 of section.fields ?? []) {
9002
+ const { path, key } = field2;
8939
9003
  if (typeof path !== "string" || typeof key !== "string") continue;
8940
9004
  if (!path.endsWith(key)) return null;
8941
9005
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8947,11 +9011,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8947
9011
  function flatPrefix(section) {
8948
9012
  const prefixes = /* @__PURE__ */ new Set();
8949
9013
  let informative = false;
8950
- for (const field of section.fields ?? []) {
8951
- const { path, key } = field;
9014
+ for (const field2 of section.fields ?? []) {
9015
+ const { path, key } = field2;
8952
9016
  if (typeof path !== "string" || typeof key !== "string") continue;
8953
9017
  informative = true;
8954
- const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
9018
+ const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8955
9019
  if (!prefix) return null;
8956
9020
  prefixes.add(prefix);
8957
9021
  }
@@ -8960,15 +9024,15 @@ function flatPrefix(section) {
8960
9024
  function flatPathMap(section, prefix) {
8961
9025
  const scalars = {};
8962
9026
  const tables = {};
8963
- for (const field of section.fields ?? []) {
8964
- const { path, key } = field;
9027
+ for (const field2 of section.fields ?? []) {
9028
+ const { path, key } = field2;
8965
9029
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8966
9030
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8967
9031
  if (!row) {
8968
- scalars[path] = field.overrideKey ?? key;
9032
+ scalars[path] = field2.overrideKey ?? key;
8969
9033
  continue;
8970
9034
  }
8971
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9035
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8972
9036
  if (!address) return null;
8973
9037
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8974
9038
  if (table.key !== address[1]) return null;
@@ -10833,7 +10897,7 @@ function styleEdits(before, after) {
10833
10897
  // package.json
10834
10898
  var package_default = {
10835
10899
  name: "@bettercms-ai/convert",
10836
- version: "0.25.0",
10900
+ version: "0.26.0",
10837
10901
  type: "module",
10838
10902
  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.",
10839
10903
  bin: {
@@ -10866,7 +10930,7 @@ var package_default = {
10866
10930
  dependencies: {
10867
10931
  "@astrojs/compiler": "^2.13.1",
10868
10932
  "@babel/parser": "^7.29.2",
10869
- "@bettercms-ai/types": "^1.30.0",
10933
+ "@bettercms-ai/types": "^1.32.0",
10870
10934
  "@vue/compiler-sfc": "^3.5.0",
10871
10935
  "magic-string": "^0.30.21",
10872
10936
  parse5: "^7.3.0",