@stetcms/client 0.1.0 → 0.2.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/README.md +12 -8
- package/dist/codegen.d.ts +4 -1
- package/dist/codegen.js +10 -7
- package/dist/index.d.ts +27 -3
- package/dist/index.js +14 -16
- package/package.json +16 -6
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# @stetcms/client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/jamiedavenport/stet/actions/workflows/ci.yml)
|
|
4
|
+
[](https://docs.stetcms.com/reference/client)
|
|
5
|
+
[](./LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Typed client for the API of [Stet](https://stetcms.com), the CMS where marketing owns the content model and engineering gets a typed client generated from it. The types come straight from the contract the server implements, so calls and responses are fully typed end to end.
|
|
8
|
+
|
|
9
|
+
It also carries the content client runtime that [`@stetcms/vite`](https://docs.stetcms.com/reference/codegen)'s generated `stet.gen.ts` instantiates:
|
|
6
10
|
|
|
7
11
|
```ts
|
|
8
12
|
import { createContentClient } from '@stetcms/client';
|
|
@@ -55,14 +59,14 @@ Options:
|
|
|
55
59
|
- `apiKey`: organization API key sent as `x-api-key`.
|
|
56
60
|
- `fetch`: custom fetch implementation.
|
|
57
61
|
|
|
58
|
-
The API itself is plain REST under `/api/v1`, described by the OpenAPI document generated from the same contract.
|
|
62
|
+
The API itself is plain REST under `/api/v1`, described by the [OpenAPI document](https://docs.stetcms.com/api) generated from the same contract. [Authentication](https://docs.stetcms.com/api/authentication) covers how keys are scoped.
|
|
59
63
|
|
|
60
|
-
##
|
|
64
|
+
## Asset URLs
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
The API returns asset paths relative to itself; the client joins them to the `origin` it was created with, so what you get is ready for an `img` tag on your own origin. That covers an asset field's `url` and assets embedded or linked inside rich-text markdown and HTML.
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
Calling the REST API directly instead? `assetUrl(url, origin)` joins one value and `resolveAssetPaths(text, origin)` joins the ones inside rich-text markdown or HTML.
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
## License
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
Apache-2.0
|
package/dist/codegen.d.ts
CHANGED
|
@@ -14,10 +14,13 @@ type ContentModel = {
|
|
|
14
14
|
options: {
|
|
15
15
|
name: string;
|
|
16
16
|
}[]; /** Slug of the collection a reference field points at. */
|
|
17
|
-
collection?: string;
|
|
17
|
+
collection?: string;
|
|
18
18
|
deprecated?: {
|
|
19
|
+
reason: 'deleted' | 'renamed';
|
|
19
20
|
at: string;
|
|
20
21
|
by?: string;
|
|
22
|
+
note?: string;
|
|
23
|
+
renamedTo?: string;
|
|
21
24
|
};
|
|
22
25
|
}[];
|
|
23
26
|
}[];
|
package/dist/codegen.js
CHANGED
|
@@ -16,7 +16,7 @@ async function fetchContentModel(origin, apiKey) {
|
|
|
16
16
|
}
|
|
17
17
|
/** `case-studies` → `CaseStudiesEntry`; never starts with a digit. */
|
|
18
18
|
function entryTypeName(slug) {
|
|
19
|
-
const pascal = slug.split(/[^a-zA-Z0-9]+/).filter((part) => part.length > 0).map((part) => part
|
|
19
|
+
const pascal = slug.split(/[^a-zA-Z0-9]+/).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
20
20
|
return /^[A-Za-z]/.test(pascal) ? `${pascal}Entry` : `Stet${pascal}Entry`;
|
|
21
21
|
}
|
|
22
22
|
function optionUnion(options) {
|
|
@@ -33,17 +33,19 @@ function fieldTsType(field) {
|
|
|
33
33
|
case "asset": return "ContentAsset";
|
|
34
34
|
case "reference": return "ContentReference";
|
|
35
35
|
case "multi_reference": return "ContentReference[]";
|
|
36
|
+
case "rich_text": return "ContentRichText";
|
|
36
37
|
default: return "string";
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Names the change that retired the key, so whoever meets the strikethrough
|
|
41
|
-
* knows when it happened and who to ask. The date is the ISO day rather than a
|
|
42
|
-
* locale format, which would make the generated file differ between machines.
|
|
43
|
-
*/
|
|
44
40
|
function deprecationNote(deprecation) {
|
|
45
41
|
const who = deprecation.by === void 0 ? "" : ` by ${deprecation.by}`;
|
|
46
|
-
|
|
42
|
+
const when = deprecation.at.slice(0, 10);
|
|
43
|
+
const context = deprecation.note === void 0 ? "" : ` ${safeDoc(deprecation.note)}`;
|
|
44
|
+
if (deprecation.reason === "renamed") return `@deprecated Renamed to ${deprecation.renamedTo === void 0 ? "the new key" : `\`${deprecation.renamedTo}\``} on ${when}${who}; this alias returns its current value until the Action is completed.${context}`;
|
|
45
|
+
return `@deprecated Deleted from the content model on ${when}${who}; entries still return the last value it held until the Action is completed.${context}`;
|
|
46
|
+
}
|
|
47
|
+
function safeDoc(note) {
|
|
48
|
+
return note.replaceAll("*/", "*\\/").replaceAll(/\s+/g, " ").trim();
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* What the editor needs that the type cannot say: which collection a reference
|
|
@@ -87,6 +89,7 @@ function renderContentModule(model, origin) {
|
|
|
87
89
|
const imported = ["ContentEntryBase"];
|
|
88
90
|
if (fields.some((field) => field.type === "asset")) imported.push("ContentAsset");
|
|
89
91
|
if (fields.some((field) => field.type === "reference" || field.type === "multi_reference")) imported.push("ContentReference");
|
|
92
|
+
if (fields.some((field) => field.type === "rich_text")) imported.push("ContentRichText");
|
|
90
93
|
const lines = [
|
|
91
94
|
"// Generated by Stet from your content model. Do not edit: it is",
|
|
92
95
|
"// regenerated by @stetcms/vite or `stet generate`.",
|
package/dist/index.d.ts
CHANGED
|
@@ -42,8 +42,14 @@ declare const contentTypeSchema: z.ZodObject<{
|
|
|
42
42
|
}, z.core.$strip>>;
|
|
43
43
|
collection: z.ZodOptional<z.ZodString>;
|
|
44
44
|
deprecated: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
reason: z.ZodEnum<{
|
|
46
|
+
deleted: "deleted";
|
|
47
|
+
renamed: "renamed";
|
|
48
|
+
}>;
|
|
45
49
|
at: z.ZodISODateTime;
|
|
46
50
|
by: z.ZodOptional<z.ZodString>;
|
|
51
|
+
note: z.ZodOptional<z.ZodString>;
|
|
52
|
+
renamedTo: z.ZodOptional<z.ZodString>;
|
|
47
53
|
}, z.core.$strip>>;
|
|
48
54
|
}, z.core.$strip>>;
|
|
49
55
|
}, z.core.$strip>;
|
|
@@ -87,6 +93,7 @@ declare const contract: {
|
|
|
87
93
|
props: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
88
94
|
timestamp: z.ZodNumber;
|
|
89
95
|
url: z.ZodOptional<z.ZodString>;
|
|
96
|
+
route: z.ZodOptional<z.ZodString>;
|
|
90
97
|
referrer: z.ZodOptional<z.ZodString>;
|
|
91
98
|
}, z.core.$strip>>;
|
|
92
99
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -149,8 +156,14 @@ declare const contract: {
|
|
|
149
156
|
}, z.core.$strip>>;
|
|
150
157
|
collection: z.ZodOptional<z.ZodString>;
|
|
151
158
|
deprecated: z.ZodOptional<z.ZodObject<{
|
|
159
|
+
reason: z.ZodEnum<{
|
|
160
|
+
deleted: "deleted";
|
|
161
|
+
renamed: "renamed";
|
|
162
|
+
}>;
|
|
152
163
|
at: z.ZodISODateTime;
|
|
153
164
|
by: z.ZodOptional<z.ZodString>;
|
|
165
|
+
note: z.ZodOptional<z.ZodString>;
|
|
166
|
+
renamedTo: z.ZodOptional<z.ZodString>;
|
|
154
167
|
}, z.core.$strip>>;
|
|
155
168
|
}, z.core.$strip>>;
|
|
156
169
|
}, z.core.$strip>>;
|
|
@@ -200,8 +213,14 @@ declare const contract: {
|
|
|
200
213
|
}, z.core.$strip>>;
|
|
201
214
|
collection: z.ZodOptional<z.ZodString>;
|
|
202
215
|
deprecated: z.ZodOptional<z.ZodObject<{
|
|
216
|
+
reason: z.ZodEnum<{
|
|
217
|
+
deleted: "deleted";
|
|
218
|
+
renamed: "renamed";
|
|
219
|
+
}>;
|
|
203
220
|
at: z.ZodISODateTime;
|
|
204
221
|
by: z.ZodOptional<z.ZodString>;
|
|
222
|
+
note: z.ZodOptional<z.ZodString>;
|
|
223
|
+
renamedTo: z.ZodOptional<z.ZodString>;
|
|
205
224
|
}, z.core.$strip>>;
|
|
206
225
|
}, z.core.$strip>>;
|
|
207
226
|
}, z.core.$strip>;
|
|
@@ -512,6 +531,11 @@ type ContentAsset = {
|
|
|
512
531
|
contentType: string; /** Bytes. */
|
|
513
532
|
size: number;
|
|
514
533
|
};
|
|
534
|
+
/** A rich text field rendered canonically by Stet from its editor document. */
|
|
535
|
+
type ContentRichText = {
|
|
536
|
+
markdown: string;
|
|
537
|
+
html: string;
|
|
538
|
+
};
|
|
515
539
|
/**
|
|
516
540
|
* The shape `@stetcms/vite` generates from the organization's content model:
|
|
517
541
|
* one key per collection or map, carrying its kind and full entry type.
|
|
@@ -545,8 +569,8 @@ type ContentClientOptions = {
|
|
|
545
569
|
*/
|
|
546
570
|
declare function assetUrl(url: string, origin: string): string;
|
|
547
571
|
/**
|
|
548
|
-
* The same join applied inside a body's markdown, so an
|
|
549
|
-
*
|
|
572
|
+
* The same join applied inside a body's markdown or HTML, so an asset an
|
|
573
|
+
* editor embedded or linked renders on your site as readily as an asset field.
|
|
550
574
|
*/
|
|
551
575
|
declare function resolveAssetPaths(text: string, origin: string): string;
|
|
552
576
|
/**
|
|
@@ -568,4 +592,4 @@ type StetClientOptions = {
|
|
|
568
592
|
};
|
|
569
593
|
declare function createStetClient(options?: StetClientOptions): StetClient;
|
|
570
594
|
//#endregion
|
|
571
|
-
export { ApiInputs, ApiOutputs, type CollectionClient, type ContentAsset, type ContentClient, type ContentClientOptions, type ContentEntry, type ContentEntryBase, type ContentModel, type ContentModelShape, type ContentReference, type ContentType, DEFAULT_ORIGIN, type MapClient, type Organization, StetClient, StetClientOptions, assetUrl, createContentClient, createStetClient, entryTypeName, fetchContentModel, isDefinedError, renderContentModule, resolveAssetPaths, safe };
|
|
595
|
+
export { ApiInputs, ApiOutputs, type CollectionClient, type ContentAsset, type ContentClient, type ContentClientOptions, type ContentEntry, type ContentEntryBase, type ContentModel, type ContentModelShape, type ContentReference, type ContentRichText, type ContentType, DEFAULT_ORIGIN, type MapClient, type Organization, StetClient, StetClientOptions, assetUrl, createContentClient, createStetClient, entryTypeName, fetchContentModel, isDefinedError, renderContentModule, resolveAssetPaths, safe };
|
package/dist/index.js
CHANGED
|
@@ -166,10 +166,12 @@ const contentFieldTypeSchema = z.enum([
|
|
|
166
166
|
"multi_reference"
|
|
167
167
|
]);
|
|
168
168
|
const contentDeprecationSchema = z.object({
|
|
169
|
-
|
|
169
|
+
reason: z.enum(["deleted", "renamed"]),
|
|
170
170
|
at: z.iso.datetime(),
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
by: z.string().optional(),
|
|
172
|
+
note: z.string().optional(),
|
|
173
|
+
/** Current canonical key, present for a rename. */
|
|
174
|
+
renamedTo: z.string().optional()
|
|
173
175
|
});
|
|
174
176
|
const contentFieldSchema = z.object({
|
|
175
177
|
key: z.string(),
|
|
@@ -183,14 +185,8 @@ const contentFieldSchema = z.object({
|
|
|
183
185
|
/** Slug of the collection a reference or multi-reference field points at. */
|
|
184
186
|
collection: z.string().optional(),
|
|
185
187
|
/**
|
|
186
|
-
* Present
|
|
187
|
-
*
|
|
188
|
-
* Editors stop seeing the field, but entries keep the last value it held
|
|
189
|
-
* and go on returning it, so a deletion costs a running site nothing. A
|
|
190
|
-
* generated client turns it into a deprecation rather than dropping the
|
|
191
|
-
* key, so code reading it keeps compiling. The key and its values go for
|
|
192
|
-
* good only when a developer purges the field from the Danger Zone, after
|
|
193
|
-
* which it leaves this list.
|
|
188
|
+
* Present for deleted fields and old rename aliases. Entries keep returning
|
|
189
|
+
* the value through this key until its Action is completed.
|
|
194
190
|
*/
|
|
195
191
|
deprecated: contentDeprecationSchema.optional()
|
|
196
192
|
});
|
|
@@ -214,7 +210,7 @@ const getContentModel = oc.errors(authErrors).route({
|
|
|
214
210
|
method: "GET",
|
|
215
211
|
path: "/model",
|
|
216
212
|
summary: "Content model",
|
|
217
|
-
description: "Every collection and map in the organization with its fields. The generated client is typed from this. Deleted fields
|
|
213
|
+
description: "Every collection and map in the organization with its fields. The generated client is typed from this. Deleted fields and old rename aliases carry a `deprecated` record so regeneration keeps downstream clients compiling until the matching Action is completed.",
|
|
218
214
|
tags: ["Content"]
|
|
219
215
|
}).output(z.object({ types: z.array(contentTypeSchema) }));
|
|
220
216
|
const listContent = oc.errors({
|
|
@@ -224,7 +220,7 @@ const listContent = oc.errors({
|
|
|
224
220
|
method: "GET",
|
|
225
221
|
path: "/content/{type}",
|
|
226
222
|
summary: "List entries",
|
|
227
|
-
description: "A content type's entries with resolved field values; rich text bodies
|
|
223
|
+
description: "A content type's entries with resolved field values; rich text bodies contain canonical HTML and markdown. A map returns its single entry as a one-element list.",
|
|
228
224
|
tags: ["Content"]
|
|
229
225
|
}).input(z.object({ type: z.string() })).output(z.object({
|
|
230
226
|
type: contentTypeSchema,
|
|
@@ -268,6 +264,8 @@ const analyticsEventSchema = z.object({
|
|
|
268
264
|
/** Epoch milliseconds, stamped in the browser when the event happened. */
|
|
269
265
|
timestamp: z.number().int().nonnegative(),
|
|
270
266
|
url: z.string().optional(),
|
|
267
|
+
/** Router template for the URL, e.g. `/blog/[slug]`. */
|
|
268
|
+
route: z.string().min(1).max(500).optional(),
|
|
271
269
|
referrer: z.string().optional()
|
|
272
270
|
});
|
|
273
271
|
const contract = {
|
|
@@ -324,10 +322,10 @@ const assetPath = "/assets/";
|
|
|
324
322
|
function assetUrl(url, origin) {
|
|
325
323
|
return url.startsWith(assetPath) ? `${origin}${url}` : url;
|
|
326
324
|
}
|
|
327
|
-
const assetPathInText = /(\]\(|
|
|
325
|
+
const assetPathInText = /(\]\(|\s(?:src|href)=["'])(\/assets\/)/g;
|
|
328
326
|
/**
|
|
329
|
-
* The same join applied inside a body's markdown, so an
|
|
330
|
-
*
|
|
327
|
+
* The same join applied inside a body's markdown or HTML, so an asset an
|
|
328
|
+
* editor embedded or linked renders on your site as readily as an asset field.
|
|
331
329
|
*/
|
|
332
330
|
function resolveAssetPaths(text, origin) {
|
|
333
331
|
return text.replace(assetPathInText, (_match, prefix) => `${prefix}${origin}${assetPath}`);
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stetcms/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Typed client for
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Typed content and REST client for Stet, the CMS for marketing and engineering.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
7
7
|
"client",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"cms",
|
|
9
|
+
"content-api",
|
|
10
|
+
"headless-cms",
|
|
11
|
+
"sdk",
|
|
12
|
+
"stet",
|
|
13
|
+
"stetcms",
|
|
14
|
+
"typesafe",
|
|
15
|
+
"typescript"
|
|
10
16
|
],
|
|
11
|
-
"homepage": "https://
|
|
17
|
+
"homepage": "https://docs.stetcms.com/reference/client",
|
|
12
18
|
"bugs": "https://github.com/jamiedavenport/stet/issues",
|
|
13
19
|
"license": "Apache-2.0",
|
|
14
20
|
"author": "Jamie Davenport (https://jxd.dev)",
|
|
@@ -21,6 +27,7 @@
|
|
|
21
27
|
"dist"
|
|
22
28
|
],
|
|
23
29
|
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
24
31
|
"exports": {
|
|
25
32
|
".": {
|
|
26
33
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +46,7 @@
|
|
|
39
46
|
"@orpc/contract": "^1.14.8",
|
|
40
47
|
"@orpc/openapi-client": "^1.14.8",
|
|
41
48
|
"zod": "^4.4.3",
|
|
42
|
-
"@stetcms/config": "0.1.
|
|
49
|
+
"@stetcms/config": "0.1.1"
|
|
43
50
|
},
|
|
44
51
|
"devDependencies": {
|
|
45
52
|
"publint": "^0.3.21",
|
|
@@ -48,6 +55,9 @@
|
|
|
48
55
|
"vitest": "^4.1.9",
|
|
49
56
|
"@repo/api": "0.0.0"
|
|
50
57
|
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
},
|
|
51
61
|
"scripts": {
|
|
52
62
|
"build": "vp pack",
|
|
53
63
|
"tc": "tsc --noEmit",
|