@bettercms-ai/convert 0.22.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 +6 -0
- package/README.md +39 -0
- package/dist/cli.js +428 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +79 -1
- package/dist/index.js +371 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6c75c11: `bettercms-convert --upgrade-sections --root .` gives a site converted before helper v10 the section style marker, so the visual editor shows section spacing and style handles on its pages. Every element that carries `data-bcms-block` and no `data-bcms-style` gets the marker together with the style it promises. A component that `--componentize` wrote gets `data-bcms-style="" style={bcmsSectionStyle[Text](blockStyle)}` and the `blockStyle` prop, and its registry and `src/lib/bcms-sections.ts` are patched to pass each placement's `block.style`. The result is byte-identical to what a fresh extraction writes today. `data-bcms-block={block.id}` becomes `{...sectionRootAttrs(block)}` when the SDK range guarantees that export (`@bettercms-ai/astro` 0.19, `@bettercms-ai/next` 0.18), and otherwise gets the marker and `block.style` through the helper. The helper is upgraded to the current version, or added with its layout stub. Roots with their own `style` or a spread, roots that receive only a block id, and literal, conditional or unresolvable blocks are left alone. Each one is listed in the receipt with its reason (`OWN_STYLE`, `SPREAD`, `BLOCK_ID_ONLY`, `BLOCK_LITERAL`, `BLOCK_UNRESOLVABLE`, …). A second run writes nothing, and no class, className or existing style token changes. `--dry-run`, `--receipt`, `--strict` and `--overwrite-helper` work as in the other modes.
|
|
8
|
+
|
|
3
9
|
## 0.22.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -601,6 +601,45 @@ the now-unused `bcms`/snapshot imports P2 added, and a page whose registered cal
|
|
|
601
601
|
`<Sections>` keeps the imports of the components it used to call. They are harmless at build time
|
|
602
602
|
and removing them would mean editing statements no section asked about.
|
|
603
603
|
|
|
604
|
+
### Section spacing handles on a site converted before helper v10 (`--upgrade-sections`)
|
|
605
|
+
|
|
606
|
+
The visual editor shows a page's section spacing and style handles only when EVERY root the build
|
|
607
|
+
stamps with `data-bcms-block` also carries `data-bcms-style` — the site's promise that the root's
|
|
608
|
+
inline style is `blockStyleToCss(block.style)`. Conversions before 0.21 (helper v6–v9, componentize
|
|
609
|
+
v2 sections, hand-written roots) stamp the block and not the marker, so those pages never get
|
|
610
|
+
handles. `get_next_steps` says so (`upgrade-section-roots`) when the release finds such roots.
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
npx @bettercms-ai/convert@latest --upgrade-sections --root . --dry-run # read the plan
|
|
614
|
+
npx @bettercms-ai/convert@latest --upgrade-sections --root . # then push and release
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Each root with `data-bcms-block` and no `data-bcms-style` gets the marker together with the style:
|
|
618
|
+
|
|
619
|
+
- a component `--componentize` wrote (`data-bcms-block={blockId}`): `data-bcms-style=""
|
|
620
|
+
style={bcmsSectionStyle[Text](blockStyle)}`, the `blockStyle` prop, and the registry and
|
|
621
|
+
`src/lib/bcms-sections.ts` patched to pass `block.style` down — byte-identical to what a fresh
|
|
622
|
+
extraction writes today;
|
|
623
|
+
- `data-bcms-block={block.id}` where the SDK has `sectionRootAttrs` (`@bettercms-ai/astro` >= 0.19,
|
|
624
|
+
`@bettercms-ai/next` >= 0.18, read off the LOWEST version the range admits):
|
|
625
|
+
`{...sectionRootAttrs(block)}`;
|
|
626
|
+
- the same root on an older SDK: `data-bcms-style="" style={bcmsSectionStyle[Text](block.style)}`
|
|
627
|
+
through the helper, which is upgraded (or added, with its `bcms-content/layout.json` stub).
|
|
628
|
+
|
|
629
|
+
Left alone, and listed as `skipped` with a reason: `OWN_STYLE` (the root sets its own `style`),
|
|
630
|
+
`SPREAD` (a `{...rest}` that may carry one), `BLOCK_ID_ONLY` (only an id reaches the root —
|
|
631
|
+
`data-bcms-block={Astro.props.bcmsBlock}`; pass the block or its style down), `BLOCK_LITERAL`,
|
|
632
|
+
`BLOCK_UNRESOLVABLE` (a conditional or a call), `NOT_AN_ELEMENT`, `NAME_TAKEN`,
|
|
633
|
+
`REGISTRY_UNRECOGNISED`, `DIALECT_UNSUPPORTED` (html, vue, svelte), `PARSE_ERROR`. A helper this
|
|
634
|
+
package did not write is `HELPER_CONFLICT` (exit 1) only when a root needs it; `--overwrite-helper`
|
|
635
|
+
replaces it.
|
|
636
|
+
|
|
637
|
+
The receipt (`--receipt <file>`) is `{ helpers, registry, roots: { upgraded, alreadyMarked,
|
|
638
|
+
skipped } }`, each root as `{ file, line, tag, block, form | reason + message }`. No class,
|
|
639
|
+
className or existing style token changes (`--verify-styles` stays clean), and a second run writes
|
|
640
|
+
nothing and counts every root it marked as `alreadyMarked`. `--strict` exits 2 while any root is
|
|
641
|
+
skipped.
|
|
642
|
+
|
|
604
643
|
## Forms
|
|
605
644
|
|
|
606
645
|
The third mode, and the shortest. The release-time derive lane reads every `<form>` in the built
|
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
|
});
|
|
@@ -9178,6 +9178,371 @@ function readComponentizePlan(value) {
|
|
|
9178
9178
|
return readPlan(value);
|
|
9179
9179
|
}
|
|
9180
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
|
+
|
|
9181
9546
|
// src/index.ts
|
|
9182
9547
|
var UNPARSEABLE_DIALECT = {
|
|
9183
9548
|
roots: [],
|
|
@@ -9515,7 +9880,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
9515
9880
|
rewritten++;
|
|
9516
9881
|
}
|
|
9517
9882
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
9518
|
-
const source = new
|
|
9883
|
+
const source = new MagicString7(contents.get(file));
|
|
9519
9884
|
const seen = /* @__PURE__ */ new Set();
|
|
9520
9885
|
for (const splice of splices) {
|
|
9521
9886
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -10340,7 +10705,7 @@ function styleEdits(before, after) {
|
|
|
10340
10705
|
// package.json
|
|
10341
10706
|
var package_default = {
|
|
10342
10707
|
name: "@bettercms-ai/convert",
|
|
10343
|
-
version: "0.
|
|
10708
|
+
version: "0.23.0",
|
|
10344
10709
|
type: "module",
|
|
10345
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.",
|
|
10346
10711
|
bin: {
|
|
@@ -10403,6 +10768,7 @@ function parseArgs(argv) {
|
|
|
10403
10768
|
componentize: false,
|
|
10404
10769
|
forms: false,
|
|
10405
10770
|
pins: {},
|
|
10771
|
+
upgradeSections: false,
|
|
10406
10772
|
xray: void 0,
|
|
10407
10773
|
verifyStyles: false,
|
|
10408
10774
|
base: "HEAD",
|
|
@@ -10421,6 +10787,7 @@ function parseArgs(argv) {
|
|
|
10421
10787
|
if (arg === "--brief") args.brief = argv[++i];
|
|
10422
10788
|
else if (arg === "--componentize") args.componentize = true;
|
|
10423
10789
|
else if (arg === "--forms") args.forms = true;
|
|
10790
|
+
else if (arg === "--upgrade-sections") args.upgradeSections = true;
|
|
10424
10791
|
else if (arg === "--pin") {
|
|
10425
10792
|
const raw = argv[++i] ?? "";
|
|
10426
10793
|
const at = raw.indexOf("=");
|
|
@@ -10460,6 +10827,7 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
10460
10827
|
bettercms-convert --brief <json> --root . [options]
|
|
10461
10828
|
bettercms-convert --componentize --brief <json> --plan <json> --root . [options]
|
|
10462
10829
|
bettercms-convert --forms --brief <json> --root . [options]
|
|
10830
|
+
bettercms-convert --upgrade-sections --root . [--dry-run]
|
|
10463
10831
|
bettercms-convert --xray <dist-dir>
|
|
10464
10832
|
bettercms-convert --verify-styles [--base <ref>] --root .
|
|
10465
10833
|
|
|
@@ -10472,6 +10840,14 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
10472
10840
|
--pin <form>=<file> With --forms: the file a form's <form> is in, when the run reports
|
|
10473
10841
|
FORM_AMBIGUOUS (two files post exactly its fields). Form by id or
|
|
10474
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.
|
|
10475
10851
|
--xray <dist-dir> Measure a LOCAL build instead of converting: how much of each page's
|
|
10476
10852
|
visible copy no binding owns, and the biggest buckets of it. Run it
|
|
10477
10853
|
after a build to see what the next conversion still has to reach,
|
|
@@ -10603,6 +10979,28 @@ ${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-
|
|
|
10603
10979
|
);
|
|
10604
10980
|
}
|
|
10605
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
|
+
}
|
|
10606
11004
|
async function xray(dir, strict) {
|
|
10607
11005
|
const pages = [];
|
|
10608
11006
|
const walk = async (at) => {
|
|
@@ -10676,6 +11074,31 @@ async function main() {
|
|
|
10676
11074
|
return await xray(resolve(args.xray), args.strict);
|
|
10677
11075
|
}
|
|
10678
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
|
+
}
|
|
10679
11102
|
if (!args.brief) throw new CliError("--brief <file> is required. See --help.");
|
|
10680
11103
|
if (args.plan && !args.componentize) {
|
|
10681
11104
|
throw new CliError("--plan is only read with --componentize. See --help.");
|