fontdue-js 3.0.0-alpha8 → 3.0.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 +23 -0
- package/README.md +253 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CartQuery.graphql.js +9 -3
- package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
- package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
- package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
- package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
- package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
- 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 +9 -3
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +1 -8
- package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
- package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +276 -0
- package/dist/__tests__/imageLoader.test.js +62 -0
- package/dist/__tests__/metricFallback.test.js +74 -0
- package/dist/__tests__/networkFetch.test.js +188 -0
- package/dist/__tests__/nextAdapter.test.js +273 -18
- package/dist/__tests__/preview.test.js +217 -0
- package/dist/__tests__/previewServer.test.js +118 -0
- package/dist/__tests__/previewState.test.js +63 -0
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/BuyButton/index.d.ts +2 -2
- package/dist/components/BuyButton/index.js +3 -3
- package/dist/components/Cart/CartOrder.js +9 -1
- package/dist/components/Cart/orderTracking.js +8 -15
- package/dist/components/CharacterViewer/index.d.ts +2 -2
- package/dist/components/CharacterViewer/index.js +20 -11
- package/dist/components/ConfigContext.d.ts +21 -2
- package/dist/components/ConfigContext.js +12 -2
- package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
- package/dist/components/ConnectionErrorToolbar.js +106 -0
- package/dist/components/FontStyle/index.d.ts +2 -0
- package/dist/components/FontStyle/index.js +4 -2
- package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
- package/dist/components/FontdueAdminToolbar/index.js +299 -0
- package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
- package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
- package/dist/components/FontdueContextProvider/index.js +4 -2
- package/dist/components/FontdueProvider/index.js +6 -1
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/components/NewsletterSignup/index.d.ts +2 -2
- package/dist/components/NewsletterSignup/index.js +2 -2
- package/dist/components/Root/index.js +16 -2
- package/dist/components/TestFontsForm/index.d.ts +2 -2
- package/dist/components/TestFontsForm/index.js +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
- package/dist/components/TypeTester/index.js +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +70 -20
- package/dist/components/TypeTesters/index.d.ts +2 -2
- package/dist/components/TypeTesters/index.js +3 -3
- package/dist/components/elements/StoreModalUnifiedCheckout.js +8 -0
- package/dist/components/useFontStyle.d.ts +8 -0
- package/dist/components/useFontStyle.js +14 -4
- package/dist/corsError.d.ts +1 -5
- package/dist/corsError.js +23 -13
- package/dist/data/unicodeNamesUrl.d.ts +2 -0
- package/dist/data/unicodeNamesUrl.js +18 -0
- package/dist/data/unicodeNamesVersion.d.ts +1 -0
- package/dist/data/unicodeNamesVersion.js +4 -0
- package/dist/fallbackFontData.d.ts +2 -0
- package/dist/fallbackFontData.js +10 -0
- package/dist/fontdue.css +231 -4
- package/dist/loadFontdueProviderQuery.d.ts +2 -1
- package/dist/loadFontdueProviderQuery.js +5 -2
- package/dist/metricFallback.d.ts +48 -0
- package/dist/metricFallback.js +98 -0
- package/dist/next/config.d.ts +1 -5
- package/dist/next/config.js +14 -1
- package/dist/next/image-loader.js +22 -3
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +14 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +35 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +10 -2
- package/dist/next/tenant.js +111 -16
- package/dist/preview/constants.d.ts +9 -0
- package/dist/preview/constants.js +117 -0
- package/dist/preview/index.d.ts +53 -0
- package/dist/preview/index.js +190 -0
- package/dist/preview/server.d.ts +20 -0
- package/dist/preview/server.js +89 -0
- package/dist/relay/environment.d.ts +8 -0
- package/dist/relay/environment.js +81 -25
- package/dist/relay/loadSerializableQuery.d.ts +13 -3
- package/dist/relay/loadSerializableQuery.js +2 -0
- package/dist/relay/serverConfig.d.ts +5 -1
- package/dist/relay/serverConfig.js +83 -8
- package/dist/scripts/publishUnicodeData.js +68 -0
- package/dist/scripts/updateUnicodeData.js +41 -6
- package/dist/server/index.d.ts +37 -0
- package/dist/server/index.js +160 -0
- package/package.json +5 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +10 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
// environment.ts reads env at module load, so stub env and re-import fresh.
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
vi.unstubAllEnvs();
|
|
6
|
+
vi.unstubAllGlobals();
|
|
7
|
+
});
|
|
8
|
+
const request = {
|
|
9
|
+
name: 'TestQuery',
|
|
10
|
+
text: 'query TestQuery { viewer { id } }'
|
|
11
|
+
};
|
|
12
|
+
describe('createNetworkFetch (server)', () => {
|
|
13
|
+
it('opts fetches into the Next data cache, tagged for revalidation (production)', async () => {
|
|
14
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
15
|
+
vi.stubEnv('NODE_ENV', 'production');
|
|
16
|
+
const fetchMock = vi.fn(async () => ({
|
|
17
|
+
json: async () => ({
|
|
18
|
+
data: {}
|
|
19
|
+
})
|
|
20
|
+
}));
|
|
21
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
22
|
+
const {
|
|
23
|
+
createNetworkFetch
|
|
24
|
+
} = await import("../relay/environment.js");
|
|
25
|
+
await createNetworkFetch()(request, {});
|
|
26
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
27
|
+
const [url, options] = fetchMock.mock.calls[0];
|
|
28
|
+
expect(url).toBe('https://acme.fontdue.com/graphql?queryName=TestQuery');
|
|
29
|
+
// Without an explicit cache option Next 15 treats the fetch as no-store
|
|
30
|
+
// and silently makes every page fully dynamic — the static + revalidate
|
|
31
|
+
// pattern depends on this.
|
|
32
|
+
expect(options.cache).toBe('force-cache');
|
|
33
|
+
expect(options.next.tags).toContain('graphql');
|
|
34
|
+
expect(options.next.tags).toContain('operation:TestQuery');
|
|
35
|
+
});
|
|
36
|
+
it('skips the data cache in development so local dev is always fresh', async () => {
|
|
37
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
38
|
+
vi.stubEnv('NODE_ENV', 'development');
|
|
39
|
+
const fetchMock = vi.fn(async () => ({
|
|
40
|
+
json: async () => ({
|
|
41
|
+
data: {}
|
|
42
|
+
})
|
|
43
|
+
}));
|
|
44
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
45
|
+
const {
|
|
46
|
+
createNetworkFetch
|
|
47
|
+
} = await import("../relay/environment.js");
|
|
48
|
+
await createNetworkFetch()(request, {});
|
|
49
|
+
const [, options] = fetchMock.mock.calls[0];
|
|
50
|
+
// `next dev`'s data cache + revalidateTag don't reliably refresh, so dev
|
|
51
|
+
// fetches stay out of it (no-store) and every render fetches fresh.
|
|
52
|
+
expect(options.cache).toBe('no-store');
|
|
53
|
+
expect(options.next).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
it('applies the per-render server config tags and headers', async () => {
|
|
56
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
57
|
+
vi.stubEnv('NODE_ENV', 'production');
|
|
58
|
+
const fetchMock = vi.fn(async () => ({
|
|
59
|
+
json: async () => ({
|
|
60
|
+
data: {}
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
64
|
+
|
|
65
|
+
// React.cache doesn't memoize outside a React render, so the store is
|
|
66
|
+
// mocked rather than set through setFontdueServerConfig. The network layer
|
|
67
|
+
// reads it through resolveFontdueServerConfig (awaited per fetch).
|
|
68
|
+
vi.doMock('../relay/serverConfig', async importActual => ({
|
|
69
|
+
...(await importActual()),
|
|
70
|
+
resolveFontdueServerConfig: async () => ({
|
|
71
|
+
url: 'http://app:4000',
|
|
72
|
+
headers: {
|
|
73
|
+
'x-forwarded-host': 'acme.fontdue.com'
|
|
74
|
+
},
|
|
75
|
+
cacheTags: ['graphql:acme.fontdue.com']
|
|
76
|
+
})
|
|
77
|
+
}));
|
|
78
|
+
const {
|
|
79
|
+
createNetworkFetch
|
|
80
|
+
} = await import("../relay/environment.js");
|
|
81
|
+
await createNetworkFetch()(request, {});
|
|
82
|
+
vi.doUnmock('../relay/serverConfig');
|
|
83
|
+
const [url, options] = fetchMock.mock.calls[0];
|
|
84
|
+
expect(url).toBe('http://app:4000/graphql?queryName=TestQuery');
|
|
85
|
+
expect(options.headers['x-forwarded-host']).toBe('acme.fontdue.com');
|
|
86
|
+
expect(options.next.tags).toContain('graphql:acme.fontdue.com');
|
|
87
|
+
});
|
|
88
|
+
it('forwards per-call options.headers (e.g. a preview Bearer token)', async () => {
|
|
89
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
90
|
+
const fetchMock = vi.fn(async () => ({
|
|
91
|
+
json: async () => ({
|
|
92
|
+
data: {}
|
|
93
|
+
})
|
|
94
|
+
}));
|
|
95
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
96
|
+
|
|
97
|
+
// This is the non-RSC path: the render-scoped serverConfig store is a
|
|
98
|
+
// no-op outside an RSC render, so apps forward the token per call instead.
|
|
99
|
+
const {
|
|
100
|
+
createNetworkFetch
|
|
101
|
+
} = await import("../relay/environment.js");
|
|
102
|
+
await createNetworkFetch({
|
|
103
|
+
headers: {
|
|
104
|
+
authorization: 'Bearer preview-tok'
|
|
105
|
+
}
|
|
106
|
+
})(request, {});
|
|
107
|
+
const [, options] = fetchMock.mock.calls[0];
|
|
108
|
+
expect(options.headers.authorization).toBe('Bearer preview-tok');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
function headersOf(fetchMock) {
|
|
112
|
+
return fetchMock.mock.calls[0][1].headers;
|
|
113
|
+
}
|
|
114
|
+
describe('createNetworkFetch (fontdue-preview header)', () => {
|
|
115
|
+
it('sends fontdue-preview: false on a public server fetch (no token)', async () => {
|
|
116
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
117
|
+
const fetchMock = vi.fn(async () => ({
|
|
118
|
+
json: async () => ({
|
|
119
|
+
data: {}
|
|
120
|
+
})
|
|
121
|
+
}));
|
|
122
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
123
|
+
const {
|
|
124
|
+
createNetworkFetch
|
|
125
|
+
} = await import("../relay/environment.js");
|
|
126
|
+
await createNetworkFetch()(request, {});
|
|
127
|
+
expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
|
|
128
|
+
});
|
|
129
|
+
it('sends fontdue-preview: true when a preview Bearer token is forwarded (server)', async () => {
|
|
130
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
131
|
+
const fetchMock = vi.fn(async () => ({
|
|
132
|
+
json: async () => ({
|
|
133
|
+
data: {}
|
|
134
|
+
})
|
|
135
|
+
}));
|
|
136
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
137
|
+
const {
|
|
138
|
+
createNetworkFetch
|
|
139
|
+
} = await import("../relay/environment.js");
|
|
140
|
+
await createNetworkFetch({
|
|
141
|
+
headers: {
|
|
142
|
+
authorization: 'Bearer tok'
|
|
143
|
+
}
|
|
144
|
+
})(request, {});
|
|
145
|
+
expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
|
|
146
|
+
});
|
|
147
|
+
it('on the client, sends true only when the preview marker cookie is set', async () => {
|
|
148
|
+
// typeof window must be defined at module load for IS_SERVER to be false.
|
|
149
|
+
vi.stubGlobal('window', {
|
|
150
|
+
addEventListener: () => {}
|
|
151
|
+
});
|
|
152
|
+
vi.stubGlobal('document', {
|
|
153
|
+
cookie: 'fontdue_preview=1'
|
|
154
|
+
});
|
|
155
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
|
|
156
|
+
const fetchMock = vi.fn(async () => ({
|
|
157
|
+
json: async () => ({
|
|
158
|
+
data: {}
|
|
159
|
+
})
|
|
160
|
+
}));
|
|
161
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
162
|
+
const {
|
|
163
|
+
createNetworkFetch
|
|
164
|
+
} = await import("../relay/environment.js");
|
|
165
|
+
await createNetworkFetch()(request, {});
|
|
166
|
+
expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
|
|
167
|
+
});
|
|
168
|
+
it('on the client, sends false when the marker cookie is absent (logged-in admin browsing normally)', async () => {
|
|
169
|
+
vi.stubGlobal('window', {
|
|
170
|
+
addEventListener: () => {}
|
|
171
|
+
});
|
|
172
|
+
vi.stubGlobal('document', {
|
|
173
|
+
cookie: 'other=1'
|
|
174
|
+
});
|
|
175
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
|
|
176
|
+
const fetchMock = vi.fn(async () => ({
|
|
177
|
+
json: async () => ({
|
|
178
|
+
data: {}
|
|
179
|
+
})
|
|
180
|
+
}));
|
|
181
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
182
|
+
const {
|
|
183
|
+
createNetworkFetch
|
|
184
|
+
} = await import("../relay/environment.js");
|
|
185
|
+
await createNetworkFetch()(request, {});
|
|
186
|
+
expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
|
|
3
6
|
// The next adapter modules read process.env at module load, so each test
|
|
4
7
|
// group stubs the env and re-imports them fresh.
|
|
5
8
|
async function importTenant() {
|
|
6
9
|
return await import("../next/tenant.js");
|
|
7
10
|
}
|
|
11
|
+
// Importing this module registers the single-tenant ambient resolver as a side
|
|
12
|
+
// effect, the same way the FontdueProvider react-server entrypoint does.
|
|
13
|
+
async function importRegister() {
|
|
14
|
+
return await import("../next/registerSingleTenantResolver.js");
|
|
15
|
+
}
|
|
8
16
|
async function importConfig() {
|
|
9
17
|
return await import("../next/config.js");
|
|
10
18
|
}
|
|
@@ -15,10 +23,58 @@ const revalidateTag = vi.fn();
|
|
|
15
23
|
vi.mock('next/cache', () => ({
|
|
16
24
|
revalidateTag: tag => revalidateTag(tag)
|
|
17
25
|
}));
|
|
26
|
+
|
|
27
|
+
// Next's notFound() throws a sentinel the framework catches; the mock does
|
|
28
|
+
// the same so tests can assert on it. unstable_rethrow re-throws Next's
|
|
29
|
+
// internal control-flow errors (dynamic bailout, notFound, redirect) and is a
|
|
30
|
+
// no-op for anything else — the mock keys off the `digest` convention Next
|
|
31
|
+
// uses to tag those errors.
|
|
32
|
+
vi.mock('next/navigation', () => ({
|
|
33
|
+
notFound: () => {
|
|
34
|
+
throw new Error('NEXT_NOT_FOUND');
|
|
35
|
+
},
|
|
36
|
+
unstable_rethrow: error => {
|
|
37
|
+
const digest = error === null || error === void 0 ? void 0 : error.digest;
|
|
38
|
+
if (typeof digest === 'string' && (digest.startsWith('DYNAMIC_SERVER_USAGE') || digest.startsWith('NEXT_') || digest.startsWith('BAILOUT_TO_CLIENT_SIDE_RENDERING'))) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Draft mode + the preview token cookie, controllable per test. Default: not
|
|
45
|
+
// previewing, so __prepareFontdueRender takes the public (cached) path. Set
|
|
46
|
+
// cookiesError to make cookies() throw (e.g. simulate Next's dynamic bailout
|
|
47
|
+
// during a prerender pass).
|
|
48
|
+
const draft = vi.hoisted(() => ({
|
|
49
|
+
enabled: false,
|
|
50
|
+
token: undefined,
|
|
51
|
+
cookiesError: undefined
|
|
52
|
+
}));
|
|
53
|
+
vi.mock('next/headers', () => ({
|
|
54
|
+
draftMode: async () => ({
|
|
55
|
+
isEnabled: draft.enabled
|
|
56
|
+
}),
|
|
57
|
+
cookies: async () => {
|
|
58
|
+
if (draft.cookiesError) throw draft.cookiesError;
|
|
59
|
+
return {
|
|
60
|
+
get: name => name === 'fontdue_preview_token' && draft.token ? {
|
|
61
|
+
value: draft.token
|
|
62
|
+
} : undefined
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
18
66
|
beforeEach(() => {
|
|
19
67
|
vi.resetModules();
|
|
68
|
+
// The server-config store (incl. the ambient resolver) is anchored on
|
|
69
|
+
// globalThis, so it survives resetModules; clear it so a resolver registered
|
|
70
|
+
// by one test can't leak into the next.
|
|
71
|
+
delete globalThis.__fontdueServerConfigStore__;
|
|
20
72
|
revalidateTag.mockClear();
|
|
21
73
|
vi.unstubAllEnvs();
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
draft.enabled = false;
|
|
76
|
+
draft.token = undefined;
|
|
77
|
+
draft.cookiesError = undefined;
|
|
22
78
|
});
|
|
23
79
|
function stubSingleTenant() {
|
|
24
80
|
let url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'https://acme.fontdue.com';
|
|
@@ -52,13 +108,14 @@ describe('isValidDomain', () => {
|
|
|
52
108
|
expect(isValidDomain('a'.repeat(254) + '.example')).toBe(false);
|
|
53
109
|
});
|
|
54
110
|
});
|
|
55
|
-
describe('
|
|
111
|
+
describe('endpointForDomain', () => {
|
|
56
112
|
it('single-tenant: targets NEXT_PUBLIC_FONTDUE_URL with no headers', async () => {
|
|
57
113
|
stubSingleTenant('https://acme.fontdue.com');
|
|
58
114
|
const {
|
|
59
|
-
|
|
115
|
+
endpointForDomain
|
|
60
116
|
} = await importTenant();
|
|
61
|
-
expect(
|
|
117
|
+
expect(endpointForDomain('acme.fontdue.com')).toEqual({
|
|
118
|
+
domain: 'acme.fontdue.com',
|
|
62
119
|
origin: 'https://acme.fontdue.com',
|
|
63
120
|
headers: {},
|
|
64
121
|
tags: ['graphql', 'graphql:acme.fontdue.com']
|
|
@@ -68,9 +125,9 @@ describe('fontdueEndpoint', () => {
|
|
|
68
125
|
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
69
126
|
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
70
127
|
const {
|
|
71
|
-
|
|
128
|
+
endpointForDomain
|
|
72
129
|
} = await importTenant();
|
|
73
|
-
expect(() =>
|
|
130
|
+
expect(() => endpointForDomain('acme.fontdue.com')).toThrow(/NEXT_PUBLIC_FONTDUE_URL/);
|
|
74
131
|
});
|
|
75
132
|
it('multi-tenant: forwards the host to FONTDUE_ORIGIN with the proxy secret', async () => {
|
|
76
133
|
stubMultiTenant({
|
|
@@ -78,9 +135,10 @@ describe('fontdueEndpoint', () => {
|
|
|
78
135
|
secret: 's3cret'
|
|
79
136
|
});
|
|
80
137
|
const {
|
|
81
|
-
|
|
138
|
+
endpointForDomain
|
|
82
139
|
} = await importTenant();
|
|
83
|
-
expect(
|
|
140
|
+
expect(endpointForDomain('acme.fontdue.com')).toEqual({
|
|
141
|
+
domain: 'acme.fontdue.com',
|
|
84
142
|
origin: 'http://app:4000',
|
|
85
143
|
headers: {
|
|
86
144
|
'x-forwarded-host': 'acme.fontdue.com',
|
|
@@ -94,18 +152,18 @@ describe('fontdueEndpoint', () => {
|
|
|
94
152
|
origin: 'http://app:4000'
|
|
95
153
|
});
|
|
96
154
|
const {
|
|
97
|
-
|
|
155
|
+
endpointForDomain
|
|
98
156
|
} = await importTenant();
|
|
99
|
-
expect(
|
|
157
|
+
expect(endpointForDomain('acme.fontdue.com').headers).toEqual({
|
|
100
158
|
'x-forwarded-host': 'acme.fontdue.com'
|
|
101
159
|
});
|
|
102
160
|
});
|
|
103
161
|
it('multi-tenant: falls back to the tenant public URL without FONTDUE_ORIGIN', async () => {
|
|
104
162
|
stubMultiTenant();
|
|
105
163
|
const {
|
|
106
|
-
|
|
164
|
+
endpointForDomain
|
|
107
165
|
} = await importTenant();
|
|
108
|
-
expect(
|
|
166
|
+
expect(endpointForDomain('acme.fontdue.com').origin).toBe('https://acme.fontdue.com');
|
|
109
167
|
});
|
|
110
168
|
});
|
|
111
169
|
describe('configureFontdueRender', () => {
|
|
@@ -116,11 +174,8 @@ describe('configureFontdueRender', () => {
|
|
|
116
174
|
const {
|
|
117
175
|
configureFontdueRender
|
|
118
176
|
} = await importTenant();
|
|
119
|
-
|
|
120
|
-
getFontdueServerConfig
|
|
121
|
-
} = await import("../relay/serverConfig.js");
|
|
177
|
+
// Invalid domain returns null before any setFontdueServerConfig call.
|
|
122
178
|
expect(configureFontdueRender('not a domain')).toBeNull();
|
|
123
|
-
expect(getFontdueServerConfig()).toBeUndefined();
|
|
124
179
|
});
|
|
125
180
|
it('sets the per-render server config and returns the endpoint', async () => {
|
|
126
181
|
stubMultiTenant({
|
|
@@ -130,9 +185,6 @@ describe('configureFontdueRender', () => {
|
|
|
130
185
|
const {
|
|
131
186
|
configureFontdueRender
|
|
132
187
|
} = await importTenant();
|
|
133
|
-
const {
|
|
134
|
-
getFontdueServerConfig
|
|
135
|
-
} = await import("../relay/serverConfig.js");
|
|
136
188
|
const endpoint = configureFontdueRender('acme.fontdue.com');
|
|
137
189
|
expect(endpoint === null || endpoint === void 0 ? void 0 : endpoint.origin).toBe('http://app:4000');
|
|
138
190
|
// Outside an RSC render React.cache doesn't memoize, so the write is a
|
|
@@ -151,7 +203,180 @@ describe('configureFontdueRender', () => {
|
|
|
151
203
|
});
|
|
152
204
|
});
|
|
153
205
|
});
|
|
206
|
+
describe('__prepareFontdueRender', () => {
|
|
207
|
+
const props = params => ({
|
|
208
|
+
params: Promise.resolve(params)
|
|
209
|
+
});
|
|
210
|
+
it('configures the render and returns the endpoint', async () => {
|
|
211
|
+
stubMultiTenant({
|
|
212
|
+
origin: 'http://app:4000'
|
|
213
|
+
});
|
|
214
|
+
const {
|
|
215
|
+
__prepareFontdueRender
|
|
216
|
+
} = await importTenant();
|
|
217
|
+
const endpoint = await __prepareFontdueRender(props({
|
|
218
|
+
domain: 'acme.fontdue.com',
|
|
219
|
+
slug: 'sans'
|
|
220
|
+
}));
|
|
221
|
+
expect(endpoint.origin).toBe('http://app:4000');
|
|
222
|
+
expect(endpoint.tags).toContain('graphql:acme.fontdue.com');
|
|
223
|
+
});
|
|
224
|
+
it('404s invalid or missing domains', async () => {
|
|
225
|
+
stubMultiTenant();
|
|
226
|
+
const {
|
|
227
|
+
__prepareFontdueRender
|
|
228
|
+
} = await importTenant();
|
|
229
|
+
await expect(__prepareFontdueRender(props({
|
|
230
|
+
domain: 'not a domain'
|
|
231
|
+
}))).rejects.toThrow('NEXT_NOT_FOUND');
|
|
232
|
+
await expect(__prepareFontdueRender(props({
|
|
233
|
+
slug: 'sans'
|
|
234
|
+
}))).rejects.toThrow('NEXT_NOT_FOUND');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// React.cache doesn't memoize outside an RSC render (see configureFontdueRender
|
|
238
|
+
// above), so the slot write is a no-op here — capture what __prepareFontdueRender
|
|
239
|
+
// passes to setFontdueServerConfig instead.
|
|
240
|
+
async function captureRenderConfig(params) {
|
|
241
|
+
let captured;
|
|
242
|
+
vi.doMock('../relay/serverConfig', async importActual => ({
|
|
243
|
+
...(await importActual()),
|
|
244
|
+
setFontdueServerConfig: c => {
|
|
245
|
+
captured = c;
|
|
246
|
+
}
|
|
247
|
+
}));
|
|
248
|
+
const {
|
|
249
|
+
__prepareFontdueRender
|
|
250
|
+
} = await importTenant();
|
|
251
|
+
await __prepareFontdueRender(props(params));
|
|
252
|
+
vi.doUnmock('../relay/serverConfig');
|
|
253
|
+
return captured;
|
|
254
|
+
}
|
|
255
|
+
it('public render: sets the tenant config with cache tags and no token', async () => {
|
|
256
|
+
var _config$headers;
|
|
257
|
+
stubMultiTenant({
|
|
258
|
+
origin: 'http://app:4000'
|
|
259
|
+
});
|
|
260
|
+
const config = await captureRenderConfig({
|
|
261
|
+
domain: 'acme.fontdue.com'
|
|
262
|
+
});
|
|
263
|
+
expect(config.cacheTags).toEqual(['graphql:acme.fontdue.com']);
|
|
264
|
+
expect((_config$headers = config.headers) === null || _config$headers === void 0 ? void 0 : _config$headers.authorization).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
it('preview render: folds the token into headers and drops cache tags', async () => {
|
|
267
|
+
var _config$headers2;
|
|
268
|
+
stubMultiTenant({
|
|
269
|
+
origin: 'http://app:4000'
|
|
270
|
+
});
|
|
271
|
+
draft.enabled = true;
|
|
272
|
+
draft.token = 'admin-tok';
|
|
273
|
+
const config = await captureRenderConfig({
|
|
274
|
+
domain: 'acme.fontdue.com'
|
|
275
|
+
});
|
|
276
|
+
// Embeds (createNetworkFetch) and the app's fetch (createFontdueFetch) both
|
|
277
|
+
// read this, so both reveal hidden fonts; no tags keeps the render live.
|
|
278
|
+
expect((_config$headers2 = config.headers) === null || _config$headers2 === void 0 ? void 0 : _config$headers2.authorization).toBe('Bearer admin-tok');
|
|
279
|
+
expect(config.cacheTags).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('single-tenant ambient resolver (no per-render call)', () => {
|
|
283
|
+
// Importing registerSingleTenantResolver registers the resolver as a module
|
|
284
|
+
// side effect — the FontdueProvider RSC entrypoint does this import, so
|
|
285
|
+
// merely mounting the provider wires it up. No per-render setup call is made
|
|
286
|
+
// in any of these tests; the config is pulled at fetch time.
|
|
287
|
+
it('public render: feeds the env URL + cache tags, no token', async () => {
|
|
288
|
+
var _config$headers3;
|
|
289
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
290
|
+
await importRegister();
|
|
291
|
+
const {
|
|
292
|
+
resolveFontdueServerConfig
|
|
293
|
+
} = await import("../relay/serverConfig.js");
|
|
294
|
+
const config = await resolveFontdueServerConfig();
|
|
295
|
+
expect(config === null || config === void 0 ? void 0 : config.url).toBe('https://acme.fontdue.com');
|
|
296
|
+
expect(config === null || config === void 0 ? void 0 : config.cacheTags).toEqual(['graphql:acme.fontdue.com']);
|
|
297
|
+
expect(config === null || config === void 0 ? void 0 : (_config$headers3 = config.headers) === null || _config$headers3 === void 0 ? void 0 : _config$headers3.authorization).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
it('preview render: folds in the admin token and drops cache tags', async () => {
|
|
300
|
+
var _config$headers4;
|
|
301
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
302
|
+
draft.enabled = true;
|
|
303
|
+
draft.token = 'admin-tok';
|
|
304
|
+
await importRegister();
|
|
305
|
+
const {
|
|
306
|
+
resolveFontdueServerConfig
|
|
307
|
+
} = await import("../relay/serverConfig.js");
|
|
308
|
+
const config = await resolveFontdueServerConfig();
|
|
309
|
+
// This is what reveals hidden fonts in an embedded component's own preload
|
|
310
|
+
// on a page that never calls the app's GraphQL fetcher.
|
|
311
|
+
expect(config === null || config === void 0 ? void 0 : (_config$headers4 = config.headers) === null || _config$headers4 === void 0 ? void 0 : _config$headers4.authorization).toBe('Bearer admin-tok');
|
|
312
|
+
expect(config === null || config === void 0 ? void 0 : config.cacheTags).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
it('multi-tenant: resolver stays out of the way so the slot drives config', async () => {
|
|
315
|
+
stubMultiTenant({
|
|
316
|
+
origin: 'http://app:4000'
|
|
317
|
+
});
|
|
318
|
+
await importRegister();
|
|
319
|
+
const {
|
|
320
|
+
resolveFontdueServerConfig
|
|
321
|
+
} = await import("../relay/serverConfig.js");
|
|
322
|
+
expect(await resolveFontdueServerConfig()).toBeUndefined();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Regression for FD-712: a preview render that begins as a static/prerender
|
|
326
|
+
// pass must NOT swallow Next's dynamic bailout — re-throwing it is what takes
|
|
327
|
+
// the route off the full-route cache so the embeds' preloads re-run with the
|
|
328
|
+
// token. If this were swallowed, every server fetch (including a <BuyButton>
|
|
329
|
+
// node(id) @required(THROW) preload) would silently get the public view.
|
|
330
|
+
it('preview render: propagates Next’s dynamic bailout instead of dropping the token', async () => {
|
|
331
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
332
|
+
draft.enabled = true;
|
|
333
|
+
draft.token = 'admin-tok';
|
|
334
|
+
draft.cookiesError = Object.assign(new Error('Dynamic server usage'), {
|
|
335
|
+
digest: 'DYNAMIC_SERVER_USAGE'
|
|
336
|
+
});
|
|
337
|
+
await importRegister();
|
|
338
|
+
const {
|
|
339
|
+
resolveFontdueServerConfig
|
|
340
|
+
} = await import("../relay/serverConfig.js");
|
|
341
|
+
await expect(resolveFontdueServerConfig()).rejects.toBe(draft.cookiesError);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// A non-control-flow throw (e.g. no request scope at all) is not a bailout,
|
|
345
|
+
// so it's swallowed and the render degrades to the public (token-less) config
|
|
346
|
+
// rather than crashing.
|
|
347
|
+
it('preview render: swallows a non-control-flow throw and falls back to public', async () => {
|
|
348
|
+
var _config$headers5;
|
|
349
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
350
|
+
draft.enabled = true;
|
|
351
|
+
draft.token = 'admin-tok';
|
|
352
|
+
draft.cookiesError = new Error('no request scope');
|
|
353
|
+
await importRegister();
|
|
354
|
+
const {
|
|
355
|
+
resolveFontdueServerConfig
|
|
356
|
+
} = await import("../relay/serverConfig.js");
|
|
357
|
+
const config = await resolveFontdueServerConfig();
|
|
358
|
+
expect(config === null || config === void 0 ? void 0 : config.url).toBe('https://acme.fontdue.com');
|
|
359
|
+
expect(config === null || config === void 0 ? void 0 : config.cacheTags).toEqual(['graphql:acme.fontdue.com']);
|
|
360
|
+
expect(config === null || config === void 0 ? void 0 : (_config$headers5 = config.headers) === null || _config$headers5 === void 0 ? void 0 : _config$headers5.authorization).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// withFontdue detects the route-tree shape from the working directory; give
|
|
365
|
+
// it one with or without src/app/[domain].
|
|
366
|
+
function mockAppDir(_ref) {
|
|
367
|
+
let {
|
|
368
|
+
domainTree
|
|
369
|
+
} = _ref;
|
|
370
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fontdue-app-'));
|
|
371
|
+
fs.mkdirSync(path.join(dir, domainTree ? 'src/app/[domain]' : 'src/app'), {
|
|
372
|
+
recursive: true
|
|
373
|
+
});
|
|
374
|
+
vi.spyOn(process, 'cwd').mockReturnValue(dir);
|
|
375
|
+
}
|
|
154
376
|
describe('withFontdue', () => {
|
|
377
|
+
beforeEach(() => mockAppDir({
|
|
378
|
+
domainTree: true
|
|
379
|
+
}));
|
|
155
380
|
it('throws when neither mode is configured', async () => {
|
|
156
381
|
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
157
382
|
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
@@ -267,6 +492,36 @@ describe('withFontdue', () => {
|
|
|
267
492
|
htmlLimitedBots: /Googlebot/
|
|
268
493
|
}).htmlLimitedBots).toEqual(/Googlebot/);
|
|
269
494
|
});
|
|
495
|
+
it('single-tenant flat tree: installs no tenant rewrites', async () => {
|
|
496
|
+
mockAppDir({
|
|
497
|
+
domainTree: false
|
|
498
|
+
});
|
|
499
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
500
|
+
const {
|
|
501
|
+
withFontdue
|
|
502
|
+
} = await importConfig();
|
|
503
|
+
const userRule = {
|
|
504
|
+
source: '/old',
|
|
505
|
+
destination: '/new'
|
|
506
|
+
};
|
|
507
|
+
const rewrites = await withFontdue({
|
|
508
|
+
rewrites: async () => ({
|
|
509
|
+
beforeFiles: [userRule]
|
|
510
|
+
})
|
|
511
|
+
}).rewrites();
|
|
512
|
+
expect(rewrites.beforeFiles).toEqual([userRule]);
|
|
513
|
+
expect(rewrites.afterFiles).toEqual([]);
|
|
514
|
+
});
|
|
515
|
+
it('multi-tenant flat tree: refuses to start', async () => {
|
|
516
|
+
mockAppDir({
|
|
517
|
+
domainTree: false
|
|
518
|
+
});
|
|
519
|
+
stubMultiTenant();
|
|
520
|
+
const {
|
|
521
|
+
withFontdue
|
|
522
|
+
} = await importConfig();
|
|
523
|
+
expect(() => withFontdue({})).toThrow(/\[domain\]/);
|
|
524
|
+
});
|
|
270
525
|
});
|
|
271
526
|
describe('revalidate POST', () => {
|
|
272
527
|
it('multi-tenant: purges only the tenant tag', async () => {
|