fontdue-js 3.0.0-alpha10 → 3.0.0-alpha11

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.
Files changed (41) hide show
  1. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
  2. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
  3. package/dist/__tests__/createFontdueFetch.test.js +104 -0
  4. package/dist/__tests__/imageLoader.test.js +62 -0
  5. package/dist/__tests__/networkFetch.test.js +22 -0
  6. package/dist/__tests__/preview.test.js +96 -0
  7. package/dist/__tests__/previewServer.test.js +118 -0
  8. package/dist/components/BuyButton/index.d.ts +2 -2
  9. package/dist/components/BuyButton/index.js +3 -3
  10. package/dist/components/CharacterViewer/index.d.ts +2 -2
  11. package/dist/components/CharacterViewer/index.js +3 -3
  12. package/dist/components/ConfigContext.d.ts +10 -0
  13. package/dist/components/ConfigContext.js +5 -1
  14. package/dist/components/FontdueAdminToolbar/index.js +26 -25
  15. package/dist/components/FontdueProvider/index.js +2 -2
  16. package/dist/components/NewsletterSignup/index.d.ts +2 -2
  17. package/dist/components/NewsletterSignup/index.js +2 -2
  18. package/dist/components/TestFontsForm/index.d.ts +2 -2
  19. package/dist/components/TestFontsForm/index.js +2 -2
  20. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
  21. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
  22. package/dist/components/TypeTesters/index.d.ts +2 -2
  23. package/dist/components/TypeTesters/index.js +3 -3
  24. package/dist/loadFontdueProviderQuery.d.ts +2 -1
  25. package/dist/loadFontdueProviderQuery.js +5 -2
  26. package/dist/next/image-loader.js +22 -3
  27. package/dist/preview/constants.d.ts +3 -0
  28. package/dist/preview/constants.js +18 -0
  29. package/dist/preview/index.d.ts +49 -0
  30. package/dist/preview/index.js +164 -0
  31. package/dist/preview/server.d.ts +20 -0
  32. package/dist/preview/server.js +89 -0
  33. package/dist/relay/environment.d.ts +6 -0
  34. package/dist/relay/environment.js +4 -1
  35. package/dist/relay/loadSerializableQuery.d.ts +13 -3
  36. package/dist/relay/loadSerializableQuery.js +2 -0
  37. package/dist/relay/serverConfig.d.ts +3 -0
  38. package/dist/relay/serverConfig.js +26 -1
  39. package/dist/server/index.d.ts +25 -0
  40. package/dist/server/index.js +101 -0
  41. package/package.json +4 -1
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @generated SignedSource<<c46bd30612c05d6cd1c334c403aafb35>>
3
+ * @lightSyntaxTransform
4
+ * @nogrep
5
+ */
6
+ import { ConcreteRequest } from 'relay-runtime';
7
+ export type FontdueAdminToolbarQuery$variables = Record<PropertyKey, never>;
8
+ export type FontdueAdminToolbarQuery$data = {
9
+ readonly viewer: {
10
+ readonly adminUser: {
11
+ readonly name: string | null;
12
+ } | null;
13
+ };
14
+ };
15
+ export type FontdueAdminToolbarQuery = {
16
+ response: FontdueAdminToolbarQuery$data;
17
+ variables: FontdueAdminToolbarQuery$variables;
18
+ };
19
+ declare const node: ConcreteRequest;
20
+ export default node;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @generated SignedSource<<c46bd30612c05d6cd1c334c403aafb35>>
3
+ * @lightSyntaxTransform
4
+ * @nogrep
5
+ */
6
+
7
+ /* tslint:disable */
8
+ /* eslint-disable */
9
+ // @ts-nocheck
10
+
11
+ const node = function () {
12
+ var v0 = {
13
+ "alias": null,
14
+ "args": null,
15
+ "concreteType": "User",
16
+ "kind": "LinkedField",
17
+ "name": "adminUser",
18
+ "plural": false,
19
+ "selections": [{
20
+ "alias": null,
21
+ "args": null,
22
+ "kind": "ScalarField",
23
+ "name": "name",
24
+ "storageKey": null
25
+ }],
26
+ "storageKey": null
27
+ };
28
+ return {
29
+ "fragment": {
30
+ "argumentDefinitions": [],
31
+ "kind": "Fragment",
32
+ "metadata": null,
33
+ "name": "FontdueAdminToolbarQuery",
34
+ "selections": [{
35
+ "alias": null,
36
+ "args": null,
37
+ "concreteType": "Viewer",
38
+ "kind": "LinkedField",
39
+ "name": "viewer",
40
+ "plural": false,
41
+ "selections": [v0 /*: any*/],
42
+ "storageKey": null
43
+ }],
44
+ "type": "RootQueryType",
45
+ "abstractKey": null
46
+ },
47
+ "kind": "Request",
48
+ "operation": {
49
+ "argumentDefinitions": [],
50
+ "kind": "Operation",
51
+ "name": "FontdueAdminToolbarQuery",
52
+ "selections": [{
53
+ "alias": null,
54
+ "args": null,
55
+ "concreteType": "Viewer",
56
+ "kind": "LinkedField",
57
+ "name": "viewer",
58
+ "plural": false,
59
+ "selections": [v0 /*: any*/, {
60
+ "alias": null,
61
+ "args": null,
62
+ "kind": "ScalarField",
63
+ "name": "id",
64
+ "storageKey": null
65
+ }],
66
+ "storageKey": null
67
+ }]
68
+ },
69
+ "params": {
70
+ "cacheID": "a48946b192fea90930ffac67eff7b1b5",
71
+ "id": null,
72
+ "metadata": {},
73
+ "name": "FontdueAdminToolbarQuery",
74
+ "operationKind": "query",
75
+ "text": "query FontdueAdminToolbarQuery {\n viewer {\n adminUser {\n name\n }\n id\n }\n}\n"
76
+ }
77
+ };
78
+ }();
79
+ node.hash = "8660b45f086137d249eee82459ad648d";
80
+ export default node;
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createFontdueFetch, FontdueNotFoundError } from '../server/index.js';
3
+ beforeEach(() => {
4
+ vi.unstubAllEnvs();
5
+ vi.unstubAllGlobals();
6
+ });
7
+ function mockFetch(impl) {
8
+ const fetchMock = vi.fn(async (url, init) => impl(url, init));
9
+ vi.stubGlobal('fetch', fetchMock);
10
+ return fetchMock;
11
+ }
12
+ describe('createFontdueFetch', () => {
13
+ it('posts the query to the configured URL and unwraps data', async () => {
14
+ const fetchMock = mockFetch(() => ({
15
+ status: 200,
16
+ json: async () => ({
17
+ data: {
18
+ viewer: {
19
+ id: '1'
20
+ }
21
+ }
22
+ })
23
+ }));
24
+ const fetchGraphql = createFontdueFetch({
25
+ url: 'https://acme.fontdue.com'
26
+ });
27
+ const data = await fetchGraphql('Index', 'query Index { viewer { id } }', {
28
+ a: 1
29
+ });
30
+ expect(data).toEqual({
31
+ viewer: {
32
+ id: '1'
33
+ }
34
+ });
35
+ const [url, init] = fetchMock.mock.calls[0];
36
+ expect(url).toBe('https://acme.fontdue.com/graphql?query=Index');
37
+ expect(init.method).toBe('POST');
38
+ expect(JSON.parse(init.body)).toEqual({
39
+ query: 'query Index { viewer { id } }',
40
+ variables: {
41
+ a: 1
42
+ }
43
+ });
44
+ });
45
+ it('forwards bound headers (the preview Bearer token) on every call', async () => {
46
+ const fetchMock = mockFetch(() => ({
47
+ status: 200,
48
+ json: async () => ({
49
+ data: {}
50
+ })
51
+ }));
52
+ const fetchGraphql = createFontdueFetch({
53
+ url: 'https://acme.fontdue.com',
54
+ headers: {
55
+ authorization: 'Bearer admin-tok'
56
+ }
57
+ });
58
+ await fetchGraphql('A', 'query A { __typename }');
59
+ await fetchGraphql('B', 'query B { __typename }');
60
+ for (const call of fetchMock.mock.calls) {
61
+ const init = call[1];
62
+ expect(init.headers.authorization).toBe('Bearer admin-tok');
63
+ }
64
+ });
65
+ it('throws FontdueNotFoundError on a 404 (host did not resolve)', async () => {
66
+ mockFetch(() => ({
67
+ status: 404,
68
+ json: async () => ({})
69
+ }));
70
+ const fetchGraphql = createFontdueFetch({
71
+ url: 'https://acme.fontdue.com'
72
+ });
73
+ await expect(fetchGraphql('X', 'query X { __typename }')).rejects.toBeInstanceOf(FontdueNotFoundError);
74
+ });
75
+ it('throws on GraphQL errors in a 200 response', async () => {
76
+ mockFetch(() => ({
77
+ status: 200,
78
+ json: async () => ({
79
+ errors: [{
80
+ message: 'boom'
81
+ }]
82
+ })
83
+ }));
84
+ const fetchGraphql = createFontdueFetch({
85
+ url: 'https://acme.fontdue.com'
86
+ });
87
+ await expect(fetchGraphql('X', 'query X { __typename }')).rejects.toThrow('boom');
88
+ });
89
+ it('resolves the URL from the environment when none is passed', async () => {
90
+ vi.stubEnv('FONTDUE_URL', 'https://env.fontdue.com');
91
+ const fetchMock = mockFetch(() => ({
92
+ status: 200,
93
+ json: async () => ({
94
+ data: {}
95
+ })
96
+ }));
97
+ const fetchGraphql = createFontdueFetch();
98
+ await fetchGraphql('Q', 'query Q { __typename }');
99
+ expect(fetchMock.mock.calls[0][0]).toBe('https://env.fontdue.com/graphql?query=Q');
100
+ });
101
+ it('throws a helpful error when no URL is configured', () => {
102
+ expect(() => createFontdueFetch()).toThrow(/no Fontdue URL configured/);
103
+ });
104
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import fontdueImageLoader from '../next/image-loader.js';
3
+
4
+ // The loader reads NEXT_PUBLIC_FONTDUE_IMAGE_HOST / _ORIGINS at call time, so
5
+ // each test stubs them and we clear stubs afterwards.
6
+ afterEach(() => {
7
+ vi.unstubAllEnvs();
8
+ });
9
+ function load(src) {
10
+ let origins = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
11
+ let host = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'img.fontdue.xyz';
12
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_IMAGE_HOST', host);
13
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS', origins);
14
+ return fontdueImageLoader({
15
+ src,
16
+ width: 800,
17
+ quality: 75
18
+ });
19
+ }
20
+ const transformed = src => `https://img.fontdue.xyz/cdn-cgi/image/width=800,quality=75,format=auto/${src}`;
21
+ describe('fontdueImageLoader', () => {
22
+ it('transforms any absolute src when no allowlist is set', () => {
23
+ const src = 'https://anywhere.example/a.jpg';
24
+ expect(load(src)).toBe(transformed(src));
25
+ });
26
+ it('serves relative srcs as-is', () => {
27
+ expect(load('/logo.svg', 'cdn.fontdue.xyz')).toBe('/logo.svg');
28
+ });
29
+ it('serves srcs as-is when no transform host is configured', () => {
30
+ const src = 'https://cdn.fontdue.xyz/a.jpg';
31
+ expect(load(src, 'cdn.fontdue.xyz', '')).toBe(src);
32
+ });
33
+ describe('origins allowlist', () => {
34
+ it('transforms an exact hostname match and skips a non-match', () => {
35
+ const ok = 'https://cdn.fontdue.xyz/a.jpg';
36
+ const no = 'https://other.example/a.jpg';
37
+ expect(load(ok, 'cdn.fontdue.xyz')).toBe(transformed(ok));
38
+ expect(load(no, 'cdn.fontdue.xyz')).toBe(no);
39
+ });
40
+ it('matches a "*." wildcard against any subdomain', () => {
41
+ const cdn = 'https://cdn.fontdue.xyz/a.jpg';
42
+ const assets = 'https://assets.fontdue.xyz/a.jpg';
43
+ expect(load(cdn, '*.fontdue.xyz')).toBe(transformed(cdn));
44
+ expect(load(assets, '*.fontdue.xyz')).toBe(transformed(assets));
45
+ });
46
+ it('does not let a wildcard match the apex or a lookalike domain', () => {
47
+ const apex = 'https://fontdue.xyz/a.jpg';
48
+ const lookalike = 'https://evilfontdue.xyz/a.jpg';
49
+ expect(load(apex, '*.fontdue.xyz')).toBe(apex);
50
+ expect(load(lookalike, '*.fontdue.xyz')).toBe(lookalike);
51
+ });
52
+ it('ignores an optional scheme on allowlist entries', () => {
53
+ const src = 'https://cdn.fontdue.xyz/a.jpg';
54
+ expect(load(src, 'https://cdn.fontdue.xyz')).toBe(transformed(src));
55
+ expect(load(src, 'https://*.fontdue.xyz')).toBe(transformed(src));
56
+ });
57
+ it('supports a comma-separated list', () => {
58
+ const src = 'https://assets.fontdue.xyz/a.jpg';
59
+ expect(load(src, 'cdn.fontdue.xyz, assets.fontdue.xyz')).toBe(transformed(src));
60
+ });
61
+ });
62
+ });
@@ -63,4 +63,26 @@ describe('createNetworkFetch (server)', () => {
63
63
  expect(options.headers['x-forwarded-host']).toBe('acme.fontdue.com');
64
64
  expect(options.next.tags).toContain('graphql:acme.fontdue.com');
65
65
  });
66
+ it('forwards per-call options.headers (e.g. a preview Bearer token)', async () => {
67
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
68
+ const fetchMock = vi.fn(async () => ({
69
+ json: async () => ({
70
+ data: {}
71
+ })
72
+ }));
73
+ vi.stubGlobal('fetch', fetchMock);
74
+
75
+ // This is the non-RSC path: the render-scoped serverConfig store is a
76
+ // no-op outside an RSC render, so apps forward the token per call instead.
77
+ const {
78
+ createNetworkFetch
79
+ } = await import("../relay/environment.js");
80
+ await createNetworkFetch({
81
+ headers: {
82
+ authorization: 'Bearer preview-tok'
83
+ }
84
+ })(request, {});
85
+ const [, options] = fetchMock.mock.calls[0];
86
+ expect(options.headers.authorization).toBe('Bearer preview-tok');
87
+ });
66
88
  });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { handlePreviewRequest, readPreviewToken, previewAuthHeaders, PREVIEW_TOKEN_COOKIE, PREVIEW_MARKER_COOKIE } from '../preview/index.js';
