fontdue-js 3.0.5 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/__generated__/FeatureTesterCard_fontStyle.graphql.d.ts +23 -0
- package/dist/__generated__/FeatureTesterCard_fontStyle.graphql.js +62 -0
- package/dist/__generated__/FeatureTesterStandaloneQuery.graphql.d.ts +25 -0
- package/dist/__generated__/FeatureTesterStandaloneQuery.graphql.js +240 -0
- package/dist/__generated__/FeatureTestersIdQuery.graphql.d.ts +21 -0
- package/dist/__generated__/FeatureTestersIdQuery.graphql.js +344 -0
- package/dist/__generated__/FeatureTestersSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/FeatureTestersSlugQuery.graphql.js +364 -0
- package/dist/__generated__/FeatureTesters_collection.graphql.d.ts +39 -0
- package/dist/__generated__/FeatureTesters_collection.graphql.js +145 -0
- package/dist/__generated__/NodePasswordFormAccessNodeMutation.graphql.d.ts +2 -1
- package/dist/__generated__/NodePasswordFormAccessNodeMutation.graphql.js +10 -4
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.d.ts +21 -0
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.js +86 -0
- package/dist/__generated__/NodePasswordFormElementSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/NodePasswordFormElementSlugQuery.graphql.js +108 -0
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +10 -4
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesters_collection.graphql.d.ts +2 -1
- package/dist/__generated__/TypeTesters_collection.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +128 -4
- package/dist/__tests__/highlight.test.js +141 -0
- package/dist/__tests__/middleware.test.js +132 -0
- package/dist/components/FeatureTester/FeatureTesterCard.d.ts +30 -0
- package/dist/components/FeatureTester/FeatureTesterCard.js +167 -0
- package/dist/components/FeatureTester/FeatureTesterElement.d.ts +19 -0
- package/dist/components/FeatureTester/FeatureTesterElement.js +42 -0
- package/dist/components/FeatureTester/FeatureTesters.d.ts +43 -0
- package/dist/components/FeatureTester/FeatureTesters.js +156 -0
- package/dist/components/FeatureTester/FeatureTesters.server.d.ts +9 -0
- package/dist/components/FeatureTester/FeatureTesters.server.js +40 -0
- package/dist/components/FeatureTester/FeatureTestersElement.d.ts +10 -0
- package/dist/components/FeatureTester/FeatureTestersElement.js +27 -0
- package/dist/components/FeatureTester/highlight.d.ts +19 -0
- package/dist/components/FeatureTester/highlight.js +71 -0
- package/dist/components/FeatureTester/index.d.ts +26 -0
- package/dist/components/FeatureTester/index.js +66 -0
- package/dist/components/FeatureTester/index.server.d.ts +7 -0
- package/dist/components/FeatureTester/index.server.js +30 -0
- package/dist/components/NodePasswordForm/NodePasswordFormElement.d.ts +3 -0
- package/dist/components/NodePasswordForm/NodePasswordFormElement.js +65 -0
- package/dist/components/NodePasswordForm/index.d.ts +16 -1
- package/dist/components/NodePasswordForm/index.js +30 -59
- package/dist/components/Root/index.js +2 -1
- package/dist/components/TypeTesters/index.js +5 -1
- package/dist/fontdue.css +145 -0
- package/dist/hooks/useFeatureTesterAutofit.d.ts +17 -0
- package/dist/hooks/useFeatureTesterAutofit.js +106 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +16 -1
- package/dist/next/registerSingleTenantResolver.js +13 -1
- package/dist/next/tenant.d.ts +1 -0
- package/dist/next/tenant.js +27 -2
- package/dist/nodeAccess.d.ts +18 -0
- package/dist/nodeAccess.js +84 -0
- package/dist/preview/server.d.ts +1 -20
- package/dist/preview/server.js +16 -85
- package/dist/relay/environment.js +8 -2
- package/dist/relay/serverConfig.d.ts +3 -0
- package/dist/relay/serverConfig.js +22 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +90 -7
- package/dist/server/middleware.d.ts +37 -0
- package/dist/server/middleware.js +144 -0
- package/package.json +8 -2
package/dist/preview/server.js
CHANGED
|
@@ -1,89 +1,20 @@
|
|
|
1
|
-
// Ambient (request-scoped) preview for non-RSC server frameworks.
|
|
1
|
+
// Ambient (request-scoped) admin preview for non-RSC server frameworks.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The ambient-context machinery moved to ../server/middleware, because it is
|
|
4
|
+
// shared by admin preview and the per-collection password unlock (and a
|
|
5
|
+
// storefront usually wants both). This module re-exports the preview-facing
|
|
6
|
+
// pieces so existing `fontdue-js/preview/server` imports keep working:
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// automatically forwards the admin preview token, with no per-call plumbing —
|
|
12
|
-
// the same ergonomics RSC gets from React.cache, and the same mechanism Next's
|
|
13
|
-
// own headers()/draftMode() use under the hood. Install it once in your
|
|
14
|
-
// framework's middleware:
|
|
8
|
+
// - runWithPreview — forward only the admin preview token (reveals
|
|
9
|
+
// unpublished fonts). Mount it in your framework's request middleware.
|
|
10
|
+
// - isPreviewing / ambientPreviewHeaders — read the current ambient state.
|
|
15
11
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// export const middleware = [({ request }, next) => runWithPreview(request, next)];
|
|
21
|
-
//
|
|
22
|
-
// runWithPreview also binds the cache guarantee to the same call: when a request
|
|
23
|
-
// is previewing, the response is forced out of any shared/CDN cache. Because the
|
|
24
|
-
// context and the no-cache rule are set in one place, you can't be ambiently
|
|
25
|
-
// previewing without the response being uncacheable — so an admin render is never
|
|
26
|
-
// served to the public.
|
|
27
|
-
//
|
|
28
|
-
// Requirements & fallback: AsyncLocalStorage must propagate from the install
|
|
29
|
-
// point into the render. That holds for in-process middleware — Node (Netlify
|
|
30
|
-
// Functions, the default SSR target), Deno (Netlify Edge), Bun. It does NOT hold
|
|
31
|
-
// when middleware runs in a separate runtime from the render (e.g. Astro with
|
|
32
|
-
// `edgeMiddleware: true`, where locals cross the boundary as a serialized
|
|
33
|
-
// header). In that case fall back to the explicit path: read the token in
|
|
34
|
-
// middleware and thread `previewAuthHeaders(token)` via the `headers` option on
|
|
35
|
-
// fetches/preloads. The explicit `headers` option always overrides the ambient
|
|
36
|
-
// context, so the two compose.
|
|
37
|
-
|
|
38
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
39
|
-
import { readPreviewToken, previewAuthHeaders } from './index.js';
|
|
40
|
-
import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
|
|
41
|
-
const previewStore = new AsyncLocalStorage();
|
|
42
|
-
|
|
43
|
-
// Feed the ambient token into fontdue-js's server config resolution. Returns
|
|
44
|
-
// undefined outside a preview context, so non-preview requests are untouched.
|
|
45
|
-
registerAmbientConfigResolver(() => {
|
|
46
|
-
const store = previewStore.getStore();
|
|
47
|
-
return store ? {
|
|
48
|
-
headers: store.headers
|
|
49
|
-
} : undefined;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** Whether the current async context is an admin preview. */
|
|
53
|
-
export function isPreviewing() {
|
|
54
|
-
return previewStore.getStore() != null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Preview auth headers for the current async context, or {} when not previewing.
|
|
59
|
-
* Spread into a hand-rolled fetch you make inside `runWithPreview`; the
|
|
60
|
-
* fontdue-js fetch/preload helpers pick these up automatically and don't need
|
|
61
|
-
* it.
|
|
62
|
-
*/
|
|
63
|
-
export function ambientPreviewHeaders() {
|
|
64
|
-
var _previewStore$getStor;
|
|
65
|
-
return ((_previewStore$getStor = previewStore.getStore()) === null || _previewStore$getStor === void 0 ? void 0 : _previewStore$getStor.headers) ?? {};
|
|
66
|
-
}
|
|
67
|
-
const NO_STORE = 'private, no-store';
|
|
12
|
+
// To also forward a visitor's password-unlock token, mount the precomposed
|
|
13
|
+
// `runWithFontdue` from `fontdue-js/server/middleware` in place of
|
|
14
|
+
// runWithPreview (or compose runWithPreview with runWithNodeAccess yourself).
|
|
15
|
+
// See the password-protected collections guide.
|
|
68
16
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* in a shared/CDN cache. Public (no-token) requests pass through untouched and
|
|
74
|
-
* stay fully cacheable.
|
|
75
|
-
*
|
|
76
|
-
* `next` is your middleware's continuation — `next` in Astro, `next` in a React
|
|
77
|
-
* Router 7 middleware. It must return (a promise of) the Response.
|
|
78
|
-
*/
|
|
79
|
-
export async function runWithPreview(request, next) {
|
|
80
|
-
const token = readPreviewToken(request.headers.get('cookie'));
|
|
81
|
-
if (!token) return next();
|
|
82
|
-
const response = await previewStore.run({
|
|
83
|
-
headers: previewAuthHeaders(token)
|
|
84
|
-
}, next);
|
|
85
|
-
response.headers.set('Cache-Control', NO_STORE);
|
|
86
|
-
response.headers.delete('Netlify-CDN-Cache-Control');
|
|
87
|
-
response.headers.delete('CDN-Cache-Control');
|
|
88
|
-
return response;
|
|
89
|
-
}
|
|
17
|
+
export { runWithPreview, isPreviewing,
|
|
18
|
+
// Historical name kept for back-compat; returns the full ambient header set
|
|
19
|
+
// (preview and/or node-access), so it works under runWithFontdue too.
|
|
20
|
+
ambientHeaders as ambientPreviewHeaders } from '../server/middleware.js';
|
|
@@ -2,12 +2,13 @@ import { Environment, Network, RecordSource, Store, QueryResponseCache } from 'r
|
|
|
2
2
|
import { handlePossibleCorsError } from '../corsError.js';
|
|
3
3
|
import { resolveFontdueServerConfig } from './serverConfig.js';
|
|
4
4
|
import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js';
|
|
5
|
+
import { NODE_ACCESS_HEADER } from '../nodeAccess.js';
|
|
5
6
|
|
|
6
7
|
// `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
|
|
7
8
|
// (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
|
|
8
9
|
// Exported so UI (the admin toolbar) can surface it without re-reading the
|
|
9
10
|
// build-time global in a 'use client' module.
|
|
10
|
-
export const version = "3.0
|
|
11
|
+
export const version = "3.1.0";
|
|
11
12
|
const IS_SERVER = typeof window === typeof undefined;
|
|
12
13
|
|
|
13
14
|
// Opt server fetches into Next's data cache only in production; dev stays
|
|
@@ -91,6 +92,11 @@ export function createNetworkFetch(options) {
|
|
|
91
92
|
const previewing = IS_SERVER ? headers.authorization != null || headers.Authorization != null : hasPreviewMarkerCookie();
|
|
92
93
|
headers[PREVIEW_HEADER] = previewing ? 'true' : 'false';
|
|
93
94
|
|
|
95
|
+
// A forwarded node-access token (a collection this visitor unlocked) makes
|
|
96
|
+
// the server response per-visitor just like preview, so it must never be
|
|
97
|
+
// written to or served from the shared data cache.
|
|
98
|
+
const perVisitor = previewing || IS_SERVER && headers[NODE_ACCESS_HEADER] != null;
|
|
99
|
+
|
|
94
100
|
// Public server fetches are all site content (per-session data is fetched
|
|
95
101
|
// client-side only), so opt them into Next's data cache — without this,
|
|
96
102
|
// Next 15 treats them as no-store and silently makes every page fully
|
|
@@ -113,7 +119,7 @@ export function createNetworkFetch(options) {
|
|
|
113
119
|
variables
|
|
114
120
|
})
|
|
115
121
|
};
|
|
116
|
-
if (IS_SERVER && (
|
|
122
|
+
if (IS_SERVER && (perVisitor || !cacheInProduction())) {
|
|
117
123
|
init.cache = 'no-store';
|
|
118
124
|
} else if (IS_SERVER) {
|
|
119
125
|
init.cache = 'force-cache';
|
|
@@ -8,7 +8,10 @@ export interface FontdueServerConfig {
|
|
|
8
8
|
}
|
|
9
9
|
type MaybePromise<T> = T | Promise<T>;
|
|
10
10
|
type AmbientConfigResolver = () => MaybePromise<FontdueServerConfig | undefined>;
|
|
11
|
+
type NodeAccessRecovery = () => MaybePromise<Record<string, string>>;
|
|
11
12
|
export declare function setFontdueServerConfig(config: FontdueServerConfig): void;
|
|
12
13
|
export declare function registerAmbientConfigResolver(resolver: AmbientConfigResolver): void;
|
|
14
|
+
export declare function registerNodeAccessRecovery(recovery: NodeAccessRecovery): void;
|
|
15
|
+
export declare function resolveNodeAccessRecovery(): NodeAccessRecovery | undefined;
|
|
13
16
|
export declare function resolveFontdueServerConfig(): Promise<FontdueServerConfig | undefined>;
|
|
14
17
|
export {};
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
import * as React from 'react';
|
|
28
28
|
|
|
29
|
+
// Recovers a visitor's per-collection node-access headers when a server fetch
|
|
30
|
+
// comes back `password_protected` — see registerNodeAccessRecovery below.
|
|
31
|
+
|
|
29
32
|
// This module owns two pieces of cross-cutting state — the render-scoped slot
|
|
30
33
|
// and the ambient resolver — and both must be shared by every server-side
|
|
31
34
|
// fetch in a render. The catch: a Next.js server build can bundle this same
|
|
@@ -58,7 +61,8 @@ function store() {
|
|
|
58
61
|
})) : () => ({
|
|
59
62
|
current: undefined
|
|
60
63
|
}),
|
|
61
|
-
ambientResolver: undefined
|
|
64
|
+
ambientResolver: undefined,
|
|
65
|
+
nodeAccessRecovery: undefined
|
|
62
66
|
};
|
|
63
67
|
g[STORE_KEY] = s;
|
|
64
68
|
}
|
|
@@ -84,6 +88,23 @@ export function registerAmbientConfigResolver(resolver) {
|
|
|
84
88
|
store().ambientResolver = resolver;
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
// Node-access recovery — the seam that lets the browser-safe transport recover
|
|
92
|
+
// from a `password_protected` error without importing a framework's request
|
|
93
|
+
// APIs. The Next adapter registers a recovery that reads the visitor's
|
|
94
|
+
// node-access cookie (via next/headers); createFontdueFetch calls it when a
|
|
95
|
+
// fetch comes back locked, folds the returned headers into the per-render config
|
|
96
|
+
// so the page fetch AND the embedded server components' own preloads resolve,
|
|
97
|
+
// then retries once (see ../server/index.ts). Returns {} when the visitor hasn't
|
|
98
|
+
// unlocked the collection. Unregistered (non-Next, or frameworks that forward
|
|
99
|
+
// the token ambiently up-front via runWithNodeAccess) → no recovery, the error
|
|
100
|
+
// propagates so the page can render the password form.
|
|
101
|
+
export function registerNodeAccessRecovery(recovery) {
|
|
102
|
+
store().nodeAccessRecovery = recovery;
|
|
103
|
+
}
|
|
104
|
+
export function resolveNodeAccessRecovery() {
|
|
105
|
+
return store().nodeAccessRecovery;
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
// Merge an ambient config under the RSC slot: the slot wins for scalar fields;
|
|
88
109
|
// headers merge with the slot overriding.
|
|
89
110
|
function mergeServerConfig(ambient, slot) {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ export declare class FontdueNotFoundError extends Error {
|
|
|
2
2
|
readonly queryName: string;
|
|
3
3
|
constructor(queryName: string);
|
|
4
4
|
}
|
|
5
|
+
export declare class FontdueGraphqlError extends Error {
|
|
6
|
+
readonly code: string | undefined;
|
|
7
|
+
readonly queryName: string;
|
|
8
|
+
constructor(message: string, code: string | undefined, queryName: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class FontduePasswordProtectedError extends FontdueGraphqlError {
|
|
11
|
+
constructor(message: string, queryName: string);
|
|
12
|
+
}
|
|
5
13
|
export interface CreateFontdueFetchOptions {
|
|
6
14
|
/**
|
|
7
15
|
* GraphQL base URL (without the trailing /graphql), e.g.
|
package/dist/server/index.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
// The Next fetch hints are inert in other runtimes: Node's fetch accepts and
|
|
46
46
|
// ignores the cache mode, and `next` is just an unknown init property.
|
|
47
47
|
|
|
48
|
-
import { resolveFontdueServerConfig } from '../relay/serverConfig.js';
|
|
48
|
+
import { resolveFontdueServerConfig, setFontdueServerConfig, resolveNodeAccessRecovery } from '../relay/serverConfig.js';
|
|
49
49
|
import { PREVIEW_HEADER } from '../preview/constants.js';
|
|
50
50
|
function readEnv(name) {
|
|
51
51
|
if (typeof process !== 'undefined' && process.env) {
|
|
@@ -87,6 +87,30 @@ export class FontdueNotFoundError extends Error {
|
|
|
87
87
|
this.name = 'FontdueNotFoundError';
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
// Thrown when a GraphQL response carries an `errors` entry. Subclasses Error so
|
|
92
|
+
// existing `catch`/rethrow keeps working, but also exposes the machine-readable
|
|
93
|
+
// `code` from the first error (e.g. "password_protected") so callers can branch
|
|
94
|
+
// on a specific failure instead of string-matching the message.
|
|
95
|
+
export class FontdueGraphqlError extends Error {
|
|
96
|
+
constructor(message, code, queryName) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.code = code;
|
|
99
|
+
this.queryName = queryName;
|
|
100
|
+
this.name = 'FontdueGraphqlError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Thrown when the requested collection is password-protected and the visitor
|
|
105
|
+
// hasn't unlocked it. Catch it to render a <NodePasswordForm> instead of a 404:
|
|
106
|
+
// the collection exists, it's just gated. The matching server signal is the
|
|
107
|
+
// GraphQL error code "password_protected" (see the Fontdue slug resolver).
|
|
108
|
+
export class FontduePasswordProtectedError extends FontdueGraphqlError {
|
|
109
|
+
constructor(message, queryName) {
|
|
110
|
+
super(message, 'password_protected', queryName);
|
|
111
|
+
this.name = 'FontduePasswordProtectedError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
/**
|
|
91
115
|
* Build a server-side GraphQL fetcher bound to a base URL and a set of headers.
|
|
92
116
|
* The returned function takes the operation name, the query text (load it from
|
|
@@ -94,8 +118,10 @@ export class FontdueNotFoundError extends Error {
|
|
|
94
118
|
*/
|
|
95
119
|
export function createFontdueFetch() {
|
|
96
120
|
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
async function runOnce(queryName, query, variables) {
|
|
122
|
+
let {
|
|
123
|
+
forceUncached = false
|
|
124
|
+
} = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
99
125
|
// Per-render config (a runWithPreview AsyncLocalStorage store, the Next
|
|
100
126
|
// render-scoped slot, or the Next single-tenant ambient resolver). Awaited
|
|
101
127
|
// per call so a single module-level fetcher picks up the current request's
|
|
@@ -111,7 +137,9 @@ export function createFontdueFetch() {
|
|
|
111
137
|
// Cache tags: explicit option wins, else the per-render config's. A
|
|
112
138
|
// non-empty list opts the fetch into Next's data cache (inert elsewhere);
|
|
113
139
|
// an empty/absent list leaves it uncached (preview + CDN-cached frameworks).
|
|
114
|
-
|
|
140
|
+
// A forced-uncached retry (an unlocked, per-visitor render) drops them
|
|
141
|
+
// entirely so nothing tags the response into the shared cache.
|
|
142
|
+
const cacheTags = forceUncached ? undefined : options.cacheTags ?? (config === null || config === void 0 ? void 0 : config.cacheTags);
|
|
115
143
|
|
|
116
144
|
// Ambient headers (e.g. tenant + preview token), overridden by any explicit
|
|
117
145
|
// per-fetcher headers.
|
|
@@ -151,10 +179,65 @@ export function createFontdueFetch() {
|
|
|
151
179
|
throw new Error(`Fontdue request failed: ${response.status}`);
|
|
152
180
|
}
|
|
153
181
|
const json = await response.json();
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
182
|
+
const errors = json.errors ?? [];
|
|
183
|
+
if (errors.length > 0) {
|
|
184
|
+
// Absinthe puts a resolver's error `code` on the error itself; some errors
|
|
185
|
+
// carry it under `extensions` instead — check both.
|
|
186
|
+
const codeOf = e => {
|
|
187
|
+
var _e$extensions;
|
|
188
|
+
return e.code ?? ((_e$extensions = e.extensions) === null || _e$extensions === void 0 ? void 0 : _e$extensions.code);
|
|
189
|
+
};
|
|
190
|
+
// Look across every error, not just the first: a query that touches a
|
|
191
|
+
// locked collection alongside another failing field can return
|
|
192
|
+
// `password_protected` in any position, and the recovery + the app's
|
|
193
|
+
// `instanceof FontduePasswordProtectedError` catch both depend on it being
|
|
194
|
+
// recognized regardless of order.
|
|
195
|
+
const locked = errors.find(e => codeOf(e) === 'password_protected');
|
|
196
|
+
if (locked) {
|
|
197
|
+
throw new FontduePasswordProtectedError(`Fontdue GraphQL error: ${locked.message}`, queryName);
|
|
198
|
+
}
|
|
199
|
+
const first = errors[0];
|
|
200
|
+
throw new FontdueGraphqlError(`Fontdue GraphQL error: ${first.message}`, codeOf(first), queryName);
|
|
157
201
|
}
|
|
158
202
|
return json.data;
|
|
203
|
+
}
|
|
204
|
+
return async function fetchGraphql(queryName, query, variables) {
|
|
205
|
+
try {
|
|
206
|
+
return await runOnce(queryName, query, variables);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// A password-protected collection comes back as a `password_protected`
|
|
209
|
+
// error. In a server render, recover transparently: a registered recovery
|
|
210
|
+
// (the Next adapter) reads the visitor's node-access cookie. If they've
|
|
211
|
+
// unlocked it, fold the token into the per-render config — so this fetch
|
|
212
|
+
// AND the page's embedded server components' own (Suspense) preloads
|
|
213
|
+
// resolve, not just this one call — and retry once. Reading the cookie is
|
|
214
|
+
// deferred to here (only when a fetch is actually gated) so non-protected
|
|
215
|
+
// pages stay static/cacheable; the retry runs `forceUncached` so the
|
|
216
|
+
// unlocked, per-visitor render is never written to the shared cache.
|
|
217
|
+
// Frameworks that forward the token ambiently up-front (runWithNodeAccess)
|
|
218
|
+
// never reach this path. With no recovery registered, or no token, the
|
|
219
|
+
// error propagates so the page can render the password form.
|
|
220
|
+
if (error instanceof FontduePasswordProtectedError) {
|
|
221
|
+
const recover = resolveNodeAccessRecovery();
|
|
222
|
+
if (recover) {
|
|
223
|
+
const nodeAccess = await recover();
|
|
224
|
+
if (Object.keys(nodeAccess).length > 0) {
|
|
225
|
+
const config = (await resolveFontdueServerConfig()) ?? {};
|
|
226
|
+
setFontdueServerConfig({
|
|
227
|
+
...config,
|
|
228
|
+
headers: {
|
|
229
|
+
...config.headers,
|
|
230
|
+
...nodeAccess
|
|
231
|
+
},
|
|
232
|
+
cacheTags: undefined
|
|
233
|
+
});
|
|
234
|
+
return await runOnce(queryName, query, variables, {
|
|
235
|
+
forceUncached: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
159
242
|
};
|
|
160
243
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Whether the current async context is an admin preview. */
|
|
2
|
+
export declare function isPreviewing(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Request-scoped Fontdue headers for the current async context (admin preview
|
|
5
|
+
* and/or the visitor's node-access token), or {} when there are none. Spread
|
|
6
|
+
* into a hand-rolled fetch you make inside a runWith* middleware; the fontdue-js
|
|
7
|
+
* fetch/preload helpers pick these up automatically and don't need it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ambientHeaders(): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Run `next` (your framework's render/loader chain) with the request's admin
|
|
12
|
+
* preview token in ambient scope, then return its Response. While active, every
|
|
13
|
+
* fontdue-js server fetch forwards the token (reveals unpublished fonts) with no
|
|
14
|
+
* per-call plumbing; when previewing, the response is forced out of any
|
|
15
|
+
* shared/CDN cache. Public requests (no preview cookie) pass through untouched
|
|
16
|
+
* and stay fully cacheable. For the visitor password-unlock token use
|
|
17
|
+
* runWithNodeAccess; for both, runWithFontdue.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runWithPreview(request: Request, next: () => Response | Promise<Response>): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Run `next` with the visitor's per-collection node-access token in ambient
|
|
22
|
+
* scope, then return its Response. The token is read from the first-party cookie
|
|
23
|
+
* the password form set; while active, every fontdue-js server fetch forwards it
|
|
24
|
+
* so a collection the visitor unlocked resolves for them. A request carrying an
|
|
25
|
+
* unlock is per-visitor, so its response is forced out of any shared/CDN cache —
|
|
26
|
+
* an unlocked render is never served to someone who hasn't unlocked. Requests
|
|
27
|
+
* with no node-access cookie pass through untouched and stay cacheable.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runWithNodeAccess(request: Request, next: () => Response | Promise<Response>): Promise<Response>;
|
|
30
|
+
/**
|
|
31
|
+
* The precomposed middleware most storefronts want: forwards the admin preview
|
|
32
|
+
* token AND the visitor's node-access token, each handled by its own
|
|
33
|
+
* single-responsibility middleware. Equivalent to nesting
|
|
34
|
+
* `runWithPreview(request, () => runWithNodeAccess(request, next))` — mount this
|
|
35
|
+
* in place of runWithPreview to get the password-unlock forwarding too.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runWithFontdue(request: Request, next: () => Response | Promise<Response>): Promise<Response>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Request-scoped Fontdue context for non-RSC server frameworks (Astro, React
|
|
2
|
+
// Router 7, TanStack Start). RSC apps (Next) don't use this — they drive the
|
|
3
|
+
// per-render config through React.cache (see ../relay/serverConfig.ts).
|
|
4
|
+
//
|
|
5
|
+
// fontdue-js carries two kinds of request-scoped state to every server fetch:
|
|
6
|
+
//
|
|
7
|
+
// - the admin *preview* token (reveals unpublished fonts), and
|
|
8
|
+
// - the visitor's per-collection *node-access* token (a collection they
|
|
9
|
+
// unlocked with a password).
|
|
10
|
+
//
|
|
11
|
+
// Both need the same plumbing: read a cookie, hold a header in ambient scope for
|
|
12
|
+
// the whole render so every fontdue-js fetch/preload forwards it with no per-call
|
|
13
|
+
// threading, and force the response out of any shared/CDN cache because it's now
|
|
14
|
+
// per-visitor. This module provides that machinery once and exposes three
|
|
15
|
+
// single-responsibility middlewares over it:
|
|
16
|
+
//
|
|
17
|
+
// - runWithPreview — forwards only the admin preview token.
|
|
18
|
+
// - runWithNodeAccess — forwards only the visitor's node-access token.
|
|
19
|
+
// - runWithFontdue — both, composed (runWithPreview ∘ runWithNodeAccess).
|
|
20
|
+
//
|
|
21
|
+
// Mount one of these in your framework's request middleware. A storefront that
|
|
22
|
+
// wants both features mounts the precomposed runWithFontdue:
|
|
23
|
+
//
|
|
24
|
+
// // Astro: src/middleware.ts
|
|
25
|
+
// export const onRequest = (ctx, next) => runWithFontdue(ctx.request, next);
|
|
26
|
+
//
|
|
27
|
+
// // React Router 7 (root route, with future.v8_middleware):
|
|
28
|
+
// export const middleware = [({ request }, next) => runWithFontdue(request, next)];
|
|
29
|
+
//
|
|
30
|
+
// They compose because they share one ambient store and each merges its slice
|
|
31
|
+
// onto whatever an outer middleware established — so nesting accumulates headers
|
|
32
|
+
// rather than clobbering. Mount only the ones you need; the unused cookie is
|
|
33
|
+
// simply never present.
|
|
34
|
+
//
|
|
35
|
+
// Requirement: AsyncLocalStorage must propagate from the install point into the
|
|
36
|
+
// render. That holds for in-process middleware — Node (Netlify Functions, the
|
|
37
|
+
// default SSR target), Deno (Netlify Edge), Bun. It does NOT hold when
|
|
38
|
+
// middleware runs in a separate runtime from the render (e.g. Astro with
|
|
39
|
+
// `edgeMiddleware: true`, where locals cross the boundary as a serialized
|
|
40
|
+
// header); mount the middleware in-process on those setups.
|
|
41
|
+
|
|
42
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
43
|
+
import { readPreviewToken, previewAuthHeaders } from '../preview/index.js';
|
|
44
|
+
import { nodeAccessHeadersFromCookie } from '../nodeAccess.js';
|
|
45
|
+
import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
|
|
46
|
+
const ambientStore = new AsyncLocalStorage();
|
|
47
|
+
|
|
48
|
+
// Feed the ambient headers into fontdue-js's server config resolution. Returns
|
|
49
|
+
// undefined outside the context, so plain public requests are untouched.
|
|
50
|
+
registerAmbientConfigResolver(() => {
|
|
51
|
+
const store = ambientStore.getStore();
|
|
52
|
+
return store ? {
|
|
53
|
+
headers: store.headers
|
|
54
|
+
} : undefined;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Whether the current async context is an admin preview. */
|
|
58
|
+
export function isPreviewing() {
|
|
59
|
+
var _ambientStore$getStor;
|
|
60
|
+
return ((_ambientStore$getStor = ambientStore.getStore()) === null || _ambientStore$getStor === void 0 ? void 0 : _ambientStore$getStor.previewing) === true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Request-scoped Fontdue headers for the current async context (admin preview
|
|
65
|
+
* and/or the visitor's node-access token), or {} when there are none. Spread
|
|
66
|
+
* into a hand-rolled fetch you make inside a runWith* middleware; the fontdue-js
|
|
67
|
+
* fetch/preload helpers pick these up automatically and don't need it.
|
|
68
|
+
*/
|
|
69
|
+
export function ambientHeaders() {
|
|
70
|
+
var _ambientStore$getStor2;
|
|
71
|
+
return ((_ambientStore$getStor2 = ambientStore.getStore()) === null || _ambientStore$getStor2 === void 0 ? void 0 : _ambientStore$getStor2.headers) ?? {};
|
|
72
|
+
}
|
|
73
|
+
const NO_STORE = 'private, no-store';
|
|
74
|
+
|
|
75
|
+
// A per-visitor response (preview or an unlocked collection) must never be
|
|
76
|
+
// stored in or served from a shared/CDN cache.
|
|
77
|
+
function forceNoStore(response) {
|
|
78
|
+
response.headers.set('Cache-Control', NO_STORE);
|
|
79
|
+
response.headers.delete('Netlify-CDN-Cache-Control');
|
|
80
|
+
response.headers.delete('CDN-Cache-Control');
|
|
81
|
+
return response;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Run `next` with `patch` merged onto any ambient context an outer middleware
|
|
85
|
+
// already set, so runWithPreview ∘ runWithNodeAccess accumulate (headers union,
|
|
86
|
+
// previewing OR'd) instead of the inner call clobbering the outer.
|
|
87
|
+
function withAmbient(patch, next) {
|
|
88
|
+
const existing = ambientStore.getStore();
|
|
89
|
+
return Promise.resolve(ambientStore.run({
|
|
90
|
+
headers: {
|
|
91
|
+
...(existing === null || existing === void 0 ? void 0 : existing.headers),
|
|
92
|
+
...patch.headers
|
|
93
|
+
},
|
|
94
|
+
previewing: (existing === null || existing === void 0 ? void 0 : existing.previewing) === true || patch.previewing === true
|
|
95
|
+
}, next));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run `next` (your framework's render/loader chain) with the request's admin
|
|
100
|
+
* preview token in ambient scope, then return its Response. While active, every
|
|
101
|
+
* fontdue-js server fetch forwards the token (reveals unpublished fonts) with no
|
|
102
|
+
* per-call plumbing; when previewing, the response is forced out of any
|
|
103
|
+
* shared/CDN cache. Public requests (no preview cookie) pass through untouched
|
|
104
|
+
* and stay fully cacheable. For the visitor password-unlock token use
|
|
105
|
+
* runWithNodeAccess; for both, runWithFontdue.
|
|
106
|
+
*/
|
|
107
|
+
export async function runWithPreview(request, next) {
|
|
108
|
+
const token = readPreviewToken(request.headers.get('cookie'));
|
|
109
|
+
if (!token) return next();
|
|
110
|
+
const response = await withAmbient({
|
|
111
|
+
headers: previewAuthHeaders(token),
|
|
112
|
+
previewing: true
|
|
113
|
+
}, next);
|
|
114
|
+
return forceNoStore(response);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Run `next` with the visitor's per-collection node-access token in ambient
|
|
119
|
+
* scope, then return its Response. The token is read from the first-party cookie
|
|
120
|
+
* the password form set; while active, every fontdue-js server fetch forwards it
|
|
121
|
+
* so a collection the visitor unlocked resolves for them. A request carrying an
|
|
122
|
+
* unlock is per-visitor, so its response is forced out of any shared/CDN cache —
|
|
123
|
+
* an unlocked render is never served to someone who hasn't unlocked. Requests
|
|
124
|
+
* with no node-access cookie pass through untouched and stay cacheable.
|
|
125
|
+
*/
|
|
126
|
+
export async function runWithNodeAccess(request, next) {
|
|
127
|
+
const headers = nodeAccessHeadersFromCookie(request.headers.get('cookie'));
|
|
128
|
+
if (Object.keys(headers).length === 0) return next();
|
|
129
|
+
const response = await withAmbient({
|
|
130
|
+
headers
|
|
131
|
+
}, next);
|
|
132
|
+
return forceNoStore(response);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The precomposed middleware most storefronts want: forwards the admin preview
|
|
137
|
+
* token AND the visitor's node-access token, each handled by its own
|
|
138
|
+
* single-responsibility middleware. Equivalent to nesting
|
|
139
|
+
* `runWithPreview(request, () => runWithNodeAccess(request, next))` — mount this
|
|
140
|
+
* in place of runWithPreview to get the password-unlock forwarding too.
|
|
141
|
+
*/
|
|
142
|
+
export async function runWithFontdue(request, next) {
|
|
143
|
+
return runWithPreview(request, () => runWithNodeAccess(request, next));
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdue-js",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "npm run relay && run-p build-js build-css build-ts",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"./preview": "./dist/preview/index.js",
|
|
87
87
|
"./preview/server": "./dist/preview/server.js",
|
|
88
88
|
"./server": "./dist/server/index.js",
|
|
89
|
+
"./server/middleware": "./dist/server/middleware.js",
|
|
89
90
|
"./next": "./dist/next/index.js",
|
|
90
91
|
"./next/config": "./dist/next/config.js",
|
|
91
92
|
"./next/revalidate": "./dist/next/revalidate.js",
|
|
@@ -101,6 +102,10 @@
|
|
|
101
102
|
"default": "./dist/components/CharacterViewer/index.js"
|
|
102
103
|
},
|
|
103
104
|
"./CustomerLoginForm": "./dist/components/CustomerLoginForm/index.js",
|
|
105
|
+
"./FeatureTesters": {
|
|
106
|
+
"react-server": "./dist/components/FeatureTester/FeatureTesters.server.js",
|
|
107
|
+
"default": "./dist/components/FeatureTester/FeatureTesters.js"
|
|
108
|
+
},
|
|
104
109
|
"./FontdueProvider": {
|
|
105
110
|
"react-server": "./dist/components/FontdueProvider/index.server.js",
|
|
106
111
|
"default": "./dist/components/FontdueProvider/index.js"
|
|
@@ -109,6 +114,7 @@
|
|
|
109
114
|
"react-server": "./dist/components/NewsletterSignup/index.server.js",
|
|
110
115
|
"default": "./dist/components/NewsletterSignup/index.js"
|
|
111
116
|
},
|
|
117
|
+
"./NodePasswordForm": "./dist/components/NodePasswordForm/index.js",
|
|
112
118
|
"./StoreModal": "./dist/components/StoreModal/index.js",
|
|
113
119
|
"./TestFontsForm": {
|
|
114
120
|
"react-server": "./dist/components/TestFontsForm/index.server.js",
|
|
@@ -128,4 +134,4 @@
|
|
|
128
134
|
"./useFont": "./dist/components/useFont.js",
|
|
129
135
|
"./useFontStyle": "./dist/components/useFont.js"
|
|
130
136
|
}
|
|
131
|
-
}
|
|
137
|
+
}
|