@bettercms-ai/convert 0.1.0 → 0.2.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 MagicString3 from "magic-string";
2
+ import MagicString4 from "magic-string";
3
3
 
4
4
  // src/brief.ts
5
5
  import { createHash } from "crypto";
@@ -128,7 +128,7 @@ function propsAttribute(props) {
128
128
 
129
129
  // src/helper.ts
130
130
  import { createHash as createHash2 } from "crypto";
131
- var HELPER_VERSION = 2;
131
+ var HELPER_VERSION = 3;
132
132
  var HEADER = "// @bettercms-ai/convert helper v";
133
133
  var HELPER_PATH = Object.fromEntries(
134
134
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -207,12 +207,37 @@ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: s
207
207
  const value = readPath(root, path);
208
208
  return Array.isArray(value) ? (value as T[]) : fallback;
209
209
  }
210
+
211
+ /**
212
+ * ONE SECTION's copy, for a component extracted by \`--componentize\`.
213
+ *
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.
218
+ */
219
+ 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
+ const group = bind ? readPath(page, bind) : undefined;
224
+ return isRecord(group) ? group : {};
225
+ }
210
226
  `;
211
227
  }
212
228
  var HISTORICAL = {
213
- "src/bcms-content.ts": { 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407" },
214
- "src/lib/bcms-content.ts": { 1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2" },
215
- "composables/bcms-content.ts": { 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407" }
229
+ "src/bcms-content.ts": {
230
+ 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
231
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
232
+ },
233
+ "src/lib/bcms-content.ts": {
234
+ 1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
235
+ 2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002"
236
+ },
237
+ "composables/bcms-content.ts": {
238
+ 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
239
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0"
240
+ }
216
241
  };
217
242
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
218
243
  function isKnownHelper(content, dialect) {
@@ -3063,7 +3088,9 @@ function attrsOf2(open, content) {
3063
3088
  value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
3064
3089
  valueRange: null,
3065
3090
  expression: true,
3066
- range: { start: attr.start ?? 0, end: attr.end ?? 0 }
3091
+ range: { start: attr.start ?? 0, end: attr.end ?? 0 },
3092
+ // `{...props}` — the braces ARE the attribute, so the expression is its whole span.
3093
+ expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
3067
3094
  }
3068
3095
  ];
3069
3096
  }
@@ -3078,7 +3105,8 @@ function attrsOf2(open, content) {
3078
3105
  // The quotes are one character each on a StringLiteral; babel's range covers them.
3079
3106
  valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
3080
3107
  expression: !literal,
3081
- range
3108
+ range,
3109
+ expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
3082
3110
  }
3083
3111
  ];
3084
3112
  });
@@ -3143,6 +3171,7 @@ function convert(node, content) {
3143
3171
  type: "element",
3144
3172
  tag,
3145
3173
  start,
3174
+ end: node.end ?? start,
3146
3175
  // Babel's own offsets: the last attribute's `end`, or the end of the element's NAME. Both
3147
3176
  // come out of the parse, so an escaped quote inside an attribute cannot move them.
3148
3177
  attrInsertAt: node.type === "JSXFragment" ? start + 1 : attrInsertAfter(attrs, open?.start ?? start, tag),
@@ -3578,8 +3607,9 @@ function importsIn(parsed, _dialect) {
3578
3607
  if (entry.importKind === "type") continue;
3579
3608
  const local = entry.local?.name;
3580
3609
  if (typeof local !== "string") continue;
3581
- const imported = entry.type === "ImportDefaultSpecifier" ? null : entry.imported?.name ?? local;
3582
- out.set(local, { specifier, imported });
3610
+ const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
3611
+ const imported = kind === "named" ? entry.imported?.name ?? local : null;
3612
+ out.set(local, { specifier, imported, kind });
3583
3613
  }
3584
3614
  });
3585
3615
  return out;
@@ -3587,6 +3617,33 @@ function importsIn(parsed, _dialect) {
3587
3617
 
3588
3618
  // src/routes.ts
3589
3619
  var isDynamicRoute = (file) => file.split("/").some((segment) => /^\[.+\]/.test(segment) || /\[.+\]\.[a-z]+$/i.test(segment));
3620
+ var PAGE_EXTENSIONS = /* @__PURE__ */ new Set(["astro", "tsx", "jsx", "ts", "js", "svelte", "vue"]);
3621
+ var isOrganisational = (segment) => segment.startsWith("(") && segment.endsWith(")") || segment.startsWith("@");
3622
+ function routeOfFile(file) {
3623
+ const extension = file.slice(file.lastIndexOf(".") + 1);
3624
+ if (!PAGE_EXTENSIONS.has(extension)) return null;
3625
+ let parts = file.split("/");
3626
+ if (parts[0] === "src") parts = parts.slice(1);
3627
+ if (parts[0] === "app" && parts[1] === "pages") parts = parts.slice(1);
3628
+ const router = parts[0];
3629
+ if (router !== "pages" && router !== "app" && router !== "routes") return null;
3630
+ let segments = parts.slice(1);
3631
+ const last = segments[segments.length - 1];
3632
+ if (last === void 0) return null;
3633
+ const base = last.slice(0, last.lastIndexOf("."));
3634
+ if (router === "pages") {
3635
+ segments = [...segments.slice(0, -1), ...base === "index" ? [] : [base]];
3636
+ } else {
3637
+ if (base !== "page" && base !== "+page") return null;
3638
+ segments = segments.slice(0, -1);
3639
+ }
3640
+ const url = segments.filter((segment) => !isOrganisational(segment));
3641
+ return `/${url.join("/")}`.replace(/\/{2,}/g, "/");
3642
+ }
3643
+ function pageFilesFor(route, files) {
3644
+ const wanted = route === "" ? "/" : route.replace(/\/+$/, "") || "/";
3645
+ return [...files].filter((file) => routeOfFile(file) === wanted).sort();
3646
+ }
3590
3647
  var FUNCTIONS2 = /* @__PURE__ */ new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
3591
3648
  function defaultComponent(ast) {
3592
3649
  let found = null;
@@ -3852,7 +3909,7 @@ function namesFile(fromFile, specifier, target) {
3852
3909
  function snapshotLocal(parsed, dialect, slug, file) {
3853
3910
  const target = `${CONTENT_DIR}/${slug}.json`;
3854
3911
  for (const [local, from] of importsIn(parsed, dialect)) {
3855
- if (from.imported !== null) continue;
3912
+ if (from.kind !== "default") continue;
3856
3913
  if (namesFile(file, from.specifier, target)) return local;
3857
3914
  }
3858
3915
  return null;
@@ -4070,6 +4127,8 @@ var attrsOf3 = (el, content) => {
4070
4127
  value: attr.value,
4071
4128
  valueRange,
4072
4129
  expression: false,
4130
+ // parse5 reads HTML, which has no expressions at all.
4131
+ expressionRange: null,
4073
4132
  range: range ?? { start: 0, end: 0 }
4074
4133
  };
4075
4134
  });
@@ -4100,6 +4159,7 @@ function convert2(node, content) {
4100
4159
  type: "element",
4101
4160
  tag: el.tagName,
4102
4161
  start,
4162
+ end: at.endOffset,
4103
4163
  attrInsertAt: insertAt,
4104
4164
  inner,
4105
4165
  attrs,
@@ -4120,6 +4180,18 @@ function parse2(content) {
4120
4180
  // src/sites/astro.ts
4121
4181
  import { parse as parse3 } from "@astrojs/compiler";
4122
4182
  import { parse as parseScript } from "@babel/parser";
4183
+ function byteToChar(content) {
4184
+ if (!/[^\u0000-\u007f]/.test(content)) return null;
4185
+ const bytes = [];
4186
+ for (let at = 0; at < content.length; ) {
4187
+ const code = content.codePointAt(at);
4188
+ const width = code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
4189
+ for (let n = 0; n < width; n++) bytes.push(at);
4190
+ at += code < 65536 ? 1 : 2;
4191
+ }
4192
+ bytes.push(content.length);
4193
+ return (offset) => bytes[offset] ?? content.length;
4194
+ }
4123
4195
  var RAW_TAGS2 = /* @__PURE__ */ new Set(["script", "style"]);
4124
4196
  var backOverSpace = (content, at) => {
4125
4197
  let end = at;
@@ -4166,6 +4238,8 @@ function attrsOf4(node, content) {
4166
4238
  const raw = attr.raw ?? "";
4167
4239
  const end = attrEnd(content, attr, start, attributes[at + 1]?.position?.start?.offset);
4168
4240
  const range = { start, end };
4241
+ const open = attr.kind === "spread" || attr.kind === "shorthand" ? content.lastIndexOf("{", start) : content.indexOf("{", start + attr.name.length);
4242
+ const close = content.lastIndexOf("}", end);
4169
4243
  const quoted = attr.kind === "quoted";
4170
4244
  const rawStart = quoted ? content.indexOf(raw, start) : -1;
4171
4245
  const unquoted = raw.length === attr.value.length;
@@ -4175,7 +4249,8 @@ function attrsOf4(node, content) {
4175
4249
  value: attr.value,
4176
4250
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
4177
4251
  expression: !quoted,
4178
- range
4252
+ range,
4253
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
4179
4254
  }
4180
4255
  ];
4181
4256
  });
@@ -4214,11 +4289,13 @@ function convert3(node, content) {
4214
4289
  const reportedEnd = node.position?.end?.offset;
4215
4290
  const closes = reportedEnd !== void 0 && content.slice(reportedEnd - closeTag.length, reportedEnd) === closeTag;
4216
4291
  const inner = closes ? { start: content.indexOf(">", insertAt) + 1, end: reportedEnd - closeTag.length } : null;
4292
+ const end = closes ? reportedEnd : content.indexOf(">", insertAt) + 1;
4217
4293
  return [
4218
4294
  {
4219
4295
  type: "element",
4220
4296
  tag,
4221
4297
  start,
4298
+ end,
4222
4299
  attrInsertAt: insertAt,
4223
4300
  inner,
4224
4301
  attrs,
@@ -4227,11 +4304,26 @@ function convert3(node, content) {
4227
4304
  }
4228
4305
  ];
4229
4306
  }
4307
+ function remap(nodes, at) {
4308
+ for (const node of nodes) {
4309
+ const start = node.position?.start;
4310
+ if (start?.offset !== void 0) start.offset = at(start.offset);
4311
+ const end = node.position?.end;
4312
+ if (end?.offset !== void 0) end.offset = at(end.offset);
4313
+ for (const attr of node.attributes ?? []) {
4314
+ const attrStart = attr.position?.start;
4315
+ if (attrStart?.offset !== void 0) attrStart.offset = at(attrStart.offset);
4316
+ }
4317
+ remap(node.children ?? [], at);
4318
+ }
4319
+ }
4230
4320
  async function parseFile(content) {
4231
4321
  let top;
4232
4322
  try {
4233
4323
  const result = await parse3(content, { position: true });
4234
4324
  top = result.ast.children ?? [];
4325
+ const at = byteToChar(content);
4326
+ if (at) remap(top, at);
4235
4327
  } catch (error) {
4236
4328
  return { roots: [], error: { code: "PARSE_ERROR", message: error.message } };
4237
4329
  }
@@ -4273,7 +4365,7 @@ function attrsOf5(node) {
4273
4365
  const name = typeof attr.name === "string" ? attr.name : attr.type;
4274
4366
  const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
4275
4367
  if (attr.type !== "Attribute") {
4276
- return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, range }];
4368
+ return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
4277
4369
  }
4278
4370
  const literal = literalValue(attr);
4279
4371
  return [
@@ -4282,6 +4374,8 @@ function attrsOf5(node) {
4282
4374
  value: literal?.value ?? "",
4283
4375
  valueRange: literal && literal.value !== "" ? literal.range : null,
4284
4376
  expression: literal === null,
4377
+ // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
4378
+ expressionRange: null,
4285
4379
  range
4286
4380
  }
4287
4381
  ];
@@ -4327,6 +4421,7 @@ function convert4(node, content) {
4327
4421
  type: "element",
4328
4422
  tag,
4329
4423
  start,
4424
+ end,
4330
4425
  attrInsertAt: insertAt,
4331
4426
  inner: closes ? { start: content.indexOf(">", insertAt) + 1, end: end - close.length } : null,
4332
4427
  attrs,
@@ -4391,6 +4486,7 @@ function attrsOf6(node) {
4391
4486
  value: "",
4392
4487
  valueRange: null,
4393
4488
  expression: true,
4489
+ expressionRange: null,
4394
4490
  range: spanOf(prop.loc)
4395
4491
  }
4396
4492
  ];
@@ -4402,6 +4498,7 @@ function attrsOf6(node) {
4402
4498
  value: "",
4403
4499
  valueRange: null,
4404
4500
  expression: true,
4501
+ expressionRange: null,
4405
4502
  range: spanOf(prop.loc)
4406
4503
  }
4407
4504
  ];
@@ -4415,6 +4512,7 @@ function attrsOf6(node) {
4415
4512
  value,
4416
4513
  valueRange: quoted && value !== "" ? { start: quoted.start + 1, end: quoted.end - 1 } : null,
4417
4514
  expression: false,
4515
+ expressionRange: null,
4418
4516
  range
4419
4517
  }
4420
4518
  ];
@@ -4444,6 +4542,7 @@ function convert5(node, content) {
4444
4542
  type: "element",
4445
4543
  tag,
4446
4544
  start,
4545
+ end,
4447
4546
  attrInsertAt: insertAt,
4448
4547
  inner: closes ? { start: content.indexOf(">", insertAt) + 1, end: end - close.length } : null,
4449
4548
  attrs,
@@ -4543,6 +4642,123 @@ function findSitesTolerant(content, literals) {
4543
4642
  return sites;
4544
4643
  }
4545
4644
 
4645
+ // src/validate.ts
4646
+ var SAFE_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
4647
+ var ROUTE = /^\/(?:[A-Za-z0-9_\-.~%$@+[\]()!'*,:=&]+(?:\/[A-Za-z0-9_\-.~%$@+[\]()!'*,:=&]+)*\/?)?$/;
4648
+ var ValidationError = class extends Error {
4649
+ constructor(code, message) {
4650
+ super(message);
4651
+ this.code = code;
4652
+ this.name = "ValidationError";
4653
+ }
4654
+ code;
4655
+ };
4656
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4657
+ var Check = class {
4658
+ constructor(code) {
4659
+ this.code = code;
4660
+ }
4661
+ code;
4662
+ fail(where, why) {
4663
+ throw new ValidationError(this.code, `${where} ${why}.`);
4664
+ }
4665
+ record(value, where) {
4666
+ if (!isRecord(value)) this.fail(where, "is not an object");
4667
+ return value;
4668
+ }
4669
+ array(value, where) {
4670
+ if (!Array.isArray(value)) this.fail(where, "is not an array");
4671
+ return value;
4672
+ }
4673
+ string(value, where) {
4674
+ if (typeof value !== "string") this.fail(where, "is not a string");
4675
+ return value;
4676
+ }
4677
+ optionalString(value, where) {
4678
+ if (value === void 0 || value === null) return void 0;
4679
+ return this.string(value, where);
4680
+ }
4681
+ /** A value that becomes a FILE NAME or an object key in generated source. @see SAFE_SEGMENT */
4682
+ segment(value, where) {
4683
+ const held = this.string(value, where);
4684
+ if (!SAFE_SEGMENT.test(held)) {
4685
+ this.fail(where, `is not a safe path segment (${JSON.stringify(held)}); expected ${SAFE_SEGMENT.source}`);
4686
+ }
4687
+ return held;
4688
+ }
4689
+ route(value, where) {
4690
+ const held = this.string(value, where);
4691
+ if (!ROUTE.test(held) || held.includes("..")) this.fail(where, `is not an absolute URL path (${JSON.stringify(held)})`);
4692
+ return held;
4693
+ }
4694
+ };
4695
+ function readBrief(value) {
4696
+ const check = new Check("BRIEF_INVALID");
4697
+ const brief = check.record(value, "the brief");
4698
+ const pages = check.array(brief.pages, `the brief's "pages"`);
4699
+ for (const [at, entry] of pages.entries()) {
4700
+ const where = `brief.pages[${at}]`;
4701
+ const page = check.record(entry, where);
4702
+ check.segment(page.slug, `${where}.slug`);
4703
+ check.route(page.route, `${where}.route`);
4704
+ for (const [n, held] of check.array(page.paths, `${where}.paths`).entries()) {
4705
+ const path = check.record(held, `${where}.paths[${n}]`);
4706
+ check.string(path.path, `${where}.paths[${n}].path`);
4707
+ check.string(path.kind, `${where}.paths[${n}].kind`);
4708
+ const scope = check.optionalString(path.scope, `${where}.paths[${n}].scope`);
4709
+ if (scope !== void 0 && scope !== "page" && scope !== "layout") {
4710
+ check.fail(`${where}.paths[${n}].scope`, 'is neither "page" nor "layout"');
4711
+ }
4712
+ if (path.original !== null && path.original !== void 0 && typeof path.original !== "string") {
4713
+ check.fail(`${where}.paths[${n}].original`, "is not a string or null");
4714
+ }
4715
+ }
4716
+ }
4717
+ return brief;
4718
+ }
4719
+ function readPlan(value) {
4720
+ const check = new Check("PLAN_INVALID");
4721
+ const plan2 = check.record(value, "the plan");
4722
+ if (!Array.isArray(plan2.pages)) check.fail(`the plan's "pages"`, "is missing or not an array");
4723
+ for (const [at, entry] of plan2.pages.entries()) {
4724
+ const where = `plan.pages[${at}]`;
4725
+ const page = check.record(entry, where);
4726
+ check.route(page.route, `${where}.route`);
4727
+ if (page.sections === void 0) continue;
4728
+ for (const [n, held] of check.array(page.sections, `${where}.sections`).entries()) {
4729
+ const at2 = `${where}.sections[${n}]`;
4730
+ const section = check.record(held, at2);
4731
+ check.segment(section.groupKey, `${at2}.groupKey`);
4732
+ check.optionalString(section.sectionType, `${at2}.sectionType`);
4733
+ if (section.fields !== void 0) {
4734
+ for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
4735
+ const shape = check.record(field, `${at2}.fields[${f}]`);
4736
+ check.optionalString(shape.key, `${at2}.fields[${f}].key`);
4737
+ check.optionalString(shape.path, `${at2}.fields[${f}].path`);
4738
+ }
4739
+ }
4740
+ if (section.reuse !== void 0) {
4741
+ const reuse = check.record(section.reuse, `${at2}.reuse`);
4742
+ check.optionalString(reuse.componentId, `${at2}.reuse.componentId`);
4743
+ if (reuse.proposedSlug !== void 0) check.segment(reuse.proposedSlug, `${at2}.reuse.proposedSlug`);
4744
+ }
4745
+ if (section.pending !== void 0) check.record(section.pending, `${at2}.pending`);
4746
+ }
4747
+ }
4748
+ if (plan2.components !== void 0) {
4749
+ for (const [at, entry] of check.array(plan2.components, `the plan's "components"`).entries()) {
4750
+ const where = `plan.components[${at}]`;
4751
+ const component = check.record(entry, where);
4752
+ check.segment(component.proposedSlug, `${where}.proposedSlug`);
4753
+ check.optionalString(component.name, `${where}.name`);
4754
+ check.optionalString(component.sectionType, `${where}.sectionType`);
4755
+ check.optionalString(component.componentId, `${where}.componentId`);
4756
+ }
4757
+ }
4758
+ check.optionalString(plan2.digest, `the plan's "digest"`);
4759
+ return plan2;
4760
+ }
4761
+
4546
4762
  // src/scan.ts
