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.
- package/.playwright-mcp/console-2026-06-15T09-14-00-118Z.log +84 -0
- package/.playwright-mcp/console-2026-06-15T09-25-42-726Z.log +2 -0
- package/.playwright-mcp/console-2026-06-15T09-25-47-707Z.log +1 -0
- package/.playwright-mcp/page-2026-06-15T09-14-01-054Z.yml +13 -0
- package/CHANGELOG.md +14 -0
- package/README.md +144 -17
- package/dist/__tests__/createFontdueFetch.test.js +154 -3
- package/dist/__tests__/networkFetch.test.js +81 -2
- package/dist/__tests__/nextAdapter.test.js +249 -40
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/ConfigContext.d.ts +3 -0
- package/dist/components/ConfigContext.js +5 -2
- package/dist/components/FontdueAdminToolbar/index.js +72 -16
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/fontdue.css +59 -0
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +16 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +36 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +6 -4
- package/dist/next/tenant.js +122 -49
- package/dist/preview/constants.d.ts +2 -0
- package/dist/preview/constants.js +20 -1
- package/dist/relay/environment.d.ts +2 -0
- package/dist/relay/environment.js +67 -38
- package/dist/relay/serverConfig.d.ts +6 -4
- package/dist/relay/serverConfig.js +81 -19
- package/dist/server/index.d.ts +15 -3
- package/dist/server/index.js +77 -31
- package/package.json +1 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +4 -0
- 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
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
|
|
1
|
+
export { configureFontduePreview, fontdueEndpoint, __prepareFontdueRender, type FontdueEndpoint, } from './tenant.js';
|
package/dist/next/index.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
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
|
+
});
|
package/dist/next/revalidate.js
CHANGED
|
@@ -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
|
|
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
|
|
package/dist/next/tenant.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
24
|
-
export declare function
|
|
25
|
-
export declare function
|
|
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 {};
|
package/dist/next/tenant.js
CHANGED
|
@@ -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
|
-
//
|
|
24
|
-
// /graphql on the page's own
|
|
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 {
|
|
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
|
|
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
|
-
} =
|
|
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
|
|
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
|
-
//
|
|
114
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
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
|
-
//
|
|
144
|
-
// [domain] tree, where
|
|
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
|
|
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
|
|
150
|
-
var
|
|
151
|
-
const domain = (
|
|
152
|
-
if (domain) return
|
|
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
|
|
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
|
|
234
|
+
return endpointForDomain(new URL(requireSingleTenantUrl()).host);
|
|
157
235
|
}
|
|
158
236
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
|
42
|
-
// FontdueProvider server entrypoint
|
|
43
|
-
//
|
|
44
|
-
|
|
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
|
|
11
|
-
* (
|
|
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
|
|
19
|
+
export declare function getFontdueSlotConfig(): FontdueServerConfig | undefined;
|
|
20
|
+
export declare function resolveFontdueServerConfig(): Promise<FontdueServerConfig | undefined>;
|
|
19
21
|
export {};
|