@cmssy/cli 16.9.1 → 16.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.
@@ -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,
@@ -1683,22 +1771,58 @@ async function runSyncManifest(options, deps) {
1683
1771
  apiUrl: deps.env.CMSSY_API_URL,
1684
1772
  fetch: deps.fetch
1685
1773
  };
1774
+ deps.log(
1775
+ `cmssy: ${org}/${slug} at ${request2.apiUrl?.trim() || DEFAULT_ADMIN_API_URL}`
1776
+ );
1686
1777
  const workspace = matchWorkspace(
1687
1778
  await fetchMyWorkspaces(request2),
1688
1779
  org,
1689
1780
  slug
1690
1781
  );
1691
1782
  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"}` : ""}`;
1783
+ const impact = await fetchBlockManifestImpact(manifest, scoped);
1784
+ if (impact.unchanged) {
1785
+ deps.log(
1786
+ `cmssy: ${org}/${slug} already has this manifest - ${count} unchanged ${sources}`
1787
+ );
1788
+ for (const line of describe(manifest)) deps.log(line);
1789
+ deps.log(` manifest ${impact.hash.slice(0, 12)}`);
1790
+ return 0;
1791
+ }
1792
+ const effects = describeImpact(impact);
1696
1793
  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})`
1794
+ 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
1795
  );
1796
+ for (const line of effects) deps.log(line);
1699
1797
  for (const line of describe(manifest)) deps.log(line);
1798
+ if (impact.lossyMoves > 0 && !options.allowLossy) {
1799
+ deps.log(
1800
+ `cmssy: not pushed - rerun with --allow-lossy to accept dropping those values`
1801
+ );
1802
+ return 1;
1803
+ }
1804
+ if (options.dryRun) {
1805
+ deps.log("cmssy: dry run - nothing pushed");
1806
+ return 0;
1807
+ }
1808
+ let saved;
1809
+ try {
1810
+ saved = await saveBlockManifest(manifest, scoped, {
1811
+ ...impact.activeHash === null ? { onlyIfAbsent: true } : { expectedHash: impact.activeHash },
1812
+ ...options.allowLossy ? { allowLossy: true } : {}
1813
+ });
1814
+ } catch (error) {
1815
+ if (error instanceof CliError && error.code === "CONFLICT") {
1816
+ throw new CliError(
1817
+ `the block manifest of ${org}/${slug} changed while this ran - nothing pushed`,
1818
+ "run cmssy sync-manifest again to compare with the new manifest"
1819
+ );
1820
+ }
1821
+ throw error;
1822
+ }
1823
+ deps.log(`cmssy: pushed ${count} to ${org}/${slug}`);
1700
1824
  deps.log(
1701
- unchanged ? ` manifest ${saved.hash.slice(0, 12)} - last updated ${saved.updatedAt}` : ` manifest ${saved.hash.slice(0, 12)} - updated ${saved.updatedAt}`
1825
+ ` manifest ${saved.hash.slice(0, 12)} - updated ${saved.updatedAt}`
1702
1826
  );
1703
1827
  return 0;
1704
1828
  } catch (error) {
@@ -1712,6 +1836,7 @@ async function runSyncManifest(options, deps) {
1712
1836
  }
1713
1837
 
1714
1838
  // src/link.ts
1839
+ 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
1840
  function resolveToken2(options, deps) {
1716
1841
  const token = options.token?.trim() || deps.env.CMSSY_API_TOKEN?.trim();
1717
1842
  if (!token) {
@@ -1855,15 +1980,25 @@ function resolvePreviewUrl(options) {
1855
1980
  }
1856
1981
  async function pushBlockManifest(workspace, admin, deps) {
1857
1982
  if (!hasBlocksModule(deps.cwd)) return null;
1983
+ const scoped = { ...admin, workspaceId: workspace.id };
1858
1984
  try {
1985
+ if (await fetchBlockManifestHash(scoped) !== null) {
1986
+ return {
1987
+ status: "unknown",
1988
+ message: MANIFEST_KEPT
1989
+ };
1990
+ }
1859
1991
  const { manifest, blocksPath } = await collectManifest({}, deps);
1860
- await saveBlockManifest(manifest, { ...admin, workspaceId: workspace.id });
1992
+ await saveBlockManifest(manifest, scoped, { onlyIfAbsent: true });
1861
1993
  const count = manifest.blocks.length;
1862
1994
  return {
1863
1995
  status: "ok",
1864
1996
  message: `pushed the block manifest from ${blocksPath} (${count} block${count === 1 ? "" : "s"}) - the editor palette knows your blocks before the first deploy`
1865
1997
  };
1866
1998
  } catch (error) {
1999
+ if (error instanceof CliError && error.code === "CONFLICT") {
2000
+ return { status: "unknown", message: MANIFEST_KEPT };
2001
+ }
1867
2002
  if (error instanceof CliError) {
1868
2003
  const fix = error.fix ?? "run cmssy sync-manifest once the blocks module loads";
1869
2004
  return {
@@ -2572,7 +2707,8 @@ async function main() {
2572
2707
  token: flagValue(args, "--token"),
2573
2708
  org: flagValue(args, "--org"),
2574
2709
  workspace: flagValue(args, "--workspace"),
2575
- dryRun: hasFlag(args, "--dry-run")
2710
+ dryRun: hasFlag(args, "--dry-run"),
2711
+ allowLossy: hasFlag(args, "--allow-lossy")
2576
2712
  },
2577
2713
  {
2578
2714
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "16.9.1",
3
+ "version": "16.11.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.1"
31
+ "@cmssy/core": "16.11.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",