@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/dist/index.d.ts CHANGED
@@ -589,6 +589,8 @@ interface AttrNode {
589
589
  * that has an expression says where it starts, rather than leaving each reader to scan for it.
590
590
  */
591
591
  expressionRange: Range | null;
592
+ /** The digits of `price={49}` — a number is copy a reader sees, so it is matched like a string. */
593
+ number?: string;
592
594
  }
593
595
  type Node = {
594
596
  type: "element";
@@ -1853,6 +1855,36 @@ declare function readBrief(value: unknown): Brief;
1853
1855
  */
1854
1856
  declare function readPlan(value: unknown): ComponentizePlan;
1855
1857
 
1858
+ /**
1859
+ * `build:bcms` — the build BetterCMS runs instead of `build`, when a repository declares one.
1860
+ *
1861
+ * The builder runs `build:bcms` in place of `build` (as Vercel runs `vercel-build`), and never edits
1862
+ * a project's build script itself. A converted repository's `build` usually chains the checks its
1863
+ * own CI wants in front of the build — `astro check && astro build` — and every BetterCMS deploy
1864
+ * paid for them: the type check alone was 8–10 s of a warm ScrewFast build. This writes the same
1865
+ * build without those checks, once, beside the script it came from. `build` is left exactly as it
1866
+ * was, so the project's own CI still checks everything.
1867
+ *
1868
+ * Only a script it can read with certainty: steps joined by `&&` and nothing else. A `;`, `||`,
1869
+ * pipe, background `&` or substitution means the steps do not simply run in order, and that script
1870
+ * is left alone. `prebuild` and `postbuild` are folded in (minus their own checks), because a run
1871
+ * of `build:bcms` triggers `prebuild:bcms`, not `prebuild` — dropping them would lose a step the
1872
+ * build needs (ScrewFast splits its content snapshot in `prebuild`).
1873
+ */
1874
+ interface BcmsBuild {
1875
+ /** The new package.json, formatted as the original was. */
1876
+ content: string;
1877
+ /** The `build:bcms` it adds. */
1878
+ script: string;
1879
+ /** The steps of `build` (and its pre/post hooks) left out of it. */
1880
+ dropped: string[];
1881
+ }
1882
+ /**
1883
+ * The package.json with a `build:bcms` added, or null when there is nothing to add: one already
1884
+ * exists, no `build` chains a check, or a script is not one it can read with certainty.
1885
+ */
1886
+ declare function bcmsBuildScript(packageJson: string): BcmsBuild | null;
1887
+
1856
1888
  /** The plan's own file shape, restated rather than imported to keep `index.ts` acyclic. */
1857
1889
  interface UpgradeFile {
1858
1890
  path: string;
@@ -1965,4 +1997,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
1965
1997
  receipt: ConversionReceipt;
1966
1998
  }>;
1967
1999
 
1968
- export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, type FixAction, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type HelperUpgrade, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingFix, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionUpgradeReceipt, type SectionsReceipt, type Site, type SiteWhere, type SkippedRoot, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, UpgradeError, type UpgradeOptions, type UpgradeRootRef, type UpgradeSkipReason, type UpgradedRoot, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findLiteral, findSites, findSitesTolerant, fixFor, flat, helperSource, helperUpgrades, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeCounts, routeOfFile, stripTags, unreadableConfigs, upgradeSections, walkAst, wireForms };
2000
+ export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type BcmsBuild, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, type FixAction, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type HelperUpgrade, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingFix, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionUpgradeReceipt, type SectionsReceipt, type Site, type SiteWhere, type SkippedRoot, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, UpgradeError, type UpgradeOptions, type UpgradeRootRef, type UpgradeSkipReason, type UpgradedRoot, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bcmsBuildScript, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findLiteral, findSites, findSitesTolerant, fixFor, flat, helperSource, helperUpgrades, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeCounts, routeOfFile, stripTags, unreadableConfigs, upgradeSections, walkAst, wireForms };
package/dist/index.js CHANGED
@@ -2711,8 +2711,9 @@ function collectSites(roots, literals) {
2711
2711
  }
2712
2712
  const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
