@cmssy/cli 10.5.2 → 10.6.1
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.
|
@@ -2,5 +2,28 @@ import { createCmssyPage } from "@cmssy/next/server";
|
|
|
2
2
|
import { cmssy } from "@/cmssy.config";
|
|
3
3
|
import { blocks } from "@/cmssy/blocks";
|
|
4
4
|
import { CmssyEditor } from "@/cmssy/editor";
|
|
5
|
+
import { CmssyLayoutSlot } from "@/cmssy/layout-slot";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
type PageProps = {
|
|
8
|
+
params: Promise<{ path?: string[] }>;
|
|
9
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const CmssyPage = createCmssyPage(cmssy, blocks, { editor: CmssyEditor });
|
|
13
|
+
|
|
14
|
+
// The header and the footer are layout blocks, mounted here rather than in
|
|
15
|
+
// app/layout.tsx: this route knows its path, and the language prefix in that
|
|
16
|
+
// path is what says which language to render them in. Reading it from the
|
|
17
|
+
// request header instead would make every page dynamic.
|
|
18
|
+
export default async function Page(props: PageProps) {
|
|
19
|
+
const { path } = await props.params;
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<CmssyLayoutSlot position="header" path={path} />
|
|
23
|
+
<main>
|
|
24
|
+
<CmssyPage {...props} />
|
|
25
|
+
</main>
|
|
26
|
+
<CmssyLayoutSlot position="footer" path={path} />
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createCmssyEditPage } from "@cmssy/next/server";
|
|
|
2
2
|
import { cmssy } from "@/cmssy.config";
|
|
3
3
|
import { blocks } from "@/cmssy/blocks";
|
|
4
4
|
import { CmssyEditor } from "@/cmssy/editor";
|
|
5
|
+
import { CmssyLayoutSlot } from "@/cmssy/layout-slot";
|
|
5
6
|
|
|
6
7
|
// The route the middleware rewrites a verified editor request onto. The public
|
|
7
8
|
// pages stay static, and a static page never sees the query string that would
|
|
@@ -10,4 +11,26 @@ import { CmssyEditor } from "@/cmssy/editor";
|
|
|
10
11
|
// Delete this file and the editor preview goes blank.
|
|
11
12
|
export const dynamic = "force-dynamic";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
type PageProps = {
|
|
15
|
+
params: Promise<{ path?: string[] }>;
|
|
16
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CmssyEditPage = createCmssyEditPage(cmssy, blocks, {
|
|
20
|
+
editor: CmssyEditor,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// The same chrome the public route renders - the editor frames THIS page, so a
|
|
24
|
+
// header missing here is a header missing in the editor.
|
|
25
|
+
export default async function EditPage(props: PageProps) {
|
|
26
|
+
const { path } = await props.params;
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<CmssyLayoutSlot position="header" path={path} />
|
|
30
|
+
<main>
|
|
31
|
+
<CmssyEditPage {...props} />
|
|
32
|
+
</main>
|
|
33
|
+
<CmssyLayoutSlot position="footer" path={path} />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
import { CmssyServerLayout, graphqlRequest } from "@cmssy/react";
|
|
3
|
+
import type { CmssyLayoutGroup } from "@cmssy/react";
|
|
4
|
+
import { resolveEditorLayoutBlockData } from "@cmssy/react/internal-server";
|
|
5
|
+
import { resolveEditorOrigin } from "@cmssy/next";
|
|
6
|
+
import { isCmssyEditMode } from "@cmssy/next/server";
|
|
7
|
+
import { cmssy } from "@/cmssy.config";
|
|
8
|
+
import { blocks } from "@/cmssy/blocks";
|
|
9
|
+
import { EditableLayout } from "@/cmssy/editable-layout";
|
|
10
|
+
|
|
11
|
+
// The header and the footer are layout blocks: content, not markup. Fetching
|
|
12
|
+
// and rendering them is your app's, so both queries live here.
|
|
13
|
+
const LAYOUTS_QUERY = `query PublicPageLayouts($workspaceSlug: String!, $pageSlug: String!, $previewSecret: String) {
|
|
14
|
+
public {
|
|
15
|
+
page {
|
|
16
|
+
layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug, previewSecret: $previewSecret) {
|
|
17
|
+
position
|
|
18
|
+
blocks { id type content style advanced order isActive }
|
|
19
|
+
settings { desktopWidth mobileBehavior }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}`;
|
|
24
|
+
|
|
25
|
+
const SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
|
|
26
|
+
public {
|
|
27
|
+
siteConfig(workspaceSlug: $workspaceSlug) {
|
|
28
|
+
defaultLanguage
|
|
29
|
+
enabledLanguages
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}`;
|
|
33
|
+
|
|
34
|
+
// Both slots render on every page; `cache` makes that one request each.
|
|
35
|
+
const fetchLayouts = cache(
|
|
36
|
+
async (pageSlug: string, previewSecret: string | null) => {
|
|
37
|
+
try {
|
|
38
|
+
const data = await graphqlRequest<{
|
|
39
|
+
public?: { page?: { layouts?: CmssyLayoutGroup[] | null } | null } | null;
|
|
40
|
+
}>(
|
|
41
|
+
cmssy,
|
|
42
|
+
LAYOUTS_QUERY,
|
|
43
|
+
{ workspaceSlug: cmssy.workspaceSlug, pageSlug, previewSecret },
|
|
44
|
+
{ public: true, retry: {} },
|
|
45
|
+
"page layouts",
|
|
46
|
+
);
|
|
47
|
+
return data.public?.page?.layouts ?? [];
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("[cmssy] page layouts could not be fetched", error);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const fetchLocales = cache(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const data = await graphqlRequest<{
|
|
58
|
+
public?: {
|
|
59
|
+
siteConfig?: {
|
|
60
|
+
defaultLanguage: string | null;
|
|
61
|
+
enabledLanguages: string[] | null;
|
|
62
|
+
} | null;
|
|
63
|
+
} | null;
|
|
64
|
+
}>(
|
|
65
|
+
cmssy,
|
|
66
|
+
SITE_CONFIG_QUERY,
|
|
67
|
+
{ workspaceSlug: cmssy.workspaceSlug },
|
|
68
|
+
{ public: true, retry: {} },
|
|
69
|
+
"site config",
|
|
70
|
+
);
|
|
71
|
+
const config = data.public?.siteConfig;
|
|
72
|
+
// The languages are the workspace's, never this repo's.
|
|
73
|
+
const defaultLocale = config?.defaultLanguage ?? "";
|
|
74
|
+
const enabled = config?.enabledLanguages ?? [];
|
|
75
|
+
return {
|
|
76
|
+
defaultLocale,
|
|
77
|
+
locales: enabled.length > 0 ? enabled : defaultLocale ? [defaultLocale] : [],
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("[cmssy] site config could not be fetched", error);
|
|
81
|
+
return { defaultLocale: "", locales: [] as string[] };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export interface CmssyLayoutSlotProps {
|
|
86
|
+
position: "header" | "footer";
|
|
87
|
+
/**
|
|
88
|
+
* The catch-all segments of the route rendering this slot, as routed. The
|
|
89
|
+
* language prefix in them IS the language - reading it here keeps the route
|
|
90
|
+
* statically renderable, where reading the request header would force every
|
|
91
|
+
* page dynamic and kill ISR.
|
|
92
|
+
*/
|
|
93
|
+
path?: string[];
|
|
94
|
+
/** The page whose layout to render. Defaults to the site-wide header/footer. */
|
|
95
|
+
page?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The site-wide header and footer, rendered the way each mode needs them:
|
|
100
|
+
*
|
|
101
|
+
* - published traffic: server-rendered layout blocks, no client cost;
|
|
102
|
+
* - the editor: the same blocks through the edit bridge, fetched with the
|
|
103
|
+
* preview secret, so what you see is the draft you are editing.
|
|
104
|
+
*
|
|
105
|
+
* Getting this wrong is invisible - the site looks right while the editor shows
|
|
106
|
+
* a header it can select but not fill, or the published version of one.
|
|
107
|
+
*/
|
|
108
|
+
export async function CmssyLayoutSlot({
|
|
109
|
+
position,
|
|
110
|
+
path,
|
|
111
|
+
page = "/",
|
|
112
|
+
}: CmssyLayoutSlotProps) {
|
|
113
|
+
const [editMode, { defaultLocale, locales }] = await Promise.all([
|
|
114
|
+
isCmssyEditMode(),
|
|
115
|
+
fetchLocales(),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const [first] = path ?? [];
|
|
119
|
+
const locale =
|
|
120
|
+
first && first !== defaultLocale && locales.includes(first)
|
|
121
|
+
? first
|
|
122
|
+
: defaultLocale;
|
|
123
|
+
|
|
124
|
+
const groups = await fetchLayouts(
|
|
125
|
+
page,
|
|
126
|
+
editMode ? cmssy.draftSecret : null,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!editMode) {
|
|
130
|
+
return (
|
|
131
|
+
<CmssyServerLayout
|
|
132
|
+
groups={groups}
|
|
133
|
+
blocks={blocks}
|
|
134
|
+
position={position}
|
|
135
|
+
locale={locale}
|
|
136
|
+
defaultLocale={defaultLocale}
|
|
137
|
+
enabledLocales={locales}
|
|
138
|
+
config={cmssy}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const origin = resolveEditorOrigin(cmssy.editorOrigin);
|
|
144
|
+
// The editor canvas renders stored content, and for a relation field that is
|
|
145
|
+
// raw ids - so the server-resolved content is handed over with it.
|
|
146
|
+
const editorData = await resolveEditorLayoutBlockData({
|
|
147
|
+
groups,
|
|
148
|
+
blocks,
|
|
149
|
+
position,
|
|
150
|
+
locale,
|
|
151
|
+
defaultLocale,
|
|
152
|
+
enabledLocales: locales,
|
|
153
|
+
isPreview: true,
|
|
154
|
+
config: cmssy,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<EditableLayout
|
|
159
|
+
groups={groups}
|
|
160
|
+
position={position}
|
|
161
|
+
locale={locale}
|
|
162
|
+
defaultLocale={defaultLocale}
|
|
163
|
+
enabledLocales={locales}
|
|
164
|
+
edit={{ editorOrigin: Array.isArray(origin) ? origin[0] : origin }}
|
|
165
|
+
data={editorData.data}
|
|
166
|
+
resolvedContent={editorData.content}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -168,6 +168,7 @@ var FRAMEWORKS = [
|
|
|
168
168
|
"cmssy/blocks.ts",
|
|
169
169
|
"cmssy/editor.tsx",
|
|
170
170
|
"cmssy/editable-layout.tsx",
|
|
171
|
+
"cmssy/layout-slot.tsx",
|
|
171
172
|
"blocks/hero/block.ts",
|
|
172
173
|
"blocks/hero/Hero.tsx",
|
|
173
174
|
"app/[[...path]]/page.tsx",
|
|
@@ -661,14 +662,18 @@ function runInit(options, deps) {
|
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
// src/link.ts
|
|
664
|
-
import { existsSync as
|
|
665
|
-
import { join as
|
|
665
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
666
|
+
import { join as join5 } from "path";
|
|
666
667
|
import {
|
|
667
668
|
buildEditorUrl,
|
|
668
669
|
checkDraftSecret,
|
|
669
670
|
checkWorkspaceReachable
|
|
670
671
|
} from "@cmssy/core/preflight";
|
|
671
672
|
|
|
673
|
+
// src/env-load.ts
|
|
674
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
675
|
+
import { join as join4 } from "path";
|
|
676
|
+
|
|
672
677
|
// src/env-file.ts
|
|
673
678
|
function parseEnvFile(content) {
|
|
674
679
|
const vars = {};
|
|
@@ -702,6 +707,16 @@ function applyEnv(vars, env) {
|
|
|
702
707
|
return applied;
|
|
703
708
|
}
|
|
704
709
|
|
|
710
|
+
// src/env-load.ts
|
|
711
|
+
var ENV_FILES = [".env.local", ".env"];
|
|
712
|
+
function loadEnvFiles(cwd, env) {
|
|
713
|
+
for (const file of ENV_FILES) {
|
|
714
|
+
const path = join4(cwd, file);
|
|
715
|
+
if (!existsSync4(path)) continue;
|
|
716
|
+
applyEnv(parseEnvFile(readFileSync4(path, "utf8")), env);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
705
720
|
// src/env-write.ts
|
|
706
721
|
function needsQuoting(value) {
|
|
707
722
|
return /[\s#"']/.test(value) || value === "";
|
|
@@ -729,14 +744,6 @@ function mergeEnvContent(existing, updates) {
|
|
|
729
744
|
}
|
|
730
745
|
|
|
731
746
|
// src/link.ts
|
|
732
|
-
var ENV_FILES = [".env.local", ".env"];
|
|
733
|
-
function loadEnvFiles(cwd, env) {
|
|
734
|
-
for (const file of ENV_FILES) {
|
|
735
|
-
const path = join4(cwd, file);
|
|
736
|
-
if (!existsSync4(path)) continue;
|
|
737
|
-
applyEnv(parseEnvFile(readFileSync4(path, "utf8")), env);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
747
|
function resolveToken(options, deps) {
|
|
741
748
|
const token = options.token?.trim() || deps.env.CMSSY_API_TOKEN?.trim();
|
|
742
749
|
if (!token) {
|
|
@@ -883,8 +890,8 @@ function resolvePreviewUrl(options) {
|
|
|
883
890
|
return origin;
|
|
884
891
|
}
|
|
885
892
|
function writeEnvLocal(cwd, updates) {
|
|
886
|
-
const path =
|
|
887
|
-
const existing =
|
|
893
|
+
const path = join5(cwd, ".env.local");
|
|
894
|
+
const existing = existsSync5(path) ? readFileSync5(path, "utf8") : null;
|
|
888
895
|
writeFileSync3(path, mergeEnvContent(existing, updates));
|
|
889
896
|
}
|
|
890
897
|
async function runLink(options, deps) {
|
|
@@ -998,12 +1005,326 @@ async function runLink(options, deps) {
|
|
|
998
1005
|
}
|
|
999
1006
|
}
|
|
1000
1007
|
|
|
1008
|
+
// src/types-command.ts
|
|
1009
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1010
|
+
import { dirname as dirname3, relative, resolve as resolvePath } from "path";
|
|
1011
|
+
import { DEFAULT_CMSSY_API_URL } from "@cmssy/core";
|
|
1012
|
+
|
|
1013
|
+
// src/model-types.ts
|
|
1014
|
+
var TEXTUAL = /* @__PURE__ */ new Set([
|
|
1015
|
+
"text",
|
|
1016
|
+
"textarea",
|
|
1017
|
+
"richText",
|
|
1018
|
+
"markdown",
|
|
1019
|
+
"link",
|
|
1020
|
+
"url",
|
|
1021
|
+
"email",
|
|
1022
|
+
"phone",
|
|
1023
|
+
"color",
|
|
1024
|
+
"date",
|
|
1025
|
+
"datetime",
|
|
1026
|
+
"password",
|
|
1027
|
+
"pageSelector"
|
|
1028
|
+
]);
|
|
1029
|
+
var INDENT = " ";
|
|
1030
|
+
function pascalCase(value) {
|
|
1031
|
+
const parts = value.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1032
|
+
const joined = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1033
|
+
return /^[A-Za-z]/.test(joined) ? joined : `Model${joined}`;
|
|
1034
|
+
}
|
|
1035
|
+
function propertyKey(key) {
|
|
1036
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
1037
|
+
}
|
|
1038
|
+
function stringUnion(options) {
|
|
1039
|
+
return options.map((option) => JSON.stringify(option)).join(" | ");
|
|
1040
|
+
}
|
|
1041
|
+
function scalarType(field) {
|
|
1042
|
+
if (TEXTUAL.has(field.type)) {
|
|
1043
|
+
return field.localized ? "CmssyLocalized" : "string";
|
|
1044
|
+
}
|
|
1045
|
+
switch (field.type) {
|
|
1046
|
+
case "number":
|
|
1047
|
+
return "number";
|
|
1048
|
+
case "boolean":
|
|
1049
|
+
return "boolean";
|
|
1050
|
+
case "media":
|
|
1051
|
+
case "file":
|
|
1052
|
+
return field.multiple ? "string[]" : "string";
|
|
1053
|
+
case "select":
|
|
1054
|
+
case "radio":
|
|
1055
|
+
return field.options?.length ? stringUnion(field.options) : "string";
|
|
1056
|
+
case "multiselect":
|
|
1057
|
+
return field.options?.length ? `Array<${stringUnion(field.options)}>` : "string[]";
|
|
1058
|
+
case "relation":
|
|
1059
|
+
return field.relationType === "hasMany" || field.multiple ? "string[]" : "string";
|
|
1060
|
+
case "json":
|
|
1061
|
+
case "table":
|
|
1062
|
+
case "form":
|
|
1063
|
+
return "unknown";
|
|
1064
|
+
default:
|
|
1065
|
+
return "unknown";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function fieldType(field, depth) {
|
|
1069
|
+
if (field.type === "object") {
|
|
1070
|
+
return field.fields?.length ? objectType(field.fields, depth) : "Record<string, unknown>";
|
|
1071
|
+
}
|
|
1072
|
+
if (field.type === "repeater" || field.type === "list") {
|
|
1073
|
+
if (field.itemFields?.length) {
|
|
1074
|
+
return `Array<${objectType(field.itemFields, depth)}>`;
|
|
1075
|
+
}
|
|
1076
|
+
if (field.itemType) {
|
|
1077
|
+
const item = scalarType({ key: field.key, type: field.itemType });
|
|
1078
|
+
return `${item.includes(" ") ? `Array<${item}>` : `${item}[]`}`;
|
|
1079
|
+
}
|
|
1080
|
+
return "unknown[]";
|
|
1081
|
+
}
|
|
1082
|
+
return scalarType(field);
|
|
1083
|
+
}
|
|
1084
|
+
function docComment(field, pad) {
|
|
1085
|
+
const parts = [];
|
|
1086
|
+
if (field.label && field.label !== field.key) parts.push(field.label);
|
|
1087
|
+
if (field.description) parts.push(field.description);
|
|
1088
|
+
if (field.type === "relation" && field.relationTo) {
|
|
1089
|
+
parts.push(`Record id(s) from \`${field.relationTo.replace(/^model:/, "")}\`.`);
|
|
1090
|
+
}
|
|
1091
|
+
if (!parts.length) return [];
|
|
1092
|
+
return [`${pad}/** ${parts.join(" - ")} */`];
|
|
1093
|
+
}
|
|
1094
|
+
function members(fields, depth) {
|
|
1095
|
+
const pad = INDENT.repeat(depth + 1);
|
|
1096
|
+
const lines = [];
|
|
1097
|
+
for (const field of fields) {
|
|
1098
|
+
if (field.hidden) continue;
|
|
1099
|
+
lines.push(...docComment(field, pad));
|
|
1100
|
+
const optional = field.required ? "" : "?";
|
|
1101
|
+
lines.push(
|
|
1102
|
+
`${pad}${propertyKey(field.key)}${optional}: ${fieldType(field, depth + 1)};`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return lines;
|
|
1106
|
+
}
|
|
1107
|
+
function objectType(fields, depth) {
|
|
1108
|
+
const body = members(fields, depth);
|
|
1109
|
+
if (!body.length) return "Record<string, unknown>";
|
|
1110
|
+
return ["{", ...body, `${INDENT.repeat(depth)}}`].join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
var PREAMBLE = `/** A translatable field: one string, or one per enabled language. */
|
|
1113
|
+
export type CmssyLocalized = string | Record<string, string>;
|
|
1114
|
+
|
|
1115
|
+
/** A record as \`public.model.records\` returns it, with \`data\` typed. */
|
|
1116
|
+
export interface CmssyRecordOf<Data> {
|
|
1117
|
+
id: string;
|
|
1118
|
+
modelId: string;
|
|
1119
|
+
status: string;
|
|
1120
|
+
createdAt: string;
|
|
1121
|
+
updatedAt: string;
|
|
1122
|
+
data: Data;
|
|
1123
|
+
}`;
|
|
1124
|
+
function generateModelTypes(models, options) {
|
|
1125
|
+
const command = options.command ?? "npx @cmssy/cli types";
|
|
1126
|
+
const sorted = [...models].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1127
|
+
const used = /* @__PURE__ */ new Map();
|
|
1128
|
+
const typeNames = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const model of sorted) {
|
|
1130
|
+
let name = pascalCase(model.slug);
|
|
1131
|
+
if (used.has(name)) name = `${name}_${used.size}`;
|
|
1132
|
+
used.set(name, model.slug);
|
|
1133
|
+
typeNames.set(model.slug, name);
|
|
1134
|
+
}
|
|
1135
|
+
const blocks = [
|
|
1136
|
+
`// Generated by \`${command}\` from the "${options.workspace}" workspace.`,
|
|
1137
|
+
"// Do not edit: change the model in the CMS and run the command again.",
|
|
1138
|
+
"",
|
|
1139
|
+
PREAMBLE
|
|
1140
|
+
];
|
|
1141
|
+
for (const model of sorted) {
|
|
1142
|
+
const name = typeNames.get(model.slug) ?? pascalCase(model.slug);
|
|
1143
|
+
const title = model.name ?? model.slug;
|
|
1144
|
+
const doc = [`/**`, ` * ${title}`];
|
|
1145
|
+
if (model.description) doc.push(` *`, ` * ${model.description}`);
|
|
1146
|
+
doc.push(` *`, ` * Model slug: \`${model.slug}\``);
|
|
1147
|
+
if (model.displayField) doc.push(` * Display field: \`${model.displayField}\``);
|
|
1148
|
+
doc.push(` */`);
|
|
1149
|
+
blocks.push(
|
|
1150
|
+
"",
|
|
1151
|
+
doc.join("\n"),
|
|
1152
|
+
`export interface ${name}Data ${objectType(model.fields, 0)}`,
|
|
1153
|
+
"",
|
|
1154
|
+
`export type ${name}Record = CmssyRecordOf<${name}Data>;`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const mapEntries = sorted.map((model) => {
|
|
1158
|
+
const name = typeNames.get(model.slug) ?? pascalCase(model.slug);
|
|
1159
|
+
return `${INDENT}${propertyKey(model.slug)}: ${name}Data;`;
|
|
1160
|
+
});
|
|
1161
|
+
blocks.push(
|
|
1162
|
+
"",
|
|
1163
|
+
"/** Every model in the workspace, by slug. */",
|
|
1164
|
+
["export interface CmssyModels {", ...mapEntries, "}"].join("\n"),
|
|
1165
|
+
"",
|
|
1166
|
+
"export type CmssyModelSlug = keyof CmssyModels;",
|
|
1167
|
+
"",
|
|
1168
|
+
"/** The typed record list `public.model.records` returns for a model. */",
|
|
1169
|
+
"export interface CmssyRecordList<Slug extends CmssyModelSlug> {",
|
|
1170
|
+
`${INDENT}items: Array<CmssyRecordOf<CmssyModels[Slug]>>;`,
|
|
1171
|
+
`${INDENT}total: number;`,
|
|
1172
|
+
`${INDENT}hasMore: boolean;`,
|
|
1173
|
+
"}",
|
|
1174
|
+
""
|
|
1175
|
+
);
|
|
1176
|
+
return blocks.join("\n");
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/types-command.ts
|
|
1180
|
+
var DEFAULT_OUT = "cmssy/models.ts";
|
|
1181
|
+
var SITE_CONFIG_QUERY = `query CliSiteConfig($workspaceSlug: String!) {
|
|
1182
|
+
public { siteConfig(workspaceSlug: $workspaceSlug) { workspaceId } }
|
|
1183
|
+
}`;
|
|
1184
|
+
var DEFINITIONS_QUERY = `query CliModelDefinitions($workspaceId: String!) {
|
|
1185
|
+
public {
|
|
1186
|
+
model {
|
|
1187
|
+
definitions(workspaceId: $workspaceId) {
|
|
1188
|
+
slug
|
|
1189
|
+
name
|
|
1190
|
+
description
|
|
1191
|
+
displayField
|
|
1192
|
+
fields {
|
|
1193
|
+
key
|
|
1194
|
+
label
|
|
1195
|
+
description
|
|
1196
|
+
type
|
|
1197
|
+
required
|
|
1198
|
+
localized
|
|
1199
|
+
multiple
|
|
1200
|
+
hidden
|
|
1201
|
+
options
|
|
1202
|
+
relationTo
|
|
1203
|
+
relationType
|
|
1204
|
+
fields
|
|
1205
|
+
itemType
|
|
1206
|
+
itemFields
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}`;
|
|
1212
|
+
function publicEndpoint(env, org, workspace) {
|
|
1213
|
+
const apiUrl = env.CMSSY_API_URL?.trim() || DEFAULT_CMSSY_API_URL;
|
|
1214
|
+
const base = apiUrl.replace(/\/graphql\/?$/, "").replace(/\/+$/, "");
|
|
1215
|
+
return `${base}/public/${org}/${workspace}/graphql`;
|
|
1216
|
+
}
|
|
1217
|
+
async function request(deps, endpoint, query, variables) {
|
|
1218
|
+
let response;
|
|
1219
|
+
try {
|
|
1220
|
+
response = await deps.fetch(endpoint, {
|
|
1221
|
+
method: "POST",
|
|
1222
|
+
headers: { "content-type": "application/json" },
|
|
1223
|
+
body: JSON.stringify({ query, variables })
|
|
1224
|
+
});
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
throw new CliError(
|
|
1227
|
+
`could not reach ${endpoint}`,
|
|
1228
|
+
error instanceof Error ? error.message : String(error)
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (!response.ok) {
|
|
1232
|
+
throw new CliError(
|
|
1233
|
+
`the delivery API answered ${response.status}`,
|
|
1234
|
+
"check CMSSY_ORG_SLUG and CMSSY_WORKSPACE_SLUG - they form the delivery path"
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const body = await response.json();
|
|
1238
|
+
if (body.errors?.length) {
|
|
1239
|
+
throw new CliError(
|
|
1240
|
+
"the delivery API rejected the query",
|
|
1241
|
+
body.errors.map((error) => error.message).join("; ")
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (!body.data) throw new CliError("the delivery API returned no data");
|
|
1245
|
+
return body.data;
|
|
1246
|
+
}
|
|
1247
|
+
function resolve3(value, fromEnv, variable) {
|
|
1248
|
+
const resolved = value?.trim() || fromEnv?.trim();
|
|
1249
|
+
if (!resolved) {
|
|
1250
|
+
throw new CliError(
|
|
1251
|
+
`no workspace to read: ${variable} is not set`,
|
|
1252
|
+
"run cmssy link first, or pass --org / --workspace"
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
return resolved;
|
|
1256
|
+
}
|
|
1257
|
+
async function runTypes(options, deps) {
|
|
1258
|
+
try {
|
|
1259
|
+
loadEnvFiles(deps.cwd, deps.env);
|
|
1260
|
+
const org = resolve3(options.org, deps.env.CMSSY_ORG_SLUG, "CMSSY_ORG_SLUG");
|
|
1261
|
+
const workspace = resolve3(
|
|
1262
|
+
options.workspace,
|
|
1263
|
+
deps.env.CMSSY_WORKSPACE_SLUG,
|
|
1264
|
+
"CMSSY_WORKSPACE_SLUG"
|
|
1265
|
+
);
|
|
1266
|
+
const endpoint = publicEndpoint(deps.env, org, workspace);
|
|
1267
|
+
const site = await request(deps, endpoint, SITE_CONFIG_QUERY, { workspaceSlug: workspace });
|
|
1268
|
+
const workspaceId = site.public.siteConfig?.workspaceId;
|
|
1269
|
+
if (!workspaceId) {
|
|
1270
|
+
throw new CliError(
|
|
1271
|
+
`workspace "${org}/${workspace}" was not found`,
|
|
1272
|
+
"check the slugs under Settings \u2192 Headless"
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const result = await request(deps, endpoint, DEFINITIONS_QUERY, { workspaceId });
|
|
1276
|
+
const models = result.public.model.definitions;
|
|
1277
|
+
if (models.length === 0) {
|
|
1278
|
+
deps.log(
|
|
1279
|
+
`cmssy: the "${workspace}" workspace has no models yet - nothing to generate`
|
|
1280
|
+
);
|
|
1281
|
+
return 0;
|
|
1282
|
+
}
|
|
1283
|
+
const requested = options.out?.trim() || DEFAULT_OUT;
|
|
1284
|
+
const outPath = resolvePath(deps.cwd, requested);
|
|
1285
|
+
const inside = relative(deps.cwd, outPath);
|
|
1286
|
+
const shown = inside && !inside.startsWith("..") ? inside : outPath;
|
|
1287
|
+
const source = generateModelTypes(models, { workspace });
|
|
1288
|
+
let previous = null;
|
|
1289
|
+
try {
|
|
1290
|
+
previous = readFileSync6(outPath, "utf8");
|
|
1291
|
+
} catch {
|
|
1292
|
+
previous = null;
|
|
1293
|
+
}
|
|
1294
|
+
if (previous === source) {
|
|
1295
|
+
deps.log(`cmssy: ${shown} is up to date`);
|
|
1296
|
+
return 0;
|
|
1297
|
+
}
|
|
1298
|
+
mkdirSync3(dirname3(outPath), { recursive: true });
|
|
1299
|
+
writeFileSync4(outPath, source);
|
|
1300
|
+
const fieldCount = models.reduce(
|
|
1301
|
+
(total, model) => total + model.fields.length,
|
|
1302
|
+
0
|
|
1303
|
+
);
|
|
1304
|
+
deps.log(
|
|
1305
|
+
`cmssy: wrote ${shown} - ${models.length} model${models.length === 1 ? "" : "s"}, ${fieldCount} field${fieldCount === 1 ? "" : "s"}`
|
|
1306
|
+
);
|
|
1307
|
+
deps.log(
|
|
1308
|
+
` ${models.map((model) => model.slug).join(", ")}`
|
|
1309
|
+
);
|
|
1310
|
+
return 0;
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
if (error instanceof CliError) {
|
|
1313
|
+
deps.log(`cmssy: ${error.message}`);
|
|
1314
|
+
if (error.fix) deps.log(` ${error.fix}`);
|
|
1315
|
+
return 1;
|
|
1316
|
+
}
|
|
1317
|
+
throw error;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1001
1321
|
// src/index.ts
|
|
1002
1322
|
var USAGE = [
|
|
1003
1323
|
"usage: cmssy <command>",
|
|
1004
1324
|
" cmssy init [--dir <path>] [--force]",
|
|
1005
1325
|
" cmssy add block <name> [--dir <path>]",
|
|
1006
|
-
" cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]"
|
|
1326
|
+
" cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]",
|
|
1327
|
+
" cmssy types [--out <path>] [--org <slug>] [--workspace <slug>]"
|
|
1007
1328
|
].join("\n");
|
|
1008
1329
|
function flagValue(args, name) {
|
|
1009
1330
|
const index = args.findIndex((arg) => arg === name);
|
|
@@ -1061,6 +1382,22 @@ async function main() {
|
|
|
1061
1382
|
process.exitCode = await runLinkCommand(args);
|
|
1062
1383
|
return;
|
|
1063
1384
|
}
|
|
1385
|
+
if (command === "types") {
|
|
1386
|
+
process.exitCode = await runTypes(
|
|
1387
|
+
{
|
|
1388
|
+
out: flagValue(args, "--out"),
|
|
1389
|
+
org: flagValue(args, "--org"),
|
|
1390
|
+
workspace: flagValue(args, "--workspace")
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
cwd: process.cwd(),
|
|
1394
|
+
env: process.env,
|
|
1395
|
+
log: (line) => console.log(line),
|
|
1396
|
+
fetch: globalThis.fetch
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1064
1401
|
console.error(USAGE);
|
|
1065
1402
|
process.exitCode = command === void 0 || command === "--help" ? 0 : 1;
|
|
1066
1403
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmssy/cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.6.1",
|
|
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.
|
|
30
|
+
"@cmssy/core": "10.6.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^20.0.0",
|