fontdue-js 3.0.0-alpha8 → 3.0.0-alpha9
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 +9 -0
- package/README.md +84 -1
- package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +1 -8
- package/dist/__tests__/networkFetch.test.js +66 -0
- package/dist/__tests__/nextAdapter.test.js +141 -1
- package/dist/components/Cart/CartOrder.js +9 -1
- package/dist/components/Cart/orderTracking.js +8 -15
- package/dist/components/FontStyle/index.d.ts +2 -0
- package/dist/components/FontStyle/index.js +4 -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/elements/StoreModalUnifiedCheckout.js +8 -0
- package/dist/components/useFontStyle.d.ts +7 -0
- package/dist/components/useFontStyle.js +2 -1
- package/dist/next/config.d.ts +1 -5
- package/dist/next/config.js +14 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +1 -1
- package/dist/next/tenant.d.ts +9 -1
- package/dist/next/tenant.js +74 -10
- package/dist/relay/environment.js +11 -1
- package/dist/relay/serverConfig.d.ts +6 -0
- package/package.json +1 -1
- package/types/next-navigation.d.ts +6 -0
- package/vitest.config.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 2.28.0
|
|
2
|
+
|
|
3
|
+
- Checkout now captures the buyer's **analytics consent and ad attribution** on the order. When the cart or store-modal checkout opens, fontdue-js sends the consent-banner state, anonymous ID, Meta browser IDs (the `_fbp`/`_fbc` cookies), and the page URL to the server, where they're stored on the order. This lets Fontdue emit a server-side *purchase* conversion event (Facebook Conversions API, Google Ads) when the order completes — completion happens in a Stripe webhook, outside the browser — while respecting the buyer's cookie-consent choice: if consent was declined, no identifiers are sent and nothing is forwarded to ad platforms. The call is fire-and-forget and never affects checkout.
|
|
4
|
+
|
|
5
|
+
## 2.27.0
|
|
6
|
+
|
|
7
|
+
- The `TypeTester` `truncate` option now accepts a **number** to cap the sample paragraph at N lines (in addition to `false` = off and `true` = 1 line). Truncation is now ascender/descender-safe at any line-height — it reads the font's vertical metrics so accents and descenders are never clipped — and flows the clipped text into columns, so multi-column truncated specimens render correctly and side-by-side testers keep their toolbars aligned.
|
|
8
|
+
- The `TypeTester` **family selector now lazy-loads its data**. The family list is fetched on first dropdown open (shared once per page) and per-family styles load on demand, instead of fetching the whole font library up front. This substantially speeds up the initial render of pages with type testers, especially with several standalone testers. The dropdown shows a “Loading…” state while data loads.
|
|
9
|
+
|
|
1
10
|
## 2.26.1
|
|
2
11
|
|
|
3
12
|
- Fixed a jitter in the `TypeTester` variable-font axis sliders: dragging a slider no longer shifts the handle position as the instance name and value beside it change width. The instance name now settles shortly after dragging stops, and the value reserves a fixed width for its range.
|
package/README.md
CHANGED
|
@@ -53,7 +53,18 @@ The shape of step 3 and 4 is the only thing that changes between frameworks.
|
|
|
53
53
|
<details>
|
|
54
54
|
<summary><b>Next.js (App Router)</b></summary>
|
|
55
55
|
|
|
56
|
-
No Vite plugin needed
|
|
56
|
+
No Vite plugin needed — wrap your Next config with [`withFontdue`](#nextjs-adapter) instead:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// next.config.mjs
|
|
60
|
+
import { withFontdue } from "fontdue-js/next/config";
|
|
61
|
+
|
|
62
|
+
export default withFontdue({
|
|
63
|
+
// your Next config
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The simplest setup omits the layout preload — with React Server Components, each fontdue-js component preloads its own query internally on the server and streams to the client.
|
|
57
68
|
|
|
58
69
|
```tsx
|
|
59
70
|
// app/layout.tsx
|
|
@@ -84,6 +95,8 @@ export default function FontPage() {
|
|
|
84
95
|
}
|
|
85
96
|
```
|
|
86
97
|
|
|
98
|
+
Beyond the components, Next.js projects get a few extra entry points — config wrapping, cache revalidation, and helpers for your own GraphQL fetches. See [Next.js adapter](#nextjs-adapter).
|
|
99
|
+
|
|
87
100
|
Example repo: [`fontdue/fontdue-example-next`](https://github.com/fontdue/fontdue-example-next)
|
|
88
101
|
|
|
89
102
|
</details>
|
|
@@ -356,6 +369,76 @@ export default function App() {
|
|
|
356
369
|
|
|
357
370
|
</details>
|
|
358
371
|
|
|
372
|
+
## Next.js adapter
|
|
373
|
+
|
|
374
|
+
Next.js App Router projects get a few extra entry points beyond the components. The [example repo](https://github.com/fontdue/fontdue-example-next) wires up all of them.
|
|
375
|
+
|
|
376
|
+
### `withFontdue` — next.config wrapper
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
// next.config.mjs
|
|
380
|
+
import { withFontdue } from "fontdue-js/next/config";
|
|
381
|
+
|
|
382
|
+
export default withFontdue({
|
|
383
|
+
// your Next config
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
What it installs:
|
|
388
|
+
|
|
389
|
+
- **Image settings** — `images.remotePatterns` entries for Fontdue's image hosts (plus `dangerouslyAllowSVG`, since font specimens are often SVGs), merged with your own `images` config.
|
|
390
|
+
- **Correct 404 statuses** — Next's streamed metadata locks in a `200` response before a `notFound()` thrown during `generateMetadata` can take effect ([vercel/next.js#82041](https://github.com/vercel/next.js/issues/82041)). `withFontdue` sets `htmlLimitedBots` to match every user agent so metadata rendering blocks the response and missing pages come out as real 404s.
|
|
391
|
+
|
|
392
|
+
The rest of your config — `rewrites` included — passes through unchanged.
|
|
393
|
+
|
|
394
|
+
#### Optional: image optimization on Cloudflare
|
|
395
|
+
|
|
396
|
+
If you have a Cloudflare zone with [image transformations](https://developers.cloudflare.com/images/transform-images/) enabled, set:
|
|
397
|
+
|
|
398
|
+
```shell
|
|
399
|
+
NEXT_PUBLIC_FONTDUE_IMAGE_HOST=img.your-domain.com
|
|
400
|
+
NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS=cdn.fontdue.com
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`next/image` optimization then moves to the Cloudflare edge, and your deployment needs neither the `/_next/image` endpoint nor sharp. `NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS` (comma-separated hostnames) should mirror the transformation host's allowed source origins — sources on other hosts are served as originals rather than as transform URLs Cloudflare would refuse. Both variables must be present when `next build` runs (the loader is inlined into the client bundle), not just at serve time.
|
|
404
|
+
|
|
405
|
+
### Updating content: `/api/revalidate`
|
|
406
|
+
|
|
407
|
+
fontdue-js's server-side fetches are cached by Next and tagged `graphql`. Re-export the deploy-hook route handler:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
// app/api/revalidate/route.ts
|
|
411
|
+
export { POST } from "fontdue-js/next/revalidate";
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
and set the Deploy hook URL in your Fontdue admin (Settings → Website settings) to `https://your-site.example/api/revalidate`. Fontdue calls it whenever your site's content changes, purging everything tagged `graphql` so the next request renders fresh.
|
|
415
|
+
|
|
416
|
+
fontdue-js's own server-side fetches opt into Next's data cache (and the `graphql` tag) automatically — static pages revalidated by the deploy hook is the intended way to run a Fontdue site, not dynamic rendering. Give your own fetches the same treatment; `currentFontdueEndpoint()` below shows how.
|
|
417
|
+
|
|
418
|
+
### Your own GraphQL fetches
|
|
419
|
+
|
|
420
|
+
`currentFontdueEndpoint()` (from `fontdue-js/next`) describes the endpoint your own server-side [GraphQL](https://docs.fontdue.com/graphql-api) fetches should use — the base origin, required headers, and the cache tags that tie them into `/api/revalidate`:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { currentFontdueEndpoint } from "fontdue-js/next";
|
|
424
|
+
|
|
425
|
+
export async function fetchGraphql(query: string, variables?: unknown) {
|
|
426
|
+
const endpoint = currentFontdueEndpoint();
|
|
427
|
+
const response = await fetch(`${endpoint.origin}/graphql`, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
body: JSON.stringify({ query, variables }),
|
|
430
|
+
headers: { "content-type": "application/json", ...endpoint.headers },
|
|
431
|
+
// Cache explicitly (Next 15 doesn't cache fetch by default), tagged so
|
|
432
|
+
// the deploy hook purges this too.
|
|
433
|
+
cache: "force-cache",
|
|
434
|
+
next: { tags: endpoint.tags },
|
|
435
|
+
});
|
|
436
|
+
return (await response.json()).data;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The shape is exported as `type FontdueEndpoint`.
|
|
441
|
+
|
|
359
442
|
## UI config
|
|
360
443
|
|
|
361
444
|
Most components accept a `config` object that controls UI behavior — type-tester options (`selectable`, `priceBar`, size ranges, OpenType-feature UI…), store-modal layout, form styling, analytics tracking, and more. See the [full config reference](https://docs.fontdue.com/fontduejs#b3dec49aa08240bba2b4c71a67c08333).
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = void 0;
|
|
7
1
|
/**
|
|
8
2
|
* @generated SignedSource<<017a8a724b3a0fd0918153ce04d07f68>>
|
|
9
3
|
* @lightSyntaxTransform
|
|
@@ -68,5 +62,4 @@ const node = function () {
|
|
|
68
62
|
};
|
|
69
63
|
}();
|
|
70
64
|
node.hash = "d59a127a7f140424f507ae549731bac7";
|
|
71
|
-
|
|
72
|
-
exports.default = _default;
|
|
65
|
+
export default node;
|
|
@@ -0,0 +1,66 @@
|
|
|
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', async () => {
|
|
14
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
15
|
+
const fetchMock = vi.fn(async () => ({
|
|
16
|
+
json: async () => ({
|
|
17
|
+
data: {}
|
|
18
|
+
})
|
|
19
|
+
}));
|
|
20
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
21
|
+
const {
|
|
22
|
+
createNetworkFetch
|
|
23
|
+
} = await import("../relay/environment.js");
|
|
24
|
+
await createNetworkFetch()(request, {});
|
|
25
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
26
|
+
const [url, options] = fetchMock.mock.calls[0];
|
|
27
|
+
expect(url).toBe('https://acme.fontdue.com/graphql?queryName=TestQuery');
|
|
28
|
+
// Without an explicit cache option Next 15 treats the fetch as no-store
|
|
29
|
+
// and silently makes every page fully dynamic — the static + revalidate
|
|
30
|
+
// pattern depends on this.
|
|
31
|
+
expect(options.cache).toBe('force-cache');
|
|
32
|
+
expect(options.next.tags).toContain('graphql');
|
|
33
|
+
expect(options.next.tags).toContain('operation:TestQuery');
|
|
34
|
+
});
|
|
35
|
+
it('applies the per-render server config tags and headers', async () => {
|
|
36
|
+
vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
|
|
37
|
+
const fetchMock = vi.fn(async () => ({
|
|
38
|
+
json: async () => ({
|
|
39
|
+
data: {}
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
43
|
+
|
|
44
|
+
// React.cache doesn't memoize outside a React render, so the store is
|
|
45
|
+
// mocked rather than set through setFontdueServerConfig.
|
|
46
|
+
vi.doMock('../relay/serverConfig', async importActual => ({
|
|
47
|
+
...(await importActual()),
|
|
48
|
+
getFontdueServerConfig: () => ({
|
|
49
|
+
url: 'http://app:4000',
|
|
50
|
+
headers: {
|
|
51
|
+
'x-forwarded-host': 'acme.fontdue.com'
|
|
52
|
+
},
|
|
53
|
+
cacheTags: ['graphql:acme.fontdue.com']
|
|
54
|
+
})
|
|
55
|
+
}));
|
|
56
|
+
const {
|
|
57
|
+
createNetworkFetch
|
|
58
|
+
} = await import("../relay/environment.js");
|
|
59
|
+
await createNetworkFetch()(request, {});
|
|
60
|
+
vi.doUnmock('../relay/serverConfig');
|
|
61
|
+
const [url, options] = fetchMock.mock.calls[0];
|
|
62
|
+
expect(url).toBe('http://app:4000/graphql?queryName=TestQuery');
|
|
63
|
+
expect(options.headers['x-forwarded-host']).toBe('acme.fontdue.com');
|
|
64
|
+
expect(options.next.tags).toContain('graphql:acme.fontdue.com');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,4 +1,7 @@
|
|
|
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.
|
|
@@ -15,10 +18,19 @@ const revalidateTag = vi.fn();
|
|
|
15
18
|
vi.mock('next/cache', () => ({
|
|
16
19
|
revalidateTag: tag => revalidateTag(tag)
|
|
17
20
|
}));
|
|
21
|
+
|
|
22
|
+
// Next's notFound() throws a sentinel the framework catches; the mock does
|
|
23
|
+
// the same so tests can assert on it.
|
|
24
|
+
vi.mock('next/navigation', () => ({
|
|
25
|
+
notFound: () => {
|
|
26
|
+
throw new Error('NEXT_NOT_FOUND');
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
18
29
|
beforeEach(() => {
|
|
19
30
|
vi.resetModules();
|
|
20
31
|
revalidateTag.mockClear();
|
|
21
32
|
vi.unstubAllEnvs();
|
|
33
|
+
vi.restoreAllMocks();
|
|
22
34
|
});
|
|
23
35
|
function stubSingleTenant() {
|
|
24
36
|
let url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'https://acme.fontdue.com';
|
|
@@ -59,6 +71,7 @@ describe('fontdueEndpoint', () => {
|
|
|
59
71
|
fontdueEndpoint
|
|
60
72
|
} = await importTenant();
|
|
61
73
|
expect(fontdueEndpoint('acme.fontdue.com')).toEqual({
|
|
74
|
+
domain: 'acme.fontdue.com',
|
|
62
75
|
origin: 'https://acme.fontdue.com',
|
|
63
76
|
headers: {},
|
|
64
77
|
tags: ['graphql', 'graphql:acme.fontdue.com']
|
|
@@ -81,6 +94,7 @@ describe('fontdueEndpoint', () => {
|
|
|
81
94
|
fontdueEndpoint
|
|
82
95
|
} = await importTenant();
|
|
83
96
|
expect(fontdueEndpoint('acme.fontdue.com')).toEqual({
|
|
97
|
+
domain: 'acme.fontdue.com',
|
|
84
98
|
origin: 'http://app:4000',
|
|
85
99
|
headers: {
|
|
86
100
|
'x-forwarded-host': 'acme.fontdue.com',
|
|
@@ -147,11 +161,107 @@ describe('configureFontdueRender', () => {
|
|
|
147
161
|
'x-forwarded-host': 'acme.fontdue.com',
|
|
148
162
|
'x-fontdue-proxy-secret': 's3cret'
|
|
149
163
|
},
|
|
150
|
-
cacheTags: ['graphql:acme.fontdue.com']
|
|
164
|
+
cacheTags: ['graphql:acme.fontdue.com'],
|
|
165
|
+
domain: 'acme.fontdue.com'
|
|
151
166
|
});
|
|
152
167
|
});
|
|
153
168
|
});
|
|
169
|
+
describe('prepareFontdueRender', () => {
|
|
170
|
+
const props = params => ({
|
|
171
|
+
params: Promise.resolve(params)
|
|
172
|
+
});
|
|
173
|
+
it('configures the render and returns the endpoint', async () => {
|
|
174
|
+
stubMultiTenant({
|
|
175
|
+
origin: 'http://app:4000'
|
|
176
|
+
});
|
|
177
|
+
const {
|
|
178
|
+
prepareFontdueRender
|
|
179
|
+
} = await importTenant();
|
|
180
|
+
const endpoint = await prepareFontdueRender(props({
|
|
181
|
+
domain: 'acme.fontdue.com',
|
|
182
|
+
slug: 'sans'
|
|
183
|
+
}));
|
|
184
|
+
expect(endpoint.origin).toBe('http://app:4000');
|
|
185
|
+
expect(endpoint.tags).toContain('graphql:acme.fontdue.com');
|
|
186
|
+
});
|
|
187
|
+
it('404s invalid or missing domains', async () => {
|
|
188
|
+
stubMultiTenant();
|
|
189
|
+
const {
|
|
190
|
+
prepareFontdueRender
|
|
191
|
+
} = await importTenant();
|
|
192
|
+
await expect(prepareFontdueRender(props({
|
|
193
|
+
domain: 'not a domain'
|
|
194
|
+
}))).rejects.toThrow('NEXT_NOT_FOUND');
|
|
195
|
+
await expect(prepareFontdueRender(props({
|
|
196
|
+
slug: 'sans'
|
|
197
|
+
}))).rejects.toThrow('NEXT_NOT_FOUND');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('currentFontdueEndpoint', () => {
|
|
201
|
+
it('multi-tenant: throws when no render config was set', async () => {
|
|
202
|
+
stubMultiTenant({
|
|
203
|
+
origin: 'http://app:4000'
|
|
204
|
+
});
|
|
205
|
+
const {
|
|
206
|
+
currentFontdueEndpoint
|
|
207
|
+
} = await importTenant();
|
|
208
|
+
expect(() => currentFontdueEndpoint()).toThrow(/prepareFontdueRender/);
|
|
209
|
+
});
|
|
210
|
+
it('single-tenant: derives the endpoint from NEXT_PUBLIC_FONTDUE_URL', async () => {
|
|
211
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
212
|
+
const {
|
|
213
|
+
currentFontdueEndpoint
|
|
214
|
+
} = await importTenant();
|
|
215
|
+
expect(currentFontdueEndpoint()).toEqual({
|
|
216
|
+
domain: 'acme.fontdue.com',
|
|
217
|
+
origin: 'https://acme.fontdue.com',
|
|
218
|
+
headers: {},
|
|
219
|
+
tags: ['graphql', 'graphql:acme.fontdue.com']
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
it('uses the render-configured domain when set', async () => {
|
|
223
|
+
stubMultiTenant({
|
|
224
|
+
origin: 'http://app:4000'
|
|
225
|
+
});
|
|
226
|
+
vi.doMock('../relay/serverConfig', async importActual => ({
|
|
227
|
+
...(await importActual()),
|
|
228
|
+
getFontdueServerConfig: () => ({
|
|
229
|
+
domain: 'acme.fontdue.com'
|
|
230
|
+
})
|
|
231
|
+
}));
|
|
232
|
+
const {
|
|
233
|
+
currentFontdueEndpoint
|
|
234
|
+
} = await importTenant();
|
|
235
|
+
expect(currentFontdueEndpoint().headers['x-forwarded-host']).toBe('acme.fontdue.com');
|
|
236
|
+
vi.doUnmock('../relay/serverConfig');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('generateStaticParams', () => {
|
|
240
|
+
it('returns no build-time params (everything renders on demand)', async () => {
|
|
241
|
+
stubMultiTenant();
|
|
242
|
+
const {
|
|
243
|
+
generateStaticParams
|
|
244
|
+
} = await importTenant();
|
|
245
|
+
await expect(generateStaticParams()).resolves.toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// withFontdue detects the route-tree shape from the working directory; give
|
|
250
|
+
// it one with or without src/app/[domain].
|
|
251
|
+
function mockAppDir(_ref) {
|
|
252
|
+
let {
|
|
253
|
+
domainTree
|
|
254
|
+
} = _ref;
|
|
255
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fontdue-app-'));
|
|
256
|
+
fs.mkdirSync(path.join(dir, domainTree ? 'src/app/[domain]' : 'src/app'), {
|
|
257
|
+
recursive: true
|
|
258
|
+
});
|
|
259
|
+
vi.spyOn(process, 'cwd').mockReturnValue(dir);
|
|
260
|
+
}
|
|
154
261
|
describe('withFontdue', () => {
|
|
262
|
+
beforeEach(() => mockAppDir({
|
|
263
|
+
domainTree: true
|
|
264
|
+
}));
|
|
155
265
|
it('throws when neither mode is configured', async () => {
|
|
156
266
|
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
157
267
|
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
@@ -267,6 +377,36 @@ describe('withFontdue', () => {
|
|
|
267
377
|
htmlLimitedBots: /Googlebot/
|
|
268
378
|
}).htmlLimitedBots).toEqual(/Googlebot/);
|
|
269
379
|
});
|
|
380
|
+
it('single-tenant flat tree: installs no tenant rewrites', async () => {
|
|
381
|
+
mockAppDir({
|
|
382
|
+
domainTree: false
|
|
383
|
+
});
|
|
384
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
385
|
+
const {
|
|
386
|
+
withFontdue
|
|
387
|
+
} = await importConfig();
|
|
388
|
+
const userRule = {
|
|
389
|
+
source: '/old',
|
|
390
|
+
destination: '/new'
|
|
391
|
+
};
|
|
392
|
+
const rewrites = await withFontdue({
|
|
393
|
+
rewrites: async () => ({
|
|
394
|
+
beforeFiles: [userRule]
|
|
395
|
+
})
|
|
396
|
+
}).rewrites();
|
|
397
|
+
expect(rewrites.beforeFiles).toEqual([userRule]);
|
|
398
|
+
expect(rewrites.afterFiles).toEqual([]);
|
|
399
|
+
});
|
|
400
|
+
it('multi-tenant flat tree: refuses to start', async () => {
|
|
401
|
+
mockAppDir({
|
|
402
|
+
domainTree: false
|
|
403
|
+
});
|
|
404
|
+
stubMultiTenant();
|
|
405
|
+
const {
|
|
406
|
+
withFontdue
|
|
407
|
+
} = await importConfig();
|
|
408
|
+
expect(() => withFontdue({})).toThrow(/\[domain\]/);
|
|
409
|
+
});
|
|
270
410
|
});
|
|
271
411
|
describe('revalidate POST', () => {
|
|
272
412
|
it('multi-tenant: purges only the tenant tag', async () => {
|
|
@@ -4,7 +4,7 @@ import _CartOrderRemoveDiscountMutation from "../../__generated__/CartOrderRemov
|
|
|
4
4
|
import _CartOrderUpdateMutation from "../../__generated__/CartOrderUpdateMutation.graphql.js";
|
|
5
5
|
import _CartOrder_UpdateErrors from "../../__generated__/CartOrder_UpdateErrors.graphql.js";
|
|
6
6
|
import _CartOrderCompleteOrderMutation from "../../__generated__/CartOrderCompleteOrderMutation.graphql.js";
|
|
7
|
-
import React, { Fragment, useRef, useState } from 'react';
|
|
7
|
+
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
9
9
|
import Checkout from './Checkout.js';
|
|
10
10
|
import CheckoutSteps from './CheckoutSteps.js';
|
|
@@ -21,6 +21,7 @@ import { textVariablesAllHaveText } from './utils.js';
|
|
|
21
21
|
import { useStripe } from '@stripe/react-stripe-js';
|
|
22
22
|
import { useDispatch } from 'react-redux';
|
|
23
23
|
import CartState from './CartState.js';
|
|
24
|
+
import { sendOrderTracking } from './orderTracking.js';
|
|
24
25
|
|
|
25
26
|
// note here we're only updating the order (by id).
|
|
26
27
|
// careful not to update the current customer's reference
|
|
@@ -44,6 +45,13 @@ const CartOrder = _ref => {
|
|
|
44
45
|
const environment = useRelayEnvironment();
|
|
45
46
|
const stripe = useStripe();
|
|
46
47
|
const dispatch = useDispatch();
|
|
48
|
+
|
|
49
|
+
// Capture analytics consent + attribution on the order for the purchase
|
|
50
|
+
// event the server emits on completion (from a Stripe webhook, where no
|
|
51
|
+
// browser context exists).
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (open) sendOrderTracking(environment);
|
|
54
|
+
}, [open, environment]);
|
|
47
55
|
const order = useFragment((_CartOrder_order.hash && _CartOrder_order.hash !== "25ef000c40000c4f76f973ac6ac7f593" && console.error("The definition of 'CartOrder_order' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CartOrder_order), orderKey);
|
|
48
56
|
const viewer = useFragment((_CartOrder_viewer.hash && _CartOrder_viewer.hash !== "8cf5ab5a33a474483a6c231cc44b0df1" && console.error("The definition of 'CartOrder_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CartOrder_viewer), viewerKey);
|
|
49
57
|
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.sendOrderTracking = sendOrderTracking;
|
|
7
|
-
var _orderTrackingUpdateOrderTrackingMutation2 = _interopRequireDefault(require("../../__generated__/orderTrackingUpdateOrderTrackingMutation.graphql"));
|
|
8
|
-
var _reactRelay = require("react-relay");
|
|
9
|
-
var _consent = require("../ConsentBanner/consent");
|
|
10
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
1
|
+
import _orderTrackingUpdateOrderTrackingMutation from "../../__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js";
|
|
2
|
+
import { commitMutation, graphql } from 'react-relay';
|
|
3
|
+
import { hasConsent, getClientAnonymousId } from '../ConsentBanner/consent.js';
|
|
11
4
|
function readCookie(name) {
|
|
12
5
|
const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
|
|
13
6
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
@@ -21,14 +14,14 @@ function readCookie(name) {
|
|
|
21
14
|
*
|
|
22
15
|
* Fire-and-forget: tracking must never break checkout.
|
|
23
16
|
*/
|
|
24
|
-
function sendOrderTracking(environment) {
|
|
17
|
+
export function sendOrderTracking(environment) {
|
|
25
18
|
try {
|
|
26
|
-
|
|
27
|
-
mutation: (
|
|
19
|
+
commitMutation(environment, {
|
|
20
|
+
mutation: (_orderTrackingUpdateOrderTrackingMutation.hash && _orderTrackingUpdateOrderTrackingMutation.hash !== "d59a127a7f140424f507ae549731bac7" && console.error("The definition of 'orderTrackingUpdateOrderTrackingMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _orderTrackingUpdateOrderTrackingMutation),
|
|
28
21
|
variables: {
|
|
29
22
|
input: {
|
|
30
|
-
analyticsConsent:
|
|
31
|
-
anonymousId:
|
|
23
|
+
analyticsConsent: hasConsent('analytics'),
|
|
24
|
+
anonymousId: getClientAnonymousId(),
|
|
32
25
|
fbp: readCookie('_fbp'),
|
|
33
26
|
fbc: readCookie('_fbc'),
|
|
34
27
|
url: window.location.href
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { type JSX } from 'react';
|
|
2
|
+
import { VerticalMetrics } from '../useFontStyle.js';
|
|
2
3
|
import { FontStyle_fontStyle$key } from '../../__generated__/FontStyle_fontStyle.graphql.js';
|
|
3
4
|
interface FontStyleProps {
|
|
4
5
|
fontStyle: FontStyle_fontStyle$key;
|
|
@@ -7,6 +8,7 @@ interface FontStyleProps {
|
|
|
7
8
|
children: ((props: {
|
|
8
9
|
loaded: boolean;
|
|
9
10
|
style: React.CSSProperties;
|
|
11
|
+
verticalMetrics: VerticalMetrics | null;
|
|
10
12
|
}) => React.ReactNode) | React.ReactNode;
|
|
11
13
|
}
|
|
12
14
|
export default function FontStyle({ Component, fontStyle: fontStyleKey, style, children, ...rest }: Omit<React.HTMLAttributes<HTMLDivElement | HTMLSpanElement>, 'children'> & FontStyleProps): JSX.Element;
|
|
@@ -13,7 +13,8 @@ export default function FontStyle(_ref) {
|
|
|
13
13
|
const fontStyle = useFragment((_FontStyle_fontStyle.hash && _FontStyle_fontStyle.hash !== "7891603bea1a3e40f40297bf1697eb3c" && console.error("The definition of 'FontStyle_fontStyle' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontStyle_fontStyle), fontStyleKey);
|
|
14
14
|
const {
|
|
15
15
|
style: fontStyleStyle,
|
|
16
|
-
loaded
|
|
16
|
+
loaded,
|
|
17
|
+
verticalMetrics
|
|
17
18
|
} = useFontStyle({
|
|
18
19
|
fontStyle
|
|
19
20
|
});
|
|
@@ -26,6 +27,7 @@ export default function FontStyle(_ref) {
|
|
|
26
27
|
};
|
|
27
28
|
return /*#__PURE__*/React.createElement(Component, renderProps, typeof children === 'function' ? children({
|
|
28
29
|
loaded,
|
|
29
|
-
style: fontStyleStyle
|
|
30
|
+
style: fontStyleStyle,
|
|
31
|
+
verticalMetrics
|
|
30
32
|
}) : children);
|
|
31
33
|
}
|
|
@@ -142,12 +142,14 @@ const TypeTester = _ref => {
|
|
|
142
142
|
}, _ref3 => {
|
|
143
143
|
let {
|
|
144
144
|
loaded,
|
|
145
|
-
style
|
|
145
|
+
style,
|
|
146
|
+
verticalMetrics
|
|
146
147
|
} = _ref3;
|
|
147
148
|
if (loaded) {
|
|
148
149
|
return /*#__PURE__*/React.createElement(TypeTesterContent, _extends({}, props, {
|
|
149
150
|
ref: contentRef,
|
|
150
151
|
fontStyles: style,
|
|
152
|
+
verticalMetrics: verticalMetrics,
|
|
151
153
|
truncate: config.truncate,
|
|
152
154
|
direction: props.direction,
|
|
153
155
|
min: config.size.min,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EditorState } from 'draft-js';
|
|
2
2
|
import { Alignment } from './types.js';
|
|
3
3
|
import { VariableSettings } from '../../utils.js';
|
|
4
|
+
import { VerticalMetrics } from '../useFontStyle.js';
|
|
4
5
|
export type ColumnsConfig = false | {
|
|
5
6
|
count: 'auto' | number;
|
|
6
7
|
width: string;
|
|
@@ -21,13 +22,14 @@ export interface UseTypeTesterStylerProps {
|
|
|
21
22
|
focused: boolean;
|
|
22
23
|
truncate: boolean | number;
|
|
23
24
|
fontStyles: React.CSSProperties;
|
|
25
|
+
verticalMetrics?: VerticalMetrics | null;
|
|
24
26
|
content: EditorState;
|
|
25
27
|
contentEdited: boolean;
|
|
26
28
|
alignment: Alignment;
|
|
27
29
|
variableSettings: VariableSettings | null;
|
|
28
30
|
columns: ColumnsConfig;
|
|
29
31
|
}
|
|
30
|
-
declare const useTypeTesterStyler: ({ size, autofit, autofitOnChange, features, setSize, min, max, truncate, focused, lineHeight, letterSpacing, fontStyles, content, contentEdited, alignment, variableSettings, columns, }: UseTypeTesterStylerProps) => {
|
|
32
|
+
declare const useTypeTesterStyler: ({ size, autofit, autofitOnChange, features, setSize, min, max, truncate, focused, lineHeight, letterSpacing, fontStyles, verticalMetrics, content, contentEdited, alignment, variableSettings, columns, }: UseTypeTesterStylerProps) => {
|
|
31
33
|
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
32
34
|
style: import("react").CSSProperties;
|
|
33
35
|
};
|
|
@@ -15,6 +15,7 @@ const useTypeTesterStyler = _ref => {
|
|
|
15
15
|
lineHeight,
|
|
16
16
|
letterSpacing,
|
|
17
17
|
fontStyles,
|
|
18
|
+
verticalMetrics,
|
|
18
19
|
content,
|
|
19
20
|
contentEdited,
|
|
20
21
|
alignment,
|
|
@@ -58,33 +59,82 @@ const useTypeTesterStyler = _ref => {
|
|
|
58
59
|
const truncateLines = truncate === true ? 1 : typeof truncate === 'number' ? Math.max(0, Math.floor(truncate)) : 0;
|
|
59
60
|
const shouldTruncate = truncateLines > 0 && !focused;
|
|
60
61
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
62
|
+
// How far a line's ink spills past its line-box, top and bottom, as a fraction
|
|
63
|
+
// of the font-size (em). `fontLoader` feeds these exact metrics into the
|
|
64
|
+
// @font-face as ascent/descent-override, so this matches the browser's line
|
|
65
|
+
// geometry precisely. It is 0 once the line-height is at least the font's
|
|
66
|
+
// natural content height ((ascender − descender) / unitsPerEm) — i.e. for any
|
|
67
|
+
// normal reading line-height — and only grows for tight, overlapping setting.
|
|
68
|
+
// Kept in em (not px) so it scales with the *rendered* size, including the
|
|
69
|
+
// `--type-tester--adjustment` that shrinks the preview on mobile.
|
|
70
|
+
const lineOverflowEm = shouldTruncate && verticalMetrics && verticalMetrics.unitsPerEm > 0 ? Math.max(0, ((verticalMetrics.ascender - verticalMetrics.descender) / verticalMetrics.unitsPerEm - lineHeight) / 2) : 0;
|
|
71
|
+
|
|
72
|
+
// Columns are what make truncation overflow flow sideways — into clipped,
|
|
73
|
+
// off-screen columns — instead of stacking below the last visible line. That
|
|
74
|
+
// is what lets the metric padding reveal descenders/ascenders cleanly, and it
|
|
75
|
+
// means a multi-column tester (e.g. a 3-column specimen) truncates to
|
|
76
|
+
// `truncateLines` lines *per column*. When truncating with columns disabled we
|
|
77
|
+
// still establish a single column so the clipped overflow behaves the same way.
|
|
78
|
+
const columnStyle = columns ? {
|
|
79
|
+
columnCount: columns.count,
|
|
80
|
+
columnWidth: columns.width,
|
|
81
|
+
columnGap: columns.gap
|
|
82
|
+
} : undefined;
|
|
83
|
+
|
|
84
|
+
// A glyph's side bearings — ink that sits outside its advance width, like a
|
|
85
|
+
// leading lowercase 'j' or an italic overhang — would be shorn by the inline
|
|
86
|
+
// clip. Per-glyph bearings aren't in the font metrics, so reserve a flat 0.1em
|
|
87
|
+
// on each inline edge and cancel it with a negative margin so
|
|
88
|
+
// wrapping and text position stay put.
|
|
89
|
+
const sideBearing = shouldTruncate ? '0.1em' : '0px';
|
|
65
90
|
const style = {
|
|
66
91
|
fontSize: autofit ? `${size}px` : `calc(var(--type-tester--adjustment, 1) * ${size}px)`,
|
|
67
92
|
lineHeight: `${lineHeight}`,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
...(shouldTruncate ? {
|
|
94
|
+
// Cap each column at `truncateLines` lines. `max-height` (not `height`)
|
|
95
|
+
// lets shorter paragraphs collapse to their content instead of padding
|
|
96
|
+
// out to N lines of whitespace. Columns send the clipped overflow
|
|
97
|
+
// sideways (into off-screen columns) rather than below, so the last
|
|
98
|
+
// visible line of every column is never partially covered by the next
|
|
99
|
+
// one. That lets the symmetric `lineOverflowEm` padding fully reveal the
|
|
100
|
+
// first line's ascenders/accents and the last line's descenders at any
|
|
101
|
+
// line-height; the negative margins cancel that padding in layout so a
|
|
102
|
+
// collapsed paragraph still occupies exactly its line count.
|
|
103
|
+
//
|
|
104
|
+
// Everything is in `em` so it tracks the rendered font-size (including
|
|
105
|
+
// the mobile `--type-tester--adjustment`); the `+ 2px` is slack that
|
|
106
|
+
// keeps sub-pixel rounding from dropping the Nth line (the extra never
|
|
107
|
+
// reveals line N+1 because that line is clipped out to the side).
|
|
108
|
+
boxSizing: 'border-box',
|
|
109
|
+
maxHeight: `calc(${truncateLines * lineHeight + 2 * lineOverflowEm}em + 2px)`,
|
|
110
|
+
paddingTop: `${lineOverflowEm}em`,
|
|
111
|
+
paddingBottom: `${lineOverflowEm}em`,
|
|
112
|
+
paddingLeft: sideBearing,
|
|
113
|
+
paddingRight: sideBearing,
|
|
114
|
+
marginTop: `${-lineOverflowEm}em`,
|
|
115
|
+
marginBottom: `${-lineOverflowEm}em`,
|
|
116
|
+
overflow: 'hidden',
|
|
117
|
+
// Fill each column to its full N lines before spilling to the next, and
|
|
118
|
+
// allow a final column with a single line — otherwise the default
|
|
119
|
+
// `widows: 2` / `column-fill: balance` rebalance a 4-line paragraph into
|
|
120
|
+
// two columns of 2, showing N−1 lines instead of N.
|
|
121
|
+
columnFill: 'auto',
|
|
122
|
+
widows: 1,
|
|
123
|
+
orphans: 1,
|
|
124
|
+
...(columnStyle ?? {
|
|
125
|
+
columnCount: 1
|
|
126
|
+
})
|
|
127
|
+
} : {
|
|
128
|
+
height: 'auto',
|
|
129
|
+
overflow: 'visible',
|
|
130
|
+
...columnStyle
|
|
76
131
|
}),
|
|
77
132
|
fontFeatureSettings,
|
|
78
|
-
marginRight: alignment !== 'right' ? -sideBuffer : 0
|
|
79
|
-
marginLeft: alignment === 'right' ? -sideBuffer : 0
|
|
133
|
+
marginRight: `calc(${alignment !== 'right' ? -sideBuffer : 0}px - ${sideBearing})`,
|
|
134
|
+
marginLeft: `calc(${alignment === 'right' ? -sideBuffer : 0}px - ${sideBearing})`,
|
|
80
135
|
letterSpacing: `${letterSpacing}em`,
|
|
81
136
|
fontVariationSettings,
|
|
82
|
-
textAlign: alignment
|
|
83
|
-
...(columns && {
|
|
84
|
-
columnCount: columns.count,
|
|
85
|
-
columnWidth: columns.width,
|
|
86
|
-
columnGap: columns.gap
|
|
87
|
-
})
|
|
137
|
+
textAlign: alignment
|
|
88
138
|
};
|
|
89
139
|
return {
|
|
90
140
|
ref,
|
|
@@ -75,6 +75,7 @@ import StripeLogo from '../StripeLogo.js';
|
|
|
75
75
|
import { Check, X } from '../Icons/index.js';
|
|
76
76
|
import ConfigContext from '../ConfigContext.js';
|
|
77
77
|
import { Price } from '../Price/index.js';
|
|
78
|
+
import { sendOrderTracking } from '../Cart/orderTracking.js';
|
|
78
79
|
|
|
79
80
|
/* const LICENSEE_REQUIRED_FIELDS = ['organization', 'country'] as (keyof Identity)[]; */
|
|
80
81
|
|
|
@@ -274,6 +275,13 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
274
275
|
}));
|
|
275
276
|
}, [setLicenseeIdentity]);
|
|
276
277
|
const environment = useRelayEnvironment();
|
|
278
|
+
|
|
279
|
+
// Capture analytics consent + attribution on the order for the purchase
|
|
280
|
+
// event the server emits on completion (from a Stripe webhook, where no
|
|
281
|
+
// browser context exists).
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
sendOrderTracking(environment);
|
|
284
|
+
}, [environment]);
|
|
277
285
|
const [error, setError] = useState(null);
|
|
278
286
|
const [errorsObject, setErrorsObject] = useState(null);
|
|
279
287
|
const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
|
|
@@ -3,8 +3,15 @@ import { useFontStyle_fontStyle$key } from '../__generated__/useFontStyle_fontSt
|
|
|
3
3
|
interface UseFontStyle_props {
|
|
4
4
|
fontStyle: useFontStyle_fontStyle$key | null | undefined;
|
|
5
5
|
}
|
|
6
|
+
export type VerticalMetrics = {
|
|
7
|
+
readonly unitsPerEm: number;
|
|
8
|
+
readonly ascender: number;
|
|
9
|
+
readonly descender: number;
|
|
10
|
+
readonly lineGap: number | null;
|
|
11
|
+
};
|
|
6
12
|
declare const useFontStyle: ({ fontStyle: fontStyleKey, }: UseFontStyle_props) => {
|
|
7
13
|
style: React.CSSProperties;
|
|
8
14
|
loaded: boolean;
|
|
15
|
+
verticalMetrics: VerticalMetrics | null;
|
|
9
16
|
};
|
|
10
17
|
export default useFontStyle;
|
package/dist/next/config.d.ts
CHANGED
|
@@ -25,11 +25,7 @@ interface NextConfigLike {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function tenantRewrites(): Rewrite[];
|
|
27
27
|
export declare function withFontdue<C extends NextConfigLike>(nextConfig?: C): C & {
|
|
28
|
-
rewrites(): Promise<
|
|
29
|
-
beforeFiles: Rewrite[];
|
|
30
|
-
afterFiles: Rewrite[];
|
|
31
|
-
fallback: Rewrite[];
|
|
32
|
-
}>;
|
|
28
|
+
rewrites(): Promise<RewriteGroups>;
|
|
33
29
|
images: {
|
|
34
30
|
remotePatterns: unknown[];
|
|
35
31
|
dangerouslyAllowSVG: boolean;
|
package/dist/next/config.js
CHANGED
|
@@ -9,9 +9,17 @@
|
|
|
9
9
|
// This module is evaluated at config-load time, so it must not import React,
|
|
10
10
|
// Relay, or anything else from the component tree.
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
16
|
+
// The tenant rewrites only make sense when the app routes live under the
|
|
17
|
+
// /[domain]/... tree. A single-tenant app with a flat tree (the ejected
|
|
18
|
+
// fontdue/example-next shape) routes normally and needs none of them.
|
|
19
|
+
function hasDomainTree() {
|
|
20
|
+
return ['src/app/[domain]', 'app/[domain]'].some(dir => existsSync(join(process.cwd(), dir)));
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
// Minimal structural types so this module doesn't need `next` installed to
|
|
16
24
|
// type-check; the shapes match next/dist/lib/load-custom-routes.
|
|
17
25
|
|
|
@@ -121,11 +129,16 @@ export function withFontdue() {
|
|
|
121
129
|
if (!isMultiTenant && !fontdueUrl) {
|
|
122
130
|
throw new Error('Set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
|
|
123
131
|
}
|
|
132
|
+
const domainTree = hasDomainTree();
|
|
133
|
+
if (isMultiTenant && !domainTree) {
|
|
134
|
+
throw new Error('FONTDUE_MULTI_TENANT=1 requires the app routes to live under src/app/[domain]/ (one cache entry per domain).');
|
|
135
|
+
}
|
|
124
136
|
const userImages = nextConfig.images ?? {};
|
|
125
137
|
return {
|
|
126
138
|
...nextConfig,
|
|
127
139
|
async rewrites() {
|
|
128
140
|
const user = await resolveRewrites(nextConfig.rewrites);
|
|
141
|
+
if (!domainTree) return user;
|
|
129
142
|
return {
|
|
130
143
|
// App rules run first, against the public (pre-tenant) path; because
|
|
131
144
|
// beforeFiles rules chain, a dotless path they produce is then
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { isMultiTenant, isValidDomain, fontdueEndpoint,
|
|
1
|
+
export { isMultiTenant, isValidDomain, fontdueEndpoint, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams, type FontdueEndpoint, } from './tenant.js';
|
|
2
2
|
export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
|
package/dist/next/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The config-time wrapper lives in 'fontdue-js/next/config' and the deploy
|
|
3
3
|
// hook route handler in 'fontdue-js/next/revalidate'.
|
|
4
4
|
|
|
5
|
-
export { isMultiTenant, isValidDomain, fontdueEndpoint,
|
|
5
|
+
export { isMultiTenant, isValidDomain, fontdueEndpoint, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams } from './tenant.js';
|
|
6
6
|
|
|
7
7
|
// The per-render config store consumed by fontdue-js's own server-side
|
|
8
8
|
// fetches. configureFontdueRender covers the common case; these are exported
|
package/dist/next/tenant.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type FontdueServerConfig } from '../relay/serverConfig.js';
|
|
|
2
2
|
export declare const isMultiTenant: boolean;
|
|
3
3
|
export declare function isValidDomain(domain: string): boolean;
|
|
4
4
|
export interface FontdueEndpoint {
|
|
5
|
+
/** The site domain this endpoint resolves. */
|
|
6
|
+
domain: string;
|
|
5
7
|
/** Base URL the app's own GraphQL fetches should target. */
|
|
6
8
|
origin: string;
|
|
7
9
|
/** Headers the Fontdue server needs to resolve the tenant. */
|
|
@@ -13,6 +15,12 @@ export interface FontdueEndpoint {
|
|
|
13
15
|
tags: string[];
|
|
14
16
|
}
|
|
15
17
|
export declare function fontdueEndpoint(domain: string): FontdueEndpoint;
|
|
16
|
-
export declare function fallbackSiteUrl(domain: string): string;
|
|
17
18
|
export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
|
|
18
19
|
export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
|
|
20
|
+
interface RenderProps {
|
|
21
|
+
params: Promise<Record<string, string | string[] | undefined>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
|
|
24
|
+
export declare function currentFontdueEndpoint(): FontdueEndpoint;
|
|
25
|
+
export declare function generateStaticParams(): Promise<never[]>;
|
|
26
|
+
export {};
|
package/dist/next/tenant.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
// /graphql on the page's own origin — so multi-tenant mode needs no
|
|
25
25
|
// NEXT_PUBLIC_FONTDUE_URL at all.
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { notFound } from 'next/navigation';
|
|
28
|
+
import { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
|
|
28
29
|
export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
|
|
29
30
|
const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
|
|
30
31
|
const internalOrigin = process.env.FONTDUE_ORIGIN;
|
|
@@ -41,17 +42,16 @@ export function isValidDomain(domain) {
|
|
|
41
42
|
export function fontdueEndpoint(domain) {
|
|
42
43
|
const tags = ['graphql', `graphql:${domain}`];
|
|
43
44
|
if (!isMultiTenant) {
|
|
44
|
-
if (!singleTenantUrl) {
|
|
45
|
-
throw new Error('fontdue-js/next: set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
|
|
46
|
-
}
|
|
47
45
|
return {
|
|
48
|
-
|
|
46
|
+
domain,
|
|
47
|
+
origin: requireSingleTenantUrl(),
|
|
49
48
|
headers: {},
|
|
50
49
|
tags
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
if (internalOrigin) {
|
|
54
53
|
return {
|
|
54
|
+
domain,
|
|
55
55
|
origin: internalOrigin,
|
|
56
56
|
headers: {
|
|
57
57
|
'x-forwarded-host': domain,
|
|
@@ -63,15 +63,17 @@ export function fontdueEndpoint(domain) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
return {
|
|
66
|
+
domain,
|
|
66
67
|
origin: `https://${domain}`,
|
|
67
68
|
headers: {},
|
|
68
69
|
tags
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
function requireSingleTenantUrl() {
|
|
73
|
+
if (!singleTenantUrl) {
|
|
74
|
+
throw new Error('fontdue-js/next: set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
|
|
75
|
+
}
|
|
76
|
+
return singleTenantUrl;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
// Per-render config for fontdue-js's own server-side fetches: same endpoint
|
|
@@ -86,7 +88,8 @@ export function fontdueServerConfig(domain) {
|
|
|
86
88
|
return {
|
|
87
89
|
url: origin,
|
|
88
90
|
headers,
|
|
89
|
-
cacheTags: [`graphql:${domain}`]
|
|
91
|
+
cacheTags: [`graphql:${domain}`],
|
|
92
|
+
domain
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -102,4 +105,65 @@ export function configureFontdueRender(domain) {
|
|
|
102
105
|
if (!isValidDomain(domain)) return null;
|
|
103
106
|
setFontdueServerConfig(fontdueServerConfig(domain));
|
|
104
107
|
return fontdueEndpoint(domain);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The props every page, layout and generateMetadata receives. Any params
|
|
111
|
+
// object is structurally compatible; only `domain` is read.
|
|
112
|
+
|
|
113
|
+
// The one line at the top of every page, layout and generateMetadata body
|
|
114
|
+
// in the [domain] route tree:
|
|
115
|
+
//
|
|
116
|
+
// await prepareFontdueRender(props);
|
|
117
|
+
//
|
|
118
|
+
// Reads the request's site from the [domain] route param, 404s anything
|
|
119
|
+
// that isn't a plain hostname (stray paths can reach the catch-all route
|
|
120
|
+
// with their first segment as the "domain"), and points every Fontdue fetch
|
|
121
|
+
// in the rest of this render pass — the app's fetches via
|
|
122
|
+
// currentFontdueEndpoint and fontdue-js's embedded-component preloads — at
|
|
123
|
+
// that site.
|
|
124
|
+
//
|
|
125
|
+
// It must run per entry point, not only in the layout: a soft navigation
|
|
126
|
+
// re-renders just the page segment, and the per-render store starts empty
|
|
127
|
+
// on every pass. Forgetting it is loud, not subtle: currentFontdueEndpoint
|
|
128
|
+
// throws in multi-tenant mode when no render config was set.
|
|
129
|
+
//
|
|
130
|
+
// Route handlers are not React renders — the render-scoped store doesn't
|
|
131
|
+
// exist there, so use the returned endpoint explicitly instead of relying
|
|
132
|
+
// on currentFontdueEndpoint.
|
|
133
|
+
export async function prepareFontdueRender(props) {
|
|
134
|
+
const {
|
|
135
|
+
domain
|
|
136
|
+
} = await props.params;
|
|
137
|
+
const endpoint = typeof domain === 'string' ? configureFontdueRender(domain) : null;
|
|
138
|
+
if (!endpoint) notFound();
|
|
139
|
+
return endpoint;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Endpoint for the app's own GraphQL fetches in this render pass: whatever
|
|
143
|
+
// prepareFontdueRender configured, or — in a single-tenant app without the
|
|
144
|
+
// [domain] tree, where prepareFontdueRender is never called — the
|
|
145
|
+
// NEXT_PUBLIC_FONTDUE_URL site. Throwing rather than guessing in
|
|
146
|
+
// multi-tenant mode turns a forgotten prepareFontdueRender into an
|
|
147
|
+
// unmissable error instead of a silent wrong-site fetch on soft
|
|
148
|
+
// navigations.
|
|
149
|
+
export function currentFontdueEndpoint() {
|
|
150
|
+
var _getFontdueServerConf;
|
|
151
|
+
const domain = (_getFontdueServerConf = getFontdueServerConfig()) === null || _getFontdueServerConf === void 0 ? void 0 : _getFontdueServerConf.domain;
|
|
152
|
+
if (domain) return fontdueEndpoint(domain);
|
|
153
|
+
if (isMultiTenant) {
|
|
154
|
+
throw new Error('fontdue-js/next: no render config set — call prepareFontdueRender(props) at the top of every page, layout and generateMetadata that fetches.');
|
|
155
|
+
}
|
|
156
|
+
return fontdueEndpoint(new URL(requireSingleTenantUrl()).host);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Re-export from routes in the [domain] tree:
|
|
160
|
+
//
|
|
161
|
+
// export { generateStaticParams } from 'fontdue-js/next';
|
|
162
|
+
//
|
|
163
|
+
// Domains aren't known at build time, so nothing is prerendered — but
|
|
164
|
+
// providing generateStaticParams is what opts a dynamic route into
|
|
165
|
+
// static-on-demand rendering (generated on first request, then cached until
|
|
166
|
+
// revalidated) instead of fully dynamic rendering.
|
|
167
|
+
export async function generateStaticParams() {
|
|
168
|
+
return [];
|
|
105
169
|
}
|
|
@@ -4,7 +4,7 @@ import { getFontdueServerConfig } from './serverConfig.js';
|
|
|
4
4
|
|
|
5
5
|
// `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
|
|
6
6
|
// (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
|
|
7
|
-
const version = "3.0.0-
|
|
7
|
+
const version = "3.0.0-alpha9";
|
|
8
8
|
const IS_SERVER = typeof window === typeof undefined;
|
|
9
9
|
|
|
10
10
|
// Read env from either process.env (Node/Next.js) or import.meta.env (Vite/Astro).
|
|
@@ -63,6 +63,16 @@ export function createNetworkFetch(options) {
|
|
|
63
63
|
query: request.text,
|
|
64
64
|
variables
|
|
65
65
|
}),
|
|
66
|
+
// Server-side fetches are all site content (per-session data is
|
|
67
|
+
// fetched client-side only), so opt them into Next's data cache —
|
|
68
|
+
// without this, Next 15 treats them as no-store and silently makes
|
|
69
|
+
// every page fully dynamic. The tags below let the revalidate
|
|
70
|
+
// handler purge them when content changes. Inert outside Next:
|
|
71
|
+
// Node's fetch accepts and ignores this cache mode, and the
|
|
72
|
+
// browser path doesn't set it.
|
|
73
|
+
...(IS_SERVER ? {
|
|
74
|
+
cache: 'force-cache'
|
|
75
|
+
} : {}),
|
|
66
76
|
// @ts-ignore
|
|
67
77
|
next: {
|
|
68
78
|
tags: ['graphql', ...((serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.cacheTags) ?? []), `operation:${request.name}`]
|
|
@@ -5,6 +5,12 @@ export interface FontdueServerConfig {
|
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
6
|
/** Extra Next.js fetch cache tags applied to every server-side GraphQL fetch. */
|
|
7
7
|
cacheTags?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* The site domain this render is for, when known. Set by
|
|
10
|
+
* fontdue-js/next's prepareFontdueRender so render-scoped helpers
|
|
11
|
+
* (currentFontdueEndpoint) can recover it.
|
|
12
|
+
*/
|
|
13
|
+
domain?: string;
|
|
8
14
|
}
|
|
9
15
|
export declare function setFontdueServerConfig(config: FontdueServerConfig): void;
|
|
10
16
|
export declare function getFontdueServerConfig(): FontdueServerConfig | undefined;
|
package/package.json
CHANGED
package/vitest.config.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { defineConfig } from 'vitest/config';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
+
// Stand-in for the babel define plugin that inlines the package version.
|
|
6
|
+
define: {
|
|
7
|
+
__FONTDUE_JS_VERSION__: JSON.stringify('0.0.0-test'),
|
|
8
|
+
},
|
|
5
9
|
test: {
|
|
6
10
|
alias: {
|
|
7
11
|
'__generated__': path.resolve(__dirname, 'src/__generated__'),
|