@bettercms-ai/convert 0.17.2 → 0.18.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 +8 -0
- package/dist/cli.js +176 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +89 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6cb71a0: - **Images passed to a section component are bound.** `<Hero src={heroImage} />` rendering `<Image src={src}>` used to be `IMAGE_ASSET_UNRESOLVED`. The call site now reads `bcmsImage(...)` and passes `bcmsBindings`, and the component's `<Image>` gets the binding. No class or markup changes.
|
|
8
|
+
- **`bcmsImage` keeps an imported asset at build** (helper v8). `astro:assets` refuses a remote url without width and height, so returning the CMS url broke the build once an editor replaced an image. The published page shows the editor's image through the release write.
|
|
9
|
+
- **New `--verify-styles [--base <ref>]`.** It lists every `class`, `className` and `style` token changed since the base commit, repo-wide so a `--componentize` move is not an edit, and exits 1 when there are any. Put its `styleEdits` in the conversion receipt; a receipt with edits is refused.
|
|
10
|
+
|
|
3
11
|
## 0.17.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
4
5
|
import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
5
6
|
import { dirname, join as join2, relative, resolve, sep } from "path";
|
|
6
7
|
|
|
@@ -3187,6 +3188,7 @@ function soleChild(node) {
|
|
|
3187
3188
|
const kept = node.children.filter((c) => !(c.type === "text" && c.value.trim() === ""));
|
|
3188
3189
|
return kept.length === 1 ? kept[0] : null;
|
|
3189
3190
|
}
|
|
3191
|
+
var ASSET_TAGS = /* @__PURE__ */ new Set(["Image", "Picture"]);
|
|
3190
3192
|
function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
3191
3193
|
const empty = {
|
|
3192
3194
|
targets: [],
|
|
@@ -3203,6 +3205,7 @@ function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
|
3203
3205
|
const reception = dialect === "astro" ? astroReception(ast, propName, offset, content) : receptionIn(component, propName, offset, content);
|
|
3204
3206
|
if (!reception.receivable) return empty;
|
|
3205
3207
|
const names = new Set(reception.names.size > 0 ? reception.names : [propName]);
|
|
3208
|
+
const assetComponents = /from\s+["']astro:assets["']/.test(content);
|
|
3206
3209
|
const within = (node) => component === null || node.start >= (component.start ?? 0) + offset && node.start <= (component.end ?? 0) + offset;
|
|
3207
3210
|
const targets = [];
|
|
3208
3211
|
const forwards = [];
|
|
@@ -3212,6 +3215,15 @@ function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
|
3212
3215
|
return;
|
|
3213
3216
|
}
|
|
3214
3217
|
if (node.type !== "element") return;
|
|
3218
|
+
const sources = IMAGE_SOURCES[node.tag.toLowerCase()] ?? (assetComponents && ASSET_TAGS.has(node.tag) ? ["src"] : []);
|
|
3219
|
+
const readsImage = node.attrs.some((attr) => {
|
|
3220
|
+
if (!sources.includes(attr.name) || !attr.expressionRange) return false;
|
|
3221
|
+
return readsProp(content.slice(attr.expressionRange.start, attr.expressionRange.end), propName, names);
|
|
3222
|
+
});
|
|
3223
|
+
if (readsImage) {
|
|
3224
|
+
if (within(node)) targets.push({ attrInsertAt: node.attrInsertAt, inner: null, tag: node.tag });
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3215
3227
|
if (isIntrinsic(node.tag)) {
|
|
3216
3228
|
const child = soleChild(node);
|
|
3217
3229
|
if (within(node) && child?.type === "expr" && readsProp(child.value, propName, names)) {
|
|
@@ -4069,6 +4081,27 @@ function baseName(url) {
|
|
|
4069
4081
|
const base = file.split(".")[0] ?? "";
|
|
4070
4082
|
return base === "" ? null : base.toLowerCase();
|
|
4071
4083
|
}
|
|
4084
|
+
function passedTo(content, parsed, binding) {
|
|
4085
|
+
const out = [];
|
|
4086
|
+
const visit = (node) => {
|
|
4087
|
+
if (node.type === "expr") {
|
|
4088
|
+
for (const child of node.children) visit(child);
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
if (node.type !== "element" || node.raw) return;
|
|
4092
|
+
const isComponent = /^[A-Z]/.test(node.tag) && !ASSET_COMPONENTS.has(node.tag);
|
|
4093
|
+
if (isComponent && !node.attrs.some((a) => a.name === "bcmsBindings")) {
|
|
4094
|
+
for (const attr of node.attrs) {
|
|
4095
|
+
if (!attr.expressionRange) continue;
|
|
4096
|
+
const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
|
|
4097
|
+
if (inner === binding) out.push({ node, prop: attr.name, attr });
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
for (const child of node.children) visit(child);
|
|
4101
|
+
};
|
|
4102
|
+
for (const root of parsed.roots) visit(root);
|
|
4103
|
+
return out;
|
|
4104
|
+
}
|
|
4072
4105
|
function assetCandidates(parsed, dialect, original) {
|
|
4073
4106
|
const wanted = baseName(original);
|
|
4074
4107
|
if (wanted === null || parsed.error) return [];
|
|
@@ -4106,8 +4139,8 @@ function renderedBy(content, parsed, dialect, binding) {
|
|
|
4106
4139
|
for (const root of parsed.roots) visit(root);
|
|
4107
4140
|
return out;
|
|
4108
4141
|
}
|
|
4109
|
-
function bindImages(ctx, targets) {
|
|
4110
|
-
const out = { splices: [], slugs: [], bound: [], refused: [] };
|
|
4142
|
+
async function bindImages(ctx, targets) {
|
|
4143
|
+
const out = { splices: [], slugs: [], bound: [], refused: [], componentEdits: [], props: [] };
|
|
4111
4144
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
4112
4145
|
if (!rule.attr || !rule.text || ctx.parsed.error) return out;
|
|
4113
4146
|
const claimed = /* @__PURE__ */ new Set();
|
|
@@ -4115,6 +4148,23 @@ function bindImages(ctx, targets) {
|
|
|
4115
4148
|
const binding = assetImport(ctx.parsed, ctx.dialect, target.original);
|
|
4116
4149
|
const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
|
|
4117
4150
|
const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
|
|
4151
|
+
const args = (b) => [ctx.snapshotName(target.slug), JSON.stringify(target.path), b, ...ctx.slug ? [ctx.slug] : []];
|
|
4152
|
+
if (free.length === 0 && binding !== null && ctx.drill) {
|
|
4153
|
+
const calls = passedTo(ctx.content, ctx.parsed, binding).filter((c) => !claimed.has(c.node.attrInsertAt));
|
|
4154
|
+
const call = calls.length === 1 ? calls[0] : null;
|
|
4155
|
+
const drilled = call ? await ctx.drill(call.node.tag, call.prop, target.kind || "image") : null;
|
|
4156
|
+
if (call && drilled?.ok) {
|
|
4157
|
+
claimed.add(call.node.attrInsertAt);
|
|
4158
|
+
const literal = `${call.prop}: ${JSON.stringify(target.path)}`;
|
|
4159
|
+
out.splices.push({ start: call.attr.range.start, end: call.attr.range.end, text: rule.attr(call.prop, `${ctx.image}(${args(binding).join(", ")})`) });
|
|
4160
|
+
out.splices.push({ start: call.node.attrInsertAt, end: call.node.attrInsertAt, text: ` bcmsBindings={{ ${literal} }}` });
|
|
4161
|
+
out.componentEdits.push(...drilled.edits);
|
|
4162
|
+
out.props.push({ file: drilled.edits[0].file, prop: call.prop, path: target.path, literal });
|
|
4163
|
+
out.slugs.push(target.slug);
|
|
4164
|
+
out.bound.push(target.key);
|
|
4165
|
+
continue;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4118
4168
|
if (free.length !== 1) {
|
|
4119
4169
|
out.refused.push({ key: target.key, reason: "IMAGE_ASSET_UNRESOLVED" });
|
|
4120
4170
|
continue;
|
|
@@ -4122,16 +4172,10 @@ function bindImages(ctx, targets) {
|
|
|
4122
4172
|
const hit = free[0];
|
|
4123
4173
|
claimed.add(hit.node.attrInsertAt);
|
|
4124
4174
|
const attr = hit.node.attrs.find((a) => a.name === hit.attr);
|
|
4125
|
-
const args = [
|
|
4126
|
-
ctx.snapshotName(target.slug),
|
|
4127
|
-
JSON.stringify(target.path),
|
|
4128
|
-
binding,
|
|
4129
|
-
...ctx.slug ? [ctx.slug] : []
|
|
4130
|
-
];
|
|
4131
4175
|
out.splices.push({
|
|
4132
4176
|
start: attr.range.start,
|
|
4133
4177
|
end: attr.range.end,
|
|
4134
|
-
text: rule.attr(hit.attr, `${ctx.image}(${args.join(", ")})`)
|
|
4178
|
+
text: rule.attr(hit.attr, `${ctx.image}(${args(binding).join(", ")})`)
|
|
4135
4179
|
});
|
|
4136
4180
|
out.splices.push({
|
|
4137
4181
|
start: hit.node.attrInsertAt,
|
|
@@ -4146,7 +4190,7 @@ function bindImages(ctx, targets) {
|
|
|
4146
4190
|
|
|
4147
4191
|
// src/helper.ts
|
|
4148
4192
|
import { createHash as createHash2 } from "crypto";
|
|
4149
|
-
var HELPER_VERSION =
|
|
4193
|
+
var HELPER_VERSION = 8;
|
|
4150
4194
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4151
4195
|
var HELPER_PATH = Object.fromEntries(
|
|
4152
4196
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4278,8 +4322,18 @@ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][
|
|
|
4278
4322
|
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4279
4323
|
}
|
|
4280
4324
|
|
|
4281
|
-
/**
|
|
4325
|
+
/**
|
|
4326
|
+
* An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED
|
|
4327
|
+
*
|
|
4328
|
+
* \u{1F534} AN IMPORTED ASSET IS KEPT AT BUILD. \`<Image src={\u2026}>\` from \`astro:assets\` refuses a remote
|
|
4329
|
+
* url string without width, height and an allowed domain, so returning the CMS url here broke the
|
|
4330
|
+
* build the moment an editor replaced the image. The build renders the optimised import; the
|
|
4331
|
+
* release write (\`writeImage\`) puts the editor's image on the \`data-bcms-field\` element, which
|
|
4332
|
+
* is where every published value lands. A plain string fallback (\`<img src="/hero.png">\`) has
|
|
4333
|
+
* no such rule and takes the CMS url directly.
|
|
4334
|
+
*/
|
|
4282
4335
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
4336
|
+
if (fallback !== null && typeof fallback === "object") return fallback;
|
|
4283
4337
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
4284
4338
|
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4285
4339
|
return render(readPath(root, path)) ?? fallback;
|
|
@@ -4445,7 +4499,8 @@ var HISTORICAL = {
|
|
|
4445
4499
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4446
4500
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4447
4501
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4448
|
-
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4502
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4503
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4449
4504
|
},
|
|
4450
4505
|
"src/lib/bcms-content.ts": {
|
|
4451
4506
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
@@ -4453,7 +4508,8 @@ var HISTORICAL = {
|
|
|
4453
4508
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
4454
4509
|
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4455
4510
|
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
|
|
4456
|
-
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
|
|
4511
|
+
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5",
|
|
4512
|
+
7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7"
|
|
4457
4513
|
},
|
|
4458
4514
|
"composables/bcms-content.ts": {
|
|
4459
4515
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
@@ -4461,7 +4517,8 @@ var HISTORICAL = {
|
|
|
4461
4517
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4462
4518
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4463
4519
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4464
|
-
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4520
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4521
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4465
4522
|
}
|
|
4466
4523
|
};
|
|
4467
4524
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -9564,8 +9621,21 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
9564
9621
|
callSites: [{ file, literal: drilledProp.literal }]
|
|
9565
9622
|
});
|
|
9566
9623
|
}
|
|
9567
|
-
const images = bindImages(
|
|
9568
|
-
{
|
|
9624
|
+
const images = await bindImages(
|
|
9625
|
+
{
|
|
9626
|
+
content,
|
|
9627
|
+
parsed,
|
|
9628
|
+
dialect: writing,
|
|
9629
|
+
image: helperName("bcmsImage"),
|
|
9630
|
+
snapshotName,
|
|
9631
|
+
slug: params?.slug ?? null,
|
|
9632
|
+
// An image handed to a section component is bound in that component, by the same drill a
|
|
9633
|
+
// text prop takes. @see images.ts
|
|
9634
|
+
drill: async (component, prop, kind) => {
|
|
9635
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
9636
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
9637
|
+
}
|
|
9638
|
+
},
|
|
9569
9639
|
targets.filter((t) => {
|
|
9570
9640
|
const outcome = outcomes.get(t.key);
|
|
9571
9641
|
return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
@@ -9573,6 +9643,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
9573
9643
|
);
|
|
9574
9644
|
for (const key of images.bound) outcomes.set(key, "rewritten");
|
|
9575
9645
|
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
9646
|
+
componentEdits.push(...images.componentEdits);
|
|
9647
|
+
for (const drilledProp of images.props) {
|
|
9648
|
+
dynamic.push({ file: drilledProp.file, prop: drilledProp.prop, path: drilledProp.path, callSites: [{ file, literal: drilledProp.literal }] });
|
|
9649
|
+
}
|
|
9576
9650
|
const lanes = [...data.splices, ...images.splices];
|
|
9577
9651
|
if (applied.length === 0 && lanes.length === 0) {
|
|
9578
9652
|
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|
|
@@ -9804,10 +9878,58 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9804
9878
|
}
|
|
9805
9879
|
}
|
|
9806
9880
|
|
|
9881
|
+
// src/styles.ts
|
|
9882
|
+
var ATTR = /\b(class|className|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*`([^`$]*)`\s*\})/g;
|
|
9883
|
+
function styleTokens(file, content) {
|
|
9884
|
+
const out = [];
|
|
9885
|
+
for (const match of content.matchAll(ATTR)) {
|
|
9886
|
+
const attr = match[1] === "className" ? "class" : match[1];
|
|
9887
|
+
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
9888
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
9889
|
+
const tokens = attr === "style" ? value.split(";").map((d) => d.replace(/\s+/g, "").toLowerCase()).filter(Boolean) : value.split(/\s+/).filter(Boolean);
|
|
9890
|
+
for (const token of tokens) out.push({ attr, token, file, line });
|
|
9891
|
+
}
|
|
9892
|
+
return out;
|
|
9893
|
+
}
|
|
9894
|
+
function styleEdits(before, after) {
|
|
9895
|
+
const count = (files) => {
|
|
9896
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9897
|
+
for (const f of files) {
|
|
9898
|
+
for (const o of styleTokens(f.path, f.content)) {
|
|
9899
|
+
const key = `${o.attr}\0${o.token}`;
|
|
9900
|
+
seen.set(key, [...seen.get(key) ?? [], o]);
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
return seen;
|
|
9904
|
+
};
|
|
9905
|
+
const perFile = (list) => {
|
|
9906
|
+
const files = /* @__PURE__ */ new Map();
|
|
9907
|
+
for (const o of list) files.set(o.file, [...files.get(o.file) ?? [], o]);
|
|
9908
|
+
return files;
|
|
9909
|
+
};
|
|
9910
|
+
const pick = (from, other, n) => {
|
|
9911
|
+
const mine = perFile(from);
|
|
9912
|
+
const theirs = perFile(other);
|
|
9913
|
+
const out = [];
|
|
9914
|
+
for (const [file, list] of mine) out.push(...list.slice(theirs.get(file)?.length ?? 0));
|
|
9915
|
+
return out.slice(0, n);
|
|
9916
|
+
};
|
|
9917
|
+
const was = count(before);
|
|
9918
|
+
const now = count(after);
|
|
9919
|
+
const edits = [];
|
|
9920
|
+
for (const key of /* @__PURE__ */ new Set([...was.keys(), ...now.keys()])) {
|
|
9921
|
+
const a = was.get(key) ?? [];
|
|
9922
|
+
const b = now.get(key) ?? [];
|
|
9923
|
+
if (a.length > b.length) for (const o of pick(a, b, a.length - b.length)) edits.push({ change: "removed", ...o });
|
|
9924
|
+
else if (b.length > a.length) for (const o of pick(b, a, b.length - a.length)) edits.push({ change: "added", ...o });
|
|
9925
|
+
}
|
|
9926
|
+
return edits.sort((x, y) => x.file.localeCompare(y.file) || x.line - y.line || x.token.localeCompare(y.token));
|
|
9927
|
+
}
|
|
9928
|
+
|
|
9807
9929
|
// package.json
|
|
9808
9930
|
var package_default = {
|
|
9809
9931
|
name: "@bettercms-ai/convert",
|
|
9810
|
-
version: "0.
|
|
9932
|
+
version: "0.18.0",
|
|
9811
9933
|
type: "module",
|
|
9812
9934
|
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.",
|
|
9813
9935
|
bin: {
|
|
@@ -9870,6 +9992,8 @@ function parseArgs(argv) {
|
|
|
9870
9992
|
forms: false,
|
|
9871
9993
|
pins: {},
|
|
9872
9994
|
xray: void 0,
|
|
9995
|
+
verifyStyles: false,
|
|
9996
|
+
base: "HEAD",
|
|
9873
9997
|
plan: void 0,
|
|
9874
9998
|
copy: "bind",
|
|
9875
9999
|
root: ".",
|
|
@@ -9895,6 +10019,8 @@ function parseArgs(argv) {
|
|
|
9895
10019
|
if (Object.hasOwn(args.pins, form)) throw new CliError(`--pin ${form} twice: give one file. See --help.`);
|
|
9896
10020
|
args.pins[form] = raw.slice(at + 1);
|
|
9897
10021
|
} else if (arg === "--xray") args.xray = argv[++i];
|
|
10022
|
+
else if (arg === "--verify-styles") args.verifyStyles = true;
|
|
10023
|
+
else if (arg === "--base") args.base = argv[++i] ?? "HEAD";
|
|
9898
10024
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
9899
10025
|
else if (arg === "--copy") {
|
|
9900
10026
|
const mode = argv[++i];
|
|
@@ -9923,6 +10049,7 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
9923
10049
|
bettercms-convert --componentize --brief <json> --plan <json> --root . [options]
|
|
9924
10050
|
bettercms-convert --forms --brief <json> --root . [options]
|
|
9925
10051
|
bettercms-convert --xray <dist-dir>
|
|
10052
|
+
bettercms-convert --verify-styles [--base <ref>] --root .
|
|
9926
10053
|
|
|
9927
10054
|
--brief <file> The conversion brief, as get_conversion_brief returns it. Required.
|
|
9928
10055
|
--componentize Lift each planned SECTION into a component the CMS can reorder.
|
|
@@ -9938,6 +10065,12 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
9938
10065
|
after a build to see what the next conversion still has to reach,
|
|
9939
10066
|
without deploying. Takes no --brief. Exit 2 with --strict when any
|
|
9940
10067
|
page is above 2 %.
|
|
10068
|
+
--verify-styles Check that no class, className or style was changed by hand: every
|
|
10069
|
+
token in the repository is compared with --base (default HEAD).
|
|
10070
|
+
Conversion maps content into the CMS and never edits the site's
|
|
10071
|
+
design; exit 1 lists each removed or added token with file:line.
|
|
10072
|
+
Include the result in the conversion receipt as styleEdits.
|
|
10073
|
+
--base <ref> With --verify-styles: the commit before the conversion.
|
|
9941
10074
|
--plan <file> The componentize plan, as get_componentize_plan returns it.
|
|
9942
10075
|
Required with --componentize.
|
|
9943
10076
|
--copy bind|instance Which copy model the plan is expected to describe. Default: bind
|
|
@@ -10095,6 +10228,31 @@ ${pages.length - over} of ${pages.length} pages at or under 2% unaddressable
|
|
|
10095
10228
|
`);
|
|
10096
10229
|
return strict && over > 0 ? 2 : 0;
|
|
10097
10230
|
}
|
|
10231
|
+
function treeAt(root, ref) {
|
|
10232
|
+
const git = (...argv) => execFileSync("git", ["-C", root, ...argv], { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
|
|
10233
|
+
let listed;
|
|
10234
|
+
try {
|
|
10235
|
+
listed = git("ls-tree", "-r", "--name-only", ref);
|
|
10236
|
+
} catch {
|
|
10237
|
+
throw new CliError(`--base ${ref} is not a commit in ${root}.`);
|
|
10238
|
+
}
|
|
10239
|
+
return listed.split("\n").filter((path) => path && isSourceCandidate(path)).map((path) => ({ path, content: git("show", `${ref}:${path}`) }));
|
|
10240
|
+
}
|
|
10241
|
+
async function verifyStyles(root, base, verbose) {
|
|
10242
|
+
const edits = styleEdits(treeAt(root, base), await readTree(root, verbose));
|
|
10243
|
+
process.stdout.write(`${JSON.stringify({ base, styleEdits: edits }, null, 2)}
|
|
10244
|
+
`);
|
|
10245
|
+
if (edits.length === 0) {
|
|
10246
|
+
process.stderr.write(`styles: unchanged since ${base}.
|
|
10247
|
+
`);
|
|
10248
|
+
return 0;
|
|
10249
|
+
}
|
|
10250
|
+
process.stderr.write(
|
|
10251
|
+
`styles: ${edits.length} class/style token(s) changed since ${base}. Conversion must not edit the site's design \u2014 restore these, the CMS keeps inline markup as written:
|
|
10252
|
+
` + edits.map((e) => ` ${e.file}:${e.line} ${e.change === "removed" ? "-" : "+"} ${e.attr} ${e.token}`).join("\n") + "\n"
|
|
10253
|
+
);
|
|
10254
|
+
return 1;
|
|
10255
|
+
}
|
|
10098
10256
|
async function main() {
|
|
10099
10257
|
const args = parseArgs(process.argv.slice(2));
|
|
10100
10258
|
if (args.help) {
|
|
@@ -10105,6 +10263,7 @@ async function main() {
|
|
|
10105
10263
|
if (args.brief) throw new CliError("--xray measures a build; it reads no brief. See --help.");
|
|
10106
10264
|
return await xray(resolve(args.xray), args.strict);
|
|
10107
10265
|
}
|
|
10266
|
+
if (args.verifyStyles) return await verifyStyles(resolve(args.root), args.base, args.verbose);
|
|
10108
10267
|
if (!args.brief) throw new CliError("--brief <file> is required. See --help.");
|
|
10109
10268
|
if (args.plan && !args.componentize) {
|
|
10110
10269
|
throw new CliError("--plan is only read with --componentize. See --help.");
|