fontdue-js 3.1.0 → 3.2.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 +7 -0
- package/dist/__tests__/nextAdapter.test.js +115 -1
- package/dist/components/NodePasswordForm/index.d.ts +11 -0
- package/dist/components/NodePasswordForm/index.js +24 -1
- package/dist/next/index.js +3 -2
- package/dist/next/tenant.js +35 -4
- package/dist/next/unlock.d.ts +3 -0
- package/dist/next/unlock.js +49 -0
- package/dist/relay/environment.js +1 -1
- package/package.json +2 -1
- package/types/next-headers.d.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 3.2.0
|
|
2
|
+
|
|
3
|
+
- **Fixed password-protected collections crashing statically-rendered Next.js storefronts.** Locked slugs are excluded from `generateStaticParams`, so a visitor's first request for one triggers an on-demand static fill of the font page — and the unlock-cookie read during that fill crashed it with a 500 (digest `DYNAMIC_SERVER_USAGE`). A locked page's static fill now renders (and caches) the password form, and unlocking takes that visitor past the static cache instead — the same mechanism as admin preview:
|
|
4
|
+
- New route handler `fontdue-js/next/unlock`. Mount it in your app (`export { POST } from "fontdue-js/next/unlock"` in `app/api/unlock/route.ts`): on a successful unlock it enables Next draft mode, whose bypass cookie makes that one visitor's renders dynamic, so the unlock cookie is read and the collection resolves. Everyone else keeps the static pages.
|
|
5
|
+
- New `unlockEndpoint` prop on `NodePasswordForm` — the form POSTs the access token there before reloading. Pass `unlockEndpoint="/api/unlock"` on statically-rendered Next storefronts; omit it on frameworks that render per request (Astro, React Router 7, TanStack Start), where the cookie alone unlocks.
|
|
6
|
+
- `nodeAccessHeaders()` (`fontdue-js/next`) now reads the unlock cookie only during draft-mode renders, so a locked page's static fill is never flagged dynamic. See [Password-protected collections](https://www.fontdue.com/docs/develop/password-protected-collections).
|
|
7
|
+
|
|
1
8
|
## 3.1.0
|
|
2
9
|
|
|
3
10
|
- **New `FeatureTesters` component** (`fontdue-js/FeatureTesters`) — renders a collection's *feature tester* cards, each showcasing an OpenType feature by toggling it on and off and tinting exactly the characters the feature changes. Pass `collectionId` or `collectionSlug`; in a React Server Component the server entry preloads its own query, and in a client tree (`preloadedQuery` or inside a `<FontdueProvider>`) it hydrates or lazy-loads like the other collection components. Collection-level props override the per-card dashboard settings: `highlightColor`, `toggle` (`'button'` or `'hover'`), and `autofit`. Requires a Fontdue backend that exposes `FontCollection.featureTesters` and the `FeatureTester` node. See [Feature Testers](https://www.fontdue.com/docs/reference/feature-testers).
|
|
@@ -48,17 +48,23 @@ vi.mock('next/navigation', () => ({
|
|
|
48
48
|
const draft = vi.hoisted(() => ({
|
|
49
49
|
enabled: false,
|
|
50
50
|
token: undefined,
|
|
51
|
+
nodeAccess: undefined,
|
|
51
52
|
cookiesError: undefined
|
|
52
53
|
}));
|
|
53
54
|
vi.mock('next/headers', () => ({
|
|
54
55
|
draftMode: async () => ({
|
|
55
|
-
isEnabled: draft.enabled
|
|
56
|
+
isEnabled: draft.enabled,
|
|
57
|
+
enable: () => {
|
|
58
|
+
draft.enabled = true;
|
|
59
|
+
}
|
|
56
60
|
}),
|
|
57
61
|
cookies: async () => {
|
|
58
62
|
if (draft.cookiesError) throw draft.cookiesError;
|
|
59
63
|
return {
|
|
60
64
|
get: name => name === 'fontdue_preview_token' && draft.token ? {
|
|
61
65
|
value: draft.token
|
|
66
|
+
} : name === 'fontdue_node_access' && draft.nodeAccess ? {
|
|
67
|
+
value: draft.nodeAccess
|
|
62
68
|
} : undefined
|
|
63
69
|
};
|
|
64
70
|
}
|
|
@@ -74,6 +80,7 @@ beforeEach(() => {
|
|
|
74
80
|
vi.restoreAllMocks();
|
|
75
81
|
draft.enabled = false;
|
|
76
82
|
draft.token = undefined;
|
|
83
|
+
draft.nodeAccess = undefined;
|
|
77
84
|
draft.cookiesError = undefined;
|
|
78
85
|
});
|
|
79
86
|
function stubSingleTenant() {
|
|
@@ -360,6 +367,113 @@ describe('single-tenant ambient resolver (no per-render call)', () => {
|
|
|
360
367
|
expect(config === null || config === void 0 ? void 0 : (_config$headers5 = config.headers) === null || _config$headers5 === void 0 ? void 0 : _config$headers5.authorization).toBeUndefined();
|
|
361
368
|
});
|
|
362
369
|
});
|
|
370
|
+
describe('nodeAccessHeaders', () => {
|
|
371
|
+
it('draft-mode bypass render: returns the forwarding header', async () => {
|
|
372
|
+
stubSingleTenant();
|
|
373
|
+
draft.enabled = true;
|
|
374
|
+
draft.nodeAccess = 'na-tok';
|
|
375
|
+
const {
|
|
376
|
+
nodeAccessHeaders
|
|
377
|
+
} = await importTenant();
|
|
378
|
+
expect(await nodeAccessHeaders()).toEqual({
|
|
379
|
+
'fontdue-node-access': 'na-tok'
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
it('draft-mode bypass render without the cookie: returns {}', async () => {
|
|
383
|
+
stubSingleTenant();
|
|
384
|
+
draft.enabled = true;
|
|
385
|
+
const {
|
|
386
|
+
nodeAccessHeaders
|
|
387
|
+
} = await importTenant();
|
|
388
|
+
expect(await nodeAccessHeaders()).toEqual({});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Regression for FD-773: locked slugs are excluded from generateStaticParams,
|
|
392
|
+
// so a visitor's first request is an on-demand STATIC fill of the SSG route.
|
|
393
|
+
// Merely calling cookies() there flags the render as dynamic and Next 15
|
|
394
|
+
// 500s ("Page changed from static to dynamic at runtime") — catching the
|
|
395
|
+
// throw doesn't unpoison it. Off draft mode the hook must return "no token"
|
|
396
|
+
// WITHOUT touching cookies(), so the fill can prerender the password form.
|
|
397
|
+
// cookiesError guards the "without touching" half: any cookies() call would
|
|
398
|
+
// reject.
|
|
399
|
+
it('runtime static fill (no draft mode): no token, and cookies() is never called', async () => {
|
|
400
|
+
stubSingleTenant();
|
|
401
|
+
draft.cookiesError = Object.assign(new Error('Dynamic server usage'), {
|
|
402
|
+
digest: 'DYNAMIC_SERVER_USAGE'
|
|
403
|
+
});
|
|
404
|
+
const {
|
|
405
|
+
nodeAccessHeaders
|
|
406
|
+
} = await importTenant();
|
|
407
|
+
expect(await nodeAccessHeaders()).toEqual({});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// At build time the gate is skipped and the bailout keeps propagating: a
|
|
411
|
+
// static route whose own fetch touches a locked collection bails out to
|
|
412
|
+
// dynamic at build today, and the fix must not change build classification.
|
|
413
|
+
it('build-time prerender: still propagates the dynamic bailout', async () => {
|
|
414
|
+
stubSingleTenant();
|
|
415
|
+
vi.stubEnv('NEXT_PHASE', 'phase-production-build');
|
|
416
|
+
draft.cookiesError = Object.assign(new Error('Dynamic server usage'), {
|
|
417
|
+
digest: 'DYNAMIC_SERVER_USAGE'
|
|
418
|
+
});
|
|
419
|
+
const {
|
|
420
|
+
nodeAccessHeaders
|
|
421
|
+
} = await importTenant();
|
|
422
|
+
await expect(nodeAccessHeaders()).rejects.toBe(draft.cookiesError);
|
|
423
|
+
});
|
|
424
|
+
it('swallows a non-control-flow throw (no request scope)', async () => {
|
|
425
|
+
stubSingleTenant();
|
|
426
|
+
draft.enabled = true;
|
|
427
|
+
draft.cookiesError = new Error('no request scope');
|
|
428
|
+
const {
|
|
429
|
+
nodeAccessHeaders
|
|
430
|
+
} = await importTenant();
|
|
431
|
+
expect(await nodeAccessHeaders()).toEqual({});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
describe('unlock route handler', () => {
|
|
435
|
+
async function importUnlock() {
|
|
436
|
+
return await import("../next/unlock.js");
|
|
437
|
+
}
|
|
438
|
+
function unlockRequest(body) {
|
|
439
|
+
return new Request('https://storefront.example/api/unlock', {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
headers: {
|
|
442
|
+
'content-type': 'application/json'
|
|
443
|
+
},
|
|
444
|
+
body
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
it('POST with a token enables the draft-mode bypass', async () => {
|
|
448
|
+
const {
|
|
449
|
+
POST
|
|
450
|
+
} = await importUnlock();
|
|
451
|
+
const response = await POST(unlockRequest(JSON.stringify({
|
|
452
|
+
token: 'na-tok'
|
|
453
|
+
})));
|
|
454
|
+
expect(response.status).toBe(200);
|
|
455
|
+
expect(await response.json()).toEqual({
|
|
456
|
+
ok: true
|
|
457
|
+
});
|
|
458
|
+
expect(draft.enabled).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
it('rejects a missing token without enabling the bypass', async () => {
|
|
461
|
+
const {
|
|
462
|
+
POST
|
|
463
|
+
} = await importUnlock();
|
|
464
|
+
const response = await POST(unlockRequest(JSON.stringify({})));
|
|
465
|
+
expect(response.status).toBe(400);
|
|
466
|
+
expect(draft.enabled).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
it('rejects a malformed body without enabling the bypass', async () => {
|
|
469
|
+
const {
|
|
470
|
+
POST
|
|
471
|
+
} = await importUnlock();
|
|
472
|
+
const response = await POST(unlockRequest('not json'));
|
|
473
|
+
expect(response.status).toBe(400);
|
|
474
|
+
expect(draft.enabled).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
363
477
|
|
|
364
478
|
// withFontdue detects the route-tree shape from the working directory; give
|
|
365
479
|
// it one with or without src/app/[domain].
|
|
@@ -2,6 +2,17 @@ import React from 'react';
|
|
|
2
2
|
export interface NodePasswordForm_props {
|
|
3
3
|
collectionId?: string | null;
|
|
4
4
|
collectionSlug?: string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Path of the storefront's unlock route (see fontdue-js/next/unlock), e.g.
|
|
7
|
+
* "/api/unlock". On a successful unlock the form POSTs `{ token }` there
|
|
8
|
+
* before reloading. Required on statically-rendered Next storefronts: the
|
|
9
|
+
* route enables the draft-mode bypass that takes this visitor off the
|
|
10
|
+
* full-route cache, so the reload can actually render the unlocked
|
|
11
|
+
* collection instead of re-serving the cached password form. Omit on
|
|
12
|
+
* frameworks that render server responses per request (Astro, React Router,
|
|
13
|
+
* TanStack Start) — there the cookie alone is enough.
|
|
14
|
+
*/
|
|
15
|
+
unlockEndpoint?: string | null;
|
|
5
16
|
}
|
|
6
17
|
/**
|
|
7
18
|
* Password form for a collection you already know is locked. Render it when a
|
|
@@ -25,6 +25,29 @@ const accessNodeMutation = (_NodePasswordFormAccessNodeMutation.hash && _NodePas
|
|
|
25
25
|
export default function NodePasswordForm(props) {
|
|
26
26
|
return /*#__PURE__*/React.createElement(EnsureFontdueContext, null, /*#__PURE__*/React.createElement(NodePasswordFormFields, props));
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
// Tell the storefront's unlock route (fontdue-js/next/unlock) about a
|
|
30
|
+
// successful unlock so it can enable Next's draft-mode bypass — without it, a
|
|
31
|
+
// statically-cached font page would keep serving this visitor the password
|
|
32
|
+
// form after they unlocked (the full-route cache is visitor-blind). Always
|
|
33
|
+
// resolves: the reload must happen even if the endpoint is absent or errors,
|
|
34
|
+
// because on per-request-rendered frameworks the cookie alone unlocks.
|
|
35
|
+
async function notifyUnlockEndpoint(endpoint, token) {
|
|
36
|
+
if (!endpoint || !token) return;
|
|
37
|
+
try {
|
|
38
|
+
await fetch(endpoint, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'content-type': 'application/json'
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
token
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore: reload regardless, the cookie may still be enough.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
28
51
|
function NodePasswordFormFields(props) {
|
|
29
52
|
const [password, setPassword] = useState('');
|
|
30
53
|
const [error, setError] = useState(null);
|
|
@@ -49,7 +72,7 @@ function NodePasswordFormFields(props) {
|
|
|
49
72
|
// ride a proxied or cross-origin server fetch, so we forward this
|
|
50
73
|
// token as a header instead (see fontdue-js nodeAccess).
|
|
51
74
|
rememberNodeAccessToken(res.accessNode.token);
|
|
52
|
-
location.reload();
|
|
75
|
+
notifyUnlockEndpoint(props.unlockEndpoint, res.accessNode.token).then(() => location.reload());
|
|
53
76
|
} else {
|
|
54
77
|
setError('Incorrect password');
|
|
55
78
|
}
|
package/dist/next/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Server-side entrypoint for Next.js apps (the App Router / RSC adapter).
|
|
2
|
-
// The config-time wrapper lives in 'fontdue-js/next/config'
|
|
3
|
-
// hook route handler in 'fontdue-js/next/revalidate'
|
|
2
|
+
// The config-time wrapper lives in 'fontdue-js/next/config', the deploy
|
|
3
|
+
// hook route handler in 'fontdue-js/next/revalidate', and the
|
|
4
|
+
// password-unlock route handler in 'fontdue-js/next/unlock'.
|
|
4
5
|
|
|
5
6
|
// Single-tenant apps need NO per-render setup and nothing from this entrypoint:
|
|
6
7
|
// mounting <FontdueProvider> registers the ambient resolver that configures
|
package/dist/next/tenant.js
CHANGED
|
@@ -163,13 +163,41 @@ export async function __prepareFontdueRender(props) {
|
|
|
163
163
|
//
|
|
164
164
|
// Reading cookies() opts the route into dynamic rendering, so call this only
|
|
165
165
|
// when it's actually needed — after a public fetch comes back
|
|
166
|
-
// password-protected — to keep non-protected font pages static/cacheable.
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
166
|
+
// password-protected — to keep non-protected font pages static/cacheable.
|
|
167
|
+
//
|
|
168
|
+
// The draft-mode gate is where the caching architecture for locked
|
|
169
|
+
// collections lives, so spelling it out: locked slugs are excluded from
|
|
170
|
+
// generateStaticParams, so a visitor's first request for one is an on-demand
|
|
171
|
+
// STATIC fill of an SSG route. Merely CALLING cookies() there flags the
|
|
172
|
+
// render as dynamic, and Next 15 cannot take a runtime fill dynamic the way a
|
|
173
|
+
// build-time pass can — it 500s ("Page changed from static to dynamic at
|
|
174
|
+
// runtime", or DYNAMIC_SERVER_USAGE; FD-773). Catching the throw doesn't
|
|
175
|
+
// help: the render is already poisoned by the call. So the cookie must not be
|
|
176
|
+
// read at all unless this render is a draft-mode one — draftMode().isEnabled
|
|
177
|
+
// is static-safe (readPreviewHeaders below leans on the same property), false
|
|
178
|
+
// during any static pass.
|
|
179
|
+
//
|
|
180
|
+
// Gating on draft mode is not just crash avoidance, it IS the design: with
|
|
181
|
+
// the gate, a locked slug's static fill renders (and caches) the password
|
|
182
|
+
// form — the correct public view. The full-route cache is visitor-blind, so
|
|
183
|
+
// no cookie could ever change what it serves anyway; the only way a visitor
|
|
184
|
+
// sees unlocked content is off the static cache entirely. That's what the
|
|
185
|
+
// unlock route handler (see ./unlock) provides: on a successful unlock the
|
|
186
|
+
// form POSTs there, draft mode's bypass cookie takes this visitor's requests
|
|
187
|
+
// to dynamic renders, the gate opens, and the token folds in here. (Exactly
|
|
188
|
+
// the admin-preview model, with the password standing in for the admin
|
|
189
|
+
// session.)
|
|
190
|
+
//
|
|
191
|
+
// At build time (NEXT_PHASE=phase-production-build) the gate is skipped and
|
|
192
|
+
// the cookies() read trips Next's dynamic bailout, which unstable_rethrow
|
|
193
|
+
// propagates: a build-time render whose own fetch touches a locked collection
|
|
194
|
+
// bails that route out to dynamic today, and baking a cookie-blind form into
|
|
195
|
+
// such a page is a behavior change we don't need for this fix. Non-bailout
|
|
196
|
+
// throws (no request scope at all) still fall through to "no token".
|
|
170
197
|
export async function nodeAccessHeaders() {
|
|
171
198
|
try {
|
|
172
199
|
var _await$cookies$get;
|
|
200
|
+
if (!isBuildPhase() && !(await draftMode()).isEnabled) return {};
|
|
173
201
|
const value = (_await$cookies$get = (await cookies()).get(NODE_ACCESS_COOKIE)) === null || _await$cookies$get === void 0 ? void 0 : _await$cookies$get.value;
|
|
174
202
|
return value ? {
|
|
175
203
|
[NODE_ACCESS_HEADER]: value
|
|
@@ -179,6 +207,9 @@ export async function nodeAccessHeaders() {
|
|
|
179
207
|
return {};
|
|
180
208
|
}
|
|
181
209
|
}
|
|
210
|
+
function isBuildPhase() {
|
|
211
|
+
return typeof process !== 'undefined' && process.env.NEXT_PHASE === 'phase-production-build';
|
|
212
|
+
}
|
|
182
213
|
|
|
183
214
|
// The admin preview token for this render, as Authorization headers, or
|
|
184
215
|
// undefined when not previewing. Draft mode gates it: reading draftMode().
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Route handler that lets a visitor who unlocked a password-protected
|
|
2
|
+
// collection actually see it on a statically-served Next storefront.
|
|
3
|
+
// Re-export it from app/api/unlock/route.ts:
|
|
4
|
+
//
|
|
5
|
+
// export { POST } from 'fontdue-js/next/unlock';
|
|
6
|
+
//
|
|
7
|
+
// and pass the route's path to the form: <NodePasswordForm
|
|
8
|
+
// unlockEndpoint="/api/unlock" …/>. On a successful unlock the form POSTs the
|
|
9
|
+
// access token here before reloading.
|
|
10
|
+
//
|
|
11
|
+
// Why this exists (FD-773): font pages are SSG, and Next's full-route cache is
|
|
12
|
+
// visitor-blind — once a locked collection's page is (correctly) cached as the
|
|
13
|
+
// password form, no cookie the form sets can change what the cache serves.
|
|
14
|
+
// Enabling Next draft mode sets the __prerender_bypass cookie, which is the
|
|
15
|
+
// one sanctioned way to take a single visitor off the full-route cache: their
|
|
16
|
+
// subsequent requests render dynamically, nodeAccessHeaders() (fontdue-js/next)
|
|
17
|
+
// reads the node-access cookie, and the collection resolves. Everyone else
|
|
18
|
+
// keeps getting the static pages. This mirrors the admin-preview route
|
|
19
|
+
// (/api/preview in the templates), which layers the same draft-mode bypass on
|
|
20
|
+
// the preview cookie contract — including its trust model: the token is opaque
|
|
21
|
+
// here and validated by the Fontdue GraphQL server on every request, so a
|
|
22
|
+
// forged POST reveals nothing and buys the caller only their own dynamic
|
|
23
|
+
// renders, exactly like a forged preview POST.
|
|
24
|
+
//
|
|
25
|
+
// The bypass cookie is a session cookie, while the node-access cookie lasts ~30
|
|
26
|
+
// days. A returning visitor whose bypass lapsed sees the static form again and
|
|
27
|
+
// re-enters the password; re-unlocking re-enables the bypass. There is no
|
|
28
|
+
// DELETE: draft mode is shared with admin preview, so an unlock exit must not
|
|
29
|
+
// tear down an active preview — the bypass simply expires with the session.
|
|
30
|
+
|
|
31
|
+
import { draftMode } from 'next/headers';
|
|
32
|
+
|
|
33
|
+
/** Default path the templates mount the handler at. */
|
|
34
|
+
export const UNLOCK_ENDPOINT = '/api/unlock';
|
|
35
|
+
export async function POST(request) {
|
|
36
|
+
const body = await request.json().catch(() => ({}));
|
|
37
|
+
if (typeof body.token !== 'string' || body.token === '') {
|
|
38
|
+
return Response.json({
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'Missing access token'
|
|
41
|
+
}, {
|
|
42
|
+
status: 400
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
(await draftMode()).enable();
|
|
46
|
+
return Response.json({
|
|
47
|
+
ok: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -8,7 +8,7 @@ import { NODE_ACCESS_HEADER } from '../nodeAccess.js';
|
|
|
8
8
|
// (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
|
|
9
9
|
// Exported so UI (the admin toolbar) can surface it without re-reading the
|
|
10
10
|
// build-time global in a 'use client' module.
|
|
11
|
-
export const version = "3.
|
|
11
|
+
export const version = "3.2.0";
|
|
12
12
|
const IS_SERVER = typeof window === typeof undefined;
|
|
13
13
|
|
|
14
14
|
// Opt server fetches into Next's data cache only in production; dev stays
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdue-js",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "npm run relay && run-p build-js build-css build-ts",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"./next": "./dist/next/index.js",
|
|
91
91
|
"./next/config": "./dist/next/config.js",
|
|
92
92
|
"./next/revalidate": "./dist/next/revalidate.js",
|
|
93
|
+
"./next/unlock": "./dist/next/unlock.js",
|
|
93
94
|
"./next/image-loader": "./dist/next/image-loader.js",
|
|
94
95
|
"./fontdue.css": "./dist/fontdue.css",
|
|
95
96
|
"./BuyButton": {
|
package/types/next-headers.d.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// installed (it's the host app's dependency; this package only ever runs the
|
|
3
3
|
// import inside a Next.js server).
|
|
4
4
|
declare module 'next/headers' {
|
|
5
|
-
export function draftMode(): Promise<{
|
|
5
|
+
export function draftMode(): Promise<{
|
|
6
|
+
isEnabled: boolean;
|
|
7
|
+
enable(): void;
|
|
8
|
+
disable(): void;
|
|
9
|
+
}>;
|
|
6
10
|
export function cookies(): Promise<{
|
|
7
11
|
get(name: string): { value: string } | undefined;
|
|
8
12
|
}>;
|