3
+ function postPreview(token) {
4
+ let url = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'https://site.test/api/preview';
5
+ return handlePreviewRequest(new Request(url, {
6
+ method: 'POST',
7
+ headers: {
8
+ 'content-type': 'application/json'
9
+ },
10
+ body: JSON.stringify({
11
+ token
12
+ })
13
+ }));
14
+ }
15
+ describe('handlePreviewRequest', () => {
16
+ it('enters preview: sets an httpOnly token cookie + a readable marker', async () => {
17
+ const res = await postPreview('admin.tok.123');
18
+ expect(res.status).toBe(200);
19
+ expect(await res.json()).toEqual({
20
+ ok: true,
21
+ preview: true
22
+ });
23
+ const cookies = res.headers.getSetCookie();
24
+ const tokenCookie = cookies.find(c => c.startsWith(`${PREVIEW_TOKEN_COOKIE}=`));
25
+ const markerCookie = cookies.find(c => c.startsWith(`${PREVIEW_MARKER_COOKIE}=`));
26
+ expect(tokenCookie).toContain(`${PREVIEW_TOKEN_COOKIE}=admin.tok.123`);
27
+ expect(tokenCookie).toContain('HttpOnly');
28
+ expect(tokenCookie).toContain('SameSite=Lax');
29
+ expect(tokenCookie).toContain('Path=/');
30
+ expect(markerCookie).toContain(`${PREVIEW_MARKER_COOKIE}=1`);
31
+ // The marker must be readable by the client toolbar.
32
+ expect(markerCookie).not.toContain('HttpOnly');
33
+ });
34
+ it('marks cookies Secure on https and not on http (local dev)', async () => {
35
+ const httpsCookies = (await postPreview('t')).headers.getSetCookie();
36
+ expect(httpsCookies.every(c => c.includes('Secure'))).toBe(true);
37
+ const httpCookies = (await postPreview('t', 'http://localhost:4321/api/preview')).headers.getSetCookie();
38
+ expect(httpCookies.some(c => c.includes('Secure'))).toBe(false);
39
+ });
40
+ it('rejects a POST with no token', async () => {
41
+ const res = await postPreview('');
42
+ expect(res.status).toBe(400);
43
+ expect(res.headers.getSetCookie()).toHaveLength(0);
44
+ });
45
+ it('exits preview: clears both cookies', async () => {
46
+ const res = await handlePreviewRequest(new Request('https://site.test/api/preview', {
47
+ method: 'DELETE'
48
+ }));
49
+ expect(res.status).toBe(200);
50
+ expect(await res.json()).toEqual({
51
+ ok: true,
52
+ preview: false
53
+ });
54
+ const cookies = res.headers.getSetCookie();
55
+ expect(cookies).toHaveLength(2);
56
+ // Expiry via Max-Age=0 clears the cookie.
57
+ expect(cookies.every(c => c.includes('Max-Age=0'))).toBe(true);
58
+ });
59
+ it('rejects unsupported methods', async () => {
60
+ const res = await handlePreviewRequest(new Request('https://site.test/api/preview', {
61
+ method: 'GET'
62
+ }));
63
+ expect(res.status).toBe(405);
64
+ });
65
+ it('round-trips a token with cookie-unsafe characters', async () => {
66
+ const token = 'tok+en/with=specials';
67
+ const res = await postPreview(token);
68
+ const setCookie = res.headers.getSetCookie().find(c => c.startsWith(`${PREVIEW_TOKEN_COOKIE}=`));
69
+ // Reconstruct the Cookie header the browser would send back (name=value).
70
+ const cookieHeader = setCookie.split(';')[0];
71
+ expect(readPreviewToken(cookieHeader)).toBe(token);
72
+ });
73
+ });
74
+ describe('readPreviewToken', () => {
75
+ it('extracts the token from a multi-cookie header', () => {
76
+ expect(readPreviewToken(`a=1; ${PREVIEW_TOKEN_COOKIE}=abc; ${PREVIEW_MARKER_COOKIE}=1`)).toBe('abc');
77
+ });
78
+ it('returns undefined when there is no token cookie', () => {
79
+ expect(readPreviewToken('a=1; b=2')).toBeUndefined();
80
+ expect(readPreviewToken('')).toBeUndefined();
81
+ expect(readPreviewToken(null)).toBeUndefined();
82
+ expect(readPreviewToken(undefined)).toBeUndefined();
83
+ });
84
+ });
85
+ describe('previewAuthHeaders', () => {
86
+ it('builds a Bearer header when a token is present', () => {
87
+ expect(previewAuthHeaders('abc')).toEqual({
88
+ authorization: 'Bearer abc'
89
+ });
90
+ });
91
+ it('returns an empty object when there is no token (safe to spread)', () => {
92
+ expect(previewAuthHeaders(undefined)).toEqual({});
93
+ expect(previewAuthHeaders(null)).toEqual({});
94
+ expect(previewAuthHeaders('')).toEqual({});
95
+ });
96
+ });
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { runWithPreview, isPreviewing, ambientPreviewHeaders } from '../preview/server.js';
3
+ import { createFontdueFetch } from '../server/index.js';
4
+ import { PREVIEW_TOKEN_COOKIE } from '../preview/constants.js';
5
+ beforeEach(() => {
6
+ vi.unstubAllGlobals();
7
+ });
8
+ function req(token) {
9
+ return new Request('https://store.test/', {
10
+ headers: token ? {
11
+ cookie: `${PREVIEW_TOKEN_COOKIE}=${token}`
12
+ } : {}
13
+ });
14
+ }
15
+ describe('runWithPreview', () => {
16
+ it('passes public (no-token) requests through untouched and cacheable', async () => {
17
+ const response = await runWithPreview(req(), async () => {
18
+ expect(isPreviewing()).toBe(false);
19
+ return new Response('ok', {
20
+ headers: {
21
+ 'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=300'
22
+ }
23
+ });
24
+ });
25
+ expect(response.headers.get('Cache-Control')).toBeNull();
26
+ expect(response.headers.get('Netlify-CDN-Cache-Control')).toBe('public, durable, s-maxage=300');
27
+ });
28
+ it('exposes the token in ambient context while previewing', async () => {
29
+ await runWithPreview(req('tok123'), async () => {
30
+ expect(isPreviewing()).toBe(true);
31
+ expect(ambientPreviewHeaders()).toEqual({
32
+ authorization: 'Bearer tok123'
33
+ });
34
+ return new Response('ok');
35
+ });
36
+
37
+ // Context unwinds after the request.
38
+ expect(isPreviewing()).toBe(false);
39
+ expect(ambientPreviewHeaders()).toEqual({});
40
+ });
41
+ it('forces preview responses out of any shared/CDN cache', async () => {
42
+ const response = await runWithPreview(req('tok123'), async () => new Response('secret', {
43
+ headers: {
44
+ 'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=31536000',
45
+ 'CDN-Cache-Control': 'public, s-maxage=300',
46
+ 'Cache-Control': 'public, max-age=0, must-revalidate'
47
+ }
48
+ }));
49
+ expect(response.headers.get('Cache-Control')).toBe('private, no-store');
50
+ expect(response.headers.get('Netlify-CDN-Cache-Control')).toBeNull();
51
+ expect(response.headers.get('CDN-Cache-Control')).toBeNull();
52
+ });
53
+ it('isolates concurrent preview contexts (no cross-request leak)', async () => {
54
+ const seen = {};
55
+ await Promise.all([runWithPreview(req('tokA'), async () => {
56
+ await new Promise(r => setTimeout(r, 15));
57
+ seen.a = ambientPreviewHeaders().authorization;
58
+ return new Response('a');
59
+ }), runWithPreview(req('tokB'), async () => {
60
+ seen.b = ambientPreviewHeaders().authorization;
61
+ return new Response('b');
62
+ }), runWithPreview(req(), async () => {
63
+ seen.public = ambientPreviewHeaders().authorization;
64
+ return new Response('public');
65
+ })]);
66
+ expect(seen.a).toBe('Bearer tokA');
67
+ expect(seen.b).toBe('Bearer tokB');
68
+ expect(seen.public).toBeUndefined();
69
+ });
70
+ });
71
+ describe('createFontdueFetch + ambient preview', () => {
72
+ function mockFetch() {
73
+ const fetchMock = vi.fn(async () => ({
74
+ status: 200,
75
+ json: async () => ({
76
+ data: {}
77
+ })
78
+ }));
79
+ vi.stubGlobal('fetch', fetchMock);
80
+ return fetchMock;
81
+ }
82
+ it('a module-level fetcher forwards the ambient token, no per-call plumbing', async () => {
83
+ const fetchMock = mockFetch();
84
+ const fetchGraphql = createFontdueFetch({
85
+ url: 'https://acme.fontdue.com'
86
+ });
87
+ await runWithPreview(req('admin-tok'), async () => {
88
+ await fetchGraphql('Q', 'query Q { __typename }');
89
+ return new Response('ok');
90
+ });
91
+ const init = fetchMock.mock.calls[0][1];
92
+ expect(init.headers.authorization).toBe('Bearer admin-tok');
93
+ });
94
+ it('does not forward a token for public requests', async () => {
95
+ const fetchMock = mockFetch();
96
+ const fetchGraphql = createFontdueFetch({
97
+ url: 'https://acme.fontdue.com'
98
+ });
99
+ await fetchGraphql('Q', 'query Q { __typename }');
100
+ const init = fetchMock.mock.calls[0][1];
101
+ expect(init.headers.authorization).toBeUndefined();
102
+ });
103
+ it('explicit per-fetcher headers override the ambient context', async () => {
104
+ const fetchMock = mockFetch();
105
+ const fetchGraphql = createFontdueFetch({
106
+ url: 'https://acme.fontdue.com',
107
+ headers: {
108
+ authorization: 'Bearer explicit'
109
+ }
110
+ });
111
+ await runWithPreview(req('ambient-tok'), async () => {
112
+ await fetchGraphql('Q', 'query Q { __typename }');
113
+ return new Response('ok');
114
+ });
115
+ const init = fetchMock.mock.calls[0][1];
116
+ expect(init.headers.authorization).toBe('Bearer explicit');
117
+ });
118
+ });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { BuyButtonIDQuery } from '../../__generated__/BuyButtonIDQuery.graphql.js';
3
3
  import { BuyButtonSlugQuery } from '../../__generated__/BuyButtonSlugQuery.graphql.js';
