@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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import MagicString4 from "magic-string";
2
+ import MagicString5 from "magic-string";
3
3
 
4
4
  // src/brief.ts
5
5
  import { createHash } from "crypto";
@@ -21,6 +21,21 @@ var BY_EXTENSION = {
21
21
  ".svelte": "svelte",
22
22
  ".vue": "vue"
23
23
  };
24
+ var TOLERANT_EXTENSIONS = /* @__PURE__ */ new Set([
25
+ ".md",
26
+ ".mdx",
27
+ ".njk",
28
+ ".hbs",
29
+ ".ejs",
30
+ ".liquid",
31
+ ".erb",
32
+ ".twig"
33
+ ]);
34
+ function isTolerantDialect(path) {
35
+ const lower = path.toLowerCase();
36
+ const at = lower.lastIndexOf(".");
37
+ return at >= 0 && TOLERANT_EXTENSIONS.has(lower.slice(at));
38
+ }
24
39
  function dialectOf(path, _content) {
25
40
  const lower = path.toLowerCase();
26
41
  const at = lower.lastIndexOf(".");
@@ -128,7 +143,7 @@ function propsAttribute(props) {
128
143
 
129
144
  // src/helper.ts
130
145
  import { createHash as createHash2 } from "crypto";
131
- var HELPER_VERSION = 3;
146
+ var HELPER_VERSION = 4;
132
147
  var HEADER = "// @bettercms-ai/convert helper v";
133
148
  var HELPER_PATH = Object.fromEntries(
134
149
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -211,32 +226,97 @@ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: s
211
226
  /**
212
227
  * ONE SECTION's copy, for a component extracted by \`--componentize\`.
213
228
  *
214
- * \`overrides ?? pageGroup(bind) ?? defaults\` \u2014 the instance's own values when the editor gave it
215
- * any, otherwise the page field group the placement is bound to, otherwise nothing at all, in
216
- * which case every \`bcms(...)\` below renders the fallback the template shipped with. That last
217
- * step is why a section renders correctly against the committed \`{}\` stub.
229
+ * OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
230
+ * (Phase B) carries an override for every prop it declares, so the group underneath it is never
231
+ * read; a placement still bound to a page field group (Phase A) carries none, so the group is all
232
+ * there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
233
+ * instance does not hold has to keep rendering the page's value rather than nothing.
234
+ *
235
+ * \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
236
+ * the page's old copy back into a heading somebody had deliberately cleared.
237
+ *
238
+ * With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
239
+ * the fallback the template shipped with \u2014 which is why a section renders correctly against the
240
+ * committed \`{}\` stub.
218
241
  */
219
242
  export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
220
- // \u{1F534} AN EMPTY OBJECT IS AN ANSWER. \`overrides: {}\` is an instance an editor cleared, and
221
- // "\`??\` unless it happens to be empty" quietly put the page's copy back into it.
222
- if (isRecord(overrides)) return overrides;
223
243
  const group = bind ? readPath(page, bind) : undefined;
224
- return isRecord(group) ? group : {};
244
+ const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
245
+ if (!isRecord(overrides)) return base;
246
+ for (const key of Object.keys(overrides)) {
247
+ const value = overrides[key];
248
+ if (value === undefined || value === null) continue;
249
+ /**
250
+ * \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
251
+ * dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
252
+ * helper fell back to the rows the template shipped with and the editor's work never showed.
253
+ */
254
+ base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
255
+ }
256
+ return base;
257
+ }
258
+
259
+ /**
260
+ * WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
261
+ *
262
+ * An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
263
+ * copy lives. A placement that owns its copy declares the BLOCK lane \u2014
264
+ * \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
265
+ * own value to. A placement bound to a page field group declares the PAGE path, which is what the
266
+ * page form and the coverage meter already know.
267
+ *
268
+ * \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
269
+ * page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
270
+ * second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
271
+ * Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
272
+ * \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
273
+ * the meter counted a binding that was never there. The prefix the markup was cut with comes out,
274
+ * the placement's own group goes in.
275
+ *
276
+ * \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
277
+ * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
278
+ * instance on the next, and a repository does not have to be re-cut when its copy moves.
279
+ */
280
+ export function bcmsFields(
281
+ placement: { blockId?: string; bind?: string; overrides?: unknown },
282
+ pathPrefix: string,
283
+ ): (path: string) => string {
284
+ const id = placement.blockId;
285
+ const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
286
+ /**
287
+ * What replaces the prefix the markup was cut with, or null for "leave every path alone".
288
+ *
289
+ * Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
290
+ * nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
291
+ * where the plan said the prop key IS the whole page path: there is no group segment to swap,
292
+ * so prepending one would invent an address rather than correct it.
293
+ */
294
+ const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
295
+ return (path: string): string => {
296
+ // A path outside the group this section was cut from is not one this placement re-addresses:
297
+ // it names a key the instance does not hold and a group this placement is not bound to.
298
+ // (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
299
+ if (lane === null || !path.startsWith(pathPrefix)) return path;
300
+ return lane + path.slice(pathPrefix.length);
301
+ };
225
302
  }
226
303
  `;
227
304
  }
228
305
  var HISTORICAL = {
229
306
  "src/bcms-content.ts": {
230
307
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
231
- 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
308
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
309
+ 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
232
310
  },
233
311
  "src/lib/bcms-content.ts": {
234
312
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
235
- 2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002"
313
+ 2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
314
+ 3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e"
236
315
  },
237
316
  "composables/bcms-content.ts": {
238
317
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
239
- 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
318
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
319
+ 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
240
320
  }
241
321
  };
242
322
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -4196,6 +4276,7 @@ function importLines(imports) {
4196
4276
  }
4197
4277
 
4198
4278
  // src/receipt.ts
4279
+ var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
4199
4280
  var ReceiptInvariantError = class extends Error {
4200
4281
  code = "RECEIPT_INVARIANT";
4201
4282
  constructor(message) {
@@ -4205,10 +4286,11 @@ var ReceiptInvariantError = class extends Error {
4205
4286
  };
4206
4287
  function assertReceipt(receipt) {
4207
4288
  const { declared, rewritten, alreadyDeclared, pending } = receipt.paths;
4208
- const sum = rewritten + alreadyDeclared + pending.length;
4289
+ const paths = pending.filter((p) => !CANVAS_REASONS.has(p.reason));
4290
+ const sum = rewritten + alreadyDeclared + paths.length;
4209
4291
  if (sum !== declared) {
4210
4292
  throw new ReceiptInvariantError(
4211
- `The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${pending.length} pending = ${sum}.`
4293
+ `The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${paths.length} pending = ${sum}.`
4212
4294
  );
4213
4295
  }
4214
4296
  if (receipt.occurrences.rewritten > receipt.occurrences.located) {
@@ -4720,6 +4802,218 @@ var findSites = async (dialect, content, literals) => {
4720
4802
  return parsed.error ? { sites: [], error: parsed.error } : { sites: collectSites(parsed.roots, literals), importAt: parsed.importAt };
4721
4803
  };
4722
4804
 
4805
+ // src/canvas.ts
4806
+ import MagicString3 from "magic-string";
4807
+ 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'.";
4808
+ var isNode = (value) => !!value && typeof value === "object" && typeof value.type === "string";
4809
+ var FUNCTIONS3 = /* @__PURE__ */ new Set([
4810
+ "FunctionDeclaration",
4811
+ "FunctionExpression",
4812
+ "ArrowFunctionExpression",
4813
+ "ObjectMethod",
4814
+ "ClassMethod"
4815
+ ]);
4816
+ function patternNames2(node, out) {
4817
+ if (!isNode(node)) return;
4818
+ switch (node.type) {
4819
+ case "Identifier":
4820
+ out.add(String(node.name));
4821
+ return;
4822
+ case "ObjectPattern":
4823
+ for (const p of node.properties ?? [])
4824
+ patternNames2(p.type === "RestElement" ? p.argument : p.value, out);
4825
+ return;
4826
+ case "ArrayPattern":
4827
+ for (const el of node.elements ?? []) patternNames2(el, out);
4828
+ return;
4829
+ case "AssignmentPattern":
4830
+ patternNames2(node.left, out);
4831
+ return;
4832
+ case "RestElement":
4833
+ patternNames2(node.argument, out);
4834
+ return;
4835
+ default:
4836
+ return;
4837
+ }
4838
+ }
4839
+ function statementNames2(statements, out) {
4840
+ for (const statement of statements ?? []) {
4841
+ if (!isNode(statement)) continue;
4842
+ const node = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
4843
+ if (!isNode(node)) continue;
4844
+ if (node.type === "VariableDeclaration")
4845
+ for (const d of node.declarations ?? []) patternNames2(d.id, out);
4846
+ else if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") patternNames2(node.id, out);
4847
+ else if (node.type === "ImportDeclaration")
4848
+ for (const specifier of node.specifiers ?? []) patternNames2(specifier.local, out);
4849
+ }
4850
+ }
4851
+ function functionBindings(fn) {
4852
+ const out = /* @__PURE__ */ new Set();
4853
+ for (const param of fn.params ?? []) patternNames2(param, out);
4854
+ const body = fn.body;
4855
+ if (body?.type === "BlockStatement") statementNames2(body.body, out);
4856
+ return out;
4857
+ }
4858
+ function stringLiterals(node, out) {
4859
+ if (!isNode(node)) {
4860
+ if (Array.isArray(node)) for (const child of node) stringLiterals(child, out);
4861
+ return;
4862
+ }
4863
+ if (FUNCTIONS3.has(node.type)) return;
4864
+ if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
4865
+ for (const key of Object.keys(node)) {
4866
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
4867
+ stringLiterals(node[key], out);
4868
+ }
4869
+ }
4870
+ function usages(ast, local) {
4871
+ const found = [];
4872
+ const moduleScope2 = /* @__PURE__ */ new Set();
4873
+ statementNames2(ast?.program?.body, moduleScope2);
4874
+ const walk = (node, scope, literals) => {
4875
+ if (!isNode(node)) {
4876
+ if (Array.isArray(node)) for (const child of node) walk(child, scope, literals);
4877
+ return;
4878
+ }
4879
+ let nextScope = scope;
4880
+ let nextLiterals = literals;
4881
+ if (FUNCTIONS3.has(node.type)) {
4882
+ nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
4883
+ nextLiterals = /* @__PURE__ */ new Set();
4884
+ stringLiterals(node.body, nextLiterals);
4885
+ }
4886
+ if (node.type === "JSXElement") {
4887
+ const name = node.openingElement?.name;
4888
+ if (name?.type === "JSXIdentifier" && name.name === local)
4889
+ found.push({ element: node, scope: nextScope, literals: nextLiterals });
4890
+ }
4891
+ for (const key of Object.keys(node)) {
4892
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
4893
+ walk(node[key], nextScope, nextLiterals);
4894
+ }
4895
+ };
4896
+ walk(ast?.program ?? ast, moduleScope2, /* @__PURE__ */ new Set());
4897
+ return found;
4898
+ }
4899
+ function bridgedAlready(ast) {
4900
+ const body = ast?.program?.body ?? [];
4901
+ return body.some(
4902
+ (statement) => statement.type === "ImportDeclaration" && String(statement.source?.value ?? "") === "@bettercms-ai/next/draft-bridge" && (statement.specifiers ?? []).some(
4903
+ (specifier) => specifier.imported?.name === "BcmsDraftBridge"
4904
+ )
4905
+ );
4906
+ }
4907
+ function blocksImport(ast) {
4908
+ const body = ast?.program?.body ?? [];
4909
+ for (const statement of body) {
4910
+ if (statement.type !== "ImportDeclaration") continue;
4911
+ const source = String(statement.source?.value ?? "");
4912
+ if (source !== "@bettercms-ai/next" && source !== "@bettercms-ai/next/blocks") continue;
4913
+ for (const specifier of statement.specifiers ?? []) {
4914
+ const imported = specifier.imported;
4915
+ if (specifier.type !== "ImportSpecifier" || imported?.name !== "BcmsBlocks") continue;
4916
+ return { declaration: statement, specifier, local: String(specifier.local.name) };
4917
+ }
4918
+ }
4919
+ return null;
4920
+ }
4921
+ function slugExpression(usage, routes) {
4922
+ if (usage.scope.has("slug")) return "{slug}";
4923
+ if (usage.scope.has("page")) return "{page.slug}";
4924
+ const matched = [...new Set([...usage.literals].map((l) => routes.get(l)).filter((s) => !!s))];
4925
+ return matched.length === 1 ? JSON.stringify(matched[0]) : null;
4926
+ }
4927
+ var lineOf = (content, offset) => content.slice(0, offset).split("\n").length;
4928
+ var pend = (reason, hint, file, line) => ({
4929
+ route: "",
4930
+ scope: "page",
4931
+ path: reason === "SSR_DRAFT_ROUTE" ? "canvas.lane" : "canvas.bridge",
4932
+ kind: "canvas",
4933
+ ...file ? { file } : {},
4934
+ ...line ? { line } : {},
4935
+ reason,
4936
+ hint
4937
+ });
4938
+ var ASTRO_LANE_PENDING = pend("SSR_DRAFT_ROUTE", SSR_DRAFT_RECIPE);
4939
+ function canvasBridge(sources, routes) {
4940
+ if (sources.some((s) => s.path.toLowerCase().endsWith(".astro")))
4941
+ return { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
4942
+ const files = [];
4943
+ const edits = /* @__PURE__ */ new Map();
4944
+ const pending = [];
4945
+ for (const source of sources) {
4946
+ if (!/\.[jt]sx$/i.test(source.path) || !source.ast) continue;
4947
+ if (bridgedAlready(source.ast)) {
4948
+ files.push(source.path);
4949
+ continue;
4950
+ }
4951
+ const imported = blocksImport(source.ast);
4952
+ if (!imported) continue;
4953
+ const found = usages(source.ast, imported.local);
4954
+ const slugs = found.map((usage) => slugExpression(usage, routes));
4955
+ const unresolved = found.findIndex((_, i) => slugs[i] === null);
4956
+ if (found.length === 0 || unresolved !== -1) {
4957
+ const at = found[unresolved === -1 ? 0 : unresolved]?.element.start;
4958
+ pending.push(
4959
+ pend(
4960
+ "CANVAS_BRIDGE_MANUAL",
4961
+ "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.",
4962
+ source.path,
4963
+ at === void 0 ? void 0 : lineOf(source.content, at)
4964
+ )
4965
+ );
4966
+ continue;
4967
+ }
4968
+ const out = new MagicString3(source.content);
4969
+ rewriteImport(out, imported);
4970
+ for (const [i, usage] of found.entries()) rewriteElement(out, usage, imported.local, slugs[i]);
4971
+ edits.set(source.path, out.toString());
4972
+ files.push(source.path);
4973
+ }
4974
+ return { lane: files.length > 0 ? "bridge" : "pending", files, edits, pending };
4975
+ }
4976
+ function rewriteImport(out, imported) {
4977
+ const { declaration: declaration2, specifier, local } = imported;
4978
+ const line = `import { BcmsDraftBridge${local === "BcmsBlocks" ? "" : ` as ${local}`} } from "@bettercms-ai/next/draft-bridge";`;
4979
+ const specifiers = declaration2.specifiers ?? [];
4980
+ const named = specifiers.filter((s) => s.type === "ImportSpecifier");
4981
+ if (named.length === 1) {
4982
+ if (specifiers.length === 1) {
4983
+ out.overwrite(declaration2.start, declaration2.end, line);
4984
+ return;
4985
+ }
4986
+ const other = specifiers.find((s) => s.type !== "ImportSpecifier");
4987
+ const close = out.original.indexOf("}", named[0].end);
4988
+ out.remove(other.end, close + 1);
4989
+ out.appendRight(declaration2.end, `
4990
+ ${line}`);
4991
+ return;
4992
+ }
4993
+ const index = named.indexOf(specifier);
4994
+ const previous = named[index - 1];
4995
+ const from = previous ? previous.end : specifier.start;
4996
+ const to = previous ? specifier.end : named[index + 1].start;
4997
+ out.remove(from, to);
4998
+ out.appendRight(declaration2.end, `
4999
+ ${line}`);
5000
+ }
5001
+ function rewriteElement(out, usage, local, slug) {
5002
+ const opening = usage.element.openingElement;
5003
+ const name = opening.name;
5004
+ if (local === "BcmsBlocks") {
5005
+ out.overwrite(name.start, name.end, "BcmsDraftBridge");
5006
+ const closing = usage.element.closingElement;
5007
+ if (closing) {
5008
+ const closingName = closing.name;
5009
+ out.overwrite(closingName.start, closingName.end, "BcmsDraftBridge");
5010
+ }
5011
+ }
5012
+ const attributes = opening.attributes ?? [];
5013
+ const hasSlug = attributes.some((a) => a.name?.name === "slug");
5014
+ if (!hasSlug) out.appendLeft(name.end, ` slug=${slug}`);
5015
+ }
5016
+
4723
5017
  // src/tolerant.ts
4724
5018
  var escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4725
5019
  var flexible = (literal) => new RegExp(escape(literal).split(" ").map(escape).join("\\s+"), "gu");
@@ -4849,6 +5143,7 @@ function readPlan(value) {
4849
5143
  const section = check.record(held, at2);
4850
5144
  check.segment(section.groupKey, `${at2}.groupKey`);
4851
5145
  check.optionalString(section.sectionType, `${at2}.sectionType`);
5146
+ check.optionalString(section.copy, `${at2}.copy`);
4852
5147
  if (section.fields !== void 0) {
4853
5148
  for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
4854
5149
  const shape = check.record(field, `${at2}.fields[${f}]`);
@@ -5148,10 +5443,10 @@ function scanExpression(code) {
5148
5443
  for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
5149
5444
  return;
5150
5445
  case "VariableDeclarator":
5151
- patternNames2(node.id, bound);
5446
+ patternNames3(node.id, bound);
5152
5447
  return;
5153
5448
  case "CatchClause":
5154
- patternNames2(node.param, bound);
5449
+ patternNames3(node.param, bound);
5155
5450
  return;
5156
5451
  default:
5157
5452
  return;
@@ -5159,8 +5454,8 @@ function scanExpression(code) {
5159
5454
  });
5160
5455
  walkAst(ast, (node) => {
5161
5456
  if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
5162
- for (const param of node.params ?? []) patternNames2(param, bound);
5163
- if (node.id) patternNames2(node.id, bound);
5457
+ for (const param of node.params ?? []) patternNames3(param, bound);
5458
+ if (node.id) patternNames3(node.id, bound);
5164
5459
  }
5165
5460
  });
5166
5461
  const free = /* @__PURE__ */ new Set();
@@ -5175,7 +5470,7 @@ function nameOf2(node, out) {
5175
5470
  nameOf2(node.name, out);
5176
5471
  }
5177
5472
  }
5178
- function patternNames2(node, out) {
5473
+ function patternNames3(node, out) {
5179
5474
  if (!node || typeof node !== "object") return;
5180
5475
  const typed = node;
5181
5476
  switch (typed.type) {
@@ -5184,17 +5479,17 @@ function patternNames2(node, out) {
5184
5479
  return;
5185
5480
  case "ObjectPattern":
5186
5481
  for (const property of typed.properties ?? []) {
5187
- patternNames2(property.type === "RestElement" ? property.argument : property.value, out);
5482
+ patternNames3(property.type === "RestElement" ? property.argument : property.value, out);
5188
5483
  }
5189
5484
  return;
5190
5485
  case "ArrayPattern":
5191
- for (const element of typed.elements ?? []) patternNames2(element, out);
5486
+ for (const element of typed.elements ?? []) patternNames3(element, out);
5192
5487
  return;
5193
5488
  case "AssignmentPattern":
5194
- patternNames2(typed.left, out);
5489
+ patternNames3(typed.left, out);
5195
5490
  return;
5196
5491
  case "RestElement":
5197
- patternNames2(typed.argument, out);
5492
+ patternNames3(typed.argument, out);
5198
5493
  return;
5199
5494
  default:
5200
5495
  return;
@@ -5216,9 +5511,9 @@ function moduleScope(parsed, content) {
5216
5511
  const text = source.slice(start, end);
5217
5512
  const names = /* @__PURE__ */ new Set();
5218
5513
  if (kind === "VariableDeclaration") {
5219
- for (const declarator of statement.declarations ?? []) patternNames2(declarator.id, names);
5514
+ for (const declarator of statement.declarations ?? []) patternNames3(declarator.id, names);
5220
5515
  } else {
5221
- patternNames2(statement.id, names);
5516
+ patternNames3(statement.id, names);
5222
5517
  }
5223
5518
  if (names.size === 0) continue;
5224
5519
  const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
@@ -5229,7 +5524,7 @@ function moduleScope(parsed, content) {
5229
5524
  }
5230
5525
 
5231
5526
  // src/componentize.ts
5232
- import MagicString3 from "magic-string";
5527
+ import MagicString4 from "magic-string";
5233
5528
  var SectionInvariantError = class extends Error {
5234
5529
  code = "SECTION_INVARIANT";
5235
5530
  constructor(message) {
@@ -5246,7 +5541,8 @@ function assertSections(total, sections) {
5246
5541
  }
5247
5542
  return sections;
5248
5543
  }
5249
- var SECTION_MARKER = "// @bettercms-ai/convert section v1";
5544
+ var SECTION_MARKER = "// @bettercms-ai/convert section v2";
5545
+ var SECTION_MARKERS = [SECTION_MARKER, "// @bettercms-ai/convert section v1"];
5250
5546
  var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
5251
5547
  var COMPONENT_DIR = "src/components/bcms";
5252
5548
  var SECTIONS_LIB = "src/lib/bcms-sections.ts";
@@ -5268,12 +5564,53 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5268
5564
  let registered = 0;
5269
5565
  let planPending = 0;
5270
5566
  let total = 0;
5567
+ const pend2 = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5271
5568
  const brief = readBrief(briefIn);
5272
5569
  const plan2 = readPlan(planIn);
5570
+ const copy = options?.copy ?? "bind";
5571
+ const instanceOwned = (section) => section.copy === "instance";
5572
+ const notes = unreadableConfigs(sources).map(
5573
+ (path) => ({
5574
+ code: "TSCONFIG_UNREADABLE",
5575
+ file: path,
5576
+ message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
5577
+ })
5578
+ );
5579
+ const refuseRun = (reason, message) => {
5580
+ for (const page of plan2.pages ?? []) {
5581
+ for (const section of page.sections ?? []) {
5582
+ if (section.pending) {
5583
+ planPending++;
5584
+ continue;
5585
+ }
5586
+ total++;
5587
+ pend2(page.route, section.groupKey, reason, message);
5588
+ }
5589
+ }
5590
+ return {
5591
+ files: [],
5592
+ receipt: {
5593
+ planDigest: plan2.digest ?? "",
5594
+ copy,
5595
+ sections: assertSections(total, { extracted: 0, inlineOnly: 0, alreadyExtracted: 0, registered: 0, pending }),
5596
+ components: [],
5597
+ planPending,
5598
+ notes
5599
+ }
5600
+ };
5601
+ };
5602
+ const movesCopy = (plan2.pages ?? []).some(
5603
+ (page) => (page.sections ?? []).some((section) => !section.pending && instanceOwned(section))
5604
+ );
5605
+ if (copy === "instance" && !movesCopy) {
5606
+ return refuseRun(
5607
+ "COPY_MODE_MISMATCH",
5608
+ '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.'
5609
+ );
5610
+ }
5273
5611
  const contents = new Map(sources.map((s) => [s.path, s.content]));
5274
5612
  const fileSet = new Set(contents.keys());
5275
5613
  const aliases = aliasesFrom(sources);
5276
- const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5277
5614
  const trees = /* @__PURE__ */ new Map();
5278
5615
  const treeOf = async (file) => {
5279
5616
  if (trees.has(file)) return trees.get(file);
@@ -5287,7 +5624,9 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5287
5624
  const helperPath = HELPER_PATH.astro;
5288
5625
  const helperExisting = contents.get(helperPath);
5289
5626
  const helperVersion = helperExisting === void 0 ? null : isKnownHelper(helperExisting, "astro");
5290
- const helperBlocked = helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true;
5627
+ if (helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true) {
5628
+ return refuseRun("HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
5629
+ }
5291
5630
  const outFiles = /* @__PURE__ */ new Map();
5292
5631
  const written = /* @__PURE__ */ new Map();
5293
5632
  const shipped = /* @__PURE__ */ new Map();
@@ -5297,6 +5636,29 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5297
5636
  return held;
5298
5637
  };
5299
5638
  const fresh = /* @__PURE__ */ new Set();
5639
+ const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
5640
+ const placementOf = (slug, section) => {
5641
+ const bind = bindOf(section);
5642
+ return bind === void 0 ? { slug } : { slug, bind };
5643
+ };
5644
+ const rebind = (dialect, pageSlug, all, usable) => {
5645
+ if (dialect === null || !EXTRACTABLE.includes(dialect)) return;
5646
+ const order = shipped.get(dialect);
5647
+ const held = order?.get(pageSlug);
5648
+ if (!order || !held) return;
5649
+ const queues = /* @__PURE__ */ new Map();
5650
+ for (const section of all) {
5651
+ const slug = resolveComponent(plan2, section).slug;
5652
+ queues.set(slug, [...queues.get(slug) ?? [], section]);
5653
+ }
5654
+ order.set(
5655
+ pageSlug,
5656
+ held.map((placement) => {
5657
+ const section = queues.get(placement.slug)?.shift();
5658
+ return section && usable.has(section) ? placementOf(placement.slug, section) : placement;
5659
+ })
5660
+ );
5661
+ };
5300
5662
  const registryConflicts = [];
5301
5663
  const registryUnreadable = [];
5302
5664
  for (const dialect of EXTRACTABLE) {
@@ -5322,6 +5684,11 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5322
5684
  if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
5323
5685
  const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
5324
5686
  const registryUnread = registryUnreadable.length > 0;
5687
+ const migrated = registryBlocked || registryUnread ? { files: [], refused: /* @__PURE__ */ new Map() } : await migrateSections(plan2, contents, written, fileSet, aliases);
5688
+ for (const file of migrated.files) {
5689
+ addFile(outFiles, contents, file);
5690
+ contents.set(file.path, file.content);
5691
+ }
5325
5692
  for (const page of plan2.pages ?? []) {
5326
5693
  const sections2 = (page.sections ?? []).filter((section) => {
5327
5694
  if (section.pending) {
@@ -5332,24 +5699,34 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5332
5699
  });
5333
5700
  total += sections2.length;
5334
5701
  if (sections2.length === 0) continue;
5702
+ const prefixes = /* @__PURE__ */ new Map();
5703
+ for (const section of sections2) {
5704
+ const prefix = propPrefix(section);
5705
+ if (prefix === null) {
5706
+ pend2(
5707
+ page.route,
5708
+ section.groupKey,
5709
+ "PROP_MAP_INCONSISTENT",
5710
+ "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."
5711
+ );
5712
+ continue;
5713
+ }
5714
+ prefixes.set(section, prefix);
5715
+ }
5716
+ const usable = sections2.filter((section) => prefixes.has(section));
5717
+ if (usable.length === 0) continue;
5335
5718
  const pendingBefore = pending.length;
5336
5719
  const briefPage = brief.pages.find((p) => p.route === page.route);
5337
5720
  const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5338
- if (helperBlocked) {
5339
- for (const section of sections2) {
5340
- pend(page.route, section.groupKey, "HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
5341
- }
5342
- continue;
5343
- }
5344
5721
  if (registryBlocked) {
5345
- for (const section of sections2) {
5346
- pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5722
+ for (const section of usable) {
5723
+ pend2(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5347
5724
  }
5348
5725
  continue;
5349
5726
  }
5350
5727
  if (registryUnread) {
5351
- for (const section of sections2) {
5352
- pend(
5728
+ for (const section of usable) {
5729
+ pend2(
5353
5730
  page.route,
5354
5731
  section.groupKey,
5355
5732
  "REGISTRY_UNREADABLE",
@@ -5360,36 +5737,49 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5360
5737
  }
5361
5738
  const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5362
5739
  if (!snapshotUsable(contents.get(snapshotPath))) {
5363
- for (const section of sections2) {
5364
- pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5740
+ for (const section of usable) {
5741
+ pend2(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5365
5742
  }
5366
5743
  continue;
5367
5744
  }
5368
5745
  const pageFiles = pageFilesFor(page.route, contents.keys());
5369
5746
  if (pageFiles.length > 1) {
5370
- for (const section of sections2) {
5371
- pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5747
+ for (const section of usable) {
5748
+ pend2(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5372
5749
  }
5373
5750
  continue;
5374
5751
  }
5375
5752
  const file = pageFiles[0];
5376
5753
  if (file === void 0) {
5377
- for (const section of sections2) {
5378
- pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5754
+ for (const section of usable) {
5755
+ pend2(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5379
5756
  }
5380
5757
  continue;
5381
5758
  }
5382
- const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, sections2));
5759
+ const pageDialect = dialectOf(file, contents.get(file) ?? "");
5760
+ const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, usable));
5761
+ let stuck = 0;
5762
+ for (const section of usable) {
5763
+ if (!done.has(section.groupKey)) continue;
5764
+ const why = pageDialect ? migrated.refused.get(refusalKey(pageDialect, resolveComponent(plan2, section).slug)) : void 0;
5765
+ if (why === void 0) continue;
5766
+ done.delete(section.groupKey);
5767
+ stuck++;
5768
+ pend2(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
5769
+ }
5383
5770
  alreadyExtracted += done.size;
5384
- if (done.size === sections2.length) continue;
5771
+ if (done.size + stuck === usable.length) {
5772
+ rebind(pageDialect, pageSlug, page.sections ?? [], new Set(usable));
5773
+ continue;
5774
+ }
5385
5775
  const found = await locateSections(
5386
- sections2.filter((section) => !done.has(section.groupKey)),
5776
+ usable.filter((section) => !done.has(section.groupKey)),
5387
5777
  briefPage?.paths ?? [],
5388
5778
  sources,
5389
5779
  treeOf
5390
5780
  );
5391
5781
  for (const [groupKey, reason] of found.misses) {
5392
- pend(page.route, groupKey, reason.code, reason.message);
5782
+ pend2(page.route, groupKey, reason.code, reason.message);
5393
5783
  }
5394
5784
  const live = found.placed;
5395
5785
  if (live.length === 0) continue;
@@ -5397,14 +5787,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5397
5787
  const dialect = dialectOf(file, content);
5398
5788
  if (!dialect || !EXTRACTABLE.includes(dialect)) {
5399
5789
  for (const placed of live) {
5400
- pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5790
+ pend2(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5401
5791
  }
5402
5792
  continue;
5403
5793
  }
5404
5794
  const tree = await treeOf(file);
5405
5795
  if (!tree) {
5406
5796
  for (const placed of live) {
5407
- pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5797
+ pend2(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5408
5798
  }
5409
5799
  continue;
5410
5800
  }
@@ -5429,7 +5819,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5429
5819
  const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
5430
5820
  if (elements && elements.length > 0) {
5431
5821
  if (alsoRendered.length > 0) {
5432
- pend(
5822
+ pend2(
5433
5823
  page.route,
5434
5824
  placed.section.groupKey,
5435
5825
  "SECTION_ROOT_AMBIGUOUS",
@@ -5441,7 +5831,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5441
5831
  continue;
5442
5832
  }
5443
5833
  if (alsoRendered.length > 1) {
5444
- pend(
5834
+ pend2(
5445
5835
  page.route,
5446
5836
  placed.section.groupKey,
5447
5837
  "SECTION_ROOT_AMBIGUOUS",
@@ -5455,7 +5845,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5455
5845
  importedSections.push({ section: placed.section, path: only, call: held.local });
5456
5846
  continue;
5457
5847
  }
5458
- pend(
5848
+ pend2(
5459
5849
  page.route,
5460
5850
  placed.section.groupKey,
5461
5851
  "SECTION_ROOT_AMBIGUOUS",
@@ -5469,7 +5859,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5469
5859
  const component = resolveComponent(plan2, held.section);
5470
5860
  const already = extractable.get(component.slug);
5471
5861
  if (already !== void 0 && already.path !== held.path) {
5472
- pend(
5862
+ pend2(
5473
5863
  page.route,
5474
5864
  held.section.groupKey,
5475
5865
  "COMPONENT_CONFLICT",
@@ -5480,7 +5870,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5480
5870
  const binding = bindingOf(held.path);
5481
5871
  const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
5482
5872
  if (binding.kind === "namespace" && member === void 0) {
5483
- pend(
5873
+ pend2(
5484
5874
  page.route,
5485
5875
  held.section.groupKey,
5486
5876
  "SECTION_ROOT_AMBIGUOUS",
@@ -5498,7 +5888,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5498
5888
  registered++;
5499
5889
  registeredHere.push({
5500
5890
  slug: component.slug,
5501
- bind: held.section.groupKey,
5891
+ section: held.section,
5502
5892
  call: callSiteOf(tree, member ? `${binding.local}.${member}` : held.call)
5503
5893
  });
5504
5894
  }
@@ -5506,11 +5896,17 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5506
5896
  for (const placed of placedHere) {
5507
5897
  const root = lowestCommonAncestor(placed.elements);
5508
5898
  if (!root) {
5509
- pend(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
5899
+ pend2(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
5510
5900
  continue;
5511
5901
  }
5512
5902
  const component = resolveComponent(plan2, placed.section);
5513
- roots.push({ section: placed.section, route: page.route, root, ...component });
5903
+ roots.push({
5904
+ section: placed.section,
5905
+ route: page.route,
5906
+ root,
5907
+ pathPrefix: prefixes.get(placed.section),
5908
+ ...component
5909
+ });
5514
5910
  }
5515
5911
  const nested = /* @__PURE__ */ new Set();
5516
5912
  for (const a of roots) {
@@ -5522,7 +5918,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5522
5918
  }
5523
5919
  }
5524
5920
  for (const resolved of nested) {
5525
- pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5921
+ pend2(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5526
5922
  }
5527
5923
  const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5528
5924
  const placeable = [];
@@ -5530,16 +5926,34 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5530
5926
  const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
5531
5927
  const existing = contents.get(path);
5532
5928
  if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
5533
- pend(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
5929
+ pend2(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
5534
5930
  continue;
5535
5931
  }
5536
5932
  if (extractable.has(resolved.slug)) {
5537
5933
  placeable.push(resolved);
5538
5934
  continue;
5539
5935
  }
5540
- const built = componentFile(dialect, path, file, content, parsed, resolved);
5936
+ const built = componentFile(dialect, path, file, content, parsed, resolved, resolved.pathPrefix);
5937
+ if (built.unconvertible.length > 0) {
5938
+ pend2(
5939
+ page.route,
5940
+ resolved.section.groupKey,
5941
+ "PROP_MAP_INCONSISTENT",
5942
+ `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.`
5943
+ );
5944
+ continue;
5945
+ }
5946
+ if (built.duplicates.length > 0) {
5947
+ pend2(
5948
+ page.route,
5949
+ resolved.section.groupKey,
5950
+ "DUPLICATE_FIELD_BINDING",
5951
+ `${built.duplicates.join(", ")} carries more than one data-bcms-field, so there is no single address to rewrite.`
5952
+ );
5953
+ continue;
5954
+ }
5541
5955
  if (built.free.length > 0) {
5542
- pend(
5956
+ pend2(
5543
5957
  page.route,
5544
5958
  resolved.section.groupKey,
5545
5959
  "SECTION_FREE_IDENTIFIERS",
@@ -5558,14 +5972,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5558
5972
  }
5559
5973
  if (placeable.length === 0 && registeredHere.length === 0) continue;
5560
5974
  const spans = [
5561
- ...placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey, root: r.root })),
5562
- ...registeredHere.map((r) => ({ slug: r.slug, bind: r.bind, root: r.call }))
5975
+ ...placeable.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.root })),
5976
+ ...registeredHere.map((r) => ({ slug: r.slug, bind: bindOf(r.section), root: r.call }))
5563
5977
  ].sort((a, b) => (a.root?.start ?? Number.MAX_SAFE_INTEGER) - (b.root?.start ?? Number.MAX_SAFE_INTEGER));
5564
5978
  const known = spans.filter((s) => s.root !== null);
5565
5979
  const layout = runOf(known, parsed.roots, content);
5566
5980
  if (layout === null) {
5567
5981
  for (const resolved of placeable) {
5568
- pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5982
+ pend2(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5569
5983
  }
5570
5984
  continue;
5571
5985
  }
@@ -5575,7 +5989,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5575
5989
  shipped.set(dialect, order);
5576
5990
  order.set(
5577
5991
  pageSlug,
5578
- spans.map((s) => ({ slug: s.slug, bind: s.bind }))
5992
+ spans.map((s) => s.bind === void 0 ? { slug: s.slug } : { slug: s.slug, bind: s.bind })
5579
5993
  );
5580
5994
  if (useRun) {
5581
5995
  addFile(outFiles, contents, {
@@ -5584,13 +5998,25 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5584
5998
  content: splicePageRun(file, content, dialect, parsed, known, pageSlug)
5585
5999
  });
5586
6000
  extracted += placeable.length;
5587
- } else if (placeable.length > 0) {
5588
- addFile(outFiles, contents, {
5589
- path: file,
5590
- operation: "modify",
5591
- content: splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable)
5592
- });
5593
- inlineOnly += placeable.length;
6001
+ } else {
6002
+ const inline = placeable.filter((resolved) => !instanceOwned(resolved.section));
6003
+ for (const resolved of placeable) {
6004
+ if (inline.includes(resolved)) continue;
6005
+ pend2(
6006
+ page.route,
6007
+ resolved.section.groupKey,
6008
+ "INLINE_NEEDS_INSTANCE",
6009
+ `${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.`
6010
+ );
6011
+ }
6012
+ if (inline.length > 0) {
6013
+ addFile(outFiles, contents, {
6014
+ path: file,
6015
+ operation: "modify",
6016
+ content: splicePageInline(file, content, dialect, parsed, inline, pageSlug, extractable)
6017
+ });
6018
+ }
6019
+ inlineOnly += inline.length;
5594
6020
  }
5595
6021
  }
5596
6022
  const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
@@ -5618,17 +6044,12 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5618
6044
  files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
5619
6045
  receipt: {
5620
6046
  planDigest: plan2.digest ?? "",
6047
+ copy,
5621
6048
  sections,
5622
6049
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
5623
6050
  components: [...fresh].sort(),
5624
6051
  planPending,
5625
- notes: unreadableConfigs(sources).map(
5626
- (path) => ({
5627
- code: "TSCONFIG_UNREADABLE",
5628
- file: path,
5629
- message: `${path} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
5630
- })
5631
- )
6052
+ notes
5632
6053
  }
