@bettercms-ai/convert 0.4.0 → 0.6.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(".");
@@ -4261,6 +4276,7 @@ function importLines(imports) {
4261
4276
  }
4262
4277
 
4263
4278
  // src/receipt.ts
4279
+ var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
4264
4280
  var ReceiptInvariantError = class extends Error {
4265
4281
  code = "RECEIPT_INVARIANT";
4266
4282
  constructor(message) {
@@ -4270,10 +4286,11 @@ var ReceiptInvariantError = class extends Error {
4270
4286
  };
4271
4287
  function assertReceipt(receipt) {
4272
4288
  const { declared, rewritten, alreadyDeclared, pending } = receipt.paths;
4273
- const sum = rewritten + alreadyDeclared + pending.length;
4289
+ const paths = pending.filter((p) => !CANVAS_REASONS.has(p.reason));
4290
+ const sum = rewritten + alreadyDeclared + paths.length;
4274
4291
  if (sum !== declared) {
4275
4292
  throw new ReceiptInvariantError(
4276
- `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}.`
4277
4294
  );
4278
4295
  }
4279
4296
  if (receipt.occurrences.rewritten > receipt.occurrences.located) {
@@ -4785,6 +4802,218 @@ var findSites = async (dialect, content, literals) => {
4785
4802
  return parsed.error ? { sites: [], error: parsed.error } : { sites: collectSites(parsed.roots, literals), importAt: parsed.importAt };
4786
4803
  };
4787
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 to `output: 'server'` with @astrojs/node (mode: 'standalone') \u2014 the bettercms() integration injects the draft routes for you \u2014 render pages from loadPage + loadComponents, mount <BcmsLive>, set BCMS_DRAFT_SECRET (BetterCMS hosting sets it for you), 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
+
4788
5017
  // src/tolerant.ts
4789
5018
  var escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4790
5019
  var flexible = (literal) => new RegExp(escape(literal).split(" ").map(escape).join("\\s+"), "gu");
@@ -5214,10 +5443,10 @@ function scanExpression(code) {
5214
5443
  for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
5215
5444
  return;
5216
5445
  case "VariableDeclarator":
5217
- patternNames2(node.id, bound);
5446
+ patternNames3(node.id, bound);
5218
5447
  return;
5219
5448
  case "CatchClause":
5220
- patternNames2(node.param, bound);
5449
+ patternNames3(node.param, bound);
5221
5450
  return;
5222
5451
  default:
5223
5452
  return;
@@ -5225,8 +5454,8 @@ function scanExpression(code) {
5225
5454
  });
5226
5455
  walkAst(ast, (node) => {
5227
5456
  if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
5228
- for (const param of node.params ?? []) patternNames2(param, bound);
5229
- 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);
5230
5459
  }
5231
5460
  });
5232
5461
  const free = /* @__PURE__ */ new Set();
@@ -5241,7 +5470,7 @@ function nameOf2(node, out) {
5241
5470
  nameOf2(node.name, out);
5242
5471
  }
5243
5472
  }
5244
- function patternNames2(node, out) {
5473
+ function patternNames3(node, out) {
5245
5474
  if (!node || typeof node !== "object") return;
5246
5475
  const typed = node;
5247
5476
  switch (typed.type) {
@@ -5250,17 +5479,17 @@ function patternNames2(node, out) {
5250
5479
  return;
5251
5480
  case "ObjectPattern":
5252
5481
  for (const property of typed.properties ?? []) {
5253
- patternNames2(property.type === "RestElement" ? property.argument : property.value, out);
5482
+ patternNames3(property.type === "RestElement" ? property.argument : property.value, out);
5254
5483
  }
5255
5484
  return;
5256
5485
  case "ArrayPattern":
5257
- for (const element of typed.elements ?? []) patternNames2(element, out);
5486
+ for (const element of typed.elements ?? []) patternNames3(element, out);
5258
5487
  return;
5259
5488
  case "AssignmentPattern":
5260
- patternNames2(typed.left, out);
5489
+ patternNames3(typed.left, out);
5261
5490
  return;
5262
5491
  case "RestElement":
5263
- patternNames2(typed.argument, out);
5492
+ patternNames3(typed.argument, out);
5264
5493
  return;
5265
5494
  default:
5266
5495
  return;
@@ -5282,9 +5511,9 @@ function moduleScope(parsed, content) {
5282
5511
  const text = source.slice(start, end);
5283
5512
  const names = /* @__PURE__ */ new Set();
5284
5513
  if (kind === "VariableDeclaration") {
5285
- for (const declarator of statement.declarations ?? []) patternNames2(declarator.id, names);
5514
+ for (const declarator of statement.declarations ?? []) patternNames3(declarator.id, names);
5286
5515
  } else {
5287
- patternNames2(statement.id, names);
5516
+ patternNames3(statement.id, names);
5288
5517
  }
5289
5518
  if (names.size === 0) continue;
5290
5519
  const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
@@ -5295,7 +5524,7 @@ function moduleScope(parsed, content) {
5295
5524
  }
5296
5525
 
5297
5526
  // src/componentize.ts
5298
- import MagicString3 from "magic-string";
5527
+ import MagicString4 from "magic-string";
5299
5528
  var SectionInvariantError = class extends Error {
5300
5529
  code = "SECTION_INVARIANT";
5301
5530
  constructor(message) {
@@ -5335,7 +5564,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5335
5564
  let registered = 0;
5336
5565
  let planPending = 0;
5337
5566
  let total = 0;
5338
- const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5567
+ const pend2 = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5339
5568
  const brief = readBrief(briefIn);
5340
5569
  const plan2 = readPlan(planIn);
5341
5570
  const copy = options?.copy ?? "bind";
@@ -5355,7 +5584,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5355
5584
  continue;
5356
5585
  }
5357
5586
  total++;
5358
- pend(page.route, section.groupKey, reason, message);
5587
+ pend2(page.route, section.groupKey, reason, message);
5359
5588
  }
5360
5589
  }
5361
5590
  return {
@@ -5474,7 +5703,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5474
5703
  for (const section of sections2) {
5475
5704
  const prefix = propPrefix(section);
5476
5705
  if (prefix === null) {
5477
- pend(
5706
+ pend2(
5478
5707
  page.route,
5479
5708
  section.groupKey,
5480
5709
  "PROP_MAP_INCONSISTENT",
@@ -5491,13 +5720,13 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5491
5720
  const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5492
5721
  if (registryBlocked) {
5493
5722
  for (const section of usable) {
5494
- pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5723
+ pend2(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5495
5724
  }
5496
5725
  continue;
5497
5726
  }
5498
5727
  if (registryUnread) {
5499
5728
  for (const section of usable) {
5500
- pend(
5729
+ pend2(
5501
5730
  page.route,
5502
5731
  section.groupKey,
5503
5732
  "REGISTRY_UNREADABLE",
@@ -5509,21 +5738,21 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5509
5738
  const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5510
5739
  if (!snapshotUsable(contents.get(snapshotPath))) {
5511
5740
  for (const section of usable) {
5512
- pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5741
+ pend2(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5513
5742
  }
5514
5743
  continue;
5515
5744
  }
5516
5745
  const pageFiles = pageFilesFor(page.route, contents.keys());
5517
5746
  if (pageFiles.length > 1) {
5518
5747
  for (const section of usable) {
5519
- pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5748
+ pend2(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5520
5749
  }
5521
5750
  continue;
5522
5751
  }
5523
5752
  const file = pageFiles[0];
5524
5753
  if (file === void 0) {
5525
5754
  for (const section of usable) {
5526
- pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5755
+ pend2(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5527
5756
  }
5528
5757
  continue;
5529
5758
  }
@@ -5536,7 +5765,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5536
5765
  if (why === void 0) continue;
5537
5766
  done.delete(section.groupKey);
5538
5767
  stuck++;
5539
- pend(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
5768
+ pend2(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
5540
5769
  }
5541
5770
  alreadyExtracted += done.size;
5542
5771
  if (done.size + stuck === usable.length) {
@@ -5550,7 +5779,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5550
5779
  treeOf
5551
5780
  );
5552
5781
  for (const [groupKey, reason] of found.misses) {
5553
- pend(page.route, groupKey, reason.code, reason.message);
5782
+ pend2(page.route, groupKey, reason.code, reason.message);
5554
5783
  }
5555
5784
  const live = found.placed;
5556
5785
  if (live.length === 0) continue;
@@ -5558,14 +5787,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5558
5787
  const dialect = dialectOf(file, content);
5559
5788
  if (!dialect || !EXTRACTABLE.includes(dialect)) {
5560
5789
  for (const placed of live) {
5561
- 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"}.`);
5562
5791
  }
5563
5792
  continue;
5564
5793
  }
5565
5794
  const tree = await treeOf(file);
5566
5795
  if (!tree) {
5567
5796
  for (const placed of live) {
5568
- 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.`);
5569
5798
  }
5570
5799
  continue;
5571
5800
  }
@@ -5590,7 +5819,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5590
5819
  const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
5591
5820
  if (elements && elements.length > 0) {
5592
5821
  if (alsoRendered.length > 0) {
5593
- pend(
5822
+ pend2(
5594
5823
  page.route,
5595
5824
  placed.section.groupKey,
5596
5825
  "SECTION_ROOT_AMBIGUOUS",
@@ -5602,7 +5831,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5602
5831
  continue;
5603
5832
  }
5604
5833
  if (alsoRendered.length > 1) {
5605
- pend(
5834
+ pend2(
5606
5835
  page.route,
5607
5836
  placed.section.groupKey,
5608
5837
  "SECTION_ROOT_AMBIGUOUS",
@@ -5616,7 +5845,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5616
5845
  importedSections.push({ section: placed.section, path: only, call: held.local });
5617
5846
  continue;
5618
5847
  }
5619
- pend(
5848
+ pend2(
5620
5849
  page.route,
5621
5850
  placed.section.groupKey,
5622
5851
  "SECTION_ROOT_AMBIGUOUS",
@@ -5630,7 +5859,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5630
5859
  const component = resolveComponent(plan2, held.section);
5631
5860
  const already = extractable.get(component.slug);
5632
5861
  if (already !== void 0 && already.path !== held.path) {
5633
- pend(
5862
+ pend2(
5634
5863
  page.route,
5635
5864
  held.section.groupKey,
5636
5865
  "COMPONENT_CONFLICT",
@@ -5641,7 +5870,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5641
5870
  const binding = bindingOf(held.path);
5642
5871
  const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
5643
5872
  if (binding.kind === "namespace" && member === void 0) {
5644
- pend(
5873
+ pend2(
5645
5874
  page.route,
5646
5875
  held.section.groupKey,
5647
5876
  "SECTION_ROOT_AMBIGUOUS",
@@ -5667,7 +5896,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5667
5896
  for (const placed of placedHere) {
5668
5897
  const root = lowestCommonAncestor(placed.elements);
5669
5898
  if (!root) {
5670
- 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.");
5671
5900
  continue;
5672
5901
  }
5673
5902
  const component = resolveComponent(plan2, placed.section);
@@ -5689,7 +5918,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5689
5918
  }
5690
5919
  }
5691
5920
  for (const resolved of nested) {
5692
- 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.");
5693
5922
  }
5694
5923
  const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5695
5924
  const placeable = [];
@@ -5697,7 +5926,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5697
5926
  const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
5698
5927
  const existing = contents.get(path);
5699
5928
  if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
5700
- 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.`);
5701
5930
  continue;
5702
5931
  }
5703
5932
  if (extractable.has(resolved.slug)) {
@@ -5706,7 +5935,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5706
5935
  }
5707
5936
  const built = componentFile(dialect, path, file, content, parsed, resolved, resolved.pathPrefix);
5708
5937
  if (built.unconvertible.length > 0) {
5709
- pend(
5938
+ pend2(
5710
5939
  page.route,
5711
5940
  resolved.section.groupKey,
5712
5941
  "PROP_MAP_INCONSISTENT",
@@ -5715,7 +5944,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5715
5944
  continue;
5716
5945
  }
5717
5946
  if (built.duplicates.length > 0) {
5718
- pend(
5947
+ pend2(
5719
5948
  page.route,
5720
5949
  resolved.section.groupKey,
5721
5950
  "DUPLICATE_FIELD_BINDING",
@@ -5724,7 +5953,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5724
5953
  continue;
5725
5954
  }
5726
5955
  if (built.free.length > 0) {
5727
- pend(
5956
+ pend2(
5728
5957
  page.route,
5729
5958
  resolved.section.groupKey,
5730
5959
  "SECTION_FREE_IDENTIFIERS",
@@ -5750,7 +5979,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5750
5979
  const layout = runOf(known, parsed.roots, content);
5751
5980
  if (layout === null) {
5752
5981
  for (const resolved of placeable) {
5753
- 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.");
5754
5983
  }
5755
5984
  continue;
5756
5985
  }
@@ -5773,7 +6002,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5773
6002
  const inline = placeable.filter((resolved) => !instanceOwned(resolved.section));
5774
6003
  for (const resolved of placeable) {
5775
6004
  if (inline.includes(resolved)) continue;
5776
- pend(
6005
+ pend2(
5777
6006
  page.route,
5778
6007
  resolved.section.groupKey,
5779
6008
  "INLINE_NEEDS_INSTANCE",
@@ -6221,7 +6450,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
6221
6450
  ` };
6222
6451
  }
6223
6452
  function sectionMarkup(content, root, fieldName) {
6224
- const slice = new MagicString3(content.slice(root.start, root.end));
6453
+ const slice = new MagicString4(content.slice(root.start, root.end));
6225
6454
  const at = (offset) => offset - root.start;
6226
6455
  const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
6227
6456
  if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
@@ -6420,7 +6649,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
6420
6649
  );
6421
6650
  continue;
6422
6651
  }
6423
- const source = new MagicString3(content);
6652
+ const source = new MagicString4(content);
6424
6653
  const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
6425
6654
  if (duplicates.length > 0) {
6426
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.`);
@@ -6697,7 +6926,7 @@ export default function Sections({ page }: { page: string }) {
6697
6926
  function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
6698
6927
  const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
6699
6928
  const local = names.get("Sections");
6700
- const source = new MagicString3(content);
6929
+ const source = new MagicString4(content);
6701
6930
  const first = placeable[0].root;
6702
6931
  const last = placeable[placeable.length - 1].root;
6703
6932
  source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
@@ -6717,7 +6946,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
6717
6946
  { key: "pageSnapshot", name: "pageSnapshot" }
6718
6947
  ]);
6719
6948
  const snapshot = names.get("pageSnapshot");
6720
- const source = new MagicString3(content);
6949
+ const source = new MagicString4(content);
6721
6950
  for (const resolved of placeable) {
6722
6951
  const component = written.get(resolved.slug);
6723
6952
  const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
@@ -6798,6 +7027,10 @@ function readComponentizePlan(value) {
6798
7027
  }
6799
7028
 
6800
7029
  // src/index.ts
7030
+ var UNPARSEABLE_DIALECT = {
7031
+ roots: [],
7032
+ error: { code: "DIALECT_UNSUPPORTED", message: "no parser for this extension" }
7033
+ };
6801
7034
  var ConvertError = class extends Error {
6802
7035
  constructor(code, message) {
6803
7036
  super(message);
@@ -6879,7 +7112,7 @@ async function convertSources(briefIn, sources, options) {
6879
7112
  }
6880
7113
  const { located, unlocated } = locate(brief, sources, { stripTags: true });
6881
7114
  const pending = [];
6882
- const pend = (target, reason, file, message) => {
7115
+ const pend2 = (target, reason, file, message) => {
6883
7116
  pending.push({
6884
7117
  route: target.route,
6885
7118
  scope: target.scope,
@@ -6980,7 +7213,7 @@ async function convertSources(briefIn, sources, options) {
6980
7213
  declaredRow(target, false, byHand);
6981
7214
  continue;
6982
7215
  }
6983
- pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
7216
+ pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6984
7217
  }
6985
7218
  const byFile = /* @__PURE__ */ new Map();
6986
7219
  for (const [key, hit] of locatedByKey) {
@@ -7037,12 +7270,12 @@ async function convertSources(briefIn, sources, options) {
7037
7270
  const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
7038
7271
  if (worst) {
7039
7272
  const file = hit.files[outcomes.indexOf(worst)];
7040
- pend(target, worst, file);
7273
+ pend2(target, worst, file);
7041
7274
  } else if (outcomes.every((o) => o === "declared")) {
7042
7275
  const witness = await witnessed(target);
7043
7276
  const byHand = witness === null ? declaredByHand(target, hit.files) : null;
7044
7277
  if (witness === null && byHand === null) {
7045
- pend(
7278
+ pend2(
7046
7279
  target,
7047
7280
  "AMBIGUOUS_LITERAL",
7048
7281
  hit.files[0],
@@ -7055,7 +7288,7 @@ async function convertSources(briefIn, sources, options) {
7055
7288
  } else rewritten++;
7056
7289
  }
7057
7290
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
7058
- const source = new MagicString4(contents.get(file));
7291
+ const source = new MagicString5(contents.get(file));
7059
7292
  const seen = /* @__PURE__ */ new Set();
7060
7293
  for (const splice of splices) {
7061
7294
  const id = `${splice.start}:${splice.end}:${splice.text}`;
@@ -7076,6 +7309,28 @@ async function convertSources(briefIn, sources, options) {
7076
7309
  }
7077
7310
  const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
7078
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);
7079
7334
  const receipt = assertReceipt({
7080
7335
  briefDigest: digest,
7081
7336
  paths: {
@@ -7092,6 +7347,7 @@ async function convertSources(briefIn, sources, options) {
7092
7347
  helpers,
7093
7348
  helperUpgraded,
7094
7349
  dynamicBindings,
7350
+ canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
7095
7351
  notes: unreadableConfigs(sources).map(
7096
7352
  (file) => ({
7097
7353
  code: "TSCONFIG_UNREADABLE",
@@ -7124,6 +7380,30 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7124
7380
  };
7125
7381
  if (pendingTargets.length === 0) return empty;
7126
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
+ }
7127
7407
  for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
7128
7408
  return { ...empty, dialect: null, outcomes };
7129
7409
  }
@@ -7182,7 +7462,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7182
7462
  }
7183
7463
  async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
7184
7464
  const writing = tier === 2 ? "html" : dialect;
7185
- 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";
7186
7466
  const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
7187
7467
  const slugs = [...new Set(targets.map((t) => t.slug))].sort();
7188
7468
  const preset = /* @__PURE__ */ new Map();
@@ -7604,6 +7884,7 @@ function emitStubs2(dialects, slugs, contents, out) {
7604
7884
  }
7605
7885
  }
7606
7886
  export {
7887
+ ASTRO_LANE_PENDING,
7607
7888
  COMPONENT_DIR,
7608
7889
  CONTENT_DIR,
7609
7890
  ConvertError,
@@ -7618,6 +7899,7 @@ export {
7618
7899
  SKIP_DIRS,
7619
7900
  SKIP_FILES,
7620
7901
  SOURCE_EXTENSIONS,
7902
+ SSR_DRAFT_RECIPE,
7621
7903
  STUB_CONTENT,
7622
7904
  SectionInvariantError,
7623
7905
  ValidationError,
@@ -7626,6 +7908,7 @@ export {
7626
7908
  assertSections,
7627
7909
  attrsFor,
7628
7910
  briefDigest,
7911
+ canvasBridge,
7629
7912
  carryFor,
7630
7913
  componentizeSources,
7631
7914
  convertSources,