2713
2713
  for (const attr of node.attrs) {
2714
- if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2715
- const value = norm(attr.value);
2714
+ const digits = attr.expression && attr.expressionRange ? attr.number : void 0;
2715
+ if (attr.expression && digits === void 0 || !digits && !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2716
+ const value = norm(digits ?? attr.value);
2716
2717
  for (const literal of wanted) {
2717
2718
  if (value !== literal) continue;
2718
2719
  matched.add(literal);
@@ -2720,7 +2721,7 @@ function collectSites(roots, literals) {
2720
2721
  file: "",
2721
2722
  tag: node.tag,
2722
2723
  attrInsertAt: node.attrInsertAt,
2723
- range: attr.valueRange,
2724
+ range: digits === void 0 ? attr.valueRange : attr.expressionRange,
2724
2725
  inner: node.inner,
2725
2726
  // 🔴 A COMPONENT'S ATTRIBUTE IS A PROP, and a prop is a different conversion: the value
2726
2727
  // is rendered in another file, by an element only that file knows. @see props.ts
@@ -2828,6 +2829,8 @@ function attrsOf2(open, content) {
2828
2829
  const value = attr.value;
2829
2830
  const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
2830
2831
  const literal = value?.type === "StringLiteral";
2832
+ const inside = value?.type === "JSXExpressionContainer" ? value.expression : null;
2833
+ const numeric = inside?.type === "NumericLiteral" ? String(inside.extra.raw) : "";
2831
2834
  return [
2832
2835
  {
2833
2836
  name: nameOf(attr.name),
@@ -2836,7 +2839,8 @@ function attrsOf2(open, content) {
2836
2839
  valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
2837
2840
  expression: !literal,
2838
2841
  range,
2839
- expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
2842
+ expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null,
2843
+ ...numeric ? { number: numeric } : {}
2840
2844
  }
2841
2845
  ];
2842
2846
  });
@@ -3312,14 +3316,21 @@ function exportedNames(ast) {
3312
3316
  });
3313
3317
  return out;
3314
3318
  }
3319
+ function printed(node) {
3320
+ if (node?.type === "StringLiteral") return [String(node.value)];
3321
+ if (node?.type === "NumericLiteral") return [String(node.value), String(node.extra?.raw ?? "")];
3322
+ const quasis = node?.type === "TemplateLiteral" ? node.quasis ?? [] : [];
3323
+ const expressions = node?.expressions ?? [];
3324
+ return quasis.length === 1 && expressions.length === 0 ? [String(quasis[0].value.cooked ?? "")] : [];
3325
+ }
3315
3326
  function propertyFor(row, literal) {
3316
3327
  if (row?.type !== "ObjectExpression") return null;
3317
3328
  for (const property of row.properties ?? []) {
3318
3329
  if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
3319
3330
  const key = property.key;
3320
3331
  const value = property.value;
3321
- if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
3322
- if (norm(String(value.value)) === literal) return String(key.name);
3332
+ if (key?.type !== "Identifier") continue;
3333
+ if (printed(value).some((text) => norm(text) === literal)) return String(key.name);
3323
3334
  }
3324
3335
  return null;
3325
3336
  }
@@ -3393,7 +3404,7 @@ function stringLiterals(ast) {
3393
3404
  if (node.type !== "VariableDeclarator") return;
3394
3405
  const id = node.id;
3395
3406
  const init = node.init;
3396
- if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
3407
+ if (id?.type !== "Identifier" || !init || printed(init).length === 0) return;
3397
3408
  out.set(String(id.name), init);
3398
3409
  });
3399
3410
  return out;
@@ -3424,7 +3435,7 @@ function scalarUses(content, roots, id) {
3424
3435
  }
3425
3436
  function bindScalar(ctx, member, literals, offset) {
3426
3437
  const rule = DIALECT_RULES[ctx.dialect];
3427
- const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
3438
+ const named = [...literals].filter(([, init2]) => printed(init2).some((text) => norm(text) === member.literal));
3428
3439
  if (named.length !== 1) return { splices: [], refusal: null };
3429
3440
  const [id, init] = named[0];
3430
3441
  const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
@@ -3639,16 +3650,17 @@ function literalTable(init) {
3639
3650
  const first = elements[0];
3640
3651
  if (!first || elements.length !== (init.elements ?? []).length) return null;
3641
3652
  if (elements.some((element) => element.type !== first.type)) return null;
3642
- if (first.type === "StringLiteral") {
3643
- return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
3653
+ if (printed(first).length > 0) {
3654
+ if (elements.some((element) => printed(element).length === 0)) return null;
3655
+ return { tuple: false, rows: elements.map((element) => [norm(printed(element)[0])]) };
3644
3656
  }
3645
3657
  if (first.type !== "ArrayExpression") return null;
3646
3658
  const rows = [];
3647
3659
  for (const element of elements) {
3648
3660
  const inner = (element.elements ?? []).filter((x) => x !== null);
3649
- if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
3661
+ if (inner.length === 0 || inner.some((x) => printed(x).length === 0)) return null;
3650
3662
  if (inner.length !== (element.elements ?? []).length) return null;
3651
- rows.push(inner.map((x) => norm(String(x.value))));
3663
+ rows.push(inner.map((x) => norm(printed(x)[0])));
3652
3664
  }
3653
3665
  if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3654
3666
  return { tuple: true, rows };
@@ -5465,7 +5477,8 @@ function overlapping(rewrites) {
5465
5477
  for (const a of rewrites) {
5466
5478
  for (const b of rewrites) {
5467
5479
  if (a.raw !== void 0 || b.raw !== void 0) continue;
5468
- if (a !== b && a.key !== b.key && a.site.where === "text" && b.site.where === "text" && overlaps(a.site.range, b.site.range)) {
5480
+ if (a === b || a.key === b.key) continue;
5481
+ if (collide(a, b)) {
5469
5482
  out.add(a);
5470
5483
  out.add(b);
5471
5484
  }
@@ -5473,6 +5486,16 @@ function overlapping(rewrites) {
5473
5486
  }
5474
5487
  return [...out];
5475
5488
  }
5489
+ var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
5490
+ function collide(a, b) {
5491
+ const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
5492
+ const [wa, wb] = [writes(a), writes(b)];
5493
+ if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
5494
+ if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
5495
+ const [fa, fb] = [field(a), field(b)];
5496
+ return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
5497
+ }
5498
+ 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];
5476
5499
  function declaration(rewrite) {
5477
5500
  const { site, path, kind, scope } = rewrite;
5478
5501
  if (site.where === "attr" && kind !== "image") {
@@ -5487,8 +5510,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5487
5510
  const bindings = /* @__PURE__ */ new Map();
5488
5511
  const addDeclaration = (at, add) => {
5489
5512
  const held = attributes.get(at) ?? { plain: [], props: [] };
5490
- held.plain.push(...add.plain);
5491
- held.props.push(...add.props);
5513
+ held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
5514
+ held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
5492
5515
  attributes.set(at, held);
5493
5516
  };
5494
5517
  for (const rewrite of rewrites) {
@@ -5919,7 +5942,8 @@ function attrsOf4(node, content) {
5919
5942
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5920
5943
  expression: !quoted,
5921
5944
  range,
5922
- expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
5945
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
5946
+ ...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
5923
5947
  }
5924
5948
  ];
5925
5949
  });
@@ -6038,6 +6062,9 @@ function attrsOf5(node) {
6038
6062
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6039
6063
  }
6040
6064
  const literal = literalValue(attr);
6065
+ const tag = attr.value;
6066
+ const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
6067
+ const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
6041
6068
  return [
6042
6069
  {
6043
6070
  name,
@@ -6045,7 +6072,8 @@ function attrsOf5(node) {
6045
6072
  valueRange: literal && literal.value !== "" ? literal.range : null,
6046
6073
  expression: literal === null,
6047
6074
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6048
- expressionRange: null,
6075
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6076
+ ...digits ? { number: digits } : {},
6049
6077
  range
6050
6078
  }
6051
6079
  ];
@@ -6150,13 +6178,15 @@ var spanOf = (loc) => ({
6150
6178
  function attrsOf6(node) {
6151
6179
  return (node.props ?? []).flatMap((prop) => {
6152
6180
  if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
6181
+ const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
6153
6182
  return [
6154
6183
  {
6155
6184
  name: prop.arg.content,
6156
6185
  value: "",
6157
6186
  valueRange: null,
6158
6187
  expression: true,
6159
- expressionRange: null,
6188
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6189
+ ...digits ? { number: digits } : {},
6160
6190
  range: spanOf(prop.loc)
6161
6191
  }
6162
6192
  ];
@@ -6644,8 +6674,8 @@ function readPlan(value) {
6644
6674
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6645
6675
  check.optionalString(section.copy, `${at2}.copy`);
6646
6676
  if (section.fields !== void 0) {
6647
- for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
6648
- const shape = check.record(field, `${at2}.fields[${f}]`);
6677
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6678
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6649
6679
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6650
6680
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6651
6681
  }
@@ -6964,15 +6994,15 @@ function wire(source, candidate, form) {
6964
6994
  source.remove(start, attr.range.end);
6965
6995
  }
6966
6996
  }
6967
- for (const field of form.fields) {
6968
- const control = candidate.controls.get(field.key);
6997
+ for (const field2 of form.fields) {
6998
+ const control = candidate.controls.get(field2.key);
6969
6999
  if (candidate.foreign.has(control)) continue;
6970
7000
  const host = candidate.wrapper.get(control) ?? control;
6971
- if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
7001
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6972
7002
  const name = attrOf(control, "name");
6973
- if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
6974
- else if (!name.expression && name.value !== field.key && name.valueRange) {
6975
- source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
7003
+ if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
7004
+ else if (!name.expression && name.value !== field2.key && name.valueRange) {
7005
+ source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
6976
7006
  }
6977
7007
  }
6978
7008
  if (el.inner && !hasMessage(el)) {
@@ -7016,7 +7046,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
7016
7046
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
7017
7047
  }
7018
7048
  }
7019
- const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
7049
+ const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
7020
7050
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7021
7051
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7022
7052
  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);
@@ -7138,9 +7168,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7138
7168
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7139
7169
  continue;
7140
7170
  }
7141
- const misnamed = form.fields.find((field) => {
7142
- const attr = attrOf(candidate.controls.get(field.key), "name");
7143
- return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
7171
+ const misnamed = form.fields.find((field2) => {
7172
+ const attr = attrOf(candidate.controls.get(field2.key), "name");
7173
+ return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
7144
7174
  });
7145
7175
  if (misnamed) {
7146
7176
  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.`);
@@ -7220,7 +7250,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7220
7250
  );
7221
7251
  }
7222
7252
  for (const { form, candidate } of matched) {
7223
- const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
7253
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7224
7254
  if (keys.length > 0) {
7225
7255
  notes.push(
7226
7256
  `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"}.`
@@ -7690,6 +7720,54 @@ function moduleScope(parsed, content) {
7690
7720
  return out;
7691
7721
  }
7692
7722
 
7723
+ // src/build-script.ts
7724
+ 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)/;
7725
+ 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*$/;
7726
+ var UNSAFE = /;|\|\||\||(?:^|[^&])&(?:[^&]|$)|\$\(|`|\n/;
7727
+ var isCheck = (step) => CHECK.test(step) || RUN_CHECK.test(step);
7728
+ function steps(script) {
7729
+ if (UNSAFE.test(script)) return null;
7730
+ const out = script.split("&&").map((s) => s.trim());
7731
+ return out.some((s) => s === "") ? null : out;
7732
+ }
7733
+ function bcmsBuildScript(packageJson) {
7734
+ let pkg;
7735
+ try {
7736
+ pkg = JSON.parse(packageJson);
7737
+ } catch {
7738
+ return null;
7739
+ }
7740
+ const scripts = pkg?.scripts;
7741
+ if (!scripts || typeof scripts !== "object" || "build:bcms" in scripts) return null;
7742
+ const build = scripts.build;
7743
+ if (typeof build !== "string") return null;
7744
+ const parts = [];
7745
+ for (const name of ["prebuild", "build", "postbuild"]) {
7746
+ const value = scripts[name];
7747
+ if (value === void 0) continue;
7748
+ if (typeof value !== "string") return null;
7749
+ const s = steps(value);
7750
+ if (!s) return null;
7751
+ parts.push(s);
7752
+ }
7753
+ const all = parts.flat();
7754
+ const dropped = all.filter(isCheck);
7755
+ const kept = all.filter((s) => !isCheck(s));
7756
+ if (dropped.length === 0 || kept.length === 0) return null;
7757
+ const script = kept.join(" && ");
7758
+ const next = {};
7759
+ for (const [key, value] of Object.entries(scripts)) {
7760
+ next[key] = value;
7761
+ if (key === "build") next["build:bcms"] = script;
7762
+ }
7763
+ const indent = /^[ \t]+(?=")/m.exec(packageJson)?.[0] ?? " ";
7764
+ const eol = packageJson.includes("\r\n") ? "\r\n" : "\n";
7765
+ let content = JSON.stringify({ ...pkg, scripts: next }, null, indent);
7766
+ if (eol !== "\n") content = content.replace(/\n/g, eol);
7767
+ if (/\r?\n$/.test(packageJson)) content += eol;
7768
+ return { content, script, dropped };
7769
+ }
7770
+
7693
7771
  // src/componentize.ts
7694
7772
  import MagicString5 from "magic-string";
7695
7773
  var SectionInvariantError = class extends Error {
@@ -8439,7 +8517,7 @@ var inSection = (section, path) => {
8439
8517
  const target = rowless(path);
8440
8518
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8441
8519
  };
8442
- var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
8520
+ var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
8443
8521
  function elementIndex(roots) {
8444
8522
  const out = /* @__PURE__ */ new Map();
8445
8523
  const visit = (node, chain) => {
@@ -8597,8 +8675,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8597
8675
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8598
8676
  ` : "";
8599
8677
  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) };`;
8600
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8601
- const declared = [copy, field].filter(Boolean).join("\n");
8678
+ const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8679
+ const declared = [copy, field2].filter(Boolean).join("\n");
8602
8680
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8603
8681
  if (dialect === "astro") {
8604
8682
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8911,8 +8989,8 @@ function propPrefix(section) {
8911
8989
  }
8912
8990
  function exactPrefix(section) {
8913
8991
  const prefixes = /* @__PURE__ */ new Set();
8914
- for (const field of section.fields ?? []) {
8915
- const { path, key } = field;
8992
+ for (const field2 of section.fields ?? []) {
8993
+ const { path, key } = field2;
8916
8994
  if (typeof path !== "string" || typeof key !== "string") continue;
8917
8995
  if (!path.endsWith(key)) return null;
8918
8996
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8924,11 +9002,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8924
9002
  function flatPrefix(section) {
8925
9003
  const prefixes = /* @__PURE__ */ new Set();
8926
9004
  let informative = false;
8927
- for (const field of section.fields ?? []) {
8928
- const { path, key } = field;
9005
+ for (const field2 of section.fields ?? []) {
9006
+ const { path, key } = field2;
8929
9007
  if (typeof path !== "string" || typeof key !== "string") continue;
8930
9008
  informative = true;
8931
- const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
9009
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8932
9010
  if (!prefix) return null;
8933
9011
  prefixes.add(prefix);
8934
9012
  }
@@ -8937,15 +9015,15 @@ function flatPrefix(section) {
8937
9015
  function flatPathMap(section, prefix) {
8938
9016
  const scalars = {};
8939
9017
  const tables = {};
8940
- for (const field of section.fields ?? []) {
8941
- const { path, key } = field;
9018
+ for (const field2 of section.fields ?? []) {
9019
+ const { path, key } = field2;
8942
9020
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8943
9021
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8944
9022
  if (!row) {
8945
- scalars[path] = field.overrideKey ?? key;
9023
+ scalars[path] = field2.overrideKey ?? key;
8946
9024
  continue;
8947
9025
  }
8948
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9026
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8949
9027
  if (!address) return null;
8950
9028
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8951
9029
  if (table.key !== address[1]) return null;
@@ -10788,6 +10866,7 @@ export {
10788
10866
  assetCandidates,
10789
10867
  assetImport,
10790
10868
  attrsFor,
10869
+ bcmsBuildScript,
10791
10870
  bindDataLiterals,
10792
10871
  bindImages,
10793
10872
  briefDigest,