@bettercms-ai/convert 0.25.0 → 0.26.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 +14 -0
- package/dist/cli.js +126 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +124 -60
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 69dfcfa: A server app hosted on BetterCMS shows a publish on its very next request: the converted-site helper (v14) and `readPublishedContent` ask the content file's time on every read instead of once a second. Helper v14 also renders an image an editor replaced on a page the app renders per request: `bcmsImage` returns the local copy BetterCMS puts beside the app, in the shape of an import, where it used to keep the build's picture. A site built with an older helper gets v14 on its next BetterCMS build, with no commit.
|
|
8
|
+
|
|
9
|
+
## 0.25.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- b1e5eb0: Prices and other numbers are now editable: a value written as `49` or `price={49}` in a data object, a `const`, or a component prop is bound like any other copy, and still renders the same text. `--brief` no longer writes a second `data-bcms-field` on an element, and two paths that want the same element, or a rewrite inside text another rewrite replaces, are reported as ambiguous instead of corrupting the file; a second run over converted source changes nothing.
|
|
14
|
+
- Updated dependencies [56335ff]
|
|
15
|
+
- @bettercms-ai/types@1.32.0
|
|
16
|
+
|
|
3
17
|
## 0.25.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/dist/cli.js
CHANGED
|
@@ -2715,8 +2715,9 @@ function collectSites(roots, literals) {
|
|
|
2715
2715
|
}
|
|
2716
2716
|
const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
|
|
2717
2717
|
for (const attr of node.attrs) {
|
|
2718
|
-
|
|
2719
|
-
|
|
2718
|
+
const digits = attr.expression && attr.expressionRange ? attr.number : void 0;
|
|
2719
|
+
if (attr.expression && digits === void 0 || !digits && !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
|
|
2720
|
+
const value = norm(digits ?? attr.value);
|
|
2720
2721
|
for (const literal of wanted) {
|
|
2721
2722
|
if (value !== literal) continue;
|
|
2722
2723
|
matched.add(literal);
|
|
@@ -2724,7 +2725,7 @@ function collectSites(roots, literals) {
|
|
|
2724
2725
|
file: "",
|
|
2725
2726
|
tag: node.tag,
|
|
2726
2727
|
attrInsertAt: node.attrInsertAt,
|
|
2727
|
-
range: attr.valueRange,
|
|
2728
|
+
range: digits === void 0 ? attr.valueRange : attr.expressionRange,
|
|
2728
2729
|
inner: node.inner,
|
|
2729
2730
|
// 🔴 A COMPONENT'S ATTRIBUTE IS A PROP, and a prop is a different conversion: the value
|
|
2730
2731
|
// is rendered in another file, by an element only that file knows. @see props.ts
|
|
@@ -2832,6 +2833,8 @@ function attrsOf2(open, content) {
|
|
|
2832
2833
|
const value = attr.value;
|
|
2833
2834
|
const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
|
|
2834
2835
|
const literal = value?.type === "StringLiteral";
|
|
2836
|
+
const inside = value?.type === "JSXExpressionContainer" ? value.expression : null;
|
|
2837
|
+
const numeric = inside?.type === "NumericLiteral" ? String(inside.extra.raw) : "";
|
|
2835
2838
|
return [
|
|
2836
2839
|
{
|
|
2837
2840
|
name: nameOf(attr.name),
|
|
@@ -2840,7 +2843,8 @@ function attrsOf2(open, content) {
|
|
|
2840
2843
|
valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
|
|
2841
2844
|
expression: !literal,
|
|
2842
2845
|
range,
|
|
2843
|
-
expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
|
|
2846
|
+
expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null,
|
|
2847
|
+
...numeric ? { number: numeric } : {}
|
|
2844
2848
|
}
|
|
2845
2849
|
];
|
|
2846
2850
|
});
|
|
@@ -3316,14 +3320,21 @@ function exportedNames(ast) {
|
|
|
3316
3320
|
});
|
|
3317
3321
|
return out;
|
|
3318
3322
|
}
|
|
3323
|
+
function printed(node) {
|
|
3324
|
+
if (node?.type === "StringLiteral") return [String(node.value)];
|
|
3325
|
+
if (node?.type === "NumericLiteral") return [String(node.value), String(node.extra?.raw ?? "")];
|
|
3326
|
+
const quasis = node?.type === "TemplateLiteral" ? node.quasis ?? [] : [];
|
|
3327
|
+
const expressions = node?.expressions ?? [];
|
|
3328
|
+
return quasis.length === 1 && expressions.length === 0 ? [String(quasis[0].value.cooked ?? "")] : [];
|
|
3329
|
+
}
|
|
3319
3330
|
function propertyFor(row, literal) {
|
|
3320
3331
|
if (row?.type !== "ObjectExpression") return null;
|
|
3321
3332
|
for (const property of row.properties ?? []) {
|
|
3322
3333
|
if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
|
|
3323
3334
|
const key = property.key;
|
|
3324
3335
|
const value = property.value;
|
|
3325
|
-
if (
|
|
3326
|
-
if (
|
|
3336
|
+
if (key?.type !== "Identifier") continue;
|
|
3337
|
+
if (printed(value).some((text) => norm(text) === literal)) return String(key.name);
|
|
3327
3338
|
}
|
|
3328
3339
|
return null;
|
|
3329
3340
|
}
|
|
@@ -3397,7 +3408,7 @@ function stringLiterals(ast) {
|
|
|
3397
3408
|
if (node.type !== "VariableDeclarator") return;
|
|
3398
3409
|
const id = node.id;
|
|
3399
3410
|
const init = node.init;
|
|
3400
|
-
if (id?.type !== "Identifier" || init
|
|
3411
|
+
if (id?.type !== "Identifier" || !init || printed(init).length === 0) return;
|
|
3401
3412
|
out.set(String(id.name), init);
|
|
3402
3413
|
});
|
|
3403
3414
|
return out;
|
|
@@ -3428,7 +3439,7 @@ function scalarUses(content, roots, id) {
|
|
|
3428
3439
|
}
|
|
3429
3440
|
function bindScalar(ctx, member, literals, offset) {
|
|
3430
3441
|
const rule = DIALECT_RULES[ctx.dialect];
|
|
3431
|
-
const named = [...literals].filter(([, init2]) =>
|
|
3442
|
+
const named = [...literals].filter(([, init2]) => printed(init2).some((text) => norm(text) === member.literal));
|
|
3432
3443
|
if (named.length !== 1) return { splices: [], refusal: null };
|
|
3433
3444
|
const [id, init] = named[0];
|
|
3434
3445
|
const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
|
|
@@ -3643,16 +3654,17 @@ function literalTable(init) {
|
|
|
3643
3654
|
const first = elements[0];
|
|
3644
3655
|
if (!first || elements.length !== (init.elements ?? []).length) return null;
|
|
3645
3656
|
if (elements.some((element) => element.type !== first.type)) return null;
|
|
3646
|
-
if (first.
|
|
3647
|
-
|
|
3657
|
+
if (printed(first).length > 0) {
|
|
3658
|
+
if (elements.some((element) => printed(element).length === 0)) return null;
|
|
3659
|
+
return { tuple: false, rows: elements.map((element) => [norm(printed(element)[0])]) };
|
|
3648
3660
|
}
|
|
3649
3661
|
if (first.type !== "ArrayExpression") return null;
|
|
3650
3662
|
const rows = [];
|
|
3651
3663
|
for (const element of elements) {
|
|
3652
3664
|
const inner = (element.elements ?? []).filter((x) => x !== null);
|
|
3653
|
-
if (inner.length === 0 || inner.some((x) => x.
|
|
3665
|
+
if (inner.length === 0 || inner.some((x) => printed(x).length === 0)) return null;
|
|
3654
3666
|
if (inner.length !== (element.elements ?? []).length) return null;
|
|
3655
|
-
rows.push(inner.map((x) => norm(
|
|
3667
|
+
rows.push(inner.map((x) => norm(printed(x)[0])));
|
|
3656
3668
|
}
|
|
3657
3669
|
if (new Set(rows.map((row) => row.length)).size !== 1) return null;
|
|
3658
3670
|
return { tuple: true, rows };
|
|
@@ -4191,7 +4203,7 @@ async function bindImages(ctx, targets) {
|
|
|
4191
4203
|
|
|
4192
4204
|
// src/helper.ts
|
|
4193
4205
|
import { createHash as createHash2 } from "crypto";
|
|
4194
|
-
var HELPER_VERSION =
|
|
4206
|
+
var HELPER_VERSION = 14;
|
|
4195
4207
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4196
4208
|
var HELPER_PATH = Object.fromEntries(
|
|
4197
4209
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4265,11 +4277,11 @@ function render(value: unknown): string | undefined {
|
|
|
4265
4277
|
*
|
|
4266
4278
|
* Nowhere else does anything change: with no variable (a build, \`astro dev\`, a browser, an edge
|
|
4267
4279
|
* runtime) every snapshot is the one the bundle imported, exactly as before. A file that cannot be
|
|
4268
|
-
* read keeps the last good copy
|
|
4280
|
+
* read keeps the last good copy. Its modification time is asked on EVERY read (a stat, microseconds;
|
|
4281
|
+
* the file is parsed again only when it changed), so a publish shows on the very next request.
|
|
4269
4282
|
*/
|
|
4270
|
-
type BcmsLiveContent = { stamp: number; bySlug: Map<string, unknown>; layout: unknown };
|
|
4283
|
+
type BcmsLiveContent = { helper: string; stamp: number; bySlug: Map<string, unknown>; entries: Record<string, unknown>; layout: unknown };
|
|
4271
4284
|
let bcmsLive: BcmsLiveContent | null = null;
|
|
4272
|
-
let bcmsLiveCheckedAt = 0;
|
|
4273
4285
|
|
|
4274
4286
|
function liveContent(): BcmsLiveContent | null {
|
|
4275
4287
|
const proc = (globalThis as {
|
|
@@ -4277,9 +4289,6 @@ function liveContent(): BcmsLiveContent | null {
|
|
|
4277
4289
|
}).process;
|
|
4278
4290
|
const file = proc?.env?.BCMS_CONTENT_FILE;
|
|
4279
4291
|
if (!file || typeof proc?.getBuiltinModule !== "function") return null;
|
|
4280
|
-
const now = Date.now();
|
|
4281
|
-
if (now - bcmsLiveCheckedAt < 1000) return bcmsLive;
|
|
4282
|
-
bcmsLiveCheckedAt = now;
|
|
4283
4292
|
try {
|
|
4284
4293
|
const fs = proc.getBuiltinModule("node:fs") as {
|
|
4285
4294
|
statSync(path: string): { mtimeMs: number };
|
|
@@ -4293,13 +4302,30 @@ function liveContent(): BcmsLiveContent | null {
|
|
|
4293
4302
|
for (const rows of Object.values(collections)) {
|
|
4294
4303
|
if (!Array.isArray(rows)) continue;
|
|
4295
4304
|
for (const row of rows) {
|
|
4296
|
-
|
|
4305
|
+
// A published row carries its values under \`data\` (the delivery API's shape); the snapshot a
|
|
4306
|
+
// template reads is the values themselves. Read flat, as before, where a row has none.
|
|
4307
|
+
if (isRecord(row) && typeof row.slug === "string" && !bySlug.has(row.slug)) {
|
|
4308
|
+
bySlug.set(row.slug, isRecord(row.data) ? { ...row.data, slug: row.slug } : row);
|
|
4309
|
+
}
|
|
4297
4310
|
}
|
|
4298
4311
|
}
|
|
4312
|
+
// A dynamic route reads \`page.entries[slug]\`: every published row, by its slug.
|
|
4313
|
+
const entries: Record<string, unknown> = Object.fromEntries(bySlug);
|
|
4314
|
+
for (const row of bySlug.values()) if (isRecord(row) && !("entries" in row)) row.entries = entries;
|
|
4299
4315
|
const pages = isRecord(snapshot) && Array.isArray(snapshot.pages) ? snapshot.pages : [];
|
|
4300
4316
|
const withLayout = pages.find((page: unknown) => isRecord(page) && isRecord(page.layout));
|
|
4301
|
-
|
|
4302
|
-
|
|
4317
|
+
// The site's Layout first; a page's own (its overrides resolved in) only where there is no other.
|
|
4318
|
+
const delivered = isRecord(snapshot) && isRecord(snapshot.layout) ? snapshot.layout : isRecord(withLayout) ? withLayout.layout : undefined;
|
|
4319
|
+
// A template addresses a Layout value as \`<section id>:<field id>\`; the published Layout holds
|
|
4320
|
+
// each Section's values under \`sections.<slug>.fields\`. Both spellings are answered.
|
|
4321
|
+
const layout: Record<string, unknown> | undefined = isRecord(delivered) ? { ...delivered } : undefined;
|
|
4322
|
+
for (const section of layout && isRecord(delivered) && isRecord(delivered.sections) ? Object.values(delivered.sections) : []) {
|
|
4323
|
+
if (!isRecord(section) || typeof section.id !== "string" || !isRecord(section.fields)) continue;
|
|
4324
|
+
for (const [field, value] of Object.entries(section.fields)) layout![section.id + ":" + field] = value;
|
|
4325
|
+
}
|
|
4326
|
+
// \`helper\` names this reader in the built bundle: the platform reads it to know the app shows a
|
|
4327
|
+
// publish on its next request, and an image BetterCMS copied beside it (@see bcmsImage).
|
|
4328
|
+
bcmsLive = { helper: "bcms-helper@${HELPER_VERSION}", stamp, bySlug, entries, layout };
|
|
4303
4329
|
} catch {
|
|
4304
4330
|
// Unreadable, or caught mid-write: keep serving the last good copy.
|
|
4305
4331
|
}
|
|
@@ -4311,7 +4337,10 @@ function live(page: unknown): unknown {
|
|
|
4311
4337
|
const content = liveContent();
|
|
4312
4338
|
if (!content) return page;
|
|
4313
4339
|
if (page === layoutSnapshot) return isRecord(content.layout) ? content.layout : page;
|
|
4314
|
-
if (isRecord(page) && typeof page.slug === "string")
|
|
4340
|
+
if (isRecord(page) && typeof page.slug === "string") {
|
|
4341
|
+
// A collection's page has no row of its own: its entries are the published ones all the same.
|
|
4342
|
+
return content.bySlug.get(page.slug) ?? ("entries" in page ? page : { ...page, entries: content.entries });
|
|
4343
|
+
}
|
|
4315
4344
|
return page;
|
|
4316
4345
|
}
|
|
4317
4346
|
|
|
@@ -4325,7 +4354,10 @@ export function bcms(page: unknown, path: string, fallback: string, slug?: strin
|
|
|
4325
4354
|
|
|
4326
4355
|
/** One field of the shared layout \u2014 the header and footer every route renders. */
|
|
4327
4356
|
export function bcmsLayout(path: string, fallback: string): string {
|
|
4328
|
-
|
|
4357
|
+
const layout = live(layoutSnapshot);
|
|
4358
|
+
// The whole address first: a field id may carry a dot of its own (\`derived.nav-cta\`).
|
|
4359
|
+
const whole = isRecord(layout) ? layout[path] : undefined;
|
|
4360
|
+
return render(whole !== undefined ? whole : readPath(layout, path)) ?? fallback;
|
|
4329
4361
|
}
|
|
4330
4362
|
|
|
4331
4363
|
/**
|
|
@@ -4402,13 +4434,24 @@ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][
|
|
|
4402
4434
|
* release write (\`writeImage\`) puts the editor's image on the \`data-bcms-field\` element, which
|
|
4403
4435
|
* is where every published value lands. A plain string fallback (\`<img src="/hero.png">\`) has
|
|
4404
4436
|
* no such rule and takes the CMS url directly.
|
|
4437
|
+
*
|
|
4438
|
+
* \u{1F534} ON A HOSTED SERVER THE EDITOR'S IMAGE IS A LOCAL FILE. A page the app renders per request has no
|
|
4439
|
+
* release write: what this returns is the page. BetterCMS copies a replaced image beside the app and
|
|
4440
|
+
* names the copy on the published value (\`local\`, with its size), and it is returned in the shape
|
|
4441
|
+
* of an import (\`{ src, width, height, format }\`), so \`<Image>\` optimises it like the site's own.
|
|
4442
|
+
* Absent (a build, or the site's own picture) the import stands.
|
|
4405
4443
|
*/
|
|
4406
4444
|
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
4407
4445
|
page = live(page);
|
|
4408
|
-
if (fallback !== null && typeof fallback === "object") return fallback;
|
|
4409
4446
|
const entries = isRecord(page) ? page.entries : undefined;
|
|
4410
4447
|
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4411
|
-
|
|
4448
|
+
const value = readPath(root, path);
|
|
4449
|
+
if (fallback !== null && typeof fallback === "object") {
|
|
4450
|
+
if (!isRecord(value) || typeof value.local !== "string" || typeof value.width !== "number" || typeof value.height !== "number") return fallback;
|
|
4451
|
+
const format = /\\.([a-z0-9]+)$/i.exec(value.local)?.[1]?.toLowerCase() ?? "";
|
|
4452
|
+
return { src: value.local, width: value.width, height: value.height, format: format === "jpg" ? "jpeg" : format } as unknown as T;
|
|
4453
|
+
}
|
|
4454
|
+
return render(value) ?? fallback;
|
|
4412
4455
|
}
|
|
4413
4456
|
|
|
4414
4457
|
/**
|
|
@@ -4741,7 +4784,8 @@ var HISTORICAL = {
|
|
|
4741
4784
|
9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
|
|
4742
4785
|
10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
|
|
4743
4786
|
11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
|
|
4744
|
-
12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
|
|
4787
|
+
12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
|
|
4788
|
+
13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
|
|
4745
4789
|
},
|
|
4746
4790
|
"src/lib/bcms-content.ts": {
|
|
4747
4791
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
@@ -4755,7 +4799,8 @@ var HISTORICAL = {
|
|
|
4755
4799
|
9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494",
|
|
4756
4800
|
10: "35bb7c0e30f5c41fe68e52259ec4764de6de2734064fa0e1521f3338eef0a09e",
|
|
4757
4801
|
11: "818784f2088150d64d91b42eac944dc9503f104d32a0e7a18c753d2982a5adc0",
|
|
4758
|
-
12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208"
|
|
4802
|
+
12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208",
|
|
4803
|
+
13: "06ee70c9f0bb25e03f3930d2250da8dae172ade457f27ebba20382b6482ad3f4"
|
|
4759
4804
|
},
|
|
4760
4805
|
"composables/bcms-content.ts": {
|
|
4761
4806
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
@@ -4769,7 +4814,8 @@ var HISTORICAL = {
|
|
|
4769
4814
|
9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
|
|
4770
4815
|
10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
|
|
4771
4816
|
11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
|
|
4772
|
-
12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
|
|
4817
|
+
12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
|
|
4818
|
+
13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
|
|
4773
4819
|
}
|
|
4774
4820
|
};
|
|
4775
4821
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -5460,7 +5506,8 @@ function overlapping(rewrites) {
|
|
|
5460
5506
|
for (const a of rewrites) {
|
|
5461
5507
|
for (const b of rewrites) {
|
|
5462
5508
|
if (a.raw !== void 0 || b.raw !== void 0) continue;
|
|
5463
|
-
if (a
|
|
5509
|
+
if (a === b || a.key === b.key) continue;
|
|
5510
|
+
if (collide(a, b)) {
|
|
5464
5511
|
out.add(a);
|
|
5465
5512
|
out.add(b);
|
|
5466
5513
|
}
|
|
@@ -5468,6 +5515,16 @@ function overlapping(rewrites) {
|
|
|
5468
5515
|
}
|
|
5469
5516
|
return [...out];
|
|
5470
5517
|
}
|
|
5518
|
+
var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
|
|
5519
|
+
function collide(a, b) {
|
|
5520
|
+
const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
|
|
5521
|
+
const [wa, wb] = [writes(a), writes(b)];
|
|
5522
|
+
if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
|
|
5523
|
+
if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
|
|
5524
|
+
const [fa, fb] = [field(a), field(b)];
|
|
5525
|
+
return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
|
|
5526
|
+
}
|
|
5527
|
+
var field = (r) => r.site.where === "prop" || r.site.where === "text" && !ownsSubtree(r.kind, r.site, r.inline) && r.site.mixed && r.site.textRange ? void 0 : declaration(r).plain[0];
|
|
5471
5528
|
function declaration(rewrite) {
|
|
5472
5529
|
const { site, path, kind, scope } = rewrite;
|
|
5473
5530
|
if (site.where === "attr" && kind !== "image") {
|
|
@@ -5482,8 +5539,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
5482
5539
|
const bindings = /* @__PURE__ */ new Map();
|
|
5483
5540
|
const addDeclaration = (at, add) => {
|
|
5484
5541
|
const held = attributes.get(at) ?? { plain: [], props: [] };
|
|
5485
|
-
held.plain.push(...add.plain);
|
|
5486
|
-
held.props.push(...add.props);
|
|
5542
|
+
held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
|
|
5543
|
+
held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
|
|
5487
5544
|
attributes.set(at, held);
|
|
5488
5545
|
};
|
|
5489
5546
|
for (const rewrite of rewrites) {
|
|
@@ -5906,7 +5963,8 @@ function attrsOf4(node, content) {
|
|
|
5906
5963
|
valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
|
|
5907
5964
|
expression: !quoted,
|
|
5908
5965
|
range,
|
|
5909
|
-
expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
|
|
5966
|
+
expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
|
|
5967
|
+
...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
|
|
5910
5968
|
}
|
|
5911
5969
|
];
|
|
5912
5970
|
});
|
|
@@ -6025,6 +6083,9 @@ function attrsOf5(node) {
|
|
|
6025
6083
|
return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
|
|
6026
6084
|
}
|
|
6027
6085
|
const literal = literalValue(attr);
|
|
6086
|
+
const tag = attr.value;
|
|
6087
|
+
const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
|
|
6088
|
+
const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
|
|
6028
6089
|
return [
|
|
6029
6090
|
{
|
|
6030
6091
|
name,
|
|
@@ -6032,7 +6093,8 @@ function attrsOf5(node) {
|
|
|
6032
6093
|
valueRange: literal && literal.value !== "" ? literal.range : null,
|
|
6033
6094
|
expression: literal === null,
|
|
6034
6095
|
// Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
|
|
6035
|
-
expressionRange: null,
|
|
6096
|
+
expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
|
|
6097
|
+
...digits ? { number: digits } : {},
|
|
6036
6098
|
range
|
|
6037
6099
|
}
|
|
6038
6100
|
];
|
|
@@ -6137,13 +6199,15 @@ var spanOf = (loc) => ({
|
|
|
6137
6199
|
function attrsOf6(node) {
|
|
6138
6200
|
return (node.props ?? []).flatMap((prop) => {
|
|
6139
6201
|
if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
|
|
6202
|
+
const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
|
|
6140
6203
|
return [
|
|
6141
6204
|
{
|
|
6142
6205
|
name: prop.arg.content,
|
|
6143
6206
|
value: "",
|
|
6144
6207
|
valueRange: null,
|
|
6145
6208
|
expression: true,
|
|
6146
|
-
expressionRange: null,
|
|
6209
|
+
expressionRange: digits ? spanOf(prop.exp?.loc) : null,
|
|
6210
|
+
...digits ? { number: digits } : {},
|
|
6147
6211
|
range: spanOf(prop.loc)
|
|
6148
6212
|
}
|
|
6149
6213
|
];
|
|
@@ -6627,8 +6691,8 @@ function readPlan(value) {
|
|
|
6627
6691
|
check.optionalString(section.sectionType, `${at2}.sectionType`);
|
|
6628
6692
|
check.optionalString(section.copy, `${at2}.copy`);
|
|
6629
6693
|
if (section.fields !== void 0) {
|
|
6630
|
-
for (const [f,
|
|
6631
|
-
const shape = check.record(
|
|
6694
|
+
for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
|
|
6695
|
+
const shape = check.record(field2, `${at2}.fields[${f}]`);
|
|
6632
6696
|
check.optionalString(shape.key, `${at2}.fields[${f}].key`);
|
|
6633
6697
|
check.optionalString(shape.path, `${at2}.fields[${f}].path`);
|
|
6634
6698
|
}
|
|
@@ -6947,15 +7011,15 @@ function wire(source, candidate, form) {
|
|
|
6947
7011
|
source.remove(start, attr.range.end);
|
|
6948
7012
|
}
|
|
6949
7013
|
}
|
|
6950
|
-
for (const
|
|
6951
|
-
const control = candidate.controls.get(
|
|
7014
|
+
for (const field2 of form.fields) {
|
|
7015
|
+
const control = candidate.controls.get(field2.key);
|
|
6952
7016
|
if (candidate.foreign.has(control)) continue;
|
|
6953
7017
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6954
|
-
if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(
|
|
7018
|
+
if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
|
|
6955
7019
|
const name = attrOf(control, "name");
|
|
6956
|
-
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(
|
|
6957
|
-
else if (!name.expression && name.value !==
|
|
6958
|
-
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(
|
|
7020
|
+
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
|
|
7021
|
+
else if (!name.expression && name.value !== field2.key && name.valueRange) {
|
|
7022
|
+
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
|
|
6959
7023
|
}
|
|
6960
7024
|
}
|
|
6961
7025
|
if (el.inner && !hasMessage(el)) {
|
|
@@ -6999,7 +7063,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6999
7063
|
candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
|
|
7000
7064
|
}
|
|
7001
7065
|
}
|
|
7002
|
-
const holds = (c, f) => f.fields.filter((
|
|
7066
|
+
const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
|
|
7003
7067
|
const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
|
|
7004
7068
|
const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
|
|
7005
7069
|
const wiredTo = (c, f) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") === f.name || byId(c, f) && programAction(c) : byId(c, f);
|
|
@@ -7121,9 +7185,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
7121
7185
|
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
7122
7186
|
continue;
|
|
7123
7187
|
}
|
|
7124
|
-
const misnamed = form.fields.find((
|
|
7125
|
-
const attr = attrOf(candidate.controls.get(
|
|
7126
|
-
return !!attr && attr.value !==
|
|
7188
|
+
const misnamed = form.fields.find((field2) => {
|
|
7189
|
+
const attr = attrOf(candidate.controls.get(field2.key), "name");
|
|
7190
|
+
return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
|
|
7127
7191
|
});
|
|
7128
7192
|
if (misnamed) {
|
|
7129
7193
|
pend2(form, "FIELD_UNMATCHED", candidate.file, `the control for ${misnamed.key} posts under an expression, so its name cannot be set to the CMS key.`);
|
|
@@ -7203,7 +7267,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
7203
7267
|
);
|
|
7204
7268
|
}
|
|
7205
7269
|
for (const { form, candidate } of matched) {
|
|
7206
|
-
const keys = form.fields.filter((
|
|
7270
|
+
const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
|
|
7207
7271
|
if (keys.length > 0) {
|
|
7208
7272
|
notes.push(
|
|
7209
7273
|
`form ${form.name}: ${keys.join(", ")} matched a control inside an imported component, so no data-bcms-form-field marker was written for ${keys.length === 1 ? "it" : "them"}.`
|
|
@@ -8462,7 +8526,7 @@ var inSection = (section, path) => {
|
|
|
8462
8526
|
const target = rowless(path);
|
|
8463
8527
|
return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
|
|
8464
8528
|
};
|
|
8465
|
-
var fieldPaths = (section) => (section.fields ?? []).map((
|
|
8529
|
+
var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
|
|
8466
8530
|
function elementIndex(roots) {
|
|
8467
8531
|
const out = /* @__PURE__ */ new Map();
|
|
8468
8532
|
const visit = (node, chain) => {
|
|
@@ -8620,8 +8684,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
8620
8684
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
8621
8685
|
` : "";
|
|
8622
8686
|
const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)}, bindPaths);` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides, bindPaths) };`;
|
|
8623
|
-
const
|
|
8624
|
-
const declared = [copy,
|
|
8687
|
+
const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
|
|
8688
|
+
const declared = [copy, field2].filter(Boolean).join("\n");
|
|
8625
8689
|
const styles = dialect === "astro" ? carriedStyles(content) : "";
|
|
8626
8690
|
if (dialect === "astro") {
|
|
8627
8691
|
return { free: [], duplicates: [], unconvertible: [], content: `---
|
|
@@ -8934,8 +8998,8 @@ function propPrefix(section) {
|
|
|
8934
8998
|
}
|
|
8935
8999
|
function exactPrefix(section) {
|
|
8936
9000
|
const prefixes = /* @__PURE__ */ new Set();
|
|
8937
|
-
for (const
|
|
8938
|
-
const { path, key } =
|
|
9001
|
+
for (const field2 of section.fields ?? []) {
|
|
9002
|
+
const { path, key } = field2;
|
|
8939
9003
|
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
8940
9004
|
if (!path.endsWith(key)) return null;
|
|
8941
9005
|
prefixes.add(path.slice(0, path.length - key.length));
|
|
@@ -8947,11 +9011,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
|
|
|
8947
9011
|
function flatPrefix(section) {
|
|
8948
9012
|
const prefixes = /* @__PURE__ */ new Set();
|
|
8949
9013
|
let informative = false;
|
|
8950
|
-
for (const
|
|
8951
|
-
const { path, key } =
|
|
9014
|
+
for (const field2 of section.fields ?? []) {
|
|
9015
|
+
const { path, key } = field2;
|
|
8952
9016
|
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
8953
9017
|
informative = true;
|
|
8954
|
-
const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof
|
|
9018
|
+
const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
|
|
8955
9019
|
if (!prefix) return null;
|
|
8956
9020
|
prefixes.add(prefix);
|
|
8957
9021
|
}
|
|
@@ -8960,15 +9024,15 @@ function flatPrefix(section) {
|
|
|
8960
9024
|
function flatPathMap(section, prefix) {
|
|
8961
9025
|
const scalars = {};
|
|
8962
9026
|
const tables = {};
|
|
8963
|
-
for (const
|
|
8964
|
-
const { path, key } =
|
|
9027
|
+
for (const field2 of section.fields ?? []) {
|
|
9028
|
+
const { path, key } = field2;
|
|
8965
9029
|
if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
|
|
8966
9030
|
const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
|
|
8967
9031
|
if (!row) {
|
|
8968
|
-
scalars[path] =
|
|
9032
|
+
scalars[path] = field2.overrideKey ?? key;
|
|
8969
9033
|
continue;
|
|
8970
9034
|
}
|
|
8971
|
-
const address = typeof
|
|
9035
|
+
const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
|
|
8972
9036
|
if (!address) return null;
|
|
8973
9037
|
const table = tables[row[1]] ??= { key: address[1], columns: {} };
|
|
8974
9038
|
if (table.key !== address[1]) return null;
|
|
@@ -10833,7 +10897,7 @@ function styleEdits(before, after) {
|
|
|
10833
10897
|
// package.json
|
|
10834
10898
|
var package_default = {
|
|
10835
10899
|
name: "@bettercms-ai/convert",
|
|
10836
|
-
version: "0.
|
|
10900
|
+
version: "0.26.0",
|
|
10837
10901
|
type: "module",
|
|
10838
10902
|
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.",
|
|
10839
10903
|
bin: {
|
|
@@ -10866,7 +10930,7 @@ var package_default = {
|
|
|
10866
10930
|
dependencies: {
|
|
10867
10931
|
"@astrojs/compiler": "^2.13.1",
|
|
10868
10932
|
"@babel/parser": "^7.29.2",
|
|
10869
|
-
"@bettercms-ai/types": "^1.
|
|
10933
|
+
"@bettercms-ai/types": "^1.32.0",
|
|
10870
10934
|
"@vue/compiler-sfc": "^3.5.0",
|
|
10871
10935
|
"magic-string": "^0.30.21",
|
|
10872
10936
|
parse5: "^7.3.0",
|