fontdue-js 3.0.0-alpha11 → 3.0.0-alpha13

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 (35) hide show
  1. package/.playwright-mcp/console-2026-06-15T09-14-00-118Z.log +84 -0
  2. package/.playwright-mcp/console-2026-06-15T09-25-42-726Z.log +2 -0
  3. package/.playwright-mcp/console-2026-06-15T09-25-47-707Z.log +1 -0
  4. package/.playwright-mcp/page-2026-06-15T09-14-01-054Z.yml +13 -0
  5. package/CHANGELOG.md +14 -0
  6. package/README.md +144 -17
  7. package/dist/__tests__/createFontdueFetch.test.js +154 -3
  8. package/dist/__tests__/networkFetch.test.js +81 -2
  9. package/dist/__tests__/nextAdapter.test.js +249 -40
  10. package/dist/__tests__/serverConfig.test.js +62 -0
  11. package/dist/components/ConfigContext.d.ts +3 -0
  12. package/dist/components/ConfigContext.js +5 -2
  13. package/dist/components/FontdueAdminToolbar/index.js +72 -16
  14. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  15. package/dist/components/FontdueProvider/index.server.js +10 -0
  16. package/dist/fontdue.css +59 -0
  17. package/dist/next/index.d.ts +1 -2
  18. package/dist/next/index.js +16 -6
  19. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  20. package/dist/next/registerSingleTenantResolver.js +36 -0
  21. package/dist/next/revalidate.js +1 -1
  22. package/dist/next/tenant.d.ts +6 -4
  23. package/dist/next/tenant.js +122 -49
  24. package/dist/preview/constants.d.ts +2 -0
  25. package/dist/preview/constants.js +20 -1
  26. package/dist/relay/environment.d.ts +2 -0
  27. package/dist/relay/environment.js +67 -38
  28. package/dist/relay/serverConfig.d.ts +6 -4
  29. package/dist/relay/serverConfig.js +81 -19
  30. package/dist/server/index.d.ts +15 -3
  31. package/dist/server/index.js +77 -31
  32. package/package.json +1 -1
  33. package/types/next-headers.d.ts +9 -0
  34. package/types/next-navigation.d.ts +4 -0
  35. package/vitest.config.ts +5 -0
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { FontdueProvider_props } from './index.js';
3
+ import '../../next/registerSingleTenantResolver.js';
3
4
  export type { FontdueProvider_props } from './index.js';
4
5
  export type { FontdueProviderPreloadedQuery } from '../../loadFontdueProviderQuery.js';
5
6
  export type { FontdueServerConfig } from '../../relay/serverConfig.js';
@@ -3,6 +3,16 @@ import React from 'react';
3
3
  import FontdueProvider from './index.js';
4
4
  import loadFontdueProviderQueryImpl from '../../loadFontdueProviderQuery.js';
5
5
  import { setFontdueServerConfig } from '../../relay/serverConfig.js';
6
+ // Side-effect import: registers the single-tenant ambient config resolver so a
7
+ // single-tenant foundry that mounts <FontdueProvider> never has to call a
8
+ // per-render setup function. The module statically pulls in only browser-safe
9
+ // code and lazy-loads the Next request APIs (next/headers, next/navigation)
10
+ // behind a dynamic import that runs only when the resolver resolves config —
11
+ // keeping next/* off this react-server entrypoint's eager graph. The resolver
12
+ // no-ops in multi-tenant mode (which drives config through the slot) and is
13
+ // inert in the default build Astro/RR7 use. See
14
+ // ../../next/registerSingleTenantResolver.ts.
15
+ import '../../next/registerSingleTenantResolver.js';
6
16
  // Stub for the RSC export condition. The default `<FontdueProvider>` server
7
17
  // entrypoint (this file) awaits the query for consumers, so RSC users should
8
18
  // never call it manually. Re-exporting a throwing stub makes the mistake
package/dist/fontdue.css CHANGED
@@ -3431,6 +3431,65 @@ textarea.text-field__input {
3431
3431
  word-break: break-word;
3432
3432
  }
3433
3433
 