4
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
4
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
5
5
  import { Config } from '../ConfigContext.js';
6
6
  export interface BuyButton_props {
7
7
  collectionName?: string;
@@ -15,7 +15,7 @@ export type LoadBuyButtonQueryVariables = {
15
15
  collectionId?: never;
16
16
  collectionSlug: string;
17
17
  };
18
- export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables): Promise<BuyButtonPreloadedQuery>;
18
+ export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables, options?: LoadQueryOptions): Promise<BuyButtonPreloadedQuery>;
19
19
  export declare function BuyButtonPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: BuyButton_props & {
20
20
  preloadedQuery: SerializablePreloadedQuery<BuyButtonIDQuery>;
21
21
  }): React.JSX.Element;
@@ -57,16 +57,16 @@ function BuyButtonComponent(_ref) {
57
57
  }
58
58
  const idQuery = (_BuyButtonIDQuery.hash && _BuyButtonIDQuery.hash !== "4fbf1dbf9e6c530a5d38c697b174d8b0" && console.error("The definition of 'BuyButtonIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonIDQuery);
59
59
  const slugQuery = (_BuyButtonSlugQuery.hash && _BuyButtonSlugQuery.hash !== "6e750adea09698f7cb61f435cd88fd26" && console.error("The definition of 'BuyButtonSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonSlugQuery);
60
- export async function loadBuyButtonQuery(variables) {
60
+ export async function loadBuyButtonQuery(variables, options) {
61
61
  if (variables.collectionId) {
62
62
  return loadSerializableQuery(BuyButtonIDQueryNode, {
63
63
  collectionId: variables.collectionId
64
- });
64
+ }, options);
65
65
  }
66
66
  if (variables.collectionSlug) {
67
67
  return loadSerializableQuery(BuyButtonSlugQueryNode, {
68
68
  collectionSlug: variables.collectionSlug
69
- });
69
+ }, options);
70
70
  }
71
71
  throw new Error('loadBuyButtonQuery expected either a collectionId or collectionSlug');
72
72
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
2
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
3
3
  import { CharacterViewerIDQuery } from '../../__generated__/CharacterViewerIDQuery.graphql.js';
4
4
  import { CharacterViewerSlugQuery } from '../../__generated__/CharacterViewerSlugQuery.graphql.js';
5
5
  import { Config } from '../ConfigContext.js';
@@ -13,7 +13,7 @@ export type LoadCharacterViewerQueryVariables = {
13
13
  collectionId?: never;
14
14
  collectionSlug: string;
15
15
  };
16
- export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables): Promise<CharacterViewerPreloadedQuery>;
16
+ export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables, options?: LoadQueryOptions): Promise<CharacterViewerPreloadedQuery>;
17
17
  export declare function CharacterViewerPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: CharacterViewer_props & {
18
18
  preloadedQuery: SerializablePreloadedQuery<CharacterViewerIDQuery>;
19
19
  }): React.JSX.Element | null;
