@cmssy/cli 16.9.0 → 16.10.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.
@@ -1,7 +1,5 @@
1
1
  import {
2
- CmssyBlock,
3
- buildBlockContext,
4
- buildBlockMap,
2
+ CmssyLayoutRegion,
5
3
  type CmssyBlockPage,
6
4
  type CmssyLayoutGroup,
7
5
  } from "@cmssy/react";
@@ -50,36 +48,17 @@ export default function LayoutSlot({
50
48
  );
51
49
  }
52
50
 
53
- const group = groups.find((candidate) => candidate.region === region);
54
- if (!group) return null;
55
-
56
- const blockMap = buildBlockMap(blocks);
57
- const context = buildBlockContext(
58
- locale,
59
- defaultLocale,
60
- enabledLocales,
61
- false,
62
- undefined,
63
- { page },
64
- );
65
-
66
51
  return (
67
- <>
68
- {[...group.blocks]
69
- .filter((block) => block.isActive !== false)
70
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
71
- .map((block) => (
72
- <CmssyBlock
73
- key={block.id}
74
- block={block}
75
- blockMap={blockMap}
76
- locale={locale}
77
- defaultLocale={defaultLocale}
78
- context={context}
79
- resolvedContent={resolvedContent?.[block.id]}
80
- data={data?.[block.id]}
81
- />
82
- ))}
83
- </>
52
+ <CmssyLayoutRegion
53
+ groups={groups}
54
+ region={region}
55
+ blocks={blocks}
56
+ page={page}
57
+ locale={locale}
58
+ defaultLocale={defaultLocale}
59
+ enabledLocales={enabledLocales}
60
+ blockData={data}
61
+ blockContent={resolvedContent}
62
+ />
84
63
  );
85
64
  }
@@ -1,4 +1,4 @@
1
- import { CmssyBlock, buildBlockMap, buildBlockContext } from "@cmssy/react";
1
+ import { CmssyBlocks } from "@cmssy/react";
2
2
  import type { CmssyPageData } from "@cmssy/core";
3
3
  import { blocks } from "../cmssy/blocks";
4
4
 
@@ -17,23 +17,15 @@ export function Blocks({
17
17
  blockData: Record<string, unknown>;
18
18
  blockContent: Record<string, Record<string, unknown>>;
19
19
  }) {
20
- const blockMap = buildBlockMap(blocks);
21
- const context = buildBlockContext(locale, defaultLocale, enabledLocales);
22
-
23
20
  return (
24
- <>
25
- {(page.blocks ?? []).map((block) => (
26
- <CmssyBlock
27
- key={block.id}
28
- block={block}
29
- blockMap={blockMap}
30
- locale={locale}
31
- defaultLocale={defaultLocale}
32
- context={context}
33
- resolvedContent={blockContent[block.id]}
34
- data={blockData[block.id]}
35
- />
36
- ))}
37
- </>
21
+ <CmssyBlocks
22
+ page={page}
23
+ blocks={blocks}
24
+ locale={locale}
25
+ defaultLocale={defaultLocale}
26
+ enabledLocales={enabledLocales}
27
+ blockData={blockData}
28
+ blockContent={blockContent}
29
+ />
38
30
  );
39
31
  }
@@ -1,7 +1,5 @@
1
1
  import {
2
- CmssyBlock,
3
- buildBlockContext,
4
- buildBlockMap,
2
+ CmssyLayoutRegion,
5
3
  type CmssyBlockPage,
6
4
  type CmssyLayoutGroup,
7
5
  } from "@cmssy/react";
@@ -50,36 +48,17 @@ export function LayoutSlot({
50
48
  );
51
49
  }
52
50
 
53
- const group = groups.find((candidate) => candidate.region === region);
54
- if (!group) return null;
55
-
56
- const blockMap = buildBlockMap(blocks);
57
- const context = buildBlockContext(
58
- locale,
59
- defaultLocale,
60
- enabledLocales,
61
- false,
62
- undefined,
63
- { page },
64
- );
65
-
66
51
  return (
67
- <>
68
- {[...group.blocks]
69
- .filter((block) => block.isActive !== false)
70
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
71
- .map((block) => (
72
- <CmssyBlock
73
- key={block.id}
74
- block={block}
75
- blockMap={blockMap}
76
- locale={locale}
77
- defaultLocale={defaultLocale}
78
- context={context}
79
- resolvedContent={resolvedContent?.[block.id]}
80
- data={data?.[block.id]}
81
- />
82
- ))}
83
- </>
52
+ <CmssyLayoutRegion
53
+ groups={groups}
54
+ region={region}
55
+ blocks={blocks}
56
+ page={page}
57
+ locale={locale}
58
+ defaultLocale={defaultLocale}
59
+ enabledLocales={enabledLocales}
60
+ blockData={data}
61
+ blockContent={resolvedContent}
62
+ />
84
63
  );