3434
+ .fontdue-admin-toolbar__actions {
3435
+ display: flex;
3436
+ flex-direction: column;
3437
+ gap: 6px;
3438
+ margin-top: 12px;
3439
+ }
3440
+
3441
+ .fontdue-admin-toolbar__action {
3442
+ display: inline-flex;
3443
+ align-items: center;
3444
+ justify-content: space-between;
3445
+ gap: 8px;
3446
+ width: 100%;
3447
+ margin: 0;
3448
+ padding: 7px 10px;
3449
+ font: inherit;
3450
+ font-size: 12px;
3451
+ text-align: left;
3452
+ text-decoration: none;
3453
+ color: #fff;
3454
+ background: transparent;
3455
+ border: 1px solid var(--fdat-border);
3456
+ border-radius: 0;
3457
+ cursor: pointer;
3458
+ }
3459
+ .fontdue-admin-toolbar__action:hover {
3460
+ border-color: rgba(255, 255, 255, 0.4);
3461
+ }
3462
+ .fontdue-admin-toolbar__action:focus-visible {
3463
+ outline: 1px solid #fff;
3464
+ outline-offset: 1px;
3465
+ }
3466
+ .fontdue-admin-toolbar__action:disabled {
3467
+ opacity: 0.5;
3468
+ cursor: default;
3469
+ }
3470
+
3471
+ .fontdue-admin-toolbar__notice {
3472
+ margin: 10px 0 0;
3473
+ color: var(--fdat-muted);
3474
+ font-size: 12px;
3475
+ }
3476
+
3477
+ .fontdue-admin-toolbar__meta {
3478
+ margin: 12px 0 0;
3479
+ padding-top: 10px;
3480
+ border-top: 1px solid var(--fdat-border);
3481
+ color: var(--fdat-muted);
3482
+ font-size: 11px;
3483
+ word-break: break-word;
3484
+ }
3485
+
3486
+ .fontdue-admin-toolbar__version {
3487
+ display: block;
3488
+ margin-top: 4px;
3489
+ color: rgba(255, 255, 255, 0.32);
3490
+ font-size: 11px;
3491
+ }
3492
+
3434
3493
  :root {
3435
3494
  --section_spacing: 60px;
3436
3495
  }
@@ -1,2 +1 @@
1
- export { isMultiTenant, isValidDomain, fontdueEndpoint, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams, type FontdueEndpoint, } from './tenant.js';
2
- export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
1
+ export { configureFontduePreview, fontdueEndpoint, __prepareFontdueRender, type FontdueEndpoint, } from './tenant.js';
@@ -2,9 +2,19 @@
2
2
  // The config-time wrapper lives in 'fontdue-js/next/config' and the deploy
3
3
  // hook route handler in 'fontdue-js/next/revalidate'.
4
4
 