@@ -416,16 +416,16 @@ function CharacterViewerComponent(_ref3) {
416
416
  }
417
417
  const idQuery = (_CharacterViewerIDQuery.hash && _CharacterViewerIDQuery.hash !== "f90b09a4df6d95307b0a5d5fda487cdc" && console.error("The definition of 'CharacterViewerIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerIDQuery);
418
418
  const slugQuery = (_CharacterViewerSlugQuery.hash && _CharacterViewerSlugQuery.hash !== "afa08a8f050e0434308892fea6e3c267" && console.error("The definition of 'CharacterViewerSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerSlugQuery);
419
- export async function loadCharacterViewerQuery(variables) {
419
+ export async function loadCharacterViewerQuery(variables, options) {
420
420
  if (variables.collectionId) {
421
421
  return loadSerializableQuery(CharacterViewerIDQueryNode, {
422
422
  collectionId: variables.collectionId
423
- });
423
+ }, options);
424
424
  }
425
425
  if (variables.collectionSlug) {
426
426
  return loadSerializableQuery(CharacterViewerSlugQueryNode, {
427
427
  collectionSlug: variables.collectionSlug
428
- });
428
+ }, options);
429
429
  }
430
430
  throw new Error('loadCharacterViewerQuery expected either collectionId or collectionSlug');
431
431
  }
@@ -23,12 +23,16 @@ interface TrackingConfig {
23
23
  consentMessage?: string;
24
24
  segment?: SegmentConfig;
25
25
  }
26
+ interface PreviewConfig {
27
+ endpoint?: string;
28
+ }
26
29
  export interface Config {
27
30
  form?: FormConfig;
28
31
  storeModal?: StoreModalConfig;
29
32
  typeTester?: TypeTesterConfig;
30
33
  stripe?: StripeConfig;
31
34
  tracking?: TrackingConfig;
35
+ preview?: PreviewConfig;
32
36
  corsErrorModal?: boolean;
33
37
  }
34
38
  export declare const mergeConfig: (base?: Config, override?: Config) => Config;
@@ -102,6 +106,9 @@ export declare const makeConfig: (config?: Config) => {
102
106
  consentMessage: string | undefined;
103
107
  segment: SegmentConfig | undefined;
104
108
  };
109
+ preview: {
110
+ endpoint: string;
111
+ };
105
112
  corsErrorModal: boolean;
106
113
  };
