storyloco 2.2.0 → 2.3.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
@@ -176,10 +176,19 @@ await storyblok.story('layout')
176
176
  // A list. `total` is the unpaged count, for "showing n of m"
177
177
  const { stories, total } = await storyblok.stories({ content_type: 'blog', per_page: 2 })
178
178
 
179
+ // Everything that matches, paged through the API's 100-per-page cap
180
+ const products = await storyblok.all({ content_type: 'product' })
181
+
179
182
  // The link tree, flattened — Storyblok returns it keyed by uuid
180
183
  const links = await storyblok.links({ starts_with: 'blog/' })
181
184
  ```
182
185
 
186
+ Reads with a `content_type` are typed from your generated schema: the plugin registers
187
+ the space's content types with storyloco, so `stories({ content_type: 'product' })` yields
188
+ product stories and `content_type: 'product,category'` a union of both — no type argument
189
+ needed. Until a schema has been generated, or for a name it doesn't know, the content falls
190
+ back to the SDK's permissive type. An explicit type argument always wins.
191
+
183
192
  Your params are spread last, so any default can be overridden:
184
193
 
185
194
  ```typescript
@@ -194,6 +203,18 @@ const product = await storyblok.find<Product>(`products/${slug}`)
194
203
  if (!product) error(404, 'Product not found')
195
204
  ```
196
205
 
206
+ The content type can be a union. Each member becomes its own story type, so `is_component`
207
+ can narrow the result — checking `story.content.component` yourself narrows only `content`,
208
+ not the story:
209
+
210
+ ```typescript
211
+ import { is_component } from 'storyloco'
212
+
213
+ const story = await storyblok.find<Product | Category>(`products/${slug}`)
214
+ if (is_component(story, 'product')) story.content.sku // ISbStoryData<Product>
215
+ if (is_component(story, 'category')) story.content.description // ISbStoryData<Category>
216
+ ```
217
+
197
218
  It's callable too, for anything the methods don't cover:
198
219
 
199
220
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -218,6 +218,11 @@ import type { ISbStoryData } from '@storyblok/js';`, "");
218
218
  }
219
219
 
220
220
  export type ContentType = ContentTypes[keyof ContentTypes]
221
+
222
+ /** Lets storyloco type reads by \`content_type\` without a type argument. */
223
+ declare module "storyloco" {
224
+ interface Registry extends ContentTypes {}
225
+ }
221
226
  `;
222
227
  let content = component_types.replace(`// This file was generated by the storyblok CLI.
223
228
  // DO NOT MODIFY THIS FILE BY HAND.`, header);
@@ -1,6 +1,6 @@
1
- import type { ISbStoriesParams, ISbStoryData, ISbStoryParams } from "@storyblok/svelte";
1
+ import type { ISbStoriesParams, ISbStoryParams } from "@storyblok/svelte";
2
2
  import type { ISbLink, ISbLinksParams } from "storyblok-js-client";
3
- import { type StoryblokClient, type Version } from "./storyblok.svelte.js";
3
+ import { type ContentFor, type Story, type StoryblokClient, type Version } from "./storyblok.svelte.js";
4
4
  /** The content version for the current request, as set by the `version` hook. */
5
5
  export declare function version(): Version;
