@bettercms-ai/convert 0.4.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/cli.js CHANGED
@@ -5,7 +5,7 @@ import { readdir, readFile, writeFile, mkdir, stat } from "fs/promises";
5
5
  import { dirname, join as join2, relative, resolve, sep } from "path";
6
6
 
7
7
  // src/index.ts
8
- import MagicString4 from "magic-string";
8
+ import MagicString5 from "magic-string";
9
9
 
10
10
  // src/brief.ts
11
11
  import { createHash } from "crypto";
@@ -27,6 +27,21 @@ var BY_EXTENSION = {
27
27
  ".svelte": "svelte",
28
28
  ".vue": "vue"
29
29
  };
30
+ var TOLERANT_EXTENSIONS = /* @__PURE__ */ new Set([
31
+ ".md",
32
+ ".mdx",
33
+ ".njk",
34
+ ".hbs",
35
+ ".ejs",
36
+ ".liquid",
37
+ ".erb",
38
+ ".twig"
39
+ ]);
40
+ function isTolerantDialect(path) {
41
+ const lower = path.toLowerCase();
42
+ const at = lower.lastIndexOf(".");
43
+ return at >= 0 && TOLERANT_EXTENSIONS.has(lower.slice(at));
44
+ }
30
45
  function dialectOf(path, _content) {
31
46
  const lower = path.toLowerCase();
32
47
  const at = lower.lastIndexOf(".");
@@ -4263,6 +4278,7 @@ function importLines(imports) {
4263
4278
  }
4264
4279
 
4265
4280
  // src/receipt.ts
4281
+ var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
4266
4282
  var ReceiptInvariantError = class extends Error {
4267
4283
  code = "RECEIPT_INVARIANT";
4268
4284
  constructor(message) {
@@ -4272,10 +4288,11 @@ var ReceiptInvariantError = class extends Error {
4272
4288
  };
4273
4289
  function assertReceipt(receipt) {
4274
4290
  const { declared, rewritten, alreadyDeclared, pending } = receipt.paths;
4275
- const sum = rewritten + alreadyDeclared + pending.length;
4291
+ const paths = pending.filter((p) => !CANVAS_REASONS.has(p.reason));
4292
+ const sum = rewritten + alreadyDeclared + paths.length;
4276
4293
  if (sum !== declared) {
4277
4294
  throw new ReceiptInvariantError(
4278
- `The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${pending.length} pending = ${sum}.`
4295
+ `The receipt does not account for every path: ${declared} declared but ${rewritten} rewritten + ${alreadyDeclared} already declared + ${paths.length} pending = ${sum}.`
4279
4296
  );
4280
4297
  }
4281
4298
  if (receipt.occurrences.rewritten > receipt.occurrences.located) {
@@ -4775,6 +4792,218 @@ function tryParse3(code) {
4775
4792
  var PARSE_FILE = { html: parse2, astro: parseFile, jsx: parse, svelte: parseFile2, vue: parseFile3 };
4776
4793
  var parseFile4 = (dialect, content) => Promise.resolve(PARSE_FILE[dialect](content));
4777
4794
 
4795
+ // src/canvas.ts
4796
+ import MagicString3 from "magic-string";
4797
+ var SSR_DRAFT_RECIPE = "Astro renders this site at build time, so there is no server to answer the editor's draft request. Either mount <BcmsLiveBlocks client:load> from @bettercms-ai/astro on the pages whose body is a block tree, or switch the preview routes to `output: 'server'` with the node adapter, mount @bettercms-ai/astro's draft routes, set BCMS_DRAFT_SECRET, redeploy, and switch the project to 'Run as a server app'.";
4798
+ var isNode = (value) => !!value && typeof value === "object" && typeof value.type === "string";
4799
+ var FUNCTIONS3 = /* @__PURE__ */ new Set([
4800
+ "FunctionDeclaration",
4801
+ "FunctionExpression",
4802
+ "ArrowFunctionExpression",
4803
+ "ObjectMethod",
4804
+ "ClassMethod"
4805
+ ]);
4806
+ function patternNames2(node, out) {
4807
+ if (!isNode(node)) return;
4808
+ switch (node.type) {
4809
+ case "Identifier":
4810
+ out.add(String(node.name));
4811
+ return;
4812
+ case "ObjectPattern":
4813
+ for (const p of node.properties ?? [])
4814
+ patternNames2(p.type === "RestElement" ? p.argument : p.value, out);
4815
+ return;
4816
+ case "ArrayPattern":
4817
+ for (const el of node.elements ?? []) patternNames2(el, out);
4818
+ return;
4819
+ case "AssignmentPattern":
4820
+ patternNames2(node.left, out);
4821
+ return;
4822
+ case "RestElement":
4823
+ patternNames2(node.argument, out);
4824
+ return;
4825
+ default:
4826
+ return;
4827
+ }
4828
+ }
4829
+ function statementNames2(statements, out) {
4830
+ for (const statement of statements ?? []) {
4831
+ if (!isNode(statement)) continue;
4832
+ const node = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
4833
+ if (!isNode(node)) continue;
4834
+ if (node.type === "VariableDeclaration")
4835
+ for (const d of node.declarations ?? []) patternNames2(d.id, out);
4836
+ else if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") patternNames2(node.id, out);
4837
+ else if (node.type === "ImportDeclaration")
4838
+ for (const specifier of node.specifiers ?? []) patternNames2(specifier.local, out);
4839
+ }
4840
+ }
4841
+ function functionBindings(fn) {
4842
+ const out = /* @__PURE__ */ new Set();
4843
+ for (const param of fn.params ?? []) patternNames2(param, out);
4844
+ const body = fn.body;
4845
+ if (body?.type === "BlockStatement") statementNames2(body.body, out);
4846
+ return out;
4847
+ }
4848
+ function stringLiterals(node, out) {
4849
+ if (!isNode(node)) {
4850
+ if (Array.isArray(node)) for (const child of node) stringLiterals(child, out);
4851
+ return;
4852
+ }
4853
+ if (FUNCTIONS3.has(node.type)) return;
4854
+ if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
4855
+ for (const key of Object.keys(node)) {
4856
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
4857
+ stringLiterals(node[key], out);
4858
+ }
4859
+ }
4860
+ function usages(ast, local) {
4861
+ const found = [];
4862
+ const moduleScope2 = /* @__PURE__ */ new Set();
4863
+ statementNames2(ast?.program?.body, moduleScope2);
4864
+ const walk = (node, scope, literals) => {
4865
+ if (!isNode(node)) {
4866
+ if (Array.isArray(node)) for (const child of node) walk(child, scope, literals);
4867
+ return;
4868
+ }
4869
+ let nextScope = scope;
4870
+ let nextLiterals = literals;
4871
+ if (FUNCTIONS3.has(node.type)) {
4872
+ nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
4873
+ nextLiterals = /* @__PURE__ */ new Set();
4874
+ stringLiterals(node.body, nextLiterals);
4875
+ }
4876
+ if (node.type === "JSXElement") {
4877
+ const name = node.openingElement?.name;
4878
+ if (name?.type === "JSXIdentifier" && name.name === local)
4879
+ found.push({ element: node, scope: nextScope, literals: nextLiterals });
4880
+ }
4881
+ for (const key of Object.keys(node)) {
4882
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
4883
+ walk(node[key], nextScope, nextLiterals);
4884
+ }
4885
+ };
4886
+ walk(ast?.program ?? ast, moduleScope2, /* @__PURE__ */ new Set());
4887
+ return found;
4888
+ }
4889
+ function bridgedAlready(ast) {
4890
+ const body = ast?.program?.body ?? [];
4891
+ return body.some(
4892
+ (statement) => statement.type === "ImportDeclaration" && String(statement.source?.value ?? "") === "@bettercms-ai/next/draft-bridge" && (statement.specifiers ?? []).some(
4893
+ (specifier) => specifier.imported?.name === "BcmsDraftBridge"
4894
+ )
4895
+ );
4896
+ }
4897
+ function blocksImport(ast) {
4898
+ const body = ast?.program?.body ?? [];
4899
+ for (const statement of body) {
4900
+ if (statement.type !== "ImportDeclaration") continue;
4901
+ const source = String(statement.source?.value ?? "");
4902
+ if (source !== "@bettercms-ai/next" && source !== "@bettercms-ai/next/blocks") continue;
4903
+ for (const specifier of statement.specifiers ?? []) {
4904
+ const imported = specifier.imported;
4905
+ if (specifier.type !== "ImportSpecifier" || imported?.name !== "BcmsBlocks") continue;
4906
+ return { declaration: statement, specifier, local: String(specifier.local.name) };
4907
+ }
4908
+ }
4909
+ return null;
4910
+ }
4911
+ function slugExpression(usage, routes) {
4912
+ if (usage.scope.has("slug")) return "{slug}";
4913
+ if (usage.scope.has("page")) return "{page.slug}";
4914
+ const matched = [...new Set([...usage.literals].map((l) => routes.get(l)).filter((s) => !!s))];
4915
+ return matched.length === 1 ? JSON.stringify(matched[0]) : null;
4916
+ }
4917
+ var lineOf = (content, offset) => content.slice(0, offset).split("\n").length;
4918
+ var pend = (reason, hint, file, line) => ({
4919
+ route: "",
4920
+ scope: "page",
4921
+ path: reason === "SSR_DRAFT_ROUTE" ? "canvas.lane" : "canvas.bridge",
4922
+ kind: "canvas",
4923
+ ...file ? { file } : {},
4924
+ ...line ? { line } : {},
4925
+ reason,
4926
+ hint
4927
+ });
4928
+ var ASTRO_LANE_PENDING = pend("SSR_DRAFT_ROUTE", SSR_DRAFT_RECIPE);
4929
+ function canvasBridge(sources, routes) {
4930
+ if (sources.some((s) => s.path.toLowerCase().endsWith(".astro")))
4931
+ return { lane: "pending", files: [], edits: /* @__PURE__ */ new Map(), pending: [ASTRO_LANE_PENDING] };
4932
+ const files = [];
4933
+ const edits = /* @__PURE__ */ new Map();
4934
+ const pending = [];
4935
+ for (const source of sources) {
4936
+ if (!/\.[jt]sx$/i.test(source.path) || !source.ast) continue;
4937
+ if (bridgedAlready(source.ast)) {
4938
+ files.push(source.path);
4939
+ continue;
4940
+ }
4941
+ const imported = blocksImport(source.ast);
4942
+ if (!imported) continue;
4943
+ const found = usages(source.ast, imported.local);
4944
+ const slugs = found.map((usage) => slugExpression(usage, routes));
4945
+ const unresolved = found.findIndex((_, i) => slugs[i] === null);
4946
+ if (found.length === 0 || unresolved !== -1) {
4947
+ const at = found[unresolved === -1 ? 0 : unresolved]?.element.start;
4948
+ pending.push(
4949
+ pend(
4950
+ "CANVAS_BRIDGE_MANUAL",
4951
+ "Wrap this page's <BcmsBlocks> in <BcmsDraftBridge slug={\u2026}> from @bettercms-ai/next/draft-bridge. No slug expression was derivable in the component's own scope, so the codemod will not guess one: a bridge on the wrong slug never updates in the editor.",
4952
+ source.path,
4953
+ at === void 0 ? void 0 : lineOf(source.content, at)
4954
+ )
4955
+ );
4956
+ continue;
4957
+ }
4958
+ const out = new MagicString3(source.content);
4959
+ rewriteImport(out, imported);
4960
+ for (const [i, usage] of found.entries()) rewriteElement(out, usage, imported.local, slugs[i]);
4961
+ edits.set(source.path, out.toString());
4962
+ files.push(source.path);
4963
+ }
4964
+ return { lane: files.length > 0 ? "bridge" : "pending", files, edits, pending };
4965
+ }
4966
+ function rewriteImport(out, imported) {
4967
+ const { declaration: declaration2, specifier, local } = imported;
4968
+ const line = `import { BcmsDraftBridge${local === "BcmsBlocks" ? "" : ` as ${local}`} } from "@bettercms-ai/next/draft-bridge";`;
4969
+ const specifiers = declaration2.specifiers ?? [];
4970
+ const named = specifiers.filter((s) => s.type === "ImportSpecifier");
4971
+ if (named.length === 1) {
4972
+ if (specifiers.length === 1) {
4973
+ out.overwrite(declaration2.start, declaration2.end, line);
4974
+ return;
4975
+ }
4976
+ const other = specifiers.find((s) => s.type !== "ImportSpecifier");
4977
+ const close = out.original.indexOf("}", named[0].end);
4978
+ out.remove(other.end, close + 1);
4979
+ out.appendRight(declaration2.end, `
4980
+ ${line}`);
4981
+ return;
4982
+ }
4983
+ const index = named.indexOf(specifier);
4984
+ const previous = named[index - 1];
4985
+ const from = previous ? previous.end : specifier.start;
4986
+ const to = previous ? specifier.end : named[index + 1].start;
4987
+ out.remove(from, to);
4988
+ out.appendRight(declaration2.end, `
4989
+ ${line}`);
4990
+ }
4991
+ function rewriteElement(out, usage, local, slug) {
4992
+ const opening = usage.element.openingElement;
4993
+ const name = opening.name;
4994
+ if (local === "BcmsBlocks") {
4995
+ out.overwrite(name.start, name.end, "BcmsDraftBridge");
4996
+ const closing = usage.element.closingElement;
4997
+ if (closing) {
4998
+ const closingName = closing.name;
4999
+ out.overwrite(closingName.start, closingName.end, "BcmsDraftBridge");
5000
+ }
5001
+ }
5002
+ const attributes = opening.attributes ?? [];
5003
+ const hasSlug = attributes.some((a) => a.name?.name === "slug");
5004
+ if (!hasSlug) out.appendLeft(name.end, ` slug=${slug}`);
5005
+ }
5006
+
4778
5007
  // src/tolerant.ts
4779
5008
  var escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4780
5009
  var flexible = (literal) => new RegExp(escape(literal).split(" ").map(escape).join("\\s+"), "gu");
@@ -5196,10 +5425,10 @@ function scanExpression(code) {
5196
5425
  for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
5197
5426
  return;
5198
5427
  case "VariableDeclarator":
5199
- patternNames2(node.id, bound);
5428
+ patternNames3(node.id, bound);
5200
5429
  return;
5201
5430
  case "CatchClause":
5202
- patternNames2(node.param, bound);
5431
+ patternNames3(node.param, bound);
5203
5432
  return;
5204
5433
  default:
5205
5434
  return;
@@ -5207,8 +5436,8 @@ function scanExpression(code) {
5207
5436
  });
5208
5437
  walkAst(ast, (node) => {
5209
5438
  if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
5210
- for (const param of node.params ?? []) patternNames2(param, bound);
5211
- if (node.id) patternNames2(node.id, bound);
5439
+ for (const param of node.params ?? []) patternNames3(param, bound);
5440
+ if (node.id) patternNames3(node.id, bound);
5212
5441
  }
5213
5442
  });
5214
5443
  const free = /* @__PURE__ */ new Set();
@@ -5223,7 +5452,7 @@ function nameOf2(node, out) {
5223
5452
  nameOf2(node.name, out);
5224
5453
  }
5225
5454
  }
5226
- function patternNames2(node, out) {
5455
+ function patternNames3(node, out) {
5227
5456
  if (!node || typeof node !== "object") return;
5228
5457
  const typed = node;
5229
5458
  switch (typed.type) {
@@ -5232,17 +5461,17 @@ function patternNames2(node, out) {
5232
5461
  return;
5233
5462
  case "ObjectPattern":
5234
5463
  for (const property of typed.properties ?? []) {
5235
- patternNames2(property.type === "RestElement" ? property.argument : property.value, out);
5464
+ patternNames3(property.type === "RestElement" ? property.argument : property.value, out);
5236
5465
  }
5237
5466
  return;
5238
5467
  case "ArrayPattern":
5239
- for (const element of typed.elements ?? []) patternNames2(element, out);
5468
+ for (const element of typed.elements ?? []) patternNames3(element, out);
5240
5469
  return;
5241
5470
  case "AssignmentPattern":
5242
- patternNames2(typed.left, out);
5471
+ patternNames3(typed.left, out);
5243
5472
  return;
5244
5473
  case "RestElement":
5245
- patternNames2(typed.argument, out);
5474
+ patternNames3(typed.argument, out);
5246
5475
  return;
5247
5476
  default:
5248
5477
  return;
@@ -5264,9 +5493,9 @@ function moduleScope(parsed, content) {
5264
5493
  const text = source.slice(start, end);
5265
5494
  const names = /* @__PURE__ */ new Set();
5266
5495
  if (kind === "VariableDeclaration") {
5267
- for (const declarator of statement.declarations ?? []) patternNames2(declarator.id, names);
5496
+ for (const declarator of statement.declarations ?? []) patternNames3(declarator.id, names);
5268
5497
  } else {
5269
- patternNames2(statement.id, names);
5498
+ patternNames3(statement.id, names);
5270
5499
  }
5271
5500
  if (names.size === 0) continue;
5272
5501
  const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
@@ -5277,7 +5506,7 @@ function moduleScope(parsed, content) {
5277
5506
  }
5278
5507
 
5279
5508
  // src/componentize.ts
5280
- import MagicString3 from "magic-string";
5509
+ import MagicString4 from "magic-string";
5281
5510
  var SectionInvariantError = class extends Error {
5282
5511
  code = "SECTION_INVARIANT";
5283
5512
  constructor(message) {
@@ -5317,7 +5546,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5317
5546
  let registered = 0;
5318
5547
  let planPending = 0;
5319
5548
  let total = 0;
5320
- const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5549
+ const pend2 = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5321
5550
  const brief = readBrief(briefIn);
5322
5551
  const plan2 = readPlan(planIn);
5323
5552
  const copy = options?.copy ?? "bind";
@@ -5337,7 +5566,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5337
5566
  continue;
5338
5567
  }
5339
5568
  total++;
5340
- pend(page.route, section.groupKey, reason, message);
5569
+ pend2(page.route, section.groupKey, reason, message);
5341
5570
  }
5342
5571
  }
5343
5572
  return {
@@ -5456,7 +5685,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5456
5685
  for (const section of sections2) {
5457
5686
  const prefix = propPrefix(section);
5458
5687
  if (prefix === null) {
5459
- pend(
5688
+ pend2(
5460
5689
  page.route,
5461
5690
  section.groupKey,
5462
5691
  "PROP_MAP_INCONSISTENT",
@@ -5473,13 +5702,13 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5473
5702
  const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5474
5703
  if (registryBlocked) {
5475
5704
  for (const section of usable) {
5476
- pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5705
+ pend2(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5477
5706
  }
5478
5707
  continue;
5479
5708
  }
5480
5709
  if (registryUnread) {
5481
5710
  for (const section of usable) {
5482
- pend(
5711
+ pend2(
5483
5712
  page.route,
5484
5713
  section.groupKey,
5485
5714
  "REGISTRY_UNREADABLE",
@@ -5491,21 +5720,21 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5491
5720
  const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5492
5721
  if (!snapshotUsable(contents.get(snapshotPath))) {
5493
5722
  for (const section of usable) {
5494
- pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5723
+ pend2(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5495
5724
  }
5496
5725
  continue;
5497
5726
  }
5498
5727
  const pageFiles = pageFilesFor(page.route, contents.keys());
5499
5728
  if (pageFiles.length > 1) {
5500
5729
  for (const section of usable) {
5501
- pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5730
+ pend2(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5502
5731
  }
5503
5732
  continue;
5504
5733
  }
5505
5734
  const file = pageFiles[0];
5506
5735
  if (file === void 0) {
5507
5736
  for (const section of usable) {
5508
- pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5737
+ pend2(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5509
5738
  }
5510
5739
  continue;
5511
5740
  }
@@ -5518,7 +5747,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5518
5747
  if (why === void 0) continue;
5519
5748
  done.delete(section.groupKey);
5520
5749
  stuck++;
5521
- pend(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
5750
+ pend2(page.route, section.groupKey, "SECTION_UNMIGRATABLE", why);
5522
5751
  }
5523
5752
  alreadyExtracted += done.size;
5524
5753
  if (done.size + stuck === usable.length) {
@@ -5532,7 +5761,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5532
5761
  treeOf
5533
5762
  );
5534
5763
  for (const [groupKey, reason] of found.misses) {
5535
- pend(page.route, groupKey, reason.code, reason.message);
5764
+ pend2(page.route, groupKey, reason.code, reason.message);
5536
5765
  }
5537
5766
  const live = found.placed;
5538
5767
  if (live.length === 0) continue;
@@ -5540,14 +5769,14 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5540
5769
  const dialect = dialectOf(file, content);
5541
5770
  if (!dialect || !EXTRACTABLE.includes(dialect)) {
5542
5771
  for (const placed of live) {
5543
- pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5772
+ pend2(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5544
5773
  }
5545
5774
  continue;
5546
5775
  }
5547
5776
  const tree = await treeOf(file);
5548
5777
  if (!tree) {
5549
5778
  for (const placed of live) {
5550
- pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5779
+ pend2(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5551
5780
  }
5552
5781
  continue;
5553
5782
  }
@@ -5572,7 +5801,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5572
5801
  const alsoRendered = [...placed.byFile.keys()].filter((held2) => held2 !== file && importedFiles.has(held2));
5573
5802
  if (elements && elements.length > 0) {
5574
5803
  if (alsoRendered.length > 0) {
5575
- pend(
5804
+ pend2(
5576
5805
  page.route,
5577
5806
  placed.section.groupKey,
5578
5807
  "SECTION_ROOT_AMBIGUOUS",
@@ -5584,7 +5813,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5584
5813
  continue;
5585
5814
  }
5586
5815
  if (alsoRendered.length > 1) {
5587
- pend(
5816
+ pend2(
5588
5817
  page.route,
5589
5818
  placed.section.groupKey,
5590
5819
  "SECTION_ROOT_AMBIGUOUS",
@@ -5598,7 +5827,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5598
5827
  importedSections.push({ section: placed.section, path: only, call: held.local });
5599
5828
  continue;
5600
5829
  }
5601
- pend(
5830
+ pend2(
5602
5831
  page.route,
5603
5832
  placed.section.groupKey,
5604
5833
  "SECTION_ROOT_AMBIGUOUS",
@@ -5612,7 +5841,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5612
5841
  const component = resolveComponent(plan2, held.section);
5613
5842
  const already = extractable.get(component.slug);
5614
5843
  if (already !== void 0 && already.path !== held.path) {
5615
- pend(
5844
+ pend2(
5616
5845
  page.route,
5617
5846
  held.section.groupKey,
5618
5847
  "COMPONENT_CONFLICT",
@@ -5623,7 +5852,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5623
5852
  const binding = bindingOf(held.path);
5624
5853
  const member = binding.kind === "namespace" ? namespaceMember(tree, binding.local) : void 0;
5625
5854
  if (binding.kind === "namespace" && member === void 0) {
5626
- pend(
5855
+ pend2(
5627
5856
  page.route,
5628
5857
  held.section.groupKey,
5629
5858
  "SECTION_ROOT_AMBIGUOUS",
@@ -5649,7 +5878,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5649
5878
  for (const placed of placedHere) {
5650
5879
  const root = lowestCommonAncestor(placed.elements);
5651
5880
  if (!root) {
5652
- pend(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
5881
+ pend2(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
5653
5882
  continue;
5654
5883
  }
5655
5884
  const component = resolveComponent(plan2, placed.section);
@@ -5671,7 +5900,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5671
5900
  }
5672
5901
  }
5673
5902
  for (const resolved of nested) {
5674
- pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5903
+ pend2(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5675
5904
  }
5676
5905
  const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5677
5906
  const placeable = [];
@@ -5679,7 +5908,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5679
5908
  const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
5680
5909
  const existing = contents.get(path);
5681
5910
  if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
5682
- pend(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
5911
+ pend2(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
5683
5912
  continue;
5684
5913
  }
5685
5914
  if (extractable.has(resolved.slug)) {
@@ -5688,7 +5917,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5688
5917
  }
5689
5918
  const built = componentFile(dialect, path, file, content, parsed, resolved, resolved.pathPrefix);
5690
5919
  if (built.unconvertible.length > 0) {
5691
- pend(
5920
+ pend2(
5692
5921
  page.route,
5693
5922
  resolved.section.groupKey,
5694
5923
  "PROP_MAP_INCONSISTENT",
@@ -5697,7 +5926,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5697
5926
  continue;
5698
5927
  }
5699
5928
  if (built.duplicates.length > 0) {
5700
- pend(
5929
+ pend2(
5701
5930
  page.route,
5702
5931
  resolved.section.groupKey,
5703
5932
  "DUPLICATE_FIELD_BINDING",
@@ -5706,7 +5935,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5706
5935
  continue;
5707
5936
  }
5708
5937
  if (built.free.length > 0) {
5709
- pend(
5938
+ pend2(
5710
5939
  page.route,
5711
5940
  resolved.section.groupKey,
5712
5941
  "SECTION_FREE_IDENTIFIERS",
@@ -5732,7 +5961,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5732
5961
  const layout = runOf(known, parsed.roots, content);
5733
5962
  if (layout === null) {
5734
5963
  for (const resolved of placeable) {
5735
- pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5964
+ pend2(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5736
5965
  }
5737
5966
  continue;
5738
5967
  }
@@ -5755,7 +5984,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
5755
5984
  const inline = placeable.filter((resolved) => !instanceOwned(resolved.section));
5756
5985
  for (const resolved of placeable) {
5757
5986
  if (inline.includes(resolved)) continue;
5758
- pend(
5987
+ pend2(
5759
5988
  page.route,
5760
5989
  resolved.section.groupKey,
5761
5990
  "INLINE_NEEDS_INSTANCE",
@@ -6203,7 +6432,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
6203
6432
  ` };
6204
6433
  }
6205
6434
  function sectionMarkup(content, root, fieldName) {
6206
- const slice = new MagicString3(content.slice(root.start, root.end));
6435
+ const slice = new MagicString4(content.slice(root.start, root.end));
6207
6436
  const at = (offset) => offset - root.start;
6208
6437
  const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
6209
6438
  if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
@@ -6402,7 +6631,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
6402
6631
  );
6403
6632
  continue;
6404
6633
  }
6405
- const source = new MagicString3(content);
6634
+ const source = new MagicString4(content);
6406
6635
  const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
6407
6636
  if (duplicates.length > 0) {
6408
6637
  refused.set(refusalKey(dialect, slug), `${entry.path} has ${duplicates.join(", ")} carrying more than one data-bcms-field, so there is no single address to rewrite.`);
@@ -6679,7 +6908,7 @@ export default function Sections({ page }: { page: string }) {
6679
6908
  function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
6680
6909
  const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
6681
6910
  const local = names.get("Sections");
6682
- const source = new MagicString3(content);
6911
+ const source = new MagicString4(content);
6683
6912
  const first = placeable[0].root;
6684
6913
  const last = placeable[placeable.length - 1].root;
6685
6914
  source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
@@ -6699,7 +6928,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
6699
6928
  { key: "pageSnapshot", name: "pageSnapshot" }
6700
6929
  ]);
6701
6930
  const snapshot = names.get("pageSnapshot");
6702
- const source = new MagicString3(content);
6931
+ const source = new MagicString4(content);
6703
6932
  for (const resolved of placeable) {
6704
6933
  const component = written.get(resolved.slug);
6705
6934
  const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
@@ -6780,6 +7009,10 @@ function readComponentizePlan(value) {
6780
7009
  }
6781
7010
 
6782
7011
  // src/index.ts
7012
+ var UNPARSEABLE_DIALECT = {
7013
+ roots: [],
7014
+ error: { code: "DIALECT_UNSUPPORTED", message: "no parser for this extension" }
7015
+ };
6783
7016
  var ConvertError = class extends Error {
6784
7017
  constructor(code, message) {
6785
7018
  super(message);
@@ -6861,7 +7094,7 @@ async function convertSources(briefIn, sources, options) {
6861
7094
  }
6862
7095
  const { located, unlocated } = locate(brief, sources, { stripTags: true });
6863
7096
  const pending = [];
6864
- const pend = (target, reason, file, message) => {
7097
+ const pend2 = (target, reason, file, message) => {
6865
7098
  pending.push({
6866
7099
  route: target.route,
6867
7100
  scope: target.scope,
@@ -6962,7 +7195,7 @@ async function convertSources(briefIn, sources, options) {
6962
7195
  declaredRow(target, false, byHand);
6963
7196
  continue;
6964
7197
  }
6965
- pend(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
7198
+ pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
6966
7199
  }
6967
7200
  const byFile = /* @__PURE__ */ new Map();
6968
7201
  for (const [key, hit] of locatedByKey) {
@@ -7019,12 +7252,12 @@ async function convertSources(briefIn, sources, options) {
7019
7252
  const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
7020
7253
  if (worst) {
7021
7254
  const file = hit.files[outcomes.indexOf(worst)];
7022
- pend(target, worst, file);
7255
+ pend2(target, worst, file);
7023
7256
  } else if (outcomes.every((o) => o === "declared")) {
7024
7257
  const witness = await witnessed(target);
7025
7258
  const byHand = witness === null ? declaredByHand(target, hit.files) : null;
7026
7259
  if (witness === null && byHand === null) {
7027
- pend(
7260
+ pend2(
7028
7261
  target,
7029
7262
  "AMBIGUOUS_LITERAL",
7030
7263
  hit.files[0],
@@ -7037,7 +7270,7 @@ async function convertSources(briefIn, sources, options) {
7037
7270
  } else rewritten++;
7038
7271
  }
7039
7272
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
7040
- const source = new MagicString4(contents.get(file));
7273
+ const source = new MagicString5(contents.get(file));
7041
7274
  const seen = /* @__PURE__ */ new Set();
7042
7275
  for (const splice of splices) {
7043
7276
  const id = `${splice.start}:${splice.end}:${splice.text}`;
@@ -7058,6 +7291,28 @@ async function convertSources(briefIn, sources, options) {
7058
7291
  }
7059
7292
  const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
7060
7293
  emitStubs2(dialects, snapshotSlugs, contents, outFiles);
7294
+ const latest = new Map(contents);
7295
+ for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
7296
+ const canvasSources = [];
7297
+ for (const source of sources) {
7298
+ const content = latest.get(source.path) ?? source.content;
7299
+ if (!/\.[jt]sx$/i.test(source.path)) {
7300
+ canvasSources.push({ path: source.path, content });
7301
+ continue;
7302
+ }
7303
+ const parsed = content === source.content ? await parseOf(source.path) : await parseFile4("jsx", content);
7304
+ canvasSources.push({ path: source.path, content, ...parsed && !parsed.error ? { ast: parsed.ast } : {} });
7305
+ }
7306
+ const canvas = canvasBridge(
7307
+ canvasSources,
7308
+ new Map(brief.pages.map((page) => [page.route, page.slug]))
7309
+ );
7310
+ for (const [path, content] of canvas.edits) {
7311
+ const existing = outFiles.find((f) => f.path === path);
7312
+ if (existing) existing.content = content;
7313
+ else outFiles.push({ path, operation: "modify", content });
7314
+ }
7315
+ pending.push(...canvas.pending);
7061
7316
  const receipt = assertReceipt({
7062
7317
  briefDigest: digest,
7063
7318
  paths: {
@@ -7074,6 +7329,7 @@ async function convertSources(briefIn, sources, options) {
7074
7329
  helpers,
7075
7330
  helperUpgraded,
7076
7331
  dynamicBindings,
7332
+ canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
7077
7333
  notes: unreadableConfigs(sources).map(
7078
7334
  (file) => ({
7079
7335
  code: "TSCONFIG_UNREADABLE",
@@ -7106,6 +7362,30 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7106
7362
  };
7107
7363
  if (pendingTargets.length === 0) return empty;
7108
7364
  if (!dialect) {
7365
+ if (isTolerantDialect(file)) {
7366
+ const tolerantLiterals = [...new Set(pendingTargets.map((t) => t.literal))];
7367
+ const tolerantSites = findSitesTolerant(content, tolerantLiterals);
7368
+ if (tolerantSites.length > 0) {
7369
+ return plan(
7370
+ file,
7371
+ content,
7372
+ // Never read: `plan` forces `writing = "html"` at tier 2, and every other use of this
7373
+ // argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
7374
+ // the parameter honest rather than widening its type for one caller.
7375
+ "html",
7376
+ pendingTargets,
7377
+ tolerantSites,
7378
+ void 0,
7379
+ // no parse, no import point: tier 2 declares, it does not read
7380
+ outcomes,
7381
+ 2,
7382
+ UNPARSEABLE_DIALECT,
7383
+ drill,
7384
+ []
7385
+ // no parse, so nothing in this file is already declared
7386
+ );
7387
+ }
7388
+ }
7109
7389
  for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
7110
7390
  return { ...empty, dialect: null, outcomes };
7111
7391
  }
@@ -7164,7 +7444,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7164
7444
  }
7165
7445
  async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
7166
7446
  const writing = tier === 2 ? "html" : dialect;
7167
- const missing = tier === 2 ? "PARSE_ERROR" : "NOT_IN_SOURCE";
7447
+ const missing = tier !== 2 ? "NOT_IN_SOURCE" : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
7168
7448
  const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
7169
7449
  const slugs = [...new Set(targets.map((t) => t.slug))].sort();
7170
7450
  const preset = /* @__PURE__ */ new Map();