@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 CHANGED
@@ -1,8 +1,12 @@
1
1
  # @stetcms/client
2
2
 
3
- Typed client for the [Stet](https://github.com/jamiedavenport/stet) API. The types come straight from the oRPC contract the server implements, so calls and responses are fully typed end to end.
3
+ [![CI](https://github.com/jamiedavenport/stet/actions/workflows/ci.yml/badge.svg)](https://github.com/jamiedavenport/stet/actions/workflows/ci.yml)
4
+ [![Docs](https://img.shields.io/badge/docs-stetcms.com-black.svg)](https://docs.stetcms.com/reference/client)
5
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
4
6
 
5
- It also carries the content client runtime that [`@stetcms/vite`](https://github.com/jamiedavenport/stet/tree/main/published/vite)'s generated `stet.gen.ts` instantiates:
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
- ## License
64
+ ## Asset URLs
61
65
 
62
- Apache-2.0
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
- ## Asset URLs
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
- 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 both an asset field's `url` and the images inside a rich text body, so a body renders as it stands.
70
+ ## License
67
71
 
68
- Calling the REST API directly instead? `assetUrl(url, origin)` joins one value and `resolveAssetPaths(markdown, origin)` joins the ones inside a body.
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; /** Deleted from the model: emitted as a deprecation, never dropped. */
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[0].toUpperCase() + part.slice(1)).join("");
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
- return `@deprecated Deleted from the content model on ${deprecation.at.slice(0, 10)}${who}; entries still return the last value it held, until it is purged.`;
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 image an editor
549
- * dropped into a body renders on your site as readily as an asset field does.
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
- /** When the field was deleted from the model. */
169
+ reason: z.enum(["deleted", "renamed"]),
170
170
  at: z.iso.datetime(),
171
- /** Who deleted it; absent when no signed-in user did, or the account is gone. */
172
- by: z.string().optional()
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 once the field has been deleted from the model, naming when and
187
- * by whom so a stale key can be traced back to the change that retired it.
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 are still listed, carrying a `deprecated` record of when and by whom, so a client regenerated after a deletion marks the key instead of dropping it and entries go on returning the last value it held.",
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 are markdown as last saved by the realtime room. A map returns its single entry as a one-element list.",
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 = /(\]\(|src=["'])(\/assets\/)/g;
325
+ const assetPathInText = /(\]\(|\s(?:src|href)=["'])(\/assets\/)/g;
328
326
  /**
329
- * The same join applied inside a body's markdown, so an image an editor
330
- * dropped into a body renders on your site as readily as an asset field does.
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.1.0",
4
- "description": "Typed client for the Stet API, generated from the oRPC contract.",
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
- "orpc",
9
- "stet"
8
+ "cms",
9
+ "content-api",
10
+ "headless-cms",
11
+ "sdk",
12
+ "stet",
13
+ "stetcms",
14
+ "typesafe",
15
+ "typescript"
10
16
  ],
11
- "homepage": "https://github.com/jamiedavenport/stet/tree/main/published/client#readme",
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.0"
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",