@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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `--componentize` extracts FLAT field families (`wrap-h2-latest`, whose key is `h2_latest`), which were
8
+ refused as `PROP_MAP_INCONSISTENT`. The section reads its copy through the new helper `bcmsSectionPaths`
9
+ and addresses instance copy by the override key the plan names (`overrideKey`, including repeater rows).
10
+ - An Astro section carries the page's scoped plain-CSS `<style>` blocks, so it renders styled on its own —
11
+ for every newly extracted section, dotted families included (Astro scopes styles per file, so page CSS
12
+ never reached a moved section). `is:global`, `define:vars`, `lang="…"` and `@import`/`@use` blocks stay on
13
+ the page.
14
+ - The componentize receipt lists `componentFiles` (`{ slug, path, export }`): submit it with the MCP tool
15
+ `submit_componentize_receipt` so every extracted section is recorded as its component's source and the
16
+ dashboard's Output can build, validate and render it.
17
+ - Helper v7. A v6 helper is recognised and upgraded in place.
18
+
3
19
  ## 0.10.0
4
20
 
5
21
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -4142,7 +4142,7 @@ function bindImages(ctx, targets) {
4142
4142
 
4143
4143
  // src/helper.ts
4144
4144
  import { createHash as createHash2 } from "crypto";
4145
- var HELPER_VERSION = 6;
4145
+ var HELPER_VERSION = 7;
4146
4146
  var HEADER = "// @bettercms-ai/convert helper v";
4147
4147
  var HELPER_PATH = Object.fromEntries(
4148
4148
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4335,9 +4335,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
4335
4335
  * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4336
4336
  * instance on the next, and a repository does not have to be re-cut when its copy moves.
4337
4337
  */
4338
+ /** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
4339
+ export type BcmsPathMap = {
4340
+ scalars?: Record<string, string>;
4341
+ tables?: Record<string, { key: string; columns: Record<string, string> }>;
4342
+ };
4343
+
4344
+ /**
4345
+ * The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
4346
+ *
4347
+ * \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
4348
+ * copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
4349
+ * markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
4350
+ *
4351
+ * - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
4352
+ * column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
4353
+ * - else, for a placement bound to a group, the page value at the path re-based onto that group;
4354
+ * - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
4355
+ * \`bcmsSection\` synthesizes no group for it.
4356
+ *
4357
+ * Every other page key passes through, so reads outside the family (a shared link) keep working.
4358
+ */
4359
+ export function bcmsSectionPaths(
4360
+ page: unknown,
4361
+ bind: string | undefined,
4362
+ overrides: unknown,
4363
+ prefix: string,
4364
+ map: BcmsPathMap,
4365
+ ): Record<string, unknown> {
4366
+ const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
4367
+ const own = isRecord(overrides) ? overrides : {};
4368
+ const separator = prefix.slice(-1);
4369
+ const fromPage = (path: string): unknown =>
4370
+ bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
4371
+ for (const [path, key] of Object.entries(map.scalars ?? {})) {
4372
+ const value = own[key];
4373
+ out[path] = value !== undefined && value !== null ? value : fromPage(path);
4374
+ }
4375
+ for (const [path, table] of Object.entries(map.tables ?? {})) {
4376
+ const held = own[table.key];
4377
+ const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
4378
+ if (!Array.isArray(rows)) {
4379
+ out[path] = fromPage(path);
4380
+ continue;
4381
+ }
4382
+ const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
4383
+ out[path] = rows.map((row) => {
4384
+ if (!isRecord(row)) return row;
4385
+ const spelled: Record<string, unknown> = {};
4386
+ for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
4387
+ return spelled;
4388
+ });
4389
+ }
4390
+ return out;
4391
+ }
4392
+
4338
4393
  export function bcmsFields(
4339
4394
  placement: { blockId?: string; bind?: string; overrides?: unknown },
4340
4395
  pathPrefix: string,
4396
+ map?: BcmsPathMap,
4341
4397
  ): (path: string) => string {
4342
4398
  const id = placement.blockId;
4343
4399
  const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
@@ -4350,6 +4406,24 @@ export function bcmsFields(
4350
4406
  * so prepending one would invent an address rather than correct it.
4351
4407
  */
4352
4408
  const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
4409
+ if (map) {
4410
+ /**
4411
+ * A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
4412
+ * plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
4413
+ * separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
4414
+ */
4415
+ const separator = pathPrefix.slice(-1);
4416
+ return (path: string): string => {
4417
+ if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
4418
+ if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
4419
+ const scalar = map.scalars?.[path];
4420
+ if (scalar !== undefined) return id + "__overrides." + scalar;
4421
+ const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
4422
+ const table = row ? map.tables?.[row[1]] : undefined;
4423
+ const column = row && table ? table.columns[row[3]] : undefined;
4424
+ return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
4425
+ };
4426
+ }
4353
4427
  return (path: string): string => {
4354
4428
  // A path outside the group this section was cut from is not one this placement re-addresses:
4355
4429
  // it names a key the instance does not hold and a group this placement is not bound to.
@@ -4366,21 +4440,24 @@ var HISTORICAL = {
4366
4440
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4367
4441
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4368
4442
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4369
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4443
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4444
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4370
4445
  },
4371
4446
  "src/lib/bcms-content.ts": {
4372
4447
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4373
4448
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4374
4449
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4375
4450
  4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4376
- 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4451
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4452
+ 6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
4377
4453
  },
4378
4454
  "composables/bcms-content.ts": {
4379
4455
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4380
4456
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4381
4457
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4382
4458
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4383
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4459
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4460
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4384
4461
  }
4385
4462
  };
4386
4463
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -6869,6 +6946,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
6869
6946
  return held;
6870
6947
  };
6871
6948
  const fresh = /* @__PURE__ */ new Set();
6949
+ const freshFiles = /* @__PURE__ */ new Map();
6872
6950
  const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
6873
6951
  const placementOf = (slug, section) => {
6874
6952
  const bind = bindOf(section);
@@ -7196,6 +7274,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7196
7274
  }
7197
7275
  extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
7198
7276
  fresh.add(resolved.slug);
7277
+ freshFiles.set(resolved.slug, path);
7199
7278
  addFile(outFiles, contents, {
7200
7279
  path,
7201
7280
  operation: existing === void 0 ? "add" : "modify",
@@ -7281,6 +7360,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7281
7360
  sections,
7282
7361
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
7283
7362
  components: [...fresh].sort(),
7363
+ componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
7284
7364
  planPending,
7285
7365
  notes
7286
7366
  }
@@ -7460,7 +7540,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7460
7540
  for (const section of sections) {
7461
7541
  const paths = fieldPaths(section);
7462
7542
  const literals = new Set(
7463
- briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7543
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7464
7544
  );
7465
7545
  const hits = /* @__PURE__ */ new Map();
7466
7546
  for (const source of sources) {
@@ -7471,7 +7551,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7471
7551
  const found = /* @__PURE__ */ new Map();
7472
7552
  for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
7473
7553
  if (declaration2.scope !== "page") continue;
7474
- if (groupOf(declaration2.path) !== section.groupKey) continue;
7554
+ if (!inSection(section, declaration2.path)) continue;
7475
7555
  const placed2 = elements.get(declaration2.at);
7476
7556
  if (placed2) found.set(declaration2.at, placed2);
7477
7557
  }
@@ -7496,6 +7576,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7496
7576
  return { placed, misses };
7497
7577
  }
7498
7578
  var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
7579
+ var inSection = (section, path) => {
7580
+ if (groupOf(path) === section.groupKey) return true;
7581
+ const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
7582
+ const target = rowless(path);
7583
+ return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
7584
+ };
7499
7585
  var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
7500
7586
  function elementIndex(roots) {
7501
7587
  const out = /* @__PURE__ */ new Map();
@@ -7598,6 +7684,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7598
7684
  ...addresses.opaque
7599
7685
  ];
7600
7686
  if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
7687
+ const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
7688
+ const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
7689
+ if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
7601
7690
  const cut = content.slice(resolved.root.start, resolved.root.end);
7602
7691
  const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
7603
7692
  const introduced = allocateNames(reserved, [
@@ -7616,7 +7705,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7616
7705
  const helpers = [
7617
7706
  .../* @__PURE__ */ new Set([
7618
7707
  ...helperNames.map((h) => alias(h.name, h.local)),
7619
- "bcmsSection",
7708
+ flat3 ? "bcmsSectionPaths" : "bcmsSection",
7620
7709
  ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
7621
7710
  ])
7622
7711
  ].sort();
@@ -7646,9 +7735,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7646
7735
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
7647
7736
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
7648
7737
  ` : "";
7649
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
7650
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
7738
+ const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
7739
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
7651
7740
  const declared = [copy, field].filter(Boolean).join("\n");
7741
+ const styles = dialect === "astro" ? carriedStyles(content) : "";
7652
7742
  if (dialect === "astro") {
7653
7743
  return { free: [], duplicates: [], unconvertible: [], content: `---
7654
7744
  ${SECTION_MARKER}
@@ -7668,7 +7758,9 @@ const { blockId, bind, overrides, page } = Astro.props;
7668
7758
  ${carriedDeclarations}${declared}
7669
7759
  ---
7670
7760
  ${markup}
7671
- ` };
7761
+ ${styles ? `
7762
+ ${styles}
7763
+ ` : ""}` };
7672
7764
  }
7673
7765
  return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
7674
7766
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
@@ -7942,6 +8034,9 @@ import { bcmsFields } from "${helper}";`);
7942
8034
  return { files, refused };
7943
8035
  }
7944
8036
  function propPrefix(section) {
8037
+ return exactPrefix(section) ?? flatPrefix(section);
8038
+ }
8039
+ function exactPrefix(section) {
7945
8040
  const prefixes = /* @__PURE__ */ new Set();
7946
8041
  for (const field of section.fields ?? []) {
7947
8042
  const { path, key } = field;
@@ -7952,6 +8047,49 @@ function propPrefix(section) {
7952
8047
  if (prefixes.size > 1) return null;
7953
8048
  return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
7954
8049
  }
8050
+ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
8051
+ function flatPrefix(section) {
8052
+ const prefixes = /* @__PURE__ */ new Set();
8053
+ let informative = false;
8054
+ for (const field of section.fields ?? []) {
8055
+ const { path, key } = field;
8056
+ if (typeof path !== "string" || typeof key !== "string") continue;
8057
+ informative = true;
8058
+ const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8059
+ if (!prefix) return null;
8060
+ prefixes.add(prefix);
8061
+ }
8062
+ return informative && prefixes.size === 1 ? [...prefixes][0] : null;
8063
+ }
8064
+ function flatPathMap(section, prefix) {
8065
+ const scalars = {};
8066
+ const tables = {};
8067
+ for (const field of section.fields ?? []) {
8068
+ const { path, key } = field;
8069
+ if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8070
+ const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8071
+ if (!row) {
8072
+ scalars[path] = field.overrideKey ?? key;
8073
+ continue;
8074
+ }
8075
+ const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
8076
+ if (!address) return null;
8077
+ const table = tables[row[1]] ??= { key: address[1], columns: {} };
8078
+ if (table.key !== address[1]) return null;
8079
+ table.columns[row[2]] = address[2];
8080
+ }
8081
+ return { scalars, tables };
8082
+ }
8083
+ function carriedStyles(content) {
8084
+ const blocks = [];
8085
+ for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
8086
+ const attributes = match[1] ?? "";
8087
+ if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
8088
+ if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
8089
+ blocks.push(match[0]);
8090
+ }
8091
+ return blocks.join("\n");
8092
+ }
7955
8093
  function referencedNames2(markup) {
7956
8094
  const out = /* @__PURE__ */ new Set();
7957
8095
  for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
@@ -9288,7 +9426,7 @@ function emitStubs2(dialects, slugs, contents, out) {
9288
9426
  // package.json
9289
9427
  var package_default = {
9290
9428
  name: "@bettercms-ai/convert",
9291
- version: "0.10.0",
9429
+ version: "0.11.0",
9292
9430
  type: "module",
9293
9431
  description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
9294
9432
  bin: {