@bettercms-ai/convert 0.21.0 → 0.23.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/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
6
6
  import { dirname, join as join2, relative, resolve, sep } from "path";
7
7
 
8
8
  // src/index.ts
9
- import MagicString6 from "magic-string";
9
+ import MagicString7 from "magic-string";
10
10
 
11
11
  // src/brief.ts
12
12
  import { createHash } from "crypto";
@@ -3767,8 +3767,8 @@ function bindLists(ctx, members, module, out, indexes) {
3767
3767
  const loops = iterations(ctx.parsed.roots, name);
3768
3768
  if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
3769
3769
  if (loops.length === 0) return null;
3770
- const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
3771
- if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
3770
+ const mentions3 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
3771
+ if (mentions3.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
3772
3772
  return { loop: loops[0], params: callbackParams(loops[0], name) };
3773
3773
  }
3774
3774
  });
@@ -4190,7 +4190,7 @@ async function bindImages(ctx, targets) {
4190
4190
 
4191
4191
  // src/helper.ts
4192
4192
  import { createHash as createHash2 } from "crypto";
4193
- var HELPER_VERSION = 10;
4193
+ var HELPER_VERSION = 11;
4194
4194
  var HEADER = "// @bettercms-ai/convert helper v";
4195
4195
  var HELPER_PATH = Object.fromEntries(
4196
4196
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4552,6 +4552,46 @@ const STYLE_SPACE_TOKEN_PX: Record<string, string> = {
4552
4552
  "space-xl": "64px",
4553
4553
  };
4554
4554
 
4555
+ const STYLE_THEME: Record<string, { colorScheme: string; background: string; color: string }> = {
4556
+ light: { colorScheme: "light", background: "#ffffff", color: "#111111" },
4557
+ dark: { colorScheme: "dark", background: "#0b0b0d", color: "#f7f8f8" },
4558
+ };
4559
+ const STYLE_BG: Record<string, string> = {
4560
+ surface: "var(--bcms-bg-surface,#ffffff)",
4561
+ muted: "var(--bcms-bg-muted,#f3f4f6)",
4562
+ accent: "var(--bcms-bg-accent,#4f46e5)",
4563
+ dark: "var(--bcms-bg-dark,#0b0b0d)",
4564
+ };
4565
+ const STYLE_CORNER_PX: Record<string, string> = { "0": "0", s: "4px", m: "8px", l: "16px", xl: "24px", "2xl": "32px" };
4566
+ const STYLE_SHADOW: Record<string, string> = {
4567
+ "0": "none",
4568
+ s: "0 1px 2px rgba(0,0,0,.06)",
4569
+ m: "0 4px 12px rgba(0,0,0,.08)",
4570
+ l: "0 10px 24px rgba(0,0,0,.10)",
4571
+ xl: "0 24px 48px rgba(0,0,0,.14)",
4572
+ };
4573
+ const STYLE_HAIRLINE = "1px solid rgba(0,0,0,.10)";
4574
+
4575
+ /** A map entry by OWN key only, so a token like \`constructor\` never reaches CSS. */
4576
+ function styleOwn<T>(map: Record<string, T>, key: unknown): T | undefined {
4577
+ return typeof key === "string" && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : undefined;
4578
+ }
4579
+
4580
+ /** An http(s) or root-relative url with nothing that could end \`url(\u2026)\`; "" for anything else. */
4581
+ function styleSafeUrl(raw: unknown): string {
4582
+ if (typeof raw !== "string") return "";
4583
+ const url = raw.trim();
4584
+ if (!url || url.length > 2000) return "";
4585
+ if (/["'()\\;]|\\s/.test(url)) return "";
4586
+ if (url.startsWith("//")) return "";
4587
+ if (url.startsWith("/")) return url;
4588
+ return /^https?:\\/\\//i.test(url) ? url : "";
4589
+ }
4590
+
4591
+ function styleClamp(n: number, lo: number, hi: number): number {
4592
+ return Math.min(hi, Math.max(lo, n));
4593
+ }
4594
+
4555
4595
  function styleSpaceToken(token: unknown): string {
4556
4596
  const px = typeof token === "string" ? STYLE_SPACE_TOKEN_PX[token] : undefined;
4557
4597
  return px ? "var(--bcms-" + token + "," + px + ")" : "";
@@ -4581,6 +4621,25 @@ export function bcmsSectionStyle(style: unknown): Record<string, string | number
4581
4621
  if (s.fontSize && STYLE_FONT_SIZE_REM[s.fontSize]) css.fontSize = STYLE_FONT_SIZE_REM[s.fontSize]!;
4582
4622
  if (s.weight && STYLE_WEIGHT[s.weight] != null) css.fontWeight = STYLE_WEIGHT[s.weight]!;
4583
4623
  if (s.radius && STYLE_RADIUS_PX[s.radius] != null) css.borderRadius = STYLE_RADIUS_PX[s.radius]!;
4624
+ const theme = styleOwn(STYLE_THEME, s.theme);
4625
+ if (theme) { css.colorScheme = theme.colorScheme; css.background = theme.background; css.color = theme.color; }
4626
+ if (s.bg === "custom") { if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s.bgCustom ?? "")) css.background = s.bgCustom!; }
4627
+ else if (styleOwn(STYLE_BG, s.bg)) css.background = styleOwn(STYLE_BG, s.bg)!;
4628
+ const bgImage = styleSafeUrl(s.bgImage);
4629
+ if (bgImage) { css.backgroundImage = "url(" + bgImage + ")"; css.backgroundSize = "cover"; css.backgroundPosition = "center"; }
4630
+ const n = style as Record<string, unknown>;
4631
+ if (!top && typeof n.paddingTop === "number") css.paddingTop = styleClamp(n.paddingTop, 0, 240) + "px";
4632
+ if (!bottom && typeof n.paddingBottom === "number") css.paddingBottom = styleClamp(n.paddingBottom, 0, 240) + "px";
4633
+ if (!sides && typeof n.paddingSides === "number") {
4634
+ const px = styleClamp(n.paddingSides, 0, 240) + "px";
4635
+ css.paddingLeft = px; css.paddingRight = px;
4636
+ }
4637
+ if (n.fullHeight === true) { css.minHeight = "100vh"; css.display = "flex"; css.flexDirection = "column"; css.justifyContent = "center"; }
4638
+ if (typeof n.opacity === "number") css.opacity = styleClamp(n.opacity, 0, 100) / 100;
4639
+ if (styleOwn(STYLE_CORNER_PX, s.corner) != null) { css.borderRadius = styleOwn(STYLE_CORNER_PX, s.corner)!; css.overflow = "hidden"; }
4640
+ if (styleOwn(STYLE_SHADOW, s.shadow)) css.boxShadow = styleOwn(STYLE_SHADOW, s.shadow)!;
4641
+ if (n.borderTop === true) css.borderTop = STYLE_HAIRLINE;
4642
+ if (n.borderBottom === true) css.borderBottom = STYLE_HAIRLINE;
4584
4643
  if ((style as { border?: unknown }).border === true) css.border = "1px solid currentColor";
4585
4644
  return Object.keys(css).length ? css : undefined;
4586
4645
  }
@@ -4605,7 +4664,8 @@ var HISTORICAL = {
4605
4664
  6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
4606
4665
  7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
4607
4666
  8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9",
4608
- 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3"
4667
+ 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4668
+ 10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910"
4609
4669
  },
4610
4670
  "src/lib/bcms-content.ts": {
4611
4671
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
@@ -4616,7 +4676,8 @@ var HISTORICAL = {
4616
4676
  6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5",
4617
4677
  7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7",
4618
4678
  8: "1a6f669d76d42effe77f082436e9d21c0efa6fc499e919e3d116bc3a27a7f3d5",
4619
- 9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494"
4679
+ 9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494",
4680
+ 10: "35bb7c0e30f5c41fe68e52259ec4764de6de2734064fa0e1521f3338eef0a09e"
4620
4681
  },
4621
4682
  "composables/bcms-content.ts": {
4622
4683
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
@@ -4627,7 +4688,8 @@ var HISTORICAL = {
4627
4688
  6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
4628
4689
  7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
4629
4690
  8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9",
4630
- 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3"
4691
+ 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4692
+ 10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910"
4631
4693
  }
4632
4694
  };
4633
4695
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -9116,6 +9178,371 @@ function readComponentizePlan(value) {
9116
9178
  return readPlan(value);
9117
9179
  }
9118
9180
 
9181
+ // src/upgrade.ts
9182
+ import MagicString6 from "magic-string";
9183
+ var UpgradeError = class extends Error {
9184
+ code = "HELPER_CONFLICT";
9185
+ constructor(message) {
9186
+ super(message);
9187
+ this.name = "UpgradeError";
9188
+ }
9189
+ };
9190
+ var SDK_SINCE = {
9191
+ astro: { pkg: "@bettercms-ai/astro", version: [0, 19, 0] },
9192
+ jsx: { pkg: "@bettercms-ai/next", version: [0, 18, 0] }
9193
+ };
9194
+ function sdkHasSectionRootAttrs(packageJson, dialect) {
9195
+ if (!packageJson) return false;
9196
+ let pkg;
9197
+ try {
9198
+ pkg = JSON.parse(packageJson);
9199
+ } catch {
9200
+ return false;
9201
+ }
9202
+ const { pkg: name, version: since } = SDK_SINCE[dialect];
9203
+ const range = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies].map((deps) => deps && typeof deps === "object" ? deps[name] : void 0).find((v2) => typeof v2 === "string");
9204
+ if (range === void 0) return false;
9205
+ if (range.trim() === "latest" || range.trim() === "*") return true;
9206
+ const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)\.(\d+)\.(\d+)/.exec(range);
9207
+ if (!match) return false;
9208
+ const v = [Number(match[1]), Number(match[2]), Number(match[3])];
9209
+ for (let i = 0; i < 3; i++) {
9210
+ if (v[i] !== since[i]) return v[i] > since[i];
9211
+ }
9212
+ return true;
9213
+ }
9214
+ function elementsIn2(nodes, out = []) {
9215
+ for (const node of nodes) {
9216
+ if (node.type === "element") {
9217
+ out.push(node);
9218
+ elementsIn2(node.children, out);
9219
+ } else if (node.type === "expr") elementsIn2(node.children, out);
9220
+ }
9221
+ return out;
9222
+ }
9223
+ var lineAt = (content, offset) => content.slice(0, offset).split("\n").length;
9224
+ var exprOf2 = (content, range) => content.slice(range.start + 1, range.end - 1).trim();
9225
+ var isSpread = (content, attr) => !!attr.expressionRange && content.slice(attr.expressionRange.start, attr.expressionRange.end).trimStart().startsWith("{...");
9226
+ var mentions2 = (content, name) => new RegExp(`(?<![\\w$.])${name.replace(/[$]/g, "\\$")}(?![\\w$])`).test(content);
9227
+ function importOf(content, matches) {
9228
+ const re = /import\s*\{([^}]*)\}\s*from\s*(["'])([^"']+)\2\s*;?/g;
9229
+ for (const m of content.matchAll(re)) {
9230
+ if (!matches(m[3])) continue;
9231
+ const names = m[1].split(",").map((n) => n.trim()).filter(Boolean);
9232
+ return { start: m.index, end: m.index + m[0].length, names };
9233
+ }
9234
+ return null;
9235
+ }
9236
+ function addNamedImport(source, content, parsed, specifier, sameModule, names) {
9237
+ const existing = importOf(content, sameModule);
9238
+ if (existing) {
9239
+ const missing = names.filter((n) => !existing.names.some((e) => e === n || e.endsWith(` as ${n}`)));
9240
+ if (missing.length === 0) return;
9241
+ const quote = content.slice(existing.start, existing.end).includes("'") ? "'" : '"';
9242
+ const spec = /from\s*["']([^"']+)["']/.exec(content.slice(existing.start, existing.end))[1];
9243
+ const all = [...existing.names, ...missing].sort((a, b) => a.localeCompare(b));
9244
+ const semi = content.slice(existing.start, existing.end).trimEnd().endsWith(";") ? ";" : "";
9245
+ source.overwrite(existing.start, existing.end, `import { ${all.join(", ")} } from ${quote}${spec}${quote}${semi}`);
9246
+ return;
9247
+ }
9248
+ const line = `import { ${[...names].sort().join(", ")} } from "${specifier}";`;
9249
+ const at = parsed.importAt;
9250
+ if (!at) return;
9251
+ if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${line}${at.wrap.after}`);
9252
+ else if (at.at === 0) source.appendLeft(0, `${line}
9253
+ `);
9254
+ else source.appendLeft(at.at, `
9255
+ ${line}`);
9256
+ }
9257
+ var importedFrom = (content, name, sameModule) => !!importOf(content, sameModule)?.names.some((e) => e === name || e.endsWith(` as ${name}`));
9258
+ var LIB_PROPS_DOC = " /** The placement's `block.style`, which the section root paints (`data-bcms-style`). */\n blockStyle?: Record<string, unknown>;";
9259
+ var LIB_PLACEMENT_DOC = " /** The placement's `block.style` \u2014 what the editor's section spacing and style controls write. */\n style?: Record<string, unknown>;";
9260
+ function insertAfterLine(content, from, to, anchor, addition) {
9261
+ const slice = content.slice(from, to);
9262
+ const m = anchor.exec(slice);
9263
+ if (!m) return null;
9264
+ const eol = content.indexOf("\n", from + m.index);
9265
+ if (eol < 0 || eol > to) return null;
9266
+ const nl = eolOf(content);
9267
+ return content.slice(0, eol + 1) + addition.split("\n").join(nl) + nl + content.slice(eol + 1);
9268
+ }
9269
+ var eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
9270
+ function interfaceBody(content, name) {
9271
+ const at = content.indexOf(`export interface ${name} {`);
9272
+ if (at < 0) return null;
9273
+ const end = content.indexOf("\n}", at);
9274
+ return end < 0 ? null : { start: at, end };
9275
+ }
9276
+ function patchLib(content) {
9277
+ let out = content;
9278
+ if (!/blockStyle\?:/.test(out)) {
9279
+ const body = interfaceBody(out, "BcmsSectionProps");
9280
+ if (!body) return null;
9281
+ const next = insertAfterLine(out, body.start, body.end, /^\s*page\?: unknown;/m, LIB_PROPS_DOC);
9282
+ if (next === null) return null;
9283
+ out = next;
9284
+ }
9285
+ const placement = interfaceBody(out, "BcmsPlacement");
9286
+ if (!placement) return null;
9287
+ if (!/^\s*style\?:/m.test(out.slice(placement.start, placement.end))) {
9288
+ const next = insertAfterLine(out, placement.start, placement.end, /^\s*overrides\?: Record<string, unknown>;/m, LIB_PLACEMENT_DOC);
9289
+ if (next === null) return null;
9290
+ out = next;
9291
+ }
9292
+ if (!/style: isRecord\(block\.style\)/.test(out)) {
9293
+ const push = /^([ \t]*)overrides: isRecord\(props\.overrides\) \? props\.overrides : undefined,\r?$/m.exec(out);
9294
+ if (!push) return null;
9295
+ const eol = out.indexOf("\n", push.index);
9296
+ out = out.slice(0, eol + 1) + `${push[1]}style: isRecord(block.style) ? block.style : undefined,${eolOf(out)}` + out.slice(eol + 1);
9297
+ }
9298
+ return out;
9299
+ }
9300
+ function patchRegistry(content) {
9301
+ if (content.includes("blockStyle={placement.style}")) return content;
9302
+ const lines = content.split("\n");
9303
+ const out = [];
9304
+ let patched = 0;
9305
+ for (const line of lines) {
9306
+ out.push(line);
9307
+ const m = /^([ \t]*)overrides=\{placement\.overrides\}(\r?)$/.exec(line);
9308
+ if (m) {
9309
+ out.push(`${m[1]}blockStyle={placement.style}${m[2]}`);
9310
+ patched++;
9311
+ }
9312
+ }
9313
+ return patched > 0 ? out.join("\n") : null;
9314
+ }
9315
+ var STYLE_READER = { astro: "bcmsSectionStyleText", jsx: "bcmsSectionStyle" };
9316
+ var BLOCK_MEMBER = /^([A-Za-z_$][\w$]*)(\??\.)(id|blockId)$/;
9317
+ var ID_ONLY = /^(?:(?:Astro\.)?props\??\.)?[A-Za-z_$][\w$]*$/;
9318
+ async function upgradeSections(sources, options = {}) {
9319
+ const contents = new Map(sources.map((f) => [f.path, f.content]));
9320
+ const out = /* @__PURE__ */ new Map();
9321
+ const write = (path, content) => {
9322
+ if (contents.get(path) === content) return;
9323
+ out.set(path, { path, operation: contents.has(path) ? "modify" : "add", content });
9324
+ };
9325
+ const receipt = {
9326
+ helpers: [],
9327
+ registry: [],
9328
+ roots: { upgraded: [], alreadyMarked: [], skipped: [] }
9329
+ };
9330
+ let registryOk = true;
9331
+ for (const file of sources) {
9332
+ if (!file.content.includes(REGISTRY_MARKER)) continue;
9333
+ const isLib = file.content.includes("export function pageSections(");
9334
+ const next = isLib ? patchLib(file.content) : patchRegistry(file.content);
9335
+ if (next === null) {
9336
+ registryOk = false;
9337
+ continue;
9338
+ }
9339
+ receipt.registry.push({ path: file.path, operation: next === file.content ? "unchanged" : "modify" });
9340
+ write(file.path, next);
9341
+ }
9342
+ const helperState = /* @__PURE__ */ new Map();
9343
+ const helperDialect = /* @__PURE__ */ new Map();
9344
+ for (const dialect of Object.keys(HELPER_PATH)) {
9345
+ const path = HELPER_PATH[dialect];
9346
+ if (!path || helperDialect.has(path)) continue;
9347
+ helperDialect.set(path, dialect);
9348
+ const existing = contents.get(path);
9349
+ helperState.set(path, existing === void 0 ? void 0 : isKnownHelper(existing, dialect));
9350
+ }
9351
+ const helperNeeded = /* @__PURE__ */ new Set();
9352
+ for (const file of sources) {
9353
+ const dialect = dialectOf(file.path);
9354
+ if (!dialect || !/data-bcms-block|sectionRootAttrs/.test(file.content)) continue;
9355
+ const ref = (el, attr) => ({
9356
+ file: file.path,
9357
+ line: lineAt(file.content, el.start),
9358
+ tag: el.tag,
9359
+ block: attr.expression ? file.content.slice(attr.expressionRange?.start ?? attr.range.start, attr.expressionRange?.end ?? attr.range.end) : JSON.stringify(attr.value)
9360
+ });
9361
+ const skip = (el, attr, reason, message) => {
9362
+ receipt.roots.skipped.push({ ...ref(el, attr), reason, message });
9363
+ };
9364
+ const parsed = await parseFile4(dialect, file.content);
9365
+ if (parsed.error) {
9366
+ receipt.roots.skipped.push({
9367
+ file: file.path,
9368
+ line: 1,
9369
+ tag: "",
9370
+ block: "",
9371
+ reason: "PARSE_ERROR",
9372
+ message: parsed.error.message
9373
+ });
9374
+ continue;
9375
+ }
9376
+ const roots = elementsIn2(parsed.roots).flatMap((el) => {
9377
+ const attr = el.attrs.find((a) => a.name === "data-bcms-block");
9378
+ return attr ? [{ el, attr }] : [];
9379
+ });
9380
+ for (const el of elementsIn2(parsed.roots)) {
9381
+ const spread = el.attrs.find((a) => isSpread(file.content, a) && /sectionRootAttrs\s*\(/.test(file.content.slice(a.expressionRange.start, a.expressionRange.end)));
9382
+ if (spread && !el.attrs.some((a) => a.name === "data-bcms-block")) {
9383
+ receipt.roots.alreadyMarked.push({
9384
+ file: file.path,
9385
+ line: lineAt(file.content, el.start),
9386
+ tag: el.tag,
9387
+ block: file.content.slice(spread.expressionRange.start, spread.expressionRange.end)
9388
+ });
9389
+ }
9390
+ }
9391
+ if (roots.length === 0) continue;
9392
+ if (dialect !== "astro" && dialect !== "jsx") {
9393
+ for (const { el, attr } of roots) {
9394
+ if (el.attrs.some((a) => a.name === "data-bcms-style")) receipt.roots.alreadyMarked.push(ref(el, attr));
9395
+ else skip(el, attr, "DIALECT_UNSUPPORTED", `${dialect} section roots are not rewritten by this pass; add data-bcms-style and the block's style by hand.`);
9396
+ }
9397
+ continue;
9398
+ }
9399
+ const source = new MagicString6(file.content);
9400
+ const reader = STYLE_READER[dialect];
9401
+ const helperPath = HELPER_PATH[dialect];
9402
+ const helperSpecifier = relativeImport(file.path, helperPath).replace(/\.tsx?$/, "");
9403
+ const isHelperModule = (spec) => /(^|\/)bcms-content(\.[jt]sx?)?$/.test(spec);
9404
+ const sdk = SDK_SINCE[dialect].pkg;
9405
+ const isSdk = (spec) => spec === sdk;
9406
+ const useSdk = sdkHasSectionRootAttrs(options.packageJson, dialect);
9407
+ const generated = SECTION_MARKERS.some((m) => file.content.includes(m));
9408
+ const typed = dialect === "astro" || file.path.endsWith(".tsx");
9409
+ const free = (name, from) => importedFrom(file.content, name, from) || !mentions2(file.content, name);
9410
+ let wantReader = false;
9411
+ let wantSdk = false;
9412
+ let sectionProp = false;
9413
+ for (const { el, attr } of roots) {
9414
+ if (el.attrs.some((a) => a.name === "data-bcms-style")) {
9415
+ receipt.roots.alreadyMarked.push(ref(el, attr));
9416
+ continue;
9417
+ }
9418
+ if (!isIntrinsic(el.tag)) {
9419
+ skip(el, attr, "NOT_AN_ELEMENT", `<${el.tag}> is a component: data-bcms-block on it is a prop. Mark the element that component renders.`);
9420
+ continue;
9421
+ }
9422
+ if (el.attrs.some((a) => a.name === "style")) {
9423
+ skip(el, attr, "OWN_STYLE", "The root already sets its own style; a second style attribute would be resolved by the dialect and one of them would vanish. Merge the block's style into it by hand.");
9424
+ continue;
9425
+ }
9426
+ if (el.attrs.some((a) => isSpread(file.content, a))) {
9427
+ skip(el, attr, "SPREAD", "The root spreads props that may carry a style; marking it could promise a style the spread overrides.");
9428
+ continue;
9429
+ }
9430
+ if (!attr.expression || !attr.expressionRange) {
9431
+ skip(el, attr, "BLOCK_LITERAL", "data-bcms-block is a literal id: there is no block object here to read a style from.");
9432
+ continue;
9433
+ }
9434
+ const expr = exprOf2(file.content, attr.expressionRange);
9435
+ if (generated && expr === "blockId") {
9436
+ if (!registryOk) {
9437
+ skip(el, attr, "REGISTRY_UNRECOGNISED", "The generated registry or bcms-sections does not pass block.style and could not be patched; re-run --componentize, then this.");
9438
+ continue;
9439
+ }
9440
+ if (!free(reader, isHelperModule) || /\bblockStyle\b/.test(file.content)) {
9441
+ skip(el, attr, "NAME_TAKEN", `${reader} or blockStyle already means something else in this file.`);
9442
+ continue;
9443
+ }
9444
+ source.appendLeft(attr.range.end, ` data-bcms-style="" style={${reader}(blockStyle)}`);
9445
+ receipt.roots.upgraded.push({ ...ref(el, attr), form: "section-prop" });
9446
+ wantReader = true;
9447
+ sectionProp = true;
9448
+ continue;
9449
+ }
9450
+ const member = BLOCK_MEMBER.exec(expr);
9451
+ if (member) {
9452
+ const [, object, dot, key] = member;
9453
+ if (useSdk && key === "id") {
9454
+ if (!free("sectionRootAttrs", isSdk)) {
9455
+ skip(el, attr, "NAME_TAKEN", "sectionRootAttrs already means something else in this file.");
9456
+ continue;
9457
+ }
9458
+ source.overwrite(attr.range.start, attr.range.end, `{...sectionRootAttrs(${object})}`);
9459
+ receipt.roots.upgraded.push({ ...ref(el, attr), form: "sectionRootAttrs" });
9460
+ wantSdk = true;
9461
+ continue;
9462
+ }
9463
+ if (!free(reader, isHelperModule)) {
9464
+ skip(el, attr, "NAME_TAKEN", `${reader} already means something else in this file.`);
9465
+ continue;
9466
+ }
9467
+ const style = typed && dialect === "jsx" ? `(${object} as { style?: unknown } | null | undefined)?.style` : `${object}${dot}style`;
9468
+ source.appendLeft(attr.range.end, ` data-bcms-style="" style={${reader}(${style})}`);
9469
+ receipt.roots.upgraded.push({ ...ref(el, attr), form: "helper" });
9470
+ wantReader = true;
9471
+ continue;
9472
+ }
9473
+ if (ID_ONLY.test(expr)) {
9474
+ skip(
9475
+ el,
9476
+ attr,
9477
+ "BLOCK_ID_ONLY",
9478
+ `Only a block id reaches this root (${expr}); the block's style is not in scope. Pass the block (or its style) down with the id, then re-run.`
9479
+ );
9480
+ continue;
9481
+ }
9482
+ skip(el, attr, "BLOCK_UNRESOLVABLE", `data-bcms-block={${expr}} does not name one block whose style this root could paint.`);
9483
+ }
9484
+ if (sectionProp) {
9485
+ const destructure = dialect === "astro" ? /const\s*\{([^}]*\bblockId\b[^}]*)\}\s*=\s*Astro\.props\s*;?/.exec(file.content) : /\(\s*\{([^}]*\bblockId\b[^}]*)\}\s*:\s*BcmsSectionProps\s*\)/.exec(file.content);
9486
+ if (destructure) {
9487
+ const close = destructure.index + destructure[0].indexOf("}");
9488
+ let at = close;
9489
+ while (at > 0 && /\s/.test(file.content[at - 1])) at--;
9490
+ source.appendLeft(at, ", blockStyle");
9491
+ }
9492
+ if (dialect === "astro") {
9493
+ const props = /interface Props \{[\s\S]*?\n\}/.exec(file.content);
9494
+ if (props) {
9495
+ const end = props.index + props[0].length - 1;
9496
+ const nl = eolOf(file.content);
9497
+ source.appendLeft(
9498
+ end,
9499
+ ` /** The placement's \`block.style\` \u2014 painted on the root, which says so with \`data-bcms-style\`. */${nl} blockStyle?: Record<string, unknown>;${nl}`
9500
+ );
9501
+ }
9502
+ }
9503
+ }
9504
+ if (wantReader) {
9505
+ addNamedImport(source, file.content, parsed, helperSpecifier, isHelperModule, [reader]);
9506
+ helperNeeded.add(helperPath);
9507
+ }
9508
+ if (wantSdk) addNamedImport(source, file.content, parsed, sdk, isSdk, ["sectionRootAttrs"]);
9509
+ if (source.hasChanged()) write(file.path, source.toString());
9510
+ }
9511
+ for (const [path, version] of helperState) {
9512
+ const dialect = helperDialect.get(path);
9513
+ const needed = helperNeeded.has(path);
9514
+ if (version === void 0) {
9515
+ if (!needed) continue;
9516
+ write(path, helperSource(dialect));
9517
+ receipt.helpers.push({ path, from: null, to: HELPER_VERSION, operation: "add" });
9518
+ const stub = `${CONTENT_DIR}/layout.json`;
9519
+ if (!contents.has(stub)) write(stub, STUB_CONTENT);
9520
+ continue;
9521
+ }
9522
+ if (version === null) {
9523
+ if (needed && !options.overwriteHelper) {
9524
+ throw new UpgradeError(
9525
+ `${path} already exists and its bytes are not ones @bettercms-ai/convert wrote \u2014 someone else's module, or ours with edits in it. The upgraded roots read bcmsSectionStyle from it. Move it, or re-run with --overwrite-helper.`
9526
+ );
9527
+ }
9528
+ if (needed) {
9529
+ write(path, helperSource(dialect));
9530
+ receipt.helpers.push({ path, from: null, to: HELPER_VERSION, operation: "modify" });
9531
+ } else receipt.helpers.push({ path, from: null, to: HELPER_VERSION, operation: "foreign" });
9532
+ continue;
9533
+ }
9534
+ if (version < HELPER_VERSION) {
9535
+ write(path, helperSource(dialect));
9536
+ receipt.helpers.push({ path, from: version, to: HELPER_VERSION, operation: "modify" });
9537
+ } else receipt.helpers.push({ path, from: version, to: HELPER_VERSION, operation: "unchanged" });
9538
+ }
9539
+ const byPlace = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
9540
+ receipt.roots.upgraded.sort(byPlace);
9541
+ receipt.roots.alreadyMarked.sort(byPlace);
9542
+ receipt.roots.skipped.sort(byPlace);
9543
+ return { files: [...out.values()].sort((a, b) => a.path.localeCompare(b.path)), receipt };
9544
+ }
9545
+
9119
9546
  // src/index.ts
