@bettercms-ai/convert 0.10.0 → 0.12.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 +47 -0
- package/README.md +54 -0
- package/dist/cli.js +338 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +103 -2
- package/dist/index.js +340 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4142,7 +4142,7 @@ function bindImages(ctx, targets) {
|
|
|
4142
4142
|
|
|
4143
4143
|
// src/helper.ts
|
|
4144
4144
|
import { createHash as createHash2 } from "crypto";
|
|
4145
|
-
var HELPER_VERSION =
|
|
4145
|
+
var HELPER_VERSION = 7;
|
|
4146
4146
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4147
4147
|
var HELPER_PATH = Object.fromEntries(
|
|
4148
4148
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4335,9 +4335,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
|
|
|
4335
4335
|
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
4336
4336
|
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
4337
4337
|
*/
|
|
4338
|
+
/** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
|
|
4339
|
+
export type BcmsPathMap = {
|
|
4340
|
+
scalars?: Record<string, string>;
|
|
4341
|
+
tables?: Record<string, { key: string; columns: Record<string, string> }>;
|
|
4342
|
+
};
|
|
4343
|
+
|
|
4344
|
+
/**
|
|
4345
|
+
* The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
|
|
4346
|
+
*
|
|
4347
|
+
* \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
|
|
4348
|
+
* copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
|
|
4349
|
+
* markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
|
|
4350
|
+
*
|
|
4351
|
+
* - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
|
|
4352
|
+
* column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
|
|
4353
|
+
* - else, for a placement bound to a group, the page value at the path re-based onto that group;
|
|
4354
|
+
* - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
|
|
4355
|
+
* \`bcmsSection\` synthesizes no group for it.
|
|
4356
|
+
*
|
|
4357
|
+
* Every other page key passes through, so reads outside the family (a shared link) keep working.
|
|
4358
|
+
*/
|
|
4359
|
+
export function bcmsSectionPaths(
|
|
4360
|
+
page: unknown,
|
|
4361
|
+
bind: string | undefined,
|
|
4362
|
+
overrides: unknown,
|
|
4363
|
+
prefix: string,
|
|
4364
|
+
map: BcmsPathMap,
|
|
4365
|
+
): Record<string, unknown> {
|
|
4366
|
+
const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
|
|
4367
|
+
const own = isRecord(overrides) ? overrides : {};
|
|
4368
|
+
const separator = prefix.slice(-1);
|
|
4369
|
+
const fromPage = (path: string): unknown =>
|
|
4370
|
+
bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
|
|
4371
|
+
for (const [path, key] of Object.entries(map.scalars ?? {})) {
|
|
4372
|
+
const value = own[key];
|
|
4373
|
+
out[path] = value !== undefined && value !== null ? value : fromPage(path);
|
|
4374
|
+
}
|
|
4375
|
+
for (const [path, table] of Object.entries(map.tables ?? {})) {
|
|
4376
|
+
const held = own[table.key];
|
|
4377
|
+
const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
|
|
4378
|
+
if (!Array.isArray(rows)) {
|
|
4379
|
+
out[path] = fromPage(path);
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
|
|
4383
|
+
out[path] = rows.map((row) => {
|
|
4384
|
+
if (!isRecord(row)) return row;
|
|
4385
|
+
const spelled: Record<string, unknown> = {};
|
|
4386
|
+
for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
|
|
4387
|
+
return spelled;
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
return out;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4338
4393
|
export function bcmsFields(
|
|
4339
4394
|
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
4340
4395
|
pathPrefix: string,
|
|
4396
|
+
map?: BcmsPathMap,
|
|
4341
4397
|
): (path: string) => string {
|
|
4342
4398
|
const id = placement.blockId;
|
|
4343
4399
|
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
@@ -4350,6 +4406,24 @@ export function bcmsFields(
|
|
|
4350
4406
|
* so prepending one would invent an address rather than correct it.
|
|
4351
4407
|
*/
|
|
4352
4408
|
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
4409
|
+
if (map) {
|
|
4410
|
+
/**
|
|
4411
|
+
* A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
|
|
4412
|
+
* plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
|
|
4413
|
+
* separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
|
|
4414
|
+
*/
|
|
4415
|
+
const separator = pathPrefix.slice(-1);
|
|
4416
|
+
return (path: string): string => {
|
|
4417
|
+
if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
|
|
4418
|
+
if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
|
|
4419
|
+
const scalar = map.scalars?.[path];
|
|
4420
|
+
if (scalar !== undefined) return id + "__overrides." + scalar;
|
|
4421
|
+
const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
|
|
4422
|
+
const table = row ? map.tables?.[row[1]] : undefined;
|
|
4423
|
+
const column = row && table ? table.columns[row[3]] : undefined;
|
|
4424
|
+
return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4353
4427
|
return (path: string): string => {
|
|
4354
4428
|
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
4355
4429
|
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
@@ -4366,21 +4440,24 @@ var HISTORICAL = {
|
|
|
4366
4440
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4367
4441
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4368
4442
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4369
|
-
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4443
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4444
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4370
4445
|
},
|
|
4371
4446
|
"src/lib/bcms-content.ts": {
|
|
4372
4447
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
4373
4448
|
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
4374
4449
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
4375
4450
|
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4376
|
-
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
4451
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
|
|
4452
|
+
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
|
|
4377
4453
|
},
|
|
4378
4454
|
"composables/bcms-content.ts": {
|
|
4379
4455
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
4380
4456
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4381
4457
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4382
4458
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4383
|
-
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4459
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4460
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4384
4461
|
}
|
|
4385
4462
|
};
|
|
4386
4463
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -5235,6 +5312,138 @@ function assertReceipt(receipt) {
|
|
|
5235
5312
|
return receipt;
|
|
5236
5313
|
}
|
|
5237
5314
|
|
|
5315
|
+
// src/fix.ts
|
|
5316
|
+
var MAX_SNIPPET = 160;
|
|
5317
|
+
var MAX_WORDS = 12;
|
|
5318
|
+
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5319
|
+
function findLiteral(content, literal) {
|
|
5320
|
+
const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
|
|
5321
|
+
if (words.length === 0) return null;
|
|
5322
|
+
const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
|
|
5323
|
+
const match = pattern.exec(content);
|
|
5324
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
5325
|
+
}
|
|
5326
|
+
function placeOf(content, index) {
|
|
5327
|
+
const before = content.slice(0, index);
|
|
5328
|
+
const line = before.split("\n").length;
|
|
5329
|
+
const start = before.lastIndexOf("\n") + 1;
|
|
5330
|
+
const end = content.indexOf("\n", index);
|
|
5331
|
+
return {
|
|
5332
|
+
line,
|
|
5333
|
+
col: index - start + 1,
|
|
5334
|
+
snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
function fileHead(content) {
|
|
5338
|
+
return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
|
|
5339
|
+
}
|
|
5340
|
+
var isMarkdown = (file) => /\.mdx?$/i.test(file);
|
|
5341
|
+
var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
|
|
5342
|
+
function inDataRegion(content, index) {
|
|
5343
|
+
if (content.startsWith("---")) {
|
|
5344
|
+
const close = content.indexOf("\n---", 3);
|
|
5345
|
+
if (close >= 0 && index < close) return true;
|
|
5346
|
+
}
|
|
5347
|
+
for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
5348
|
+
const start = match.index + match[0].indexOf(">") + 1;
|
|
5349
|
+
if (index >= start && index < start + match[1].length) return true;
|
|
5350
|
+
}
|
|
5351
|
+
return false;
|
|
5352
|
+
}
|
|
5353
|
+
var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
|
|
5354
|
+
"wrap-span",
|
|
5355
|
+
"declare-attr",
|
|
5356
|
+
"declare-richtext"
|
|
5357
|
+
]);
|
|
5358
|
+
function actionFor(reason, input, file, inData) {
|
|
5359
|
+
const chosen = reasonAction(reason, input, file, inData);
|
|
5360
|
+
if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
|
|
5361
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5362
|
+
}
|
|
5363
|
+
return chosen;
|
|
5364
|
+
}
|
|
5365
|
+
function reasonAction(reason, input, file, inData) {
|
|
5366
|
+
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5367
|
+
switch (reason) {
|
|
5368
|
+
case "AMBIGUOUS_LITERAL":
|
|
5369
|
+
if (input.routes > 1) {
|
|
5370
|
+
return {
|
|
5371
|
+
action: "manual",
|
|
5372
|
+
why: `${file} renders this value on ${input.routes} routes, so it is shared chrome rather than page copy: promote it to the project Layout (a Layout field is one value for the whole site) and rerun, or give each route its own element.`
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
return {
|
|
5376
|
+
action: "declare-attr",
|
|
5377
|
+
why: input.candidates.length > 1 ? `This copy appears in ${input.candidates.join(", ")}; declare the one this route renders.` : "Two paths want this element and nothing says which \u2014 declare the right one by hand."
|
|
5378
|
+
};
|
|
5379
|
+
case "REPEATER_AMBIGUOUS":
|
|
5380
|
+
case "REPEATER_FIXED_LENGTH":
|
|
5381
|
+
return {
|
|
5382
|
+
action: "declare-attr",
|
|
5383
|
+
why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
|
|
5384
|
+
};
|
|
5385
|
+
case "IN_EXPRESSION":
|
|
5386
|
+
if (inData) {
|
|
5387
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5388
|
+
}
|
|
5389
|
+
return richtext ? { action: "declare-richtext", why: "The container is fed markup \u2014 bind it with the richtext helper." } : { action: "bind-expression", why: "Replace the computed value with the helper's read for this path." };
|
|
5390
|
+
case "DIALECT_UNSUPPORTED":
|
|
5391
|
+
if (isMarkdown(file)) {
|
|
5392
|
+
return {
|
|
5393
|
+
action: "bind-data",
|
|
5394
|
+
why: "This is a content-collection body: bind the element that renders it and make the entry the source."
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
return isDataModule(file) ? { action: "bind-data", why: `${file} holds this copy as data \u2014 bind the element that renders it.` } : { action: "manual", why: `No parser here reads ${file}, so the declaration has to be written by hand.` };
|
|
5398
|
+
case "NOT_IN_SOURCE":
|
|
5399
|
+
case "NO_ORIGINAL":
|
|
5400
|
+
return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
|
|
5401
|
+
action: "manual",
|
|
5402
|
+
why: "The built page renders this and no source file holds it verbatim; find what formats it."
|
|
5403
|
+
};
|
|
5404
|
+
case "IN_DATA_FILE":
|
|
5405
|
+
case "IMAGE_ASSET_UNRESOLVED":
|
|
5406
|
+
return { action: "bind-data", why: "Bind the data field this value comes from." };
|
|
5407
|
+
case "PROP_TARGET_NOT_FOUND":
|
|
5408
|
+
case "PROP_DRILLED_DEEP":
|
|
5409
|
+
return {
|
|
5410
|
+
action: "wrap-span",
|
|
5411
|
+
why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
|
|
5412
|
+
};
|
|
5413
|
+
case "SUBSTRING_ONLY":
|
|
5414
|
+
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5415
|
+
case "SUBTREE_NOT_HTML":
|
|
5416
|
+
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5417
|
+
default:
|
|
5418
|
+
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
function fixFor(row, input) {
|
|
5422
|
+
const file = row.file ?? input.candidates[0] ?? "";
|
|
5423
|
+
const content = input.contents.get(file);
|
|
5424
|
+
const at = content ? findLiteral(content, input.literal) : null;
|
|
5425
|
+
const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
|
|
5426
|
+
const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
|
|
5427
|
+
return {
|
|
5428
|
+
file,
|
|
5429
|
+
line: row.line ?? place.line,
|
|
5430
|
+
...place.col !== void 0 ? { col: place.col } : {},
|
|
5431
|
+
snippet: place.snippet,
|
|
5432
|
+
action,
|
|
5433
|
+
...why ? { why } : {}
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
function routeCounts(pending) {
|
|
5437
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5438
|
+
for (const row of pending) {
|
|
5439
|
+
const id = `${row.scope}|${row.path}`;
|
|
5440
|
+
const held = routes.get(id) ?? /* @__PURE__ */ new Set();
|
|
5441
|
+
held.add(row.route);
|
|
5442
|
+
routes.set(id, held);
|
|
5443
|
+
}
|
|
5444
|
+
return new Map([...routes].map(([id, set]) => [id, set.size]));
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5238
5447
|
// src/sites/html.ts
|
|
5239
5448
|
import { parse as parseHtml } from "parse5";
|
|
5240
5449
|
var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
|
|
@@ -6351,7 +6560,9 @@ var SKIP_DIRS = [
|
|
|
6351
6560
|
".vercel/",
|
|
6352
6561
|
".cache/",
|
|
6353
6562
|
"coverage/",
|
|
6354
|
-
"vendor/"
|
|
6563
|
+
"vendor/",
|
|
6564
|
+
"__tests__/",
|
|
6565
|
+
"__mocks__/"
|
|
6355
6566
|
];
|
|
6356
6567
|
var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
6357
6568
|
var SOURCE_EXTENSIONS = [
|
|
@@ -6389,11 +6600,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6389
6600
|
"_includes",
|
|
6390
6601
|
"_layouts"
|
|
6391
6602
|
]);
|
|
6603
|
+
var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
|
|
6392
6604
|
function isSourceCandidate(path) {
|
|
6393
6605
|
const lower = path.toLowerCase();
|
|
6394
6606
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
6395
6607
|
return false;
|
|
6396
6608
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6609
|
+
if (isAboutTheSite(lower)) return false;
|
|
6397
6610
|
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6398
6611
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6399
6612
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
@@ -6869,6 +7082,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
6869
7082
|
return held;
|
|
6870
7083
|
};
|
|
6871
7084
|
const fresh = /* @__PURE__ */ new Set();
|
|
7085
|
+
const freshFiles = /* @__PURE__ */ new Map();
|
|
6872
7086
|
const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
|
|
6873
7087
|
const placementOf = (slug, section) => {
|
|
6874
7088
|
const bind = bindOf(section);
|
|
@@ -7196,6 +7410,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
7196
7410
|
}
|
|
7197
7411
|
extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
|
|
7198
7412
|
fresh.add(resolved.slug);
|
|
7413
|
+
freshFiles.set(resolved.slug, path);
|
|
7199
7414
|
addFile(outFiles, contents, {
|
|
7200
7415
|
path,
|
|
7201
7416
|
operation: existing === void 0 ? "add" : "modify",
|
|
@@ -7281,6 +7496,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
7281
7496
|
sections,
|
|
7282
7497
|
// What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
|
|
7283
7498
|
components: [...fresh].sort(),
|
|
7499
|
+
componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
|
|
7284
7500
|
planPending,
|
|
7285
7501
|
notes
|
|
7286
7502
|
}
|
|
@@ -7460,7 +7676,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7460
7676
|
for (const section of sections) {
|
|
7461
7677
|
const paths = fieldPaths(section);
|
|
7462
7678
|
const literals = new Set(
|
|
7463
|
-
briefPaths.filter((p) => (p.scope ?? "page") === "page" &&
|
|
7679
|
+
briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
|
|
7464
7680
|
);
|
|
7465
7681
|
const hits = /* @__PURE__ */ new Map();
|
|
7466
7682
|
for (const source of sources) {
|
|
@@ -7471,7 +7687,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7471
7687
|
const found = /* @__PURE__ */ new Map();
|
|
7472
7688
|
for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
|
|
7473
7689
|
if (declaration2.scope !== "page") continue;
|
|
7474
|
-
if (
|
|
7690
|
+
if (!inSection(section, declaration2.path)) continue;
|
|
7475
7691
|
const placed2 = elements.get(declaration2.at);
|
|
7476
7692
|
if (placed2) found.set(declaration2.at, placed2);
|
|
7477
7693
|
}
|
|
@@ -7496,6 +7712,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7496
7712
|
return { placed, misses };
|
|
7497
7713
|
}
|
|
7498
7714
|
var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
|
|
7715
|
+
var inSection = (section, path) => {
|
|
7716
|
+
if (groupOf(path) === section.groupKey) return true;
|
|
7717
|
+
const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
|
|
7718
|
+
const target = rowless(path);
|
|
7719
|
+
return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
|
|
7720
|
+
};
|
|
7499
7721
|
var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
|
|
7500
7722
|
function elementIndex(roots) {
|
|
7501
7723
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -7598,6 +7820,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7598
7820
|
...addresses.opaque
|
|
7599
7821
|
];
|
|
7600
7822
|
if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
|
|
7823
|
+
const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
|
|
7824
|
+
const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
|
|
7825
|
+
if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
|
|
7601
7826
|
const cut = content.slice(resolved.root.start, resolved.root.end);
|
|
7602
7827
|
const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
|
|
7603
7828
|
const introduced = allocateNames(reserved, [
|
|
@@ -7616,7 +7841,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7616
7841
|
const helpers = [
|
|
7617
7842
|
.../* @__PURE__ */ new Set([
|
|
7618
7843
|
...helperNames.map((h) => alias(h.name, h.local)),
|
|
7619
|
-
"bcmsSection",
|
|
7844
|
+
flat3 ? "bcmsSectionPaths" : "bcmsSection",
|
|
7620
7845
|
...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
|
|
7621
7846
|
])
|
|
7622
7847
|
].sort();
|
|
@@ -7646,9 +7871,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7646
7871
|
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
7647
7872
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
7648
7873
|
` : "";
|
|
7649
|
-
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) }
|
|
7650
|
-
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
|
|
7874
|
+
const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
|
|
7875
|
+
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
|
|
7651
7876
|
const declared = [copy, field].filter(Boolean).join("\n");
|
|
7877
|
+
const styles = dialect === "astro" ? carriedStyles(content) : "";
|
|
7652
7878
|
if (dialect === "astro") {
|
|
7653
7879
|
return { free: [], duplicates: [], unconvertible: [], content: `---
|
|
7654
7880
|
${SECTION_MARKER}
|
|
@@ -7668,7 +7894,9 @@ const { blockId, bind, overrides, page } = Astro.props;
|
|
|
7668
7894
|
${carriedDeclarations}${declared}
|
|
7669
7895
|
---
|
|
7670
7896
|
${markup}
|
|
7671
|
-
`
|
|
7897
|
+
${styles ? `
|
|
7898
|
+
${styles}
|
|
7899
|
+
` : ""}` };
|
|
7672
7900
|
}
|
|
7673
7901
|
return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
|
|
7674
7902
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
@@ -7942,6 +8170,9 @@ import { bcmsFields } from "${helper}";`);
|
|
|
7942
8170
|
return { files, refused };
|
|
7943
8171
|
}
|
|
7944
8172
|
function propPrefix(section) {
|
|
8173
|
+
return exactPrefix(section) ?? flatPrefix(section);
|
|
8174
|
+
}
|
|
8175
|
+
function exactPrefix(section) {
|
|
7945
8176
|
const prefixes = /* @__PURE__ */ new Set();
|
|
7946
8177
|
for (const field of section.fields ?? []) {
|
|
7947
8178
|
const { path, key } = field;
|
|
@@ -7952,6 +8183,49 @@ function propPrefix(section) {
|
|
|
7952
8183
|
if (prefixes.size > 1) return null;
|
|
7953
8184
|
return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
|
|
7954
8185
|
}
|
|
8186
|
+
var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
8187
|
+
function flatPrefix(section) {
|
|
8188
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
8189
|
+
let informative = false;
|
|
8190
|
+
for (const field of section.fields ?? []) {
|
|
8191
|
+
const { path, key } = field;
|
|
8192
|
+
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
8193
|
+
informative = true;
|
|
8194
|
+
const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
|
|
8195
|
+
if (!prefix) return null;
|
|
8196
|
+
prefixes.add(prefix);
|
|
8197
|
+
}
|
|
8198
|
+
return informative && prefixes.size === 1 ? [...prefixes][0] : null;
|
|
8199
|
+
}
|
|
8200
|
+
function flatPathMap(section, prefix) {
|
|
8201
|
+
const scalars = {};
|
|
8202
|
+
const tables = {};
|
|
8203
|
+
for (const field of section.fields ?? []) {
|
|
8204
|
+
const { path, key } = field;
|
|
8205
|
+
if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
|
|
8206
|
+
const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
|
|
8207
|
+
if (!row) {
|
|
8208
|
+
scalars[path] = field.overrideKey ?? key;
|
|
8209
|
+
continue;
|
|
8210
|
+
}
|
|
8211
|
+
const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
|
|
8212
|
+
if (!address) return null;
|
|
8213
|
+
const table = tables[row[1]] ??= { key: address[1], columns: {} };
|
|
8214
|
+
if (table.key !== address[1]) return null;
|
|
8215
|
+
table.columns[row[2]] = address[2];
|
|
8216
|
+
}
|
|
8217
|
+
return { scalars, tables };
|
|
8218
|
+
}
|
|
8219
|
+
function carriedStyles(content) {
|
|
8220
|
+
const blocks = [];
|
|
8221
|
+
for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
|
|
8222
|
+
const attributes = match[1] ?? "";
|
|
8223
|
+
if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
|
|
8224
|
+
if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
|
|
8225
|
+
blocks.push(match[0]);
|
|
8226
|
+
}
|
|
8227
|
+
return blocks.join("\n");
|
|
8228
|
+
}
|
|
7955
8229
|
function referencedNames2(markup) {
|
|
7956
8230
|
const out = /* @__PURE__ */ new Set();
|
|
7957
8231
|
for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
|
|
@@ -8273,7 +8547,7 @@ var ConvertError = class extends Error {
|
|
|
8273
8547
|
code;
|
|
8274
8548
|
};
|
|
8275
8549
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
8276
|
-
var
|
|
8550
|
+
var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8277
8551
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
8278
8552
|
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
8279
8553
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
@@ -8620,6 +8894,33 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8620
8894
|
else outFiles.push({ path, operation: "modify", content });
|
|
8621
8895
|
}
|
|
8622
8896
|
pending.push(...canvas.pending);
|
|
8897
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
8898
|
+
const pageFilesOrTemplate = (route, files) => {
|
|
8899
|
+
const all = [...files];
|
|
8900
|
+
const exact = pageFilesFor(route, all);
|
|
8901
|
+
if (exact.length > 0) return exact;
|
|
8902
|
+
const wanted = route.split("/").filter(Boolean);
|
|
8903
|
+
return all.filter((file) => {
|
|
8904
|
+
if (!isDynamicRoute(file)) return false;
|
|
8905
|
+
const pattern = routeOfFile(file)?.split("/").filter(Boolean);
|
|
8906
|
+
if (!pattern) return false;
|
|
8907
|
+
const rest = pattern.some((segment) => segment.startsWith("[..."));
|
|
8908
|
+
if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
|
|
8909
|
+
return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
|
|
8910
|
+
}).sort();
|
|
8911
|
+
};
|
|
8912
|
+
const routes = routeCounts(pending);
|
|
8913
|
+
for (const row of pending) {
|
|
8914
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8915
|
+
row.fix = fixFor(row, {
|
|
8916
|
+
literal: target?.literal ?? "",
|
|
8917
|
+
kind: row.kind,
|
|
8918
|
+
...target?.inline ? { inline: true } : {},
|
|
8919
|
+
candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
|
|
8920
|
+
contents: latest,
|
|
8921
|
+
routes: routes.get(`${row.scope}|${row.path}`) ?? 1
|
|
8922
|
+
});
|
|
8923
|
+
}
|
|
8623
8924
|
const receipt = assertReceipt({
|
|
8624
8925
|
briefDigest: digest,
|
|
8625
8926
|
paths: {
|
|
@@ -8832,13 +9133,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8832
9133
|
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
8833
9134
|
continue;
|
|
8834
9135
|
}
|
|
8835
|
-
const
|
|
8836
|
-
if (!assigned) {
|
|
8837
|
-
for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
8838
|
-
continue;
|
|
8839
|
-
}
|
|
9136
|
+
const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
|
|
8840
9137
|
for (const target of sharing) {
|
|
8841
|
-
|
|
9138
|
+
if (resolved.ambiguous.has(target.key)) {
|
|
9139
|
+
outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
9140
|
+
continue;
|
|
9141
|
+
}
|
|
9142
|
+
const mine = resolved.assigned.get(target.key) ?? [];
|
|
8842
9143
|
const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
|
|
8843
9144
|
if (rewritable.length === 0) {
|
|
8844
9145
|
outcomes.set(
|
|
@@ -9183,19 +9484,25 @@ function repeatGroups(targets) {
|
|
|
9183
9484
|
}
|
|
9184
9485
|
function resolvePositionally(targets, sites) {
|
|
9185
9486
|
const assigned = /* @__PURE__ */ new Map();
|
|
9186
|
-
const
|
|
9487
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
9488
|
+
const picked = /* @__PURE__ */ new Map();
|
|
9489
|
+
const wanters = /* @__PURE__ */ new Map();
|
|
9187
9490
|
for (const target of targets) {
|
|
9188
9491
|
const locator = target.locator;
|
|
9189
|
-
|
|
9190
|
-
const
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9492
|
+
const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
|
|
9493
|
+
const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
|
|
9494
|
+
if (!site) {
|
|
9495
|
+
ambiguous.add(target.key);
|
|
9496
|
+
continue;
|
|
9497
|
+
}
|
|
9498
|
+
picked.set(target.key, site);
|
|
9499
|
+
wanters.set(site, (wanters.get(site) ?? 0) + 1);
|
|
9500
|
+
}
|
|
9501
|
+
for (const [key, site] of picked) {
|
|
9502
|
+
if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
|
|
9503
|
+
else assigned.set(key, [site]);
|
|
9197
9504
|
}
|
|
9198
|
-
return assigned;
|
|
9505
|
+
return { assigned, ambiguous };
|
|
9199
9506
|
}
|
|
9200
9507
|
function isSubsequence(wanted, chain) {
|
|
9201
9508
|
let at = 0;
|
|
@@ -9218,7 +9525,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9218
9525
|
if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
|
|
9219
9526
|
const locals = helperLocals(reparsed, dialect, file);
|
|
9220
9527
|
const reads = DIALECT_RULES[dialect].text !== null;
|
|
9221
|
-
const boundary = (name) => `(?<![\\w$])${
|
|
9528
|
+
const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
|
|
9222
9529
|
const claimed = /* @__PURE__ */ new Set();
|
|
9223
9530
|
for (const element of elements) {
|
|
9224
9531
|
const candidates = targets.filter(
|
|
@@ -9226,7 +9533,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9226
9533
|
);
|
|
9227
9534
|
const match = candidates.find((target) => {
|
|
9228
9535
|
if (!reads) return true;
|
|
9229
|
-
const quoted =
|
|
9536
|
+
const quoted = escapeRegExp3(JSON.stringify(target.path));
|
|
9230
9537
|
if (target.scope === "layout") {
|
|
9231
9538
|
return locals.bcmsLayout.some(
|
|
9232
9539
|
(name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
|
|
@@ -9288,7 +9595,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9288
9595
|
// package.json
|
|
9289
9596
|
var package_default = {
|
|
9290
9597
|
name: "@bettercms-ai/convert",
|
|
9291
|
-
version: "0.
|
|
9598
|
+
version: "0.12.0",
|
|
9292
9599
|
type: "module",
|
|
9293
9600
|
description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
|
|
9294
9601
|
bin: {
|