@bettercms-ai/convert 0.10.0 → 0.11.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.d.ts CHANGED
@@ -829,7 +829,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
829
829
  declare function propsAttribute(props: AttrBinding[]): string;
830
830
 
831
831
  /** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
832
- declare const HELPER_VERSION = 6;
832
+ declare const HELPER_VERSION = 7;
833
833
  /** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
834
834
  declare const HELPER_PATH: Record<Dialect, string | null>;
835
835
  /** The directory the snapshots live in, at the repository root. */
@@ -1416,6 +1416,11 @@ declare function carryFor(options: {
1416
1416
  * an unexpected extra key is simply carried past. The shape is frozen; the reader is not brittle.
1417
1417
  */
1418
1418
  interface ComponentizePlanField {
1419
+ /**
1420
+ * Where an instance holds this leaf's copy (`h2_latest`, `group_webgl[0].a_webgl`) — allocated by the
1421
+ * server, which alone knows the dedupe suffixes and table keys. Absent from older plans.
1422
+ */
1423
+ overrideKey?: string;
1419
1424
  /** The leaf's key inside the group — `title`, `items[0].q`. */
1420
1425
  key?: string;
1421
1426
  /** The whole page path — `hero.title`. Derived from `groupKey` + `key` when absent. */
@@ -1559,6 +1564,15 @@ interface ComponentizeReceipt {
1559
1564
  sections: SectionsReceipt;
1560
1565
  /** The component slugs this run wrote a file for, sorted. */
1561
1566
  components: string[];
1567
+ /**
1568
+ * The file each of those components was written to — what lets the server record a component's source
1569
+ * (kind `section`) without anyone claiming it by hand. Additive: readers of `components` are unchanged.
1570
+ */
1571
+ componentFiles?: {
1572
+ slug: string;
1573
+ path: string;
1574
+ export: "default";
1575
+ }[];
1562
1576
  /**
1563
1577
  * Sections the SERVER already marked pending (`NO_GROUP_ROOT`, `ALREADY_COMPONENTIZED`, …).
1564
1578
  *
package/dist/index.js CHANGED
@@ -4140,7 +4140,7 @@ function bindImages(ctx, targets) {
4140
4140
 
4141
4141
  // src/helper.ts
4142
4142
  import { createHash as createHash2 } from "crypto";
4143
- var HELPER_VERSION = 6;
4143
+ var HELPER_VERSION = 7;
4144
4144
  var HEADER = "// @bettercms-ai/convert helper v";
4145
4145
  var HELPER_PATH = Object.fromEntries(
4146
4146
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4333,9 +4333,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
4333
4333
  * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4334
4334
  * instance on the next, and a repository does not have to be re-cut when its copy moves.
4335
4335
  */
4336
+ /** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
4337
+ export type BcmsPathMap = {
4338
+ scalars?: Record<string, string>;
4339
+ tables?: Record<string, { key: string; columns: Record<string, string> }>;
4340
+ };
4341
+
4342
+ /**
4343
+ * The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
4344
+ *
4345
+ * \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
4346
+ * copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
4347
+ * markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
4348
+ *
4349
+ * - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
4350
+ * column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
4351
+ * - else, for a placement bound to a group, the page value at the path re-based onto that group;
4352
+ * - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
4353
+ * \`bcmsSection\` synthesizes no group for it.
4354
+ *
4355
+ * Every other page key passes through, so reads outside the family (a shared link) keep working.
4356
+ */
4357
+ export function bcmsSectionPaths(
4358
+ page: unknown,
4359
+ bind: string | undefined,
4360
+ overrides: unknown,
4361
+ prefix: string,
4362
+ map: BcmsPathMap,
4363
+ ): Record<string, unknown> {
4364
+ const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
4365
+ const own = isRecord(overrides) ? overrides : {};
4366
+ const separator = prefix.slice(-1);
4367
+ const fromPage = (path: string): unknown =>
4368
+ bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
4369
+ for (const [path, key] of Object.entries(map.scalars ?? {})) {
4370
+ const value = own[key];
4371
+ out[path] = value !== undefined && value !== null ? value : fromPage(path);
4372
+ }
4373
+ for (const [path, table] of Object.entries(map.tables ?? {})) {
4374
+ const held = own[table.key];
4375
+ const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
4376
+ if (!Array.isArray(rows)) {
4377
+ out[path] = fromPage(path);
4378
+ continue;
4379
+ }
4380
+ const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
4381
+ out[path] = rows.map((row) => {
4382
+ if (!isRecord(row)) return row;
4383
+ const spelled: Record<string, unknown> = {};
4384
+ for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
4385
+ return spelled;
4386
+ });
4387
+ }
4388
+ return out;
4389
+ }
4390
+
4336
4391
  export function bcmsFields(
4337
4392
  placement: { blockId?: string; bind?: string; overrides?: unknown },
4338
4393
  pathPrefix: string,
4394
+ map?: BcmsPathMap,
4339
4395
  ): (path: string) => string {
4340
4396
  const id = placement.blockId;
4341
4397
  const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
@@ -4348,6 +4404,24 @@ export function bcmsFields(
4348
4404
  * so prepending one would invent an address rather than correct it.
4349
4405
  */
4350
4406
  const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
4407
+ if (map) {
4408
+ /**
4409
+ * A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
4410
+ * plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
4411
+ * separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
4412
+ */
4413
+ const separator = pathPrefix.slice(-1);
4414
+ return (path: string): string => {
4415
+ if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
4416
+ if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
4417
+ const scalar = map.scalars?.[path];
4418
+ if (scalar !== undefined) return id + "__overrides." + scalar;
4419
+ const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
4420
+ const table = row ? map.tables?.[row[1]] : undefined;
4421
+ const column = row && table ? table.columns[row[3]] : undefined;
4422
+ return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
4423
+ };
4424
+ }
4351
4425
  return (path: string): string => {
4352
4426
  // A path outside the group this section was cut from is not one this placement re-addresses:
4353
4427
  // it names a key the instance does not hold and a group this placement is not bound to.
@@ -4364,21 +4438,24 @@ var HISTORICAL = {
4364
4438
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4365
4439
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4366
4440
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4367
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4441
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4442
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4368
4443
  },
4369
4444
  "src/lib/bcms-content.ts": {
4370
4445
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4371
4446
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4372
4447
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4373
4448
  4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4374
- 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4449
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4450
+ 6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
4375
4451
  },
4376
4452
  "composables/bcms-content.ts": {
4377
4453
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4378
4454
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4379
4455
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4380
4456
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4381
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4457
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4458
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4382
4459
  }
4383
4460
  };
4384
4461
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -6887,6 +6964,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
6887
6964
  return held;
6888
6965
  };
6889
6966
  const fresh = /* @__PURE__ */ new Set();
6967
+ const freshFiles = /* @__PURE__ */ new Map();
6890
6968
  const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
6891
6969
  const placementOf = (slug, section) => {
6892
6970
  const bind = bindOf(section);
@@ -7214,6 +7292,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7214
7292
  }
7215
7293
  extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
7216
7294
  fresh.add(resolved.slug);
7295
+ freshFiles.set(resolved.slug, path);
7217
7296
  addFile(outFiles, contents, {
7218
7297
  path,
7219
7298
  operation: existing === void 0 ? "add" : "modify",
@@ -7299,6 +7378,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7299
7378
  sections,
7300
7379
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
7301
7380
  components: [...fresh].sort(),
7381
+ componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
7302
7382
  planPending,
7303
7383
  notes
7304
7384
  }
@@ -7478,7 +7558,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7478
7558
  for (const section of sections) {
7479
7559
  const paths = fieldPaths(section);
7480
7560
  const literals = new Set(
7481
- briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7561
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7482
7562
  );
7483
7563
  const hits = /* @__PURE__ */ new Map();
7484
7564
  for (const source of sources) {
@@ -7489,7 +7569,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7489
7569
  const found = /* @__PURE__ */ new Map();
7490
7570
  for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
7491
7571
  if (declaration2.scope !== "page") continue;
7492
- if (groupOf(declaration2.path) !== section.groupKey) continue;
7572
+ if (!inSection(section, declaration2.path)) continue;
7493
7573
  const placed2 = elements.get(declaration2.at);
7494
7574
  if (placed2) found.set(declaration2.at, placed2);
7495
7575
  }
@@ -7514,6 +7594,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7514
7594
  return { placed, misses };
7515
7595
  }
7516
7596
  var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
7597
+ var inSection = (section, path) => {
7598
+ if (groupOf(path) === section.groupKey) return true;
7599
+ const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
7600
+ const target = rowless(path);
7601
+ return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
7602
+ };
7517
7603
  var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
7518
7604
  function elementIndex(roots) {
7519
7605
  const out = /* @__PURE__ */ new Map();
@@ -7616,6 +7702,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7616
7702
  ...addresses.opaque
7617
7703
  ];
7618
7704
  if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
7705
+ const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
7706
+ const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
7707
+ if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
7619
7708
  const cut = content.slice(resolved.root.start, resolved.root.end);
7620
7709
  const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
7621
7710
  const introduced = allocateNames(reserved, [
@@ -7634,7 +7723,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7634
7723
  const helpers = [
7635
7724
  .../* @__PURE__ */ new Set([
7636
7725
  ...helperNames.map((h) => alias(h.name, h.local)),
7637
- "bcmsSection",
7726
+ flat3 ? "bcmsSectionPaths" : "bcmsSection",
7638
7727
  ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
7639
7728
  ])
7640
7729
  ].sort();
@@ -7664,9 +7753,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7664
7753
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
7665
7754
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
7666
7755
  ` : "";
7667
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
7668
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
7756
+ const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
7757
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
7669
7758
  const declared = [copy, field].filter(Boolean).join("\n");
7759
+ const styles = dialect === "astro" ? carriedStyles(content) : "";
7670
7760
  if (dialect === "astro") {
7671
7761
  return { free: [], duplicates: [], unconvertible: [], content: `---
7672
7762
  ${SECTION_MARKER}
@@ -7686,7 +7776,9 @@ const { blockId, bind, overrides, page } = Astro.props;
7686
7776
  ${carriedDeclarations}${declared}
7687
7777
  ---
7688
7778
  ${markup}
7689
- ` };
7779
+ ${styles ? `
7780
+ ${styles}
7781
+ ` : ""}` };
7690
7782
  }
7691
7783
  return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
7692
7784
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
@@ -7960,6 +8052,9 @@ import { bcmsFields } from "${helper}";`);
7960
8052
  return { files, refused };
7961
8053
  }
7962
8054
  function propPrefix(section) {
8055
+ return exactPrefix(section) ?? flatPrefix(section);
8056
+ }
8057
+ function exactPrefix(section) {
7963
8058
  const prefixes = /* @__PURE__ */ new Set();
7964
8059
  for (const field of section.fields ?? []) {
7965
8060
  const { path, key } = field;
@@ -7970,6 +8065,49 @@ function propPrefix(section) {
7970
8065
  if (prefixes.size > 1) return null;
7971
8066
  return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
7972
8067
  }
8068
+ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
8069
+ function flatPrefix(section) {
8070
+ const prefixes = /* @__PURE__ */ new Set();
8071
+ let informative = false;
8072
+ for (const field of section.fields ?? []) {
8073
+ const { path, key } = field;
8074
+ if (typeof path !== "string" || typeof key !== "string") continue;
8075
+ informative = true;
8076
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8077
+ if (!prefix) return null;
8078
+ prefixes.add(prefix);
8079
+ }
8080
+ return informative && prefixes.size === 1 ? [...prefixes][0] : null;
8081
+ }
8082
+ function flatPathMap(section, prefix) {
8083
+ const scalars = {};
8084
+ const tables = {};
8085
+ for (const field of section.fields ?? []) {
8086
+ const { path, key } = field;
8087
+ if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8088
+ const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8089
+ if (!row) {
8090
+ scalars[path] = field.overrideKey ?? key;
8091
+ continue;
8092
+ }
8093
+ const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
8094
+ if (!address) return null;
8095
+ const table = tables[row[1]] ??= { key: address[1], columns: {} };
8096
+ if (table.key !== address[1]) return null;
8097
+ table.columns[row[2]] = address[2];
8098
+ }
8099
+ return { scalars, tables };
8100
+ }
8101
+ function carriedStyles(content) {
8102
+ const blocks = [];
8103
+ for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
8104
+ const attributes = match[1] ?? "";
8105
+ if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
8106
+ if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
8107
+ blocks.push(match[0]);
8108
+ }
8109
+ return blocks.join("\n");
8110
+ }
7973
8111
  function referencedNames2(markup) {
7974
8112
  const out = /* @__PURE__ */ new Set();
7975
8113
  for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);