@bettercms-ai/convert 0.3.0 → 0.5.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 +260 -0
- package/README.md +109 -16
- package/dist/cli.js +904 -110
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +96 -6
- package/dist/index.js +889 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
|
5
5
|
import { dirname, join as join2, relative, resolve, sep } from "path";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
import
|
|
8
|
+
import MagicString5 from "magic-string";
|
|
9
9
|
|
|
10
10
|
// src/brief.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -27,6 +27,21 @@ var BY_EXTENSION = {
|
|
|
27
27
|
".svelte": "svelte",
|
|
28
28
|
".vue": "vue"
|
|
29
29
|
};
|
|
30
|
+
var TOLERANT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
31
|
+
".md",
|
|
32
|
+
".mdx",
|
|
33
|
+
".njk",
|
|
34
|
+
".hbs",
|
|
35
|
+
".ejs",
|
|
36
|
+
".liquid",
|
|
37
|
+
".erb",
|
|
38
|
+
".twig"
|
|
39
|
+
]);
|
|
40
|
+
function isTolerantDialect(path) {
|
|
41
|
+
const lower = path.toLowerCase();
|
|
42
|
+
const at = lower.lastIndexOf(".");
|
|
43
|
+
return at >= 0 && TOLERANT_EXTENSIONS.has(lower.slice(at));
|
|
44
|
+
}
|
|
30
45
|
function dialectOf(path, _content) {
|
|
31
46
|
const lower = path.toLowerCase();
|
|
32
47
|
const at = lower.lastIndexOf(".");
|
|
@@ -134,7 +149,7 @@ function propsAttribute(props) {
|
|
|
134
149
|
|
|
135
150
|
// src/helper.ts
|
|
136
151
|
import { createHash as createHash2 } from "crypto";
|
|
137
|
-
var HELPER_VERSION =
|
|
152
|
+
var HELPER_VERSION = 4;
|
|
138
153
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
139
154
|
var HELPER_PATH = Object.fromEntries(
|
|
140
155
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -217,32 +232,97 @@ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: s
|
|
|
217
232
|
/**
|
|
218
233
|
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
219
234
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
235
|
+
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
236
|
+
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
237
|
+
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
238
|
+
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
239
|
+
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
240
|
+
*
|
|
241
|
+
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
242
|
+
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
243
|
+
*
|
|
244
|
+
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
245
|
+
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
246
|
+
* committed \`{}\` stub.
|
|
224
247
|
*/
|
|
225
248
|
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
249
|
const group = bind ? readPath(page, bind) : undefined;
|
|
230
|
-
|
|
250
|
+
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
251
|
+
if (!isRecord(overrides)) return base;
|
|
252
|
+
for (const key of Object.keys(overrides)) {
|
|
253
|
+
const value = overrides[key];
|
|
254
|
+
if (value === undefined || value === null) continue;
|
|
255
|
+
/**
|
|
256
|
+
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
257
|
+
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
258
|
+
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
259
|
+
*/
|
|
260
|
+
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
261
|
+
}
|
|
262
|
+
return base;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
267
|
+
*
|
|
268
|
+
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
269
|
+
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
270
|
+
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
271
|
+
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
272
|
+
* page form and the coverage meter already know.
|
|
273
|
+
*
|
|
274
|
+
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
275
|
+
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
276
|
+
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
277
|
+
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
278
|
+
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
279
|
+
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
280
|
+
* the placement's own group goes in.
|
|
281
|
+
*
|
|
282
|
+
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
283
|
+
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
284
|
+
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
285
|
+
*/
|
|
286
|
+
export function bcmsFields(
|
|
287
|
+
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
288
|
+
pathPrefix: string,
|
|
289
|
+
): (path: string) => string {
|
|
290
|
+
const id = placement.blockId;
|
|
291
|
+
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
292
|
+
/**
|
|
293
|
+
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
294
|
+
*
|
|
295
|
+
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
296
|
+
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
297
|
+
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
298
|
+
* so prepending one would invent an address rather than correct it.
|
|
299
|
+
*/
|
|
300
|
+
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
301
|
+
return (path: string): string => {
|
|
302
|
+
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
303
|
+
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
304
|
+
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
305
|
+
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
306
|
+
return lane + path.slice(pathPrefix.length);
|
|
307
|
+
};
|
|
231
308
|
}
|
|
232
309
|
`;
|
|
233
310
|
}
|
|
234
311
|
var HISTORICAL = {
|
|
235
312
|
"src/bcms-content.ts": {
|
|
236
313
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
237
|
-
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
314
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
315
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
238
316
|
},
|
|
239
317
|
"src/lib/bcms-content.ts": {
|
|
240
318
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
241
|
-
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002"
|
|
319
|
+
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
320
|
+
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e"
|
|
242
321
|
},
|
|
243
322
|
"composables/bcms-content.ts": {
|
|
244
323
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
245
|
-
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
|
|
324
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
325
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
246
326
|
}
|
|
247
327
|
};
|
|
248
328
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4198,6 +4278,7 @@ function importLines(imports) {
|
|
|
4198
4278
|
}
|
|
4199
4279
|
|
|
4200
4280
|
// src/receipt.ts
|
|
4281
|
+
var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
|
|
4201
4282
|
var ReceiptInvariantError = class extends Error {
|
|
4202
4283
|
code = "RECEIPT_INVARIANT";
|
|
4203
4284
|
constructor(message) {
|
|
@@ -4207,10 +4288,11 @@ var ReceiptInvariantError = class extends Error {
|
|
|
4207
4288
|
};
|
|
4208
4289
|
function assertReceipt(receipt) {
|
|
4209
4290
|
const { declared, rewritten, alreadyDeclared, pending } = receipt.paths;
|
|
4210
|
-
const
|
|
4291
|
+
const paths = pending.filter((p) => !CANVAS_REASONS.has(p.reason));
|
|
4292
|
+
const sum = rewritten + alreadyDeclared + paths.length;
|
|
4211
4293
|
if (sum !== declared) {
|
|
4212
4294
|
throw new ReceiptInvariantError(
|
|
4213
|
-
`The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${
|
|
4295
|
+
`The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${paths.length} pending = ${sum}.`
|
|
4214
4296
|
);
|
|
4215
4297
|
}
|
|
4216
4298
|
if (receipt.occurrences.rewritten > receipt.occurrences.located) {
|
|
@@ -4710,6 +4792,218 @@ function tryParse3(code) {
|
|
|
4710
4792
|
var PARSE_FILE = { html: parse2, astro: parseFile, jsx: parse, svelte: parseFile2, vue: parseFile3 };
|
|
4711
4793
|
var parseFile4 = (dialect, content) => Promise.resolve(PARSE_FILE[dialect](content));
|
|
4712
4794
|
|
|
4795
|
+
// src/canvas.ts
|
|
4796
|
+
import MagicString3 from "magic-string";
|
|
4797
|
+
var SSR_DRAFT_RECIPE = "Astro renders this site at build time, so there is no server to answer the editor's draft request. Either mount <BcmsLiveBlocks client:load> from @bettercms-ai/astro on the pages whose body is a block tree, or switch the preview routes to `output: 'server'` with the node adapter, mount @bettercms-ai/astro's draft routes, set BCMS_DRAFT_SECRET, redeploy, and switch the project to 'Run as a server app'.";
|
|
4798
|
+
var isNode = (value) => !!value && typeof value === "object" && typeof value.type === "string";
|
|
4799
|
+
var FUNCTIONS3 = /* @__PURE__ */ new Set([
|
|
4800
|
+
"FunctionDeclaration",
|
|
4801
|
+
"FunctionExpression",
|
|
4802
|
+
"ArrowFunctionExpression",
|
|
4803
|
+
"ObjectMethod",
|
|
4804
|
+
"ClassMethod"
|
|
4805
|
+
]);
|
|
4806
|
+
function patternNames2(node, out) {
|
|
4807
|
+
if (!isNode(node)) return;
|
|
4808
|
+
switch (node.type) {
|
|
4809
|
+
case "Identifier":
|
|
4810
|
+
out.add(String(node.name));
|
|
4811
|
+
return;
|
|
4812
|
+
case "ObjectPattern":
|
|
4813
|
+
for (const p of node.properties ?? [])
|
|
4814
|
+
patternNames2(p.type === "RestElement" ? p.argument : p.value, out);
|
|
4815
|
+
return;
|
|
4816
|
+
case "ArrayPattern":
|
|
4817
|
+
for (const el of node.elements ?? []) patternNames2(el, out);
|
|
4818
|
+
return;
|
|
4819
|
+
case "AssignmentPattern":
|
|
4820
|
+
patternNames2(node.left, out);
|
|
4821
|
+
return;
|
|
4822
|
+
case "RestElement":
|
|
4823
|
+
patternNames2(node.argument, out);
|
|
4824
|
+
return;
|
|
4825
|
+
default:
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
function statementNames2(statements, out) {
|
|
4830
|
+
for (const statement of statements ?? []) {
|
|
4831
|
+
if (!isNode(statement)) continue;
|
|
4832
|
+
const node = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
4833
|
+
if (!isNode(node)) continue;
|
|
4834
|
+
if (node.type === "VariableDeclaration")
|
|
4835
|
+
for (const d of node.declarations ?? []) patternNames2(d.id, out);
|
|
4836
|
+
else if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") patternNames2(node.id, out);
|
|
4837
|
+
else if (node.type === "ImportDeclaration")
|
|
4838
|
+
for (const specifier of node.specifiers ?? []) patternNames2(specifier.local, out);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
function functionBindings(fn) {
|
|
4842
|
+
const out = /* @__PURE__ */ new Set();
|
|
4843
|
+
for (const param of fn.params ?? []) patternNames2(param, out);
|
|
4844
|
+
const body = fn.body;
|
|
4845
|
+
if (body?.type === "BlockStatement") statementNames2(body.body, out);
|
|
4846
|
+
return out;
|
|
4847
|
+
}
|
|
4848
|
+
function stringLiterals(node, out) {
|
|
4849
|
+
if (!isNode(node)) {
|
|
4850
|
+
if (Array.isArray(node)) for (const child of node) stringLiterals(child, out);
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4853
|
+
if (FUNCTIONS3.has(node.type)) return;
|
|
4854
|
+
if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
|
|
4855
|
+
for (const key of Object.keys(node)) {
|
|
4856
|
+
if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
|
|
4857
|
+
stringLiterals(node[key], out);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
function usages(ast, local) {
|
|
4861
|
+
const found = [];
|
|
4862
|
+
const moduleScope2 = /* @__PURE__ */ new Set();
|
|
4863
|
+
statementNames2(ast?.program?.body, moduleScope2);
|
|
4864
|
+
const walk = (node, scope, literals) => {
|
|
4865
|
+
if (!isNode(node)) {
|
|
4866
|
+
if (Array.isArray(node)) for (const child of node) walk(child, scope, literals);
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
let nextScope = scope;
|
|
4870
|
+
let nextLiterals = literals;
|
|
4871
|
+
if (FUNCTIONS3.has(node.type)) {
|
|
4872
|
+
nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
|
|
4873
|
+
nextLiterals = /* @__PURE__ */ new Set();
|
|
4874
|
+
stringLiterals(node.body, nextLiterals);
|
|
4875
|
+
}
|
|
4876
|
+
if (node.type === "JSXElement") {
|
|
4877
|
+
const name = node.openingElement?.name;
|
|
4878
|
+
if (name?.type === "JSXIdentifier" && name.name === local)
|
|
4879
|
+
found.push({ element: node, scope: nextScope, literals: nextLiterals });
|
|
4880
|
+
}
|
|
4881
|
+
for (const key of Object.keys(node)) {
|
|
4882
|
+
if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
|
|
4883
|
+
walk(node[key], nextScope, nextLiterals);
|
|
4884
|
+
}
|
|
4885
|
+
};
|
|
4886
|
+
walk(ast?.program ?? ast, moduleScope2, /* @__PURE__ */ new Set());
|
|
4887
|
+
return found;
|
|
4888
|
+
}
|
|
4889
|
+
function bridgedAlready(ast) {
|
|
4890
|
+
const body = ast?.program?.body ?? [];
|
|
4891
|
+
return body.some(
|
|
4892
|
+
(statement) => statement.type === "ImportDeclaration" && String(statement.source?.value ?? "") === "@bettercms-ai/next/draft-bridge" && (statement.specifiers ?? []).some(
|
|
4893
|
+
(specifier) => specifier.imported?.name === "BcmsDraftBridge"
|
|
4894
|
+
)
|
|
4895
|
+
);
|
|
4896
|
+
}
|
|
4897
|
+
function blocksImport(ast) {
|
|
4898
|
+
const body = ast?.program?.body ?? [];
|
|
4899
|
+
for (const statement of body) {
|
|
4900
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
4901
|
+
const source = String(statement.source?.value ?? "");
|
|
4902
|
+
if (source !== "@bettercms-ai/next" && source !== "@bettercms-ai/next/blocks") continue;
|
|
4903
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
4904
|
+
const imported = specifier.imported;
|
|
4905
|
+
if (specifier.type !== "ImportSpecifier" || imported?.name !== "BcmsBlocks") continue;
|
|
4906
|
+
return { declaration: statement, specifier, local: String(specifier.local.name) };
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
function slugExpression(usage, routes) {
|
|
4912
|
+
if (usage.scope.has("slug")) return "{slug}";
|
|
4913
|
+
if (usage.scope.has("page")) return "{page.slug}";
|
|
4914
|
+
const matched = [...new Set([...usage.literals].map((l) => routes.get(l)).filter((s) => !!s))];
|
|
4915
|
+
return matched.length === 1 ? JSON.stringify(matched[0]) : null;
|
|
4916
|
+
}
|
|
4917
|
+
var lineOf = (content, offset) => content.slice(0, offset).split("\n").length;
|
|
4918
|
+
var pend = (reason, hint, file, line) => ({
|
|
4919
|
+
route: "",
|
|
4920
|
+
scope: "page",
|
|
4921
|
+
path: reason === "SSR_DRAFT_ROUTE" ? "canvas.lane" : "canvas.bridge",
|
|
4922
|
+
kind: "canvas",
|
|
4923
|
+
...file ? { file } : {},
|
|
4924
|
+
...line ? { line } : {},
|
|
4925
|
+
reason,
|
|
4926
|
+
hint
|
|
4927
|
+
});
|
|
4928
|
+
var ASTRO_LANE_PENDING = pend("SSR_DRAFT_ROUTE", SSR_DRAFT_RECIPE);
|
|
4929
|
+
function canvasBridge(sources, routes) {
|
|
4930
|
+
if (sources.some((s) => s.path.toLowerCase().endsWith(".astro")))
|
|
4931
|
+
return { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
|
|
4932
|
+
const files = [];
|
|
4933
|
+
const edits = /* @__PURE__ */ new Map();
|
|
4934
|
+
const pending = [];
|
|
4935
|
+
for (const source of sources) {
|
|
4936
|
+
if (!/\.[jt]sx$/i.test(source.path) || !source.ast) continue;
|
|
4937
|
+
if (bridgedAlready(source.ast)) {
|
|
4938
|
+
files.push(source.path);
|
|
4939
|
+
continue;
|
|
4940
|
+
}
|
|
4941
|
+
const imported = blocksImport(source.ast);
|
|
4942
|
+
if (!imported) continue;
|
|
4943
|
+
const found = usages(source.ast, imported.local);
|
|
4944
|
+
const slugs = found.map((usage) => slugExpression(usage, routes));
|
|
4945
|
+
const unresolved = found.findIndex((_, i) => slugs[i] === null);
|
|
4946
|
+
if (found.length === 0 || unresolved !== -1) {
|
|
4947
|
+
const at = found[unresolved === -1 ? 0 : unresolved]?.element.start;
|
|
4948
|
+
pending.push(
|
|
4949
|
+
pend(
|
|
4950
|
+
"CANVAS_BRIDGE_MANUAL",
|
|
4951
|
+
"Wrap this page's <BcmsBlocks> in <BcmsDraftBridge slug={\u2026}> from @bettercms-ai/next/draft-bridge. No slug expression was derivable in the component's own scope, so the codemod will not guess one: a bridge on the wrong slug never updates in the editor.",
|
|
4952
|
+
source.path,
|
|
4953
|
+
at === void 0 ? void 0 : lineOf(source.content, at)
|
|
4954
|
+
)
|
|
4955
|
+
);
|
|
4956
|
+
continue;
|
|
4957
|
+
}
|
|
4958
|
+
const out = new MagicString3(source.content);
|
|
4959
|
+
rewriteImport(out, imported);
|
|
4960
|
+
for (const [i, usage] of found.entries()) rewriteElement(out, usage, imported.local, slugs[i]);
|
|
4961
|
+
edits.set(source.path, out.toString());
|
|
4962
|
+
files.push(source.path);
|
|
4963
|
+
}
|
|
4964
|
+
return { lane: files.length > 0 ? "bridge" : "pending", files, edits, pending };
|
|
4965
|
+
}
|
|
4966
|
+
function rewriteImport(out, imported) {
|
|
4967
|
+
const { declaration: declaration2, specifier, local } = imported;
|
|
4968
|
+
const line = `import { BcmsDraftBridge${local === "BcmsBlocks" ? "" : ` as ${local}`} } from "@bettercms-ai/next/draft-bridge";`;
|
|
4969
|
+
const specifiers = declaration2.specifiers ?? [];
|
|
4970
|
+
const named = specifiers.filter((s) => s.type === "ImportSpecifier");
|
|
4971
|
+
if (named.length === 1) {
|
|
4972
|
+
if (specifiers.length === 1) {
|
|
4973
|
+
out.overwrite(declaration2.start, declaration2.end, line);
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
const other = specifiers.find((s) => s.type !== "ImportSpecifier");
|
|
4977
|
+
const close = out.original.indexOf("}", named[0].end);
|
|
4978
|
+
out.remove(other.end, close + 1);
|
|
4979
|
+
out.appendRight(declaration2.end, `
|
|
4980
|
+
${line}`);
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
const index = named.indexOf(specifier);
|
|
4984
|
+
const previous = named[index - 1];
|
|
4985
|
+
const from = previous ? previous.end : specifier.start;
|
|
4986
|
+
const to = previous ? specifier.end : named[index + 1].start;
|
|
4987
|
+
out.remove(from, to);
|
|
4988
|
+
out.appendRight(declaration2.end, `
|
|
4989
|
+
${line}`);
|
|
4990
|
+
}
|
|
4991
|
+
function rewriteElement(out, usage, local, slug) {
|
|
4992
|
+
const opening = usage.element.openingElement;
|
|
4993
|
+
const name = opening.name;
|
|
4994
|
+
if (local === "BcmsBlocks") {
|
|
4995
|
+
out.overwrite(name.start, name.end, "BcmsDraftBridge");
|
|
4996
|
+
const closing = usage.element.closingElement;
|
|
4997
|
+
if (closing) {
|
|
4998
|
+
const closingName = closing.name;
|
|
4999
|
+
out.overwrite(closingName.start, closingName.end, "BcmsDraftBridge");
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
const attributes = opening.attributes ?? [];
|
|
5003
|
+
const hasSlug = attributes.some((a) => a.name?.name === "slug");
|
|
5004
|
+
if (!hasSlug) out.appendLeft(name.end, ` slug=${slug}`);
|
|
5005
|
+
}
|
|
5006
|
+
|
|
4713
5007
|
// src/tolerant.ts
|
|
4714
5008
|
var escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4715
5009
|
var flexible = (literal) => new RegExp(escape(literal).split(" ").map(escape).join("\\s+"), "gu");
|
|
@@ -4839,6 +5133,7 @@ function readPlan(value) {
|
|
|
4839
5133
|
const section = check.record(held, at2);
|
|
4840
5134
|
check.segment(section.groupKey, `${at2}.groupKey`);
|
|
4841
5135
|
check.optionalString(section.sectionType, `${at2}.sectionType`);
|
|
5136
|
+
check.optionalString(section.copy, `${at2}.copy`);
|
|
4842
5137
|
if (section.fields !== void 0) {
|
|
4843
5138
|
for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
|
|
4844
5139
|
const shape = check.record(field, `${at2}.fields[${f}]`);
|
|
@@ -5130,10 +5425,10 @@ function scanExpression(code) {
|
|
|
5130
5425
|
for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
|
|
5131
5426
|
return;
|
|
5132
5427
|
case "VariableDeclarator":
|
|
5133
|
-
|
|
5428
|
+
patternNames3(node.id, bound);
|
|
5134
5429
|
return;
|
|
5135
5430
|
case "CatchClause":
|
|
5136
|
-
|
|
5431
|
+
patternNames3(node.param, bound);
|
|
5137
5432
|
return;
|
|
5138
5433
|
default:
|
|
5139
5434
|
return;
|
|
@@ -5141,8 +5436,8 @@ function scanExpression(code) {
|
|
|
5141
5436
|
});
|
|
5142
5437
|
walkAst(ast, (node) => {
|
|
5143
5438
|
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
|
|
5144
|
-
for (const param of node.params ?? [])
|
|
5145
|
-
if (node.id)
|
|
5439
|
+
for (const param of node.params ?? []) patternNames3(param, bound);
|
|
5440
|
+
if (node.id) patternNames3(node.id, bound);
|
|
5146
5441
|
}
|
|
5147
5442
|
});
|
|
5148
5443
|
const free = /* @__PURE__ */ new Set();
|
|
@@ -5157,7 +5452,7 @@ function nameOf2(node, out) {
|
|
|
5157
5452
|
nameOf2(node.name, out);
|
|
5158
5453
|
}
|
|
5159
5454
|
}
|
|
5160
|
-
function
|
|
5455
|
+
function patternNames3(node, out) {
|
|
5161
5456
|
if (!node || typeof node !== "object") return;
|
|
5162
5457
|
const typed = node;
|
|
5163
5458
|
switch (typed.type) {
|
|
@@ -5166,17 +5461,17 @@ function patternNames2(node, out) {
|
|
|
5166
5461
|
return;
|
|
5167
5462
|
case "ObjectPattern":
|
|
5168
5463
|
for (const property of typed.properties ?? []) {
|
|
5169
|
-
|
|
5464
|
+
patternNames3(property.type === "RestElement" ? property.argument : property.value, out);
|
|
5170
5465
|
}
|
|
5171
5466
|
return;
|
|
5172
5467
|
case "ArrayPattern":
|
|
5173
|
-
for (const element of typed.elements ?? [])
|
|
5468
|
+
for (const element of typed.elements ?? []) patternNames3(element, out);
|
|
5174
5469
|
return;
|
|
5175
5470
|
case "AssignmentPattern":
|
|
5176
|
-
|
|
5471
|
+
patternNames3(typed.left, out);
|
|
5177
5472
|
return;
|
|
5178
5473
|
case "RestElement":
|
|
5179
|
-
|
|
5474
|
+
patternNames3(typed.argument, out);
|
|
5180
5475
|
return;
|
|
5181
5476
|
default:
|
|
5182
5477
|
return;
|
|
@@ -5198,9 +5493,9 @@ function moduleScope(parsed, content) {
|
|
|
5198
5493
|
const text = source.slice(start, end);
|
|
5199
5494
|
const names = /* @__PURE__ */ new Set();
|
|
5200
5495
|
if (kind === "VariableDeclaration") {
|
|
5201
|
-
for (const declarator of statement.declarations ?? [])
|
|
5496
|
+
for (const declarator of statement.declarations ?? []) patternNames3(declarator.id, names);
|
|
5202
5497
|
} else {
|
|
5203
|
-
|
|
5498
|
+
patternNames3(statement.id, names);
|
|
5204
5499
|
}
|
|
5205
5500
|
if (names.size === 0) continue;
|
|
5206
5501
|
const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
|
|
@@ -5211,7 +5506,7 @@ function moduleScope(parsed, content) {
|
|
|
5211
5506
|
}
|
|
5212
5507
|
|
|
5213
5508
|
// src/componentize.ts
|
|
5214
|
-
import
|
|
5509
|
+
import MagicString4 from "magic-string";
|
|
5215
5510
|
var SectionInvariantError = class extends Error {
|
|
5216
5511
|
code = "SECTION_INVARIANT";
|
|
5217
5512
|
constructor(message) {
|
|
@@ -5228,7 +5523,8 @@ function assertSections(total, sections) {
|
|
|
5228
5523
|
}
|
|
5229
5524
|
return sections;
|
|
5230
5525
|
}
|
|
5231
|
-
var SECTION_MARKER = "// @bettercms-ai/convert section
|
|
5526
|
+
var SECTION_MARKER = "// @bettercms-ai/convert section v2";
|
|
5527
|
+
var SECTION_MARKERS = [SECTION_MARKER, "// @bettercms-ai/convert section v1"];
|
|
5232
5528
|
var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
|
|
5233
5529
|
var COMPONENT_DIR = "src/components/bcms";
|
|
5234
5530
|
var SECTIONS_LIB = "src/lib/bcms-sections.ts";
|
|
@@ -5250,12 +5546,53 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5250
5546
|
let registered = 0;
|
|
5251
5547
|
let planPending = 0;
|
|
5252
5548
|
let total = 0;
|
|
5549
|
+
const pend2 = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
|
|
5253
5550
|
const brief = readBrief(briefIn);
|
|
5254
5551
|
const plan2 = readPlan(planIn);
|
|
5552
|
+
const copy = options?.copy ?? "bind";
|
|
5553
|
+
const instanceOwned = (section) => section.copy === "instance";
|
|
5554
|
+
const notes = unreadableConfigs(sources).map(
|
|
5555
|
+
(path) => ({
|
|
5556
|
+
code: "TSCONFIG_UNREADABLE",
|
|
5557
|
+
file: path,
|
|
5558
|
+
message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
5559
|
+
})
|
|
5560
|
+
);
|
|
5561
|
+
const refuseRun = (reason, message) => {
|
|
5562
|
+
for (const page of plan2.pages ?? []) {
|
|
5563
|
+
for (const section of page.sections ?? []) {
|
|
5564
|
+
if (section.pending) {
|
|
5565
|
+
planPending++;
|
|
5566
|
+
continue;
|
|
5567
|
+
}
|
|
5568
|
+
total++;
|
|
5569
|
+
pend2(page.route, section.groupKey, reason, message);
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
return {
|
|
5573
|
+
files: [],
|
|
5574
|
+
receipt: {
|
|
5575
|
+
planDigest: plan2.digest ?? "",
|
|
5576
|
+
copy,
|
|
5577
|
+
sections: assertSections(total, { extracted: 0, inlineOnly: 0, alreadyExtracted: 0, registered: 0, pending }),
|
|
5578
|
+
components: [],
|
|
5579
|
+
planPending,
|
|
5580
|
+
notes
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
};
|
|
5584
|
+
const movesCopy = (plan2.pages ?? []).some(
|
|
5585
|
+
(page) => (page.sections ?? []).some((section) => !section.pending && instanceOwned(section))
|
|
5586
|
+
);
|
|
5587
|
+
if (copy === "instance" && !movesCopy) {
|
|
5588
|
+
return refuseRun(
|
|
5589
|
+
"COPY_MODE_MISMATCH",
|
|
5590
|
+
'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.'
|
|
5591
|
+
);
|
|
5592
|
+
}
|
|
5255
5593
|
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
5256
5594
|
const fileSet = new Set(contents.keys());
|
|
5257
5595
|
const aliases = aliasesFrom(sources);
|
|
5258
|
-
const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
|
|
5259
5596
|
const trees = /* @__PURE__ */ new Map();
|
|
5260
5597
|
const treeOf = async (file) => {
|
|
5261
5598
|
if (trees.has(file)) return trees.get(file);
|
|
@@ -5269,7 +5606,9 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5269
5606
|
const helperPath = HELPER_PATH.astro;
|
|
5270
5607
|
const helperExisting = contents.get(helperPath);
|
|
5271
5608
|
const helperVersion = helperExisting === void 0 ? null : isKnownHelper(helperExisting, "astro");
|
|
5272
|
-
|
|
5609
|
+
if (helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true) {
|
|
5610
|
+
return refuseRun("HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
|
|
5611
|
+
}
|
|
5273
5612
|
const outFiles = /* @__PURE__ */ new Map();
|
|
5274
5613
|
const written = /* @__PURE__ */ new Map();
|
|
5275
5614
|
const shipped = /* @__PURE__ */ new Map();
|
|
@@ -5279,6 +5618,29 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5279
5618
|
return held;
|
|
5280
5619
|
};
|
|
5281
5620
|
const fresh = /* @__PURE__ */ new Set();
|
|
5621
|
+
const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
|
|
5622
|
+
const placementOf = (slug, section) => {
|
|
5623
|
+
const bind = bindOf(section);
|
|
5624
|
+
return bind === void 0 ? { slug } : { slug, bind };
|
|
5625
|
+
};
|
|
5626
|
+
const rebind = (dialect, pageSlug, all, usable) => {
|
|
5627
|
+
if (dialect === null || !EXTRACTABLE.includes(dialect)) return;
|
|
5628
|
+
const order = shipped.get(dialect);
|
|
5629
|
+
const held = order?.get(pageSlug);
|
|
5630
|
+
if (!order || !held) return;
|
|
5631
|
+
const queues = /* @__PURE__ */ new Map();
|
|
5632
|
+
for (const section of all) {
|
|
5633
|
+
const slug = resolveComponent(plan2, section).slug;
|
|
5634
|
+
queues.set(slug, [...queues.get(slug) ?? [], section]);
|
|
5635
|
+
}
|
|
5636
|
+
order.set(
|
|
5637
|
+
pageSlug,
|
|
5638
|
+
held.map((placement) => {
|
|
5639
|
+
const section = queues.get(placement.slug)?.shift();
|
|
5640
|
+
return section && usable.has(section) ? placementOf(placement.slug, section) : placement;
|
|
5641
|
+
})
|
|
5642
|
+
);
|
|
5643
|
+
};
|
|
5282
5644
|
const registryConflicts = [];
|
|
5283
5645
|
const registryUnreadable = [];
|
|
5284
5646
|
for (const dialect of EXTRACTABLE) {
|
|
@@ -5304,6 +5666,11 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5304
5666
|
if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
|
|
5305
5667
|
const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
|
|
5306
5668
|
const registryUnread = registryUnreadable.length > 0;
|
|
5669
|
+
const migrated = registryBlocked || registryUnread ? { files: [], refused: /* @__PURE__ */ new Map() } : await migrateSections(plan2, contents, written, fileSet, aliases);
|
|
5670
|
+
for (const file of migrated.files) {
|
|
5671
|
+
addFile(outFiles, contents, file);
|
|
5672
|
+
contents.set(file.path, file.content);
|
|
5673
|
+
}
|
|
5307
5674
|
for (const page of plan2.pages ?? []) {
|
|
5308
5675
|
const sections2 = (page.sections ?? []).filter((section) => {
|
|
5309
5676
|
if (section.pending) {
|
|
@@ -5314,24 +5681,34 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5314
5681
|
});
|
|
5315
5682
|
total += sections2.length;
|
|
5316
5683
|
if (sections2.length === 0) continue;
|
|
5684
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
5685
|
+
for (const section of sections2) {
|
|
5686
|
+
const prefix = propPrefix(section);
|
|
5687
|
+
if (prefix === null) {
|
|
5688
|
+
pend2(
|
|
5689
|
+
page.route,
|
|
5690
|
+
section.groupKey,
|
|
5691
|
+
"PROP_MAP_INCONSISTENT",
|
|
5692
|
+
"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."
|
|
5693
|
+
);
|
|
5694
|
+
continue;
|
|
5695
|
+
}
|
|
5696
|
+
prefixes.set(section, prefix);
|
|
5697
|
+
}
|
|
5698
|
+
const usable = sections2.filter((section) => prefixes.has(section));
|
|
5699
|
+
if (usable.length === 0) continue;
|
|
5317
5700
|
const pendingBefore = pending.length;
|
|
5318
5701
|
const briefPage = brief.pages.find((p) => p.route === page.route);
|
|
5319
5702
|
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
5703
|
if (registryBlocked) {
|
|
5327
|
-
for (const section of
|
|
5328
|
-
|
|
5704
|
+
for (const section of usable) {
|
|
5705
|
+
pend2(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
|
|
5329
5706
|
}
|
|
5330
5707
|
continue;
|
|
5331
5708
|
}
|
|
5332
5709
|
if (registryUnread) {
|
|
5333
|
-
for (const section of
|
|
5334
|
-
|
|
5710
|
+
for (const section of usable) {
|
|
5711
|
+
pend2(
|
|
5335
5712
|
page.route,
|
|
5336
5713
|
section.groupKey,
|
|
5337
5714
|
"REGISTRY_UNREADABLE",
|
|
@@ -5342,36 +5719,49 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5342
5719
|
}
|
|
5343
5720
|
const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
|
|
5344
5721
|
if (!snapshotUsable(contents.get(snapshotPath))) {
|
|
5345
|
-
for (const section of
|
|
5346
|
-
|
|
5722
|
+
for (const section of usable) {
|
|
5723
|
+
pend2(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
|
|
5347
5724
|
}
|
|
5348
5725
|
continue;
|
|
5349
5726
|
}
|
|
5350
5727
|
const pageFiles = pageFilesFor(page.route, contents.keys());
|
|
5351
5728
|
if (pageFiles.length > 1) {
|
|
5352
|
-
for (const section of
|
|
5353
|
-
|
|
5729
|
+
for (const section of usable) {
|
|
5730
|
+
pend2(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
|
|
5354
5731
|
}
|
|
5355
5732
|
continue;
|
|
5356
5733
|
}
|
|
5357
5734
|
const file = pageFiles[0];
|
|
5358
5735
|
if (file === void 0) {
|
|
5359
|
-
for (const section of
|
|
5360
|
-
|
|
5736
|
+
for (const section of usable) {
|
|
5737
|
+
pend2(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
|
|
5361
5738
|
}
|
|
5362
5739
|
continue;
|
|
5363
5740
|
}
|
|
5364
|
-
const
|
|
5741
|
+
const pageDialect = dialectOf(file, contents.get(file) ?? "");
|
|
5742
|
+
const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, usable));
|
|
5743
|
+
let stuck = 0;
|
|
5744
|
+
for (const section of usable) {
|
|
5745
|
+
if (!done.has(section.groupKey)) continue;
|
|
5746
|
+
const why = pageDialect ? migrated.refused.get(refusalKey(pageDialect, resolveComponent(plan2, section).slug)) : void 0;
|
|
5747
|
+
if (why === void 0) continue;
|
|
5748
|
+
done.delete(section.groupKey);
|
|
5749
|
+
stuck++;
|
|
5750
|
+
pend2(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
|
|
5751
|
+
}
|
|
5365
5752
|
alreadyExtracted += done.size;
|
|
5366
|
-
if (done.size ===
|
|
5753
|
+
if (done.size + stuck === usable.length) {
|
|
5754
|
+
rebind(pageDialect, pageSlug, page.sections ?? [], new Set(usable));
|
|
5755
|
+
continue;
|
|
5756
|
+
}
|
|
5367
5757
|
const found = await locateSections(
|
|
5368
|
-
|
|
5758
|
+
usable.filter((section) => !done.has(section.groupKey)),
|
|
5369
5759
|
briefPage?.paths ?? [],
|
|
5370
5760
|
sources,
|
|
5371
5761
|
treeOf
|
|
5372
5762
|
);
|
|
5373
5763
|
for (const [groupKey, reason] of found.misses) {
|
|
5374
|
-
|
|
5764
|
+
pend2(page.route, groupKey, reason.code, reason.message);
|
|
5375
5765
|
}
|
|
5376
5766
|
const live = found.placed;
|
|
5377
5767
|
if (live.length === 0) continue;
|
|
@@ -5379,14 +5769,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5379
5769
|
const dialect = dialectOf(file, content);
|
|
5380
5770
|
if (!dialect || !EXTRACTABLE.includes(dialect)) {
|
|
5381
5771
|
for (const placed of live) {
|
|
5382
|
-
|
|
5772
|
+
pend2(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
|
|
5383
5773
|
}
|
|
5384
5774
|
continue;
|
|
5385
5775
|
}
|
|
5386
5776
|
const tree = await treeOf(file);
|
|
5387
5777
|
if (!tree) {
|
|
5388
5778
|
for (const placed of live) {
|
|
5389
|
-
|
|
5779
|
+
pend2(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
|
|
5390
5780
|
}
|
|
5391
5781
|
continue;
|
|
5392
5782
|
}
|
|
@@ -5411,7 +5801,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5411
5801
|
const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
|
|
5412
5802
|
if (elements && elements.length > 0) {
|
|
5413
5803
|
if (alsoRendered.length > 0) {
|
|
5414
|
-
|
|
5804
|
+
pend2(
|
|
5415
5805
|
page.route,
|
|
5416
5806
|
placed.section.groupKey,
|
|
5417
5807
|
"SECTION_ROOT_AMBIGUOUS",
|
|
@@ -5423,7 +5813,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5423
5813
|
continue;
|
|
5424
5814
|
}
|
|
5425
5815
|
if (alsoRendered.length > 1) {
|
|
5426
|
-
|
|
5816
|
+
pend2(
|
|
5427
5817
|
page.route,
|
|
5428
5818
|
placed.section.groupKey,
|
|
5429
5819
|
"SECTION_ROOT_AMBIGUOUS",
|
|
@@ -5437,7 +5827,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5437
5827
|
importedSections.push({ section: placed.section, path: only, call: held.local });
|
|
5438
5828
|
continue;
|
|
5439
5829
|
}
|
|
5440
|
-
|
|
5830
|
+
pend2(
|
|
5441
5831
|
page.route,
|
|
5442
5832
|
placed.section.groupKey,
|
|
5443
5833
|
"SECTION_ROOT_AMBIGUOUS",
|
|
@@ -5451,7 +5841,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5451
5841
|
const component = resolveComponent(plan2, held.section);
|
|
5452
5842
|
const already = extractable.get(component.slug);
|
|
5453
5843
|
if (already !== void 0 && already.path !== held.path) {
|
|
5454
|
-
|
|
5844
|
+
pend2(
|
|
5455
5845
|
page.route,
|
|
5456
5846
|
held.section.groupKey,
|
|
5457
5847
|
"COMPONENT_CONFLICT",
|
|
@@ -5462,7 +5852,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5462
5852
|
const binding = bindingOf(held.path);
|
|
5463
5853
|
const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
|
|
5464
5854
|
if (binding.kind === "namespace" && member === void 0) {
|
|
5465
|
-
|
|
5855
|
+
pend2(
|
|
5466
5856
|
page.route,
|
|
5467
5857
|
held.section.groupKey,
|
|
5468
5858
|
"SECTION_ROOT_AMBIGUOUS",
|
|
@@ -5480,7 +5870,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5480
5870
|
registered++;
|
|
5481
5871
|
registeredHere.push({
|
|
5482
5872
|
slug: component.slug,
|
|
5483
|
-
|
|
5873
|
+
section: held.section,
|
|
5484
5874
|
call: callSiteOf(tree, member ? `${binding.local}.${member}` : held.call)
|
|
5485
5875
|
});
|
|
5486
5876
|
}
|
|
@@ -5488,11 +5878,17 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5488
5878
|
for (const placed of placedHere) {
|
|
5489
5879
|
const root = lowestCommonAncestor(placed.elements);
|
|
5490
5880
|
if (!root) {
|
|
5491
|
-
|
|
5881
|
+
pend2(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
|
|
5492
5882
|
continue;
|
|
5493
5883
|
}
|
|
5494
5884
|
const component = resolveComponent(plan2, placed.section);
|
|
5495
|
-
roots.push({
|
|
5885
|
+
roots.push({
|
|
5886
|
+
section: placed.section,
|
|
5887
|
+
route: page.route,
|
|
5888
|
+
root,
|
|
5889
|
+
pathPrefix: prefixes.get(placed.section),
|
|
5890
|
+
...component
|
|
5891
|
+
});
|
|
5496
5892
|
}
|
|
5497
5893
|
const nested = /* @__PURE__ */ new Set();
|
|
5498
5894
|
for (const a of roots) {
|
|
@@ -5504,7 +5900,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5504
5900
|
}
|
|
5505
5901
|
}
|
|
5506
5902
|
for (const resolved of nested) {
|
|
5507
|
-
|
|
5903
|
+
pend2(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
|
|
5508
5904
|
}
|
|
5509
5905
|
const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
|
|
5510
5906
|
const placeable = [];
|
|
@@ -5512,16 +5908,34 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5512
5908
|
const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
|
|
5513
5909
|
const existing = contents.get(path);
|
|
5514
5910
|
if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
|
|
5515
|
-
|
|
5911
|
+
pend2(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
|
|
5516
5912
|
continue;
|
|
5517
5913
|
}
|
|
5518
5914
|
if (extractable.has(resolved.slug)) {
|
|
5519
5915
|
placeable.push(resolved);
|
|
5520
5916
|
continue;
|
|
5521
5917
|
}
|
|
5522
|
-
const built = componentFile(dialect, path, file, content, parsed, resolved);
|
|
5918
|
+
const built = componentFile(dialect, path, file, content, parsed, resolved, resolved.pathPrefix);
|
|
5919
|
+
if (built.unconvertible.length > 0) {
|
|
5920
|
+
pend2(
|
|
5921
|
+
page.route,
|
|
5922
|
+
resolved.section.groupKey,
|
|
5923
|
+
"PROP_MAP_INCONSISTENT",
|
|
5924
|
+
`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.`
|
|
5925
|
+
);
|
|
5926
|
+
continue;
|
|
5927
|
+
}
|
|
5928
|
+
if (built.duplicates.length > 0) {
|
|
5929
|
+
pend2(
|
|
5930
|
+
page.route,
|
|
5931
|
+
resolved.section.groupKey,
|
|
5932
|
+
"DUPLICATE_FIELD_BINDING",
|
|
5933
|
+
`${built.duplicates.join(", ")} carries more than one data-bcms-field, so there is no single address to rewrite.`
|
|
5934
|
+
);
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5523
5937
|
if (built.free.length > 0) {
|
|
5524
|
-
|
|
5938
|
+
pend2(
|
|
5525
5939
|
page.route,
|
|
5526
5940
|
resolved.section.groupKey,
|
|
5527
5941
|
"SECTION_FREE_IDENTIFIERS",
|
|
@@ -5540,14 +5954,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5540
5954
|
}
|
|
5541
5955
|
if (placeable.length === 0 && registeredHere.length === 0) continue;
|
|
5542
5956
|
const spans = [
|
|
5543
|
-
...placeable.map((r) => ({ slug: r.slug, bind: r.section
|
|
5544
|
-
...registeredHere.map((r) => ({ slug: r.slug, bind: r.
|
|
5957
|
+
...placeable.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.root })),
|
|
5958
|
+
...registeredHere.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.call }))
|
|
5545
5959
|
].sort((a, b) => (a.root?.start ?? Number.MAX_SAFE_INTEGER) - (b.root?.start ?? Number.MAX_SAFE_INTEGER));
|
|
5546
5960
|
const known = spans.filter((s) => s.root !== null);
|
|
5547
5961
|
const layout = runOf(known, parsed.roots, content);
|
|
5548
5962
|
if (layout === null) {
|
|
5549
5963
|
for (const resolved of placeable) {
|
|
5550
|
-
|
|
5964
|
+
pend2(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
|
|
5551
5965
|
}
|
|
5552
5966
|
continue;
|
|
5553
5967
|
}
|
|
@@ -5557,7 +5971,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5557
5971
|
shipped.set(dialect, order);
|
|
5558
5972
|
order.set(
|
|
5559
5973
|
pageSlug,
|
|
5560
|
-
spans.map((s) =>
|
|
5974
|
+
spans.map((s) => s.bind === void 0 ? { slug: s.slug } : { slug: s.slug, bind: s.bind })
|
|
5561
5975
|
);
|
|
5562
5976
|
if (useRun) {
|
|
5563
5977
|
addFile(outFiles, contents, {
|
|
@@ -5566,13 +5980,25 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5566
5980
|
content: splicePageRun(file, content, dialect, parsed, known, pageSlug)
|
|
5567
5981
|
});
|
|
5568
5982
|
extracted += placeable.length;
|
|
5569
|
-
} else
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5983
|
+
} else {
|
|
5984
|
+
const inline = placeable.filter((resolved) => !instanceOwned(resolved.section));
|
|
5985
|
+
for (const resolved of placeable) {
|
|
5986
|
+
if (inline.includes(resolved)) continue;
|
|
5987
|
+
pend2(
|
|
5988
|
+
page.route,
|
|
5989
|
+
resolved.section.groupKey,
|
|
5990
|
+
"INLINE_NEEDS_INSTANCE",
|
|
5991
|
+
`${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.`
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
if (inline.length > 0) {
|
|
5995
|
+
addFile(outFiles, contents, {
|
|
5996
|
+
path: file,
|
|
5997
|
+
operation: "modify",
|
|
5998
|
+
content: splicePageInline(file, content, dialect, parsed, inline, pageSlug, extractable)
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
inlineOnly += inline.length;
|
|
5576
6002
|
}
|
|
5577
6003
|
}
|
|
5578
6004
|
const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
|
|
@@ -5600,17 +6026,12 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
5600
6026
|
files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
5601
6027
|
receipt: {
|
|
5602
6028
|
planDigest: plan2.digest ?? "",
|
|
6029
|
+
copy,
|
|
5603
6030
|
sections,
|
|
5604
6031
|
// What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
|
|
5605
6032
|
components: [...fresh].sort(),
|
|
5606
6033
|
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
|
-
)
|
|
6034
|
+
notes
|
|
5614
6035
|
}
|
|
5615
6036
|
};
|
|
5616
6037
|
}
|
|
@@ -5633,7 +6054,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5633
6054
|
if (!from.specifier.startsWith(".")) continue;
|
|
5634
6055
|
const resolved = resolveSpecifier(path, from.specifier, files, aliases);
|
|
5635
6056
|
if (resolved !== null) {
|
|
5636
|
-
imports.set(local, { name: local, path: resolved, kind: from.kind, imported: from.imported });
|
|
6057
|
+
imports.set(local, { name: local, path: preferDialect(resolved, dialect, files), kind: from.kind, imported: from.imported });
|
|
5637
6058
|
}
|
|
5638
6059
|
}
|
|
5639
6060
|
const registry = blockOf(content, "REGISTRY");
|
|
@@ -5642,6 +6063,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5642
6063
|
const registryRows = propertiesOf(registry);
|
|
5643
6064
|
const shippedRows = propertiesOf(table);
|
|
5644
6065
|
if (registryRows === null || shippedRows === null) return null;
|
|
6066
|
+
if (registryRows.length === 0) return null;
|
|
5645
6067
|
for (const row of registryRows) {
|
|
5646
6068
|
const named = row.value.match(/^([A-Za-z_$][\w$]*)(?:\.([A-Za-z_$][\w$]*))?$/);
|
|
5647
6069
|
const entry = named ? imports.get(named[1]) : void 0;
|
|
@@ -5651,7 +6073,8 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5651
6073
|
for (const row of shippedRows) {
|
|
5652
6074
|
try {
|
|
5653
6075
|
const placements = JSON.parse(row.value);
|
|
5654
|
-
if (!Array.isArray(placements)) return null;
|
|
6076
|
+
if (!Array.isArray(placements) || placements.length === 0 || !placements.every(isPlacement)) return null;
|
|
6077
|
+
if (placements.some((placement) => !components.has(placement.slug))) return null;
|
|
5655
6078
|
shipped.set(row.key, placements);
|
|
5656
6079
|
} catch {
|
|
5657
6080
|
return null;
|
|
@@ -5659,6 +6082,18 @@ async function readRegistry(content, dialect, path, files, aliases) {
|
|
|
5659
6082
|
}
|
|
5660
6083
|
return { components, shipped };
|
|
5661
6084
|
}
|
|
6085
|
+
function preferDialect(resolved, dialect, files) {
|
|
6086
|
+
const wanted = dialect === "astro" ? [".astro"] : [".tsx", ".jsx"];
|
|
6087
|
+
if (wanted.some((extension) => resolved.endsWith(extension))) return resolved;
|
|
6088
|
+
const base = resolved.replace(/\.[^./]+$/, "");
|
|
6089
|
+
return wanted.map((extension) => `${base}${extension}`).find((candidate) => files.has(candidate)) ?? resolved;
|
|
6090
|
+
}
|
|
6091
|
+
function isPlacement(value) {
|
|
6092
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
6093
|
+
const held = value;
|
|
6094
|
+
if (typeof held.slug !== "string") return false;
|
|
6095
|
+
return ["bind", "blockId"].every((key) => held[key] === void 0 || typeof held[key] === "string");
|
|
6096
|
+
}
|
|
5662
6097
|
function blockOf(content, name) {
|
|
5663
6098
|
const declaration2 = content.indexOf(`const ${name}`);
|
|
5664
6099
|
if (declaration2 < 0) return null;
|
|
@@ -5905,14 +6340,35 @@ function pascal(value) {
|
|
|
5905
6340
|
return /^[A-Za-z]/.test(name) ? name : `Section${name}`;
|
|
5906
6341
|
}
|
|
5907
6342
|
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
|
|
5908
|
-
function componentFile(dialect, path, fromFile, content, parsed, resolved) {
|
|
5909
|
-
const
|
|
6343
|
+
function componentFile(dialect, path, fromFile, content, parsed, resolved, pathPrefix) {
|
|
6344
|
+
const addresses = fieldAddresses(content, elementsUnder(resolved.root));
|
|
6345
|
+
const unconvertible = [
|
|
6346
|
+
...addresses.heads.filter((address) => !address.startsWith(pathPrefix)),
|
|
6347
|
+
...addresses.opaque
|
|
6348
|
+
];
|
|
6349
|
+
if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
|
|
6350
|
+
const cut = content.slice(resolved.root.start, resolved.root.end);
|
|
6351
|
+
const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
|
|
6352
|
+
const introduced = allocateNames(reserved, [
|
|
6353
|
+
{ key: "bcmsFields", name: "bcmsFields" },
|
|
6354
|
+
{ key: "bcmsField", name: "bcmsField" }
|
|
6355
|
+
]);
|
|
6356
|
+
const fieldsName = introduced.get("bcmsFields");
|
|
6357
|
+
const fieldName = introduced.get("bcmsField");
|
|
6358
|
+
const { markup, bound, duplicates } = sectionMarkup(content, resolved.root, fieldName);
|
|
6359
|
+
if (duplicates.length > 0) return { content: "", free: [], duplicates, unconvertible: [] };
|
|
5910
6360
|
const groupKey = resolved.section.groupKey;
|
|
5911
6361
|
const locals = helperLocals(parsed, dialect, fromFile);
|
|
5912
6362
|
const helperNames = ["bcms", "bcmsRows", "bcmsLayout"].flatMap(
|
|
5913
6363
|
(name) => locals[name].filter((local) => mentions(markup, local)).map((local) => ({ name, local }))
|
|
5914
6364
|
);
|
|
5915
|
-
const helpers = [
|
|
6365
|
+
const helpers = [
|
|
6366
|
+
.../* @__PURE__ */ new Set([
|
|
6367
|
+
...helperNames.map((h) => alias(h.name, h.local)),
|
|
6368
|
+
"bcmsSection",
|
|
6369
|
+
...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
|
|
6370
|
+
])
|
|
6371
|
+
].sort();
|
|
5916
6372
|
const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
|
|
5917
6373
|
const snapshot = snapshotOf(parsed, dialect, fromFile, markup);
|
|
5918
6374
|
const carry = carryFor({
|
|
@@ -5927,20 +6383,23 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved) {
|
|
|
5927
6383
|
"bind",
|
|
5928
6384
|
"overrides",
|
|
5929
6385
|
"page",
|
|
6386
|
+
fieldName,
|
|
5930
6387
|
...helperNames.map((h) => h.local),
|
|
5931
6388
|
...snapshot ? [snapshot] : []
|
|
5932
6389
|
],
|
|
5933
6390
|
// The helper and the snapshots are what the component declares for itself.
|
|
5934
6391
|
skipSpecifier: (repoPath, specifier) => specifier.includes(`${CONTENT_DIR}/`) || repoPath.startsWith(HELPER_PATH[dialect].replace(/\.tsx?$/, ""))
|
|
5935
6392
|
});
|
|
5936
|
-
if (carry.free.length > 0) return { content: "", free: carry.free };
|
|
6393
|
+
if (carry.free.length > 0) return { content: "", free: carry.free, duplicates: [], unconvertible: [] };
|
|
5937
6394
|
const carried = [...carry.imports];
|
|
5938
6395
|
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
5939
6396
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
5940
6397
|
` : "";
|
|
5941
|
-
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind
|
|
6398
|
+
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
|
|
6399
|
+
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
|
|
6400
|
+
const declared = [copy, field].filter(Boolean).join("\n");
|
|
5942
6401
|
if (dialect === "astro") {
|
|
5943
|
-
return { free: [], content: `---
|
|
6402
|
+
return { free: [], duplicates: [], unconvertible: [], content: `---
|
|
5944
6403
|
${SECTION_MARKER}
|
|
5945
6404
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`].join("\n")}
|
|
5946
6405
|
|
|
@@ -5955,27 +6414,297 @@ interface Props {
|
|
|
5955
6414
|
page?: unknown;
|
|
5956
6415
|
}
|
|
5957
6416
|
const { blockId, bind, overrides, page } = Astro.props;
|
|
5958
|
-
${carriedDeclarations}${
|
|
6417
|
+
${carriedDeclarations}${declared}
|
|
5959
6418
|
---
|
|
5960
6419
|
${markup}
|
|
5961
6420
|
` };
|
|
5962
6421
|
}
|
|
5963
|
-
return { free: [], content: `${SECTION_MARKER}
|
|
6422
|
+
return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
|
|
5964
6423
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
5965
6424
|
${carriedDeclarations === "" ? "" : `
|
|
5966
6425
|
${carriedDeclarations}`}
|
|
5967
6426
|
export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
|
|
5968
|
-
${
|
|
6427
|
+
${declared.split("\n").join("\n ")}
|
|
5969
6428
|
return (
|
|
5970
6429
|
${markup}
|
|
5971
6430
|
);
|
|
5972
6431
|
}
|
|
5973
6432
|
` };
|
|
5974
6433
|
}
|
|
5975
|
-
function
|
|
5976
|
-
const slice = new
|
|
5977
|
-
|
|
5978
|
-
|
|
6434
|
+
function sectionMarkup(content, root, fieldName) {
|
|
6435
|
+
const slice = new MagicString4(content.slice(root.start, root.end));
|
|
6436
|
+
const at = (offset) => offset - root.start;
|
|
6437
|
+
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
6438
|
+
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
6439
|
+
else {
|
|
6440
|
+
slice.overwrite(at(stamps[0].range.start), at(stamps[0].range.end), "data-bcms-block={blockId}");
|
|
6441
|
+
for (const extra of stamps.slice(1)) {
|
|
6442
|
+
let from = extra.range.start;
|
|
6443
|
+
while (from > root.start && /\s/.test(content[from - 1])) from--;
|
|
6444
|
+
slice.remove(at(from), at(extra.range.end));
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
const wrapped = wrapFieldAttrs(slice, content, elementsUnder(root), at, fieldName);
|
|
6448
|
+
return { markup: slice.toString(), ...wrapped };
|
|
6449
|
+
}
|
|
6450
|
+
function wrapFieldAttrs(slice, content, elements, at, fieldName) {
|
|
6451
|
+
let bound = 0;
|
|
6452
|
+
const duplicates = [];
|
|
6453
|
+
for (const element of elements) {
|
|
6454
|
+
const fields = element.attrs.filter((attr) => attr.name === "data-bcms-field");
|
|
6455
|
+
if (fields.length > 1) {
|
|
6456
|
+
duplicates.push(`<${element.tag}>`);
|
|
6457
|
+
continue;
|
|
6458
|
+
}
|
|
6459
|
+
for (const attr of fields) {
|
|
6460
|
+
bound++;
|
|
6461
|
+
if (attr.expressionRange) {
|
|
6462
|
+
slice.appendRight(at(attr.expressionRange.start) + 1, `${fieldName}(`);
|
|
6463
|
+
slice.appendLeft(at(attr.expressionRange.end) - 1, ")");
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
const value = attr.valueRange ? content.slice(attr.valueRange.start, attr.valueRange.end) : "";
|
|
6467
|
+
slice.overwrite(
|
|
6468
|
+
at(attr.range.start),
|
|
6469
|
+
at(attr.range.end),
|
|
6470
|
+
`data-bcms-field={${fieldName}(${JSON.stringify(value)})}`
|
|
6471
|
+
);
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
return { bound, duplicates };
|
|
6475
|
+
}
|
|
6476
|
+
function provableAddress(expression) {
|
|
6477
|
+
const first = expression[0];
|
|
6478
|
+
if (first === '"' || first === "'") {
|
|
6479
|
+
for (let at = 1; at < expression.length; at++) {
|
|
6480
|
+
if (expression[at] === "\\") {
|
|
6481
|
+
at++;
|
|
6482
|
+
continue;
|
|
6483
|
+
}
|
|
6484
|
+
if (expression[at] === first) return at === expression.length - 1 ? expression.slice(1, at) : null;
|
|
6485
|
+
}
|
|
6486
|
+
return null;
|
|
6487
|
+
}
|
|
6488
|
+
if (first !== "`" || expression.length < 2 || !expression.endsWith("`")) return null;
|
|
6489
|
+
const body = expression.slice(1, -1);
|
|
6490
|
+
if (body.includes("`") || body.includes("\\")) return null;
|
|
6491
|
+
let head = null;
|
|
6492
|
+
for (let at = 0; at < body.length; at++) {
|
|
6493
|
+
if (body[at] !== "$" || body[at + 1] !== "{") continue;
|
|
6494
|
+
const close = body.indexOf("}", at + 2);
|
|
6495
|
+
if (close < 0) return null;
|
|
6496
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(body.slice(at + 2, close).trim())) return null;
|
|
6497
|
+
if (body[at - 1] !== "[" || body[close + 1] !== "]") return null;
|
|
6498
|
+
head ??= body.slice(0, at);
|
|
6499
|
+
at = close;
|
|
6500
|
+
}
|
|
6501
|
+
return head ?? body;
|
|
6502
|
+
}
|
|
6503
|
+
function fieldAddresses(content, elements) {
|
|
6504
|
+
const heads = [];
|
|
6505
|
+
const opaque = [];
|
|
6506
|
+
for (const element of elements) {
|
|
6507
|
+
for (const attr of element.attrs) {
|
|
6508
|
+
if (attr.name !== "data-bcms-field") continue;
|
|
6509
|
+
if (!attr.expression && attr.valueRange) {
|
|
6510
|
+
heads.push(content.slice(attr.valueRange.start, attr.valueRange.end));
|
|
6511
|
+
continue;
|
|
6512
|
+
}
|
|
6513
|
+
const inner = attr.expressionRange ? content.slice(attr.expressionRange.start + 1, attr.expressionRange.end - 1).trim() : "";
|
|
6514
|
+
const head = provableAddress(inner);
|
|
6515
|
+
if (head === null) opaque.push(`data-bcms-field={${inner}}`);
|
|
6516
|
+
else heads.push(head);
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
return { heads, opaque };
|
|
6520
|
+
}
|
|
6521
|
+
function elementsUnder(...roots) {
|
|
6522
|
+
const out = [];
|
|
6523
|
+
const walk = (node) => {
|
|
6524
|
+
if (node.type === "element") {
|
|
6525
|
+
out.push(node);
|
|
6526
|
+
for (const child of node.children) walk(child);
|
|
6527
|
+
return;
|
|
6528
|
+
}
|
|
6529
|
+
if (node.type === "expr") for (const child of node.children) walk(child);
|
|
6530
|
+
};
|
|
6531
|
+
for (const root of roots) walk(root);
|
|
6532
|
+
return out;
|
|
6533
|
+
}
|
|
6534
|
+
var V1_ANCHOR = {
|
|
6535
|
+
astro: "const { blockId, bind, overrides, page } = Astro.props;",
|
|
6536
|
+
jsx: "}: BcmsSectionProps) {"
|
|
6537
|
+
};
|
|
6538
|
+
var refusalKey = (dialect, slug) => `${dialect}\0${slug}`;
|
|
6539
|
+
function generatedBindings(parsed) {
|
|
6540
|
+
const program = parsed.ast?.program;
|
|
6541
|
+
const body = program?.body ?? [];
|
|
6542
|
+
const scopes = [body];
|
|
6543
|
+
for (const statement of body) {
|
|
6544
|
+
const declaration2 = statement.type === "ExportDefaultDeclaration" ? statement.declaration : void 0;
|
|
6545
|
+
const inner = declaration2?.type === "FunctionDeclaration" ? declaration2.body?.body : void 0;
|
|
6546
|
+
if (inner) scopes.push(inner);
|
|
6547
|
+
}
|
|
6548
|
+
const out = /* @__PURE__ */ new Map();
|
|
6549
|
+
for (const scope of scopes) {
|
|
6550
|
+
for (const statement of scope) {
|
|
6551
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
6552
|
+
for (const declarator of statement.declarations ?? []) {
|
|
6553
|
+
const id = declarator.id;
|
|
6554
|
+
const name = id?.type === "Identifier" ? id.name : void 0;
|
|
6555
|
+
if (name !== void 0 && !out.has(name)) out.set(name, declarator.init);
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
return out;
|
|
6560
|
+
}
|
|
6561
|
+
function stripBindFallback(parsed, source, content, locals) {
|
|
6562
|
+
if (!parsed.ast?.program) return "unreadable";
|
|
6563
|
+
const offset = parsed.script?.offset ?? 0;
|
|
6564
|
+
let rewritten = 0;
|
|
6565
|
+
let unproven = 0;
|
|
6566
|
+
walkAst(parsed.ast, (node) => {
|
|
6567
|
+
if (node.type !== "CallExpression") return;
|
|
6568
|
+
const argument = (node.arguments ?? [])[1];
|
|
6569
|
+
if (argument?.type !== "LogicalExpression" || argument.operator !== "??") return;
|
|
6570
|
+
const callee = node.callee;
|
|
6571
|
+
if (callee?.type !== "Identifier" || !locals.has(callee.name)) {
|
|
6572
|
+
unproven++;
|
|
6573
|
+
return;
|
|
6574
|
+
}
|
|
6575
|
+
const left = argument.left;
|
|
6576
|
+
source.overwrite(
|
|
6577
|
+
offset + argument.start,
|
|
6578
|
+
offset + argument.end,
|
|
6579
|
+
content.slice(offset + left.start, offset + left.end)
|
|
6580
|
+
);
|
|
6581
|
+
rewritten++;
|
|
6582
|
+
});
|
|
6583
|
+
return rewritten === 0 && unproven > 0 ? "unproven" : "ok";
|
|
6584
|
+
}
|
|
6585
|
+
var isCallOf = (init, callee) => {
|
|
6586
|
+
const fn = init?.type === "CallExpression" ? init.callee : void 0;
|
|
6587
|
+
return fn?.type === "Identifier" && fn.name === callee;
|
|
6588
|
+
};
|
|
6589
|
+
async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
6590
|
+
const files = [];
|
|
6591
|
+
const refused = /* @__PURE__ */ new Map();
|
|
6592
|
+
const prefixesFor = (slug) => {
|
|
6593
|
+
const out = /* @__PURE__ */ new Set();
|
|
6594
|
+
for (const page of plan2.pages ?? []) {
|
|
6595
|
+
for (const section of page.sections ?? []) {
|
|
6596
|
+
if (resolveComponent(plan2, section).slug !== slug) continue;
|
|
6597
|
+
const prefix = propPrefix(section);
|
|
6598
|
+
if (prefix !== null) out.add(prefix);
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
return [...out];
|
|
6602
|
+
};
|
|
6603
|
+
for (const [dialect, components] of written) {
|
|
6604
|
+
for (const [slug, entry] of components) {
|
|
6605
|
+
const content = contents.get(entry.path);
|
|
6606
|
+
if (content === void 0 || content.includes(SECTION_MARKER)) continue;
|
|
6607
|
+
const older = SECTION_MARKERS.find((marker) => content.includes(marker));
|
|
6608
|
+
if (older === void 0) continue;
|
|
6609
|
+
const candidates = prefixesFor(slug);
|
|
6610
|
+
if (candidates.length === 0) continue;
|
|
6611
|
+
const parsed = await parseFile4(dialect, content);
|
|
6612
|
+
if (parsed.error) {
|
|
6613
|
+
refused.set(refusalKey(dialect, slug), `${entry.path} carries our marker but does not parse (${parsed.error.message}).`);
|
|
6614
|
+
continue;
|
|
6615
|
+
}
|
|
6616
|
+
const elements = elementsUnder(...parsed.roots);
|
|
6617
|
+
const imports = [...importsIn(parsed, dialect)];
|
|
6618
|
+
const helperModule = HELPER_PATH[dialect];
|
|
6619
|
+
const helperLocalsFor = (exported) => new Set(
|
|
6620
|
+
imports.filter(
|
|
6621
|
+
([, from]) => from.imported === exported && resolveSpecifier(entry.path, from.specifier, fileSet, aliases) === helperModule
|
|
6622
|
+
).map(([local]) => local)
|
|
6623
|
+
);
|
|
6624
|
+
const { heads: evidence, opaque } = fieldAddresses(content, elements);
|
|
6625
|
+
const pathPrefix = evidence.length + opaque.length === 0 ? "" : opaque.length > 0 ? null : onlyOne(candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)))) ?? null;
|
|
6626
|
+
if (pathPrefix === null) {
|
|
6627
|
+
const matching = candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)));
|
|
6628
|
+
refused.set(
|
|
6629
|
+
refusalKey(dialect, slug),
|
|
6630
|
+
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.`
|
|
6631
|
+
);
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6634
|
+
const source = new MagicString4(content);
|
|
6635
|
+
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
6636
|
+
if (duplicates.length > 0) {
|
|
6637
|
+
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.`);
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
const markerEnd = content.indexOf(older) + older.length;
|
|
6641
|
+
source.overwrite(markerEnd - older.length, markerEnd, SECTION_MARKER);
|
|
6642
|
+
const stripped = stripBindFallback(parsed, source, content, helperLocalsFor("bcmsSection"));
|
|
6643
|
+
if (stripped !== "ok") {
|
|
6644
|
+
refused.set(
|
|
6645
|
+
refusalKey(dialect, slug),
|
|
6646
|
+
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.`
|
|
6647
|
+
);
|
|
6648
|
+
continue;
|
|
6649
|
+
}
|
|
6650
|
+
if (bound > 0) {
|
|
6651
|
+
const locals = generatedBindings(parsed);
|
|
6652
|
+
const names = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...locals.keys()]);
|
|
6653
|
+
const fieldsLocal = [...helperLocalsFor("bcmsFields")][0];
|
|
6654
|
+
if (fieldsLocal === void 0 && names.has("bcmsFields")) {
|
|
6655
|
+
refused.set(
|
|
6656
|
+
refusalKey(dialect, slug),
|
|
6657
|
+
`${entry.path} already binds "bcmsFields" to something other than ${helperModule}'s export.`
|
|
6658
|
+
);
|
|
6659
|
+
continue;
|
|
6660
|
+
}
|
|
6661
|
+
const declared = names.has("bcmsField");
|
|
6662
|
+
if (declared && (fieldsLocal === void 0 || !isCallOf(locals.get("bcmsField"), fieldsLocal))) {
|
|
6663
|
+
refused.set(
|
|
6664
|
+
refusalKey(dialect, slug),
|
|
6665
|
+
`${entry.path} already binds "bcmsField" to something other than a call of ${helperModule}'s bcmsFields.`
|
|
6666
|
+
);
|
|
6667
|
+
continue;
|
|
6668
|
+
}
|
|
6669
|
+
if (!declared) {
|
|
6670
|
+
const anchor = V1_ANCHOR[dialect];
|
|
6671
|
+
const at = anchor ? content.indexOf(anchor) : -1;
|
|
6672
|
+
if (anchor === void 0 || at < 0) {
|
|
6673
|
+
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.`);
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
source.appendLeft(
|
|
6677
|
+
at + anchor.length,
|
|
6678
|
+
`
|
|
6679
|
+
${dialect === "astro" ? "" : " "}const bcmsField = ${fieldsLocal ?? "bcmsFields"}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});`
|
|
6680
|
+
);
|
|
6681
|
+
}
|
|
6682
|
+
if (fieldsLocal === void 0) {
|
|
6683
|
+
const helper = imports.find(([, from]) => from.imported === "bcmsSection")?.[1].specifier ?? relativeImport(entry.path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
|
|
6684
|
+
source.appendLeft(markerEnd, `
|
|
6685
|
+
import { bcmsFields } from "${helper}";`);
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
files.push({ path: entry.path, operation: "modify", content: source.toString() });
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
return { files, refused };
|
|
6692
|
+
}
|
|
6693
|
+
function propPrefix(section) {
|
|
6694
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
6695
|
+
for (const field of section.fields ?? []) {
|
|
6696
|
+
const { path, key } = field;
|
|
6697
|
+
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
6698
|
+
if (!path.endsWith(key)) return null;
|
|
6699
|
+
prefixes.add(path.slice(0, path.length - key.length));
|
|
6700
|
+
}
|
|
6701
|
+
if (prefixes.size > 1) return null;
|
|
6702
|
+
return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
|
|
6703
|
+
}
|
|
6704
|
+
function referencedNames2(markup) {
|
|
6705
|
+
const out = /* @__PURE__ */ new Set();
|
|
6706
|
+
for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
|
|
6707
|
+
return out;
|
|
5979
6708
|
}
|
|
5980
6709
|
var alias = (name, local) => name === local ? name : `${name} as ${local}`;
|
|
5981
6710
|
var mentions = (markup, name) => new RegExp(`(?<![\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w$])`).test(markup);
|
|
@@ -6179,7 +6908,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
6179
6908
|
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
6180
6909
|
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
6181
6910
|
const local = names.get("Sections");
|
|
6182
|
-
const source = new
|
|
6911
|
+
const source = new MagicString4(content);
|
|
6183
6912
|
const first = placeable[0].root;
|
|
6184
6913
|
const last = placeable[placeable.length - 1].root;
|
|
6185
6914
|
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
@@ -6199,7 +6928,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
|
|
|
6199
6928
|
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
6200
6929
|
]);
|
|
6201
6930
|
const snapshot = names.get("pageSnapshot");
|
|
6202
|
-
const source = new
|
|
6931
|
+
const source = new MagicString4(content);
|
|
6203
6932
|
for (const resolved of placeable) {
|
|
6204
6933
|
const component = written.get(resolved.slug);
|
|
6205
6934
|
const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
|
|
@@ -6234,7 +6963,7 @@ function addImports(source, parsed, lines) {
|
|
|
6234
6963
|
else source.appendLeft(at.at, `
|
|
6235
6964
|
${lines.join("\n")}`);
|
|
6236
6965
|
}
|
|
6237
|
-
var isGenerated = (content) => content.includes(
|
|
6966
|
+
var isGenerated = (content) => SECTION_MARKERS.some((marker) => content.includes(marker)) || content.includes(REGISTRY_MARKER);
|
|
6238
6967
|
function emitHelper(out, contents, version, overwrite) {
|
|
6239
6968
|
const path = HELPER_PATH.astro;
|
|
6240
6969
|
const source = helperSource("astro");
|
|
@@ -6280,6 +7009,10 @@ function readComponentizePlan(value) {
|
|
|
6280
7009
|
}
|
|
6281
7010
|
|
|
6282
7011
|
// src/index.ts
|
|
7012
|
+
var UNPARSEABLE_DIALECT = {
|
|
7013
|
+
roots: [],
|
|
7014
|
+
error: { code: "DIALECT_UNSUPPORTED", message: "no parser for this extension" }
|
|
7015
|
+
};
|
|
6283
7016
|
var ConvertError = class extends Error {
|
|
6284
7017
|
constructor(code, message) {
|
|
6285
7018
|
super(message);
|
|
@@ -6361,7 +7094,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6361
7094
|
}
|
|
6362
7095
|
const { located, unlocated } = locate(brief, sources, { stripTags: true });
|
|
6363
7096
|
const pending = [];
|
|
6364
|
-
const
|
|
7097
|
+
const pend2 = (target, reason, file, message) => {
|
|
6365
7098
|
pending.push({
|
|
6366
7099
|
route: target.route,
|
|
6367
7100
|
scope: target.scope,
|
|
@@ -6462,7 +7195,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6462
7195
|
declaredRow(target, false, byHand);
|
|
6463
7196
|
continue;
|
|
6464
7197
|
}
|
|
6465
|
-
|
|
7198
|
+
pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
|
|
6466
7199
|
}
|
|
6467
7200
|
const byFile = /* @__PURE__ */ new Map();
|
|
6468
7201
|
for (const [key, hit] of locatedByKey) {
|
|
@@ -6519,12 +7252,12 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6519
7252
|
const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
|
|
6520
7253
|
if (worst) {
|
|
6521
7254
|
const file = hit.files[outcomes.indexOf(worst)];
|
|
6522
|
-
|
|
7255
|
+
pend2(target, worst, file);
|
|
6523
7256
|
} else if (outcomes.every((o) => o === "declared")) {
|
|
6524
7257
|
const witness = await witnessed(target);
|
|
6525
7258
|
const byHand = witness === null ? declaredByHand(target, hit.files) : null;
|
|
6526
7259
|
if (witness === null && byHand === null) {
|
|
6527
|
-
|
|
7260
|
+
pend2(
|
|
6528
7261
|
target,
|
|
6529
7262
|
"AMBIGUOUS_LITERAL",
|
|
6530
7263
|
hit.files[0],
|
|
@@ -6537,7 +7270,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6537
7270
|
} else rewritten++;
|
|
6538
7271
|
}
|
|
6539
7272
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6540
|
-
const source = new
|
|
7273
|
+
const source = new MagicString5(contents.get(file));
|
|
6541
7274
|
const seen = /* @__PURE__ */ new Set();
|
|
6542
7275
|
for (const splice of splices) {
|
|
6543
7276
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -6558,6 +7291,28 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6558
7291
|
}
|
|
6559
7292
|
const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
|
|
6560
7293
|
emitStubs2(dialects, snapshotSlugs, contents, outFiles);
|
|
7294
|
+
const latest = new Map(contents);
|
|
7295
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
7296
|
+
const canvasSources = [];
|
|
7297
|
+
for (const source of sources) {
|
|
7298
|
+
const content = latest.get(source.path) ?? source.content;
|
|
7299
|
+
if (!/\.[jt]sx$/i.test(source.path)) {
|
|
7300
|
+
canvasSources.push({ path: source.path, content });
|
|
7301
|
+
continue;
|
|
7302
|
+
}
|
|
7303
|
+
const parsed = content === source.content ? await parseOf(source.path) : await parseFile4("jsx", content);
|
|
7304
|
+
canvasSources.push({ path: source.path, content, ...parsed && !parsed.error ? { ast: parsed.ast } : {} });
|
|
7305
|
+
}
|
|
7306
|
+
const canvas = canvasBridge(
|
|
7307
|
+
canvasSources,
|
|
7308
|
+
new Map(brief.pages.map((page) => [page.route, page.slug]))
|
|
7309
|
+
);
|
|
7310
|
+
for (const [path, content] of canvas.edits) {
|
|
7311
|
+
const existing = outFiles.find((f) => f.path === path);
|
|
7312
|
+
if (existing) existing.content = content;
|
|
7313
|
+
else outFiles.push({ path, operation: "modify", content });
|
|
7314
|
+
}
|
|
7315
|
+
pending.push(...canvas.pending);
|
|
6561
7316
|
const receipt = assertReceipt({
|
|
6562
7317
|
briefDigest: digest,
|
|
6563
7318
|
paths: {
|
|
@@ -6574,6 +7329,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
6574
7329
|
helpers,
|
|
6575
7330
|
helperUpgraded,
|
|
6576
7331
|
dynamicBindings,
|
|
7332
|
+
canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
|
|
6577
7333
|
notes: unreadableConfigs(sources).map(
|
|
6578
7334
|
(file) => ({
|
|
6579
7335
|
code: "TSCONFIG_UNREADABLE",
|
|
@@ -6606,6 +7362,30 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
6606
7362
|
};
|
|
6607
7363
|
if (pendingTargets.length === 0) return empty;
|
|
6608
7364
|
if (!dialect) {
|
|
7365
|
+
if (isTolerantDialect(file)) {
|
|
7366
|
+
const tolerantLiterals = [...new Set(pendingTargets.map((t) => t.literal))];
|
|
7367
|
+
const tolerantSites = findSitesTolerant(content, tolerantLiterals);
|
|
7368
|
+
if (tolerantSites.length > 0) {
|
|
7369
|
+
return plan(
|
|
7370
|
+
file,
|
|
7371
|
+
content,
|
|
7372
|
+
// Never read: `plan` forces `writing = "html"` at tier 2, and every other use of this
|
|
7373
|
+
// argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
|
|
7374
|
+
// the parameter honest rather than widening its type for one caller.
|
|
7375
|
+
"html",
|
|
7376
|
+
pendingTargets,
|
|
7377
|
+
tolerantSites,
|
|
7378
|
+
void 0,
|
|
7379
|
+
// no parse, no import point: tier 2 declares, it does not read
|
|
7380
|
+
outcomes,
|
|
7381
|
+
2,
|
|
7382
|
+
UNPARSEABLE_DIALECT,
|
|
7383
|
+
drill,
|
|
7384
|
+
[]
|
|
7385
|
+
// no parse, so nothing in this file is already declared
|
|
7386
|
+
);
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
6609
7389
|
for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
|
|
6610
7390
|
return { ...empty, dialect: null, outcomes };
|
|
6611
7391
|
}
|
|
@@ -6664,7 +7444,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
6664
7444
|
}
|
|
6665
7445
|
async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
|
|
6666
7446
|
const writing = tier === 2 ? "html" : dialect;
|
|
6667
|
-
const missing = tier
|
|
7447
|
+
const missing = tier !== 2 ? "NOT_IN_SOURCE" : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
|
|
6668
7448
|
const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
|
|
6669
7449
|
const slugs = [...new Set(targets.map((t) => t.slug))].sort();
|
|
6670
7450
|
const preset = /* @__PURE__ */ new Map();
|
|
@@ -7087,7 +7867,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
7087
7867
|
}
|
|
7088
7868
|
|
|
7089
7869
|
// src/cli.ts
|
|
7090
|
-
var VERSION = "0.
|
|
7870
|
+
var VERSION = "0.4.0";
|
|
7091
7871
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
7092
7872
|
var CliError = class extends Error {
|
|
7093
7873
|
};
|
|
@@ -7096,6 +7876,7 @@ function parseArgs(argv) {
|
|
|
7096
7876
|
brief: void 0,
|
|
7097
7877
|
componentize: false,
|
|
7098
7878
|
plan: void 0,
|
|
7879
|
+
copy: "bind",
|
|
7099
7880
|
root: ".",
|
|
7100
7881
|
dryRun: false,
|
|
7101
7882
|
receipt: void 0,
|
|
@@ -7109,7 +7890,13 @@ function parseArgs(argv) {
|
|
|
7109
7890
|
if (arg === "--brief") args.brief = argv[++i];
|
|
7110
7891
|
else if (arg === "--componentize") args.componentize = true;
|
|
7111
7892
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
7112
|
-
else if (arg === "--
|
|
7893
|
+
else if (arg === "--copy") {
|
|
7894
|
+
const mode = argv[++i];
|
|
7895
|
+
if (mode !== "bind" && mode !== "instance") {
|
|
7896
|
+
throw new CliError(`--copy takes "bind" or "instance", not ${JSON.stringify(mode ?? "")}. See --help.`);
|
|
7897
|
+
}
|
|
7898
|
+
args.copy = mode;
|
|
7899
|
+
} else if (arg === "--root") args.root = argv[++i] ?? ".";
|
|
7113
7900
|
else if (arg === "--receipt") args.receipt = argv[++i];
|
|
7114
7901
|
else if (arg === "--dry-run") args.dryRun = true;
|
|
7115
7902
|
else if (arg === "--strict") args.strict = true;
|
|
@@ -7133,6 +7920,10 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
7133
7920
|
--componentize Lift each planned SECTION into a component the CMS can reorder.
|
|
7134
7921
|
--plan <file> The componentize plan, as get_componentize_plan returns it.
|
|
7135
7922
|
Required with --componentize.
|
|
7923
|
+
--copy bind|instance Which copy model the plan is expected to describe. Default: bind
|
|
7924
|
+
(the copy stays in the page's field groups). "instance" is refused
|
|
7925
|
+
\u2014 COPY_MODE_MISMATCH \u2014 unless the plan's placements say the server
|
|
7926
|
+
has already moved it onto them.
|
|
7136
7927
|
--root <dir> The repository to convert. Default: the working directory.
|
|
7137
7928
|
--dry-run Print what would change; write nothing.
|
|
7138
7929
|
--receipt <file> Write the receipt as JSON.
|
|
@@ -7217,7 +8008,7 @@ function reportSections(files, receipt, dryRun) {
|
|
|
7217
8008
|
}
|
|
7218
8009
|
process.stdout.write(
|
|
7219
8010
|
`
|
|
7220
|
-
${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending)
|
|
8011
|
+
${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-only, ${alreadyExtracted} already extracted, ${pending.length} pending), copy: ${receipt.copy}
|
|
7221
8012
|
`
|
|
7222
8013
|
);
|
|
7223
8014
|
for (const section of pending) {
|
|
@@ -7237,6 +8028,9 @@ async function main() {
|
|
|
7237
8028
|
if (args.plan && !args.componentize) {
|
|
7238
8029
|
throw new CliError("--plan is only read with --componentize. See --help.");
|
|
7239
8030
|
}
|
|
8031
|
+
if (args.copy === "instance" && !args.componentize) {
|
|
8032
|
+
throw new CliError("--copy is only read with --componentize. See --help.");
|
|
8033
|
+
}
|
|
7240
8034
|
const root = resolve(args.root);
|
|
7241
8035
|
const brief = JSON.parse(await readFile(resolve(args.brief), "utf-8"));
|
|
7242
8036
|
if (!Array.isArray(brief.pages)) {
|
|
@@ -7270,7 +8064,7 @@ async function main() {
|
|
|
7270
8064
|
brief,
|
|
7271
8065
|
plan2,
|
|
7272
8066
|
[...sources, ...await readSnapshots(root)],
|
|
7273
|
-
{ overwriteHelper: args.overwriteHelper }
|
|
8067
|
+
{ overwriteHelper: args.overwriteHelper, copy: args.copy }
|
|
7274
8068
|
);
|
|
7275
8069
|
await written(files2);
|
|
7276
8070
|
await saveReceipt(receipt2);
|