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
|
@@ -25,33 +25,70 @@
|
|
|
25
25
|
// and the environment variables remain the configuration source.
|
|
26
26
|
|
|
27
27
|
import * as React from 'react';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
|
|
29
|
+
// This module owns two pieces of cross-cutting state — the render-scoped slot
|
|
30
|
+
// and the ambient resolver — and both must be shared by every server-side
|
|
31
|
+
// fetch in a render. The catch: a Next.js server build can bundle this same
|
|
32
|
+
// file into more than one server chunk (e.g. the app/provider chunk and a
|
|
33
|
+
// separate per-embed chunk), and those copies do NOT share module-level state.
|
|
34
|
+
// That split is silent until it bites: the ambient resolver is registered once
|
|
35
|
+
// (when <FontdueProvider> mounts) but an embedded component's own preload runs
|
|
36
|
+
// from the OTHER chunk's copy — so it never sees the resolver, fetches without
|
|
37
|
+
// the preview token, and a hidden-font reveal fails (a <BuyButton> node(id)
|
|
38
|
+
// @required(THROW) preload then crashes the server render — FD-712).
|
|
39
|
+
//
|
|
40
|
+
// Anchor both on globalThis so every duplicate instance shares one slot factory
|
|
41
|
+
// and one resolver. Same reasoning as the global Relay-environment singleton —
|
|
42
|
+
// see components/FontdueContextProvider.
|
|
43
|
+
|
|
44
|
+
const STORE_KEY = '__fontdueServerConfigStore__';
|
|
45
|
+
function store() {
|
|
46
|
+
const g = globalThis;
|
|
47
|
+
let s = g[STORE_KEY];
|
|
48
|
+
if (!s) {
|
|
49
|
+
// React.cache() scopes the slot to one server render pass (and so isolates
|
|
50
|
+
// concurrent requests from each other). Creating it once and sharing the
|
|
51
|
+
// single instance keeps that per-render isolation while defeating the
|
|
52
|
+
// chunk duplication. Outside an RSC render React.cache is unavailable, so
|
|
53
|
+
// the slot degrades to a throwaway object: writes no-op and the
|
|
54
|
+
// environment variables remain the configuration source.
|
|
55
|
+
s = {
|
|
56
|
+
getSlot: typeof React.cache === 'function' ? React.cache(() => ({
|
|
57
|
+
current: undefined
|
|
58
|
+
})) : () => ({
|
|
59
|
+
current: undefined
|
|
60
|
+
}),
|
|
61
|
+
ambientResolver: undefined
|
|
62
|
+
};
|
|
63
|
+
g[STORE_KEY] = s;
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
33
67
|
export function setFontdueServerConfig(config) {
|
|
34
|
-
getSlot().current = config;
|
|
68
|
+
store().getSlot().current = config;
|
|
35
69
|
}
|
|
36
70
|
|
|
37
71
|
// Ambient config resolver — the seam that lets a request-scoped store outside
|
|
38
|
-
// RSC
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
// the RSC slot feed config into server fetches without this browser-safe module
|
|
73
|
+
// importing a Node builtin. Two registrants today:
|
|
74
|
+
// - fontdue-js/preview/server (Astro/RR7): a synchronous resolver reading an
|
|
75
|
+
// AsyncLocalStorage store set by runWithPreview.
|
|
76
|
+
// - fontdue-js/next (single-tenant): an asynchronous resolver that reads the
|
|
77
|
+
// env URL plus Next's draftMode()/cookies() per fetch — so a single-tenant
|
|
78
|
+
// foundry never calls a per-render setup function.
|
|
79
|
+
// A server-only module registers a resolver on import; if nothing registers,
|
|
80
|
+
// resolution is unchanged. The RSC slot still wins on merge, so multi-tenant
|
|
81
|
+
// Next (which drives config through the slot via __prepareFontdueRender) is
|
|
82
|
+
// unaffected.
|
|
45
83
|
export function registerAmbientConfigResolver(resolver) {
|
|
46
|
-
ambientResolver = resolver;
|
|
84
|
+
store().ambientResolver = resolver;
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
|
|
87
|
+
// Merge an ambient config under the RSC slot: the slot wins for scalar fields;
|
|
88
|
+
// headers merge with the slot overriding.
|
|
89
|
+
function mergeServerConfig(ambient, slot) {
|
|
52
90
|
if (!ambient) return slot;
|
|
53
91
|
if (!slot) return ambient;
|
|
54
|
-
// RSC slot wins for scalar fields; headers merge with the slot overriding.
|
|
55
92
|
return {
|
|
56
93
|
...ambient,
|
|
57
94
|
...slot,
|
|
@@ -60,4 +97,29 @@ export function getFontdueServerConfig() {
|
|
|
60
97
|
...slot.headers
|
|
61
98
|
}
|
|
62
99
|
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Slot-only read of the render-scoped config. It deliberately does NOT run the
|
|
103
|
+
// ambient resolver: the single-tenant Next resolver is async (it awaits
|
|
104
|
+
// draftMode()/cookies()), so a synchronous read could never reflect it without
|
|
105
|
+
// silently dropping the preview token. Anything that needs the resolved
|
|
106
|
+
// url/headers — every server-side fetch — must use resolveFontdueServerConfig()
|
|
107
|
+
// instead. This exists only to recover the multi-tenant `domain` the slot
|
|
108
|
+
// carries (set by __prepareFontdueRender), which fontdueEndpoint() reads from a
|
|
109
|
+
// synchronous path.
|
|
110
|
+
export function getFontdueSlotConfig() {
|
|
111
|
+
return store().getSlot().current;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Asynchronous read used by the server-side fetch paths (the Relay network
|
|
115
|
+
// layer and createFontdueFetch). Awaits the ambient resolver so a resolver that
|
|
116
|
+
// reads request-scoped async APIs (Next's draftMode()/cookies()) can feed
|
|
117
|
+
// config into every server fetch with no per-render call, then merges it under
|
|
118
|
+
// the slot.
|
|
119
|
+
export async function resolveFontdueServerConfig() {
|
|
120
|
+
var _s$ambientResolver;
|
|
121
|
+
const s = store();
|
|
122
|
+
const slot = s.getSlot().current;
|
|
123
|
+
const ambient = await ((_s$ambientResolver = s.ambientResolver) === null || _s$ambientResolver === void 0 ? void 0 : _s$ambientResolver.call(s));
|
|
124
|
+
return mergeServerConfig(ambient, slot);
|
|
63
125
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -5,16 +5,28 @@ export declare class FontdueNotFoundError extends Error {
|
|
|
5
5
|
export interface CreateFontdueFetchOptions {
|
|
6
6
|
/**
|
|
7
7
|
* GraphQL base URL (without the trailing /graphql), e.g.
|
|
8
|
-
* https://acme.fontdue.com.
|
|
9
|
-
*
|
|
8
|
+
* https://acme.fontdue.com. Falls back to the per-render config
|
|
9
|
+
* (resolveFontdueServerConfig().url, set by the Next adapter for the current
|
|
10
|
+
* tenant) and then to FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL
|
|
11
|
+
* from the environment.
|
|
10
12
|
*/
|
|
11
13
|
url?: string;
|
|
12
14
|
/**
|
|
13
|
-
* Extra headers sent with every fetch from this fetcher
|
|
15
|
+
* Extra headers sent with every fetch from this fetcher, merged over the
|
|
16
|
+
* ambient per-render headers (explicit winning). Pass
|
|
14
17
|
* `previewAuthHeaders(token)` (from fontdue-js/preview) to reveal hidden
|
|
15
18
|
* fonts while an admin is in preview.
|
|
16
19
|
*/
|
|
17
20
|
headers?: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Next.js data-cache tags. When present — passed here or via the per-render
|
|
23
|
+
* config — the fetch is opted into Next's data cache so the revalidate
|
|
24
|
+
* handler can purge it. The global `graphql` tag is always included (and
|
|
25
|
+
* deduped), so pass the per-site tags alone or the full `endpoint.tags`.
|
|
26
|
+
* Omit (or pass `[]`) to leave the fetch uncached, which is what preview
|
|
27
|
+
* renders want. Inert outside Next.
|
|
28
|
+
*/
|
|
29
|
+
cacheTags?: string[];
|
|
18
30
|
}
|
|
19
31
|
export type FontdueFetch = <Q, V = void>(queryName: string, query: string, variables?: V) => Promise<Q>;
|
|
20
32
|
/**
|
package/dist/server/index.js
CHANGED
|
@@ -2,34 +2,50 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Every framework example ships a near-identical `fetchGraphql` — resolve the
|
|
4
4
|
// Fontdue URL, POST the query, unwrap `data`, throw on errors. This exports
|
|
5
|
-
// that transport once so apps don't re-implement it,
|
|
6
|
-
// Authorization header is forwarded in one place
|
|
7
|
-
//
|
|
5
|
+
// that transport once so apps don't re-implement it, so the preview
|
|
6
|
+
// Authorization header is forwarded in one place, and so the same fetcher
|
|
7
|
+
// works in every server runtime. It is the sibling of the Relay network layer
|
|
8
|
+
// (relay/environment.ts) that powers the embedded components: both read the
|
|
9
|
+
// per-render config from resolveFontdueServerConfig() and apply it the same way.
|
|
8
10
|
//
|
|
9
|
-
//
|
|
10
|
-
// resolved per call
|
|
11
|
+
// All three per-request inputs — the base URL, the admin preview token, and
|
|
12
|
+
// the Next cache tags — are resolved per call from two sources, so a single
|
|
13
|
+
// module-level fetcher serves every render:
|
|
11
14
|
//
|
|
12
|
-
// 1. Ambient context:
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
+
// 1. Ambient context (preferred): whatever set the per-render config.
|
|
16
|
+
// - Astro / React Router etc.: runWithPreview (fontdue-js/preview/server)
|
|
17
|
+
// rides the token through AsyncLocalStorage.
|
|
18
|
+
// - Next: configureFontduePreview / __prepareFontdueRender
|
|
19
|
+
// (fontdue-js/next) write the site URL, cache tags, and preview token
|
|
20
|
+
// into the render-scoped slot.
|
|
21
|
+
// Either way:
|
|
15
22
|
//
|
|
16
23
|
// const fetchGraphql = createFontdueFetch(); // once, at module scope
|
|
17
24
|
// const data = await fetchGraphql('Index', indexQuery, vars);
|
|
18
25
|
//
|
|
19
|
-
// 2. Explicit
|
|
20
|
-
// where ambient context can't propagate (
|
|
26
|
+
// 2. Explicit options: bind a fetcher with a url/headers/cacheTags, for
|
|
27
|
+
// runtimes where the ambient context can't propagate (route handlers,
|
|
28
|
+
// Astro edgeMiddleware — see runWithPreview's notes):
|
|
21
29
|
//
|
|
22
30
|
// const fetchGraphql = createFontdueFetch({
|
|
31
|
+
// url: endpoint.origin,
|
|
23
32
|
// headers: previewAuthHeaders(token), // from fontdue-js/preview
|
|
33
|
+
// cacheTags: [`graphql:${endpoint.domain}`],
|
|
24
34
|
// });
|
|
25
35
|
//
|
|
26
|
-
// Explicit
|
|
36
|
+
// Explicit options override the ambient context (headers merge, explicit
|
|
37
|
+
// winning), so the two compose.
|
|
27
38
|
//
|
|
28
|
-
// Caching
|
|
29
|
-
//
|
|
30
|
-
//
|
|
39
|
+
// Caching: when the resolved config carries cacheTags (the Next adapter sets
|
|
40
|
+
// them per render; supply them explicitly elsewhere) the fetch is opted into
|
|
41
|
+
// Next's data cache (`force-cache` + tags) so /api/revalidate can purge it.
|
|
42
|
+
// With no tags the fetch is left uncached, which is what preview renders and
|
|
43
|
+
// the CDN-cached frameworks (Astro/RR7 cache HTML at the response layer) want.
|
|
44
|
+
// The Next fetch hints are inert in other runtimes: Node's fetch accepts and
|
|
45
|
+
// ignores the cache mode, and `next` is just an unknown init property.
|
|
31
46
|
|
|
32
|
-
import {
|
|
47
|
+
import { resolveFontdueServerConfig } from '../relay/serverConfig.js';
|
|
48
|
+
import { PREVIEW_HEADER } from '../preview/constants.js';
|
|
33
49
|
function readEnv(name) {
|
|
34
50
|
if (typeof process !== 'undefined' && process.env) {
|
|
35
51
|
const v = process.env[name];
|
|
@@ -65,28 +81,58 @@ export class FontdueNotFoundError extends Error {
|
|
|
65
81
|
*/
|
|
66
82
|
export function createFontdueFetch() {
|
|
67
83
|
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
68
|
-
const base = options.url ?? resolveFontdueUrl();
|
|
69
|
-
if (!base) {
|
|
70
|
-
throw new Error('fontdue-js: no Fontdue URL configured. Set FONTDUE_URL / ' + 'PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL, or pass { url } to ' + 'createFontdueFetch.');
|
|
71
|
-
}
|
|
72
84
|
return async function fetchGraphql(queryName, query, variables) {
|
|
73
|
-
var
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
const
|
|
85
|
+
var _json$errors, _json$errors$;
|
|
86
|
+
// Per-render config (a runWithPreview AsyncLocalStorage store, the Next
|
|
87
|
+
// render-scoped slot, or the Next single-tenant ambient resolver). Awaited
|
|
88
|
+
// per call so a single module-level fetcher picks up the current request's
|
|
89
|
+
// tenant URL, preview token, and cache tags.
|
|
90
|
+
const config = await resolveFontdueServerConfig();
|
|
91
|
+
|
|
92
|
+
// URL: explicit option, then the per-render tenant URL, then the env.
|
|
93
|
+
const base = options.url ?? (config === null || config === void 0 ? void 0 : config.url) ?? resolveFontdueUrl();
|
|
94
|
+
if (!base) {
|
|
95
|
+
throw new Error('fontdue-js: no Fontdue URL configured. Set FONTDUE_URL / ' + 'PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL, pass { url } to ' + 'createFontdueFetch, or set it on the per-render config.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Cache tags: explicit option wins, else the per-render config's. A
|
|
99
|
+
// non-empty list opts the fetch into Next's data cache (inert elsewhere);
|
|
100
|
+
// an empty/absent list leaves it uncached (preview + CDN-cached frameworks).
|
|
101
|
+
const cacheTags = options.cacheTags ?? (config === null || config === void 0 ? void 0 : config.cacheTags);
|
|
102
|
+
|
|
103
|
+
// Ambient headers (e.g. tenant + preview token), overridden by any explicit
|
|
104
|
+
// per-fetcher headers.
|
|
105
|
+
const headers = {
|
|
106
|
+
'content-type': 'application/json',
|
|
107
|
+
...(config === null || config === void 0 ? void 0 : config.headers),
|
|
108
|
+
...options.headers
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Declare preview intent explicitly: a forwarded admin token means this is a
|
|
112
|
+
// preview render → reveal hidden fonts; otherwise "false" so a public (or
|
|
113
|
+
// merely session-authenticated) request never gets the hidden-fonts view.
|
|
114
|
+
// Older clients omit the header and keep the legacy behavior. Mirrors the
|
|
115
|
+
// Relay network layer (relay/environment.ts) and FontageWeb.Schema.Context.
|
|
116
|
+
headers[PREVIEW_HEADER] = headers.authorization != null || headers.Authorization != null ? 'true' : 'false';
|
|
117
|
+
|
|
118
|
+
// `next` is a Next.js-only fetch extension, ignored by other runtimes.
|
|
119
|
+
const init = {
|
|
79
120
|
method: 'POST',
|
|
80
121
|
body: JSON.stringify({
|
|
81
122
|
query,
|
|
82
123
|
variables
|
|
83
124
|
}),
|
|
84
|
-
headers
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
headers
|
|
126
|
+
};
|
|
127
|
+
if (Array.isArray(cacheTags) && cacheTags.length > 0) {
|
|
128
|
+
init.cache = 'force-cache';
|
|
129
|
+
// The global `graphql` tag is always present; dedupe so callers can pass
|
|
130
|
+
// the per-site tags alone or the full `endpoint.tags` (which include it).
|
|
131
|
+
init.next = {
|
|
132
|
+
tags: Array.from(new Set(['graphql', ...cacheTags]))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const response = await fetch(`${base}/graphql?query=${queryName}`, init);
|
|
90
136
|
if (response.status === 404) throw new FontdueNotFoundError(queryName);
|
|
91
137
|
if (response.status !== 200) {
|
|
92
138
|
throw new Error(`Fontdue request failed: ${response.status}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Minimal declaration so src/next/tenant.ts type-checks without `next`
|
|
2
|
+
// installed (it's the host app's dependency; this package only ever runs the
|
|
3
|
+
// import inside a Next.js server).
|
|
4
|
+
declare module 'next/headers' {
|
|
5
|
+
export function draftMode(): Promise<{ isEnabled: boolean }>;
|
|
6
|
+
export function cookies(): Promise<{
|
|
7
|
+
get(name: string): { value: string } | undefined;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
@@ -3,4 +3,8 @@
|
|
|
3
3
|
// import inside a Next.js server).
|
|
4
4
|
declare module 'next/navigation' {
|
|
5
5
|
export function notFound(): never;
|
|
6
|
+
// Re-throws Next's internal control-flow errors (dynamic-rendering bailout,
|
|
7
|
+
// notFound, redirect) and is a no-op for everything else. Used so the preview
|
|
8
|
+
// resolver doesn't swallow the bailout that forces a dynamic render.
|
|
9
|
+
export function unstable_rethrow(error: unknown): void;
|
|
6
10
|
}
|
package/vitest.config.ts
CHANGED
|
@@ -7,6 +7,11 @@ export default defineConfig({
|
|
|
7
7
|
__FONTDUE_JS_VERSION__: JSON.stringify('0.0.0-test'),
|
|
8
8
|
},
|
|
9
9
|
test: {
|
|
10
|
+
// Only the TypeScript sources are the tests of record. Without this, a
|
|
11
|
+
// prior `npm run build` leaves compiled copies in dist/__tests__ that
|
|
12
|
+
// vitest also picks up — running each suite twice and racing on
|
|
13
|
+
// module-level singletons (e.g. the ambient config resolver).
|
|
14
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
10
15
|
alias: {
|
|
11
16
|
'__generated__': path.resolve(__dirname, 'src/__generated__'),
|
|
12
17
|
},
|