@cmssy/cli 10.6.1 → 10.8.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.
@@ -0,0 +1,75 @@
1
+ import {
2
+ CmssyBlock,
3
+ buildBlockContext,
4
+ buildBlockMap,
5
+ type CmssyLayoutGroup,
6
+ } from "@cmssy/react";
7
+ import { CmssyLazyLayout } from "@cmssy/react/client";
8
+ import { blocks } from "./blocks";
9
+
10
+ export interface LayoutSlotProps {
11
+ groups: CmssyLayoutGroup[];
12
+ position: "header" | "footer";
13
+ locale: string;
14
+ defaultLocale: string;
15
+ enabledLocales: string[];
16
+ /** Set on a VERIFIED editor request; the loader reports it as `isEdit`. */
17
+ edit?: { editorOrigin: string | string[] };
18
+ }
19
+
20
+ /**
21
+ * The header and footer, which are layout blocks rather than markup you own.
22
+ *
23
+ * A React island: Astro renders it on the server for visitors (no client JS),
24
+ * and with `client:load` on the edit route, where the bridge needs the browser.
25
+ *
26
+ * Two renders, one per mode, and the difference is the point: server-rendered
27
+ * for visitors, and through the edit bridge for the editor. A header still
28
+ * server-rendered in edit mode is one the editor can select and cannot fill.
29
+ */
30
+ export default function LayoutSlot({
31
+ groups,
32
+ position,
33
+ locale,
34
+ defaultLocale,
35
+ enabledLocales,
36
+ edit,
37
+ }: LayoutSlotProps) {
38
+ if (edit) {
39
+ return (
40
+ <CmssyLazyLayout
41
+ groups={groups}
42
+ position={position}
43
+ locale={locale}
44
+ defaultLocale={defaultLocale}
45
+ enabledLocales={enabledLocales}
46
+ edit={edit}
47
+ load={() => import("./blocks")}
48
+ />
49
+ );
50
+ }
51
+
52
+ const group = groups.find((candidate) => candidate.position === position);
53
+ if (!group) return null;
54
+
55
+ const blockMap = buildBlockMap(blocks);
56
+ const context = buildBlockContext(locale, defaultLocale, enabledLocales);
57
+
58
+ return (
59
+ <>
60
+ {[...group.blocks]
61
+ .filter((block) => block.isActive !== false)
62
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
63
+ .map((block) => (
64
+ <CmssyBlock
65
+ key={block.id}
66
+ block={block}
67
+ blockMap={blockMap}
68
+ locale={locale}
69
+ defaultLocale={defaultLocale}
70
+ context={context}
71
+ />
72
+ ))}
73
+ </>
74
+ );
75
+ }
@@ -2,10 +2,11 @@
2
2
  import { loadCmssyPage } from "@cmssy/astro";
3
3
  import { cmssy } from "../cmssy.config";
4
4
  import { Blocks } from "../components/Blocks";
5
+ import LayoutSlot from "../cmssy/layout-slot";
5
6
 
6
7
  export const prerender = false;
7
8
 