9120
9547
  var UNPARSEABLE_DIALECT = {
9121
9548
  roots: [],
@@ -9453,7 +9880,7 @@ async function convertSources(briefIn, sources, options) {
9453
9880
  rewritten++;
9454
9881
  }
9455
9882
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
9456
- const source = new MagicString6(contents.get(file));
9883
+ const source = new MagicString7(contents.get(file));
9457
9884
  const seen = /* @__PURE__ */ new Set();
9458
9885
  for (const splice of splices) {
9459
9886
  const id = `${splice.start}:${splice.end}:${splice.text}`;
@@ -10278,7 +10705,7 @@ function styleEdits(before, after) {
10278
10705
  // package.json
10279
10706
  var package_default = {
10280
10707
  name: "@bettercms-ai/convert",
10281
- version: "0.21.0",
10708
+ version: "0.23.0",
10282
10709
  type: "module",
10283
10710
  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.",
10284
10711
  bin: {
@@ -10311,7 +10738,7 @@ var package_default = {
10311
10738
  dependencies: {
10312
10739
  "@astrojs/compiler": "^2.13.1",
10313
10740
  "@babel/parser": "^7.29.2",
10314
- "@bettercms-ai/types": "^1.29.0",
10741
+ "@bettercms-ai/types": "^1.30.0",
10315
10742
  "@vue/compiler-sfc": "^3.5.0",
10316
10743
  "magic-string": "^0.30.21",
10317
10744
  parse5: "^7.3.0",
@@ -10341,6 +10768,7 @@ function parseArgs(argv) {
10341
10768
  componentize: false,
10342
10769
  forms: false,
10343
10770
  pins: {},
10771
+ upgradeSections: false,
10344
10772
  xray: void 0,
10345
10773
  verifyStyles: false,
10346
10774
  base: "HEAD",
@@ -10359,6 +10787,7 @@ function parseArgs(argv) {
10359
10787
  if (arg === "--brief") args.brief = argv[++i];
10360
10788
  else if (arg === "--componentize") args.componentize = true;
10361
10789
  else if (arg === "--forms") args.forms = true;
10790
+ else if (arg === "--upgrade-sections") args.upgradeSections = true;
10362
10791
  else if (arg === "--pin") {
10363
10792
  const raw = argv[++i] ?? "";
10364
10793
  const at = raw.indexOf("=");
@@ -10398,6 +10827,7 @@ var HELP = `bettercms-convert ${VERSION}
10398
10827
  bettercms-convert --brief <json> --root . [options]
10399
10828
  bettercms-convert --componentize --brief <json> --plan <json> --root . [options]
10400
10829
  bettercms-convert --forms --brief <json> --root . [options]
10830
+ bettercms-convert --upgrade-sections --root . [--dry-run]
10401
10831
  bettercms-convert --xray <dist-dir>
10402
10832
  bettercms-convert --verify-styles [--base <ref>] --root .
10403
10833
 
@@ -10410,6 +10840,14 @@ var HELP = `bettercms-convert ${VERSION}
10410
10840
  --pin <form>=<file> With --forms: the file a form's <form> is in, when the run reports
10411
10841
  FORM_AMBIGUOUS (two files post exactly its fields). Form by id or
10412
10842
  name. Repeatable.
10843
+ --upgrade-sections For a site converted before helper v10: give every section root
10844
+ (data-bcms-block) the data-bcms-style marker and the block's style,
10845
+ so the editor's section spacing and style handles turn on. Upgrades
10846
+ the helper, patches a generated registry to pass block.style, and
10847
+ spreads {...sectionRootAttrs(block)} where the SDK has it. A root
10848
+ with its own style or a spread, or whose block is not in scope, is
10849
+ left alone and listed with its reason. Takes no --brief; a second
10850
+ run changes nothing. Exit 2 with --strict when any root was skipped.
10413
10851
  --xray <dist-dir> Measure a LOCAL build instead of converting: how much of each page's
10414
10852
  visible copy no binding owns, and the biggest buckets of it. Run it
10415
10853
  after a build to see what the next conversion still has to reach,
@@ -10541,6 +10979,28 @@ ${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-
10541
10979
  );
10542
10980
  }
10543
10981
  }
10982
+ function reportUpgrade(files, receipt, dryRun) {
10983
+ for (const file of files) {
10984
+ process.stdout.write(`${dryRun ? "would " : ""}${file.operation} ${file.path}
10985
+ `);
10986
+ }
10987
+ for (const h of receipt.helpers) {
10988
+ process.stdout.write(`helper ${h.path} ${h.from === null ? "(none)" : `v${h.from}`} -> v${h.to} (${h.operation})
10989
+ `);
10990
+ }
10991
+ for (const r of receipt.registry) process.stdout.write(`registry ${r.path} (${r.operation})
10992
+ `);
10993
+ const { upgraded, alreadyMarked, skipped } = receipt.roots;
10994
+ process.stdout.write(
10995
+ `
10996
+ ${upgraded.length} section roots upgraded (${alreadyMarked.length} already marked, ${skipped.length} skipped)
10997
+ `
10998
+ );
10999
+ for (const r of upgraded) process.stdout.write(` upgraded ${r.file}:${r.line} <${r.tag}> ${r.block} \u2014 ${r.form}
11000
+ `);
11001
+ for (const r of skipped) process.stdout.write(` skipped ${r.file}:${r.line} <${r.tag}> ${r.block} \u2014 ${r.reason}: ${r.message}
11002
+ `);
11003
+ }
10544
11004
  async function xray(dir, strict) {
10545
11005
  const pages = [];
10546
11006
  const walk = async (at) => {
@@ -10614,6 +11074,31 @@ async function main() {
10614
11074
  return await xray(resolve(args.xray), args.strict);
10615
11075
  }
10616
11076
  if (args.verifyStyles) return await verifyStyles(resolve(args.root), args.base, args.verbose);
11077
+ if (args.upgradeSections) {
11078
+ if (args.brief || args.componentize || args.forms || args.plan) {
11079
+ throw new CliError("--upgrade-sections reads the repository only: no --brief, --plan, --componentize or --forms. See --help.");
11080
+ }
11081
+ const root2 = resolve(args.root);
11082
+ let packageJson;
11083
+ try {
11084
+ packageJson = await readFile(join2(root2, "package.json"), "utf-8");
11085
+ } catch {
11086
+ packageJson = void 0;
11087
+ }
11088
+ const sources2 = [...await readTree(root2, args.verbose), ...await readSnapshots(root2)];
11089
+ const { files: files2, receipt: receipt2 } = await upgradeSections(sources2, { overwriteHelper: args.overwriteHelper, packageJson });
11090
+ if (!args.dryRun) {
11091
+ for (const file of files2) {
11092
+ const target = under(root2, file.path);
11093
+ await mkdir(dirname(target), { recursive: true });
11094
+ await writeFile(target, file.content, "utf-8");
11095
+ }
11096
+ }
11097
+ if (args.receipt) await writeFile(resolve(args.receipt), `${JSON.stringify(receipt2, null, 2)}
11098
+ `, "utf-8");
11099
+ reportUpgrade(files2, receipt2, args.dryRun);
11100
+ return args.strict && receipt2.roots.skipped.length > 0 ? 2 : 0;
11101
+ }
10617
11102
  if (!args.brief) throw new CliError("--brief <file> is required. See --help.");
10618
11103
  if (args.plan && !args.componentize) {
10619
11104
  throw new CliError("--plan is only read with --componentize. See --help.");