6
6
  /**
@@ -20,7 +20,9 @@ export declare function version(): Version;
20
20
  * // anywhere on the server
21
21
  * await storyblok.story("layout")
22
22
  * await storyblok.find<Product>(`products/${slug}`) // null when missing
23
- * await storyblok.stories<Blog>({ content_type: "blog", per_page: 2 })
23
+ * await storyblok.find<Product | Category>(slug) // a union of stories; narrow with is_component
24
+ * await storyblok.stories({ content_type: "blog", per_page: 2 }) // typed from the generated schema
25
+ * await storyblok.all<Product>({ content_type: "product" }) // every page
24
26
  *
25
27
  * const { client, version } = storyblok()
26
28
  * await client.get("cdn/links", { version })
@@ -32,18 +34,35 @@ export declare function setup(client: StoryblokClient): (() => {
32
34
  }) & {
33
35
  story: <T = import("storyblok-js-client").ISbComponentType<string> & {
34
36
  [index: string]: any;
35
- }>(slug: string, params?: ISbStoryParams) => Promise<ISbStoryData<T>>;
37
+ }>(slug: string, params?: ISbStoryParams) => Promise<Story<T>>;
36
38
  find: <T = import("storyblok-js-client").ISbComponentType<string> & {
37
39
  [index: string]: any;
38
- }>(slug: string, params?: ISbStoryParams) => Promise<ISbStoryData<T> | null>;
40
+ }>(slug: string, params?: ISbStoryParams) => Promise<Story<T> | null>;
41
+ stories: {
42
+ <K extends string>(params: ISbStoriesParams & {
43
+ content_type: K;
44
+ }): Promise<{
45
+ stories: Story<ContentFor<K>>[];
46
+ total: number;
47
+ }>;
48
+ <T = import("storyblok-js-client").ISbComponentType<string> & {
49
+ [index: string]: any;
50
+ }>(params?: ISbStoriesParams): Promise<{
51
+ stories: Story<T>[];
52
+ total: number;
53
+ }>;
54
+ };
55
+ all: {
56
+ <K extends string>(params: ISbStoriesParams & {
57
+ content_type: K;
58
+ }): Promise<Array<Story<ContentFor<K>>>>;
59
+ <T = import("storyblok-js-client").ISbComponentType<string> & {
60
+ [index: string]: any;
61
+ }>(params?: ISbStoriesParams): Promise<Array<Story<T>>>;
62
+ };
39
63
  /**
40
64
  * The link tree as a flat array — Storyblok returns it keyed by uuid, which is
41
65
  * almost never what you want. Filtering and ordering are left to the caller.
42
66
  */
43
67
  links(params?: ISbLinksParams): Promise<ISbLink[]>;
44
- /** A list of stories. `total` is the unpaged count, for "showing n of m". */
45
- stories<T>(params?: ISbStoriesParams): Promise<{
46
- stories: Array<ISbStoryData<T>>;
47
- total: number;
48
- }>;
49
68
  };
@@ -1,6 +1,6 @@
1
1
  import { getRequestEvent } from "$app/server";
2
2
  import { error } from "@sveltejs/kit";
3
- import { handle_error } from "./storyblok.svelte.js";
3
+ import { handle_error, } from "./storyblok.svelte.js";
4
4
  function is_not_found(err) {
5
5
  return typeof err === "object" && err !== null && "status" in err && err.status === 404;
6
6
  }
@@ -25,7 +25,9 @@ export function version() {
25
25
  * // anywhere on the server
26
26
  * await storyblok.story("layout")
27
27
  * await storyblok.find<Product>(`products/${slug}`) // null when missing
28
- * await storyblok.stories<Blog>({ content_type: "blog", per_page: 2 })
28
+ * await storyblok.find<Product | Category>(slug) // a union of stories; narrow with is_component
29
+ * await storyblok.stories({ content_type: "blog", per_page: 2 }) // typed from the generated schema
30
+ * await storyblok.all<Product>({ content_type: "product" }) // every page
29
31
  *
30
32
  * const { client, version } = storyblok()
31
33
  * await client.get("cdn/links", { version })
@@ -61,9 +63,33 @@ export function setup(client) {
61
63
  error(404, "Story not found");
62
64
  return found;
63
65
  }
66
+ async function stories(params = {}) {
67
+ const response = await client
68
+ .get("cdn/stories", {
69
+ version: version(),
70
+ resolve_links: "url",
71
+ resolve_assets: 1,
72
+ ...params,
73
+ })
74
+ .catch(handle_error);
75
+ return { stories: response.data.stories, total: response.total ?? 0 };
76
+ }
77
+ async function all(params = {}) {
78
+ return client
79
+ .getAll("cdn/stories", {
80
+ version: version(),
81
+ resolve_links: "url",
82
+ resolve_assets: 1,
83
+ per_page: 100,
84
+ ...params,
85
+ })
86
+ .catch(handle_error);
87
+ }
64
88
  return Object.assign(context, {
65
89
  story,
66
90
  find,
91
+ stories,
92
+ all,
67
93
  /**
68
94
  * The link tree as a flat array — Storyblok returns it keyed by uuid, which is
69
95
  * almost never what you want. Filtering and ordering are left to the caller.
@@ -74,20 +100,5 @@ export function setup(client) {
74
100
  .catch(handle_error);
75
101
  return Object.values(response.data.links ?? {});
76
102
  },
77
- /** A list of stories. `total` is the unpaged count, for "showing n of m". */
78
- async stories(params = {}) {
79
- const response = await client
80
- .get("cdn/stories", {
81
- version: version(),
82
- resolve_links: "url",
83
- resolve_assets: 1,
84
- ...params,
85
- })
86
- .catch(handle_error);
87
- return {
88
- stories: response.data.stories,
89
- total: response.total ?? 0,
90
- };
91
- },
92
103
  });
93
104
  }