5633
6054
  };
5634
6055
  }
@@ -5651,7 +6072,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
5651
6072
  if (!from.specifier.startsWith(".")) continue;
5652
6073
  const resolved = resolveSpecifier(path, from.specifier, files, aliases);
5653
6074
  if (resolved !== null) {
5654
- imports.set(local, { name: local, path: resolved, kind: from.kind, imported: from.imported });
6075
+ imports.set(local, { name: local, path: preferDialect(resolved, dialect, files), kind: from.kind, imported: from.imported });
5655
6076
  }
5656
6077
  }
5657
6078
  const registry = blockOf(content, "REGISTRY");
@@ -5660,6 +6081,7 @@ async function readRegistry(content, dialect, path, files, aliases) {
5660
6081
  const registryRows = propertiesOf(registry);
5661
6082
  const shippedRows = propertiesOf(table);
5662
6083
  if (registryRows === null || shippedRows === null) return null;
6084
+ if (registryRows.length === 0) return null;
5663
6085
  for (const row of registryRows) {
5664
6086
  const named = row.value.match(/^([A-Za-z_$][\w$]*)(?:\.([A-Za-z_$][\w$]*))?$/);
5665
6087
  const entry = named ? imports.get(named[1]) : void 0;
@@ -5669,7 +6091,8 @@ async function readRegistry(content, dialect, path, files, aliases) {
5669
6091
  for (const row of shippedRows) {
5670
6092
  try {
5671
6093
  const placements = JSON.parse(row.value);
5672
- if (!Array.isArray(placements)) return null;
6094
+ if (!Array.isArray(placements) || placements.length === 0 || !placements.every(isPlacement)) return null;
6095
+ if (placements.some((placement) => !components.has(placement.slug))) return null;
5673
6096
  shipped.set(row.key, placements);
5674
6097
  } catch {
5675
6098
  return null;
@@ -5677,6 +6100,18 @@ async function readRegistry(content, dialect, path, files, aliases) {
5677
6100
  }
5678
6101
  return { components, shipped };
5679
6102
  }
6103
+ function preferDialect(resolved, dialect, files) {
6104
+ const wanted = dialect === "astro" ? [".astro"] : [".tsx", ".jsx"];
6105
+ if (wanted.some((extension) => resolved.endsWith(extension))) return resolved;
6106
+ const base = resolved.replace(/\.[^./]+$/, "");
6107
+ return wanted.map((extension) => `${base}${extension}`).find((candidate) => files.has(candidate)) ?? resolved;
6108
+ }
6109
+ function isPlacement(value) {
6110
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
6111
+ const held = value;
6112
+ if (typeof held.slug !== "string") return false;
6113
+ return ["bind", "blockId"].every((key) => held[key] === void 0 || typeof held[key] === "string");
6114
+ }
5680
6115
  function blockOf(content, name) {
5681
6116
  const declaration2 = content.indexOf(`const ${name}`);
5682
6117
  if (declaration2 < 0) return null;
@@ -5923,14 +6358,35 @@ function pascal(value) {
5923
6358
  return /^[A-Za-z]/.test(name) ? name : `Section${name}`;
5924
6359
  }
5925
6360
  var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
5926
- function componentFile(dialect, path, fromFile, content, parsed, resolved) {
5927
- const markup = sliceWithBlockId(content, resolved.root);
6361
+ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathPrefix) {
6362
+ const addresses = fieldAddresses(content, elementsUnder(resolved.root));
6363
+ const unconvertible = [
6364
+ ...addresses.heads.filter((address) => !address.startsWith(pathPrefix)),
6365
+ ...addresses.opaque
6366
+ ];
6367
+ if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
6368
+ const cut = content.slice(resolved.root.start, resolved.root.end);
6369
+ const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
6370
+ const introduced = allocateNames(reserved, [
6371
+ { key: "bcmsFields", name: "bcmsFields" },
6372
+ { key: "bcmsField", name: "bcmsField" }
6373
+ ]);
6374
+ const fieldsName = introduced.get("bcmsFields");
6375
+ const fieldName = introduced.get("bcmsField");
6376
+ const { markup, bound, duplicates } = sectionMarkup(content, resolved.root, fieldName);
6377
+ if (duplicates.length > 0) return { content: "", free: [], duplicates, unconvertible: [] };
5928
6378
  const groupKey = resolved.section.groupKey;
5929
6379
  const locals = helperLocals(parsed, dialect, fromFile);
5930
6380
  const helperNames = ["bcms", "bcmsRows", "bcmsLayout"].flatMap(
5931
6381
  (name) => locals[name].filter((local) => mentions(markup, local)).map((local) => ({ name, local }))
5932
6382
  );
5933
- const helpers = [.../* @__PURE__ */ new Set([...helperNames.map((h) => alias(h.name, h.local)), "bcmsSection"])].sort();
6383
+ const helpers = [
6384
+ .../* @__PURE__ */ new Set([
6385
+ ...helperNames.map((h) => alias(h.name, h.local)),
6386
+ "bcmsSection",
6387
+ ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
6388
+ ])
6389
+ ].sort();
5934
6390
  const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
5935
6391
  const snapshot = snapshotOf(parsed, dialect, fromFile, markup);
5936
6392
  const carry = carryFor({
@@ -5945,20 +6401,23 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved) {
5945
6401
  "bind",
5946
6402
  "overrides",
5947
6403
  "page",
6404
+ fieldName,
5948
6405
  ...helperNames.map((h) => h.local),
5949
6406
  ...snapshot ? [snapshot] : []
5950
6407
  ],
5951
6408
  // The helper and the snapshots are what the component declares for itself.
5952
6409
  skipSpecifier: (repoPath, specifier) => specifier.includes(`${CONTENT_DIR}/`) || repoPath.startsWith(HELPER_PATH[dialect].replace(/\.tsx?$/, ""))
5953
6410
  });
5954
- if (carry.free.length > 0) return { content: "", free: carry.free };
6411
+ if (carry.free.length > 0) return { content: "", free: carry.free, duplicates: [], unconvertible: [] };
5955
6412
  const carried = [...carry.imports];
5956
6413
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
5957
6414
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
5958
6415
  ` : "";
5959
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind ?? ${JSON.stringify(groupKey)}, overrides) };` : "";
6416
+ const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
6417
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
6418
+ const declared = [copy, field].filter(Boolean).join("\n");
5960
6419
  if (dialect === "astro") {
5961
- return { free: [], content: `---
6420
+ return { free: [], duplicates: [], unconvertible: [], content: `---
5962
6421
  ${SECTION_MARKER}
5963
6422
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`].join("\n")}
5964
6423
 
@@ -5973,27 +6432,297 @@ interface Props {
5973
6432
  page?: unknown;
5974
6433
  }
5975
6434
  const { blockId, bind, overrides, page } = Astro.props;
5976
- ${carriedDeclarations}${copy}
6435
+ ${carriedDeclarations}${declared}
5977
6436
  ---
5978
6437
  ${markup}
5979
6438
  ` };
5980
6439
  }
5981
- return { free: [], content: `${SECTION_MARKER}
6440
+ return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
5982
6441
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
5983
6442
  ${carriedDeclarations === "" ? "" : `
5984
6443
  ${carriedDeclarations}`}
5985
6444
  export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
5986
- ${copy}
6445
+ ${declared.split("\n").join("\n ")}
5987
6446
  return (
5988
6447
  ${markup}
5989
6448
  );
5990
6449
  }
5991
6450
  ` };
5992
6451
  }
5993
- function sliceWithBlockId(content, root) {
5994
- const slice = new MagicString3(content.slice(root.start, root.end));
5995
- slice.appendLeft(root.attrInsertAt - root.start, " data-bcms-block={blockId}");
5996
- return slice.toString();
6452
+ function sectionMarkup(content, root, fieldName) {
6453
+ const slice = new MagicString4(content.slice(root.start, root.end));
6454
+ const at = (offset) => offset - root.start;
6455
+ const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
6456
+ if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
6457
+ else {
6458
+ slice.overwrite(at(stamps[0].range.start), at(stamps[0].range.end), "data-bcms-block={blockId}");
6459
+ for (const extra of stamps.slice(1)) {
6460
+ let from = extra.range.start;
6461
+ while (from > root.start && /\s/.test(content[from - 1])) from--;
6462
+ slice.remove(at(from), at(extra.range.end));
6463
+ }
6464
+ }
6465
+ const wrapped = wrapFieldAttrs(slice, content, elementsUnder(root), at, fieldName);
6466
+ return { markup: slice.toString(), ...wrapped };
6467
+ }
6468
+ function wrapFieldAttrs(slice, content, elements, at, fieldName) {
6469
+ let bound = 0;
6470
+ const duplicates = [];
6471
+ for (const element of elements) {
6472
+ const fields = element.attrs.filter((attr) => attr.name === "data-bcms-field");
6473
+ if (fields.length > 1) {
6474
+ duplicates.push(`<${element.tag}>`);
6475
+ continue;
6476
+ }
6477
+ for (const attr of fields) {
6478
+ bound++;
6479
+ if (attr.expressionRange) {
6480
+ slice.appendRight(at(attr.expressionRange.start) + 1, `${fieldName}(`);
6481
+ slice.appendLeft(at(attr.expressionRange.end) - 1, ")");
6482
+ continue;
6483
+ }
6484
+ const value = attr.valueRange ? content.slice(attr.valueRange.start, attr.valueRange.end) : "";
6485
+ slice.overwrite(
6486
+ at(attr.range.start),
6487
+ at(attr.range.end),
6488
+ `data-bcms-field={${fieldName}(${JSON.stringify(value)})}`
6489
+ );
6490
+ }
6491
+ }
6492
+ return { bound, duplicates };
6493
+ }
6494
+ function provableAddress(expression) {
6495
+ const first = expression[0];
6496
+ if (first === '"' || first === "'") {
6497
+ for (let at = 1; at < expression.length; at++) {
6498
+ if (expression[at] === "\\") {
6499
+ at++;
6500
+ continue;
6501
+ }
6502
+ if (expression[at] === first) return at === expression.length - 1 ? expression.slice(1, at) : null;
6503
+ }
6504
+ return null;
6505
+ }
6506
+ if (first !== "`" || expression.length < 2 || !expression.endsWith("`")) return null;
6507
+ const body = expression.slice(1, -1);
6508
+ if (body.includes("`") || body.includes("\\")) return null;
6509
+ let head = null;
6510
+ for (let at = 0; at < body.length; at++) {
6511
+ if (body[at] !== "$" || body[at + 1] !== "{") continue;
6512
+ const close = body.indexOf("}", at + 2);
6513
+ if (close < 0) return null;
6514
+ if (!/^[A-Za-z_$][\w$]*$/.test(body.slice(at + 2, close).trim())) return null;
6515
+ if (body[at - 1] !== "[" || body[close + 1] !== "]") return null;
6516
+ head ??= body.slice(0, at);
6517
+ at = close;
6518
+ }
6519
+ return head ?? body;
6520
+ }
6521
+ function fieldAddresses(content, elements) {
6522
+ const heads = [];
6523
+ const opaque = [];
6524
+ for (const element of elements) {
6525
+ for (const attr of element.attrs) {
6526
+ if (attr.name !== "data-bcms-field") continue;
6527
+ if (!attr.expression && attr.valueRange) {
6528
+ heads.push(content.slice(attr.valueRange.start, attr.valueRange.end));
6529
+ continue;
6530
+ }
6531
+ const inner = attr.expressionRange ? content.slice(attr.expressionRange.start + 1, attr.expressionRange.end - 1).trim() : "";
6532
+ const head = provableAddress(inner);
6533
+ if (head === null) opaque.push(`data-bcms-field={${inner}}`);
6534
+ else heads.push(head);
6535
+ }
6536
+ }
6537
+ return { heads, opaque };
6538
+ }
6539
+ function elementsUnder(...roots) {
6540
+ const out = [];
6541
+ const walk = (node) => {
6542
+ if (node.type === "element") {
6543
+ out.push(node);
6544
+ for (const child of node.children) walk(child);
6545
+ return;
6546
+ }
6547
+ if (node.type === "expr") for (const child of node.children) walk(child);
6548
+ };
6549
+ for (const root of roots) walk(root);
6550
+ return out;
6551
+ }
6552
+ var V1_ANCHOR = {
6553
+ astro: "const { blockId, bind, overrides, page } = Astro.props;",
6554
+ jsx: "}: BcmsSectionProps) {"
6555
+ };
6556
+ var refusalKey = (dialect, slug) => `${dialect}\0${slug}`;
6557
+ function generatedBindings(parsed) {
6558
+ const program = parsed.ast?.program;
6559
+ const body = program?.body ?? [];
6560
+ const scopes = [body];
6561
+ for (const statement of body) {
6562
+ const declaration2 = statement.type === "ExportDefaultDeclaration" ? statement.declaration : void 0;
6563
+ const inner = declaration2?.type === "FunctionDeclaration" ? declaration2.body?.body : void 0;
6564
+ if (inner) scopes.push(inner);
6565
+ }
6566
+ const out = /* @__PURE__ */ new Map();
6567
+ for (const scope of scopes) {
6568
+ for (const statement of scope) {
6569
+ if (statement.type !== "VariableDeclaration") continue;
6570
+ for (const declarator of statement.declarations ?? []) {
6571
+ const id = declarator.id;
6572
+ const name = id?.type === "Identifier" ? id.name : void 0;
6573
+ if (name !== void 0 && !out.has(name)) out.set(name, declarator.init);
6574
+ }
6575
+ }
6576
+ }
6577
+ return out;
6578
+ }
6579
+ function stripBindFallback(parsed, source, content, locals) {
6580
+ if (!parsed.ast?.program) return "unreadable";
6581
+ const offset = parsed.script?.offset ?? 0;
6582
+ let rewritten = 0;
6583
+ let unproven = 0;
6584
+ walkAst(parsed.ast, (node) => {
6585
+ if (node.type !== "CallExpression") return;
6586
+ const argument = (node.arguments ?? [])[1];
6587
+ if (argument?.type !== "LogicalExpression" || argument.operator !== "??") return;
6588
+ const callee = node.callee;
6589
+ if (callee?.type !== "Identifier" || !locals.has(callee.name)) {
6590
+ unproven++;
6591
+ return;
6592
+ }
6593
+ const left = argument.left;
6594
+ source.overwrite(
6595
+ offset + argument.start,
6596
+ offset + argument.end,
6597
+ content.slice(offset + left.start, offset + left.end)
6598
+ );
6599
+ rewritten++;
6600
+ });
6601
+ return rewritten === 0 && unproven > 0 ? "unproven" : "ok";
6602
+ }
6603
+ var isCallOf = (init, callee) => {
6604
+ const fn = init?.type === "CallExpression" ? init.callee : void 0;
6605
+ return fn?.type === "Identifier" && fn.name === callee;
6606
+ };
6607
+ async function migrateSections(plan2, contents, written, fileSet, aliases) {
6608
+ const files = [];
6609
+ const refused = /* @__PURE__ */ new Map();
6610
+ const prefixesFor = (slug) => {
6611
+ const out = /* @__PURE__ */ new Set();
6612
+ for (const page of plan2.pages ?? []) {
6613
+ for (const section of page.sections ?? []) {
6614
+ if (resolveComponent(plan2, section).slug !== slug) continue;
6615
+ const prefix = propPrefix(section);
6616
+ if (prefix !== null) out.add(prefix);
6617
+ }
6618
+ }
6619
+ return [...out];
6620
+ };
6621
+ for (const [dialect, components] of written) {
6622
+ for (const [slug, entry] of components) {
6623
+ const content = contents.get(entry.path);
6624
+ if (content === void 0 || content.includes(SECTION_MARKER)) continue;
6625
+ const older = SECTION_MARKERS.find((marker) => content.includes(marker));
6626
+ if (older === void 0) continue;
6627
+ const candidates = prefixesFor(slug);
6628
+ if (candidates.length === 0) continue;
6629
+ const parsed = await parseFile4(dialect, content);
6630
+ if (parsed.error) {
6631
+ refused.set(refusalKey(dialect, slug), `${entry.path} carries our marker but does not parse (${parsed.error.message}).`);
6632
+ continue;
6633
+ }
6634
+ const elements = elementsUnder(...parsed.roots);
6635
+ const imports = [...importsIn(parsed, dialect)];
6636
+ const helperModule = HELPER_PATH[dialect];
6637
+ const helperLocalsFor = (exported) => new Set(
6638
+ imports.filter(
6639
+ ([, from]) => from.imported === exported && resolveSpecifier(entry.path, from.specifier, fileSet, aliases) === helperModule
6640
+ ).map(([local]) => local)
6641
+ );
6642
+ const { heads: evidence, opaque } = fieldAddresses(content, elements);
6643
+ const pathPrefix = evidence.length + opaque.length === 0 ? "" : opaque.length > 0 ? null : onlyOne(candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)))) ?? null;
6644
+ if (pathPrefix === null) {
6645
+ const matching = candidates.filter((prefix) => evidence.every((head) => head.startsWith(prefix)));
6646
+ refused.set(
6647
+ refusalKey(dialect, slug),
6648
+ 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.`
6649
+ );
6650
+ continue;
6651
+ }
6652
+ const source = new MagicString4(content);
6653
+ const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
6654
+ if (duplicates.length > 0) {
6655
+ 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.`);
6656
+ continue;
6657
+ }
6658
+ const markerEnd = content.indexOf(older) + older.length;
6659
+ source.overwrite(markerEnd - older.length, markerEnd, SECTION_MARKER);
6660
+ const stripped = stripBindFallback(parsed, source, content, helperLocalsFor("bcmsSection"));
6661
+ if (stripped !== "ok") {
6662
+ refused.set(
6663
+ refusalKey(dialect, slug),
6664
+ 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.`
6665
+ );
6666
+ continue;
6667
+ }
6668
+ if (bound > 0) {
6669
+ const locals = generatedBindings(parsed);
6670
+ const names = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...locals.keys()]);
6671
+ const fieldsLocal = [...helperLocalsFor("bcmsFields")][0];
6672
+ if (fieldsLocal === void 0 && names.has("bcmsFields")) {
6673
+ refused.set(
6674
+ refusalKey(dialect, slug),
6675
+ `${entry.path} already binds "bcmsFields" to something other than ${helperModule}'s export.`
6676
+ );
6677
+ continue;
6678
+ }
6679
+ const declared = names.has("bcmsField");
6680
+ if (declared && (fieldsLocal === void 0 || !isCallOf(locals.get("bcmsField"), fieldsLocal))) {
6681
+ refused.set(
6682
+ refusalKey(dialect, slug),
6683
+ `${entry.path} already binds "bcmsField" to something other than a call of ${helperModule}'s bcmsFields.`
6684
+ );
6685
+ continue;
6686
+ }
6687
+ if (!declared) {
6688
+ const anchor = V1_ANCHOR[dialect];
6689
+ const at = anchor ? content.indexOf(anchor) : -1;
6690
+ if (anchor === void 0 || at < 0) {
6691
+ 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.`);
6692
+ continue;
6693
+ }
6694
+ source.appendLeft(
6695
+ at + anchor.length,
6696
+ `
6697
+ ${dialect === "astro" ? "" : " "}const bcmsField = ${fieldsLocal ?? "bcmsFields"}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});`
6698
+ );
6699
+ }
6700
+ if (fieldsLocal === void 0) {
6701
+ const helper = imports.find(([, from]) => from.imported === "bcmsSection")?.[1].specifier ?? relativeImport(entry.path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
6702
+ source.appendLeft(markerEnd, `
6703
+ import { bcmsFields } from "${helper}";`);
6704
+ }
6705
+ }
6706
+ files.push({ path: entry.path, operation: "modify", content: source.toString() });
6707
+ }
6708
+ }
6709
+ return { files, refused };
6710
+ }
6711
+ function propPrefix(section) {
6712
+ const prefixes = /* @__PURE__ */ new Set();
6713
+ for (const field of section.fields ?? []) {
6714
+ const { path, key } = field;
6715
+ if (typeof path !== "string" || typeof key !== "string") continue;
6716
+ if (!path.endsWith(key)) return null;
6717
+ prefixes.add(path.slice(0, path.length - key.length));
6718
+ }
6719
+ if (prefixes.size > 1) return null;
6720
+ return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
6721
+ }
6722
+ function referencedNames2(markup) {
6723
+ const out = /* @__PURE__ */ new Set();
6724
+ for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
6725
+ return out;
5997
6726
  }
5998
6727
  var alias = (name, local) => name === local ? name : `${name} as ${local}`;
5999
6728
  var mentions = (markup, name) => new RegExp(`(?<![\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w$])`).test(markup);
@@ -6197,7 +6926,7 @@ export default function Sections({ page }: { page: string }) {
6197
6926
  function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
6198
6927
  const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
6199
6928
  const local = names.get("Sections");
6200
- const source = new MagicString3(content);
6929
+ const source = new MagicString4(content);
6201
6930
  const first = placeable[0].root;
6202
6931
  const last = placeable[placeable.length - 1].root;
6203
6932
  source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
@@ -6217,7 +6946,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
6217
6946
  { key: "pageSnapshot", name: "pageSnapshot" }
6218
6947
  ]);
6219
6948
  const snapshot = names.get("pageSnapshot");
6220
- const source = new MagicString3(content);
6949
+ const source = new MagicString4(content);
6221
6950
  for (const resolved of placeable) {
6222
6951
  const component = written.get(resolved.slug);
6223
6952
  const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
@@ -6252,7 +6981,7 @@ function addImports(source, parsed, lines) {
6252
6981
  else source.appendLeft(at.at, `
6253
6982
  ${lines.join("\n")}`);
6254
6983
  }
6255
- var isGenerated = (content) => content.includes(SECTION_MARKER) || content.includes(REGISTRY_MARKER);
6984
+ var isGenerated = (content) => SECTION_MARKERS.some((marker) => content.includes(marker)) || content.includes(REGISTRY_MARKER);
6256
6985
  function emitHelper(out, contents, version, overwrite) {
6257
6986
  const path = HELPER_PATH.astro;
6258
6987
  const source = helperSource("astro");
@@ -6298,6 +7027,10 @@ function readComponentizePlan(value) {
6298
7027
  }
6299
7028
 
6300
7029
  // src/index.ts
7030
+ var UNPARSEABLE_DIALECT = {
7031
+ roots: [],
7032
+ error: { code: "DIALECT_UNSUPPORTED", message: "no parser for this extension" }
7033
+ };
6301
7034
  var ConvertError = class extends Error {
6302
7035
  constructor(code, message) {
6303
7036
  super(message);
@@ -6379,7 +7112,7 @@ async function convertSources(briefIn, sources, options) {
6379
7112
  }
6380
7113
  const { located, unlocated } = locate(brief, sources, { stripTags: true });
6381
7114
  const pending = [];
6382
- const pend = (target, reason, file, message) => {
7115
+ const pend2 = (target, reason, file, message) => {
6383
7116
  pending.push({
6384
7117
  route: target.route,
6385
7118
  scope: target.scope,
@@ -6480,7 +7213,7 @@ async function convertSources(briefIn, sources, options) {
6480
7213
  declaredRow(target, false, byHand);
6481
7214
  continue;
6482
7215
  }
6483
- pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
7216
+ pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6484
7217
  }
6485
7218
  const byFile = /* @__PURE__ */ new Map();
6486
7219
  for (const [key, hit] of locatedByKey) {
@@ -6537,12 +7270,12 @@ async function convertSources(briefIn, sources, options) {
6537
7270
  const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
6538
7271
  if (worst) {
6539
7272
  const file = hit.files[outcomes.indexOf(worst)];
6540
- pend(target, worst, file);
7273
+ pend2(target, worst, file);
6541
7274
  } else if (outcomes.every((o) => o === "declared")) {
6542
7275
  const witness = await witnessed(target);
6543
7276
  const byHand = witness === null ? declaredByHand(target, hit.files) : null;
6544
7277
  if (witness === null && byHand === null) {
6545
- pend(
7278
+ pend2(
6546
7279
  target,
6547
7280
  "AMBIGUOUS_LITERAL",
6548
7281
  hit.files[0],
@@ -6555,7 +7288,7 @@ async function convertSources(briefIn, sources, options) {
6555
7288
  } else rewritten++;
6556
7289
  }
6557
7290
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
6558
- const source = new MagicString4(contents.get(file));
7291
+ const source = new MagicString5(contents.get(file));
6559
7292
  const seen = /* @__PURE__ */ new Set();
6560
7293
  for (const splice of splices) {
6561
7294
  const id = `${splice.start}:${splice.end}:${splice.text}`;
@@ -6576,6 +7309,28 @@ async function convertSources(briefIn, sources, options) {
6576
7309
  }
6577
7310
  const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
6578
7311
  emitStubs2(dialects, snapshotSlugs, contents, outFiles);
7312
+ const latest = new Map(contents);
7313
+ for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
7314
+ const canvasSources = [];
7315
+ for (const source of sources) {
7316
+ const content = latest.get(source.path) ?? source.content;
7317
+ if (!/\.[jt]sx$/i.test(source.path)) {
7318
+ canvasSources.push({ path: source.path, content });
7319
+ continue;
7320
+ }
7321
+ const parsed = content === source.content ? await parseOf(source.path) : await parseFile4("jsx", content);
7322
+ canvasSources.push({ path: source.path, content, ...parsed && !parsed.error ? { ast: parsed.ast } : {} });
7323
+ }
7324
+ const canvas = canvasBridge(
7325
+ canvasSources,
7326
+ new Map(brief.pages.map((page) => [page.route, page.slug]))
7327
+ );
7328
+ for (const [path, content] of canvas.edits) {
7329
+ const existing = outFiles.find((f) => f.path === path);
7330
+ if (existing) existing.content = content;
7331
+ else outFiles.push({ path, operation: "modify", content });
7332
+ }
7333
+ pending.push(...canvas.pending);
6579
7334
  const receipt = assertReceipt({
6580
7335
  briefDigest: digest,
6581
7336
  paths: {
@@ -6592,6 +7347,7 @@ async function convertSources(briefIn, sources, options) {
6592
7347
  helpers,
6593
7348
  helperUpgraded,
6594
7349
  dynamicBindings,
7350
+ canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
6595
7351
  notes: unreadableConfigs(sources).map(
6596
7352
  (file) => ({
6597
7353
  code: "TSCONFIG_UNREADABLE",
@@ -6624,6 +7380,30 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
6624
7380
  };
6625
7381
  if (pendingTargets.length === 0) return empty;
6626
7382
  if (!dialect) {
7383
+ if (isTolerantDialect(file)) {
7384
+ const tolerantLiterals = [...new Set(pendingTargets.map((t) => t.literal))];
7385
+ const tolerantSites = findSitesTolerant(content, tolerantLiterals);
7386
+ if (tolerantSites.length > 0) {
7387
+ return plan(
7388
+ file,
7389
+ content,
7390
+ // Never read: `plan` forces `writing = "html"` at tier 2, and every other use of this
7391
+ // argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
7392
+ // the parameter honest rather than widening its type for one caller.
7393
+ "html",
7394
+ pendingTargets,
7395
+ tolerantSites,
7396
+ void 0,
7397
+ // no parse, no import point: tier 2 declares, it does not read
7398
+ outcomes,
7399
+ 2,
7400
+ UNPARSEABLE_DIALECT,
7401
+ drill,
7402
+ []
7403
+ // no parse, so nothing in this file is already declared
7404
+ );
7405
+ }
7406
+ }
6627
7407
  for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
6628
7408
  return { ...empty, dialect: null, outcomes };
6629
7409
  }
@@ -6682,7 +7462,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
6682
7462
  }
6683
7463
  async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
6684
7464
  const writing = tier === 2 ? "html" : dialect;
6685
- const missing = tier === 2 ? "PARSE_ERROR" : "NOT_IN_SOURCE";
7465
+ const missing = tier !== 2 ? "NOT_IN_SOURCE" : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
6686
7466
  const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
6687
7467
  const slugs = [...new Set(targets.map((t) => t.slug))].sort();
6688
7468
  const preset = /* @__PURE__ */ new Map();
@@ -7104,6 +7884,7 @@ function emitStubs2(dialects, slugs, contents, out) {
7104
7884
  }
7105
7885
  }
7106
7886
  export {
7887
+ ASTRO_LANE_PENDING,
7107
7888
  COMPONENT_DIR,
7108
7889
  CONTENT_DIR,
7109
7890
  ConvertError,
@@ -7118,6 +7899,7 @@ export {
7118
7899
  SKIP_DIRS,
7119
7900
  SKIP_FILES,
7120
7901
  SOURCE_EXTENSIONS,
7902
+ SSR_DRAFT_RECIPE,
7121
7903
  STUB_CONTENT,
7122
7904
  SectionInvariantError,
7123
7905
  ValidationError,
@@ -7126,6 +7908,7 @@ export {
7126
7908
  assertSections,
7127
7909
  attrsFor,
7128
7910
  briefDigest,
7911
+ canvasBridge,
7129
7912
  carryFor,
7130
7913
  componentizeSources,
7131
7914
  convertSources,