85
64
  }
@@ -1,7 +1,5 @@
1
1
  import {
2
- CmssyBlock,
3
- buildBlockContext,
4
- buildBlockMap,
2
+ CmssyBlocks,
5
3
  resolveEditorBlockData,
6
4
  resolveEditorLayoutBlockData,
7
5
  type EditorBlockData,
@@ -131,25 +129,19 @@ export default function CmssyPage({ loaderData }: Route.ComponentProps) {
131
129
  </>
132
130
  );
133
131
 
134
- const blockMap = buildBlockMap(blocks);
135
- const context = buildBlockContext(locale, defaultLocale, enabledLocales);
136
-
137
132
  return (
138
133
  <>
139
134
  {slot("header")}
140
135
  <main>
141
- {(page.blocks ?? []).map((block) => (
142
- <CmssyBlock
143
- key={block.id}
144
- block={block}
145
- blockMap={blockMap}
146
- locale={locale}
147
- defaultLocale={defaultLocale}
148
- context={context}
149
- resolvedContent={blockContent[block.id]}
150
- data={blockData[block.id]}
151
- />
152
- ))}
136
+ <CmssyBlocks
137
+ page={page}
138
+ blocks={blocks}
139
+ locale={locale}
140
+ defaultLocale={defaultLocale}
141
+ enabledLocales={enabledLocales}
142
+ blockData={blockData}
143
+ blockContent={blockContent}
144
+ />
153
145
  </main>
154
146
  {slot("footer")}
155
147
  </>
package/dist/index.js CHANGED
@@ -23,10 +23,12 @@ var NEEDS_PAGES_EDIT_HINT = {
23
23
  };
24
24
  var CliError = class extends Error {
25
25
  fix;
26
- constructor(message, fix) {
26
+ code;
27
+ constructor(message, fix, code) {
27
28
  super(message);
28
29
  this.name = "CliError";
29
30
  this.fix = fix;
31
+ this.code = code;
30
32
  }
31
33
  };
32
34
  function isDenied(errors, status) {
@@ -69,7 +71,9 @@ async function adminGraphql(query, variables, options) {
69
71
  }
70
72
  if (errors.length > 0) {
71
73
  throw new CliError(
72
- `the cmssy API rejected the request - ${errors.map((error) => error.message ?? "GraphQL error").join("; ")}`
74
+ `the cmssy API rejected the request - ${errors.map((error) => error.message ?? "GraphQL error").join("; ")}`,
75
+ void 0,
76
+ errors.find((error) => error.extensions?.code)?.extensions?.code
73
77
  );
74
78
  }
75
79
  if (!response.ok || envelope?.data == null) {
@@ -102,9 +106,9 @@ var UPDATE_PREVIEW_URL_MUTATION = `mutation CliSetPreviewUrl($input: UpdateSiteC
102
106
  }
103
107
  }
104
108
  }`;
105
- var SAVE_BLOCK_MANIFEST_MUTATION = `mutation CliSaveBlockManifest($blocks: JSON!, $regions: JSON) {
109
+ var SAVE_BLOCK_MANIFEST_MUTATION = `mutation CliSaveBlockManifest($blocks: JSON!, $regions: JSON, $expectedHash: String, $onlyIfAbsent: Boolean, $allowLossy: Boolean) {
106
110
  blockManifest {
107
- save(blocks: $blocks, regions: $regions) {
111
+ save(blocks: $blocks, regions: $regions, expectedHash: $expectedHash, onlyIfAbsent: $onlyIfAbsent, allowLossy: $allowLossy) {
108
112
  hash
109
113
  updatedAt
110
114
  }
@@ -147,12 +151,42 @@ async function fetchBlockManifestHash(options) {
147
151
  );
148
152
  return data.blockManifest.get?.hash ?? null;
149
153
  }
150
- async function saveBlockManifest(manifest, options) {
151
- const data = await adminGraphql(SAVE_BLOCK_MANIFEST_MUTATION, manifest, {
154
+ async function saveBlockManifest(manifest, options, guard = {}) {
155
+ const data = await adminGraphql(
156
+ SAVE_BLOCK_MANIFEST_MUTATION,
157
+ { ...manifest, ...guard },
158
+ {
159
+ ...options,
160
+ denied: NEEDS_MANIFEST_WRITE_HINT
161
+ }
162
+ );
163
+ return data.blockManifest.save;
164
+ }
165
+ var BLOCK_MANIFEST_IMPACT_QUERY = `query CliBlockManifestImpact($blocks: JSON!, $regions: JSON) {
166
+ blockManifest {
167
+ impact(blocks: $blocks, regions: $regions) {
168
+ hash
169
+ activeHash
170
+ unchanged
171
+ addedTypes
172
+ removedTypes { type pages publishedPages }
173
+ changedTypes
174
+ removedFields { type fields }
175
+ removedRegions
176
+ changedRegions
177
+ moves
178
+ lossyMoves
179
+ documents
180
+ heldDocuments
181
+ }
182
+ }
183
+ }`;
184
+ async function fetchBlockManifestImpact(manifest, options) {
185
+ const data = await adminGraphql(BLOCK_MANIFEST_IMPACT_QUERY, manifest, {
152
186
  ...options,
153
187
  denied: NEEDS_MANIFEST_WRITE_HINT
154
188
  });
155
- return data.blockManifest.save;
189
+ return data.blockManifest.impact;
156
190
  }
157
191
 
158
192
  // src/format.ts
@@ -1496,7 +1530,8 @@ var CONFIG_CANDIDATES = [
1496
1530
  ];
1497
1531
  var SYNC_MANIFEST_USAGE = [
1498
1532
  " cmssy sync-manifest [--blocks <path>] [--config <path>] [--token <cs_...>]",
1499
- " [--org <slug>] [--workspace <slug>] [--dry-run] [--help]"
1533
+ " [--org <slug>] [--workspace <slug>] [--dry-run]",
1534
+ " [--allow-lossy] [--help]"
1500
1535
  ];
1501
1536
  function hasBlocksModule(cwd) {
1502
1537
  return BLOCKS_CANDIDATES.some(
@@ -1612,6 +1647,53 @@ function describe(manifest) {
1612
1647
  lines.push(` regions: ${regions.join(", ") || "-"}`);
1613
1648
  return lines;
1614
1649
  }
1650
+ function plural(count, noun) {
1651
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
1652
+ }
1653
+ function describeImpact(impact) {
1654
+ const lines = [];
1655
+ for (const removed of impact.removedTypes) {
1656
+ lines.push(
1657
+ removed.pages === 0 ? ` removes \`${removed.type}\`, not used on any page` : ` removes \`${removed.type}\`, used on ${plural(removed.pages, "page")} (${removed.publishedPages} published)`
1658
+ );
1659
+ }
1660
+ for (const removed of impact.removedFields) {
1661
+ lines.push(
1662
+ ` removes fields from \`${removed.type}\`: ${removed.fields.join(", ")}`
1663
+ );
1664
+ }
1665
+ const withRemovedFields = new Set(
1666
+ impact.removedFields.map((removed) => removed.type)
1667
+ );
1668
+ for (const type of impact.changedTypes) {
1669
+ if (withRemovedFields.has(type)) continue;
1670
+ lines.push(
1671
+ ` reshapes \`${type}\` - stored values may no longer match its fields`
1672
+ );
1673
+ }
1674
+ if (impact.addedTypes.length > 0) {
1675
+ lines.push(
1676
+ ` adds ${impact.addedTypes.map((type) => `\`${type}\``).join(", ")}`
1677
+ );
1678
+ }
1679
+ for (const region of impact.removedRegions) {
1680
+ lines.push(` removes region \`${region}\``);
1681
+ }
1682
+ for (const region of impact.changedRegions) {
1683
+ lines.push(` reshapes the settings of region \`${region}\``);
1684
+ }
1685
+ if (impact.moves > 0) {
1686
+ lines.push(
1687
+ ` moves ${plural(impact.moves, "stored value")} in ${plural(impact.documents, "document")}`
1688
+ );
1689
+ }
1690
+ if (impact.lossyMoves > 0) {
1691
+ lines.push(
1692
+ ` ${plural(impact.lossyMoves, "move")} would drop stored values (a translation or a differing copy) in ${plural(impact.heldDocuments, "document")}`
1693
+ );
1694
+ }
1695
+ return lines;
1696
+ }
1615
1697
  async function collectManifest(options, deps) {
1616
1698
  const load = deps.load ?? loadSiteModule;
1617
1699
  const blocksPath = findModule(
@@ -1659,11 +1741,17 @@ async function runSyncManifest(options, deps) {
1659
1741
  loadEnvFiles(deps.cwd, deps.env);
1660
1742
  const collected = await collectManifest(options, deps);
1661
1743
  const { manifest } = collected;
1662
- if (options.dryRun) {
1663
- deps.log(JSON.stringify(manifest, null, 2));
1744
+ const count = `${plural(manifest.blocks.length, "block")}${manifest.regions ? ` and ${plural(manifest.regions.length, "region")}` : ""}`;
1745
+ const sources = `(${collected.blocksPath}, ${collected.configPath})`;
1746
+ const token = options.dryRun && !options.token?.trim() && !deps.env.CMSSY_API_TOKEN?.trim() ? null : resolveToken(options, deps.env);
1747
+ if (token === null) {
1748
+ deps.log(`cmssy: dry run - ${count} ${sources}`);
1749
+ for (const line of describe(manifest)) deps.log(line);
1750
+ deps.log(
1751
+ " impact not checked - set CMSSY_API_TOKEN to compare with the workspace"
1752
+ );
1664
1753
  return 0;
1665
1754
  }
1666
- const token = resolveToken(options, deps.env);
1667
1755
  const org = resolveSlug(
1668
1756
  options.org,
1669
1757
  collected.org,
@@ -1689,16 +1777,49 @@ async function runSyncManifest(options, deps) {
1689
1777
  slug
1690
1778
  );
1691
1779
  const scoped = { ...request2, workspaceId: workspace.id };
1692
- const before = await fetchBlockManifestHash(scoped);
1693
- const saved = await saveBlockManifest(manifest, scoped);
1694
- const unchanged = before === saved.hash;
1695
- const count = `${manifest.blocks.length} block${manifest.blocks.length === 1 ? "" : "s"}${manifest.regions ? ` and ${manifest.regions.length} region${manifest.regions.length === 1 ? "" : "s"}` : ""}`;
1780
+ const impact = await fetchBlockManifestImpact(manifest, scoped);
1781
+ if (impact.unchanged) {
1782
+ deps.log(
1783
+ `cmssy: ${org}/${slug} already has this manifest - ${count} unchanged ${sources}`
1784
+ );
1785
+ for (const line of describe(manifest)) deps.log(line);
1786
+ deps.log(` manifest ${impact.hash.slice(0, 12)}`);
1787
+ return 0;
1788
+ }
1789
+ const effects = describeImpact(impact);
1696
1790
  deps.log(
1697
- unchanged ? `cmssy: ${org}/${slug} already has this manifest - ${count} unchanged (${collected.blocksPath}, ${collected.configPath})` : `cmssy: pushed ${count} to ${org}/${slug} (${collected.blocksPath}, ${collected.configPath})`
1791
+ options.dryRun ? `cmssy: dry run - pushing ${count} to ${org}/${slug} would ${effects.length > 0 ? "change:" : "change nothing stored"} ${sources}` : `cmssy: ${org}/${slug} - ${count} ${sources}`
1698
1792
  );
1793
+ for (const line of effects) deps.log(line);
1699
1794
  for (const line of describe(manifest)) deps.log(line);
1795
+ if (impact.lossyMoves > 0 && !options.allowLossy) {
1796
+ deps.log(
1797
+ `cmssy: not pushed - rerun with --allow-lossy to accept dropping those values`
1798
+ );
1799
+ return 1;
1800
+ }
1801
+ if (options.dryRun) {
1802
+ deps.log("cmssy: dry run - nothing pushed");
1803
+ return 0;
1804
+ }
1805
+ let saved;
1806
+ try {
1807
+ saved = await saveBlockManifest(manifest, scoped, {
1808
+ ...impact.activeHash === null ? { onlyIfAbsent: true } : { expectedHash: impact.activeHash },
1809
+ ...options.allowLossy ? { allowLossy: true } : {}
1810
+ });
1811
+ } catch (error) {
1812
+ if (error instanceof CliError && error.code === "CONFLICT") {
1813
+ throw new CliError(
1814
+ `the block manifest of ${org}/${slug} changed while this ran - nothing pushed`,
1815
+ "run cmssy sync-manifest again to compare with the new manifest"
1816
+ );
1817
+ }
1818
+ throw error;
1819
+ }
1820
+ deps.log(`cmssy: pushed ${count} to ${org}/${slug}`);
1700
1821
  deps.log(
1701
- unchanged ? ` manifest ${saved.hash.slice(0, 12)} - last updated ${saved.updatedAt}` : ` manifest ${saved.hash.slice(0, 12)} - updated ${saved.updatedAt}`
1822
+ ` manifest ${saved.hash.slice(0, 12)} - updated ${saved.updatedAt}`
1702
1823
  );
1703
1824
  return 0;
1704
1825
  } catch (error) {
@@ -1712,6 +1833,7 @@ async function runSyncManifest(options, deps) {
1712
1833
  }
1713
1834
 
1714
1835
  // src/link.ts
1836
+ var MANIFEST_KEPT = "block manifest left unchanged - the workspace already has one, and link never replaces it from a local checkout; run cmssy sync-manifest from the code your site is deployed from to replace it";
1715
1837
  function resolveToken2(options, deps) {
1716
1838
  const token = options.token?.trim() || deps.env.CMSSY_API_TOKEN?.trim();
1717
1839
  if (!token) {
@@ -1855,15 +1977,25 @@ function resolvePreviewUrl(options) {
1855
1977
  }
1856
1978
  async function pushBlockManifest(workspace, admin, deps) {
1857
1979
  if (!hasBlocksModule(deps.cwd)) return null;
1980
+ const scoped = { ...admin, workspaceId: workspace.id };
1858
1981
  try {
1982
+ if (await fetchBlockManifestHash(scoped) !== null) {
1983
+ return {
1984
+ status: "unknown",
1985
+ message: MANIFEST_KEPT
1986
+ };
1987
+ }
1859
1988
  const { manifest, blocksPath } = await collectManifest({}, deps);
1860
- await saveBlockManifest(manifest, { ...admin, workspaceId: workspace.id });
1989
+ await saveBlockManifest(manifest, scoped, { onlyIfAbsent: true });
1861
1990
  const count = manifest.blocks.length;
1862
1991
  return {
1863
1992
  status: "ok",
1864
1993
  message: `pushed the block manifest from ${blocksPath} (${count} block${count === 1 ? "" : "s"}) - the editor palette knows your blocks before the first deploy`
1865
1994
  };
1866
1995
  } catch (error) {
1996
+ if (error instanceof CliError && error.code === "CONFLICT") {
1997
+ return { status: "unknown", message: MANIFEST_KEPT };
1998
+ }
1867
1999
  if (error instanceof CliError) {
1868
2000
  const fix = error.fix ?? "run cmssy sync-manifest once the blocks module loads";
1869
2001
  return {
@@ -2572,7 +2704,8 @@ async function main() {
2572
2704
  token: flagValue(args, "--token"),
2573
2705
  org: flagValue(args, "--org"),
2574
2706
  workspace: flagValue(args, "--workspace"),
2575
- dryRun: hasFlag(args, "--dry-run")
2707
+ dryRun: hasFlag(args, "--dry-run"),
2708
+ allowLossy: hasFlag(args, "--allow-lossy")
2576
2709
  },
2577
2710
  {
2578
2711
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "16.9.0",
3
+ "version": "16.10.0",
4
4
  "description": "The cmssy CLI: `cmssy init` wires cmssy into an existing Next.js, Astro or React Router app; `cmssy link` connects it to a workspace and verifies the editor wiring.",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "esbuild": "^0.27.7",
31
- "@cmssy/core": "16.9.0"
31
+ "@cmssy/core": "16.10.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",