@bettercms-ai/convert 0.3.0 → 0.4.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 +232 -0
- package/README.md +97 -16
- package/dist/cli.js +582 -68
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +51 -4
- package/dist/index.js +564 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -134,7 +134,7 @@ function propsAttribute(props) {
|
|
|
134
134
|
|
|
135
135
|
// src/helper.ts
|
|
136
136
|
import { createHash as createHash2 } from "crypto";
|
|
137
|
-
var HELPER_VERSION =
|
|
137
|
+
var HELPER_VERSION = 4;
|
|
138
138
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
139
139
|
var HELPER_PATH = Object.fromEntries(
|
|
140
140
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -217,32 +217,97 @@ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: s
|
|
|
217
217
|
/**
|
|
218
218
|
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
219
219
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
220
|
+
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
221
|
+
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
222
|
+
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
223
|
+
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
224
|
+
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
225
|
+
*
|
|
226
|
+
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
227
|
+
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
228
|
+
*
|
|
229
|
+
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
230
|
+
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
231
|
+
* committed \`{}\` stub.
|
|
224
232
|
*/
|
|
225
233
|
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
226
|
-
// \u{1F534} AN EMPTY OBJECT IS AN ANSWER. \`overrides: {}\` is an instance an editor cleared, and
|
|
227
|
-
// "\`??\` unless it happens to be empty" quietly put the page's copy back into it.
|
|
228
|
-
if (isRecord(overrides)) return overrides;
|
|
229
234
|
const group = bind ? readPath(page, bind) : undefined;
|
|
230
|
-
|
|
235
|
+
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
236
|
+
if (!isRecord(overrides)) return base;
|
|
237
|
+
for (const key of Object.keys(overrides)) {
|
|
238
|
+
const value = overrides[key];
|
|
239
|
+
if (value === undefined || value === null) continue;
|
|
240
|
+
/**
|
|
241
|
+
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
242
|
+
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
243
|
+
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
244
|
+
*/
|
|
245
|
+
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
246
|
+
}
|
|
247
|
+
return base;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
252
|
+
*
|
|
253
|
+
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
254
|
+
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
255
|
+
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
256
|
+
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
257
|
+
* page form and the coverage meter already know.
|
|
258
|
+
*
|
|
259
|
+
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
260
|
+
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
261
|
+
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
262
|
+
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
263
|
+
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
264
|
+
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
265
|
+
* the placement's own group goes in.
|
|
266
|
+
*
|
|
267
|
+
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
268
|
+
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
269
|
+
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
270
|
+
*/
|
|
271
|
+
export function bcmsFields(
|
|
272
|
+
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
273
|
+
pathPrefix: string,
|
|
274
|
+
): (path: string) => string {
|
|
275
|
+
const id = placement.blockId;
|
|
276
|
+
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
277
|
+
/**
|
|
278
|
+
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
279
|
+
*
|
|
280
|
+
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
281
|
+
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
282
|
+
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
283
|
+
* so prepending one would invent an address rather than correct it.
|
|
284
|
+
*/
|
|
285
|
+
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
286
|
+
return (path: string): string => {
|
|
287
|
+
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
288
|
+
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
289
|
+
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
290
|
+
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
291
|
+
return lane + path.slice(pathPrefix.length);
|
|
292
|
+
};
|
|
231
293
|
}
|
|
232
294
|
`;
|
|
233
295
|
}
|
|
234
296
|
var HISTORICAL = {
|
|
235
297
|
"src/bcms-content.ts": {
|
|
236
298
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
237
|
-
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
299
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
300
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
238
301
|
},
|
|
239
302
|
"src/lib/bcms-content.ts": {
|
|
240
303
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
241
|
-
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002"
|
|
304
|
+
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
305
|
+
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e"
|
|
242
306
|
},
|
|
243
307
|
"composables/bcms-content.ts": {
|
|
244
308
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
245
|
-
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
309
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
310
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
246
311
|
}
|
|
247
312
|
};
|
|
248
313
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4839,6 +4904,7 @@ function readPlan(value) {
|
|
|
4839
4904
|
const section = check.record(held, at2);
|
|
4840
4905
|
check.segment(section.groupKey, `${at2}.groupKey`);
|
|
4841
4906
|
check.optionalString(section.sectionType, `${at2}.sectionType`);
|
|
4907
|
+
check.optionalString(section.copy, `${at2}.copy`);
|
|
4842
4908
|
if (section.fields !== void 0) {
|
|
4843
4909
|
for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
|
|
4844
4910
|
const shape = check.record(field, `${at2}.fields[${f}]`);
|
|
@@ -5228,7 +5294,8 @@ function assertSections(total, sections) {
|
|
|
5228
5294
|
}
|
|
5229
5295
|
return sections;
|
|
5230
5296
|
}
|
|
5231
|
-
var SECTION_MARKER = "// @bettercms-ai/convert section
|
|
5297
|
+
var SECTION_MARKER = "// @bettercms-ai/convert section v2";
|
|
5298
|
+
var SECTION_MARKERS = [SECTION_MARKER, "// @bettercms-ai/convert section v1"];
|
|
5232
5299
|
var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
|
|
5233
5300
|
var COMPONENT_DIR = "src/components/bcms";
|
|
5234
5301
|
var SECTIONS_LIB = "src/lib/bcms-sections.ts";
|
|
@@ -5250,12 +5317,53 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5250
5317
|
let registered = 0;
|
|
5251
5318
|
let planPending = 0;
|
|
5252
5319
|
let total = 0;
|
|
5320
|
+
const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
|
|
5253
5321
|
const brief = readBrief(briefIn);
|
|
5254
5322
|
const plan2 = readPlan(planIn);
|
|
5323
|
+
const copy = options?.copy ?? "bind";
|
|
5324
|
+
const instanceOwned = (section) => section.copy === "instance";
|
|
5325
|
+
const notes = unreadableConfigs(sources).map(
|
|
5326
|
+
(path) => ({
|
|
5327
|
+
code: "TSCONFIG_UNREADABLE",
|
|
5328
|
+
file: path,
|
|
5329
|
+
message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
5330
|
+
})
|
|
5331
|
+
);
|
|
5332
|
+
const refuseRun = (reason, message) => {
|
|
5333
|
+
for (const page of plan2.pages ?? []) {
|
|
5334
|
+
for (const section of page.sections ?? []) {
|
|
5335
|
+
if (section.pending) {
|
|
5336
|
+
planPending++;
|
|
5337
|
+
continue;
|
|
5338
|
+
}
|
|
5339
|
+
total++;
|
|
5340
|
+
pend(page.route, section.groupKey, reason, message);
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
return {
|
|
5344
|
+
files: [],
|
|
5345
|
+
receipt: {
|
|
5346
|
+
planDigest: plan2.digest ?? "",
|
|
5347
|
+
copy,
|
|
5348
|
+
sections: assertSections(total, { extracted: 0, inlineOnly: 0, alreadyExtracted: 0, registered: 0, pending }),
|
|
5349
|
+
components: [],
|
|
5350
|
+
planPending,
|
|
5351
|
+
notes
|
|
5352
|
+
}
|
|
5353
|
+
};
|
|
5354
|
+
};
|
|
5355
|
+
const movesCopy = (plan2.pages ?? []).some(
|
|
5356
|
+
(page) => (page.sections ?? []).some((section) => !section.pending && instanceOwned(section))
|
|
5357
|
+
);
|
|
5358
|
+
if (copy === "instance" && !movesCopy) {
|
|
5359
|
+
return refuseRun(
|
|
5360
|
+
"COPY_MODE_MISMATCH",
|
|
5361
|
+
'this run was asked for --copy instance, but no placement in the plan carries copy: "instance" \u2014 run componentize_sections { copy: "instance" } on the server first, then fetch the plan again.'
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5255
5364
|
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
5256
5365
|
const fileSet = new Set(contents.keys());
|
|
5257
5366
|
const aliases = aliasesFrom(sources);
|
|
5258
|
-
const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
|
|
5259
5367
|
const trees = /* @__PURE__ */ new Map();
|
|
5260
5368
|
const treeOf = async (file) => {
|
|
5261
5369
|
if (trees.has(file)) return trees.get(file);
|
|
@@ -5269,7 +5377,9 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5269
5377
|
const helperPath = HELPER_PATH.astro;
|
|
5270
5378
|
const helperExisting = contents.get(helperPath);
|
|
5271
5379
|
const helperVersion = helperExisting === void 0 ? null : isKnownHelper(helperExisting, "astro");
|
|
5272
|
-
|
|
5380
|
+
if (helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true) {
|
|
5381
|
+
return refuseRun("HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
|
|
5382
|
+
}
|
|
5273
5383
|
const outFiles = /* @__PURE__ */ new Map();
|
|
5274
5384
|
const written = /* @__PURE__ */ new Map();
|
|
5275
5385
|
const shipped = /* @__PURE__ */ new Map();
|
|
@@ -5279,6 +5389,29 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5279
5389
|
return held;
|
|
5280
5390
|
};
|
|
5281
5391
|
const fresh = /* @__PURE__ */ new Set();
|
|
5392
|
+
const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
|
|
5393
|
+
const placementOf = (slug, section) => {
|
|
5394
|
+
const bind = bindOf(section);
|
|
5395
|
+
return bind === void 0 ? { slug } : { slug, bind };
|
|
5396
|
+
};
|
|
5397
|
+
const rebind = (dialect, pageSlug, all, usable) => {
|
|
5398
|
+
if (dialect === null || !EXTRACTABLE.includes(dialect)) return;
|
|
5399
|
+
const order = shipped.get(dialect);
|
|
5400
|
+
const held = order?.get(pageSlug);
|
|
5401
|
+
if (!order || !held) return;
|
|
5402
|
+
const queues = /* @__PURE__ */ new Map();
|
|
5403
|
+
for (const section of all) {
|
|
5404
|
+
const slug = resolveComponent(plan2, section).slug;
|
|
5405
|
+
queues.set(slug, [...queues.get(slug) ?? [], section]);
|
|
5406
|
+
}
|
|
5407
|
+
order.set(
|
|
5408
|
+
pageSlug,
|
|
5409
|
+
held.map((placement) => {
|
|
5410
|
+
const section = queues.get(placement.slug)?.shift();
|
|
5411
|
+
return section && usable.has(section) ? placementOf(placement.slug, section) : placement;
|
|
5412
|
+
})
|
|
5413
|
+
);
|
|
5414
|
+
};
|
|
5282
5415
|
const registryConflicts = [];
|
|
5283
5416
|
const registryUnreadable = [];
|
|
5284
5417
|
for (const dialect of EXTRACTABLE) {
|
|
@@ -5304,6 +5437,11 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5304
5437
|
if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
|
|
5305
5438
|
const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
|
|
5306
5439
|
const registryUnread = registryUnreadable.length > 0;
|
|
5440
|
+
const migrated = registryBlocked || registryUnread ? { files: [], refused: /* @__PURE__ */ new Map() } : await migrateSections(plan2, contents, written, fileSet, aliases);
|
|
5441
|
+
for (const file of migrated.files) {
|
|
5442
|
+
addFile(outFiles, contents, file);
|
|
5443
|
+
contents.set(file.path, file.content);
|
|
5444
|
+
}
|
|
5307
5445
|
for (const page of plan2.pages ?? []) {
|
|
5308
5446
|
const sections2 = (page.sections ?? []).filter((section) => {
|
|
5309
5447
|
if (section.pending) {
|
|
@@ -5314,23 +5452,33 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5314
5452
|
});
|
|
5315
5453
|
total += sections2.length;
|
|
5316
5454
|
if (sections2.length === 0) continue;
|
|
5455
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
5456
|
+
for (const section of sections2) {
|
|
5457
|
+
const prefix = propPrefix(section);
|
|
5458
|
+
if (prefix === null) {
|
|
5459
|
+
pend(
|
|
5460
|
+
page.route,
|
|
5461
|
+
section.groupKey,
|
|
5462
|
+
"PROP_MAP_INCONSISTENT",
|
|
5463
|
+
"the plan's fields do not spell every path as one common prefix plus its key, so a page path cannot be turned into a prop key."
|
|
5464
|
+
);
|
|
5465
|
+
continue;
|
|
5466
|
+
}
|
|
5467
|
+
prefixes.set(section, prefix);
|
|
5468
|
+
}
|
|
5469
|
+
const usable = sections2.filter((section) => prefixes.has(section));
|
|
5470
|
+
if (usable.length === 0) continue;
|
|
5317
5471
|
const pendingBefore = pending.length;
|
|
5318
5472
|
const briefPage = brief.pages.find((p) => p.route === page.route);
|
|
5319
5473
|
const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
|
|
5320
|
-
if (helperBlocked) {
|
|
5321
|
-
for (const section of sections2) {
|
|
5322
|
-
pend(page.route, section.groupKey, "HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
|
|
5323
|
-
}
|
|
5324
|
-
continue;
|
|
5325
|
-
}
|
|
5326
5474
|
if (registryBlocked) {
|
|
5327
|
-
for (const section of
|
|
5475
|
+
for (const section of usable) {
|
|
5328
5476
|
pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
|
|
5329
5477
|
}
|
|
5330
5478
|
continue;
|
|
5331
5479
|
}
|
|
5332
5480
|
if (registryUnread) {
|
|
5333
|
-
for (const section of
|
|
5481
|
+
for (const section of usable) {
|
|
5334
5482
|
pend(
|
|
5335
5483
|
page.route,
|
|
5336
5484
|
section.groupKey,
|
|
@@ -5342,30 +5490,43 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5342
5490
|
}
|
|
5343
5491
|
const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
|
|
5344
5492
|
if (!snapshotUsable(contents.get(snapshotPath))) {
|
|
5345
|
-
for (const section of
|
|
5493
|
+
for (const section of usable) {
|
|
5346
5494
|
pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
|
|
5347
5495
|
}
|
|
5348
5496
|
continue;
|
|
5349
5497
|
}
|
|
5350
5498
|
const pageFiles = pageFilesFor(page.route, contents.keys());
|
|
5351
5499
|
if (pageFiles.length > 1) {
|
|
5352
|
-
for (const section of
|
|
5500
|
+
for (const section of usable) {
|
|
5353
5501
|
pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
|
|
5354
5502
|
}
|
|
5355
5503
|
continue;
|
|
5356
5504
|
}
|
|
5357
5505
|
const file = pageFiles[0];
|
|
5358
5506
|
if (file === void 0) {
|
|
5359
|
-
for (const section of
|
|
5507
|
+
for (const section of usable) {
|
|
5360
5508
|
pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
|
|
5361
5509
|
}
|
|
5362
5510
|
continue;
|
|
5363
5511
|
}
|
|
5364
|
-
const
|
|
5512
|
+
const pageDialect = dialectOf(file, contents.get(file) ?? "");
|
|
5513
|
+
const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, usable));
|
|
5514
|
+
let stuck = 0;
|
|
5515
|
+
for (const section of usable) {
|
|
5516
|
+
if (!done.has(section.groupKey)) continue;
|
|
5517
|
+
const why = pageDialect ? migrated.refused.get(refusalKey(pageDialect, resolveComponent(plan2, section).slug)) : void 0;
|
|
5518
|
+
if (why === void 0) continue;
|
|
5519
|
+
done.delete(section.groupKey);
|
|
5520
|
+
stuck++;
|
|
5521
|
+
pend(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
|
|
5522
|
+
}
|
|
5365
5523
|
alreadyExtracted += done.size;
|
|
5366
|
-
if (done.size ===
|
|
5524
|
+
if (done.size + stuck === usable.length) {
|
|
5525
|
+
rebind(pageDialect, pageSlug, page.sections ?? [], new Set(usable));
|
|
5526
|
+
continue;
|
|
5527
|
+
}
|
|
5367
5528
|
const found = await locateSections(
|
|
5368
|
-
|
|
5529
|
+
usable.filter((section) => !done.has(section.groupKey)),
|
|
5369
5530
|
briefPage?.paths ?? [],
|
|
5370
5531
|
sources,
|
|
5371
5532
|
treeOf
|
|
@@ -5480,7 +5641,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5480
5641
|
registered++;
|
|
5481
5642
|
registeredHere.push({
|
|
5482
5643
|
slug: component.slug,
|
|
5483
|
-
|
|
5644
|
+
section: held.section,
|
|
5484
5645
|
call: callSiteOf(tree, member ? `${binding.local}.${member}` : held.call)
|
|
5485
5646
|
});
|
|
5486
5647
|
}
|
|
@@ -5492,7 +5653,13 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5492
5653
|
continue;
|
|
5493
5654
|
}
|
|
5494
5655
|
const component = resolveComponent(plan2, placed.section);
|
|
5495
|
-
roots.push({
|
|
5656
|
+
roots.push({
|
|
5657
|
+
section: placed.section,
|
|
5658
|
+
route: page.route,
|
|
5659
|
+
root,
|
|
5660
|
+
pathPrefix: prefixes.get(placed.section),
|
|
5661
|
+
...component
|
|
5662
|
+
});
|
|
5496
5663
|
}
|
|
5497
5664
|
const nested = /* @__PURE__ */ new Set();
|
|
5498
5665
|
for (const a of roots) {
|
|
@@ -5519,7 +5686,25 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5519
5686
|
placeable.push(resolved);
|
|
5520
5687
|
continue;
|
|
5521
5688
|
}
|
|
5522
|
-
const built = componentFile(dialect, path, file, content, parsed, resolved);
|
|
5689
|
+
const built = componentFile(dialect, path, file, content, parsed, resolved, resolved.pathPrefix);
|
|
5690
|
+
if (built.unconvertible.length > 0) {
|
|
5691
|
+
pend(
|
|
5692
|
+
page.route,
|
|
5693
|
+
resolved.section.groupKey,
|
|
5694
|
+
"PROP_MAP_INCONSISTENT",
|
|
5695
|
+
`its fields map to the prefix "${resolved.pathPrefix}", which ${[...new Set(built.unconvertible)].slice(0, 3).join(", ")} cannot be converted through \u2014 so those elements would stay on the page lane whatever the placement says.`
|
|
5696
|
+
);
|
|
5697
|
+
continue;
|
|
5698
|
+
}
|
|
5699
|
+
if (built.duplicates.length > 0) {
|
|
5700
|
+
pend(
|
|
5701
|
+
page.route,
|
|
5702
|
+
resolved.section.groupKey,
|
|
5703
|
+
"DUPLICATE_FIELD_BINDING",
|
|
5704
|
+
`${built.duplicates.join(", ")} carries more than one data-bcms-field, so there is no single address to rewrite.`
|
|
5705
|
+
);
|
|
5706
|
+
continue;
|
|
5707
|
+
}
|
|
5523
5708
|
if (built.free.length > 0) {
|
|
5524
5709
|
pend(
|
|
5525
5710
|
page.route,
|
|
@@ -5540,8 +5725,8 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5540
5725
|
}
|
|
5541
5726
|
if (placeable.length === 0 && registeredHere.length === 0) continue;
|
|
5542
5727
|
const spans = [
|
|
5543
|
-
...placeable.map((r) => ({ slug: r.slug, bind: r.section
|
|
5544
|
-
...registeredHere.map((r) => ({ slug: r.slug, bind: r.
|
|
5728
|
+
...placeable.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.root })),
|
|
5729
|
+
...registeredHere.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.call }))
|
|
5545
5730
|
].sort((a, b) => (a.root?.start ?? Number.MAX_SAFE_INTEGER) - (b.root?.start ?? Number.MAX_SAFE_INTEGER));
|
|
5546
5731
|
const known = spans.filter((s) => s.root !== null);
|
|
5547
5732
|
const layout = runOf(known, parsed.roots, content);
|
|
@@ -5557,7 +5742,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5557
5742
|
shipped.set(dialect, order);
|
|
5558
5743
|
order.set(
|
|
5559
5744
|
pageSlug,
|
|
5560
|
-
spans.map((s) =>
|
|
5745
|
+
spans.map((s) => s.bind === void 0 ? { slug: s.slug } : { slug: s.slug, bind: s.bind })
|
|
5561
5746
|
);
|
|
5562
5747
|
if (useRun) {
|
|
5563
5748
|
addFile(outFiles, contents, {
|
|
@@ -5566,13 +5751,25 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5566
5751
|
content: splicePageRun(file, content, dialect, parsed, known, pageSlug)
|
|
5567
5752
|
});
|
|
5568
5753
|
extracted += placeable.length;
|
|
5569
|
-
} else
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5754
|
+
} else {
|
|
5755
|
+
const inline = placeable.filter((resolved) => !instanceOwned(resolved.section));
|
|
5756
|
+
for (const resolved of placeable) {
|
|
5757
|
+
if (inline.includes(resolved)) continue;
|
|
5758
|
+
pend(
|
|
5759
|
+
page.route,
|
|
5760
|
+
resolved.section.groupKey,
|
|
5761
|
+
"INLINE_NEEDS_INSTANCE",
|
|
5762
|
+
`${file} cannot be replaced by one <Sections page="${pageSlug}" />, and an in-place call site carries no blockId or overrides \u2014 so this placement's own copy could not be rendered or addressed.`
|
|
5763
|
+
);
|
|
5764
|
+
}
|
|
5765
|
+
if (inline.length > 0) {
|
|
5766
|
+
addFile(outFiles, contents, {
|
|
5767
|
+
path: file,
|
|
5768
|
+
operation: "modify",
|
|
5769
|
+
content: splicePageInline(file, content, dialect, parsed, inline, pageSlug, extractable)
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
inlineOnly += inline.length;
|
|
5576
5773
|
}
|
|
5577
5774
|
}
|
|
5578
5775
|
const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
|
|
@@ -5600,17 +5797,12 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5600
5797
|
files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
5601
5798
|
receipt: {
|
|
5602
5799
|
planDigest: plan2.digest ?? "",
|
|
5800
|
+
copy,
|
|
5603
5801
|
sections,
|
|
5604
5802
|
// What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
|
|
5605
5803
|
components: [...fresh].sort(),
|
|
5606
5804
|
planPending,
|
|
5607
|
-
notes
|
|
5608
|
-
(path) => ({
|
|
5609
|
-
code: "TSCONFIG_UNREADABLE",
|
|
5610
|
-
file: path,
|
|
5611
|
-
message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
5612
|
-
})
|
|
5613
|
-
)
|
|
5805
|
+
notes
|
|
5614
5806
|
}
|
|
5615
5807
|
};
|
|
5616
5808
|
}
|
|
@@ -5633,7 +5825,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5633
5825
|
if (!from.specifier.startsWith(".")) continue;
|
|
5634
5826
|
const resolved = resolveSpecifier(path, from.specifier, files, aliases);
|
|
5635
5827
|
if (resolved !== null) {
|
|
5636
|
-
imports.set(local, { name: local, path: resolved, kind: from.kind, imported: from.imported });
|
|
5828
|
+
imports.set(local, { name: local, path: preferDialect(resolved, dialect, files), kind: from.kind, imported: from.imported });
|
|
5637
5829
|
}
|
|
5638
5830
|
}
|
|
5639
5831
|
const registry = blockOf(content, "REGISTRY");
|
|
@@ -5642,6 +5834,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5642
5834
|
const registryRows = propertiesOf(registry);
|
|
5643
5835
|
const shippedRows = propertiesOf(table);
|
|
5644
5836
|
if (registryRows === null || shippedRows === null) return null;
|
|
5837
|
+
if (registryRows.length === 0) return null;
|
|
5645
5838
|
for (const row of registryRows) {
|
|
5646
5839
|
const named = row.value.match(/^([A-Za-z_$][\w$]*)(?:\.([A-Za-z_$][\w$]*))?$/);
|
|
5647
5840
|
const entry = named ? imports.get(named[1]) : void 0;
|
|
@@ -5651,7 +5844,8 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5651
5844
|
for (const row of shippedRows) {
|
|
5652
5845
|
try {
|
|
5653
5846
|
const placements = JSON.parse(row.value);
|
|
5654
|
-
if (!Array.isArray(placements)) return null;
|
|
5847
|
+
if (!Array.isArray(placements) || placements.length === 0 || !placements.every(isPlacement)) return null;
|
|
5848
|
+
if (placements.some((placement) => !components.has(placement.slug))) return null;
|
|
5655
5849
|
shipped.set(row.key, placements);
|
|
5656
5850
|
} catch {
|
|
5657
5851
|
return null;
|
|
@@ -5659,6 +5853,18 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5659
5853
|
}
|
|
5660
5854
|
return { components, shipped };
|
|
5661
5855
|
}
|
|
5856
|
+
function preferDialect(resolved, dialect, files) {
|
|
5857
|
+
const wanted = dialect === "astro" ? [".astro"] : [".tsx", ".jsx"];
|
|
5858
|
+
if (wanted.some((extension) => resolved.endsWith(extension))) return resolved;
|
|
5859
|
+
const base = resolved.replace(/\.[^./]+$/, "");
|
|
5860
|
+
return wanted.map((extension) => `${base}${extension}`).find((candidate) => files.has(candidate)) ?? resolved;
|
|
5861
|
+
}
|
|
5862
|
+
function isPlacement(value) {
|
|
5863
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
5864
|
+
const held = value;
|
|
5865
|
+
if (typeof held.slug !== "string") return false;
|
|
5866
|
+
return ["bind", "blockId"].every((key) => held[key] === void 0 || typeof held[key] === "string");
|
|
5867
|
+
}
|
|
5662
5868
|
function blockOf(content, name) {
|
|
5663
5869
|
const declaration2 = content.indexOf(`const ${name}`);
|
|
5664
5870
|
if (declaration2 < 0) return null;
|
|
@@ -5905,14 +6111,35 @@ function pascal(value) {
|
|
|
5905
6111
|
return /^[A-Za-z]/.test(name) ? name : `Section${name}`;
|
|
5906
6112
|
}
|
|
5907
6113
|
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
|
|
5908
|
-
function componentFile(dialect, path, fromFile, content, parsed, resolved) {
|
|
5909
|
-
const
|
|
6114
|
+
function componentFile(dialect, path, fromFile, content, parsed, resolved, pathPrefix) {
|
|
6115
|
+
const addresses = fieldAddresses(content, elementsUnder(resolved.root));
|
|
6116
|
+
const unconvertible = [
|
|
6117
|
+
...addresses.heads.filter((address) => !address.startsWith(pathPrefix)),
|
|
6118
|
+
...addresses.opaque
|
|
6119
|
+
];
|
|
6120
|
+
if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
|
|
6121
|
+
const cut = content.slice(resolved.root.start, resolved.root.end);
|
|
6122
|
+
const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
|
|
6123
|
+
const introduced = allocateNames(reserved, [
|
|
6124
|
+
{ key: "bcmsFields", name: "bcmsFields" },
|
|
6125
|
+
{ key: "bcmsField", name: "bcmsField" }
|
|
6126
|
+
]);
|
|
6127
|
+
const fieldsName = introduced.get("bcmsFields");
|
|
6128
|
+
const fieldName = introduced.get("bcmsField");
|
|
6129
|
+
const { markup, bound, duplicates } = sectionMarkup(content, resolved.root, fieldName);
|
|
6130
|
+
if (duplicates.length > 0) return { content: "", free: [], duplicates, unconvertible: [] };
|
|
5910
6131
|
const groupKey = resolved.section.groupKey;
|
|
5911
6132
|
const locals = helperLocals(parsed, dialect, fromFile);
|
|
5912
6133
|
const helperNames = ["bcms", "bcmsRows", "bcmsLayout"].flatMap(
|
|
5913
6134
|
(name) => locals[name].filter((local) => mentions(markup, local)).map((local) => ({ name, local }))
|
|
5914
6135
|
);
|
|
5915
|
-
const helpers = [
|
|
6136
|
+
const helpers = [
|
|
6137
|
+
.../* @__PURE__ */ new Set([
|
|
6138
|
+
...helperNames.map((h) => alias(h.name, h.local)),
|
|
6139
|
+
"bcmsSection",
|
|
6140
|
+
...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
|
|
6141
|
+
])
|
|
6142
|
+
].sort();
|
|
5916
6143
|
const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
|
|
5917
6144
|
const snapshot = snapshotOf(parsed, dialect, fromFile, markup);
|
|
5918
6145
|
const carry = carryFor({
|
|
@@ -5927,20 +6154,23 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved) {
|
|
|
5927
6154
|
"bind",
|
|
5928
6155
|
"overrides",
|
|
5929
6156
|
"page",
|
|
6157
|
+
fieldName,
|
|
5930
6158
|
...helperNames.map((h) => h.local),
|
|
5931
6159
|
...snapshot ? [snapshot] : []
|
|
5932
6160
|
],
|
|
5933
6161
|
// The helper and the snapshots are what the component declares for itself.
|
|
5934
6162
|
skipSpecifier: (repoPath, specifier) => specifier.includes(`${CONTENT_DIR}/`) || repoPath.startsWith(HELPER_PATH[dialect].replace(/\.tsx?$/, ""))
|
|
5935
6163
|
});
|
|
5936
|
-
if (carry.free.length > 0) return { content: "", free: carry.free };
|
|
6164
|
+
if (carry.free.length > 0) return { content: "", free: carry.free, duplicates: [], unconvertible: [] };
|
|
5937
6165
|
const carried = [...carry.imports];
|
|
5938
6166
|
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
5939
6167
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
5940
6168
|
` : "";
|
|
5941
|
-
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind
|
|
6169
|
+
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
|
|
6170
|
+
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
|
|
6171
|
+
const declared = [copy, field].filter(Boolean).join("\n");
|
|
5942
6172
|
if (dialect === "astro") {
|
|
5943
|
-
return { free: [], content: `---
|
|
6173
|
+
return { free: [], duplicates: [], unconvertible: [], content: `---
|
|
5944
6174
|
${SECTION_MARKER}
|
|
5945
6175
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`].join("\n")}
|
|
5946
6176
|
|
|
@@ -5955,27 +6185,297 @@ interface Props {
|
|
|
5955
6185
|
page?: unknown;
|
|
5956
6186
|
}
|
|
5957
6187
|
const { blockId, bind, overrides, page } = Astro.props;
|
|
5958
|
-
${carriedDeclarations}${
|
|
6188
|
+
${carriedDeclarations}${declared}
|
|
5959
6189
|
---
|
|
5960
6190
|
${markup}
|
|
5961
6191
|
` };
|
|
5962
6192
|
}
|
|
5963
|
-
return { free: [], content: `${SECTION_MARKER}
|
|
6193
|
+
return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
|
|
5964
6194
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
5965
6195
|
${carriedDeclarations === "" ? "" : `
|
|
5966
6196
|
${carriedDeclarations}`}
|
|
5967
6197
|
export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
|
|
5968
|
-
${
|
|
6198
|
+
${declared.split("\n").join("\n ")}
|
|
5969
6199
|
return (
|
|
5970
6200
|
${markup}
|
|
5971
6201
|
);
|
|
5972
6202
|
}
|
|
5973
6203
|
` };
|
|
5974
6204
|
}
|
|
5975
|
-
function
|
|
6205
|
+
function sectionMarkup(content, root, fieldName) {
|
|
5976
6206
|
const slice = new MagicString3(content.slice(root.start, root.end));
|
|
5977
|
-
|
|
5978
|
-
|
|
6207
|
+
const at = (offset) => offset - root.start;
|
|
6208
|
+
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
6209
|
+
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
6210
|
+
else {
|
|
6211
|
+
slice.overwrite(at(stamps[0].range.start), at(stamps[0].range.end), "data-bcms-block={blockId}");
|
|
6212
|
+
for (const extra of stamps.slice(1)) {
|
|
6213
|
+
let from = extra.range.start;
|
|
6214
|
+
while (from > root.start && /\s/.test(content[from - 1])) from--;
|
|
6215
|
+
slice.remove(at(from), at(extra.range.end));
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
const wrapped = wrapFieldAttrs(slice, content, elementsUnder(root), at, fieldName);
|
|
6219
|
+
return { markup: slice.toString(), ...wrapped };
|
|
6220
|
+
}
|
|
6221
|
+
function wrapFieldAttrs(slice, content, elements, at, fieldName) {
|
|
6222
|
+
let bound = 0;
|
|
6223
|
+
const duplicates = [];
|
|
6224
|
+
for (const element of elements) {
|
|
6225
|
+
const fields = element.attrs.filter((attr) => attr.name === "data-bcms-field");
|
|
6226
|
+
if (fields.length > 1) {
|
|
6227
|
+
duplicates.push(`<${element.tag}>`);
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
for (const attr of fields) {
|
|
6231
|
+
bound++;
|
|
6232
|
+
if (attr.expressionRange) {
|
|
6233
|
+
slice.appendRight(at(attr.expressionRange.start) + 1, `${fieldName}(`);
|
|
6234
|
+
slice.appendLeft(at(attr.expressionRange.end) - 1, ")");
|
|
6235
|
+
continue;
|
|
6236
|
+
}
|
|
6237
|
+
const value = attr.valueRange ? content.slice(attr.valueRange.start, attr.valueRange.end) : "";
|
|
6238
|
+
slice.overwrite(
|
|
6239
|
+
at(attr.range.start),
|
|
6240
|
+
at(attr.range.end),
|
|
6241
|
+
`data-bcms-field={${fieldName}(${JSON.stringify(value)})}`
|
|
6242
|
+
);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
return { bound, duplicates };
|
|
6246
|
+
}
|
|
6247
|
+
function provableAddress(expression) {
|
|
6248
|
+
const first = expression[0];
|
|
6249
|
+
if (first === '"' || first === "'") {
|
|
6250
|
+
for (let at = 1; at < expression.length; at++) {
|
|
6251
|
+
if (expression[at] === "\\") {
|
|
6252
|
+
at++;
|
|
6253
|
+
continue;
|
|
6254
|
+
}
|
|
6255
|
+
if (expression[at] === first) return at === expression.length - 1 ? expression.slice(1, at) : null;
|
|
6256
|
+
}
|
|
6257
|
+
return null;
|
|
6258
|
+
}
|
|
6259
|
+
if (first !== "`" || expression.length < 2 || !expression.endsWith("`")) return null;
|
|
6260
|
+
const body = expression.slice(1, -1);
|
|
6261
|
+
if (body.includes("`") || body.includes("\\")) return null;
|
|
6262
|
+
let head = null;
|
|
6263
|
+
for (let at = 0; at < body.length; at++) {
|
|
6264
|
+
if (body[at] !== "$" || body[at + 1] !== "{") continue;
|
|
6265
|
+
const close = body.indexOf("}", at + 2);
|
|
6266
|
+
if (close < 0) return null;
|
|
6267
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(body.slice(at + 2, close).trim())) return null;
|
|
6268
|
+
if (body[at - 1] !== "[" || body[close + 1] !== "]") return null;
|
|
6269
|
+
head ??= body.slice(0, at);
|
|
6270
|
+
at = close;
|
|
6271
|
+
}
|
|
6272
|
+
return head ?? body;
|
|
6273
|
+
}
|
|
6274
|
+
function fieldAddresses(content, elements) {
|
|
6275
|
+
const heads = [];
|
|
6276
|
+
const opaque = [];
|
|
6277
|
+
for (const element of elements) {
|
|
6278
|
+
for (const attr of element.attrs) {
|
|
6279
|
+
if (attr.name !== "data-bcms-field") continue;
|
|
6280
|
+
if (!attr.expression && attr.valueRange) {
|
|
6281
|
+
heads.push(content.slice(attr.valueRange.start, attr.valueRange.end));
|
|
6282
|
+
continue;
|
|
6283
|
+
}
|
|
6284
|
+
const inner = attr.expressionRange ? content.slice(attr.expressionRange.start + 1, attr.expressionRange.end - 1).trim() : "";
|
|
6285
|
+
const head = provableAddress(inner);
|
|
6286
|
+
if (head === null) opaque.push(`data-bcms-field={${inner}}`);
|
|
6287
|
+
else heads.push(head);
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
return { heads, opaque };
|
|
6291
|
+
}
|
|
6292
|
+
function elementsUnder(...roots) {
|
|
6293
|
+
const out = [];
|
|
6294
|
+
const walk = (node) => {
|
|
6295
|
+
if (node.type === "element") {
|
|
6296
|
+
out.push(node);
|
|
6297
|
+
for (const child of node.children) walk(child);
|
|
6298
|
+
return;
|
|
6299
|
+
}
|
|
6300
|
+
if (node.type === "expr") for (const child of node.children) walk(child);
|
|
6301
|
+
};
|
|
6302
|
+
for (const root of roots) walk(root);
|
|
6303
|
+
return out;
|
|
6304
|
+
}
|
|
6305
|
+
var V1_ANCHOR = {
|
|
6306
|
+
astro: "const { blockId, bind, overrides, page } = Astro.props;",
|
|
6307
|
+
jsx: "}: BcmsSectionProps) {"
|
|
6308
|
+
};
|
|
6309
|
+
var refusalKey = (dialect, slug) => `${dialect}\0${slug}`;
|
|
6310
|
+
function generatedBindings(parsed) {
|
|
6311
|
+
const program = parsed.ast?.program;
|
|
6312
|
+
const body = program?.body ?? [];
|
|
6313
|
+
const scopes = [body];
|
|
6314
|
+
for (const statement of body) {
|
|
6315
|
+
const declaration2 = statement.type === "ExportDefaultDeclaration" ? statement.declaration : void 0;
|
|
6316
|
+
const inner = declaration2?.type === "FunctionDeclaration" ? declaration2.body?.body : void 0;
|
|
6317
|
+
if (inner) scopes.push(inner);
|
|
6318
|
+
}
|
|
6319
|
+
const out = /* @__PURE__ */ new Map();
|
|
6320
|
+
for (const scope of scopes) {
|
|
6321
|
+
for (const statement of scope) {
|
|
6322
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
6323
|
+
for (const declarator of statement.declarations ?? []) {
|
|
6324
|
+
const id = declarator.id;
|
|
6325
|
+
const name = id?.type === "Identifier" ? id.name : void 0;
|
|
6326
|
+
if (name !== void 0 && !out.has(name)) out.set(name, declarator.init);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
return out;
|
|
6331
|
+
}
|
|
6332
|
+
function stripBindFallback(parsed, source, content, locals) {
|
|
6333
|
+
if (!parsed.ast?.program) return "unreadable";
|
|
6334
|
+
const offset = parsed.script?.offset ?? 0;
|
|
6335
|
+
let rewritten = 0;
|
|
6336
|
+
let unproven = 0;
|
|
6337
|
+
walkAst(parsed.ast, (node) => {
|
|
6338
|
+
if (node.type !== "CallExpression") return;
|
|
6339
|
+
const argument = (node.arguments ?? [])[1];
|
|
6340
|
+
if (argument?.type !== "LogicalExpression" || argument.operator !== "??") return;
|
|
6341
|
+
const callee = node.callee;
|
|
6342
|
+
if (callee?.type !== "Identifier" || !locals.has(callee.name)) {
|
|
6343
|
+
unproven++;
|
|
6344
|
+
return;
|
|
6345
|
+
}
|
|
6346
|
+
const left = argument.left;
|
|
6347
|
+
source.overwrite(
|
|
6348
|
+
offset + argument.start,
|
|
6349
|
+
offset + argument.end,
|
|
6350
|
+
content.slice(offset + left.start, offset + left.end)
|
|
6351
|
+
);
|
|
6352
|
+
rewritten++;
|
|
6353
|
+
});
|
|
6354
|
+
return rewritten === 0 && unproven > 0 ? "unproven" : "ok";
|
|
6355
|
+
}
|
|
6356
|
+
var isCallOf = (init, callee) => {
|
|
6357
|
+
const fn = init?.type === "CallExpression" ? init.callee : void 0;
|
|
6358
|
+
return fn?.type === "Identifier" && fn.name === callee;
|
|
6359
|
+
};
|
|
6360
|
+
async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
6361
|
+
const files = [];
|
|
6362
|
+
const refused = /* @__PURE__ */ new Map();
|
|
6363
|
+
const prefixesFor = (slug) => {
|
|
6364
|
+
const out = /* @__PURE__ */ new Set();
|
|
6365
|
+
for (const page of plan2.pages ?? []) {
|
|
6366
|
+
for (const section of page.sections ?? []) {
|
|
6367
|
+
if (resolveComponent(plan2, section).slug !== slug) continue;
|
|
6368
|
+
const prefix = propPrefix(section);
|
|
6369
|
+
if (prefix !== null) out.add(prefix);
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
return [...out];
|
|
6373
|
+
};
|
|
6374
|
+
for (const [dialect, components] of written) {
|
|
6375
|
+
for (const [slug, entry] of components) {
|
|
6376
|
+
const content = contents.get(entry.path);
|
|
6377
|
+
if (content === void 0 || content.includes(SECTION_MARKER)) continue;
|
|
6378
|
+
const older = SECTION_MARKERS.find((marker) => content.includes(marker));
|
|
6379
|
+
if (older === void 0) continue;
|
|
6380
|
+
const candidates = prefixesFor(slug);
|
|
6381
|
+
if (candidates.length === 0) continue;
|
|
6382
|
+
const parsed = await parseFile4(dialect, content);
|
|
6383
|
+
if (parsed.error) {
|
|
6384
|
+
refused.set(refusalKey(dialect, slug), `${entry.path} carries our marker but does not parse (${parsed.error.message}).`);
|
|
6385
|
+
continue;
|
|
6386
|
+
}
|
|
6387
|
+
const elements = elementsUnder(...parsed.roots);
|
|
6388
|
+
const imports = [...importsIn(parsed, dialect)];
|
|
6389
|
+
const helperModule = HELPER_PATH[dialect];
|
|
6390
|
+
const helperLocalsFor = (exported) => new Set(
|
|
6391
|
+
imports.filter(
|
|
6392
|
+
([, from]) => from.imported === exported && resolveSpecifier(entry.path, from.specifier, fileSet, aliases) === helperModule
|
|
6393
|
+
).map(([local]) => local)
|
|
6394
|
+
);
|
|
6395
|
+
const { heads: evidence, opaque } = fieldAddresses(content, elements);
|
|
6396
|
+
const pathPrefix = evidence.length + opaque.length === 0 ? "" : opaque.length > 0 ? null : onlyOne(candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)))) ?? null;
|
|
6397
|
+
if (pathPrefix === null) {
|
|
6398
|
+
const matching = candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)));
|
|
6399
|
+
refused.set(
|
|
6400
|
+
refusalKey(dialect, slug),
|
|
6401
|
+
opaque.length > 0 ? `${entry.path} carries ${[...new Set(opaque)].slice(0, 3).join(", ")}, whose address this cannot read \u2014 so there is no prefix it can be shown to convert through.` : matching.length === 0 ? `${entry.path} declares ${evidence.length > 0 ? `addresses (${[...new Set(evidence)].slice(0, 3).join(", ")})` : "addresses"} that match none of this slug's placements (${candidates.join(", ") || "none with a usable prop map"}), so the prefix its markup was cut with cannot be recovered.` : `${entry.path} matches more than one of this slug's placements (${matching.join(", ")}), so the prefix its markup was cut with is ambiguous.`
|
|
6402
|
+
);
|
|
6403
|
+
continue;
|
|
6404
|
+
}
|
|
6405
|
+
const source = new MagicString3(content);
|
|
6406
|
+
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
6407
|
+
if (duplicates.length > 0) {
|
|
6408
|
+
refused.set(refusalKey(dialect, slug), `${entry.path} has ${duplicates.join(", ")} carrying more than one data-bcms-field, so there is no single address to rewrite.`);
|
|
6409
|
+
continue;
|
|
6410
|
+
}
|
|
6411
|
+
const markerEnd = content.indexOf(older) + older.length;
|
|
6412
|
+
source.overwrite(markerEnd - older.length, markerEnd, SECTION_MARKER);
|
|
6413
|
+
const stripped = stripBindFallback(parsed, source, content, helperLocalsFor("bcmsSection"));
|
|
6414
|
+
if (stripped !== "ok") {
|
|
6415
|
+
refused.set(
|
|
6416
|
+
refusalKey(dialect, slug),
|
|
6417
|
+
stripped === "unreadable" ? `${entry.path} carries our marker but its module could not be read as a program, so the Phase A \`bind ?? "\u2026"\` fallback cannot be shown to be gone.` : `${entry.path} carries a \`?? \` fallback in a call this cannot identify as ${helperModule}'s bcmsSection, so the Phase A page-group default cannot be shown to be gone.`
|
|
6418
|
+
);
|
|
6419
|
+
continue;
|
|
6420
|
+
}
|
|
6421
|
+
if (bound > 0) {
|
|
6422
|
+
const locals = generatedBindings(parsed);
|
|
6423
|
+
const names = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...locals.keys()]);
|
|
6424
|
+
const fieldsLocal = [...helperLocalsFor("bcmsFields")][0];
|
|
6425
|
+
if (fieldsLocal === void 0 && names.has("bcmsFields")) {
|
|
6426
|
+
refused.set(
|
|
6427
|
+
refusalKey(dialect, slug),
|
|
6428
|
+
`${entry.path} already binds "bcmsFields" to something other than ${helperModule}'s export.`
|
|
6429
|
+
);
|
|
6430
|
+
continue;
|
|
6431
|
+
}
|
|
6432
|
+
const declared = names.has("bcmsField");
|
|
6433
|
+
if (declared && (fieldsLocal === void 0 || !isCallOf(locals.get("bcmsField"), fieldsLocal))) {
|
|
6434
|
+
refused.set(
|
|
6435
|
+
refusalKey(dialect, slug),
|
|
6436
|
+
`${entry.path} already binds "bcmsField" to something other than a call of ${helperModule}'s bcmsFields.`
|
|
6437
|
+
);
|
|
6438
|
+
continue;
|
|
6439
|
+
}
|
|
6440
|
+
if (!declared) {
|
|
6441
|
+
const anchor = V1_ANCHOR[dialect];
|
|
6442
|
+
const at = anchor ? content.indexOf(anchor) : -1;
|
|
6443
|
+
if (anchor === void 0 || at < 0) {
|
|
6444
|
+
refused.set(refusalKey(dialect, slug), `${entry.path} carries our marker but no longer declares its placement, so there is nowhere the chooser can be put.`);
|
|
6445
|
+
continue;
|
|
6446
|
+
}
|
|
6447
|
+
source.appendLeft(
|
|
6448
|
+
at + anchor.length,
|
|
6449
|
+
`
|
|
6450
|
+
${dialect === "astro" ? "" : " "}const bcmsField = ${fieldsLocal ?? "bcmsFields"}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});`
|
|
6451
|
+
);
|
|
6452
|
+
}
|
|
6453
|
+
if (fieldsLocal === void 0) {
|
|
6454
|
+
const helper = imports.find(([, from]) => from.imported === "bcmsSection")?.[1].specifier ?? relativeImport(entry.path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
|
|
6455
|
+
source.appendLeft(markerEnd, `
|
|
6456
|
+
import { bcmsFields } from "${helper}";`);
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
files.push({ path: entry.path, operation: "modify", content: source.toString() });
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
return { files, refused };
|
|
6463
|
+
}
|
|
6464
|
+
function propPrefix(section) {
|
|
6465
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
6466
|
+
for (const field of section.fields ?? []) {
|
|
6467
|
+
const { path, key } = field;
|
|
6468
|
+
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
6469
|
+
if (!path.endsWith(key)) return null;
|
|
6470
|
+
prefixes.add(path.slice(0, path.length - key.length));
|
|
6471
|
+
}
|
|
6472
|
+
if (prefixes.size > 1) return null;
|
|
6473
|
+
return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
|
|
6474
|
+
}
|
|
6475
|
+
function referencedNames2(markup) {
|
|
6476
|
+
const out = /* @__PURE__ */ new Set();
|
|
6477
|
+
for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
|
|
6478
|
+
return out;
|
|
5979
6479
|
}
|
|
5980
6480
|
var alias = (name, local) => name === local ? name : `${name} as ${local}`;
|
|
5981
6481
|
var mentions = (markup, name) => new RegExp(`(?<![\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w$])`).test(markup);
|
|
@@ -6234,7 +6734,7 @@ function addImports(source, parsed, lines) {
|
|
|
6234
6734
|
else source.appendLeft(at.at, `
|
|
6235
6735
|
${lines.join("\n")}`);
|
|
6236
6736
|
}
|
|
6237
|
-
var isGenerated = (content) => content.includes(
|
|
6737
|
+
var isGenerated = (content) => SECTION_MARKERS.some((marker) => content.includes(marker)) || content.includes(REGISTRY_MARKER);
|
|
6238
6738
|
function emitHelper(out, contents, version, overwrite) {
|
|
6239
6739
|
const path = HELPER_PATH.astro;
|
|
6240
6740
|
const source = helperSource("astro");
|
|
@@ -7087,7 +7587,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
7087
7587
|
}
|
|
7088
7588
|
|
|
7089
7589
|
// src/cli.ts
|
|
7090
|
-
var VERSION = "0.
|
|
7590
|
+
var VERSION = "0.4.0";
|
|
7091
7591
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
7092
7592
|
var CliError = class extends Error {
|
|
7093
7593
|
};
|
|
@@ -7096,6 +7596,7 @@ function parseArgs(argv) {
|
|
|
7096
7596
|
brief: void 0,
|
|
7097
7597
|
componentize: false,
|
|
7098
7598
|
plan: void 0,
|
|
7599
|
+
copy: "bind",
|
|
7099
7600
|
root: ".",
|
|
7100
7601
|
dryRun: false,
|
|
7101
7602
|
receipt: void 0,
|
|
@@ -7109,7 +7610,13 @@ function parseArgs(argv) {
|
|
|
7109
7610
|
if (arg === "--brief") args.brief = argv[++i];
|
|
7110
7611
|
else if (arg === "--componentize") args.componentize = true;
|
|
7111
7612
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
7112
|
-
else if (arg === "--
|
|
7613
|
+
else if (arg === "--copy") {
|
|
7614
|
+
const mode = argv[++i];
|
|
7615
|
+
if (mode !== "bind" && mode !== "instance") {
|
|
7616
|
+
throw new CliError(`--copy takes "bind" or "instance", not ${JSON.stringify(mode ?? "")}. See --help.`);
|
|
7617
|
+
}
|
|
7618
|
+
args.copy = mode;
|
|
7619
|
+
} else if (arg === "--root") args.root = argv[++i] ?? ".";
|
|
7113
7620
|
else if (arg === "--receipt") args.receipt = argv[++i];
|
|
7114
7621
|
else if (arg === "--dry-run") args.dryRun = true;
|
|
7115
7622
|
else if (arg === "--strict") args.strict = true;
|
|
@@ -7133,6 +7640,10 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
7133
7640
|
--componentize Lift each planned SECTION into a component the CMS can reorder.
|
|
7134
7641
|
--plan <file> The componentize plan, as get_componentize_plan returns it.
|
|
7135
7642
|
Required with --componentize.
|
|
7643
|
+
--copy bind|instance Which copy model the plan is expected to describe. Default: bind
|
|
7644
|
+
(the copy stays in the page's field groups). "instance" is refused
|
|
7645
|
+
\u2014 COPY_MODE_MISMATCH \u2014 unless the plan's placements say the server
|
|
7646
|
+
has already moved it onto them.
|
|
7136
7647
|
--root <dir> The repository to convert. Default: the working directory.
|
|
7137
7648
|
--dry-run Print what would change; write nothing.
|
|
7138
7649
|
--receipt <file> Write the receipt as JSON.
|
|
@@ -7217,7 +7728,7 @@ function reportSections(files, receipt, dryRun) {
|
|
|
7217
7728
|
}
|
|
7218
7729
|
process.stdout.write(
|
|
7219
7730
|
`
|
|
7220
|
-
${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending)
|
|
7731
|
+
${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending), copy: ${receipt.copy}
|
|
7221
7732
|
`
|
|
7222
7733
|
);
|
|
7223
7734
|
for (const section of pending) {
|
|
@@ -7237,6 +7748,9 @@ async function main() {
|
|
|
7237
7748
|
if (args.plan && !args.componentize) {
|
|
7238
7749
|
throw new CliError("--plan is only read with --componentize. See --help.");
|
|
7239
7750
|
}
|
|
7751
|
+
if (args.copy === "instance" && !args.componentize) {
|
|
7752
|
+
throw new CliError("--copy is only read with --componentize. See --help.");
|
|
7753
|
+
}
|
|
7240
7754
|
const root = resolve(args.root);
|
|
7241
7755
|
const brief = JSON.parse(await readFile(resolve(args.brief), "utf-8"));
|
|
7242
7756
|
if (!Array.isArray(brief.pages)) {
|
|
@@ -7270,7 +7784,7 @@ async function main() {
|
|
|
7270
7784
|
brief,
|
|
7271
7785
|
plan2,
|
|
7272
7786
|
[...sources, ...await readSnapshots(root)],
|
|
7273
|
-
{ overwriteHelper: args.overwriteHelper }
|
|
7787
|
+
{ overwriteHelper: args.overwriteHelper, copy: args.copy }
|
|
7274
7788
|
);
|
|
7275
7789
|
await written(files2);
|
|
7276
7790
|
await saveReceipt(receipt2);
|