@foir/sdk 0.4.0 → 0.7.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +63 -0
  3. package/dist/auth/auth.d.ts +140 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +15 -0
  6. package/dist/auth/operations.d.ts +123 -0
  7. package/dist/auth/session-client.d.ts +76 -0
  8. package/dist/auth-session.js +137 -0
  9. package/dist/auth.js +238 -26
  10. package/dist/client/platform-client.d.ts +29 -0
  11. package/dist/client.js +290 -32
  12. package/dist/codegen/block-registry.d.ts +4 -8
  13. package/dist/codegen/block-scaffold.d.ts +4 -8
  14. package/dist/codegen/index.d.ts +8 -62
  15. package/dist/codegen/preset.d.ts +60 -0
  16. package/dist/codegen/shared.d.ts +25 -0
  17. package/dist/components/EditorProvider.d.ts +36 -0
  18. package/dist/content/content-renderer.d.ts +47 -0
  19. package/dist/content/index.d.ts +12 -0
  20. package/dist/content/richtext.d.ts +70 -0
  21. package/dist/data/builtins.d.ts +296 -0
  22. package/dist/data/client.d.ts +94 -0
  23. package/dist/data/index.d.ts +54 -0
  24. package/dist/data/manifest.d.ts +161 -0
  25. package/dist/data/schema.d.ts +243 -0
  26. package/dist/data/selection.d.ts +330 -0
  27. package/dist/data/transport.d.ts +107 -0
  28. package/dist/data.js +309 -0
  29. package/dist/graphql/cdn-route.d.ts +42 -0
  30. package/dist/graphql/client.d.ts +30 -0
  31. package/dist/graphql/content-client.d.ts +66 -0
  32. package/dist/graphql/foir-request.d.ts +47 -0
  33. package/dist/hooks/useAutoResize.d.ts +22 -0
  34. package/dist/hooks/useEditor.d.ts +16 -0
  35. package/dist/index.d.ts +10 -394
  36. package/dist/media/BlurhashCanvas.d.ts +10 -0
  37. package/dist/media/ProgressiveMedia.d.ts +56 -0
  38. package/dist/media/index.d.ts +14 -0
  39. package/dist/media/media.d.ts +139 -0
  40. package/dist/media/useProgressiveLoad.d.ts +23 -0
  41. package/dist/media.js +5 -0
  42. package/dist/next/cookieAdapter.d.ts +26 -0
  43. package/dist/next/index.d.ts +15 -0
  44. package/dist/next/middleware.d.ts +49 -0
  45. package/dist/next/session.d.ts +53 -0
  46. package/dist/next.js +230 -26
  47. package/dist/protocol/constants.d.ts +6 -0
  48. package/dist/protocol/messages.d.ts +155 -0
  49. package/dist/protocol/validation.d.ts +8 -0
  50. package/dist/push/index.d.ts +14 -0
  51. package/dist/push/operations.d.ts +22 -0
  52. package/dist/push/web-push.d.ts +39 -0
  53. package/dist/push.js +230 -26
  54. package/dist/react/FoirProvider.d.ts +116 -0
  55. package/dist/react/OAuthButtons.d.ts +20 -0
  56. package/dist/react/SignIn.d.ts +22 -0
  57. package/dist/react/UserButton.d.ts +13 -0
  58. package/dist/react/index.d.ts +19 -0
  59. package/dist/react/localStorageAdapter.d.ts +10 -0
  60. package/dist/react.js +230 -26
  61. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  62. package/dist/server/callback-client.d.ts +92 -0
  63. package/dist/server/index.d.ts +8 -0
  64. package/dist/server/operation-types.d.ts +134 -0
  65. package/dist/server/require-scoped-token.d.ts +76 -0
  66. package/dist/server/server-client.d.ts +34 -0
  67. package/dist/server/types.d.ts +54 -0
  68. package/dist/server/verify-scoped-token.d.ts +100 -0
  69. package/dist/server/webhook-helpers.d.ts +36 -0
  70. package/dist/transport/foir-http.d.ts +217 -0
  71. package/dist/transport/retry.d.ts +61 -0
  72. package/dist/types/index.d.ts +124 -0
  73. package/package.json +23 -13
  74. package/dist/auth.d.ts +0 -382
  75. package/dist/client.d.ts +0 -83
  76. package/dist/content.d.ts +0 -96
  77. package/dist/media.d.ts +0 -234
  78. package/dist/next.d.ts +0 -178
  79. package/dist/push.d.ts +0 -109
  80. package/dist/react.d.ts +0 -319
  81. package/dist/server.d.ts +0 -534
  82. package/dist/shared-DkWlEd2Z.d.ts +0 -18
