@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/CHANGELOG.md +18 -0
- package/README.md +39 -0
- package/dist/cli.js +495 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +80 -2
- package/dist/index.js +437 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
3771
|
-
if (
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
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.");
|