8
- const { page, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
9
+ const { page, layouts, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
9
10
  cmssy,
10
11
  Astro.request,
11
12
  Astro.url,
@@ -22,6 +23,13 @@ if (!page) {
22
23
  <title>{page?.seoTitle?.[locale] ?? page?.displayName?.[locale] ?? "cmssy on Astro"}</title>
23
24
  </head>
24
25
  <body>
26
+ <LayoutSlot
27
+ groups={layouts}
28
+ position="header"
29
+ locale={locale}
30
+ defaultLocale={defaultLocale}
31
+ enabledLocales={enabledLocales}
32
+ />
25
33
  {
26
34
  page ? (
27
35
  <Blocks
@@ -34,5 +42,12 @@ if (!page) {
34
42
  <main><h1>Not found</h1></main>
35
43
  )
36
44
  }
45
+ <LayoutSlot
46
+ groups={layouts}
47
+ position="footer"
48
+ locale={locale}
49
+ defaultLocale={defaultLocale}
50
+ enabledLocales={enabledLocales}
51
+ />
37
52
  </body>
38
53
  </html>
@@ -2,12 +2,13 @@
2
2
  import { loadCmssyPage } from "@cmssy/astro";
3
3
  import { cmssy } from "../../cmssy.config";
4
4
  import CmssyEditor from "../../cmssy/editor";
5
+ import LayoutSlot from "../../cmssy/layout-slot";
5
6
 
6
7
  // The editor iframe lands here (the middleware rewrites verified requests). It
7
8
  // must be dynamic: a prerendered page never sees the query string.
8
9
  export const prerender = false;
9
10
 
10
- const { page, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
11
+ const { page, layouts, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
11
12
  cmssy,
12
13
  Astro.request,
13
14
  Astro.url,
@@ -21,6 +22,15 @@ const editorOrigin = cmssy.editorOrigin ?? "*";
21
22
  <title>cmssy editor</title>
22
23
  </head>
23
24
  <body>
25
+ <LayoutSlot
26
+ client:load
27
+ groups={layouts}
28
+ position="header"
29
+ locale={locale}
30
+ defaultLocale={defaultLocale}
31
+ enabledLocales={enabledLocales}
32
+ edit={{ editorOrigin }}
33
+ />
24
34
  <CmssyEditor
25
35
  client:load
26
36
  page={page}
@@ -29,5 +39,14 @@ const editorOrigin = cmssy.editorOrigin ?? "*";
29
39
  enabledLocales={enabledLocales}
30
40
  edit={{ editorOrigin }}
31
41
  />
42
+ <LayoutSlot
43
+ client:load
44
+ groups={layouts}
45
+ position="footer"
46
+ locale={locale}
47
+ defaultLocale={defaultLocale}
48
+ enabledLocales={enabledLocales}
49
+ edit={{ editorOrigin }}
50
+ />
32
51
  </body>
33
52
  </html>
@@ -0,0 +1,72 @@
1
+ import {
2
+ CmssyBlock,
3
+ buildBlockContext,
4
+ buildBlockMap,
5
+ type CmssyLayoutGroup,
6
+ } from "@cmssy/react";
7
+ import { CmssyLazyLayout } from "@cmssy/react/client";
8
+ import { blocks } from "./blocks";
9
+
10
+ export interface LayoutSlotProps {
11
+ groups: CmssyLayoutGroup[];
12
+ position: "header" | "footer";
13
+ locale: string;
14
+ defaultLocale: string;
15
+ enabledLocales: string[];
16
+ /** Set on a VERIFIED editor request; the loader reports it as `isEdit`. */
17
+ edit?: { editorOrigin: string | string[] };
18
+ }
19
+
20
+ /**
21
+ * The header and footer, which are layout blocks rather than markup you own.
22
+ *
23
+ * Two renders, one per mode, and the difference is the point: server-rendered
24
+ * for visitors, and through the edit bridge for the editor. A header still
25
+ * server-rendered in edit mode is one the editor can select and cannot fill.
26
+ */
27
+ export function LayoutSlot({
28
+ groups,
29
+ position,
30
+ locale,
31
+ defaultLocale,
32
+ enabledLocales,
33
+ edit,
34
+ }: LayoutSlotProps) {
35
+ if (edit) {
36
+ return (
37
+ <CmssyLazyLayout
38
+ groups={groups}
39
+ position={position}
40
+ locale={locale}
41
+ defaultLocale={defaultLocale}
42
+ enabledLocales={enabledLocales}
43
+ edit={edit}
44
+ load={() => import("./blocks")}
45
+ />
46
+ );
47
+ }
48
+
49
+ const group = groups.find((candidate) => candidate.position === position);
50
+ if (!group) return null;
51
+
52
+ const blockMap = buildBlockMap(blocks);
53
+ const context = buildBlockContext(locale, defaultLocale, enabledLocales);
54
+
55
+ return (
56
+ <>
57
+ {[...group.blocks]
58
+ .filter((block) => block.isActive !== false)
59
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
60
+ .map((block) => (
61
+ <CmssyBlock
62
+ key={block.id}
63
+ block={block}
64
+ blockMap={blockMap}
65
+ locale={locale}
66
+ defaultLocale={defaultLocale}
67
+ context={context}
68
+ />
69
+ ))}
70
+ </>
71
+ );
72
+ }
@@ -3,6 +3,7 @@ import { createCmssyHeaders, createCmssyLoader } from "@cmssy/remix";
3
3
  import { cmssy } from "../../cmssy.config";
4
4
  import { blocks } from "../cmssy/blocks";
5
5
  import { CmssyEditor } from "../cmssy/editor";
6
+ import { LayoutSlot } from "../cmssy/layout-slot";
6
7
  import type { Route } from "./+types/page";
7
8
 
8
9
  export const loader = createCmssyLoader(cmssy);
@@ -14,6 +15,7 @@ export const headers = createCmssyHeaders(cmssy);
14
15
  export default function CmssyPage({ loaderData }: Route.ComponentProps) {
15
16
  const {
16
17
  page,
18
+ layouts,
17
19
  locale,
18
20
  defaultLocale,
19
21
  enabledLocales,
@@ -28,40 +30,63 @@ export default function CmssyPage({ loaderData }: Route.ComponentProps) {
28
30
 
29
31
  // A verified editor request renders the same page through the edit bridge.
30
32
  // No separate route: a React Router page always sees its query string.
33
+ const slot = (position: "header" | "footer") => (
34
+ <LayoutSlot
35
+ groups={layouts}
36
+ position={position}
37
+ locale={locale}
38
+ defaultLocale={defaultLocale}
39
+ enabledLocales={enabledLocales}
40
+ edit={isEdit ? { editorOrigin } : undefined}
41
+ />
42
+ );
43
+
31
44
  if (isEdit) {
32
45
  return (
33
- <CmssyEditor
34
- page={page}
35
- locale={locale}
36
- defaultLocale={defaultLocale}
37
- enabledLocales={enabledLocales}
38
- edit={{ editorOrigin }}
39
- />
46
+ <>
47
+ {slot("header")}
48
+ <CmssyEditor
49
+ page={page}
50
+ locale={locale}
51
+ defaultLocale={defaultLocale}
52
+ enabledLocales={enabledLocales}
53
+ edit={{ editorOrigin }}
54
+ />
55
+ {slot("footer")}
56
+ </>
40
57
  );
41
58
  }
42
59
 
43
60
  if (!page)
44
61
  return (
45
- <main>
46
- <h1>Not found</h1>
47
- </main>
62
+ <>
63
+ {slot("header")}
64
+ <main>
65
+ <h1>Not found</h1>
66
+ </main>
67
+ {slot("footer")}
68
+ </>
48
69
  );
49
70
 
50
71
  const blockMap = buildBlockMap(blocks);
51
72
  const context = buildBlockContext(locale, defaultLocale, enabledLocales);
52
73
 
53
74
  return (
54
- <main>
55
- {(page.blocks ?? []).map((block) => (
56
- <CmssyBlock
57
- key={block.id}
58
- block={block}
59
- blockMap={blockMap}
60
- locale={locale}
61
- defaultLocale={defaultLocale}
62
- context={context}
63
- />
64
- ))}
65
- </main>
75
+ <>
76
+ {slot("header")}
77
+ <main>
78
+ {(page.blocks ?? []).map((block) => (
79
+ <CmssyBlock
80
+ key={block.id}
81
+ block={block}
82
+ blockMap={blockMap}
83
+ locale={locale}
84
+ defaultLocale={defaultLocale}
85
+ context={context}
86
+ />
87
+ ))}
88
+ </main>
89
+ {slot("footer")}
90
+ </>
66
91
  );
67
92
  }
package/dist/index.js CHANGED
@@ -187,6 +187,7 @@ var FRAMEWORKS = [
187
187
  "src/middleware.ts",
188
188
  "src/cmssy/blocks.ts",
189
189
  "src/cmssy/editor.tsx",
190
+ "src/cmssy/layout-slot.tsx",
190
191
  "src/cmssy/hero.tsx",
191
192
  "src/components/Blocks.tsx",
192
193
  "src/pages/[...path].astro",
@@ -204,6 +205,7 @@ var FRAMEWORKS = [
204
205
  "app/routes.ts",
205
206
  "app/cmssy/blocks.ts",
206
207
  "app/cmssy/editor.tsx",
208
+ "app/cmssy/layout-slot.tsx",
207
209
  "app/cmssy/hero.tsx",
208
210
  "app/routes/page.tsx"
209
211
  ]
@@ -1244,6 +1246,22 @@ async function request(deps, endpoint, query, variables) {
1244
1246
  if (!body.data) throw new CliError("the delivery API returned no data");
1245
1247
  return body.data;
1246
1248
  }
1249
+ function describeDrift(previous, next) {
1250
+ const declared = (source, pattern) => new Set([...source.matchAll(pattern)].map((match) => match[1] ?? ""));
1251
+ const body = (source) => source.split("/** Every model in the")[0] ?? source;
1252
+ const models = /export interface (\w+)Data/g;
1253
+ const fields = /^\s{2}(\w+)\??:/gm;
1254
+ const lines = [];
1255
+ const report = (label, before, after) => {
1256
+ const added = [...after].filter((name) => !before.has(name));
1257
+ const removed = [...before].filter((name) => !after.has(name));
1258
+ if (added.length) lines.push(`+ ${label}: ${added.join(", ")}`);
1259
+ if (removed.length) lines.push(`- ${label}: ${removed.join(", ")}`);
1260
+ };
1261
+ report("models", declared(previous, models), declared(next, models));
1262
+ report("fields", declared(body(previous), fields), declared(body(next), fields));
1263
+ return lines.length ? lines : ["the generated output differs"];
1264
+ }
1247
1265
  function resolve3(value, fromEnv, variable) {
1248
1266
  const resolved = value?.trim() || fromEnv?.trim();
1249
1267
  if (!resolved) {
@@ -1295,6 +1313,16 @@ async function runTypes(options, deps) {
1295
1313
  deps.log(`cmssy: ${shown} is up to date`);
1296
1314
  return 0;
1297
1315
  }
1316
+ if (options.check) {
1317
+ deps.log(
1318
+ previous === null ? `cmssy: ${shown} is missing - run \`cmssy types\` and commit it` : `cmssy: ${shown} is out of date with the "${workspace}" workspace`
1319
+ );
1320
+ if (previous !== null) {
1321
+ for (const line of describeDrift(previous, source)) deps.log(` ${line}`);
1322
+ }
1323
+ deps.log(" run `cmssy types` and commit the result");
1324
+ return 1;
1325
+ }
1298
1326
  mkdirSync3(dirname3(outPath), { recursive: true });
1299
1327
  writeFileSync4(outPath, source);
1300
1328
  const fieldCount = models.reduce(
@@ -1324,7 +1352,7 @@ var USAGE = [
1324
1352
  " cmssy init [--dir <path>] [--force]",
1325
1353
  " cmssy add block <name> [--dir <path>]",
1326
1354
  " cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]",
1327
- " cmssy types [--out <path>] [--org <slug>] [--workspace <slug>]"
1355
+ " cmssy types [--out <path>] [--check] [--org <slug>] [--workspace <slug>]"
1328
1356
  ].join("\n");
1329
1357
  function flagValue(args, name) {
1330
1358
  const index = args.findIndex((arg) => arg === name);
@@ -1387,7 +1415,8 @@ async function main() {
1387
1415
  {
1388
1416
  out: flagValue(args, "--out"),
1389
1417
  org: flagValue(args, "--org"),
1390
- workspace: flagValue(args, "--workspace")
1418
+ workspace: flagValue(args, "--workspace"),
1419
+ check: hasFlag(args, "--check")
1391
1420
  },
1392
1421
  {
1393
1422
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "10.6.1",
3
+ "version": "10.8.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",
@@ -27,7 +27,7 @@
27
27
  "assets"
28
28
  ],
29
29
  "dependencies": {
30
- "@cmssy/core": "10.6.1"
30
+ "@cmssy/core": "10.8.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",