4547
4763
  var SKIP_DIRS = [
4548
4764
  "node_modules/",
@@ -4597,6 +4813,1104 @@ async function readDeclarations(file, content) {
4597
4813
  return parsed.error ? scannedDeclarations(content) : declarationsIn(parsed.roots, content).map((d) => ({ scope: d.scope, path: d.path }));
4598
4814
  }
4599
4815
 
4816
+ // src/carry.ts
4817
+ var GLOBALS = /* @__PURE__ */ new Set([
4818
+ "Astro",
4819
+ "Fragment",
4820
+ "React",
4821
+ "JSX",
4822
+ "globalThis",
4823
+ "undefined",
4824
+ "null",
4825
+ "true",
4826
+ "false",
4827
+ "NaN",
4828
+ "Infinity",
4829
+ "Math",
4830
+ "JSON",
4831
+ "Object",
4832
+ "Array",
4833
+ "String",
4834
+ "Number",
4835
+ "Boolean",
4836
+ "Date",
4837
+ "RegExp",
4838
+ "Map",
4839
+ "Set",
4840
+ "WeakMap",
4841
+ "WeakSet",
4842
+ "Promise",
4843
+ "Symbol",
4844
+ "BigInt",
4845
+ "Error",
4846
+ "Intl",
4847
+ "URL",
4848
+ "URLSearchParams",
4849
+ "console",
4850
+ "process",
4851
+ "window",
4852
+ "document",
4853
+ "navigator",
4854
+ "fetch",
4855
+ "isNaN",
4856
+ "isFinite",
4857
+ "parseInt",
4858
+ "parseFloat",
4859
+ "encodeURIComponent",
4860
+ "decodeURIComponent",
4861
+ "structuredClone",
4862
+ "import",
4863
+ "arguments",
4864
+ "this",
4865
+ "void",
4866
+ "typeof"
4867
+ ]);
4868
+ function carryFor(options) {
4869
+ const { dialect, fromFile, toFile, content, parsed, root } = options;
4870
+ const wanted = referencedNames(root, content);
4871
+ if (wanted === null) {
4872
+ return { imports: [], declarations: [], free: ["<an expression this version cannot read>"] };
4873
+ }
4874
+ const provided = new Set(options.provided);
4875
+ const imports = importsIn(parsed, dialect);
4876
+ const declarations = moduleScope(parsed, content);
4877
+ const carriedImports = /* @__PURE__ */ new Map();
4878
+ const carriedDeclarations = /* @__PURE__ */ new Map();
4879
+ const free = /* @__PURE__ */ new Set();
4880
+ const seen = /* @__PURE__ */ new Set();
4881
+ const queue = [...wanted];
4882
+ while (queue.length > 0) {
4883
+ const name = queue.shift();
4884
+ if (seen.has(name) || provided.has(name) || GLOBALS.has(name)) continue;
4885
+ seen.add(name);
4886
+ const imported = imports.get(name);
4887
+ if (imported) {
4888
+ if (skipImport(imported.specifier, fromFile, options.skipSpecifier)) continue;
4889
+ const specifier = imported.specifier.startsWith(".") ? relativeImport(toFile, joinFrom(fromFile, imported.specifier)) : imported.specifier;
4890
+ carriedImports.set(
4891
+ name,
4892
+ imported.kind === "namespace" ? `import * as ${name} from "${specifier}";` : imported.kind === "default" ? `import ${name} from "${specifier}";` : `import { ${imported.imported === name ? name : `${imported.imported} as ${name}`} } from "${specifier}";`
4893
+ );
4894
+ continue;
4895
+ }
4896
+ const declared = declarations.get(name);
4897
+ if (declared) {
4898
+ carriedDeclarations.set(declared.at, declared.text);
4899
+ queue.push(...declared.uses);
4900
+ continue;
4901
+ }
4902
+ free.add(name);
4903
+ }
4904
+ return {
4905
+ imports: [...new Set(carriedImports.values())].sort(),
4906
+ declarations: [...carriedDeclarations].sort(([a], [b]) => a - b).map(([, text]) => text),
4907
+ free: [...free].sort()
4908
+ };
4909
+ }
4910
+ function skipImport(specifier, fromFile, skip) {
4911
+ return skip(specifier.startsWith(".") ? joinFrom(fromFile, specifier) : "", specifier);
4912
+ }
4913
+ function joinFrom(fromFile, specifier) {
4914
+ const parts = fromFile.split("/").slice(0, -1);
4915
+ for (const segment of specifier.split("/")) {
4916
+ if (segment === "." || segment === "") continue;
4917
+ if (segment === "..") parts.pop();
4918
+ else parts.push(segment);
4919
+ }
4920
+ return parts.join("/");
4921
+ }
4922
+ function referencedNames(root, content) {
4923
+ const used = /* @__PURE__ */ new Set();
4924
+ const add = (code, scope) => {
4925
+ const scanned = scanExpression(code);
4926
+ if (!scanned) return null;
4927
+ for (const name of scanned.free) {
4928
+ if (!scope.some((held) => held.has(name))) used.add(name);
4929
+ }
4930
+ return scanned.bound;
4931
+ };
4932
+ const visit = (node, bounds, scope) => {
4933
+ if (node.type === "expr") {
4934
+ const start = Math.max(node.range.start, bounds.start);
4935
+ const end = Math.min(node.range.end, bounds.end);
4936
+ const holes = node.children.flatMap(
4937
+ (child) => child.type === "element" ? [{ start: child.start, end: child.end }] : []
4938
+ );
4939
+ let text = "";
4940
+ let at = start;
4941
+ for (const hole of [...holes].sort((a, b) => a.start - b.start)) {
4942
+ if (hole.start < at || hole.end > end) continue;
4943
+ text += `${content.slice(at, hole.start)}null`;
4944
+ at = hole.end;
4945
+ }
4946
+ text += content.slice(at, end);
4947
+ const open = text.indexOf("{");
4948
+ const close = text.lastIndexOf("}");
4949
+ const bound = add(open >= 0 && close > open ? text.slice(open + 1, close) : text, scope);
4950
+ if (!bound) return false;
4951
+ const inner2 = [...scope, bound];
4952
+ for (const child of node.children) if (!visit(child, bounds, inner2)) return false;
4953
+ return true;
4954
+ }
4955
+ if (node.type !== "element") return true;
4956
+ if (!isIntrinsic(node.tag) && node.tag !== "") {
4957
+ const tag = node.tag.split(".")[0];
4958
+ if (!scope.some((held) => held.has(tag))) used.add(tag);
4959
+ }
4960
+ for (const attr of node.attrs) {
4961
+ if (!attr.expression) continue;
4962
+ const span = attr.expressionRange;
4963
+ if (!span) continue;
4964
+ const raw = content.slice(span.start, span.end);
4965
+ if (add(stripBraces(raw), scope) === null) return false;
4966
+ }
4967
+ const inner = node.inner ?? bounds;
4968
+ const kids = node.children;
4969
+ for (const [at, child] of kids.entries()) {
4970
+ const next = kids[at + 1];
4971
+ const stop = next ? next.type === "element" ? next.start : next.range.start : inner.end;
4972
+ if (!visit(child, { start: inner.start, end: Math.min(inner.end, stop) }, scope)) return false;
4973
+ }
4974
+ return true;
4975
+ };
4976
+ return visit(root, { start: root.start, end: root.end }, []) ? used : null;
4977
+ }
4978
+ function stripBraces(text) {
4979
+ const trimmed = text.trim();
4980
+ const inner = trimmed.startsWith("{") && trimmed.endsWith("}") ? trimmed.slice(1, -1) : trimmed;
4981
+ return inner.trim().startsWith("...") ? inner.trim().slice(3) : inner;
4982
+ }
4983
+ function scanExpression(code) {
4984
+ if (code.trim() === "") return { free: /* @__PURE__ */ new Set(), bound: /* @__PURE__ */ new Set() };
4985
+ let ast;
4986
+ try {
4987
+ ast = parseProgram(`(${code}
4988
+ )`);
4989
+ } catch {
4990
+ try {
4991
+ ast = parseProgram(code);
4992
+ } catch {
4993
+ return null;
4994
+ }
4995
+ }
4996
+ const candidates = /* @__PURE__ */ new Set();
4997
+ const skip = /* @__PURE__ */ new Set();
4998
+ const bound = /* @__PURE__ */ new Set();
4999
+ walkAst(ast, (node) => {
5000
+ switch (node.type) {
5001
+ case "Identifier":
5002
+ case "JSXIdentifier":
5003
+ if (typeof node.name === "string") candidates.add(node.name);
5004
+ return;
5005
+ case "MemberExpression":
5006
+ case "OptionalMemberExpression":
5007
+ case "JSXMemberExpression":
5008
+ if (!node.computed) nameOf2(node.property, skip);
5009
+ return;
5010
+ case "ObjectProperty":
5011
+ case "ObjectMethod":
5012
+ case "ClassProperty":
5013
+ case "ClassMethod":
5014
+ if (!node.computed) nameOf2(node.key, skip);
5015
+ return;
5016
+ case "JSXAttribute":
5017
+ nameOf2(node.name, skip);
5018
+ return;
5019
+ case "JSXOpeningElement":
5020
+ case "JSXClosingElement": {
5021
+ const name = node.name;
5022
+ if (name?.type === "JSXIdentifier" && typeof name.name === "string") skip.add(name.name);
5023
+ return;
5024
+ }
5025
+ case "TSTypeReference":
5026
+ case "TSQualifiedName":
5027
+ case "TSInterfaceDeclaration":
5028
+ case "TSTypeAliasDeclaration":
5029
+ for (const key of ["typeName", "left", "right", "id"]) nameOf2(node[key], skip);
5030
+ return;
5031
+ case "VariableDeclarator":
5032
+ patternNames2(node.id, bound);
5033
+ return;
5034
+ case "CatchClause":
5035
+ patternNames2(node.param, bound);
5036
+ return;
5037
+ default:
5038
+ return;
5039
+ }
5040
+ });
5041
+ walkAst(ast, (node) => {
5042
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration" || node.type === "ObjectMethod" || node.type === "ClassMethod") {
5043
+ for (const param of node.params ?? []) patternNames2(param, bound);
5044
+ if (node.id) patternNames2(node.id, bound);
5045
+ }
5046
+ });
5047
+ const free = /* @__PURE__ */ new Set();
5048
+ for (const name of candidates) if (!skip.has(name) && !bound.has(name)) free.add(name);
5049
+ return { free, bound };
5050
+ }
5051
+ function nameOf2(node, out) {
5052
+ if (!node) return;
5053
+ if (typeof node.name === "string") out.add(node.name);
5054
+ if (node.type === "JSXNamespacedName") {
5055
+ nameOf2(node.namespace, out);
5056
+ nameOf2(node.name, out);
5057
+ }
5058
+ }
5059
+ function patternNames2(node, out) {
5060
+ if (!node || typeof node !== "object") return;
5061
+ const typed = node;
5062
+ switch (typed.type) {
5063
+ case "Identifier":
5064
+ out.add(String(typed.name));
5065
+ return;
5066
+ case "ObjectPattern":
5067
+ for (const property of typed.properties ?? []) {
5068
+ patternNames2(property.type === "RestElement" ? property.argument : property.value, out);
5069
+ }
5070
+ return;
5071
+ case "ArrayPattern":
5072
+ for (const element of typed.elements ?? []) patternNames2(element, out);
5073
+ return;
5074
+ case "AssignmentPattern":
5075
+ patternNames2(typed.left, out);
5076
+ return;
5077
+ case "RestElement":
5078
+ patternNames2(typed.argument, out);
5079
+ return;
5080
+ default:
5081
+ return;
5082
+ }
5083
+ }
5084
+ function moduleScope(parsed, content) {
5085
+ const out = /* @__PURE__ */ new Map();
5086
+ const program = parsed.ast?.program;
5087
+ if (!program) return out;
5088
+ const source = parsed.script?.code ?? content;
5089
+ for (const statement of program.body ?? []) {
5090
+ const kind = statement.type;
5091
+ if (kind !== "VariableDeclaration" && kind !== "FunctionDeclaration" && kind !== "ClassDeclaration" && kind !== "TSTypeAliasDeclaration" && kind !== "TSInterfaceDeclaration" && kind !== "TSEnumDeclaration") {
5092
+ continue;
5093
+ }
5094
+ const start = statement.start;
5095
+ const end = statement.end;
5096
+ if (start === void 0 || end === void 0) continue;
5097
+ const text = source.slice(start, end);
5098
+ const names = /* @__PURE__ */ new Set();
5099
+ if (kind === "VariableDeclaration") {
5100
+ for (const declarator of statement.declarations ?? []) patternNames2(declarator.id, names);
5101
+ } else {
5102
+ patternNames2(statement.id, names);
5103
+ }
5104
+ if (names.size === 0) continue;
5105
+ const scanned = scanExpression(text.replace(/^(?:export\s+)?/, ""));
5106
+ const uses = scanned ? [...scanned.free].filter((name) => !names.has(name)) : [];
5107
+ for (const name of names) out.set(name, { at: start, text, uses });
5108
+ }
5109
+ return out;
5110
+ }
5111
+
5112
+ // src/componentize.ts
5113
+ import MagicString3 from "magic-string";
5114
+ var SectionInvariantError = class extends Error {
5115
+ code = "SECTION_INVARIANT";
5116
+ constructor(message) {
5117
+ super(message);
5118
+ this.name = "SectionInvariantError";
5119
+ }
5120
+ };
5121
+ function assertSections(total, sections) {
5122
+ const sum = sections.extracted + sections.inlineOnly + sections.alreadyExtracted + sections.pending.length;
5123
+ if (sum !== total) {
5124
+ throw new SectionInvariantError(
5125
+ `The receipt does not account for every section: ${total} planned but ${sections.extracted} extracted + ${sections.inlineOnly} inline-only + ${sections.alreadyExtracted} already extracted + ${sections.pending.length} pending = ${sum}.`
5126
+ );
5127
+ }
5128
+ return sections;
5129
+ }
5130
+ var SECTION_MARKER = "// @bettercms-ai/convert section v1";
5131
+ var REGISTRY_MARKER = "// @bettercms-ai/convert sections v1";
5132
+ var COMPONENT_DIR = "src/components/bcms";
5133
+ var SECTIONS_LIB = "src/lib/bcms-sections.ts";
5134
+ var EXTRACTABLE = ["astro", "jsx"];
5135
+ async function componentizeSources(briefIn, planIn, sources, options) {
5136
+ const pending = [];
5137
+ let extracted = 0;
5138
+ let inlineOnly = 0;
5139
+ let alreadyExtracted = 0;
5140
+ let planPending = 0;
5141
+ let total = 0;
5142
+ const brief = readBrief(briefIn);
5143
+ const plan2 = readPlan(planIn);
5144
+ const contents = new Map(sources.map((s) => [s.path, s.content]));
5145
+ const pend = (route, groupKey, reason, message) => void pending.push({ route, groupKey, reason, ...message ? { message } : {} });
5146
+ const trees = /* @__PURE__ */ new Map();
5147
+ const treeOf = async (file) => {
5148
+ if (trees.has(file)) return trees.get(file);
5149
+ const content = contents.get(file) ?? "";
5150
+ const dialect = dialectOf(file, content);
5151
+ const parsed = dialect ? await parseFile4(dialect, content) : null;
5152
+ const tree = parsed && !parsed.error ? { parsed, elements: elementIndex(parsed.roots) } : null;
5153
+ trees.set(file, tree);
5154
+ return tree;
5155
+ };
5156
+ const helperPath = HELPER_PATH.astro;
5157
+ const helperExisting = contents.get(helperPath);
5158
+ const helperVersion = helperExisting === void 0 ? null : isKnownHelper(helperExisting, "astro");
5159
+ const helperBlocked = helperExisting !== void 0 && helperVersion === null && options?.overwriteHelper !== true;
5160
+ const outFiles = /* @__PURE__ */ new Map();
5161
+ const written = /* @__PURE__ */ new Map();
5162
+ const shipped = /* @__PURE__ */ new Map();
5163
+ const componentsOf = (dialect) => {
5164
+ const held = written.get(dialect) ?? /* @__PURE__ */ new Map();
5165
+ written.set(dialect, held);
5166
+ return held;
5167
+ };
5168
+ const fresh = /* @__PURE__ */ new Set();
5169
+ const registryConflicts = [];
5170
+ for (const dialect of EXTRACTABLE) {
5171
+ const path = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
5172
+ const existing = contents.get(path);
5173
+ if (existing === void 0) continue;
5174
+ if (!isGenerated(existing)) {
5175
+ registryConflicts.push(path);
5176
+ continue;
5177
+ }
5178
+ const held = readRegistry(existing, dialect);
5179
+ componentsOf(dialect);
5180
+ for (const [slug, component] of held.components) written.get(dialect).set(slug, component);
5181
+ const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
5182
+ shipped.set(dialect, order);
5183
+ for (const [page, list] of held.shipped) order.set(page, list);
5184
+ }
5185
+ const lib = contents.get(SECTIONS_LIB);
5186
+ if (lib !== void 0 && !isGenerated(lib)) registryConflicts.push(SECTIONS_LIB);
5187
+ const registryBlocked = registryConflicts.length > 0 && options?.overwriteHelper !== true;
5188
+ for (const page of plan2.pages ?? []) {
5189
+ const sections2 = (page.sections ?? []).filter((section) => {
5190
+ if (section.pending) {
5191
+ planPending++;
5192
+ return false;
5193
+ }
5194
+ return true;
5195
+ });
5196
+ total += sections2.length;
5197
+ if (sections2.length === 0) continue;
5198
+ const briefPage = brief.pages.find((p) => p.route === page.route);
5199
+ const pageSlug = briefPage?.slug ?? slugify(page.route === "/" ? "home" : page.route);
5200
+ if (helperBlocked) {
5201
+ for (const section of sections2) {
5202
+ pend(page.route, section.groupKey, "HELPER_CONFLICT", `${helperPath} is not a module this package wrote.`);
5203
+ }
5204
+ continue;
5205
+ }
5206
+ if (registryBlocked) {
5207
+ for (const section of sections2) {
5208
+ pend(page.route, section.groupKey, "REGISTRY_CONFLICT", `${registryConflicts.join(", ")} is not a file this package wrote.`);
5209
+ }
5210
+ continue;
5211
+ }
5212
+ const snapshotPath = `${CONTENT_DIR}/${pageSlug}.json`;
5213
+ if (!snapshotUsable(contents.get(snapshotPath))) {
5214
+ for (const section of sections2) {
5215
+ pend(page.route, section.groupKey, "SNAPSHOT_INVALID", `${snapshotPath} is not a JSON object.`);
5216
+ }
5217
+ continue;
5218
+ }
5219
+ const pageFiles = pageFilesFor(page.route, contents.keys());
5220
+ if (pageFiles.length > 1) {
5221
+ for (const section of sections2) {
5222
+ pend(page.route, section.groupKey, "ROUTE_FILE_AMBIGUOUS", `${pageFiles.join(" and ")} both serve ${page.route}.`);
5223
+ }
5224
+ continue;
5225
+ }
5226
+ const file = pageFiles[0];
5227
+ if (file === void 0) {
5228
+ for (const section of sections2) {
5229
+ pend(page.route, section.groupKey, "NOT_IN_SOURCE", `no page file found for ${page.route}.`);
5230
+ }
5231
+ continue;
5232
+ }
5233
+ const done = new Set(alreadyOn(contents.get(file) ?? "", pageSlug, sections2));
5234
+ alreadyExtracted += done.size;
5235
+ if (done.size === sections2.length) continue;
5236
+ const found = await locateSections(
5237
+ sections2.filter((section) => !done.has(section.groupKey)),
5238
+ briefPage?.paths ?? [],
5239
+ sources,
5240
+ treeOf
5241
+ );
5242
+ for (const [groupKey, reason] of found.misses) {
5243
+ pend(page.route, groupKey, reason.code, reason.message);
5244
+ }
5245
+ const live = found.placed;
5246
+ if (live.length === 0) continue;
5247
+ const placedHere = [];
5248
+ for (const placed of live) {
5249
+ const elements = placed.byFile.get(file);
5250
+ if (elements && elements.length > 0) {
5251
+ placedHere.push({ section: placed.section, elements });
5252
+ continue;
5253
+ }
5254
+ pend(
5255
+ page.route,
5256
+ placed.section.groupKey,
5257
+ "SECTION_ROOT_AMBIGUOUS",
5258
+ `rendered by ${[...placed.byFile.keys()].join(", ")}, not ${file}.`
5259
+ );
5260
+ }
5261
+ if (placedHere.length === 0) continue;
5262
+ const content = contents.get(file);
5263
+ const dialect = dialectOf(file, content);
5264
+ if (!dialect || !EXTRACTABLE.includes(dialect)) {
5265
+ for (const placed of placedHere) {
5266
+ pend(page.route, placed.section.groupKey, "DIALECT_UNSUPPORTED", `${file} is ${dialect ?? "not a template"}.`);
5267
+ }
5268
+ continue;
5269
+ }
5270
+ const tree = await treeOf(file);
5271
+ if (!tree) {
5272
+ for (const placed of placedHere) {
5273
+ pend(page.route, placed.section.groupKey, "NOT_IN_SOURCE", `${file} did not parse.`);
5274
+ }
5275
+ continue;
5276
+ }
5277
+ const parsed = tree.parsed;
5278
+ const roots = [];
5279
+ for (const placed of placedHere) {
5280
+ const root = lowestCommonAncestor(placed.elements);
5281
+ if (!root) {
5282
+ pend(page.route, placed.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its fields sit under no single element.");
5283
+ continue;
5284
+ }
5285
+ const component = resolveComponent(plan2, placed.section);
5286
+ roots.push({ section: placed.section, route: page.route, root, ...component });
5287
+ }
5288
+ const nested = /* @__PURE__ */ new Set();
5289
+ for (const a of roots) {
5290
+ for (const b of roots) {
5291
+ if (a !== b && a.root.start <= b.root.start && b.root.end <= a.root.end) {
5292
+ nested.add(a);
5293
+ nested.add(b);
5294
+ }
5295
+ }
5296
+ }
5297
+ for (const resolved of nested) {
5298
+ pend(page.route, resolved.section.groupKey, "SECTION_ROOT_AMBIGUOUS", "its root contains another section's.");
5299
+ }
5300
+ const candidates = roots.filter((r) => !nested.has(r)).sort((a, b) => a.root.start - b.root.start);
5301
+ if (candidates.length === 0) continue;
5302
+ const extractable = componentsOf(dialect);
5303
+ const placeable = [];
5304
+ for (const resolved of candidates) {
5305
+ const path = `${COMPONENT_DIR}/${resolved.name}.${dialect === "astro" ? "astro" : "tsx"}`;
5306
+ const existing = contents.get(path);
5307
+ if (existing !== void 0 && !isGenerated(existing) && !extractable.has(resolved.slug)) {
5308
+ pend(page.route, resolved.section.groupKey, "COMPONENT_CONFLICT", `${path} is not a file this package wrote.`);
5309
+ continue;
5310
+ }
5311
+ if (extractable.has(resolved.slug)) {
5312
+ placeable.push(resolved);
5313
+ continue;
5314
+ }
5315
+ const built = componentFile(dialect, path, file, content, parsed, resolved);
5316
+ if (built.free.length > 0) {
5317
+ pend(
5318
+ page.route,
5319
+ resolved.section.groupKey,
5320
+ "SECTION_FREE_IDENTIFIERS",
5321
+ `its markup uses ${built.free.join(", ")}, which the component file cannot be given.`
5322
+ );
5323
+ continue;
5324
+ }
5325
+ extractable.set(resolved.slug, { name: resolved.name, path });
5326
+ fresh.add(resolved.slug);
5327
+ addFile(outFiles, contents, {
5328
+ path,
5329
+ operation: existing === void 0 ? "add" : "modify",
5330
+ content: built.content
5331
+ });
5332
+ placeable.push(resolved);
5333
+ }
5334
+ if (placeable.length === 0) continue;
5335
+ const layout = runOf(placeable, parsed.roots);
5336
+ if (layout === null) {
5337
+ for (const resolved of placeable) {
5338
+ pend(page.route, resolved.section.groupKey, "SECTION_NOT_CONTIGUOUS", "its roots share no container.");
5339
+ }
5340
+ continue;
5341
+ }
5342
+ const order = shipped.get(dialect) ?? /* @__PURE__ */ new Map();
5343
+ shipped.set(dialect, order);
5344
+ order.set(
5345
+ pageSlug,
5346
+ placeable.map((r) => ({ slug: r.slug, bind: r.section.groupKey }))
5347
+ );
5348
+ const rewritten = layout.contiguous ? splicePageRun(file, content, dialect, parsed, placeable, pageSlug) : splicePageInline(file, content, dialect, parsed, placeable, pageSlug, extractable);
5349
+ addFile(outFiles, contents, { path: file, operation: "modify", content: rewritten });
5350
+ if (layout.contiguous) extracted += placeable.length;
5351
+ else inlineOnly += placeable.length;
5352
+ }
5353
+ const componentSlugs = [...written.values()].flatMap((held) => [...held.keys()]);
5354
+ if (componentSlugs.length > 0) {
5355
+ const pageSlugs = [...new Set([...shipped.values()].flatMap((order) => [...order.keys()]))].sort();
5356
+ addFile(outFiles, contents, {
5357
+ path: SECTIONS_LIB,
5358
+ operation: contents.has(SECTIONS_LIB) ? "modify" : "add",
5359
+ content: sectionsLib(pageSlugs)
5360
+ });
5361
+ for (const [dialect, held] of [...written].sort(([a], [b]) => a.localeCompare(b))) {
5362
+ if (held.size === 0) continue;
5363
+ const registryPath = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
5364
+ addFile(outFiles, contents, {
5365
+ path: registryPath,
5366
+ operation: contents.has(registryPath) ? "modify" : "add",
5367
+ content: registryFile(dialect, registryPath, held, shipped.get(dialect) ?? /* @__PURE__ */ new Map())
5368
+ });
5369
+ }
5370
+ emitHelper(outFiles, contents, helperVersion, options?.overwriteHelper);
5371
+ emitStubs(outFiles, contents, pageSlugs);
5372
+ }
5373
+ const sections = assertSections(total, { extracted, inlineOnly, alreadyExtracted, pending });
5374
+ return {
5375
+ files: [...outFiles.values()].sort((a, b) => a.path.localeCompare(b.path)),
5376
+ receipt: {
5377
+ planDigest: plan2.digest ?? "",
5378
+ sections,
5379
+ // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
5380
+ components: [...fresh].sort(),
5381
+ planPending
5382
+ }
5383
+ };
5384
+ }
5385
+ function alreadyOn(content, pageSlug, sections) {
5386
+ const ours = COMPONENT_DIR.split("/").slice(-2).join("/");
5387
+ const importsOurs = new RegExp(`from\\s*["'][^"']*${ours}/`).test(content);
5388
+ if (!importsOurs) return [];
5389
+ if (new RegExp(`page=\\s*["']${pageSlug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content)) {
5390
+ return sections.map((section) => section.groupKey);
5391
+ }
5392
+ return sections.map((section) => section.groupKey).filter((groupKey) => new RegExp(`bind=\\s*["']${groupKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content));
5393
+ }
5394
+ function readRegistry(content, dialect) {
5395
+ const components = /* @__PURE__ */ new Map();
5396
+ const shipped = /* @__PURE__ */ new Map();
5397
+ const paths = /* @__PURE__ */ new Map();
5398
+ for (const match of content.matchAll(/^import\s+(\w+)\s+from\s+"\.\/([^"]+)";$/gm)) {
5399
+ const [, name, file] = match;
5400
+ paths.set(name, `${COMPONENT_DIR}/${file}${dialect === "astro" ? "" : ".tsx"}`);
5401
+ }
5402
+ const registry = content.match(/const REGISTRY[^=]*=\s*\{([\s\S]*?)\n\};/);
5403
+ for (const match of (registry?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\w+),$/gm)) {
5404
+ const [, slug, name] = match;
5405
+ const path = paths.get(name);
5406
+ if (path) components.set(slug, { name, path });
5407
+ }
5408
+ const table = content.match(/const SHIPPED[^=]*=\s*\{([\s\S]*?)\n\};/);
5409
+ for (const match of (table?.[1] ?? "").matchAll(/^\s*"([^"]+)":\s*(\[.*\]),$/gm)) {
5410
+ try {
5411
+ shipped.set(match[1], JSON.parse(match[2]));
5412
+ } catch {
5413
+ }
5414
+ }
5415
+ return { components, shipped };
5416
+ }
5417
+ function snapshotUsable(existing) {
5418
+ if (existing === void 0) return true;
5419
+ try {
5420
+ const parsed = JSON.parse(existing);
5421
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
5422
+ } catch {
5423
+ return false;
5424
+ }
5425
+ }
5426
+ function addFile(out, contents, file) {
5427
+ if (contents.get(file.path) === file.content) return;
5428
+ out.set(file.path, file);
5429
+ }
5430
+ async function locateSections(sections, briefPaths, sources, treeOf) {
5431
+ const placed = [];
5432
+ const misses = [];
5433
+ for (const section of sections) {
5434
+ const paths = fieldPaths(section);
5435
+ const literals = new Set(
5436
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
5437
+ );
5438
+ const hits = /* @__PURE__ */ new Map();
5439
+ for (const source of sources) {
5440
+ const tree = await treeOf(source.path);
5441
+ if (!tree) continue;
5442
+ const parsed = tree.parsed;
5443
+ const elements = tree.elements;
5444
+ const found = /* @__PURE__ */ new Map();
5445
+ for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
5446
+ if (declaration2.scope !== "page") continue;
5447
+ if (groupOf(declaration2.path) !== section.groupKey) continue;
5448
+ const placed2 = elements.get(declaration2.at);
5449
+ if (placed2) found.set(declaration2.at, placed2);
5450
+ }
5451
+ if (found.size === 0 && literals.size > 0) {
5452
+ for (const site of collectSites(parsed.roots, [...literals])) {
5453
+ if (site.partial || site.where === "script" || site.where === "comment") continue;
5454
+ const placed2 = elements.get(site.attrInsertAt);
5455
+ if (placed2) found.set(site.attrInsertAt, placed2);
5456
+ }
5457
+ }
5458
+ if (found.size > 0) hits.set(source.path, [...found.values()]);
5459
+ }
5460
+ if (hits.size === 0) {
5461
+ misses.push([
5462
+ section.groupKey,
5463
+ { code: "NOT_IN_SOURCE", message: `no file renders ${paths.join(", ") || section.groupKey}.` }
5464
+ ]);
5465
+ continue;
5466
+ }
5467
+ placed.push({ section, byFile: hits });
5468
+ }
5469
+ return { placed, misses };
5470
+ }
5471
+ var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
5472
+ var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
5473
+ function elementIndex(roots) {
5474
+ const out = /* @__PURE__ */ new Map();
5475
+ const visit = (node, chain) => {
5476
+ if (node.type === "expr") {
5477
+ for (const child of node.children) visit(child, chain);
5478
+ return;
5479
+ }
5480
+ if (node.type !== "element") return;
5481
+ const mine = [...chain, node];
5482
+ out.set(node.attrInsertAt, { node, chain: mine });
5483
+ for (const child of node.children) visit(child, mine);
5484
+ };
5485
+ for (const root of roots) visit(root, []);
5486
+ return out;
5487
+ }
5488
+ function lowestCommonAncestor(elements) {
5489
+ if (elements.length === 0) return null;
5490
+ let common = elements[0].chain;
5491
+ for (const placed of elements.slice(1)) {
5492
+ const shared = [];
5493
+ for (let i = 0; i < Math.min(common.length, placed.chain.length); i++) {
5494
+ if (common[i] !== placed.chain[i]) break;
5495
+ shared.push(common[i]);
5496
+ }
5497
+ common = shared;
5498
+ }
5499
+ return common[common.length - 1] ?? null;
5500
+ }
5501
+ function runOf(placeable, roots) {
5502
+ const parents = placeable.map((r) => parentOf(r.root, roots));
5503
+ if (parents.some((p) => p === void 0)) return null;
5504
+ const [first, ...rest] = parents;
5505
+ if (rest.some((p) => p !== first)) return null;
5506
+ const siblings = (first === null ? roots : first.children).filter(
5507
+ (node) => node.type === "element"
5508
+ );
5509
+ const indices = placeable.map((r) => siblings.indexOf(r.root)).sort((a, b) => a - b);
5510
+ if (indices.some((at) => at < 0)) return null;
5511
+ const contiguous = indices.every((at, i) => i === 0 || at === indices[i - 1] + 1);
5512
+ return { contiguous };
5513
+ }
5514
+ function parentOf(target, roots) {
5515
+ if (roots.some((node) => node === target)) return null;
5516
+ const search = (nodes, parent) => {
5517
+ for (const node of nodes) {
5518
+ if (node.type === "expr") {
5519
+ const inside = search(node.children, parent);
5520
+ if (inside !== void 0) return inside;
5521
+ continue;
5522
+ }
5523
+ if (node.type !== "element") continue;
5524
+ if (node.children.some((child) => child === target)) return node;
5525
+ const deeper = search(node.children, node);
5526
+ if (deeper !== void 0) return deeper;
5527
+ }
5528
+ return void 0;
5529
+ };
5530
+ return search(roots, null);
5531
+ }
5532
+ function resolveComponent(plan2, section) {
5533
+ const components = plan2.components ?? [];
5534
+ const wanted = section.reuse?.proposedSlug;
5535
+ const byId = section.reuse?.componentId;
5536
+ const entry = (wanted ? components.find((c) => c.proposedSlug === wanted) : void 0) ?? (byId ? components.find((c) => c.componentId === byId) : void 0) ?? onlyOne(components.filter((c) => c.sectionType && c.sectionType === section.sectionType));
5537
+ const family = entry?.name ?? section.sectionType ?? section.groupKey;
5538
+ return { slug: entry?.proposedSlug ?? wanted ?? slugify(family), name: pascal(family) };
5539
+ }
5540
+ var onlyOne = (all) => all.length === 1 ? all[0] : void 0;
5541
+ function pascal(value) {
5542
+ const parts = value.split(/[^A-Za-z0-9]+/).filter(Boolean);
5543
+ const name = parts.map((part) => part[0].toUpperCase() + part.slice(1)).join("");
5544
+ return /^[A-Za-z]/.test(name) ? name : `Section${name}`;
5545
+ }
5546
+ var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
5547
+ function componentFile(dialect, path, fromFile, content, parsed, resolved) {
5548
+ const markup = sliceWithBlockId(content, resolved.root);
5549
+ const groupKey = resolved.section.groupKey;
5550
+ const locals = helperLocals(parsed, dialect, fromFile);
5551
+ const helperNames = ["bcms", "bcmsRows", "bcmsLayout"].flatMap(
5552
+ (name) => locals[name].filter((local) => mentions(markup, local)).map((local) => ({ name, local }))
5553
+ );
5554
+ const helpers = [.../* @__PURE__ */ new Set([...helperNames.map((h) => alias(h.name, h.local)), "bcmsSection"])].sort();
5555
+ const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
5556
+ const snapshot = snapshotOf(parsed, dialect, fromFile, markup);
5557
+ const carry = carryFor({
5558
+ dialect,
5559
+ fromFile,
5560
+ toFile: path,
5561
+ content,
5562
+ parsed,
5563
+ root: resolved.root,
5564
+ provided: [
5565
+ "blockId",
5566
+ "bind",
5567
+ "overrides",
5568
+ "page",
5569
+ ...helperNames.map((h) => h.local),
5570
+ ...snapshot ? [snapshot] : []
5571
+ ],
5572
+ // The helper and the snapshots are what the component declares for itself.
5573
+ skipSpecifier: (repoPath, specifier) => specifier.includes(`${CONTENT_DIR}/`) || repoPath.startsWith(HELPER_PATH[dialect].replace(/\.tsx?$/, ""))
5574
+ });
5575
+ if (carry.free.length > 0) return { content: "", free: carry.free };
5576
+ const carried = [...carry.imports];
5577
+ const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
5578
+ const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
5579
+ ` : "";
5580
+ const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind ?? ${JSON.stringify(groupKey)}, overrides) };` : "";
5581
+ if (dialect === "astro") {
5582
+ return { free: [], content: `---
5583
+ ${SECTION_MARKER}
5584
+ ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`].join("\n")}
5585
+
5586
+ interface Props {
5587
+ /** The placement's block id. The editor hit-tests \`[data-bcms-block]\` for section chrome. */
5588
+ blockId?: string;
5589
+ /** The page field group this placement renders \u2014 Phase A keeps the copy where P2 put it. */
5590
+ bind?: string;
5591
+ /** The instance's own copy, when an editor gave it any. Wins over the bound group. */
5592
+ overrides?: Record<string, unknown>;
5593
+ /** The page snapshot, so one component can serve every page whose group has this shape. */
5594
+ page?: unknown;
5595
+ }
5596
+ const { blockId, bind, overrides, page } = Astro.props;
5597
+ ${carriedDeclarations}${copy}
5598
+ ---
5599
+ ${markup}
5600
+ ` };
5601
+ }
5602
+ return { free: [], content: `${SECTION_MARKER}
5603
+ ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
5604
+ ${carriedDeclarations === "" ? "" : `
5605
+ ${carriedDeclarations}`}
5606
+ export default function ${resolved.name}({ blockId, bind, overrides, page }: BcmsSectionProps) {
5607
+ ${copy}
5608
+ return (
5609
+ ${markup}
5610
+ );
5611
+ }
5612
+ ` };
5613
+ }
5614
+ function sliceWithBlockId(content, root) {
5615
+ const slice = new MagicString3(content.slice(root.start, root.end));
5616
+ slice.appendLeft(root.attrInsertAt - root.start, " data-bcms-block={blockId}");
5617
+ return slice.toString();
5618
+ }
5619
+ var alias = (name, local) => name === local ? name : `${name} as ${local}`;
5620
+ var mentions = (markup, name) => new RegExp(`(?<![\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w$])`).test(markup);
5621
+ function snapshotOf(parsed, dialect, fromFile, markup) {
5622
+ for (const [local, from] of importsIn(parsed, dialect)) {
5623
+ if (from.imported !== null) continue;
5624
+ if (!from.specifier.includes(`${CONTENT_DIR}/`) || !from.specifier.endsWith(".json")) continue;
5625
+ if (mentions(markup, local)) return local;
5626
+ }
5627
+ return null;
5628
+ }
5629
+ function sectionsLib(pageSlugs) {
5630
+ const imports = pageSlugs.map(
5631
+ (slug) => `import ${identifierFor(slug)} from "${relativeImport(SECTIONS_LIB, `${CONTENT_DIR}/${slug}.json`)}";`
5632
+ );
5633
+ const table = pageSlugs.map((slug) => ` ${JSON.stringify(slug)}: ${identifierFor(slug)},`).join("\n");
5634
+ return `${REGISTRY_MARKER}
5635
+ // Structure comes from bcms-content/*.json, written into the tree before the build: each page's
5636
+ // \`blocks\` are its ordered section instances, and components.json names which component each one
5637
+ // is. The committed stubs are empty \u2014 a build with them renders the order the repository ships.
5638
+ import bcmsComponents from "${relativeImport(SECTIONS_LIB, `${CONTENT_DIR}/components.json`)}";
5639
+ ${imports.join("\n")}
5640
+
5641
+ const SNAPSHOTS: Record<string, unknown> = {
5642
+ ${table}
5643
+ };
5644
+
5645
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
5646
+ typeof v === "object" && v !== null && !Array.isArray(v);
5647
+
5648
+ /** What every extracted section component is handed. */
5649
+ export interface BcmsSectionProps {
5650
+ blockId?: string;
5651
+ bind?: string;
5652
+ overrides?: Record<string, unknown>;
5653
+ page?: unknown;
5654
+ }
5655
+
5656
+ /** One placement on one page: which component, bound to which page field group. */
5657
+ export interface BcmsPlacement {
5658
+ blockId?: string;
5659
+ slug: string;
5660
+ bind?: string;
5661
+ overrides?: Record<string, unknown>;
5662
+ }
5663
+
5664
+ /** The page's own snapshot \u2014 the copy every placement on it reads through \`bind\`. */
5665
+ export function pageSnapshot(pageSlug: string): unknown {
5666
+ return SNAPSHOTS[pageSlug];
5667
+ }
5668
+
5669
+ /** The page's \`component\` blocks, before any of them is matched to a component this repo has. */
5670
+ function componentBlocks(pageSlug: string): Record<string, unknown>[] {
5671
+ const snapshot = SNAPSHOTS[pageSlug];
5672
+ const blocks = isRecord(snapshot) ? snapshot.blocks : undefined;
5673
+ if (!Array.isArray(blocks)) return [];
5674
+ return (blocks as unknown[]).filter(
5675
+ (block): block is Record<string, unknown> => isRecord(block) && block.type === "component",
5676
+ );
5677
+ }
5678
+
5679
+ /**
5680
+ * Has the CMS said anything about this page's structure?
5681
+ *
5682
+ * \u{1F534} THE RAW LIST, NOT THE RESOLVED ONE. "No sections came back" has two causes that must not be
5683
+ * treated alike: the CMS holds nothing yet (fall back to the order this repository ships), or the
5684
+ * CMS holds a list whose components this repository does not have (render nothing \u2014 the page HAS
5685
+ * been arranged, and re-showing the old bands would silently ignore an editor's work).
5686
+ */
5687
+ export function hasCmsSections(pageSlug: string): boolean {
5688
+ return componentBlocks(pageSlug).length > 0;
5689
+ }
5690
+
5691
+ /**
5692
+ * The page's section instances, in the order the CMS holds them.
5693
+ *
5694
+ * An instance naming a component this repository does not have is SKIPPED, never rendered as a
5695
+ * hole: the CMS is free to add a section type the repo has not shipped yet, and a half-built page
5696
+ * is worse than a page missing one band.
5697
+ */
5698
+ export function pageSections(pageSlug: string): BcmsPlacement[] {
5699
+ const blocks = componentBlocks(pageSlug);
5700
+ if (blocks.length === 0) return [];
5701
+
5702
+ const slugs = new Map<string, string>();
5703
+ if (Array.isArray(bcmsComponents)) {
5704
+ for (const component of bcmsComponents as unknown[]) {
5705
+ if (!isRecord(component)) continue;
5706
+ const id = component.id ?? component.componentId;
5707
+ const slug = component.slug ?? component.proposedSlug;
5708
+ if (typeof id === "string" && typeof slug === "string") slugs.set(id, slug);
5709
+ }
5710
+ }
5711
+
5712
+ const out: BcmsPlacement[] = [];
5713
+ for (const block of blocks) {
5714
+ const props = isRecord(block.props) ? block.props : {};
5715
+ const slug = typeof props.componentId === "string" ? slugs.get(props.componentId) : undefined;
5716
+ if (!slug) continue;
5717
+ out.push({
5718
+ blockId: typeof block.id === "string" ? block.id : undefined,
5719
+ slug,
5720
+ bind: typeof props.bind === "string" ? props.bind : undefined,
5721
+ overrides: isRecord(props.overrides) ? props.overrides : undefined,
5722
+ });
5723
+ }
5724
+ return out;
5725
+ }
5726
+ `;
5727
+ }
5728
+ var identifierFor = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
5729
+ function registryFile(dialect, path, written, shipped) {
5730
+ const entries = [...written].sort(([a], [b]) => a.localeCompare(b));
5731
+ const imports = entries.map(([, component]) => {
5732
+ const specifier = relativeImport(path, component.path);
5733
+ return `import ${component.name} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
5734
+ }).join("\n");
5735
+ const registry = entries.map(([slug, component]) => ` ${JSON.stringify(slug)}: ${component.name},`).join("\n");
5736
+ const shippedTable = [...shipped].sort(([a], [b]) => a.localeCompare(b)).map(([page, list]) => ` ${JSON.stringify(page)}: ${JSON.stringify(list)},`).join("\n");
5737
+ const lib = relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "");
5738
+ const registryType = dialect === "astro" ? "Record<string, unknown>" : "Record<string, (props: BcmsSectionProps) => JSX.Element | null>";
5739
+ const shared = `${imports}
5740
+ import { hasCmsSections, pageSections, pageSnapshot, type BcmsPlacement${dialect === "astro" ? "" : ", type BcmsSectionProps"} } from "${lib}";
5741
+
5742
+ /** slug \u2192 the component that renders it. An unknown slug is skipped, never a hole. */
5743
+ const REGISTRY: ${registryType} = {
5744
+ ${registry}
5745
+ };
5746
+
5747
+ /**
5748
+ * The order the REPOSITORY ships, per page \u2014 the fallback when the CMS holds no blocks yet.
5749
+ *
5750
+ * \u{1F534} LOAD-BEARING. An empty \`blocks\` means "nothing has been said about this page", not "this
5751
+ * page is empty": it is what a fresh clone, the committed stub and every not-yet-componentised
5752
+ * page all look like. Rendering nothing there would blank the site on the first build after the
5753
+ * codemod ran, with nothing in the diff to explain it.
5754
+ */
5755
+ const SHIPPED: Record<string, BcmsPlacement[]> = {
5756
+ ${shippedTable}
5757
+ };
5758
+
5759
+ /**
5760
+ * \u{1F534} THE FALLBACK IS DECIDED ON THE RAW LIST. A page whose CMS blocks all name components this
5761
+ * repository does not have has still been ARRANGED \u2014 falling back there would put the shipped
5762
+ * bands back on a page an editor deliberately emptied, and no amount of reading the diff would
5763
+ * explain it. Only "the CMS holds nothing" takes the shipped order.
5764
+ */
5765
+ const placementsFor = (page: string): BcmsPlacement[] =>
5766
+ hasCmsSections(page) ? pageSections(page) : (SHIPPED[page] ?? []);`;
5767
+ if (dialect === "astro") {
5768
+ return `---
5769
+ ${REGISTRY_MARKER}
5770
+ ${shared}
5771
+
5772
+ interface Props {
5773
+ /** The page slug, as \`bcms-content/<slug>.json\` spells it. */
5774
+ page: string;
5775
+ }
5776
+ const { page } = Astro.props;
5777
+ const placements = placementsFor(page);
5778
+ const snapshot = pageSnapshot(page);
5779
+ ---
5780
+ {placements.map((placement) => {
5781
+ const Component = REGISTRY[placement.slug] as never;
5782
+ return Component ? (
5783
+ <Component
5784
+ blockId={placement.blockId}
5785
+ bind={placement.bind}
5786
+ overrides={placement.overrides}
5787
+ page={snapshot}
5788
+ />
5789
+ ) : null;
5790
+ })}
5791
+ `;
5792
+ }
5793
+ return `${REGISTRY_MARKER}
5794
+ ${shared}
5795
+
5796
+ export default function Sections({ page }: { page: string }) {
5797
+ const placements = placementsFor(page);
5798
+ const snapshot = pageSnapshot(page);
5799
+ return (
5800
+ <>
5801
+ {placements.map((placement, i) => {
5802
+ const Component = REGISTRY[placement.slug];
5803
+ return Component ? (
5804
+ <Component
5805
+ key={placement.blockId ?? \`\${placement.slug}-\${i}\`}
5806
+ blockId={placement.blockId}
5807
+ bind={placement.bind}
5808
+ overrides={placement.overrides}
5809
+ page={snapshot}
5810
+ />
5811
+ ) : null;
5812
+ })}
5813
+ </>
5814
+ );
5815
+ }
5816
+ `;
5817
+ }
5818
+ function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
5819
+ const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
5820
+ const local = names.get("Sections");
5821
+ const source = new MagicString3(content);
5822
+ const first = placeable[0].root;
5823
+ const last = placeable[placeable.length - 1].root;
5824
+ source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
5825
+ const registry = `${COMPONENT_DIR}/Sections.${dialect === "astro" ? "astro" : "tsx"}`;
5826
+ const specifier = relativeImport(file, registry);
5827
+ addImports(source, parsed, [
5828
+ `import ${local} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`
5829
+ ]);
5830
+ return source.toString();
5831
+ }
5832
+ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, written) {
5833
+ const wanted = [
5834
+ ...new Set(placeable.map((r) => r.slug))
5835
+ ].map((slug) => ({ key: slug, name: written.get(slug).name }));
5836
+ const names = allocateNames(topLevelBindings(dialect, parsed, content), [
5837
+ ...wanted,
5838
+ { key: "pageSnapshot", name: "pageSnapshot" }
5839
+ ]);
5840
+ const snapshot = names.get("pageSnapshot");
5841
+ const source = new MagicString3(content);
5842
+ for (const resolved of placeable) {
5843
+ source.overwrite(
5844
+ resolved.root.start,
5845
+ resolved.root.end,
5846
+ `<${names.get(resolved.slug)} bind=${JSON.stringify(resolved.section.groupKey)} page={${snapshot}(${JSON.stringify(pageSlug)})} />`
5847
+ );
5848
+ }
5849
+ const lib = relativeImport(file, SECTIONS_LIB).replace(/\.tsx?$/, "");
5850
+ const imports = [
5851
+ ...wanted.map(({ key }) => {
5852
+ const specifier = relativeImport(file, written.get(key).path);
5853
+ return `import ${names.get(key)} from "${dialect === "astro" ? specifier : specifier.replace(/\.tsx?$/, "")}";`;
5854
+ }),
5855
+ `import { pageSnapshot${snapshot === "pageSnapshot" ? "" : ` as ${snapshot}`} } from "${lib}";`
5856
+ ];
5857
+ addImports(source, parsed, imports);
5858
+ return source.toString();
5859
+ }
5860
+ function addImports(source, parsed, lines) {
5861
+ const at = parsed.importAt;
5862
+ if (!at || lines.length === 0) return;
5863
+ if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${lines.join("\n")}${at.wrap.after}`);
5864
+ else if (at.at === 0) source.appendLeft(0, `${lines.join("\n")}
5865
+ `);
5866
+ else source.appendLeft(at.at, `
5867
+ ${lines.join("\n")}`);
5868
+ }
5869
+ var isGenerated = (content) => content.includes(SECTION_MARKER) || content.includes(REGISTRY_MARKER);
5870
+ function emitHelper(out, contents, version, overwrite) {
5871
+ const path = HELPER_PATH.astro;
5872
+ const source = helperSource("astro");
5873
+ const existing = contents.get(path);
5874
+ if (existing === void 0) {
5875
+ addFile(out, contents, { path, operation: "add", content: source });
5876
+ return;
5877
+ }
5878
+ if (version === null && overwrite !== true) return;
5879
+ if (version === null || version < HELPER_VERSION) {
5880
+ addFile(out, contents, { path, operation: "modify", content: source });
5881
+ }
5882
+ }
5883
+ function emitStubs(out, contents, pageSlugs) {
5884
+ const components = `${CONTENT_DIR}/components.json`;
5885
+ if (!contents.has(components)) {
5886
+ addFile(out, contents, { path: components, operation: "add", content: "[]" });
5887
+ }
5888
+ for (const slug of pageSlugs) {
5889
+ const path = `${CONTENT_DIR}/${slug}.json`;
5890
+ const existing = contents.get(path);
5891
+ let held = {};
5892
+ if (existing !== void 0) {
5893
+ try {
5894
+ const parsed = JSON.parse(existing);
5895
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
5896
+ held = parsed;
5897
+ } catch {
5898
+ continue;
5899
+ }
5900
+ }
5901
+ if (Array.isArray(held.blocks)) continue;
5902
+ addFile(out, contents, {
5903
+ path,
5904
+ operation: existing === void 0 ? "add" : "modify",
5905
+ content: `${JSON.stringify({ ...held, blocks: [] }, null, 2)}
5906
+ `
5907
+ });
5908
+ }
5909
+ }
5910
+ function readComponentizePlan(value) {
5911
+ return readPlan(value);
5912
+ }
5913
+
4600
5914
  // src/index.ts
4601
5915
  var ConvertError = class extends Error {
4602
5916
  constructor(code, message) {
@@ -4608,7 +5922,7 @@ var ConvertError = class extends Error {
4608
5922
  };
4609
5923
  var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
4610
5924
  var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4611
- var identifierFor = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
5925
+ var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
4612
5926
  async function drillProp(ctx, fromFile, component, prop, kind, hop) {
4613
5927
  const miss = { ok: false, reason: "PROP_TARGET_NOT_FOUND", edits: [] };
4614
5928
  if (hop > MAX_HOPS) return { ok: false, reason: "PROP_DRILLED_DEEP", edits: [] };
@@ -4656,7 +5970,8 @@ async function drillProp(ctx, fromFile, component, prop, kind, hop) {
4656
5970
  if (found.propsType) at(found.propsType.at, found.propsType.text);
4657
5971
  return { ok: true, edits: [{ file, splices }] };
4658
5972
  }
4659
- async function convertSources(brief, sources, options) {
5973
+ async function convertSources(briefIn, sources, options) {
5974
+ const brief = readBrief(briefIn);
4660
5975
  const digest = briefDigest(brief);
4661
5976
  const targets = /* @__PURE__ */ new Map();
4662
5977
  for (const page of brief.pages) {
@@ -4791,7 +6106,7 @@ async function convertSources(brief, sources, options) {
4791
6106
  else rewritten++;
4792
6107
  }
4793
6108
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
4794
- const source = new MagicString3(contents.get(file));
6109
+ const source = new MagicString4(contents.get(file));
4795
6110
  const seen = /* @__PURE__ */ new Set();
4796
6111
  for (const splice of splices) {
4797
6112
  const id = `${splice.start}:${splice.end}:${splice.text}`;
@@ -4811,7 +6126,7 @@ async function convertSources(brief, sources, options) {
4811
6126
  }
4812
6127
  }
4813
6128
  const { helpers, helperUpgraded } = emitHelpers(dialects, contents, outFiles, options?.overwriteHelper);
4814
- emitStubs(dialects, snapshotSlugs, contents, outFiles);
6129
+ emitStubs2(dialects, snapshotSlugs, contents, outFiles);
4815
6130
  const receipt = assertReceipt({
4816
6131
  briefDigest: digest,
4817
6132
  paths: { declared: targets.size, rewritten, alreadyDeclared, pending },
@@ -4926,7 +6241,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
4926
6241
  { key: "helper:bcmsRows", name: "bcmsRows" },
4927
6242
  { key: "helper:bcmsLayout", name: "bcmsLayout" },
4928
6243
  // Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
4929
- ...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor(slug) }))
6244
+ ...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
4930
6245
  ].filter((entry) => !preset.has(entry.key));
4931
6246
  const names = new Map([...preset, ...allocateNames(taken, wanted)]);
4932
6247
  const snapshotName = (slug) => names.get(`slug:${slug}`);
@@ -5318,7 +6633,7 @@ function emitHelpers(dialects, contents, out, overwrite) {
5318
6633
  }
5319
6634
  return { helpers, helperUpgraded };
5320
6635
  }
5321
- function emitStubs(dialects, slugs, contents, out) {
6636
+ function emitStubs2(dialects, slugs, contents, out) {
5322
6637
  if (![...dialects].some((d) => DIALECT_RULES[d].text)) return;
5323
6638
  const paths = [...[...slugs].map((slug) => `${CONTENT_DIR}/${slug}.json`), `${CONTENT_DIR}/layout.json`];
5324
6639
  for (const path of paths.sort()) {
@@ -5327,21 +6642,30 @@ function emitStubs(dialects, slugs, contents, out) {
5327
6642
  }
5328
6643
  }
5329
6644
  export {
6645
+ COMPONENT_DIR,
5330
6646
  CONTENT_DIR,
5331
6647
  ConvertError,
5332
6648
  DIALECT_RULES,
5333
6649
  HELPER_PATH,
5334
6650
  HELPER_VERSION,
5335
6651
  PARSE_FILE,
6652
+ REGISTRY_MARKER,
5336
6653
  ReceiptInvariantError,
6654
+ SECTIONS_LIB,
6655
+ SECTION_MARKER,
5337
6656
  SKIP_DIRS,
5338
6657
  SKIP_FILES,
5339
6658
  SOURCE_EXTENSIONS,
5340
6659
  STUB_CONTENT,
6660
+ SectionInvariantError,
6661
+ ValidationError,
5341
6662
  aliasesFrom,
5342
6663
  assertReceipt,
6664
+ assertSections,
5343
6665
  attrsFor,
5344
6666
  briefDigest,
6667
+ carryFor,
6668
+ componentizeSources,
5345
6669
  convertSources,
5346
6670
  convertedHere,
5347
6671
  coverageOf,
@@ -5353,17 +6677,23 @@ export {
5353
6677
  flat,
5354
6678
  helperSource,
5355
6679
  importsIn,
6680
+ isDynamicRoute,
5356
6681
  isKnownHelper,
5357
6682
  isSourceCandidate,
5358
6683
  locate,
5359
6684
  overlapping,
6685
+ pageFilesFor,
5360
6686
  parseFile4 as parseFile,
5361
6687
  propsAttribute,
6688
+ readBrief,
6689
+ readComponentizePlan,
5362
6690
  readDeclarations,
5363
6691
  readExpr,
6692
+ readPlan,
5364
6693
  relativeImport,
5365
6694
  resolveSpecifier,
5366
6695
  rewriteFile,
6696
+ routeOfFile,
5367
6697
  stripTags,
5368
6698
  walkAst
5369
6699
  };