@bettercms-ai/convert 0.1.0 → 0.2.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 +90 -0
- package/README.md +145 -0
- package/dist/cli.js +1418 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +294 -4
- package/dist/index.js +1348 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import MagicString4 from "magic-string";
|
|
3
3
|
|
|
4
4
|
// src/brief.ts
|
|
5
5
|
import { createHash } from "crypto";
|
|
@@ -128,7 +128,7 @@ function propsAttribute(props) {
|
|
|
128
128
|
|
|
129
129
|
// src/helper.ts
|
|
130
130
|
import { createHash as createHash2 } from "crypto";
|
|
131
|
-
var HELPER_VERSION =
|
|
131
|
+
var HELPER_VERSION = 3;
|
|
132
132
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
133
133
|
var HELPER_PATH = Object.fromEntries(
|
|
134
134
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -207,12 +207,37 @@ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: s
|
|
|
207
207
|
const value = readPath(root, path);
|
|
208
208
|
return Array.isArray(value) ? (value as T[]) : fallback;
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
213
|
+
*
|
|
214
|
+
* \`overrides ?? pageGroup(bind) ?? defaults\` \u2014 the instance's own values when the editor gave it
|
|
215
|
+
* any, otherwise the page field group the placement is bound to, otherwise nothing at all, in
|
|
216
|
+
* which case every \`bcms(...)\` below renders the fallback the template shipped with. That last
|
|
217
|
+
* step is why a section renders correctly against the committed \`{}\` stub.
|
|
218
|
+
*/
|
|
219
|
+
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
220
|
+
// \u{1F534} AN EMPTY OBJECT IS AN ANSWER. \`overrides: {}\` is an instance an editor cleared, and
|
|
221
|
+
// "\`??\` unless it happens to be empty" quietly put the page's copy back into it.
|
|
222
|
+
if (isRecord(overrides)) return overrides;
|
|
223
|
+
const group = bind ? readPath(page, bind) : undefined;
|
|
224
|
+
return isRecord(group) ? group : {};
|
|
225
|
+
}
|
|
210
226
|
`;
|
|
211
227
|
}
|
|
212
228
|
var HISTORICAL = {
|
|
213
|
-
"src/bcms-content.ts": {
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
"src/bcms-content.ts": {
|
|
230
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
231
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
232
|
+
},
|
|
233
|
+
"src/lib/bcms-content.ts": {
|
|
234
|
+
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
235
|
+
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002"
|
|
236
|
+
},
|
|
237
|
+
"composables/bcms-content.ts": {
|
|
238
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
239
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
240
|
+
}
|
|
216
241
|
};
|
|
217
242
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
218
243
|
function isKnownHelper(content, dialect) {
|
|
@@ -3063,7 +3088,9 @@ function attrsOf2(open, content) {
|
|
|
3063
3088
|
value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
|
|
3064
3089
|
valueRange: null,
|
|
3065
3090
|
expression: true,
|
|
3066
|
-
range: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
3091
|
+
range: { start: attr.start ?? 0, end: attr.end ?? 0 },
|
|
3092
|
+
// `{...props}` — the braces ARE the attribute, so the expression is its whole span.
|
|
3093
|
+
expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
3067
3094
|
}
|
|
3068
3095
|
];
|
|
3069
3096
|
}
|
|
@@ -3078,7 +3105,8 @@ function attrsOf2(open, content) {
|
|
|
3078
3105
|
// The quotes are one character each on a StringLiteral; babel's range covers them.
|
|
3079
3106
|
valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
|
|
3080
3107
|
expression: !literal,
|
|
3081
|
-
range
|
|
3108
|
+
range,
|
|
3109
|
+
expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
|
|
3082
3110
|
}
|
|
3083
3111
|
];
|
|
3084
3112
|
});
|
|
@@ -3143,6 +3171,7 @@ function convert(node, content) {
|
|
|
3143
3171
|
type: "element",
|
|
3144
3172
|
tag,
|
|
3145
3173
|
start,
|
|
3174
|
+
end: node.end ?? start,
|
|
3146
3175
|
// Babel's own offsets: the last attribute's `end`, or the end of the element's NAME. Both
|
|
3147
3176
|
// come out of the parse, so an escaped quote inside an attribute cannot move them.
|
|
3148
3177
|
attrInsertAt: node.type === "JSXFragment" ? start + 1 : attrInsertAfter(attrs, open?.start ?? start, tag),
|
|
@@ -3578,8 +3607,9 @@ function importsIn(parsed, _dialect) {
|
|
|
3578
3607
|
if (entry.importKind === "type") continue;
|
|
3579
3608
|
const local = entry.local?.name;
|
|
3580
3609
|
if (typeof local !== "string") continue;
|
|
3581
|
-
const
|
|
3582
|
-
|
|
3610
|
+
const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
|
|
3611
|
+
const imported = kind === "named" ? entry.imported?.name ?? local : null;
|
|
3612
|
+
out.set(local, { specifier, imported, kind });
|
|
3583
3613
|
}
|
|
3584
3614
|
});
|
|
3585
3615
|
return out;
|
|
@@ -3587,6 +3617,33 @@ function importsIn(parsed, _dialect) {
|
|
|
3587
3617
|
|
|
3588
3618
|
// src/routes.ts
|
|
3589
3619
|
var isDynamicRoute = (file) => file.split("/").some((segment) => /^\[.+\]/.test(segment) || /\[.+\]\.[a-z]+$/i.test(segment));
|
|
3620
|
+
var PAGE_EXTENSIONS = /* @__PURE__ */ new Set(["astro", "tsx", "jsx", "ts", "js", "svelte", "vue"]);
|
|
3621
|
+
var isOrganisational = (segment) => segment.startsWith("(") && segment.endsWith(")") || segment.startsWith("@");
|
|
3622
|
+
function routeOfFile(file) {
|
|
3623
|
+
const extension = file.slice(file.lastIndexOf(".") + 1);
|
|
3624
|
+
if (!PAGE_EXTENSIONS.has(extension)) return null;
|
|
3625
|
+
let parts = file.split("/");
|
|
3626
|
+
if (parts[0] === "src") parts = parts.slice(1);
|
|
3627
|
+
if (parts[0] === "app" && parts[1] === "pages") parts = parts.slice(1);
|
|
3628
|
+
const router = parts[0];
|
|
3629
|
+
if (router !== "pages" && router !== "app" && router !== "routes") return null;
|
|
3630
|
+
let segments = parts.slice(1);
|
|
3631
|
+
const last = segments[segments.length - 1];
|
|
3632
|
+
if (last === void 0) return null;
|
|
3633
|
+
const base = last.slice(0, last.lastIndexOf("."));
|
|
3634
|
+
if (router === "pages") {
|
|
3635
|
+
segments = [...segments.slice(0, -1), ...base === "index" ? [] : [base]];
|
|
3636
|
+
} else {
|
|
3637
|
+
if (base !== "page" && base !== "+page") return null;
|
|
3638
|
+
segments = segments.slice(0, -1);
|
|
3639
|
+
}
|
|
3640
|
+
const url = segments.filter((segment) => !isOrganisational(segment));
|
|
3641
|
+
return `/${url.join("/")}`.replace(/\/{2,}/g, "/");
|
|
3642
|
+
}
|
|
3643
|
+
function pageFilesFor(route, files) {
|
|
3644
|
+
const wanted = route === "" ? "/" : route.replace(/\/+$/, "") || "/";
|
|
3645
|
+
return [...files].filter((file) => routeOfFile(file) === wanted).sort();
|
|
3646
|
+
}
|
|
3590
3647
|
var FUNCTIONS2 = /* @__PURE__ */ new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
3591
3648
|
function defaultComponent(ast) {
|
|
3592
3649
|
let found = null;
|
|
@@ -3852,7 +3909,7 @@ function namesFile(fromFile, specifier, target) {
|
|
|
3852
3909
|
function snapshotLocal(parsed, dialect, slug, file) {
|
|
3853
3910
|
const target = `${CONTENT_DIR}/${slug}.json`;
|
|
3854
3911
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
3855
|
-
if (from.
|
|
3912
|
+
if (from.kind !== "default") continue;
|
|
3856
3913
|
if (namesFile(file, from.specifier, target)) return local;
|
|
3857
3914
|
}
|
|
3858
3915
|
return null;
|
|
@@ -4070,6 +4127,8 @@ var attrsOf3 = (el, content) => {
|
|
|
4070
4127
|
value: attr.value,
|
|
4071
4128
|
valueRange,
|
|
4072
4129
|
expression: false,
|
|
4130
|
+
// parse5 reads HTML, which has no expressions at all.
|
|
4131
|
+
expressionRange: null,
|
|
4073
4132
|
range: range ?? { start: 0, end: 0 }
|
|
4074
4133
|
};
|
|
4075
4134
|
});
|
|
@@ -4100,6 +4159,7 @@ function convert2(node, content) {
|
|
|
4100
4159
|
type: "element",
|
|
4101
4160
|
tag: el.tagName,
|
|
4102
4161
|
start,
|
|
4162
|
+
end: at.endOffset,
|
|
4103
4163
|
attrInsertAt: insertAt,
|
|
4104
4164
|
inner,
|
|
4105
4165
|
attrs,
|
|
@@ -4120,6 +4180,18 @@ function parse2(content) {
|
|
|
4120
4180
|
// src/sites/astro.ts
|
|
4121
4181
|
import { parse as parse3 } from "@astrojs/compiler";
|
|
4122
4182
|
import { parse as parseScript } from "@babel/parser";
|
|
4183
|
+
function byteToChar(content) {
|
|
4184
|
+
if (!/[^\u0000-\u007f]/.test(content)) return null;
|
|
4185
|
+
const bytes = [];
|
|
4186
|
+
for (let at = 0; at < content.length; ) {
|
|
4187
|
+
const code = content.codePointAt(at);
|
|
4188
|
+
const width = code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
4189
|
+
for (let n = 0; n < width; n++) bytes.push(at);
|
|
4190
|
+
at += code < 65536 ? 1 : 2;
|
|
4191
|
+
}
|
|
4192
|
+
bytes.push(content.length);
|
|
4193
|
+
return (offset) => bytes[offset] ?? content.length;
|
|
4194
|
+
}
|
|
4123
4195
|
var RAW_TAGS2 = /* @__PURE__ */ new Set(["script", "style"]);
|
|
4124
4196
|
var backOverSpace = (content, at) => {
|
|
4125
4197
|
let end = at;
|
|
@@ -4166,6 +4238,8 @@ function attrsOf4(node, content) {
|
|
|
4166
4238
|
const raw = attr.raw ?? "";
|
|
4167
4239
|
const end = attrEnd(content, attr, start, attributes[at + 1]?.position?.start?.offset);
|
|
4168
4240
|
const range = { start, end };
|
|
4241
|
+
const open = attr.kind === "spread" || attr.kind === "shorthand" ? content.lastIndexOf("{", start) : content.indexOf("{", start + attr.name.length);
|
|
4242
|
+
const close = content.lastIndexOf("}", end);
|
|
4169
4243
|
const quoted = attr.kind === "quoted";
|
|
4170
4244
|
const rawStart = quoted ? content.indexOf(raw, start) : -1;
|
|
4171
4245
|
const unquoted = raw.length === attr.value.length;
|
|
@@ -4175,7 +4249,8 @@ function attrsOf4(node, content) {
|
|
|
4175
4249
|
value: attr.value,
|
|
4176
4250
|
valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
|
|
4177
4251
|
expression: !quoted,
|
|
4178
|
-
range
|
|
4252
|
+
range,
|
|
4253
|
+
expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
|
|
4179
4254
|
}
|
|
4180
4255
|
];
|
|
4181
4256
|
});
|
|
@@ -4214,11 +4289,13 @@ function convert3(node, content) {
|
|
|
4214
4289
|
const reportedEnd = node.position?.end?.offset;
|
|
4215
4290
|
const closes = reportedEnd !== void 0 && content.slice(reportedEnd - closeTag.length, reportedEnd) === closeTag;
|
|
4216
4291
|
const inner = closes ? { start: content.indexOf(">", insertAt) + 1, end: reportedEnd - closeTag.length } : null;
|
|
4292
|
+
const end = closes ? reportedEnd : content.indexOf(">", insertAt) + 1;
|
|
4217
4293
|
return [
|
|
4218
4294
|
{
|
|
4219
4295
|
type: "element",
|
|
4220
4296
|
tag,
|
|
4221
4297
|
start,
|
|
4298
|
+
end,
|
|
4222
4299
|
attrInsertAt: insertAt,
|
|
4223
4300
|
inner,
|
|
4224
4301
|
attrs,
|
|
@@ -4227,11 +4304,26 @@ function convert3(node, content) {
|
|
|
4227
4304
|
}
|
|
4228
4305
|
];
|
|
4229
4306
|
}
|
|
4307
|
+
function remap(nodes, at) {
|
|
4308
|
+
for (const node of nodes) {
|
|
4309
|
+
const start = node.position?.start;
|
|
4310
|
+
if (start?.offset !== void 0) start.offset = at(start.offset);
|
|
4311
|
+
const end = node.position?.end;
|
|
4312
|
+
if (end?.offset !== void 0) end.offset = at(end.offset);
|
|
4313
|
+
for (const attr of node.attributes ?? []) {
|
|
4314
|
+
const attrStart = attr.position?.start;
|
|
4315
|
+
if (attrStart?.offset !== void 0) attrStart.offset = at(attrStart.offset);
|
|
4316
|
+
}
|
|
4317
|
+
remap(node.children ?? [], at);
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4230
4320
|
async function parseFile(content) {
|
|
4231
4321
|
let top;
|
|
4232
4322
|
try {
|
|
4233
4323
|
const result = await parse3(content, { position: true });
|
|
4234
4324
|
top = result.ast.children ?? [];
|
|
4325
|
+
const at = byteToChar(content);
|
|
4326
|
+
if (at) remap(top, at);
|
|
4235
4327
|
} catch (error) {
|
|
4236
4328
|
return { roots: [], error: { code: "PARSE_ERROR", message: error.message } };
|
|
4237
4329
|
}
|
|
@@ -4273,7 +4365,7 @@ function attrsOf5(node) {
|
|
|
4273
4365
|
const name = typeof attr.name === "string" ? attr.name : attr.type;
|
|
4274
4366
|
const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
|
|
4275
4367
|
if (attr.type !== "Attribute") {
|
|
4276
|
-
return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, range }];
|
|
4368
|
+
return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
|
|
4277
4369
|
}
|
|
4278
4370
|
const literal = literalValue(attr);
|
|
4279
4371
|
return [
|
|
@@ -4282,6 +4374,8 @@ function attrsOf5(node) {
|
|
|
4282
4374
|
value: literal?.value ?? "",
|
|
4283
4375
|
valueRange: literal && literal.value !== "" ? literal.range : null,
|
|
4284
4376
|
expression: literal === null,
|
|
4377
|
+
// Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
|
|
4378
|
+
expressionRange: null,
|
|
4285
4379
|
range
|
|
4286
4380
|
}
|
|
4287
4381
|
];
|
|
@@ -4327,6 +4421,7 @@ function convert4(node, content) {
|
|
|
4327
4421
|
type: "element",
|
|
4328
4422
|
tag,
|
|
4329
4423
|
start,
|
|
4424
|
+
end,
|
|
4330
4425
|
attrInsertAt: insertAt,
|
|
4331
4426
|
inner: closes ? { start: content.indexOf(">", insertAt) + 1, end: end - close.length } : null,
|
|
4332
4427
|
attrs,
|
|
@@ -4391,6 +4486,7 @@ function attrsOf6(node) {
|
|
|
4391
4486
|
value: "",
|
|
4392
4487
|
valueRange: null,
|
|
4393
4488
|
expression: true,
|
|
4489
|
+
expressionRange: null,
|
|
4394
4490
|
range: spanOf(prop.loc)
|
|
4395
4491
|
}
|
|
4396
4492
|
];
|
|
@@ -4402,6 +4498,7 @@ function attrsOf6(node) {
|
|
|
4402
4498
|
value: "",
|
|
4403
4499
|
valueRange: null,
|
|
4404
4500
|
expression: true,
|
|
4501
|
+
expressionRange: null,
|
|
4405
4502
|
range: spanOf(prop.loc)
|
|
4406
4503
|
}
|
|
4407
4504
|
];
|
|
@@ -4415,6 +4512,7 @@ function attrsOf6(node) {
|
|
|
4415
4512
|
value,
|
|
4416
4513
|
valueRange: quoted && value !== "" ? { start: quoted.start + 1, end: quoted.end - 1 } : null,
|
|
4417
4514
|
expression: false,
|
|
4515
|
+
expressionRange: null,
|
|
4418
4516
|
range
|
|
4419
4517
|
}
|
|
4420
4518
|
];
|
|
@@ -4444,6 +4542,7 @@ function convert5(node, content) {
|
|
|
4444
4542
|
type: "element",
|
|
4445
4543
|
tag,
|
|
4446
4544
|
start,
|
|
4545
|
+
end,
|
|
4447
4546
|
attrInsertAt: insertAt,
|
|
4448
4547
|
inner: closes ? { start: content.indexOf(">", insertAt) + 1, end: end - close.length } : null,
|
|
4449
4548
|
attrs,
|
|
@@ -4543,6 +4642,123 @@ function findSitesTolerant(content, literals) {
|
|
|
4543
4642
|
return sites;
|
|
4544
4643
|
}
|
|
4545
4644
|
|
|
4645
|
+
// src/validate.ts
|
|
4646
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
4647
|
+
var ROUTE = /^\/(?:[A-Za-z0-9_\-.~%$@+[\]()!'*,:=&]+(?:\/[A-Za-z0-9_\-.~%$@+[\]()!'*,:=&]+)*\/?)?$/;
|
|
4648
|
+
var ValidationError = class extends Error {
|
|
4649
|
+
constructor(code, message) {
|
|
4650
|
+
super(message);
|
|
4651
|
+
this.code = code;
|
|
4652
|
+
this.name = "ValidationError";
|
|
4653
|
+
}
|
|
4654
|
+
code;
|
|
4655
|
+
};
|
|
4656
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4657
|
+
var Check = class {
|
|
4658
|
+
constructor(code) {
|
|
4659
|
+
this.code = code;
|
|
4660
|
+
}
|
|
4661
|
+
code;
|
|
4662
|
+
fail(where, why) {
|
|
4663
|
+
throw new ValidationError(this.code, `${where} ${why}.`);
|
|
4664
|
+
}
|
|
4665
|
+
record(value, where) {
|
|
4666
|
+
if (!isRecord(value)) this.fail(where, "is not an object");
|
|
4667
|
+
return value;
|
|
4668
|
+
}
|
|
4669
|
+
array(value, where) {
|
|
4670
|
+
if (!Array.isArray(value)) this.fail(where, "is not an array");
|
|
4671
|
+
return value;
|
|
4672
|
+
}
|
|
4673
|
+
string(value, where) {
|
|
4674
|
+
if (typeof value !== "string") this.fail(where, "is not a string");
|
|
4675
|
+
return value;
|
|
4676
|
+
}
|
|
4677
|
+
optionalString(value, where) {
|
|
4678
|
+
if (value === void 0 || value === null) return void 0;
|
|
4679
|
+
return this.string(value, where);
|
|
4680
|
+
}
|
|
4681
|
+
/** A value that becomes a FILE NAME or an object key in generated source. @see SAFE_SEGMENT */
|
|
4682
|
+
segment(value, where) {
|
|
4683
|
+
const held = this.string(value, where);
|
|
4684
|
+
if (!SAFE_SEGMENT.test(held)) {
|
|
4685
|
+
this.fail(where, `is not a safe path segment (${JSON.stringify(held)}); expected ${SAFE_SEGMENT.source}`);
|
|
4686
|
+
}
|
|
4687
|
+
return held;
|
|
4688
|
+
}
|
|
4689
|
+
route(value, where) {
|
|
4690
|
+
const held = this.string(value, where);
|
|
4691
|
+
if (!ROUTE.test(held) || held.includes("..")) this.fail(where, `is not an absolute URL path (${JSON.stringify(held)})`);
|
|
4692
|
+
return held;
|
|
4693
|
+
}
|
|
4694
|
+
};
|
|
4695
|
+
function readBrief(value) {
|
|
4696
|
+
const check = new Check("BRIEF_INVALID");
|
|
4697
|
+
const brief = check.record(value, "the brief");
|
|
4698
|
+
const pages = check.array(brief.pages, `the brief's "pages"`);
|
|
4699
|
+
for (const [at, entry] of pages.entries()) {
|
|
4700
|
+
const where = `brief.pages[${at}]`;
|
|
4701
|
+
const page = check.record(entry, where);
|
|
4702
|
+
check.segment(page.slug, `${where}.slug`);
|
|
4703
|
+
check.route(page.route, `${where}.route`);
|
|
4704
|
+
for (const [n, held] of check.array(page.paths, `${where}.paths`).entries()) {
|
|
4705
|
+
const path = check.record(held, `${where}.paths[${n}]`);
|
|
4706
|
+
check.string(path.path, `${where}.paths[${n}].path`);
|
|
4707
|
+
check.string(path.kind, `${where}.paths[${n}].kind`);
|
|
4708
|
+
const scope = check.optionalString(path.scope, `${where}.paths[${n}].scope`);
|
|
4709
|
+
if (scope !== void 0 && scope !== "page" && scope !== "layout") {
|
|
4710
|
+
check.fail(`${where}.paths[${n}].scope`, 'is neither "page" nor "layout"');
|
|
4711
|
+
}
|
|
4712
|
+
if (path.original !== null && path.original !== void 0 && typeof path.original !== "string") {
|
|
4713
|
+
check.fail(`${where}.paths[${n}].original`, "is not a string or null");
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
return brief;
|
|
4718
|
+
}
|
|
4719
|
+
function readPlan(value) {
|
|
4720
|
+
const check = new Check("PLAN_INVALID");
|
|
4721
|
+
const plan2 = check.record(value, "the plan");
|
|
4722
|
+
if (!Array.isArray(plan2.pages)) check.fail(`the plan's "pages"`, "is missing or not an array");
|
|
4723
|
+
for (const [at, entry] of plan2.pages.entries()) {
|
|
4724
|
+
const where = `plan.pages[${at}]`;
|
|
4725
|
+
const page = check.record(entry, where);
|
|
4726
|
+
check.route(page.route, `${where}.route`);
|
|
4727
|
+
if (page.sections === void 0) continue;
|
|
4728
|
+
for (const [n, held] of check.array(page.sections, `${where}.sections`).entries()) {
|
|
4729
|
+
const at2 = `${where}.sections[${n}]`;
|
|
4730
|
+
const section = check.record(held, at2);
|
|
4731
|
+
check.segment(section.groupKey, `${at2}.groupKey`);
|
|
4732
|
+
check.optionalString(section.sectionType, `${at2}.sectionType`);
|
|
4733
|
+
if (section.fields !== void 0) {
|
|
4734
|
+
for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
|
|
4735
|
+
const shape = check.record(field, `${at2}.fields[${f}]`);
|
|
4736
|
+
check.optionalString(shape.key, `${at2}.fields[${f}].key`);
|
|
4737
|
+
check.optionalString(shape.path, `${at2}.fields[${f}].path`);
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
if (section.reuse !== void 0) {
|
|
4741
|
+
const reuse = check.record(section.reuse, `${at2}.reuse`);
|
|
4742
|
+
check.optionalString(reuse.componentId, `${at2}.reuse.componentId`);
|
|
4743
|
+
if (reuse.proposedSlug !== void 0) check.segment(reuse.proposedSlug, `${at2}.reuse.proposedSlug`);
|
|
4744
|
+
}
|
|
4745
|
+
if (section.pending !== void 0) check.record(section.pending, `${at2}.pending`);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
if (plan2.components !== void 0) {
|
|
4749
|
+
for (const [at, entry] of check.array(plan2.components, `the plan's "components"`).entries()) {
|
|
4750
|
+
const where = `plan.components[${at}]`;
|
|
4751
|
+
const component = check.record(entry, where);
|
|
4752
|
+
check.segment(component.proposedSlug, `${where}.proposedSlug`);
|
|
4753
|
+
check.optionalString(component.name, `${where}.name`);
|
|
4754
|
+
check.optionalString(component.sectionType, `${where}.sectionType`);
|
|
4755
|
+
check.optionalString(component.componentId, `${where}.componentId`);
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
check.optionalString(plan2.digest, `the plan's "digest"`);
|
|
4759
|
+
return plan2;
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4546
4762
|
// src/scan.ts
|
|
4547
4763
|
var SKIP_DIRS = [
|
|
4548
4764
|
"node_modules/",
|
|
@@ -4597,6 +4813,1104 @@ async function readDeclarations(file, content) {
|
|
|
4597
4813
|
return parsed.error ? scannedDeclarations(content) : declarationsIn(parsed.roots, content).map((d) => ({ scope: d.scope, path: d.path }));
|
|
4598
4814
|
}
|
|
4599
4815
|
|
|
4816
|
+
// src/carry.ts
|
|
4817
|
+
var GLOBALS = /* @__PURE__ */ new Set([
|
|
4818
|
+
"Astro",
|
|
4819
|
+
"Fragment",
|
|
4820
|
+
"React",
|
|
4821
|
+
"JSX",
|
|
4822
|
+
"globalThis",
|
|
4823
|
+
"undefined",
|
|
4824
|
+
"null",
|
|
4825
|
+
"true",
|
|
4826
|
+
"false",
|
|
4827
|
+
"NaN",
|
|
4828
|
+
"Infinity",
|
|
4829
|
+
"Math",
|
|
4830
|
+
"JSON",
|
|
4831
|
+
"Object",
|
|
4832
|
+
"Array",
|
|
4833
|
+
"String",
|
|
4834
|
+
"Number",
|
|
4835
|
+
"Boolean",
|
|
4836
|
+
"Date",
|
|
4837
|
+
"RegExp",
|
|
4838
|
+
"Map",
|
|
4839
|
+
"Set",
|
|
4840
|
+
"WeakMap",
|
|
4841
|
+
"WeakSet",
|
|
4842
|
+
"Promise",
|
|
4843
|
+
"Symbol",
|
|
4844
|
+
"BigInt",
|
|
4845
|
+
"Error",
|
|
4846
|
+
"Intl",
|
|
4847
|
+
"URL",
|
|
4848
|
+
"URLSearchParams",
|
|
4849
|
+
"console",
|
|
4850
|
+
"process",
|
|
4851
|
+
"window",
|
|
4852
|
+
"document",
|
|
4853
|
+
"navigator",
|
|
4854
|
+
"fetch",
|
|
4855
|
+
"isNaN",
|
|
4856
|
+
"isFinite",
|
|
4857
|
+
"parseInt",
|
|
4858
|
+
"parseFloat",
|
|
4859
|
+
"encodeURIComponent",
|
|
4860
|
+
"decodeURIComponent",
|
|
4861
|
+
"structuredClone",
|
|
4862
|
+
"import",
|
|
4863
|
+
"arguments",
|
|
4864
|
+
"this",
|
|
4865
|
+
"void",
|
|
4866
|
+
"typeof"
|
|
4867
|
+
]);
|
|
4868
|
+
function carryFor(options) {
|
|
4869
|
+
const { dialect, fromFile, toFile, content, parsed, root } = options;
|
|
4870
|
+
const wanted = referencedNames(root, content);
|
|
4871
|
+
if (wanted === null) {
|
|
4872
|
+
return { imports: [], declarations: [], free: ["<an expression this version cannot read>"] };
|
|
4873
|
+
}
|
|
4874
|
+
const provided = new Set(options.provided);
|
|
4875
|
+
const imports = importsIn(parsed, dialect);
|
|
4876
|
+
const declarations = moduleScope(parsed, content);
|
|
4877
|
+
const carriedImports = /* @__PURE__ */ new Map();
|
|
4878
|
+
const carriedDeclarations = /* @__PURE__ */ new Map();
|
|
4879
|
+
const free = /* @__PURE__ */ new Set();
|
|
4880
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4881
|
+
const queue = [...wanted];
|
|
4882
|
+
while (queue.length > 0) {
|
|
4883
|
+
const name = queue.shift();
|
|
4884
|
+
if (seen.has(name) || provided.has(name) || GLOBALS.has(name)) continue;
|
|
4885
|
+
seen.add(name);
|
|
4886
|
+
const imported = imports.get(name);
|
|
4887
|
+
if (imported) {
|
|
4888
|
+
if (skipImport(imported.specifier, fromFile, options.skipSpecifier)) continue;
|
|
4889
|
+
const specifier = imported.specifier.startsWith(".") ? relativeImport(toFile, joinFrom(fromFile, imported.specifier)) : imported.specifier;
|
|
4890
|
+
carriedImports.set(
|
|
4891
|
+
name,
|
|
4892
|
+
imported.kind === "namespace" ? `import * as ${name} from "${specifier}";` : imported.kind === "default" ? `import ${name} from "${specifier}";` : `import { ${imported.imported === name ? name : `${imported.imported} as ${name}`} } from "${specifier}";`
|
|
4893
|
+
);
|
|
4894
|
+
continue;
|
|
4895
|
+
}
|
|
4896
|
+
const declared = declarations.get(name);
|
|
4897
|
+
if (declared) {
|
|
4898
|
+
carriedDeclarations.set(declared.at, declared.text);
|
|
4899
|
+
queue.push(...declared.uses);
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
free.add(name);
|
|
4903
|
+
}
|
|
4904
|
+
return {
|
|
4905
|
+
imports: [...new Set(carriedImports.values())].sort(),
|
|
4906
|
+
declarations: [...carriedDeclarations].sort(([a], [b]) => a - b).map(([, text]) => text),
|
|
4907
|
+
free: [...free].sort()
|
|
4908
|
+
};
|
|
4909
|
+
}
|
|
4910
|
+
function skipImport(specifier, fromFile, skip) {
|
|
4911
|
+
return skip(specifier.startsWith(".") ? joinFrom(fromFile, specifier) : "", specifier);
|
|
4912
|
+
}
|
|
4913
|
+
function joinFrom(fromFile, specifier) {
|
|
4914
|
+
const parts = fromFile.split("/").slice(0, -1);
|
|
4915
|
+
for (const segment of specifier.split("/")) {
|
|
4916
|
+
if (segment === "." || segment === "") continue;
|
|
4917
|
+
if (segment === "..") parts.pop();
|
|
4918
|
+
else parts.push(segment);
|
|
4919
|
+
}
|
|
4920
|
+
return parts.join("/");
|
|
4921
|
+
}
|
|
4922
|
+
function referencedNames(root, content) {
|
|
4923
|
+
const used = /* @__PURE__ */ new Set();
|
|
4924
|
+
const add = (code, scope) => {
|
|
4925
|
+
const scanned = scanExpression(code);
|
|
4926
|
+
if (!scanned) return null;
|
|
4927
|
+
for (const name of scanned.free) {
|
|
4928
|
+
if (!scope.some((held) => held.has(name))) used.add(name);
|
|
4929
|
+
}
|
|
4930
|
+
return scanned.bound;
|
|
4931
|
+
};
|
|
4932
|
+
const visit = (node, bounds, scope) => {
|
|
4933
|
+
if (node.type === "expr") {
|
|
4934
|
+
const start = Math.max(node.range.start, bounds.start);
|
|
4935
|
+
const end = Math.min(node.range.end, bounds.end);
|
|
4936
|
+
const holes = node.children.flatMap(
|
|
4937
|
+
(child) => child.type === "element" ? [{ start: child.start, end: child.end }] : []
|
|
4938
|
+
);
|
|
4939
|
+
let text = "";
|
|
4940
|
+
let at = start;
|
|
4941
|
+
for (const hole of [...holes].sort((a, b) => a.start - b.start)) {
|
|
4942
|
+
if (hole.start < at || hole.end > end) continue;
|
|
4943
|
+
text += `${content.slice(at, hole.start)}null`;
|
|
4944
|
+
at = hole.end;
|
|
4945
|
+
}
|
|
4946
|
+
text += content.slice(at, end);
|
|
4947
|
+
const open = text.indexOf("{");
|
|
4948
|
+
const close = text.lastIndexOf("}");
|
|
4949
|
+
const bound = add(open >= 0 && close > open ? text.slice(open + 1, close) : text, scope);
|
|
4950
|
+
if (!bound) return false;
|
|
4951
|
+
const inner2 = [...scope, bound];
|
|
4952
|
+
for (const child of node.children) if (!visit(child, bounds, inner2)) return false;
|
|
4953
|
+
return true;
|
|
4954
|
+
}
|
|
4955
|
+
if (node.type !== "element") return true;
|
|
4956
|
+
if (!isIntrinsic(node.tag) && node.tag !== "") {
|
|
4957
|
+
const tag = node.tag.split(".")[0];
|
|
4958
|
+
if (!scope.some((held) => held.has(tag))) used.add(tag);
|
|
4959
|
+
}
|
|
4960
|
+
for (const attr of node.attrs) {
|
|
4961
|
+
if (!attr.expression) continue;
|
|
4962
|
+
const span = attr.expressionRange;
|
|
4963
|
+
if (!span) continue;
|
|
4964
|
+
const raw = content.slice(span.start, span.end);
|
|
4965
|
+
if (add(stripBraces(raw), scope) === null) return false;
|
|
4966
|
+
}
|
|
4967
|
+
const inner = node.inner ?? bounds;
|
|
4968
|
+
const kids = node.children;
|
|
4969
|
+
for (const [at, child] of kids.entries()) {
|
|
4970
|
+
const next = kids[at + 1];
|
|
4971
|
+
const stop = next ? next.type === "element" ? next.start : next.range.start : inner.end;
|
|
4972
|
+
if (!visit(child, { start: inner.start, end: Math.min(inner.end, stop) }, scope)) return false;
|
|
4973
|
+
}
|
|
4974
|
+
return true;
|
|
4975
|
+
};
|
|
4976
|
+
return visit(root, { start: root.start, end: root.end }, []) ? used : null;
|
|
4977
|
+
}
|
|
4978
|
+
function stripBraces(text) {
|
|
4979
|
+
const trimmed = text.trim();
|
|
4980
|
+
const inner = trimmed.startsWith("{") && trimmed.endsWith("}") ? trimmed.slice(1, -1) : trimmed;
|
|
4981
|
+
return inner.trim().startsWith("...") ? inner.trim().slice(3) : inner;
|
|
4982
|
+
}
|
|
4983
|
+
function scanExpression(code) {
|
|
4984
|
+
if (code.trim() === "") return { free: /* @__PURE__ */ new Set(), bound: /* @__PURE__ */ new Set() };
|
|
4985
|
+
let ast;
|
|
4986
|
+
try {
|
|
4987
|
+
ast = parseProgram(`(${code}
|
|
4988
|
+
)`);
|
|
4989
|
+
} catch {
|
|
4990
|
+
try {
|
|
4991
|
+
ast = parseProgram(code);
|
|
4992
|
+
} catch {
|
|
4993
|
+
return null;
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
4997
|
+
const skip = /* @__PURE__ */ new Set();
|
|
4998
|
+
const bound = /* @__PURE__ */ new Set();
|
|
4999
|
+
walkAst(ast, (node) => {
|
|
5000
|
+
switch (node.type) {
|
|
5001
|
+
case "Identifier":
|
|
5002
|
+
case "JSXIdentifier":
|
|
5003
|
+
if (typeof node.name === "string") candidates.add(node.name);
|
|
5004
|
+
return;
|
|
5005
|
+
case "MemberExpression":
|
|
5006
|
+
case "OptionalMemberExpression":
|
|
5007
|
+
case "JSXMemberExpression":
|
|
5008
|
+
if (!node.computed) nameOf2(node.property, skip);
|
|
5009
|
+
return;
|
|
5010
|
+
case "ObjectProperty":
|
|
5011
|
+
case "ObjectMethod":
|
|
5012
|
+
case "ClassProperty":
|
|
5013
|
+
case "ClassMethod":
|
|
5014
|
+
if (!node.computed) nameOf2(node.key, skip);
|
|
5015
|
+
return;
|
|
5016
|
+
case "JSXAttribute":
|
|
5017
|
+
nameOf2(node.name, skip);
|
|
5018
|
+
return;
|
|
5019
|
+
case "JSXOpeningElement":
|
|
5020
|
+
case "JSXClosingElement": {
|
|
5021
|
+
const name = node.name;
|
|
5022
|
+
if (name?.type === "JSXIdentifier" && typeof name.name === "string") skip.add(name.name);
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
case "TSTypeReference":
|
|
5026
|
+
case "TSQualifiedName":
|
|
5027
|
+
case "TSInterfaceDeclaration":
|
|
5028
|
+
case "TSTypeAliasDeclaration":
|
|
5029
|
+
for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
|
|
5030
|
+
return;
|
|
5031
|
+
case "VariableDeclarator":
|
|
5032
|
+
patternNames2(node.id, bound);
|
|
5033
|
+
return;
|
|
5034
|
+
case "CatchClause":
|
|
5035
|
+
patternNames2(node.param, bound);
|
|
5036
|
+
return;
|
|
5037
|
+
default:
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5040
|
+
});
|
|
5041
|
+
walkAst(ast, (node) => {
|
|
5042
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
|
|
5043
|
+
for (const param of node.params ?? []) patternNames2(param, bound);
|
|
5044
|
+
if (node.id) patternNames2(node.id, bound);
|
|
5045
|
+
}
|
|
5046
|
+
});
|
|
5047
|
+
const free = /* @__PURE__ */ new Set();
|
|
5048
|
+
for (const name of candidates) if (!skip.has(name) && !bound.has(name)) free.add(name);
|
|
5049
|
+
return { free, bound };
|
|
5050
|
+
}
|
|
5051
|
+
function nameOf2(node, out) {
|
|
5052
|
+
if (!node) return;
|
|
5053
|
+
if (typeof node.name === "string") out.add(node.name);
|
|
5054
|
+
if (node.type === "JSXNamespacedName") {
|
|
5055
|
+
nameOf2(node.namespace, out);
|
|
5056
|
+
nameOf2(node.name, out);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
function patternNames2(node, out) {
|
|
5060
|
+
if (!node || typeof node !== "object") return;
|
|
5061
|
+
const typed = node;
|
|
5062
|
+
switch (typed.type) {
|
|
5063
|
+
case "Identifier":
|
|
5064
|
+
out.add(String(typed.name));
|
|
5065
|
+
return;
|
|
5066
|
+
case "ObjectPattern":
|
|
5067
|
+
for (const property of typed.properties ?? []) {
|
|
5068
|
+
patternNames2(property.type === "RestElement" ? property.argument : property.value, out);
|
|
5069
|
+
}
|
|
5070
|
+
return;
|
|
5071
|
+
case "ArrayPattern":
|
|
5072
|
+
for (const element of typed.elements ?? []) patternNames2(element, out);
|
|
5073
|
+
return;
|
|
5074
|
+
case "AssignmentPattern":
|
|
5075
|
+
patternNames2(typed.left, out);
|
|
5076
|
+
return;
|
|
5077
|
+
case "RestElement":
|
|
5078
|
+
patternNames2(typed.argument, out);
|
|
5079
|
+
return;
|
|
5080
|
+
default:
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
function moduleScope(parsed, content) {
|
|
5085
|
+
const out = /* @__PURE__ */ new Map();
|
|
5086
|
+
const program = parsed.ast?.program;
|
|
5087
|
+
if (!program) return out;
|
|
5088
|
+
const source = parsed.script?.code ?? content;
|
|
5089
|
+
for (const statement of program.body ?? []) {
|
|
5090
|
+
const kind = statement.type;
|
|
5091
|
+
if (kind !== "VariableDeclaration" && kind !== "FunctionDeclaration" && kind !== "ClassDeclaration" && kind !== "TSTypeAliasDeclaration" && kind !== "TSInterfaceDeclaration" && kind !== "TSEnumDeclaration") {
|
|
5092
|
+
continue;
|
|
5093
|
+
}
|
|
5094
|
+
const start = statement.start;
|
|
5095
|
+
const end = statement.end;
|
|
5096
|
+
if (start === void 0 || end === void 0) continue;
|
|
5097
|
+
const text = source.slice(start, end);
|
|
5098
|
+
const names = /* @__PURE__ */ new Set();
|
|
5099
|
+
if (kind === "VariableDeclaration") {
|
|
5100
|
+
for (const declarator of statement.declarations ?? []) patternNames2(declarator.id, names);
|
|
5101
|
+
} else {
|
|
5102
|
+
patternNames2(statement.id, names);
|
|
5103
|
+
}
|
|
5104
|
+
if (names.size === 0) continue;
|
|
5105
|
+
const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
|
|
5106
|
+
const uses = scanned ? [...scanned.free].filter((name) => !names.has(name)) : [];
|
|
5107
|
+
for (const name of names) out.set(name, { at: start, text, uses });
|
|
5108
|
+
}
|
|
5109
|
+
return out;
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
// src/componentize.ts
|
|
5113
|
+
import MagicString3 from "magic-string";
|
|
5114
|
+
var SectionInvariantError = class extends Error {
|
|
5115
|
+
code = "SECTION_INVARIANT";
|
|
5116
|
+
constructor(message) {
|
|
5117
|
+
super(message);
|
|
5118
|
+
this.name = "SectionInvariantError";
|
|
5119
|
+
}
|
|
5120
|
+
};
|
|
5121
|
+
function assertSections(total, sections) {
|
|
5122
|
+
const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.pending.length;
|
|
5123
|
+
if (sum !== total) {
|
|
5124
|
+
throw new SectionInvariantError(
|
|
5125
|
+
`The receipt does not account for every section: ${total} planned but ${sections.extracted} extracted + ${sections.inlineOnly} inline-only + ${sections.alreadyExtracted} already extracted + ${sections.pending.length} pending = ${sum}.`
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
return sections;
|
|
5129
|
+
}
|
|
5130
|
+
var SECTION_MARKER = "// @bettercms-ai/convert section v1";
|
|
5131
|
+
var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
|
|
5132
|
+
var COMPONENT_DIR = "src/components/bcms";
|
|
5133
|
+
var SECTIONS_LIB = "src/lib/bcms-sections.ts";
|
|
5134
|
+
var EXTRACTABLE = ["astro", "jsx"];
|
|
5135
|
+
async function componentizeSources(briefIn, planIn, sources, options) {
|
|
5136
|
+
const pending = [];
|
|
5137
|
+
let extracted = 0;
|
|
5138
|
+
let inlineOnly = 0;
|
|
5139
|
+
let alreadyExtracted = 0;
|
|
5140
|
+
let planPending = 0;
|
|
5141
|
+
let total = 0;
|
|
5142
|
+
const brief = readBrief(briefIn);
|
|
5143
|
+
const plan2 = readPlan(planIn);
|
|
5144
|
+
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
5145
|
+
const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
|
|
5146
|
+
const trees = /* @__PURE__ */ new Map();
|
|
5147
|
+
const treeOf = async (file) => {
|
|
5148
|
+
if (trees.has(file)) return trees.get(file);
|
|
5149
|
+
const content = contents.get(file) ?? "";
|
|
5150
|
+
const dialect = dialectOf(file, content);
|
|
5151
|
+
const parsed = dialect ? await parseFile4(dialect, content) : null;
|
|
5152
|
+
const tree = parsed && !parsed.error ? { parsed, elements: elementIndex(parsed.roots) } : null;
|
|
5153
|
+
trees.set(file, tree);
|
|
5154
|
+
return tree;
|
|
5155
|
+
};
|
|
5156
|
+
const helperPath = HELPER_PATH.astro;
|
|
5157
|
+
const helperExisting = contents.get(helperPath);
|
|
5158
|
+
const helperVersion = helperExisting === void 0 ? null : isKnownHelper(helperExisting, "astro");
|
|
5159
|
+
const helperBlocked = helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true;
|
|
5160
|
+
const outFiles = /* @__PURE__ */ new Map();
|
|
5161
|
+
const written = /* @__PURE__ */ new Map();
|
|
5162
|
+
const shipped = /* @__PURE__ */ new Map();
|
|
5163
|
+
const componentsOf = (dialect) => {
|
|
5164
|
+
const held = written.get(dialect) ?? /* @__PURE__ */ new Map();
|
|
5165
|
+
written.set(dialect, held);
|
|
5166
|
+
return held;
|
|
5167
|
+
};
|
|
5168
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
5169
|
+
const registryConflicts = [];
|
|
5170
|
+
for (const dialect of EXTRACTABLE) {
|
|
5171
|
+
const path = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
|
|
5172
|
+
const existing = contents.get(path);
|
|
5173
|
+
if (existing === void 0) continue;
|
|
5174
|
+
if (!isGenerated(existing)) {
|
|
5175
|
+
registryConflicts.push(path);
|
|
5176
|
+
continue;
|
|
5177
|
+
}
|
|
5178
|
+
const held = readRegistry(existing, dialect);
|
|
5179
|
+
componentsOf(dialect);
|
|
5180
|
+
for (const [slug, component] of held.components) written.get(dialect).set(slug, component);
|
|
5181
|
+
const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
|
|
5182
|
+
shipped.set(dialect, order);
|
|
5183
|
+
for (const [page, list] of held.shipped) order.set(page, list);
|
|
5184
|
+
}
|
|
5185
|
+
const lib = contents.get(SECTIONS_LIB);
|
|
5186
|
+
if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
|
|
5187
|
+
const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
|
|
5188
|
+
for (const page of plan2.pages ?? []) {
|
|
5189
|
+
const sections2 = (page.sections ?? []).filter((section) => {
|
|
5190
|
+
if (section.pending) {
|
|
5191
|
+
planPending++;
|
|
5192
|
+
return false;
|
|
5193
|
+
}
|
|
5194
|
+
return true;
|
|
5195
|
+
});
|
|
5196
|
+
total += sections2.length;
|
|
5197
|
+
if (sections2.length === 0) continue;
|
|
5198
|
+
const briefPage = brief.pages.find((p) => p.route === page.route);
|
|
5199
|
+
const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
|
|
5200
|
+
if (helperBlocked) {
|
|
5201
|
+
for (const section of sections2) {
|
|
5202
|
+
pend(page.route, section.groupKey, "HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
|
|
5203
|
+
}
|
|
5204
|
+
continue;
|
|
5205
|
+
}
|
|
5206
|
+
if (registryBlocked) {
|
|
5207
|
+
for (const section of sections2) {
|
|
5208
|
+
pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
|
|
5209
|
+
}
|
|
5210
|
+
continue;
|
|
5211
|
+
}
|
|
5212
|
+
const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
|
|
5213
|
+
if (!snapshotUsable(contents.get(snapshotPath))) {
|
|
5214
|
+
for (const section of sections2) {
|
|
5215
|
+
pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
|
|
5216
|
+
}
|
|
5217
|
+
continue;
|
|
5218
|
+
}
|
|
5219
|
+
const pageFiles = pageFilesFor(page.route, contents.keys());
|
|
5220
|
+
if (pageFiles.length > 1) {
|
|
5221
|
+
for (const section of sections2) {
|
|
5222
|
+
pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
|
|
5223
|
+
}
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
const file = pageFiles[0];
|
|
5227
|
+
if (file === void 0) {
|
|
5228
|
+
for (const section of sections2) {
|
|
5229
|
+
pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
|
|
5230
|
+
}
|
|
5231
|
+
continue;
|
|
5232
|
+
}
|
|
5233
|
+
const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, sections2));
|
|
5234
|
+
alreadyExtracted += done.size;
|
|
5235
|
+
if (done.size === sections2.length) continue;
|
|
5236
|
+
const found = await locateSections(
|
|
5237
|
+
sections2.filter((section) => !done.has(section.groupKey)),
|
|
5238
|
+
briefPage?.paths ?? [],
|
|
5239
|
+
sources,
|
|
5240
|
+
treeOf
|
|
5241
|
+
);
|
|
5242
|
+
for (const [groupKey, reason] of found.misses) {
|
|
5243
|
+
pend(page.route, groupKey, reason.code, reason.message);
|
|
5244
|
+
}
|
|
5245
|
+
const live = found.placed;
|
|
5246
|
+
if (live.length === 0) continue;
|
|
5247
|
+
const placedHere = [];
|
|
5248
|
+
for (const placed of live) {
|
|
5249
|
+
const elements = placed.byFile.get(file);
|
|
5250
|
+
if (elements && elements.length > 0) {
|
|
5251
|
+
placedHere.push({ section: placed.section, elements });
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
pend(
|
|
5255
|
+
page.route,
|
|
5256
|
+
placed.section.groupKey,
|
|
5257
|
+
"SECTION_ROOT_AMBIGUOUS",
|
|
5258
|
+
`rendered by ${[...placed.byFile.keys()].join(", ")}, not ${file}.`
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
if (placedHere.length === 0) continue;
|
|
5262
|
+
const content = contents.get(file);
|
|
5263
|
+
const dialect = dialectOf(file, content);
|
|
5264
|
+
if (!dialect || !EXTRACTABLE.includes(dialect)) {
|
|
5265
|
+
for (const placed of placedHere) {
|
|
5266
|
+
pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
|
|
5267
|
+
}
|
|
5268
|
+
continue;
|
|
5269
|
+
}
|
|
5270
|
+
const tree = await treeOf(file);
|
|
5271
|
+
if (!tree) {
|
|
5272
|
+
for (const placed of placedHere) {
|
|
5273
|
+
pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
|
|
5274
|
+
}
|
|
5275
|
+
continue;
|
|
5276
|
+
}
|
|
5277
|
+
const parsed = tree.parsed;
|
|
5278
|
+
const roots = [];
|
|
5279
|
+
for (const placed of placedHere) {
|
|
5280
|
+
const root = lowestCommonAncestor(placed.elements);
|
|
5281
|
+
if (!root) {
|
|
5282
|
+
pend(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
|
|
5283
|
+
continue;
|
|
5284
|
+
}
|
|
5285
|
+
const component = resolveComponent(plan2, placed.section);
|
|
5286
|
+
roots.push({ section: placed.section, route: page.route, root, ...component });
|
|
5287
|
+
}
|
|
5288
|
+
const nested = /* @__PURE__ */ new Set();
|
|
5289
|
+
for (const a of roots) {
|
|
5290
|
+
for (const b of roots) {
|
|
5291
|
+
if (a !== b && a.root.start <= b.root.start && b.root.end <= a.root.end) {
|
|
5292
|
+
nested.add(a);
|
|
5293
|
+
nested.add(b);
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
for (const resolved of nested) {
|
|
5298
|
+
pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
|
|
5299
|
+
}
|
|
5300
|
+
const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
|
|
5301
|
+
if (candidates.length === 0) continue;
|
|
5302
|
+
const extractable = componentsOf(dialect);
|
|
5303
|
+
const placeable = [];
|
|
5304
|
+
for (const resolved of candidates) {
|
|
5305
|
+
const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
|
|
5306
|
+
const existing = contents.get(path);
|
|
5307
|
+
if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
|
|
5308
|
+
pend(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
|
|
5309
|
+
continue;
|
|
5310
|
+
}
|
|
5311
|
+
if (extractable.has(resolved.slug)) {
|
|
5312
|
+
placeable.push(resolved);
|
|
5313
|
+
continue;
|
|
5314
|
+
}
|
|
5315
|
+
const built = componentFile(dialect, path, file, content, parsed, resolved);
|
|
5316
|
+
if (built.free.length > 0) {
|
|
5317
|
+
pend(
|
|
5318
|
+
page.route,
|
|
5319
|
+
resolved.section.groupKey,
|
|
5320
|
+
"SECTION_FREE_IDENTIFIERS",
|
|
5321
|
+
`its markup uses ${built.free.join(", ")}, which the component file cannot be given.`
|
|
5322
|
+
);
|
|
5323
|
+
continue;
|
|
5324
|
+
}
|
|
5325
|
+
extractable.set(resolved.slug, { name: resolved.name, path });
|
|
5326
|
+
fresh.add(resolved.slug);
|
|
5327
|
+
addFile(outFiles, contents, {
|
|
5328
|
+
path,
|
|
5329
|
+
operation: existing === void 0 ? "add" : "modify",
|
|
5330
|
+
content: built.content
|
|
5331
|
+
});
|
|
5332
|
+
placeable.push(resolved);
|
|
5333
|
+
}
|
|
5334
|
+
if (placeable.length === 0) continue;
|
|
5335
|
+
const layout = runOf(placeable, parsed.roots);
|
|
5336
|
+
if (layout === null) {
|
|
5337
|
+
for (const resolved of placeable) {
|
|
5338
|
+
pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
|
|
5339
|
+
}
|
|
5340
|
+
continue;
|
|
5341
|
+
}
|
|
5342
|
+
const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
|
|
5343
|
+
shipped.set(dialect, order);
|
|
5344
|
+
order.set(
|
|
5345
|
+
pageSlug,
|
|
5346
|
+
placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey }))
|
|
5347
|
+
);
|
|
5348
|
+
const rewritten = layout.contiguous ? splicePageRun(file, content, dialect, parsed, placeable, pageSlug) : splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable);
|
|
5349
|
+
addFile(outFiles, contents, { path: file, operation: "modify", content: rewritten });
|
|
5350
|
+
if (layout.contiguous) extracted += placeable.length;
|
|
5351
|
+
else inlineOnly += placeable.length;
|
|
5352
|
+
}
|
|
5353
|
+
const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
|
|
5354
|
+
if (componentSlugs.length > 0) {
|
|
5355
|
+
const pageSlugs = [...new Set([...shipped.values()].flatMap((order) => [...order.keys()]))].sort();
|
|
5356
|
+
addFile(outFiles, contents, {
|
|
5357
|
+
path: SECTIONS_LIB,
|
|
5358
|
+
operation: contents.has(SECTIONS_LIB) ? "modify" : "add",
|
|
5359
|
+
content: sectionsLib(pageSlugs)
|
|
5360
|
+
});
|
|
5361
|
+
for (const [dialect, held] of [...written].sort(([a], [b]) => a.localeCompare(b))) {
|
|
5362
|
+
if (held.size === 0) continue;
|
|
5363
|
+
const registryPath = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
|
|
5364
|
+
addFile(outFiles, contents, {
|
|
5365
|
+
path: registryPath,
|
|
5366
|
+
operation: contents.has(registryPath) ? "modify" : "add",
|
|
5367
|
+
content: registryFile(dialect, registryPath, held, shipped.get(dialect) ?? /* @__PURE__ */ new Map())
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
emitHelper(outFiles, contents, helperVersion, options?.overwriteHelper);
|
|
5371
|
+
emitStubs(outFiles, contents, pageSlugs);
|
|
5372
|
+
}
|
|
5373
|
+
const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, pending });
|
|
5374
|
+
return {
|
|
5375
|
+
files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
5376
|
+
receipt: {
|
|
5377
|
+
planDigest: plan2.digest ?? "",
|
|
5378
|
+
sections,
|
|
5379
|
+
// What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
|
|
5380
|
+
components: [...fresh].sort(),
|
|
5381
|
+
planPending
|
|
5382
|
+
}
|
|
5383
|
+
};
|
|
5384
|
+
}
|
|
5385
|
+
function alreadyOn(content, pageSlug, sections) {
|
|
5386
|
+
const ours = COMPONENT_DIR.split("/").slice(-2).join("/");
|
|
5387
|
+
const importsOurs = new RegExp(`from\\s*["'][^"']*${ours}/`).test(content);
|
|
5388
|
+
if (!importsOurs) return [];
|
|
5389
|
+
if (new RegExp(`page=\\s*["']${pageSlug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content)) {
|
|
5390
|
+
return sections.map((section) => section.groupKey);
|
|
5391
|
+
}
|
|
5392
|
+
return sections.map((section) => section.groupKey).filter((groupKey) => new RegExp(`bind=\\s*["']${groupKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content));
|
|
5393
|
+
}
|
|
5394
|
+
function readRegistry(content, dialect) {
|
|
5395
|
+
const components = /* @__PURE__ */ new Map();
|
|
5396
|
+
const shipped = /* @__PURE__ */ new Map();
|
|
5397
|
+
const paths = /* @__PURE__ */ new Map();
|
|
5398
|
+
for (const match of content.matchAll(/^import\s+(\w+)\s+from\s+"\.\/([^"]+)";$/gm)) {
|
|
5399
|
+
const [, name, file] = match;
|
|
5400
|
+
paths.set(name, `${COMPONENT_DIR}/${file}${dialect === "astro" ? "" : ".tsx"}`);
|
|
5401
|
+
}
|
|
5402
|
+
const registry = content.match(/const REGISTRY[^=]*=\s*\{([\s\S]*?)\n\};/);
|
|
5403
|
+
for (const match of (registry?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\w+),$/gm)) {
|
|
5404
|
+
const [, slug, name] = match;
|
|
5405
|
+
const path = paths.get(name);
|
|
5406
|
+
if (path) components.set(slug, { name, path });
|
|
5407
|
+
}
|
|
5408
|
+
const table = content.match(/const SHIPPED[^=]*=\s*\{([\s\S]*?)\n\};/);
|
|
5409
|
+
for (const match of (table?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\[.*\]),$/gm)) {
|
|
5410
|
+
try {
|
|
5411
|
+
shipped.set(match[1], JSON.parse(match[2]));
|
|
5412
|
+
} catch {
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
return { components, shipped };
|
|
5416
|
+
}
|
|
5417
|
+
function snapshotUsable(existing) {
|
|
5418
|
+
if (existing === void 0) return true;
|
|
5419
|
+
try {
|
|
5420
|
+
const parsed = JSON.parse(existing);
|
|
5421
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
5422
|
+
} catch {
|
|
5423
|
+
return false;
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
function addFile(out, contents, file) {
|
|
5427
|
+
if (contents.get(file.path) === file.content) return;
|
|
5428
|
+
out.set(file.path, file);
|
|
5429
|
+
}
|
|
5430
|
+
async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
5431
|
+
const placed = [];
|
|
5432
|
+
const misses = [];
|
|
5433
|
+
for (const section of sections) {
|
|
5434
|
+
const paths = fieldPaths(section);
|
|
5435
|
+
const literals = new Set(
|
|
5436
|
+
briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
|
|
5437
|
+
);
|
|
5438
|
+
const hits = /* @__PURE__ */ new Map();
|
|
5439
|
+
for (const source of sources) {
|
|
5440
|
+
const tree = await treeOf(source.path);
|
|
5441
|
+
if (!tree) continue;
|
|
5442
|
+
const parsed = tree.parsed;
|
|
5443
|
+
const elements = tree.elements;
|
|
5444
|
+
const found = /* @__PURE__ */ new Map();
|
|
5445
|
+
for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
|
|
5446
|
+
if (declaration2.scope !== "page") continue;
|
|
5447
|
+
if (groupOf(declaration2.path) !== section.groupKey) continue;
|
|
5448
|
+
const placed2 = elements.get(declaration2.at);
|
|
5449
|
+
if (placed2) found.set(declaration2.at, placed2);
|
|
5450
|
+
}
|
|
5451
|
+
if (found.size === 0 && literals.size > 0) {
|
|
5452
|
+
for (const site of collectSites(parsed.roots, [...literals])) {
|
|
5453
|
+
if (site.partial || site.where === "script" || site.where === "comment") continue;
|
|
5454
|
+
const placed2 = elements.get(site.attrInsertAt);
|
|
5455
|
+
if (placed2) found.set(site.attrInsertAt, placed2);
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
if (found.size > 0) hits.set(source.path, [...found.values()]);
|
|
5459
|
+
}
|
|
5460
|
+
if (hits.size === 0) {
|
|
5461
|
+
misses.push([
|
|
5462
|
+
section.groupKey,
|
|
5463
|
+
{ code: "NOT_IN_SOURCE", message: `no file renders ${paths.join(", ") || section.groupKey}.` }
|
|
5464
|
+
]);
|
|
5465
|
+
continue;
|
|
5466
|
+
}
|
|
5467
|
+
placed.push({ section, byFile: hits });
|
|
5468
|
+
}
|
|
5469
|
+
return { placed, misses };
|
|
5470
|
+
}
|
|
5471
|
+
var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
|
|
5472
|
+
var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
|
|
5473
|
+
function elementIndex(roots) {
|
|
5474
|
+
const out = /* @__PURE__ */ new Map();
|
|
5475
|
+
const visit = (node, chain) => {
|
|
5476
|
+
if (node.type === "expr") {
|
|
5477
|
+
for (const child of node.children) visit(child, chain);
|
|
5478
|
+
return;
|
|
5479
|
+
}
|
|
5480
|
+
if (node.type !== "element") return;
|
|
5481
|
+
const mine = [...chain, node];
|
|
5482
|
+
out.set(node.attrInsertAt, { node, chain: mine });
|
|
5483
|
+
for (const child of node.children) visit(child, mine);
|
|
5484
|
+
};
|
|
5485
|
+
for (const root of roots) visit(root, []);
|
|
5486
|
+
return out;
|
|
5487
|
+
}
|
|
5488
|
+
function lowestCommonAncestor(elements) {
|
|
5489
|
+
if (elements.length === 0) return null;
|
|
5490
|
+
let common = elements[0].chain;
|
|
5491
|
+
for (const placed of elements.slice(1)) {
|
|
5492
|
+
const shared = [];
|
|
5493
|
+
for (let i = 0; i < Math.min(common.length, placed.chain.length); i++) {
|
|
5494
|
+
if (common[i] !== placed.chain[i]) break;
|
|
5495
|
+
shared.push(common[i]);
|
|
5496
|
+
}
|
|
5497
|
+
common = shared;
|
|
5498
|
+
}
|
|
5499
|
+
return common[common.length - 1] ?? null;
|
|
5500
|
+
}
|
|
5501
|
+
function runOf(placeable, roots) {
|
|
5502
|
+
const parents = placeable.map((r) => parentOf(r.root, roots));
|
|
5503
|
+
if (parents.some((p) => p === void 0)) return null;
|
|
5504
|
+
const [first, ...rest] = parents;
|
|
5505
|
+
if (rest.some((p) => p !== first)) return null;
|
|
5506
|
+
const siblings = (first === null ? roots : first.children).filter(
|
|
5507
|
+
(node) => node.type === "element"
|
|
5508
|
+
);
|
|
5509
|
+
const indices = placeable.map((r) => siblings.indexOf(r.root)).sort((a, b) => a - b);
|
|
5510
|
+
if (indices.some((at) => at < 0)) return null;
|
|
5511
|
+
const contiguous = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
|
|
5512
|
+
return { contiguous };
|
|
5513
|
+
}
|
|
5514
|
+
function parentOf(target, roots) {
|
|
5515
|
+
if (roots.some((node) => node === target)) return null;
|
|
5516
|
+
const search = (nodes, parent) => {
|
|
5517
|
+
for (const node of nodes) {
|
|
5518
|
+
if (node.type === "expr") {
|
|
5519
|
+
const inside = search(node.children, parent);
|
|
5520
|
+
if (inside !== void 0) return inside;
|
|
5521
|
+
continue;
|
|
5522
|
+
}
|
|
5523
|
+
if (node.type !== "element") continue;
|
|
5524
|
+
if (node.children.some((child) => child === target)) return node;
|
|
5525
|
+
const deeper = search(node.children, node);
|
|
5526
|
+
if (deeper !== void 0) return deeper;
|
|
5527
|
+
}
|
|
5528
|
+
return void 0;
|
|
5529
|
+
};
|
|
5530
|
+
return search(roots, null);
|
|
5531
|
+
}
|
|
5532
|
+
function resolveComponent(plan2, section) {
|
|
5533
|
+
const components = plan2.components ?? [];
|
|
5534
|
+
const wanted = section.reuse?.proposedSlug;
|
|
5535
|
+
const byId = section.reuse?.componentId;
|
|
5536
|
+
const entry = (wanted ? components.find((c) => c.proposedSlug === wanted) : void 0) ?? (byId ? components.find((c) => c.componentId === byId) : void 0) ?? onlyOne(components.filter((c) => c.sectionType && c.sectionType === section.sectionType));
|
|
5537
|
+
const family = entry?.name ?? section.sectionType ?? section.groupKey;
|
|
5538
|
+
return { slug: entry?.proposedSlug ?? wanted ?? slugify(family), name: pascal(family) };
|
|
5539
|
+
}
|
|
5540
|
+
var onlyOne = (all) => all.length === 1 ? all[0] : void 0;
|
|
5541
|
+
function pascal(value) {
|
|
5542
|
+
const parts = value.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
5543
|
+
const name = parts.map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
5544
|
+
return /^[A-Za-z]/.test(name) ? name : `Section${name}`;
|
|
5545
|
+
}
|
|
5546
|
+
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
|
|
5547
|
+
function componentFile(dialect, path, fromFile, content, parsed, resolved) {
|
|
5548
|
+
const markup = sliceWithBlockId(content, resolved.root);
|
|
5549
|
+
const groupKey = resolved.section.groupKey;
|
|
5550
|
+
const locals = helperLocals(parsed, dialect, fromFile);
|
|
5551
|
+
const helperNames = ["bcms", "bcmsRows", "bcmsLayout"].flatMap(
|
|
5552
|
+
(name) => locals[name].filter((local) => mentions(markup, local)).map((local) => ({ name, local }))
|
|
5553
|
+
);
|
|
5554
|
+
const helpers = [.../* @__PURE__ */ new Set([...helperNames.map((h) => alias(h.name, h.local)), "bcmsSection"])].sort();
|
|
5555
|
+
const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
|
|
5556
|
+
const snapshot = snapshotOf(parsed, dialect, fromFile, markup);
|
|
5557
|
+
const carry = carryFor({
|
|
5558
|
+
dialect,
|
|
5559
|
+
fromFile,
|
|
5560
|
+
toFile: path,
|
|
5561
|
+
content,
|
|
5562
|
+
parsed,
|
|
5563
|
+
root: resolved.root,
|
|
5564
|
+
provided: [
|
|
5565
|
+
"blockId",
|
|
5566
|
+
"bind",
|
|
5567
|
+
"overrides",
|
|
5568
|
+
"page",
|
|
5569
|
+
...helperNames.map((h) => h.local),
|
|
5570
|
+
...snapshot ? [snapshot] : []
|
|
5571
|
+
],
|
|
5572
|
+
// The helper and the snapshots are what the component declares for itself.
|
|
5573
|
+
skipSpecifier: (repoPath, specifier) => specifier.includes(`${CONTENT_DIR}/`) || repoPath.startsWith(HELPER_PATH[dialect].replace(/\.tsx?$/, ""))
|
|
5574
|
+
});
|
|
5575
|
+
if (carry.free.length > 0) return { content: "", free: carry.free };
|
|
5576
|
+
const carried = [...carry.imports];
|
|
5577
|
+
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
5578
|
+
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
5579
|
+
` : "";
|
|
5580
|
+
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind ?? ${JSON.stringify(groupKey)}, overrides) };` : "";
|
|
5581
|
+
if (dialect === "astro") {
|
|
5582
|
+
return { free: [], content: `---
|
|
5583
|
+
${SECTION_MARKER}
|
|
5584
|
+
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`].join("\n")}
|
|
5585
|
+
|
|
5586
|
+
interface Props {
|
|
5587
|
+
/** The placement's block id. The editor hit-tests \`[data-bcms-block]\` for section chrome. */
|
|
5588
|
+
blockId?: string;
|
|
5589
|
+
/** The page field group this placement renders \u2014 Phase A keeps the copy where P2 put it. */
|
|
5590
|
+
bind?: string;
|
|
5591
|
+
/** The instance's own copy, when an editor gave it any. Wins over the bound group. */
|
|
5592
|
+
overrides?: Record<string, unknown>;
|
|
5593
|
+
/** The page snapshot, so one component can serve every page whose group has this shape. */
|
|
5594
|
+
page?: unknown;
|
|
5595
|
+
}
|
|
5596
|
+
const { blockId, bind, overrides, page } = Astro.props;
|
|
5597
|
+
${carriedDeclarations}${copy}
|
|
5598
|
+
---
|
|
5599
|
+
${markup}
|
|
5600
|
+
` };
|
|
5601
|
+
}
|
|
5602
|
+
return { free: [], content: `${SECTION_MARKER}
|
|
5603
|
+
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
5604
|
+
${carriedDeclarations === "" ? "" : `
|
|
5605
|
+
${carriedDeclarations}`}
|
|
5606
|
+
export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
|
|
5607
|
+
${copy}
|
|
5608
|
+
return (
|
|
5609
|
+
${markup}
|
|
5610
|
+
);
|
|
5611
|
+
}
|
|
5612
|
+
` };
|
|
5613
|
+
}
|
|
5614
|
+
function sliceWithBlockId(content, root) {
|
|
5615
|
+
const slice = new MagicString3(content.slice(root.start, root.end));
|
|
5616
|
+
slice.appendLeft(root.attrInsertAt - root.start, " data-bcms-block={blockId}");
|
|
5617
|
+
return slice.toString();
|
|
5618
|
+
}
|
|
5619
|
+
var alias = (name, local) => name === local ? name : `${name} as ${local}`;
|
|
5620
|
+
var mentions = (markup, name) => new RegExp(`(?<![\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w$])`).test(markup);
|
|
5621
|
+
function snapshotOf(parsed, dialect, fromFile, markup) {
|
|
5622
|
+
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
5623
|
+
if (from.imported !== null) continue;
|
|
5624
|
+
if (!from.specifier.includes(`${CONTENT_DIR}/`) || !from.specifier.endsWith(".json")) continue;
|
|
5625
|
+
if (mentions(markup, local)) return local;
|
|
5626
|
+
}
|
|
5627
|
+
return null;
|
|
5628
|
+
}
|
|
5629
|
+
function sectionsLib(pageSlugs) {
|
|
5630
|
+
const imports = pageSlugs.map(
|
|
5631
|
+
(slug) => `import ${identifierFor(slug)} from "${relativeImport(SECTIONS_LIB, `${CONTENT_DIR}/${slug}.json`)}";`
|
|
5632
|
+
);
|
|
5633
|
+
const table = pageSlugs.map((slug) => ` ${JSON.stringify(slug)}: ${identifierFor(slug)},`).join("\n");
|
|
5634
|
+
return `${REGISTRY_MARKER}
|
|
5635
|
+
// Structure comes from bcms-content/*.json, written into the tree before the build: each page's
|
|
5636
|
+
// \`blocks\` are its ordered section instances, and components.json names which component each one
|
|
5637
|
+
// is. The committed stubs are empty \u2014 a build with them renders the order the repository ships.
|
|
5638
|
+
import bcmsComponents from "${relativeImport(SECTIONS_LIB, `${CONTENT_DIR}/components.json`)}";
|
|
5639
|
+
${imports.join("\n")}
|
|
5640
|
+
|
|
5641
|
+
const SNAPSHOTS: Record<string, unknown> = {
|
|
5642
|
+
${table}
|
|
5643
|
+
};
|
|
5644
|
+
|
|
5645
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
5646
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5647
|
+
|
|
5648
|
+
/** What every extracted section component is handed. */
|
|
5649
|
+
export interface BcmsSectionProps {
|
|
5650
|
+
blockId?: string;
|
|
5651
|
+
bind?: string;
|
|
5652
|
+
overrides?: Record<string, unknown>;
|
|
5653
|
+
page?: unknown;
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
/** One placement on one page: which component, bound to which page field group. */
|
|
5657
|
+
export interface BcmsPlacement {
|
|
5658
|
+
blockId?: string;
|
|
5659
|
+
slug: string;
|
|
5660
|
+
bind?: string;
|
|
5661
|
+
overrides?: Record<string, unknown>;
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5664
|
+
/** The page's own snapshot \u2014 the copy every placement on it reads through \`bind\`. */
|
|
5665
|
+
export function pageSnapshot(pageSlug: string): unknown {
|
|
5666
|
+
return SNAPSHOTS[pageSlug];
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
/** The page's \`component\` blocks, before any of them is matched to a component this repo has. */
|
|
5670
|
+
function componentBlocks(pageSlug: string): Record<string, unknown>[] {
|
|
5671
|
+
const snapshot = SNAPSHOTS[pageSlug];
|
|
5672
|
+
const blocks = isRecord(snapshot) ? snapshot.blocks : undefined;
|
|
5673
|
+
if (!Array.isArray(blocks)) return [];
|
|
5674
|
+
return (blocks as unknown[]).filter(
|
|
5675
|
+
(block): block is Record<string, unknown> => isRecord(block) && block.type === "component",
|
|
5676
|
+
);
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
/**
|
|
5680
|
+
* Has the CMS said anything about this page's structure?
|
|
5681
|
+
*
|
|
5682
|
+
* \u{1F534} THE RAW LIST, NOT THE RESOLVED ONE. "No sections came back" has two causes that must not be
|
|
5683
|
+
* treated alike: the CMS holds nothing yet (fall back to the order this repository ships), or the
|
|
5684
|
+
* CMS holds a list whose components this repository does not have (render nothing \u2014 the page HAS
|
|
5685
|
+
* been arranged, and re-showing the old bands would silently ignore an editor's work).
|
|
5686
|
+
*/
|
|
5687
|
+
export function hasCmsSections(pageSlug: string): boolean {
|
|
5688
|
+
return componentBlocks(pageSlug).length > 0;
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5691
|
+
/**
|
|
5692
|
+
* The page's section instances, in the order the CMS holds them.
|
|
5693
|
+
*
|
|
5694
|
+
* An instance naming a component this repository does not have is SKIPPED, never rendered as a
|
|
5695
|
+
* hole: the CMS is free to add a section type the repo has not shipped yet, and a half-built page
|
|
5696
|
+
* is worse than a page missing one band.
|
|
5697
|
+
*/
|
|
5698
|
+
export function pageSections(pageSlug: string): BcmsPlacement[] {
|
|
5699
|
+
const blocks = componentBlocks(pageSlug);
|
|
5700
|
+
if (blocks.length === 0) return [];
|
|
5701
|
+
|
|
5702
|
+
const slugs = new Map<string, string>();
|
|
5703
|
+
if (Array.isArray(bcmsComponents)) {
|
|
5704
|
+
for (const component of bcmsComponents as unknown[]) {
|
|
5705
|
+
if (!isRecord(component)) continue;
|
|
5706
|
+
const id = component.id ?? component.componentId;
|
|
5707
|
+
const slug = component.slug ?? component.proposedSlug;
|
|
5708
|
+
if (typeof id === "string" && typeof slug === "string") slugs.set(id, slug);
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
const out: BcmsPlacement[] = [];
|
|
5713
|
+
for (const block of blocks) {
|
|
5714
|
+
const props = isRecord(block.props) ? block.props : {};
|
|
5715
|
+
const slug = typeof props.componentId === "string" ? slugs.get(props.componentId) : undefined;
|
|
5716
|
+
if (!slug) continue;
|
|
5717
|
+
out.push({
|
|
5718
|
+
blockId: typeof block.id === "string" ? block.id : undefined,
|
|
5719
|
+
slug,
|
|
5720
|
+
bind: typeof props.bind === "string" ? props.bind : undefined,
|
|
5721
|
+
overrides: isRecord(props.overrides) ? props.overrides : undefined,
|
|
5722
|
+
});
|
|
5723
|
+
}
|
|
5724
|
+
return out;
|
|
5725
|
+
}
|
|
5726
|
+
`;
|
|
5727
|
+
}
|
|
5728
|
+
var identifierFor = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
5729
|
+
function registryFile(dialect, path, written, shipped) {
|
|
5730
|
+
const entries = [...written].sort(([a], [b]) => a.localeCompare(b));
|
|
5731
|
+
const imports = entries.map(([, component]) => {
|
|
5732
|
+
const specifier = relativeImport(path, component.path);
|
|
5733
|
+
return `import ${component.name} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
|
|
5734
|
+
}).join("\n");
|
|
5735
|
+
const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${component.name},`).join("\n");
|
|
5736
|
+
const shippedTable = [...shipped].sort(([a], [b]) => a.localeCompare(b)).map(([page, list]) => ` ${JSON.stringify(page)}: ${JSON.stringify(list)},`).join("\n");
|
|
5737
|
+
const lib = relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "");
|
|
5738
|
+
const registryType = dialect === "astro" ? "Record<string, unknown>" : "Record<string, (props: BcmsSectionProps) => JSX.Element | null>";
|
|
5739
|
+
const shared = `${imports}
|
|
5740
|
+
import { hasCmsSections, pageSections, pageSnapshot, type BcmsPlacement${dialect === "astro" ? "" : ", type BcmsSectionProps"} } from "${lib}";
|
|
5741
|
+
|
|
5742
|
+
/** slug \u2192 the component that renders it. An unknown slug is skipped, never a hole. */
|
|
5743
|
+
const REGISTRY: ${registryType} = {
|
|
5744
|
+
${registry}
|
|
5745
|
+
};
|
|
5746
|
+
|
|
5747
|
+
/**
|
|
5748
|
+
* The order the REPOSITORY ships, per page \u2014 the fallback when the CMS holds no blocks yet.
|
|
5749
|
+
*
|
|
5750
|
+
* \u{1F534} LOAD-BEARING. An empty \`blocks\` means "nothing has been said about this page", not "this
|
|
5751
|
+
* page is empty": it is what a fresh clone, the committed stub and every not-yet-componentised
|
|
5752
|
+
* page all look like. Rendering nothing there would blank the site on the first build after the
|
|
5753
|
+
* codemod ran, with nothing in the diff to explain it.
|
|
5754
|
+
*/
|
|
5755
|
+
const SHIPPED: Record<string, BcmsPlacement[]> = {
|
|
5756
|
+
${shippedTable}
|
|
5757
|
+
};
|
|
5758
|
+
|
|
5759
|
+
/**
|
|
5760
|
+
* \u{1F534} THE FALLBACK IS DECIDED ON THE RAW LIST. A page whose CMS blocks all name components this
|
|
5761
|
+
* repository does not have has still been ARRANGED \u2014 falling back there would put the shipped
|
|
5762
|
+
* bands back on a page an editor deliberately emptied, and no amount of reading the diff would
|
|
5763
|
+
* explain it. Only "the CMS holds nothing" takes the shipped order.
|
|
5764
|
+
*/
|
|
5765
|
+
const placementsFor = (page: string): BcmsPlacement[] =>
|
|
5766
|
+
hasCmsSections(page) ? pageSections(page) : (SHIPPED[page] ?? []);`;
|
|
5767
|
+
if (dialect === "astro") {
|
|
5768
|
+
return `---
|
|
5769
|
+
${REGISTRY_MARKER}
|
|
5770
|
+
${shared}
|
|
5771
|
+
|
|
5772
|
+
interface Props {
|
|
5773
|
+
/** The page slug, as \`bcms-content/<slug>.json\` spells it. */
|
|
5774
|
+
page: string;
|
|
5775
|
+
}
|
|
5776
|
+
const { page } = Astro.props;
|
|
5777
|
+
const placements = placementsFor(page);
|
|
5778
|
+
const snapshot = pageSnapshot(page);
|
|
5779
|
+
---
|
|
5780
|
+
{placements.map((placement) => {
|
|
5781
|
+
const Component = REGISTRY[placement.slug] as never;
|
|
5782
|
+
return Component ? (
|
|
5783
|
+
<Component
|
|
5784
|
+
blockId={placement.blockId}
|
|
5785
|
+
bind={placement.bind}
|
|
5786
|
+
overrides={placement.overrides}
|
|
5787
|
+
page={snapshot}
|
|
5788
|
+
/>
|
|
5789
|
+
) : null;
|
|
5790
|
+
})}
|
|
5791
|
+
`;
|
|
5792
|
+
}
|
|
5793
|
+
return `${REGISTRY_MARKER}
|
|
5794
|
+
${shared}
|
|
5795
|
+
|
|
5796
|
+
export default function Sections({ page }: { page: string }) {
|
|
5797
|
+
const placements = placementsFor(page);
|
|
5798
|
+
const snapshot = pageSnapshot(page);
|
|
5799
|
+
return (
|
|
5800
|
+
<>
|
|
5801
|
+
{placements.map((placement, i) => {
|
|
5802
|
+
const Component = REGISTRY[placement.slug];
|
|
5803
|
+
return Component ? (
|
|
5804
|
+
<Component
|
|
5805
|
+
key={placement.blockId ?? \`\${placement.slug}-\${i}\`}
|
|
5806
|
+
blockId={placement.blockId}
|
|
5807
|
+
bind={placement.bind}
|
|
5808
|
+
overrides={placement.overrides}
|
|
5809
|
+
page={snapshot}
|
|
5810
|
+
/>
|
|
5811
|
+
) : null;
|
|
5812
|
+
})}
|
|
5813
|
+
</>
|
|
5814
|
+
);
|
|
5815
|
+
}
|
|
5816
|
+
`;
|
|
5817
|
+
}
|
|
5818
|
+
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
5819
|
+
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
5820
|
+
const local = names.get("Sections");
|
|
5821
|
+
const source = new MagicString3(content);
|
|
5822
|
+
const first = placeable[0].root;
|
|
5823
|
+
const last = placeable[placeable.length - 1].root;
|
|
5824
|
+
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
5825
|
+
const registry = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
|
|
5826
|
+
const specifier = relativeImport(file, registry);
|
|
5827
|
+
addImports(source, parsed, [
|
|
5828
|
+
`import ${local} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`
|
|
5829
|
+
]);
|
|
5830
|
+
return source.toString();
|
|
5831
|
+
}
|
|
5832
|
+
function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, written) {
|
|
5833
|
+
const wanted = [
|
|
5834
|
+
...new Set(placeable.map((r) => r.slug))
|
|
5835
|
+
].map((slug) => ({ key: slug, name: written.get(slug).name }));
|
|
5836
|
+
const names = allocateNames(topLevelBindings(dialect, parsed, content), [
|
|
5837
|
+
...wanted,
|
|
5838
|
+
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
5839
|
+
]);
|
|
5840
|
+
const snapshot = names.get("pageSnapshot");
|
|
5841
|
+
const source = new MagicString3(content);
|
|
5842
|
+
for (const resolved of placeable) {
|
|
5843
|
+
source.overwrite(
|
|
5844
|
+
resolved.root.start,
|
|
5845
|
+
resolved.root.end,
|
|
5846
|
+
`<${names.get(resolved.slug)} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5849
|
+
const lib = relativeImport(file, SECTIONS_LIB).replace(/\.tsx?$/, "");
|
|
5850
|
+
const imports = [
|
|
5851
|
+
...wanted.map(({ key }) => {
|
|
5852
|
+
const specifier = relativeImport(file, written.get(key).path);
|
|
5853
|
+
return `import ${names.get(key)} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
|
|
5854
|
+
}),
|
|
5855
|
+
`import { pageSnapshot${snapshot === "pageSnapshot" ? "" : ` as ${snapshot}`} } from "${lib}";`
|
|
5856
|
+
];
|
|
5857
|
+
addImports(source, parsed, imports);
|
|
5858
|
+
return source.toString();
|
|
5859
|
+
}
|
|
5860
|
+
function addImports(source, parsed, lines) {
|
|
5861
|
+
const at = parsed.importAt;
|
|
5862
|
+
if (!at || lines.length === 0) return;
|
|
5863
|
+
if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${lines.join("\n")}${at.wrap.after}`);
|
|
5864
|
+
else if (at.at === 0) source.appendLeft(0, `${lines.join("\n")}
|
|
5865
|
+
`);
|
|
5866
|
+
else source.appendLeft(at.at, `
|
|
5867
|
+
${lines.join("\n")}`);
|
|
5868
|
+
}
|
|
5869
|
+
var isGenerated = (content) => content.includes(SECTION_MARKER) || content.includes(REGISTRY_MARKER);
|
|
5870
|
+
function emitHelper(out, contents, version, overwrite) {
|
|
5871
|
+
const path = HELPER_PATH.astro;
|
|
5872
|
+
const source = helperSource("astro");
|
|
5873
|
+
const existing = contents.get(path);
|
|
5874
|
+
if (existing === void 0) {
|
|
5875
|
+
addFile(out, contents, { path, operation: "add", content: source });
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
if (version === null && overwrite !== true) return;
|
|
5879
|
+
if (version === null || version < HELPER_VERSION) {
|
|
5880
|
+
addFile(out, contents, { path, operation: "modify", content: source });
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
function emitStubs(out, contents, pageSlugs) {
|
|
5884
|
+
const components = `${CONTENT_DIR}/components.json`;
|
|
5885
|
+
if (!contents.has(components)) {
|
|
5886
|
+
addFile(out, contents, { path: components, operation: "add", content: "[]" });
|
|
5887
|
+
}
|
|
5888
|
+
for (const slug of pageSlugs) {
|
|
5889
|
+
const path = `${CONTENT_DIR}/${slug}.json`;
|
|
5890
|
+
const existing = contents.get(path);
|
|
5891
|
+
let held = {};
|
|
5892
|
+
if (existing !== void 0) {
|
|
5893
|
+
try {
|
|
5894
|
+
const parsed = JSON.parse(existing);
|
|
5895
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
|
|
5896
|
+
held = parsed;
|
|
5897
|
+
} catch {
|
|
5898
|
+
continue;
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
if (Array.isArray(held.blocks)) continue;
|
|
5902
|
+
addFile(out, contents, {
|
|
5903
|
+
path,
|
|
5904
|
+
operation: existing === void 0 ? "add" : "modify",
|
|
5905
|
+
content: `${JSON.stringify({ ...held, blocks: [] }, null, 2)}
|
|
5906
|
+
`
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
function readComponentizePlan(value) {
|
|
5911
|
+
return readPlan(value);
|
|
5912
|
+
}
|
|
5913
|
+
|
|
4600
5914
|
// src/index.ts
|
|
4601
5915
|
var ConvertError = class extends Error {
|
|
4602
5916
|
constructor(code, message) {
|
|
@@ -4608,7 +5922,7 @@ var ConvertError = class extends Error {
|
|
|
4608
5922
|
};
|
|
4609
5923
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
4610
5924
|
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4611
|
-
var
|
|
5925
|
+
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
4612
5926
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
4613
5927
|
const miss = { ok: false, reason: "PROP_TARGET_NOT_FOUND", edits: [] };
|
|
4614
5928
|
if (hop > MAX_HOPS) return { ok: false, reason: "PROP_DRILLED_DEEP", edits: [] };
|
|
@@ -4656,7 +5970,8 @@ async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
|
4656
5970
|
if (found.propsType) at(found.propsType.at, found.propsType.text);
|
|
4657
5971
|
return { ok: true, edits: [{ file, splices }] };
|
|
4658
5972
|
}
|
|
4659
|
-
async function convertSources(
|
|
5973
|
+
async function convertSources(briefIn, sources, options) {
|
|
5974
|
+
const brief = readBrief(briefIn);
|
|
4660
5975
|
const digest = briefDigest(brief);
|
|
4661
5976
|
const targets = /* @__PURE__ */ new Map();
|
|
4662
5977
|
for (const page of brief.pages) {
|
|
@@ -4791,7 +6106,7 @@ async function convertSources(brief, sources, options) {
|
|
|
4791
6106
|
else rewritten++;
|
|
4792
6107
|
}
|
|
4793
6108
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
4794
|
-
const source = new
|
|
6109
|
+
const source = new MagicString4(contents.get(file));
|
|
4795
6110
|
const seen = /* @__PURE__ */ new Set();
|
|
4796
6111
|
for (const splice of splices) {
|
|
4797
6112
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -4811,7 +6126,7 @@ async function convertSources(brief, sources, options) {
|
|
|
4811
6126
|
}
|
|
4812
6127
|
}
|
|
4813
6128
|
const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
|
|
4814
|
-
|
|
6129
|
+
emitStubs2(dialects, snapshotSlugs, contents, outFiles);
|
|
4815
6130
|
const receipt = assertReceipt({
|
|
4816
6131
|
briefDigest: digest,
|
|
4817
6132
|
paths: { declared: targets.size, rewritten, alreadyDeclared, pending },
|
|
@@ -4926,7 +6241,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
4926
6241
|
{ key: "helper:bcmsRows", name: "bcmsRows" },
|
|
4927
6242
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
4928
6243
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
4929
|
-
...slugs.map((slug) => ({ key: `slug:${slug}`, name:
|
|
6244
|
+
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
4930
6245
|
].filter((entry) => !preset.has(entry.key));
|
|
4931
6246
|
const names = new Map([...preset, ...allocateNames(taken, wanted)]);
|
|
4932
6247
|
const snapshotName = (slug) => names.get(`slug:${slug}`);
|
|
@@ -5318,7 +6633,7 @@ function emitHelpers(dialects, contents, out, overwrite) {
|
|
|
5318
6633
|
}
|
|
5319
6634
|
return { helpers, helperUpgraded };
|
|
5320
6635
|
}
|
|
5321
|
-
function
|
|
6636
|
+
function emitStubs2(dialects, slugs, contents, out) {
|
|
5322
6637
|
if (![...dialects].some((d) => DIALECT_RULES[d].text)) return;
|
|
5323
6638
|
const paths = [...[...slugs].map((slug) => `${CONTENT_DIR}/${slug}.json`), `${CONTENT_DIR}/layout.json`];
|
|
5324
6639
|
for (const path of paths.sort()) {
|
|
@@ -5327,21 +6642,30 @@ function emitStubs(dialects, slugs, contents, out) {
|
|
|
5327
6642
|
}
|
|
5328
6643
|
}
|
|
5329
6644
|
export {
|
|
6645
|
+
COMPONENT_DIR,
|
|
5330
6646
|
CONTENT_DIR,
|
|
5331
6647
|
ConvertError,
|
|
5332
6648
|
DIALECT_RULES,
|
|
5333
6649
|
HELPER_PATH,
|
|
5334
6650
|
HELPER_VERSION,
|
|
5335
6651
|
PARSE_FILE,
|
|
6652
|
+
REGISTRY_MARKER,
|
|
5336
6653
|
ReceiptInvariantError,
|
|
6654
|
+
SECTIONS_LIB,
|
|
6655
|
+
SECTION_MARKER,
|
|
5337
6656
|
SKIP_DIRS,
|
|
5338
6657
|
SKIP_FILES,
|
|
5339
6658
|
SOURCE_EXTENSIONS,
|
|
5340
6659
|
STUB_CONTENT,
|
|
6660
|
+
SectionInvariantError,
|
|
6661
|
+
ValidationError,
|
|
5341
6662
|
aliasesFrom,
|
|
5342
6663
|
assertReceipt,
|
|
6664
|
+
assertSections,
|
|
5343
6665
|
attrsFor,
|
|
5344
6666
|
briefDigest,
|
|
6667
|
+
carryFor,
|
|
6668
|
+
componentizeSources,
|
|
5345
6669
|
convertSources,
|
|
5346
6670
|
convertedHere,
|
|
5347
6671
|
coverageOf,
|
|
@@ -5353,17 +6677,23 @@ export {
|
|
|
5353
6677
|
flat,
|
|
5354
6678
|
helperSource,
|
|
5355
6679
|
importsIn,
|
|
6680
|
+
isDynamicRoute,
|
|
5356
6681
|
isKnownHelper,
|
|
5357
6682
|
isSourceCandidate,
|
|
5358
6683
|
locate,
|
|
5359
6684
|
overlapping,
|
|
6685
|
+
pageFilesFor,
|
|
5360
6686
|
parseFile4 as parseFile,
|
|
5361
6687
|
propsAttribute,
|
|
6688
|
+
readBrief,
|
|
6689
|
+
readComponentizePlan,
|
|
5362
6690
|
readDeclarations,
|
|
5363
6691
|
readExpr,
|
|
6692
|
+
readPlan,
|
|
5364
6693
|
relativeImport,
|
|
5365
6694
|
resolveSpecifier,
|
|
5366
6695
|
rewriteFile,
|
|
6696
|
+
routeOfFile,
|
|
5367
6697
|
stripTags,
|
|
5368
6698
|
walkAst
|
|
5369
6699
|
};
|