@@ -140,6 +140,46 @@ export interface Richtext<Blocks = unknown> {
140
140
  content?: Array<Richtext<Blocks>>;
141
141
  marks?: Array<Richtext<Blocks>>;
142
142
  }
143
+ /**
144
+ * Content types by component name, for narrowing reads by `content_type`. Empty here;
145
+ * the schema plugin's generated file merges the space's `ContentTypes` into it, so
146
+ * `storyblok.stories({ content_type: "product" })` is typed as products without a type
147
+ * argument. Until a schema is generated every name falls back to the SDK's content type.
148
+ */
149
+ export interface Registry {
150
+ }
151
+ /** The SDK's permissive content type: any component, any fields. */
152
+ export type Content = ISbStoryData["content"];
153
+ /** Each name in a comma-separated `content_type`. */
154
+ type Names<S extends string> = S extends `${infer A},${infer B}` ? A | Names<B> : S;
155
+ /**
156
+ * The content behind a `content_type` param. Registered names map to their generated
157
+ * types (a list to their union); anything else is the permissive `Content`.
158
+ */
159
+ export type ContentFor<K extends string> = [Names<K>] extends [keyof Registry] ? Registry[Names<K>] : Content;
160
+ /**
161
+ * A story per member of `T`, so a union of content types is a union of stories —
162
+ * `Story<Product | Category>` is `ISbStoryData<Product> | ISbStoryData<Category>`, which
163
+ * `is_component` can narrow. `ISbStoryData<Product | Category>` can't be.
164
+ */
165
+ export type Story<T> = T extends unknown ? ISbStoryData<T> : never;
166
+ /**
167
+ * Narrow a story (or a union of them) by its content's `component`. TypeScript won't
168
+ * narrow the story through the nested discriminant on its own.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const story = await storyblok.find<Product | Category>(`products/${slug}`)
173
+ * if (is_component(story, "product")) story.content.sku // ISbStoryData<Product>
174
+ * ```
175
+ */
176
+ export declare function is_component<S extends ISbStoryData<{
177
+ component?: string;
178
+ }>, K extends string>(story: S | null | undefined, component: K): story is Extract<S, {
179
+ content: {
180
+ component?: K;
181
+ };
182
+ }>;
143
183
  /** Adds the `story.url` that `resolve_links: "url"` injects but the CLI doesn't declare. */
144
184
  export type Resolved<T extends {
145
185
  story?: unknown;
@@ -137,6 +137,19 @@ export class StoryblokClient {
137
137
  };
138
138
  }
139
139
  }
140
+ /**
141
+ * Narrow a story (or a union of them) by its content's `component`. TypeScript won't
142
+ * narrow the story through the nested discriminant on its own.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const story = await storyblok.find<Product | Category>(`products/${slug}`)
147
+ * if (is_component(story, "product")) story.content.sku // ISbStoryData<Product>
148
+ * ```
149
+ */
150
+ export function is_component(story, component) {
151
+ return story?.content.component === component;
152
+ }
140
153
  /**
141
154
  * Resolve a multilink to an href. Story links become root-relative, everything else passes
142
155
  * through. Prefers `story.url` over `cached_url`, which goes stale when a target moves —