@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/dist/index.d.ts
CHANGED
|
@@ -951,7 +951,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
|
|
|
951
951
|
declare function propsAttribute(props: AttrBinding[]): string;
|
|
952
952
|
|
|
953
953
|
/** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
|
|
954
|
-
declare const HELPER_VERSION =
|
|
954
|
+
declare const HELPER_VERSION = 8;
|
|
955
955
|
/** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
|
|
956
956
|
declare const HELPER_PATH: Record<Dialect, string | null>;
|
|
957
957
|
/** The directory the snapshots live in, at the repository root. */
|
|
@@ -1196,6 +1196,14 @@ interface ImageContext {
|
|
|
1196
1196
|
snapshotName: (slug: string) => string;
|
|
1197
1197
|
/** How this file reads its route parameter, on a dynamic route. Null off one. */
|
|
1198
1198
|
slug: string | null;
|
|
1199
|
+
/**
|
|
1200
|
+
* Follow a prop into the component that renders it. @see drillProp
|
|
1201
|
+
*
|
|
1202
|
+
* Most Astro pages import an image and hand it to a section component (`<Hero src={hero} />`),
|
|
1203
|
+
* which renders the `<Image>`. Without this, every such image was `IMAGE_ASSET_UNRESOLVED` and a
|
|
1204
|
+
* converted site had no editable images at all.
|
|
1205
|
+
*/
|
|
1206
|
+
drill?: (component: string, prop: string, kind: string) => Promise<DrillOutcome>;
|
|
1199
1207
|
}
|
|
1200
1208
|
interface ImageResult {
|
|
1201
1209
|
splices: ImageSplice[];
|
|
@@ -1205,6 +1213,18 @@ interface ImageResult {
|
|
|
1205
1213
|
key: string;
|
|
1206
1214
|
reason: "IMAGE_ASSET_UNRESOLVED";
|
|
1207
1215
|
}[];
|
|
1216
|
+
/** Edits to the components an image prop was drilled into. */
|
|
1217
|
+
componentEdits: {
|
|
1218
|
+
file: string;
|
|
1219
|
+
splices: ImageSplice[];
|
|
1220
|
+
}[];
|
|
1221
|
+
/** Each drilled image: the component file, the prop, the path, and the call-site literal. */
|
|
1222
|
+
props: {
|
|
1223
|
+
file: string;
|
|
1224
|
+
prop: string;
|
|
1225
|
+
path: string;
|
|
1226
|
+
literal: string;
|
|
1227
|
+
}[];
|
|
1208
1228
|
}
|
|
1209
1229
|
/**
|
|
1210
1230
|
* Which local binding this built url came from, or null when nothing here says.
|
|
@@ -1231,7 +1251,7 @@ declare function assetImport(parsed: ParsedFile, dialect: Dialect, original: str
|
|
|
1231
1251
|
* one, the import the bundler already optimised when there is not — so a build against the
|
|
1232
1252
|
* committed `{}` stub renders exactly the image the repository renders today.
|
|
1233
1253
|
*/
|
|
1234
|
-
declare function bindImages(ctx: ImageContext, targets: ImageTarget[]): ImageResult
|
|
1254
|
+
declare function bindImages(ctx: ImageContext, targets: ImageTarget[]): Promise<ImageResult>;
|
|
1235
1255
|
|
|
1236
1256
|
/** The recipe an Astro site needs instead — never auto-migrate a site to SSR. */
|
|
1237
1257
|
declare const SSR_DRAFT_RECIPE: string;
|
package/dist/index.js
CHANGED
|
@@ -3185,6 +3185,7 @@ function soleChild(node) {
|
|
|
3185
3185
|
const kept = node.children.filter((c) => !(c.type === "text" && c.value.trim() === ""));
|
|
3186
3186
|
return kept.length === 1 ? kept[0] : null;
|
|
3187
3187
|
}
|
|
3188
|
+
var ASSET_TAGS = /* @__PURE__ */ new Set(["Image", "Picture"]);
|
|
3188
3189
|
function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
3189
3190
|
const empty = {
|
|
3190
3191
|
targets: [],
|
|
@@ -3201,6 +3202,7 @@ function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
|
3201
3202
|
const reception = dialect === "astro" ? astroReception(ast, propName, offset, content) : receptionIn(component, propName, offset, content);
|
|
3202
3203
|
if (!reception.receivable) return empty;
|
|
3203
3204
|
const names = new Set(reception.names.size > 0 ? reception.names : [propName]);
|
|
3205
|
+
const assetComponents = /from\s+["']astro:assets["']/.test(content);
|
|
3204
3206
|
const within = (node) => component === null || node.start >= (component.start ?? 0) + offset && node.start <= (component.end ?? 0) + offset;
|
|
3205
3207
|
const targets = [];
|
|
3206
3208
|
const forwards = [];
|
|
@@ -3210,6 +3212,15 @@ function findPropTargets(content, parsed, propName, dialect, imported = null) {
|
|
|
3210
3212
|
return;
|
|
3211
3213
|
}
|
|
3212
3214
|
if (node.type !== "element") return;
|
|
3215
|
+
const sources = IMAGE_SOURCES[node.tag.toLowerCase()] ?? (assetComponents && ASSET_TAGS.has(node.tag) ? ["src"] : []);
|
|
3216
|
+
const readsImage = node.attrs.some((attr) => {
|
|
3217
|
+
if (!sources.includes(attr.name) || !attr.expressionRange) return false;
|
|
3218
|
+
return readsProp(content.slice(attr.expressionRange.start, attr.expressionRange.end), propName, names);
|
|
3219
|
+
});
|
|
3220
|
+
if (readsImage) {
|
|
3221
|
+
if (within(node)) targets.push({ attrInsertAt: node.attrInsertAt, inner: null, tag: node.tag });
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3213
3224
|
if (isIntrinsic(node.tag)) {
|
|
3214
3225
|
const child = soleChild(node);
|
|
3215
3226
|
if (within(node) && child?.type === "expr" && readsProp(child.value, propName, names)) {
|
|
@@ -4067,6 +4078,27 @@ function baseName(url) {
|
|
|
4067
4078
|
const base = file.split(".")[0] ?? "";
|
|
4068
4079
|
return base === "" ? null : base.toLowerCase();
|
|
4069
4080
|
}
|
|
4081
|
+
function passedTo(content, parsed, binding) {
|
|
4082
|
+
const out = [];
|
|
4083
|
+
const visit = (node) => {
|
|
4084
|
+
if (node.type === "expr") {
|
|
4085
|
+
for (const child of node.children) visit(child);
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
if (node.type !== "element" || node.raw) return;
|
|
4089
|
+
const isComponent = /^[A-Z]/.test(node.tag) && !ASSET_COMPONENTS.has(node.tag);
|
|
4090
|
+
if (isComponent && !node.attrs.some((a) => a.name === "bcmsBindings")) {
|
|
4091
|
+
for (const attr of node.attrs) {
|
|
4092
|
+
if (!attr.expressionRange) continue;
|
|
4093
|
+
const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
|
|
4094
|
+
if (inner === binding) out.push({ node, prop: attr.name, attr });
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
for (const child of node.children) visit(child);
|
|
4098
|
+
};
|
|
4099
|
+
for (const root of parsed.roots) visit(root);
|
|
4100
|
+
return out;
|
|
4101
|
+
}
|
|
4070
4102
|
function assetCandidates(parsed, dialect, original) {
|
|
4071
4103
|
const wanted = baseName(original);
|
|
4072
4104
|
if (wanted === null || parsed.error) return [];
|
|
@@ -4104,8 +4136,8 @@ function renderedBy(content, parsed, dialect, binding) {
|
|
|
4104
4136
|
for (const root of parsed.roots) visit(root);
|
|
4105
4137
|
return out;
|
|
4106
4138
|
}
|
|
4107
|
-
function bindImages(ctx, targets) {
|
|
4108
|
-
const out = { splices: [], slugs: [], bound: [], refused: [] };
|
|
4139
|
+
async function bindImages(ctx, targets) {
|
|
4140
|
+
const out = { splices: [], slugs: [], bound: [], refused: [], componentEdits: [], props: [] };
|
|
4109
4141
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
4110
4142
|
if (!rule.attr || !rule.text || ctx.parsed.error) return out;
|
|
4111
4143
|
const claimed = /* @__PURE__ */ new Set();
|
|
@@ -4113,6 +4145,23 @@ function bindImages(ctx, targets) {
|
|
|
4113
4145
|
const binding = assetImport(ctx.parsed, ctx.dialect, target.original);
|
|
4114
4146
|
const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
|
|
4115
4147
|
const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
|
|
4148
|
+
const args = (b) => [ctx.snapshotName(target.slug), JSON.stringify(target.path), b, ...ctx.slug ? [ctx.slug] : []];
|
|
4149
|
+
if (free.length === 0 && binding !== null && ctx.drill) {
|
|
4150
|
+
const calls = passedTo(ctx.content, ctx.parsed, binding).filter((c) => !claimed.has(c.node.attrInsertAt));
|
|
4151
|
+
const call = calls.length === 1 ? calls[0] : null;
|
|
4152
|
+
const drilled = call ? await ctx.drill(call.node.tag, call.prop, target.kind || "image") : null;
|
|
4153
|
+
if (call && drilled?.ok) {
|
|
4154
|
+
claimed.add(call.node.attrInsertAt);
|
|
4155
|
+
const literal = `${call.prop}: ${JSON.stringify(target.path)}`;
|
|
4156
|
+
out.splices.push({ start: call.attr.range.start, end: call.attr.range.end, text: rule.attr(call.prop, `${ctx.image}(${args(binding).join(", ")})`) });
|
|
4157
|
+
out.splices.push({ start: call.node.attrInsertAt, end: call.node.attrInsertAt, text: ` bcmsBindings={{ ${literal} }}` });
|
|
4158
|
+
out.componentEdits.push(...drilled.edits);
|
|
4159
|
+
out.props.push({ file: drilled.edits[0].file, prop: call.prop, path: target.path, literal });
|
|
4160
|
+
out.slugs.push(target.slug);
|
|
4161
|
+
out.bound.push(target.key);
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4116
4165
|
if (free.length !== 1) {
|
|
4117
4166
|
out.refused.push({ key: target.key, reason: "IMAGE_ASSET_UNRESOLVED" });
|
|
4118
4167
|
continue;
|
|
@@ -4120,16 +4169,10 @@ function bindImages(ctx, targets) {
|
|
|
4120
4169
|
const hit = free[0];
|
|
4121
4170
|
claimed.add(hit.node.attrInsertAt);
|
|
4122
4171
|
const attr = hit.node.attrs.find((a) => a.name === hit.attr);
|
|
4123
|
-
const args = [
|
|
4124
|
-
ctx.snapshotName(target.slug),
|
|
4125
|
-
JSON.stringify(target.path),
|
|
4126
|
-
binding,
|
|
4127
|
-
...ctx.slug ? [ctx.slug] : []
|
|
4128
|
-
];
|
|
4129
4172
|
out.splices.push({
|
|
4130
4173
|
start: attr.range.start,
|
|
4131
4174
|
end: attr.range.end,
|
|
4132
|
-
text: rule.attr(hit.attr, `${ctx.image}(${args.join(", ")})`)
|
|
4175
|
+
text: rule.attr(hit.attr, `${ctx.image}(${args(binding).join(", ")})`)
|
|
4133
4176
|
});
|
|
4134
4177
|
out.splices.push({
|
|
4135
4178
|
start: hit.node.attrInsertAt,
|
|
@@ -4144,7 +4187,7 @@ function bindImages(ctx, targets) {
|
|
|
4144
4187
|
|
|
4145
4188
|
// src/helper.ts
|
|
4146
4189
|
import { createHash as createHash2 } from "crypto";
|
|
4147
|
-
var HELPER_VERSION =
|
|
4190
|
+
var HELPER_VERSION = 8;
|
|
4148
4191
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4149
4192
|
var HELPER_PATH = Object.fromEntries(
|
|
4150
4193
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4276,8 +4319,18 @@ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][
|
|
|
4276
4319
|
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4277
4320
|
}
|
|
4278
4321
|
|
|
4279
|
-
/**
|
|
4322
|
+
/**
|
|
4323
|
+
* An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED
|
|
4324
|
+
*
|
|
4325
|
+
* \u{1F534} AN IMPORTED ASSET IS KEPT AT BUILD. \`<Image src={\u2026}>\` from \`astro:assets\` refuses a remote
|
|
4326
|
+
* url string without width, height and an allowed domain, so returning the CMS url here broke the
|
|
4327
|
+
* build the moment an editor replaced the image. The build renders the optimised import; the
|
|
4328
|
+
* release write (\`writeImage\`) puts the editor's image on the \`data-bcms-field\` element, which
|
|
4329
|
+
* is where every published value lands. A plain string fallback (\`<img src="/hero.png">\`) has
|
|
4330
|
+
* no such rule and takes the CMS url directly.
|
|
4331
|
+
*/
|
|
4280
4332
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
4333
|
+
if (fallback !== null && typeof fallback === "object") return fallback;
|
|
4281
4334
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
4282
4335
|
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4283
4336
|
return render(readPath(root, path)) ?? fallback;
|
|
@@ -4443,7 +4496,8 @@ var HISTORICAL = {
|
|
|
4443
4496
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4444
4497
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4445
4498
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4446
|
-
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4499
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4500
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4447
4501
|
},
|
|
4448
4502
|
"src/lib/bcms-content.ts": {
|
|
4449
4503
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
@@ -4451,7 +4505,8 @@ var HISTORICAL = {
|
|
|
4451
4505
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
4452
4506
|
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4453
4507
|
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
|
|
4454
|
-
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
|
|
4508
|
+
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5",
|
|
4509
|
+
7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7"
|
|
4455
4510
|
},
|
|
4456
4511
|
"composables/bcms-content.ts": {
|
|
4457
4512
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
@@ -4459,7 +4514,8 @@ var HISTORICAL = {
|
|
|
4459
4514
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4460
4515
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4461
4516
|
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4462
|
-
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4517
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
|
|
4518
|
+
7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93"
|
|
4463
4519
|
}
|
|
4464
4520
|
};
|
|
4465
4521
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -9582,8 +9638,21 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
9582
9638
|
callSites: [{ file, literal: drilledProp.literal }]
|
|
9583
9639
|
});
|
|
9584
9640
|
}
|
|
9585
|
-
const images = bindImages(
|
|
9586
|
-
{
|
|
9641
|
+
const images = await bindImages(
|
|
9642
|
+
{
|
|
9643
|
+
content,
|
|
9644
|
+
parsed,
|
|
9645
|
+
dialect: writing,
|
|
9646
|
+
image: helperName("bcmsImage"),
|
|
9647
|
+
snapshotName,
|
|
9648
|
+
slug: params?.slug ?? null,
|
|
9649
|
+
// An image handed to a section component is bound in that component, by the same drill a
|
|
9650
|
+
// text prop takes. @see images.ts
|
|
9651
|
+
drill: async (component, prop, kind) => {
|
|
9652
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
9653
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
9654
|
+
}
|
|
9655
|
+
},
|
|
9587
9656
|
targets.filter((t) => {
|
|
9588
9657
|
const outcome = outcomes.get(t.key);
|
|
9589
9658
|
return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
@@ -9591,6 +9660,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
9591
9660
|
);
|
|
9592
9661
|
for (const key of images.bound) outcomes.set(key, "rewritten");
|
|
9593
9662
|
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
9663
|
+
componentEdits.push(...images.componentEdits);
|
|
9664
|
+
for (const drilledProp of images.props) {
|
|
9665
|
+
dynamic.push({ file: drilledProp.file, prop: drilledProp.prop, path: drilledProp.path, callSites: [{ file, literal: drilledProp.literal }] });
|
|
9666
|
+
}
|
|
9594
9667
|
const lanes = [...data.splices, ...images.splices];
|
|
9595
9668
|
if (applied.length === 0 && lanes.length === 0) {
|
|
9596
9669
|
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|