5
- export { isMultiTenant, isValidDomain, fontdueEndpoint, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams } from './tenant.js';
6
-
7
- // The per-render config store consumed by fontdue-js's own server-side
8
- // fetches. configureFontdueRender covers the common case; these are exported
9
- // for apps that need to set or inspect the config directly.
10
- export { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
5
+ // Public foundry-facing API. Single-tenant apps need no per-render setup call
6
+ // mounting <FontdueProvider> registers the ambient resolver that configures
7
+ // every server fetch (see ../components/FontdueProvider/index.server.tsx ->
8
+ // registerSingleTenantResolver). configureFontduePreview() remains exported as
9
+ // an optional explicit hook (e.g. to get the resolved endpoint in a render);
10
+ // route handlers (sitemap, robots) read fontdueEndpoint() since they run
11
+ // outside React.
12
+ //
13
+ // __prepareFontdueRender is the internal multi-tenant API used only by the
14
+ // Fontdue-hosted next-template — not part of the foundry surface. The
15
+ // remaining tenant/config helpers (isMultiTenant, isValidDomain,
16
+ // endpointForDomain, fontdueServerConfig, configureFontdueRender,
17
+ // setFontdueServerConfig/getFontdueSlotConfig) are deliberately not re-exported
18
+ // here; the modules that need them import them directly from './tenant.js' /
19
+ // '../relay/serverConfig.js'.
20
+ export { configureFontduePreview, fontdueEndpoint, __prepareFontdueRender } from './tenant.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ // Registers the single-tenant ambient config resolver (env URL +
2
+ // draftMode()/cookies() preview token, resolved per fetch) so a single-tenant
3
+ // foundry that mounts <FontdueProvider> never has to call a per-render setup
4
+ // function. The FontdueProvider react-server entrypoint imports this module as
5
+ // a side effect — in the call-free model the app no longer imports
6
+ // 'fontdue-js/next' itself, so the provider mount is the only thing guaranteed
7
+ // to wire the resolver up.
8
+ //
9
+ // Why this lives apart from ./tenant.ts: it statically imports ONLY the
10
+ // browser-safe registerAmbientConfigResolver. The Next request APIs
11
+ // (next/headers, next/navigation) the resolver needs sit behind a lazy
12
+ // `import('./tenant.js')` in the resolver body, which runs only when a server
13
+ // fetch actually resolves config — i.e. only under Next. That keeps next/* off
14
+ // the provider entrypoint's eager import graph, so mounting <FontdueProvider>
15
+ // stays safe for any non-Next RSC consumer. (Today only Next resolves the
16
+ // react-server export condition, but the decoupling makes that a structural
17
+ // invariant rather than an incidental one.)
18
+ import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
19
+ registerAmbientConfigResolver(async () => {
20
+ // Loaded lazily so next/headers + next/navigation never enter the
21
+ // provider's static graph; config is read per fetch from Next's request
22
+ // context (draftMode()/cookies()), which also covers soft navigations that
23
+ // re-render only the page segment.
24
+ const {
25
+ isMultiTenant,
26
+ singleTenantUrl,
27
+ buildRenderConfig
28
+ } = await import('./tenant.js');
29
+ // Multi-tenant drives config through the React.cache slot
30
+ // (__prepareFontdueRender), which needs the per-request route param, so the
31
+ // resolver stays out of its way. The slot also wins the merge, so an
32
+ // explicit configureFontduePreview()/__prepareFontdueRender call still
33
+ // overrides this when present.
34
+ if (isMultiTenant || !singleTenantUrl) return undefined;
35
+ return buildRenderConfig(new URL(singleTenantUrl).host);
36
+ });
@@ -7,7 +7,7 @@
7
7
  // receive the tenant in the URL, e.g.
8
8
  // POST /api/revalidate?domain=acme.fontdue.com
9
9
  // and only that tenant's cache is purged (pages and embed data share the
10
- // per-domain tag — see fontdueEndpoint/fontdueServerConfig in ./tenant).
10
+ // per-domain tag — see endpointForDomain/fontdueServerConfig in ./tenant).
11
11
  // Single-tenant deployments use the parameterless form and purge everything
12
12
  // carrying the 'graphql' tag.
13
13
 
@@ -1,5 +1,6 @@
1
1
  import { type FontdueServerConfig } from '../relay/serverConfig.js';
2
2
  export declare const isMultiTenant: boolean;
3
+ export declare const singleTenantUrl: string | undefined;
3
4
  export declare function isValidDomain(domain: string): boolean;
4
5
  export interface FontdueEndpoint {
5
6
  /** The site domain this endpoint resolves. */
@@ -14,13 +15,14 @@ export interface FontdueEndpoint {
14
15
  */
15
16
  tags: string[];
16
17
  }
17
- export declare function fontdueEndpoint(domain: string): FontdueEndpoint;
18
+ export declare function endpointForDomain(domain: string): FontdueEndpoint;
18
19
  export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
19
20
  export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
20
21
  interface RenderProps {
21
22
  params: Promise<Record<string, string | string[] | undefined>>;
22
23
  }
23
- export declare function prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
24
- export declare function currentFontdueEndpoint(): FontdueEndpoint;
25
- export declare function generateStaticParams(): Promise<never[]>;
24
+ export declare function buildRenderConfig(domain: string): Promise<FontdueServerConfig>;
25
+ export declare function configureFontduePreview(): Promise<FontdueEndpoint>;
26
+ export declare function __prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
27
+ export declare function fontdueEndpoint(): FontdueEndpoint;
26
28
  export {};
@@ -20,14 +20,16 @@
20
20
  //
21
21
  // The fontdue-js components embedded in pages fetch the same way: their
22
22
  // server-side preloads read the per-render config set by
23
- // configureFontdueRender, and in the browser they fetch the relative
24
- // /graphql on the page's own origin — so multi-tenant mode needs no
25
- // NEXT_PUBLIC_FONTDUE_URL at all.
23
+ // __prepareFontdueRender (or configureFontduePreview in single-tenant apps),
24
+ // and in the browser they fetch the relative /graphql on the page's own
25
+ // origin — so multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
26
26
 
27
- import { notFound } from 'next/navigation';
28
- import { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
27
+ import { notFound, unstable_rethrow } from 'next/navigation';
28
+ import { cookies, draftMode } from 'next/headers';
29
+ import { setFontdueServerConfig, getFontdueSlotConfig } from '../relay/serverConfig.js';
30
+ import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from '../preview/index.js';
29
31
  export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
30
- const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
32
+ export const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
31
33
  const internalOrigin = process.env.FONTDUE_ORIGIN;
32
34
  const proxySecret = process.env.FONTDUE_PROXY_SECRET;
33
35
 
@@ -39,7 +41,7 @@ export function isValidDomain(domain) {
39
41
  }
40
42
  // Where to fetch GraphQL for a given tenant domain, plus any headers needed
41
43
  // for the Fontdue server to resolve that tenant.
42
- export function fontdueEndpoint(domain) {
44
+ export function endpointForDomain(domain) {
43
45
  const tags = ['graphql', `graphql:${domain}`];
44
46
  if (!isMultiTenant) {
45
47
  return {
@@ -84,7 +86,7 @@ export function fontdueServerConfig(domain) {
84
86
  const {
85
87
  origin,
86
88
  headers
87
- } = fontdueEndpoint(domain);
89
+ } = endpointForDomain(domain);
88
90
  return {
89
91
  url: origin,
90
92
  headers,
@@ -104,66 +106,137 @@ export function fontdueServerConfig(domain) {
104
106
  export function configureFontdueRender(domain) {
105
107
  if (!isValidDomain(domain)) return null;
106
108
  setFontdueServerConfig(fontdueServerConfig(domain));
107
- return fontdueEndpoint(domain);
109
+ return endpointForDomain(domain);
108
110
  }
109
111
 
110
112
  // The props every page, layout and generateMetadata receives. Any params
111
113
  // object is structurally compatible; only `domain` is read.
112
114
 
113
- // The one line at the top of every page, layout and generateMetadata body
114
- // in the [domain] route tree:
115
+ // Build this render's server config for `domain` (origin, headers, per-site
116
+ // cache tags) and install it for the rest of the pass. When a logged-in admin
117
+ // is in preview (Next draft mode), fold the token into the headers and drop the
118
+ // cache tags, so every server fetch in the render — the app's own and the
119
+ // embedded components' preloads — reveals hidden fonts and stays live
120
+ // (uncached, never landing in a shared cache).
121
+ export async function buildRenderConfig(domain) {
122
+ const config = fontdueServerConfig(domain);
123
+ const previewHeaders = await readPreviewHeaders();
124
+ return previewHeaders ? {
125
+ ...config,
126
+ headers: {
127
+ ...config.headers,
128
+ ...previewHeaders
129
+ },
130
+ cacheTags: undefined
131
+ } : config;
132
+ }
133
+ async function applyRenderConfig(domain) {
134
+ setFontdueServerConfig(await buildRenderConfig(domain));
135
+ }
136
+
137
+ // Single-tenant render setup — the one line at the top of every page, layout
138
+ // and generateMetadata that fetches Fontdue data or renders Fontdue components:
139
+ //
140
+ // await configureFontduePreview();
115
141
  //
116
- // await prepareFontdueRender(props);
142
+ // It points this render's server-side fetches — your own (createFontdueFetch)
143
+ // AND the embedded components' preloads (type testers, store, character viewer)
144
+ // — at your Fontdue site (NEXT_PUBLIC_FONTDUE_URL), and when a logged-in admin
145
+ // has entered preview it forwards their token and serves a live (uncached)
146
+ // render so hidden (unpublished) fonts show up. Public renders stay cached and
147
+ // tagged for /api/revalidate.
117
148
  //
118
- // Reads the request's site from the [domain] route param, 404s anything
119
- // that isn't a plain hostname (stray paths can reach the catch-all route
120
- // with their first segment as the "domain"), and points every Fontdue fetch
121
- // in the rest of this render pass — the app's fetches via
122
- // currentFontdueEndpoint and fontdue-js's embedded-component preloads — at
123
- // that site.
149
+ // Call it per entry point, not just the layout: a soft navigation re-renders
150
+ // only the page segment with a fresh render store. Reading draft mode is
151
+ // static-safe it only forces dynamic rendering when an admin is actually
152
+ // previewing, so static generation keeps working otherwise.
153
+ //
154
+ // Returns the endpoint, handy for e.g. a metadataBase fallback. Route handlers
155
+ // (robots/sitemap) aren't React renders, so call fontdueEndpoint()
156
+ // there instead.
157
+ export async function configureFontduePreview() {
158
+ const domain = new URL(requireSingleTenantUrl()).host;
159
+ await applyRenderConfig(domain);
160
+ return endpointForDomain(domain);
161
+ }
162
+
163
+ // Internal multi-tenant API (double-underscored, not part of the public
164
+ // foundry surface): the one line at the top of every page, layout and
165
+ // generateMetadata body in the multi-tenant [domain] route tree (the
166
+ // Fontdue-hosted service):
124
167
  //
125
- // It must run per entry point, not only in the layout: a soft navigation
126
- // re-renders just the page segment, and the per-render store starts empty
127
- // on every pass. Forgetting it is loud, not subtle: currentFontdueEndpoint
128
- // throws in multi-tenant mode when no render config was set.
168
+ // await __prepareFontdueRender(props);
129
169
  //
130
- // Route handlers are not React renders the render-scoped store doesn't
131
- // exist there, so use the returned endpoint explicitly instead of relying
132
- // on currentFontdueEndpoint.
133
- export async function prepareFontdueRender(props) {
170
+ // Reads the request's site from the route param, 404s anything that isn't a
171
+ // plain hostname, and configures the render (including preview) for that site.
172
+ // Like configureFontduePreview, it runs per entry point. Route handlers are not
173
+ // React renders use the returned endpoint explicitly there.
174
+ export async function __prepareFontdueRender(props) {
134
175
  const {
135
176
  domain
136
177
  } = await props.params;
137
- const endpoint = typeof domain === 'string' ? configureFontdueRender(domain) : null;
138
- if (!endpoint) notFound();
139
- return endpoint;
178
+ if (typeof domain !== 'string' || !isValidDomain(domain)) notFound();
179
+ await applyRenderConfig(domain);
180
+ return endpointForDomain(domain);
181
+ }
182
+
183
+ // The admin preview token for this render, as Authorization headers, or
184
+ // undefined when not previewing. Draft mode gates it: reading draftMode().
185
+ // isEnabled is static-safe (it returns false during static generation and only
186
+ // forces dynamic rendering when actually enabled), and the token cookie is only
187
+ // read in the preview branch.
188
+ //
189
+ // The catch handles two very different throws, and the difference is load-
190
+ // bearing for preview:
191
+ //
192
+ // 1. Next's dynamic-rendering bailout. When an admin is previewing, reading
193
+ // cookies() during a prerender/static pass throws a control-flow error
194
+ // (DynamicServerError / prerender interrupt) whose whole purpose is to
195
+ // tell Next "abandon the static render and go dynamic." This is the ONLY
196
+ // signal that takes a route in generateStaticParams off the full-route
197
+ // cache for the preview request. Swallowing it (as this used to) let Next
198
+ // serve the cached, token-less *public* render in preview, so the embedded
199
+ // components' own server preloads (createNetworkFetch) never saw the token
200
+ // — hidden fonts stayed hidden and @required(THROW) embeds like
201
+ // <BuyButton> crashed the server render (FD-712). unstable_rethrow re-
202
+ // throws exactly these control-flow errors so the route bails to a
203
+ // dynamic, token-bearing render. Public renders never hit this branch
204
+ // (draftMode().isEnabled is false → early return), so they stay static.
205
+ // 2. A genuine "no request scope" throw (older runtimes calling
206
+ // draftMode()/cookies() with no request at all): not a control-flow error,
207
+ // so unstable_rethrow is a no-op and we fall through to "not previewing".
208
+ async function readPreviewHeaders() {
209
+ try {
210
+ var _await$cookies$get;
211
+ if (!(await draftMode()).isEnabled) return undefined;
212
+ const token = (_await$cookies$get = (await cookies()).get(PREVIEW_TOKEN_COOKIE)) === null || _await$cookies$get === void 0 ? void 0 : _await$cookies$get.value;
213
+ return token ? previewAuthHeaders(token) : undefined;
214
+ } catch (error) {
215
+ unstable_rethrow(error);
216
+ return undefined;
217
+ }
140
218
  }
141
219
 
142
220
  // Endpoint for the app's own GraphQL fetches in this render pass: whatever
143
- // prepareFontdueRender configured, or — in a single-tenant app without the
144
- // [domain] tree, where prepareFontdueRender is never called — the
221
+ // __prepareFontdueRender configured, or — in a single-tenant app without the
222
+ // [domain] tree, where __prepareFontdueRender is never called — the
145
223
  // NEXT_PUBLIC_FONTDUE_URL site. Throwing rather than guessing in
146
- // multi-tenant mode turns a forgotten prepareFontdueRender into an
224
+ // multi-tenant mode turns a forgotten __prepareFontdueRender into an
147
225
  // unmissable error instead of a silent wrong-site fetch on soft
148
226
  // navigations.
149
- export function currentFontdueEndpoint() {
150
- var _getFontdueServerConf;
151
- const domain = (_getFontdueServerConf = getFontdueServerConfig()) === null || _getFontdueServerConf === void 0 ? void 0 : _getFontdueServerConf.domain;
152
- if (domain) return fontdueEndpoint(domain);
227
+ export function fontdueEndpoint() {
228
+ var _getFontdueSlotConfig;
229
+ const domain = (_getFontdueSlotConfig = getFontdueSlotConfig()) === null || _getFontdueSlotConfig === void 0 ? void 0 : _getFontdueSlotConfig.domain;
230
+ if (domain) return endpointForDomain(domain);
153
231
  if (isMultiTenant) {
154
- throw new Error('fontdue-js/next: no render config set — call prepareFontdueRender(props) at the top of every page, layout and generateMetadata that fetches.');
232
+ throw new Error('fontdue-js/next: no render config set — call __prepareFontdueRender(props) at the top of every page, layout and generateMetadata that fetches.');
155
233
  }
156
- return fontdueEndpoint(new URL(requireSingleTenantUrl()).host);
234
+ return endpointForDomain(new URL(requireSingleTenantUrl()).host);
157
235
  }
158
236
 
159
- // Re-export from routes in the [domain] tree:
160
- //
161
- // export { generateStaticParams } from 'fontdue-js/next';
162
- //
163
- // Domains aren't known at build time, so nothing is prerendered — but
164
- // providing generateStaticParams is what opts a dynamic route into
165
- // static-on-demand rendering (generated on first request, then cached until
166
- // revalidated) instead of fully dynamic rendering.
167
- export async function generateStaticParams() {
168
- return [];
169
- }
237
+ // The single-tenant ambient resolver that lets a foundry skip per-render setup
238
+ // is registered from ./registerSingleTenantResolver.ts (imported as a side
239
+ // effect by the FontdueProvider react-server entrypoint), not here — so this
240
+ // module's static next/* imports stay off that entrypoint's eager graph. The
241
+ // resolver lazy-imports buildRenderConfig + singleTenantUrl + isMultiTenant
242
+ // from this module at fetch time.
@@ -1,3 +1,5 @@
1
1
  export declare const PREVIEW_TOKEN_COOKIE = "fontdue_preview_token";
2
2
  export declare const PREVIEW_MARKER_COOKIE = "fontdue_preview";
3
3
  export declare const PREVIEW_ENDPOINT = "/api/preview";
4
+ export declare const PREVIEW_HEADER = "fontdue-preview";
5
+ export declare function hasPreviewMarkerCookie(): boolean;
@@ -15,4 +15,23 @@ export const PREVIEW_MARKER_COOKIE = 'fontdue_preview';
15
15
  // Default path the toolbar POSTs (enter) / DELETEs (exit) to. Relative, so it
16
16
  // hits the storefront's own origin. Override per app via
17
17
  // FontdueConfig.preview.endpoint (and mount the route to match).
18
- export const PREVIEW_ENDPOINT = '/api/preview';
18
+ export const PREVIEW_ENDPOINT = '/api/preview';
19
+
20
+ // Request header storefront fetches send to declare preview intent explicitly.
21
+ // The GraphQL server reveals hidden (unpublished) fonts only when this is
22
+ // "true" AND the request is an authenticated admin — so merely *being* logged
23
+ // in (an admin session cookie riding a storefront fetch) never leaks hidden
24
+ // fonts outside preview. "false" forces the public view even for an admin; an
25
+ // absent header (older fontdue-js clients) keeps the legacy "any admin sees
26
+ // hidden" behavior, so older integrations are unaffected. See the matching
27
+ // server logic in FontageWeb.Schema.Context.
28
+ export const PREVIEW_HEADER = 'fontdue-preview';
29
+
30
+ // Whether the readable preview marker cookie is set on the current document.
31
+ // Browser-only (guarded so it stays inert during server rendering): lets the
32
+ // admin toolbar and the client-side Relay network layer tell that an admin is
33
+ // actively previewing, so storefront fetches can send PREVIEW_HEADER "true"
34
+ // while previewing and "false" otherwise.
35
+ export function hasPreviewMarkerCookie() {
36
+ return typeof document !== 'undefined' && document.cookie.split('; ').some(c => c === `${PREVIEW_MARKER_COOKIE}=1`);
37
+ }
@@ -1,4 +1,6 @@
1
1
  import { Environment, RequestParameters, QueryResponseCache, Variables, GraphQLResponse } from 'relay-runtime';
2
+ export declare const version: string;
3
+ export declare function fontdueBaseUrl(): string | undefined;
2
4
  export declare function createNetworkFetch(options?: CreateRelayEnvironmentOptions): (request: RequestParameters, variables: Variables) => Promise<GraphQLResponse>;
3
5
  export declare const networkFetch: (request: RequestParameters, variables: Variables) => Promise<GraphQLResponse>;
4
6
  export declare const responseCache: QueryResponseCache | null;
@@ -1,10 +1,13 @@
1
1
  import { Environment, Network, RecordSource, Store, QueryResponseCache } from 'relay-runtime';
2
2
  import { handlePossibleCorsError } from '../corsError.js';
3
- import { getFontdueServerConfig } from './serverConfig.js';
3
+ import { resolveFontdueServerConfig } from './serverConfig.js';
4
+ import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js';
4
5
 
5
6
  // `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
6
7
  // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
7
- const version = "3.0.0-alpha11";
8
+ // Exported so UI (the admin toolbar) can surface it without re-reading the
9
+ // build-time global in a 'use client' module.
10
+ export const version = "3.0.0-alpha13";
8
11
  const IS_SERVER = typeof window === typeof undefined;
9
12
 
10
13
  // Read env from either process.env (Node/Next.js) or import.meta.env (Vite/Astro).
@@ -36,51 +39,77 @@ const FONTDUE_URL = readEnv('FONTDUE_URL') ?? readEnv('NEXT_PUBLIC_FONTDUE_URL')
36
39
  const STRIPE_INTEGRATION = readEnv('FONTDUE_STRIPE_INTEGRATION') ?? readEnv('NEXT_PUBLIC_FONTDUE_STRIPE_INTEGRATION') ?? NEXT_PUBLIC_STRIPE;
37
40
  const CACHE_TTL = 10 * 1000; // 10 seconds, to resolve preloaded results
38
41
 
42
+ // The configured Fontdue base URL resolved for the current runtime, or
43
+ // undefined when it can't be determined (e.g. multi-tenant, where fetches go to
44
+ // the page's own origin). The admin toolbar uses it to show where the admin is
45
+ // signed in and to link back to the Fontdue admin.
46
+ export function fontdueBaseUrl() {
47
+ return FONTDUE_URL || undefined;
48
+ }
39
49
  export function createNetworkFetch(options) {
40
50
  return async function networkFetch(request, variables) {
41
- // Per-render server config (set via setFontdueServerConfig or the
42
- // FontdueProvider server entrypoint). Resolved per call, not per
43
- // createNetworkFetch, because module-level fetchers outlive renders.
44
- const serverConfig = IS_SERVER ? getFontdueServerConfig() : undefined;
51
+ // Per-render server config (the RSC slot set via setFontdueServerConfig /
52
+ // the FontdueProvider server entrypoint, the runWithPreview AsyncLocalStorage
53
+ // store, or the Next single-tenant ambient resolver). Awaited per call, not
54
+ // per createNetworkFetch, because module-level fetchers outlive renders — so
55
+ // an embedded component's own preload reveals hidden fonts in preview even
56
+ // on a page that never calls the app's GraphQL fetcher.
57
+ const serverConfig = IS_SERVER ? await resolveFontdueServerConfig() : undefined;
45
58
  const base = (options === null || options === void 0 ? void 0 : options.url) ?? (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.url) ?? FONTDUE_URL;
46
59
  if (IS_SERVER && (base == null || base === '')) {
47
60
  throw new Error('fontdue-js: no Fontdue URL configured for server-side fetch. ' + 'Set FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL in your environment, ' + 'pass `url` to loadSerializableQuery, or call setFontdueServerConfig ' + '(or render <FontdueProvider url=…>) earlier in the server render.');
48
61
  }
49
62
  const url = `${base ?? ''}/graphql`;
63
+ const headers = {
64
+ ...(serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.headers),
65
+ // Per-call headers (e.g. a preview Authorization: Bearer token passed to
66
+ // a preload helper) override the render-store headers.
67
+ ...(options === null || options === void 0 ? void 0 : options.headers),
68
+ Accept: 'application/json',
69
+ 'Content-Type': 'application/json',
70
+ 'fontdue-stripe-integration': (options === null || options === void 0 ? void 0 : options.stripeIntegration) ?? STRIPE_INTEGRATION ?? 'dynamic',
71
+ 'fontdue-client-version': version
72
+ };
73
+
74
+ // Whether this request is an admin *preview*. On the server that's a
75
+ // forwarded admin token; in the browser it's the readable preview marker
76
+ // cookie — NOT a bare admin session cookie that happens to ride the fetch
77
+ // (that conflation was the leak: a logged-in admin browsing normally would
78
+ // see hidden fonts). It's sent explicitly as the `fontdue-preview` header so
79
+ // the GraphQL server reveals hidden (unpublished) fonts only in preview:
80
+ // "false" forces the public view even for a logged-in admin, and an absent
81
+ // header (older clients) keeps the legacy "any admin sees hidden" behavior.
82
+ const previewing = IS_SERVER ? headers.authorization != null || headers.Authorization != null : hasPreviewMarkerCookie();
83
+ headers[PREVIEW_HEADER] = previewing ? 'true' : 'false';
84
+
85
+ // Public server fetches are all site content (per-session data is fetched
86
+ // client-side only), so opt them into Next's data cache — without this,
87
+ // Next 15 treats them as no-store and silently makes every page fully
88
+ // dynamic. The tags let the revalidate handler purge them. A preview render,
89
+ // though, carries an admin Authorization token: those fetches must stay live
90
+ // and never be written to or served from the shared data cache (a public
91
+ // request could otherwise be handed the hidden-fonts response), so opt them
92
+ // out with no-store. Both hints are inert outside Next.
93
+ const init = {
94
+ method: 'POST',
95
+ credentials: 'include',
96
+ headers,
97
+ body: JSON.stringify({
98
+ query: request.text,
99
+ variables
100
+ })
101
+ };
102
+ if (IS_SERVER && previewing) {
103
+ init.cache = 'no-store';
104
+ } else if (IS_SERVER) {
105
+ init.cache = 'force-cache';
106
+ init.next = {
107
+ tags: ['graphql', ...((serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.cacheTags) ?? []), `operation:${request.name}`]
108
+ };
109
+ }
50
110
  for (let attempt = 0; attempt <= 2; attempt++) {
51
111
  try {
52
- const resp = await fetch(url + `?queryName=${request.name}`, {
53
- method: 'POST',
54
- credentials: 'include',
55
- headers: {
56
- ...(serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.headers),
57
- // Per-call headers (e.g. a preview Authorization: Bearer token
58
- // passed to a preload helper) override the render-store headers.
59
- ...(options === null || options === void 0 ? void 0 : options.headers),
60
- Accept: 'application/json',
61
- 'Content-Type': 'application/json',
62
- 'fontdue-stripe-integration': (options === null || options === void 0 ? void 0 : options.stripeIntegration) ?? STRIPE_INTEGRATION ?? 'dynamic',
63
- 'fontdue-client-version': version
64
- },
65
- body: JSON.stringify({
66
- query: request.text,
67
- variables
68
- }),
69
- // Server-side fetches are all site content (per-session data is
70
- // fetched client-side only), so opt them into Next's data cache —
71
- // without this, Next 15 treats them as no-store and silently makes
72
- // every page fully dynamic. The tags below let the revalidate
73
- // handler purge them when content changes. Inert outside Next:
74
- // Node's fetch accepts and ignores this cache mode, and the
75
- // browser path doesn't set it.
76
- ...(IS_SERVER ? {
77
- cache: 'force-cache'
78
- } : {}),
79
- // @ts-ignore
80
- next: {
81
- tags: ['graphql', ...((serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.cacheTags) ?? []), `operation:${request.name}`]
82
- }
83
- });
112
+ const resp = await fetch(url + `?queryName=${request.name}`, init);
84
113
  const json = await resp.json();
85
114
 
86
115
  // GraphQL returns exceptions (for example, a missing required variable) in the "errors"
@@ -7,13 +7,15 @@ export interface FontdueServerConfig {
7
7
  cacheTags?: string[];
8
8
  /**
9
9
  * The site domain this render is for, when known. Set by
10
- * fontdue-js/next's prepareFontdueRender so render-scoped helpers
11
- * (currentFontdueEndpoint) can recover it.
10
+ * fontdue-js/next's __prepareFontdueRender so render-scoped helpers
11
+ * (fontdueEndpoint) can recover it.
12
12
  */
13
13
  domain?: string;
14
14
  }
15
+ type MaybePromise<T> = T | Promise<T>;
16
+ type AmbientConfigResolver = () => MaybePromise<FontdueServerConfig | undefined>;
15
17
  export declare function setFontdueServerConfig(config: FontdueServerConfig): void;
16
- type AmbientConfigResolver = () => FontdueServerConfig | undefined;
17
18
  export declare function registerAmbientConfigResolver(resolver: AmbientConfigResolver): void;
18
- export declare function getFontdueServerConfig(): FontdueServerConfig | undefined;
19
+ export declare function getFontdueSlotConfig(): FontdueServerConfig | undefined;
20
+ export declare function resolveFontdueServerConfig(): Promise<FontdueServerConfig | undefined>;
19
21
  export {};