package/dist/client.js CHANGED
@@ -1,13 +1,15 @@
1
1
  // src/graphql/foir-request.ts
2
+ import { print as print2 } from "graphql";
3
+
4
+ // src/graphql/cdn-route.ts
2
5
  import { print } from "graphql";
6
+
7
+ // src/transport/foir-http.ts
3
8
  function isFoirPreview(env) {
4
9
  return env.FOIR_PREVIEW === "true";
5
10
  }
6
- var FOIR_CDN_BASE = "https://cdn.foir.dev/";
7
- function absolutiseFoirMediaUrl(url, cdnBase = FOIR_CDN_BASE) {
8
- if (!url) return void 0;
9
- if (/^https?:\/\//i.test(url)) return url;
10
- return cdnBase + url.replace(/^\/+/, "");
11
+ function resolveFoirApiKey(env) {
12
+ return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
11
13
  }
12
14
  var FoirRequestError = class extends Error {
13
15
  /** HTTP status code, when the request failed at the HTTP layer. */
@@ -22,41 +24,95 @@ var FoirRequestError = class extends Error {
22
24
  this.status = options?.status;
23
25
  this.graphQLErrors = options?.graphQLErrors;
24
26
  }
27
+ /**
28
+ * The code of the first error, if the response carried one.
29
+ *
30
+ * A getter rather than a stored field so it stays correct for an error
31
+ * constructed anywhere, including by a caller in a test.
32
+ */
33
+ get code() {
34
+ return this.graphQLErrors?.[0]?.extensions?.code;
35
+ }
36
+ /** The correlation id, if the response carried one. */
37
+ get requestId() {
38
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
39
+ }
40
+ /**
41
+ * Whether retrying this request could succeed.
42
+ *
43
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
44
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
45
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
46
+ * own. A storefront wrote that policy, its key saturated, and every read
47
+ * failed on the first attempt for an hour.
48
+ */
49
+ get retryable() {
50
+ if (this.status === void 0) return true;
51
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
52
+ if (this.status >= 500) return true;
53
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
54
+ }
55
+ /**
56
+ * How long to wait before retrying, in milliseconds, when the server said.
57
+ * Undefined when it did not, which means the caller picks its own backoff.
58
+ */
59
+ get retryAfterMs() {
60
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
61
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
62
+ }
25
63
  };
26
- async function foirRequest(env, document, variables, options) {
27
- const endpoint = env.PLATFORM_API_URL;
28
- const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
29
- if (!endpoint || !apiKey) {
30
- throw new FoirRequestError(
31
- "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
32
- );
64
+ function isFoirRateLimited(err) {
65
+ return err instanceof FoirRequestError && (err.status === 429 || err.code === "RATE_LIMITED");
66
+ }
67
+ function isFoirUnauthenticated(err) {
68
+ return err instanceof FoirRequestError && (err.status === 401 || err.code === "UNAUTHENTICATED");
69
+ }
70
+ function isFoirForbidden(err) {
71
+ return err instanceof FoirRequestError && (err.status === 403 || err.code === "PERMISSION_DENIED");
72
+ }
73
+ function isFoirNotFound(err) {
74
+ return err instanceof FoirRequestError && err.code === "NOT_FOUND";
75
+ }
76
+ function readFoirRateLimit(headers) {
77
+ const raw = {
78
+ limit: headers.get("X-RateLimit-Limit"),
79
+ remaining: headers.get("X-RateLimit-Remaining"),
80
+ resetAt: headers.get("X-RateLimit-Reset")
81
+ };
82
+ if (raw.limit === null || raw.remaining === null || raw.resetAt === null) {
83
+ return void 0;
84
+ }
85
+ const limit = Number(raw.limit);
86
+ const remaining = Number(raw.remaining);
87
+ const resetAt = Number(raw.resetAt);
88
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAt)) {
89
+ return void 0;
33
90
  }
91
+ return { limit, remaining, resetAt };
92
+ }
93
+ function readFoirRetryAfterMs(headers) {
94
+ const seconds = Number(headers.get("Retry-After"));
95
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
96
+ }
97
+ function buildFoirHeaders(input) {
34
98
  const headers = {
35
99
  "Content-Type": "application/json",
36
- "x-api-key": apiKey
100
+ "x-api-key": input.apiKey
37
101
  };
38
- if (isFoirPreview(env)) {
102
+ if (input.draftChannel) {
39
103
  headers["X-Foir-Schema-Channel"] = "draft";
40
104
  }
41
- if (options?.bearerToken) {
42
- headers["Authorization"] = `Bearer ${options.bearerToken}`;
105
+ if (input.bearerToken) {
106
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
43
107
  }
44
- let res;
45
- try {
46
- res = await fetch(endpoint, {
47
- method: "POST",
48
- headers,
49
- body: JSON.stringify({ query: print(document), variables })
50
- });
51
- } catch (err) {
52
- throw new FoirRequestError("[foir] network error", { cause: err });
53
- }
54
- if (!res.ok) {
55
- throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
56
- status: res.status
57
- });
108
+ if (input.extra) {
109
+ for (const [key, value] of Object.entries(input.extra)) {
110
+ headers[key] = value;
111
+ }
58
112
  }
59
- const json = await res.json();
113
+ return headers;
114
+ }
115
+ function readFoirEnvelope(json) {
60
116
  if (json.errors?.length) {
61
117
  const msg = json.errors.map((e) => e.message).join("; ");
62
118
  if (json.data) {
@@ -72,10 +128,212 @@ async function foirRequest(env, document, variables, options) {
72
128
  }
73
129
  return json.data;
74
130
  }
131
+
132
+ // src/graphql/cdn-route.ts
133
+ var CDN_PATH = "/cdn/graphql";
134
+ function isCdnEligible(env, documentText, bearerToken) {
135
+ if (bearerToken) return false;
136
+ if (isFoirPreview(env)) return false;
137
+ const apiKey = resolveFoirApiKey(env);
138
+ if (!apiKey?.startsWith("pk_")) return false;
139
+ return !/\bmutation\b/.test(documentText);
140
+ }
141
+ function buildCdnUrl(endpoint, apiKey, hash, variables) {
142
+ const url = new URL(endpoint);
143
+ url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + CDN_PATH;
144
+ url.searchParams.set("key", apiKey);
145
+ url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
146
+ if (variables && Object.keys(variables).length > 0) {
147
+ url.searchParams.set("variables", JSON.stringify(variables));
148
+ }
149
+ return url.toString();
150
+ }
151
+ function persistedQueryExtension(hash) {
152
+ return { persistedQuery: { version: 1, sha256Hash: hash } };
153
+ }
154
+ async function documentHash(documentText) {
155
+ const digest = await crypto.subtle.digest(
156
+ "SHA-256",
157
+ new TextEncoder().encode(documentText)
158
+ );
159
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
160
+ }
161
+ var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
162
+ var NOT_SHAREABLE = "NOT_SHAREABLE";
163
+
164
+ // src/transport/retry.ts
165
+ var DEFAULT_ATTEMPTS = 3;
166
+ var DEFAULT_BASE_DELAY_MS = 200;
167
+ var DEFAULT_MAX_DELAY_MS = 2e4;
168
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
169
+ function foirRetryDelayMs(error, attempt, policy = {}) {
170
+ if (!(error instanceof FoirRequestError) || !error.retryable) return null;
171
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
172
+ const stated = error.retryAfterMs;
173
+ if (stated !== void 0) return Math.min(stated, max);
174
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
175
+ return Math.min(base * 2 ** (attempt - 1), max);
176
+ }
177
+ async function withFoirRetry(fn, policy = {}) {
178
+ const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
179
+ const sleep = policy.sleep ?? defaultSleep;
180
+ for (let attempt = 1; ; attempt += 1) {
181
+ try {
182
+ return await fn();
183
+ } catch (error) {
184
+ const delayMs = attempt >= attempts ? null : foirRetryDelayMs(error, attempt, policy);
185
+ if (delayMs === null) throw error;
186
+ policy.onRetry?.({ attempt, delayMs, error });
187
+ await sleep(delayMs);
188
+ }
189
+ }
190
+ }
191
+
192
+ // src/graphql/foir-request.ts
193
+ var FOIR_CDN_BASE = "https://cdn.foir.dev/";
194
+ function absolutiseFoirMediaUrl(url, cdnBase = FOIR_CDN_BASE) {
195
+ if (!url) return void 0;
196
+ if (/^https?:\/\//i.test(url)) return url;
197
+ return cdnBase + url.replace(/^\/+/, "");
198
+ }
199
+ async function foirRequest(env, document, variables, options) {
200
+ const endpoint = env.PLATFORM_API_URL;
201
+ const apiKey = resolveFoirApiKey(env);
202
+ if (!endpoint || !apiKey) {
203
+ throw new FoirRequestError(
204
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
205
+ );
206
+ }
207
+ const headers = buildFoirHeaders({
208
+ apiKey,
209
+ draftChannel: isFoirPreview(env),
210
+ bearerToken: options?.bearerToken
211
+ });
212
+ const documentText = print2(document);
213
+ let res;
214
+ try {
215
+ if (isCdnEligible(env, documentText, options?.bearerToken)) {
216
+ const hash = await documentHash(documentText);
217
+ let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
218
+ method: "GET",
219
+ headers
220
+ });
221
+ if (await isPersistedQueryMiss(shared)) {
222
+ shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
223
+ method: "POST",
224
+ headers,
225
+ body: JSON.stringify({
226
+ query: documentText,
227
+ variables,
228
+ extensions: persistedQueryExtension(hash)
229
+ })
230
+ });
231
+ }
232
+ if (await isSharedRefusal(shared)) {
233
+ res = await fetch(endpoint, {
234
+ method: "POST",
235
+ headers,
236
+ body: JSON.stringify({ query: documentText, variables })
237
+ });
238
+ } else {
239
+ res = shared;
240
+ }
241
+ } else {
242
+ res = await fetch(endpoint, {
243
+ method: "POST",
244
+ headers,
245
+ body: JSON.stringify({ query: documentText, variables })
246
+ });
247
+ }
248
+ } catch (err) {
249
+ throw new FoirRequestError("[foir] network error", { cause: err });
250
+ }
251
+ options?.onRateLimit?.(readFoirRateLimit(res.headers));
252
+ if (!res.ok) {
253
+ const errors = await readErrorEnvelope(res);
254
+ const first = errors?.[0]?.message;
255
+ throw new FoirRequestError(
256
+ first ? `[foir] HTTP ${res.status} ${res.statusText}: ${first}` : `[foir] HTTP ${res.status} ${res.statusText}`,
257
+ { status: res.status, graphQLErrors: errors }
258
+ );
259
+ }
260
+ const json = await res.json();
261
+ return readFoirEnvelope(json);
262
+ }
263
+ async function readErrorEnvelope(res) {
264
+ let errors;
265
+ try {
266
+ const body = await res.json();
267
+ errors = body.errors;
268
+ } catch {
269
+ errors = void 0;
270
+ }
271
+ const headerRetryMs = readFoirRetryAfterMs(res.headers);
272
+ if (headerRetryMs === void 0) return errors;
273
+ const seconds = Math.round(headerRetryMs / 1e3);
274
+ if (!errors?.length) {
275
+ return [
276
+ {
277
+ message: res.statusText || `HTTP ${res.status}`,
278
+ extensions: { code: "RATE_LIMITED", retryAfter: seconds }
279
+ }
280
+ ];
281
+ }
282
+ if (errors[0].extensions?.retryAfter === void 0) {
283
+ errors[0] = {
284
+ ...errors[0],
285
+ extensions: { ...errors[0].extensions, retryAfter: seconds }
286
+ };
287
+ }
288
+ return errors;
289
+ }
290
+ async function isSharedRefusal(res) {
291
+ if (res.ok) return false;
292
+ try {
293
+ const body = await res.clone().json();
294
+ return body.errors?.some((e) => e.extensions?.code === NOT_SHAREABLE) ?? false;
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+ async function isPersistedQueryMiss(res) {
300
+ try {
301
+ const body = await res.clone().json();
302
+ return body.errors?.some(
303
+ (e) => e.extensions?.code === PERSISTED_QUERY_NOT_FOUND
304
+ ) ?? false;
305
+ } catch {
306
+ return false;
307
+ }
308
+ }
309
+
310
+ // src/graphql/content-client.ts
311
+ function createFoirContentClient(config) {
312
+ return {
313
+ async request(document, variables, options) {
314
+ return withFoirRetry(async () => {
315
+ const bound = config.getToken ? await config.getToken() : null;
316
+ return foirRequest(config.env, document, variables, {
317
+ ...options,
318
+ bearerToken: options?.bearerToken ?? bound ?? void 0,
319
+ onRateLimit: options?.onRateLimit ?? config.onRateLimit
320
+ });
321
+ }, config.retry);
322
+ }
323
+ };
324
+ }
75
325
  export {
76
326
  FOIR_CDN_BASE,
77
327
  FoirRequestError,
78
328
  absolutiseFoirMediaUrl,
329
+ createFoirContentClient,
79
330
  foirRequest,
80
- isFoirPreview
331
+ foirRetryDelayMs,
332
+ isFoirForbidden,
333
+ isFoirNotFound,
334
+ isFoirPreview,
335
+ isFoirRateLimited,
336
+ isFoirUnauthenticated,
337
+ readFoirRateLimit,
338
+ withFoirRetry
81
339
  };
@@ -1,6 +1,3 @@
1
- import { GraphQLSchema } from 'graphql';
2
- import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
3
-
4
1
  /**
5
2
  * `@foir/sdk/codegen/block-registry` — a graphql-codegen plugin.
6
3
  *
@@ -16,12 +13,11 @@ import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
16
13
  * types from the codegen output (`./graphql`) and `BlockComponent` from
17
14
  * `@foir/sdk/content`, so it pairs directly with `createContentRenderer`.
18
15
  */
19
-
20
- interface BlockRegistryConfig extends BlockPluginConfig {
16
+ import type { GraphQLSchema } from 'graphql';
17
+ import { type BlockPluginConfig } from './shared';
18
+ export interface BlockRegistryConfig extends BlockPluginConfig {
21
19
  /** Name of the generated registry type. Default `BlockRegistry`. */
22
20
  registryName?: string;
23
21
  }
24
22
  /** graphql-codegen plugin entrypoint. */
25
- declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockRegistryConfig) => string;
26
-
27
- export { type BlockRegistryConfig, plugin };
23
+ export declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockRegistryConfig) => string;
@@ -1,6 +1,3 @@
1
- import { GraphQLSchema } from 'graphql';
2
- import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
3
-
4
1
  /**
5
2
  * `@foir/sdk/codegen/block-scaffold` — a one-shot graphql-codegen plugin.
6
3
  *
@@ -13,8 +10,9 @@ import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
13
10
  * in. Adding a new block type later surfaces as a compile error on
14
11
  * `blockRegistry` (via the regenerated `BlockRegistry` type), not here.
15
12
  */
16
-
17
- interface BlockScaffoldConfig extends BlockPluginConfig {
13
+ import type { GraphQLSchema } from 'graphql';
14
+ import { type BlockPluginConfig } from './shared';
15
+ export interface BlockScaffoldConfig extends BlockPluginConfig {
18
16
  /** Import path for the generated registry type. Default `./block-registry`. */
19
17
  registryImport?: string;
20
18
  /** Render-context type name threaded to every component. Default `unknown`. */
@@ -23,6 +21,4 @@ interface BlockScaffoldConfig extends BlockPluginConfig {
23
21
  ctxImport?: string;
24
22
  }
25
23
  /** graphql-codegen plugin entrypoint. */
26
- declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockScaffoldConfig) => string;
27
-
28
- export { type BlockScaffoldConfig, plugin };
24
+ export declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockScaffoldConfig) => string;
@@ -1,65 +1,11 @@
1
- import { CodegenConfig } from '@graphql-codegen/cli';
2
- export { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
3
-
4
1
  /**
5
- * `@foir/sdk/codegen` — the Foir graphql-codegen preset.
2
+ * `@foir/sdk/codegen` — Foir graphql-codegen preset + block-registry plugin.
6
3
  *
7
- * `foirCodegen()` returns a complete `@graphql-codegen/client-preset` config so
8
- * a consumer's `codegen.ts` is ~3 lines instead of the ~109 every storefront
9
- * hand-copies. It standardizes the Foir conventions (custom scalars,
10
- * `enumValues: 'keep'`, the `X-Foir-Schema-Channel: draft` introspection header)
11
- * AND wires the block-registry plugin so the exhaustive `BlockRegistry` type is
12
- * regenerated alongside the typed documents.
13
- *
14
- * `foirScaffold()` is the separate one-shot that writes the ContentRenderer
15
- * shell (run once; the dev owns the output).
16
- */
17
-
18
- interface FoirCodegenOptions {
19
- /** Foir public API GraphQL endpoint. */
20
- apiUrl: string;
21
- /**
22
- * Public (`pk_`) API key. Reads the published schema. Used as the fallback
23
- * when no preview key is set.
24
- */
25
- apiKey?: string;
26
- /**
27
- * Preview (`sk_`) key with `drafts:read`. Reads the DRAFT schema channel so
28
- * freshly pushed (un-promoted) model changes are visible to codegen.
29
- * Strongly preferred — a `pk_` key only sees the published schema.
30
- */
31
- previewKey?: string;
32
- /** Output directory for the generated client types. Default `./app/gql/foir/`. */
33
- outputDir?: string;
34
- /** Operation document glob(s). Default `app/graphql/foir/**\/*.graphql`. */
35
- documents?: string | string[];
36
- /** Block union name(s) for the registry. Default `['Block']`. */
37
- blockUnions?: string[];
38
- /** Generated registry file path. Default `<outputDir>block-registry.ts`. */
39
- registryFile?: string;
40
- /** Extra/override custom scalars merged onto the Foir defaults. */
41
- scalars?: Record<string, unknown>;
42
- }
43
- /**
44
- * Build the continuous Foir codegen config: typed documents + the exhaustive
45
- * block registry. Drop it into `codegen.ts` and run `graphql-codegen`.
46
- */
47
- declare function foirCodegen(options: FoirCodegenOptions): CodegenConfig;
48
- interface FoirScaffoldOptions extends FoirCodegenOptions {
49
- /** Output path for the scaffold. Default `<outputDir>ContentRenderer.tsx`. */
50
- scaffoldFile?: string;
51
- /** Render-context type threaded to every block component. Default `unknown`. */
52
- ctxType?: string;
53
- /** Optional ctx-type import line, e.g. `import type { Ctx } from '~/ctx'`. */
54
- ctxImport?: string;
55
- /** Import path for the generated registry type. Default `./block-registry`. */
56
- registryImport?: string;
57
- }
58
- /**
59
- * Build the one-shot scaffold config — writes a ContentRenderer shell with a
60
- * typed stub per block type. Run once (`graphql-codegen --config scaffold.ts`),
61
- * then own and implement the output.
4
+ * Build-time only. `@graphql-codegen/cli` + `@graphql-codegen/client-preset`
5
+ * are optional peer dependencies (the consumer's devDeps). The block-registry
6
+ * and block-scaffold plugins are wired by the preset and also available at
7
+ * `@foir/sdk/codegen/block-registry` / `@foir/sdk/codegen/block-scaffold`.
62
8
  */
63
- declare function foirScaffold(options: FoirScaffoldOptions): CodegenConfig;
64
-
65
- export { type FoirCodegenOptions, type FoirScaffoldOptions, foirCodegen, foirScaffold };
9
+ export { foirCodegen, foirScaffold } from './preset';
10
+ export type { FoirCodegenOptions, FoirScaffoldOptions } from './preset';
11
+ export type { BlockPluginConfig } from './shared';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `@foir/sdk/codegen` — the Foir graphql-codegen preset.
3
+ *
4
+ * `foirCodegen()` returns a complete `@graphql-codegen/client-preset` config so
5
+ * a consumer's `codegen.ts` is ~3 lines instead of the ~109 every storefront
6
+ * hand-copies. It standardizes the Foir conventions (custom scalars,
7
+ * `enumValues: 'keep'`, the `X-Foir-Schema-Channel: draft` introspection header)
8
+ * AND wires the block-registry plugin so the exhaustive `BlockRegistry` type is
9
+ * regenerated alongside the typed documents.
10
+ *
11
+ * `foirScaffold()` is the separate one-shot that writes the ContentRenderer
12
+ * shell (run once; the dev owns the output).
13
+ */
14
+ import type { CodegenConfig } from '@graphql-codegen/cli';
15
+ export interface FoirCodegenOptions {
16
+ /** Foir public API GraphQL endpoint. */
17
+ apiUrl: string;
18
+ /**
19
+ * Public (`pk_`) API key. Reads the published schema. Used as the fallback
20
+ * when no preview key is set.
21
+ */
22
+ apiKey?: string;
23
+ /**
24
+ * Preview (`sk_`) key with `drafts:read`. Reads the DRAFT schema channel so
25
+ * freshly pushed (un-promoted) model changes are visible to codegen.
26
+ * Strongly preferred — a `pk_` key only sees the published schema.
27
+ */
28
+ previewKey?: string;
29
+ /** Output directory for the generated client types. Default `./app/gql/foir/`. */
30
+ outputDir?: string;
31
+ /** Operation document glob(s). Default `app/graphql/foir/**\/*.graphql`. */
32
+ documents?: string | string[];
33
+ /** Block union name(s) for the registry. Default `['Block']`. */
34
+ blockUnions?: string[];
35
+ /** Generated registry file path. Default `<outputDir>block-registry.ts`. */
36
+ registryFile?: string;
37
+ /** Extra/override custom scalars merged onto the Foir defaults. */
38
+ scalars?: Record<string, unknown>;
39
+ }
40
+ /**
41
+ * Build the continuous Foir codegen config: typed documents + the exhaustive
42
+ * block registry. Drop it into `codegen.ts` and run `graphql-codegen`.
43
+ */
44
+ export declare function foirCodegen(options: FoirCodegenOptions): CodegenConfig;
45
+ export interface FoirScaffoldOptions extends FoirCodegenOptions {
46
+ /** Output path for the scaffold. Default `<outputDir>ContentRenderer.tsx`. */
47
+ scaffoldFile?: string;
48
+ /** Render-context type threaded to every block component. Default `unknown`. */
49
+ ctxType?: string;
50
+ /** Optional ctx-type import line, e.g. `import type { Ctx } from '~/ctx'`. */
51
+ ctxImport?: string;
52
+ /** Import path for the generated registry type. Default `./block-registry`. */
53
+ registryImport?: string;
54
+ }
55
+ /**
56
+ * Build the one-shot scaffold config — writes a ContentRenderer shell with a
57
+ * typed stub per block type. Run once (`graphql-codegen --config scaffold.ts`),
58
+ * then own and implement the output.
59
+ */
60
+ export declare function foirScaffold(options: FoirScaffoldOptions): CodegenConfig;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared introspection helpers for the Foir codegen plugins.
3
+ */
4
+ import { type GraphQLSchema } from 'graphql';
5
+ export interface BlockPluginConfig {
6
+ /**
7
+ * Union type name(s) to collect block members from. Defaults to `['Block']`
8
+ * (the canonical Foir block union). A project with a distinct content-field
9
+ * segment union lists it here too.
10
+ */
11
+ unions?: string[];
12
+ /** Import path for the codegen-generated types. Default `./graphql`. */
13
+ graphqlImport?: string;
14
+ /** Import path for the SDK content primitives. Default `@foir/sdk/content`. */
15
+ sdkImport?: string;
16
+ }
17
+ export declare const DEFAULT_UNIONS: string[];
18
+ export declare const DEFAULT_GRAPHQL_IMPORT = "./graphql";
19
+ export declare const DEFAULT_SDK_IMPORT = "@foir/sdk/content";
20
+ /**
21
+ * Collect the deduped, sorted set of object-type member names across the named
22
+ * union(s). Throws when none of the unions resolve to members — that almost
23
+ * always means a misconfigured union name or a schema that hasn't been pushed.
24
+ */
25
+ export declare function collectBlockMembers(schema: GraphQLSchema, unionNames: string[]): string[];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * EditorProvider - React context provider for Foir editor iframes.
3
+ *
4
+ * Wraps your editor root to handle postMessage lifecycle, theme sync,
5
+ * and provide the platform client via context.
6
+ */
7
+ import { type ReactNode } from 'react';
8
+ import type { EditorInit, EditorContextValue, LocaleContext } from '../types';
9
+ export declare const EditorContext: import("react").Context<EditorContextValue | null>;
10
+ export interface EditorProviderProps {
11
+ children: ReactNode;
12
+ /**
13
+ * Parent window origin for postMessage validation.
14
+ * Must be an explicit origin (e.g., 'https://admin.example.com').
15
+ * Falls back to VITE_PARENT_ORIGIN or NEXT_PUBLIC_PARENT_ORIGIN env var.
16
+ */
17
+ parentOrigin?: string;
18
+ /**
19
+ * Platform API URL for GraphQL requests.
20
+ * Falls back to VITE_API_URL or NEXT_PUBLIC_API_URL env var.
21
+ */
22
+ apiUrl?: string;
23
+ onInit?: (init: EditorInit) => void;
24
+ onValuesChanged?: (values: Record<string, unknown>) => void;
25
+ onVariantChanged?: (variantKey: string, values: Record<string, unknown>) => void;
26
+ onSaveResult?: (success: boolean, error?: string, versionId?: string) => void;
27
+ onPublishResult?: (success: boolean, error?: string) => void;
28
+ onThemeChanged?: (theme: 'light' | 'dark') => void;
29
+ onSelectionChanged?: (keys: string[], fields: Array<{
30
+ key: string;
31
+ label: string;
32
+ type: string;
33
+ }>) => void;
34
+ onLocaleChanged?: (locale: LocaleContext) => void;
35
+ }
36
+ export declare function EditorProvider({ children, parentOrigin: parentOriginProp, apiUrl: apiUrlProp, onInit, onValuesChanged, onVariantChanged, onSaveResult, onPublishResult, onThemeChanged, onSelectionChanged, onLocaleChanged, }: EditorProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * createContentRenderer — the generic content-dispatch primitive.
3
+ *
4
+ * It does NOT know any concrete block types. Given a registry that maps each
5
+ * block `__typename` to a component, it renders a `flexible` field's block
6
+ * array: looks each block up by `__typename`, renders unknown types as null,
7
+ * keys by the block `_id`, assigns LCP `priority` to the first block that
8
+ * actually renders, and threads a consumer-defined `Ctx` to every component.
9
+ *
10
+ * The "switch" is a typed object lookup. Exhaustiveness is enforced where the
11
+ * registry is BUILT — against the exhaustive `BlockRegistry` type the codegen
12
+ * plugin (EID-92) generates per project, so omitting a block type is a compile
13
+ * error there, not a runtime surprise here. The consumer can supply different
14
+ * registries per surface (e.g. web vs email).
15
+ */
16
+ import { type ReactNode } from 'react';
17
+ /** Minimal shape every block in a `flexible` field carries. */
18
+ export interface BlockLike {
19
+ __typename: string;
20
+ _id?: string | null;
21
+ }
22
+ /** Props a block component receives from the content renderer. */
23
+ export interface BlockComponentProps<TBlock, TCtx = unknown> {
24
+ block: TBlock;
25
+ /** Consumer-defined render context (host product, locale, etc.). */
26
+ ctx: TCtx;
27
+ /** True only for the first block that renders — the LCP candidate. */
28
+ priority: boolean;
29
+ }
30
+ /** A component that renders one block type. */
31
+ export type BlockComponent<TBlock, TCtx = unknown> = (props: BlockComponentProps<TBlock, TCtx>) => ReactNode;
32
+ /**
33
+ * A registry mapping `__typename` → block component. In practice this is the
34
+ * exhaustive `BlockRegistry` type the codegen plugin emits for the project;
35
+ * here it's the structural lower bound the renderer needs.
36
+ */
37
+ export type BlockRegistry<TCtx = unknown> = Record<string, BlockComponent<any, TCtx>>;
38
+ export interface ContentRendererProps<TCtx> {
39
+ blocks: readonly BlockLike[] | null | undefined;
40
+ /** Consumer-defined context passed to every block component. */
41
+ ctx: TCtx;
42
+ }
43
+ /**
44
+ * Build a `<ContentRenderer>` bound to a block registry. Returns a component
45
+ * that takes the `flexible` field's `blocks` array plus a `ctx`.
46
+ */
47
+ export declare function createContentRenderer<TCtx = unknown>(registry: BlockRegistry<TCtx>): (props: ContentRendererProps<TCtx>) => ReactNode;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@foir/sdk/content` — generic content-rendering primitives.
3
+ *
4
+ * Ships ONLY the framework-agnostic primitives: the block-dispatch renderer
5
+ * and a Lexical richtext renderer with injected typography tokens. Concrete
6
+ * block components, host/Shopify context, and layout systems stay app-side.
7
+ * `react` is a peer dependency.
8
+ */
9
+ export { createContentRenderer } from './content-renderer';
10
+ export type { BlockLike, BlockComponent, BlockComponentProps, BlockRegistry, ContentRendererProps, } from './content-renderer';
11
+ export { RichTextRenderer, RichTextTypographyProvider, useRichTextTypography, } from './richtext';
12
+ export type { RichTextRendererProps, TypographyToken, } from './richtext';