@bettercms-ai/convert 0.25.0 → 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,13 @@
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
+
3
11
  ## 0.25.0
4
12
 
5
13
  ### 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 };
@@ -5460,7 +5472,8 @@ function overlapping(rewrites) {
5460
5472
  for (const a of rewrites) {
5461
5473
  for (const b of rewrites) {
5462
5474
  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)) {
5475
+ if (a === b || a.key === b.key) continue;
5476
+ if (collide(a, b)) {
5464
5477
  out.add(a);
5465
5478
  out.add(b);
5466
5479
  }
@@ -5468,6 +5481,16 @@ function overlapping(rewrites) {
5468
5481
  }
5469
5482
  return [...out];
5470
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];
5471
5494
  function declaration(rewrite) {
5472
5495
  const { site, path, kind, scope } = rewrite;
5473
5496
  if (site.where === "attr" && kind !== "image") {
@@ -5482,8 +5505,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5482
5505
  const bindings = /* @__PURE__ */ new Map();
5483
5506
  const addDeclaration = (at, add) => {
5484
5507
  const held = attributes.get(at) ?? { plain: [], props: [] };
5485
- held.plain.push(...add.plain);
5486
- 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))));
5487
5510
  attributes.set(at, held);
5488
5511
  };
5489
5512
  for (const rewrite of rewrites) {
@@ -5906,7 +5929,8 @@ function attrsOf4(node, content) {
5906
5929
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5907
5930
  expression: !quoted,
5908
5931
  range,
5909
- 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() } : {}
5910
5934
  }
5911
5935
  ];
5912
5936
  });
@@ -6025,6 +6049,9 @@ function attrsOf5(node) {
6025
6049
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6026
6050
  }
6027
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 ?? "") : "";
6028
6055
  return [
6029
6056
  {
6030
6057
  name,
@@ -6032,7 +6059,8 @@ function attrsOf5(node) {
6032
6059
  valueRange: literal && literal.value !== "" ? literal.range : null,
6033
6060
  expression: literal === null,
6034
6061
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6035
- expressionRange: null,
6062
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6063
+ ...digits ? { number: digits } : {},
6036
6064
  range
6037
6065
  }
6038
6066
  ];
@@ -6137,13 +6165,15 @@ var spanOf = (loc) => ({
6137
6165
  function attrsOf6(node) {
6138
6166
  return (node.props ?? []).flatMap((prop) => {
6139
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() : "";
6140
6169
  return [
6141
6170
  {
6142
6171
  name: prop.arg.content,
6143
6172
  value: "",
6144
6173
  valueRange: null,
6145
6174
  expression: true,
6146
- expressionRange: null,
6175
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6176
+ ...digits ? { number: digits } : {},
6147
6177
  range: spanOf(prop.loc)
6148
6178
  }
6149
6179
  ];
@@ -6627,8 +6657,8 @@ function readPlan(value) {
6627
6657
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6628
6658
  check.optionalString(section.copy, `${at2}.copy`);
6629
6659
  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}]`);
6660
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6661
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6632
6662
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6633
6663
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6634
6664
  }
@@ -6947,15 +6977,15 @@ function wire(source, candidate, form) {
6947
6977
  source.remove(start, attr.range.end);
6948
6978
  }
6949
6979
  }
6950
- for (const field of form.fields) {
6951
- const control = candidate.controls.get(field.key);
6980
+ for (const field2 of form.fields) {
6981
+ const control = candidate.controls.get(field2.key);
6952
6982
  if (candidate.foreign.has(control)) continue;
6953
6983
  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)}"`);
6984
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6955
6985
  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));
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));
6959
6989
  }
6960
6990
  }
6961
6991
  if (el.inner && !hasMessage(el)) {
@@ -6999,7 +7029,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
6999
7029
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
7000
7030
  }
7001
7031
  }
7002
- 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;
7003
7033
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7004
7034
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7005
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);
@@ -7121,9 +7151,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7121
7151
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7122
7152
  continue;
7123
7153
  }
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);
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);
7127
7157
  });
7128
7158
  if (misnamed) {
7129
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.`);
@@ -7203,7 +7233,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7203
7233
  );
7204
7234
  }
7205
7235
  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);
7236
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7207
7237
  if (keys.length > 0) {
7208
7238
  notes.push(
7209
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"}.`
@@ -8462,7 +8492,7 @@ var inSection = (section, path) => {
8462
8492
  const target = rowless(path);
8463
8493
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8464
8494
  };
8465
- 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 ?? ""}`);
8466
8496
  function elementIndex(roots) {
8467
8497
  const out = /* @__PURE__ */ new Map();
8468
8498
  const visit = (node, chain) => {
@@ -8620,8 +8650,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8620
8650
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8621
8651
  ` : "";
8622
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) };`;
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");
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");
8625
8655
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8626
8656
  if (dialect === "astro") {
8627
8657
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8934,8 +8964,8 @@ function propPrefix(section) {
8934
8964
  }
8935
8965
  function exactPrefix(section) {
8936
8966
  const prefixes = /* @__PURE__ */ new Set();
8937
- for (const field of section.fields ?? []) {
8938
- const { path, key } = field;
8967
+ for (const field2 of section.fields ?? []) {
8968
+ const { path, key } = field2;
8939
8969
  if (typeof path !== "string" || typeof key !== "string") continue;
8940
8970
  if (!path.endsWith(key)) return null;
8941
8971
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8947,11 +8977,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8947
8977
  function flatPrefix(section) {
8948
8978
  const prefixes = /* @__PURE__ */ new Set();
8949
8979
  let informative = false;
8950
- for (const field of section.fields ?? []) {
8951
- const { path, key } = field;
8980
+ for (const field2 of section.fields ?? []) {
8981
+ const { path, key } = field2;
8952
8982
  if (typeof path !== "string" || typeof key !== "string") continue;
8953
8983
  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));
8984
+ const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8955
8985
  if (!prefix) return null;
8956
8986
  prefixes.add(prefix);
8957
8987
  }
@@ -8960,15 +8990,15 @@ function flatPrefix(section) {
8960
8990
  function flatPathMap(section, prefix) {
8961
8991
  const scalars = {};
8962
8992
  const tables = {};
8963
- for (const field of section.fields ?? []) {
8964
- const { path, key } = field;
8993
+ for (const field2 of section.fields ?? []) {
8994
+ const { path, key } = field2;
8965
8995
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8966
8996
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8967
8997
  if (!row) {
8968
- scalars[path] = field.overrideKey ?? key;
8998
+ scalars[path] = field2.overrideKey ?? key;
8969
8999
  continue;
8970
9000
  }
8971
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9001
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8972
9002
  if (!address) return null;
8973
9003
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8974
9004
  if (table.key !== address[1]) return null;
@@ -10833,7 +10863,7 @@ function styleEdits(before, after) {
10833
10863
  // package.json
10834
10864
  var package_default = {
10835
10865
  name: "@bettercms-ai/convert",
10836
- version: "0.25.0",
10866
+ version: "0.25.1",
10837
10867
  type: "module",
10838
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.",
10839
10869
  bin: {
@@ -10866,7 +10896,7 @@ var package_default = {
10866
10896
  dependencies: {
10867
10897
  "@astrojs/compiler": "^2.13.1",
10868
10898
  "@babel/parser": "^7.29.2",
10869
- "@bettercms-ai/types": "^1.30.0",
10899
+ "@bettercms-ai/types": "^1.32.0",
10870
10900
  "@vue/compiler-sfc": "^3.5.0",
10871
10901
  "magic-string": "^0.30.21",
10872
10902
  parse5: "^7.3.0",