107
114
  declare const _default: React.Context<{
@@ -174,6 +181,9 @@ declare const _default: React.Context<{
174
181
  consentMessage: string | undefined;
175
182
  segment: SegmentConfig | undefined;
176
183
  };
184
+ preview: {
185
+ endpoint: string;
186
+ };
177
187
  corsErrorModal: boolean;
178
188
  }>;
179
189
  export default _default;
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
+ import { PREVIEW_ENDPOINT } from '../preview/constants.js';
4
5
  const makeTypeTesterConfig = config => {
5
6
  var _config$openTypeFeatu, _config$openTypeFeatu2, _config$openTypeFeatu3, _config$openTypeFeatu4, _config$size, _config$size2, _config$size3, _config$lineHeight, _config$lineHeight2, _config$lineHeight3, _config$letterSpacing, _config$letterSpacing2, _config$letterSpacing3, _config$columns, _config$columns2, _config$columns3;
6
7
  let shy = (config === null || config === void 0 ? void 0 : config.shy) ?? false;
@@ -79,7 +80,7 @@ export const mergeConfig = (base, override) => {
79
80
  return deepMerge(base, override);
80
81
  };
81
82
  export const makeConfig = config => {
82
- var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4;
83
+ var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview;
83
84
  return {
84
85
  typeTester: makeTypeTesterConfig(config === null || config === void 0 ? void 0 : config.typeTester),
85
86
  form: {
@@ -101,6 +102,9 @@ export const makeConfig = config => {
101
102
  consentMessage: config === null || config === void 0 ? void 0 : (_config$tracking3 = config.tracking) === null || _config$tracking3 === void 0 ? void 0 : _config$tracking3.consentMessage,
102
103
  segment: config === null || config === void 0 ? void 0 : (_config$tracking4 = config.tracking) === null || _config$tracking4 === void 0 ? void 0 : _config$tracking4.segment
103
104
  },
105
+ preview: {
106
+ endpoint: (config === null || config === void 0 ? void 0 : (_config$preview = config.preview) === null || _config$preview === void 0 ? void 0 : _config$preview.endpoint) ?? PREVIEW_ENDPOINT
107
+ },
104
108
  corsErrorModal: (config === null || config === void 0 ? void 0 : config.corsErrorModal) ?? true
105
109
  };
106
110
  };