fontdue-js 3.0.0-alpha9 → 3.0.1

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 (135) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +182 -13
  3. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  4. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
  5. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
  7. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
  9. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartQuery.graphql.js +9 -3
  11. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
  13. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  14. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
  15. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
  17. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
  19. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  20. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
  21. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
  23. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  24. package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
  25. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  26. package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
  27. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
  28. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
  29. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
  30. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
  31. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  32. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
  33. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  34. package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
  35. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
  37. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  38. package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
  39. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
  41. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
  43. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  44. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
  45. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  46. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
  47. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  48. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
  49. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
  51. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
  53. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  54. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
  55. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  56. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
  57. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
  58. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
  59. package/dist/__tests__/createFontdueFetch.test.js +276 -0
  60. package/dist/__tests__/imageLoader.test.js +62 -0
  61. package/dist/__tests__/metricFallback.test.js +74 -0
  62. package/dist/__tests__/networkFetch.test.js +125 -3
  63. package/dist/__tests__/nextAdapter.test.js +175 -60
  64. package/dist/__tests__/preview.test.js +217 -0
  65. package/dist/__tests__/previewServer.test.js +118 -0
  66. package/dist/__tests__/previewState.test.js +63 -0
  67. package/dist/__tests__/serverConfig.test.js +62 -0
  68. package/dist/components/BuyButton/index.d.ts +2 -2
  69. package/dist/components/BuyButton/index.js +3 -3
  70. package/dist/components/CharacterViewer/index.d.ts +2 -2
  71. package/dist/components/CharacterViewer/index.js +20 -11
  72. package/dist/components/ConfigContext.d.ts +21 -2
  73. package/dist/components/ConfigContext.js +12 -2
  74. package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
  75. package/dist/components/ConnectionErrorToolbar.js +106 -0
  76. package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
  77. package/dist/components/FontdueAdminToolbar/index.js +299 -0
  78. package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
  79. package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
  80. package/dist/components/FontdueContextProvider/index.js +6 -4
  81. package/dist/components/FontdueProvider/index.js +6 -1
  82. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  83. package/dist/components/FontdueProvider/index.server.js +10 -0
  84. package/dist/components/NewsletterSignup/index.d.ts +2 -2
  85. package/dist/components/NewsletterSignup/index.js +2 -2
  86. package/dist/components/Root/index.js +16 -2
  87. package/dist/components/TestFontsForm/index.d.ts +2 -2
  88. package/dist/components/TestFontsForm/index.js +2 -2
  89. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
  90. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
  91. package/dist/components/TypeTesters/index.d.ts +2 -2
  92. package/dist/components/TypeTesters/index.js +3 -3
  93. package/dist/components/useFontStyle.d.ts +1 -0
  94. package/dist/components/useFontStyle.js +12 -3
  95. package/dist/corsError.d.ts +1 -5
  96. package/dist/corsError.js +23 -13
  97. package/dist/data/unicodeNamesUrl.d.ts +2 -0
  98. package/dist/data/unicodeNamesUrl.js +18 -0
  99. package/dist/data/unicodeNamesVersion.d.ts +1 -0
  100. package/dist/data/unicodeNamesVersion.js +4 -0
  101. package/dist/fallbackFontData.d.ts +2 -0
  102. package/dist/fallbackFontData.js +10 -0
  103. package/dist/fontdue.css +231 -4
  104. package/dist/loadFontdueProviderQuery.d.ts +2 -1
  105. package/dist/loadFontdueProviderQuery.js +5 -2
  106. package/dist/metricFallback.d.ts +48 -0
  107. package/dist/metricFallback.js +98 -0
  108. package/dist/next/image-loader.js +22 -3
  109. package/dist/next/index.d.ts +1 -2
  110. package/dist/next/index.js +14 -6
  111. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  112. package/dist/next/registerSingleTenantResolver.js +35 -0
  113. package/dist/next/revalidate.js +1 -1
  114. package/dist/next/tenant.d.ts +4 -4
  115. package/dist/next/tenant.js +89 -58
  116. package/dist/preview/constants.d.ts +9 -0
  117. package/dist/preview/constants.js +117 -0
  118. package/dist/preview/index.d.ts +53 -0
  119. package/dist/preview/index.js +190 -0
  120. package/dist/preview/server.d.ts +20 -0
  121. package/dist/preview/server.js +89 -0
  122. package/dist/relay/environment.d.ts +8 -0
  123. package/dist/relay/environment.js +81 -35
  124. package/dist/relay/loadSerializableQuery.d.ts +13 -3
  125. package/dist/relay/loadSerializableQuery.js +2 -0
  126. package/dist/relay/serverConfig.d.ts +5 -7
  127. package/dist/relay/serverConfig.js +83 -8
  128. package/dist/scripts/publishUnicodeData.js +68 -0
  129. package/dist/scripts/updateUnicodeData.js +41 -6
  130. package/dist/server/index.d.ts +37 -0
  131. package/dist/server/index.js +160 -0
  132. package/package.json +5 -1
  133. package/types/next-headers.d.ts +9 -0
  134. package/types/next-navigation.d.ts +4 -0
  135. package/vitest.config.ts +5 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Per-family metric-matched fallback fonts.
3
+ *
4
+ * The generic `Fallback` font renders a dot for every codepoint but has fixed
5
+ * metrics, so while a real font loads (or for glyphs it lacks) the dots don't
6
+ * match the real font's line height or average width — the text reflows when
7
+ * the real font swaps in.
8
+ *
9
+ * This builds, per family, a fallback `FontFace` from the *same* dot-font bytes
10
+ * but with `size-adjust` + ascent/descent/line-gap overrides derived from that
11
+ * font's metrics, so the placeholder dots already occupy the right box. One
12
+ * shared buffer backs every face. The maths is the capsize / `next/font`
13
+ * formulation: with `size-adjust: S`, the override percentages are the target
14
+ * ratios divided by S (a metric overridden as a % of font-size is then scaled
15
+ * by `size-adjust`, so we pre-divide to land back on the target ratio).
16
+ */
17
+ declare global {
18
+ interface FontFaceDescriptors {
19
+ sizeAdjust?: string;
20
+ }
21
+ }
22
+ export interface TargetMetrics {
23
+ readonly unitsPerEm: number;
24
+ readonly ascender: number;
25
+ readonly descender: number;
26
+ readonly lineGap: number | null;
27
+ readonly avgCharWidth?: number | null;
28
+ }
29
+ /** The font-family name of the metric-matched fallback for `family`. */
30
+ export declare function metricFallbackFamily(family: string): string;
31
+ /**
32
+ * The FontFace descriptors that make the dot font match `metrics`, or `null`
33
+ * if the metrics are unusable. `size-adjust` matches the average character
34
+ * width; the overrides are the target ratios divided by `size-adjust` (it
35
+ * scales overridden metrics, so we pre-divide). Without `avgCharWidth` there's
36
+ * no `size-adjust` and the overrides are the raw target ratios.
37
+ *
38
+ * Pure (no FontFace/DOM) so the maths is unit-testable.
39
+ */
40
+ export declare function fallbackDescriptors(metrics: TargetMetrics | null | undefined): FontFaceDescriptors | null;
41
+ /**
42
+ * Register (once) a metric-matched fallback face for `family`, derived from the
43
+ * dot font + `metrics`. Safe to call repeatedly and on every render. Returns
44
+ * the family name to put in the CSS stack, or `null` when it can't be built
45
+ * (no metrics, or no FontFace API / SSR) so the caller uses the generic
46
+ * `Fallback`.
47
+ */
48
+ export declare function ensureMetricFallback(family: string, metrics: TargetMetrics | null | undefined): string | null;
@@ -0,0 +1,98 @@
1
+ import { FALLBACK_FONT_WOFF2_BASE64, FALLBACK_ADVANCE_PER_EM } from './fallbackFontData.js';
2
+
3
+ /**
4
+ * Per-family metric-matched fallback fonts.
5
+ *
6
+ * The generic `Fallback` font renders a dot for every codepoint but has fixed
7
+ * metrics, so while a real font loads (or for glyphs it lacks) the dots don't
8
+ * match the real font's line height or average width — the text reflows when
9
+ * the real font swaps in.
10
+ *
11
+ * This builds, per family, a fallback `FontFace` from the *same* dot-font bytes
12
+ * but with `size-adjust` + ascent/descent/line-gap overrides derived from that
13
+ * font's metrics, so the placeholder dots already occupy the right box. One
14
+ * shared buffer backs every face. The maths is the capsize / `next/font`
15
+ * formulation: with `size-adjust: S`, the override percentages are the target
16
+ * ratios divided by S (a metric overridden as a % of font-size is then scaled
17
+ * by `size-adjust`, so we pre-divide to land back on the target ratio).
18
+ */
19
+
20
+ // `sizeAdjust` is a real FontFace descriptor (Chrome 92+, Firefox 92+,
21
+ // Safari 17+) but is missing from the bundled lib.dom.d.ts FontFaceDescriptors.
22
+
23
+ /** The font-family name of the metric-matched fallback for `family`. */
24
+ export function metricFallbackFamily(family) {
25
+ return `${family} fallback`;
26
+ }
27
+ let sharedBuffer = null;
28
+ const registered = new Set();
29
+ function getBuffer() {
30
+ if (!sharedBuffer) {
31
+ const binary = atob(FALLBACK_FONT_WOFF2_BASE64);
32
+ const bytes = new Uint8Array(binary.length);
33
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
34
+ sharedBuffer = bytes.buffer;
35
+ }
36
+ return sharedBuffer;
37
+ }
38
+ function pct(value) {
39
+ return `${value * 100}%`;
40
+ }
41
+
42
+ /**
43
+ * The FontFace descriptors that make the dot font match `metrics`, or `null`
44
+ * if the metrics are unusable. `size-adjust` matches the average character
45
+ * width; the overrides are the target ratios divided by `size-adjust` (it
46
+ * scales overridden metrics, so we pre-divide). Without `avgCharWidth` there's
47
+ * no `size-adjust` and the overrides are the raw target ratios.
48
+ *
49
+ * Pure (no FontFace/DOM) so the maths is unit-testable.
50
+ */
51
+ export function fallbackDescriptors(metrics) {
52
+ if (!metrics || !(metrics.unitsPerEm > 0)) return null;
53
+ const {
54
+ unitsPerEm: upem,
55
+ ascender,
56
+ descender,
57
+ lineGap,
58
+ avgCharWidth
59
+ } = metrics;
60
+ const descriptors = {
61
+ display: 'block'
62
+ };
63
+ let sizeAdjust = 1;
64
+ if (avgCharWidth && avgCharWidth > 0) {
65
+ sizeAdjust = avgCharWidth / upem / FALLBACK_ADVANCE_PER_EM;
66
+ descriptors.sizeAdjust = pct(sizeAdjust);
67
+ }
68
+ descriptors.ascentOverride = pct(ascender / upem / sizeAdjust);
69
+ descriptors.descentOverride = pct(Math.abs(descender) / upem / sizeAdjust);
70
+ descriptors.lineGapOverride = pct((lineGap ?? 0) / upem / sizeAdjust);
71
+ return descriptors;
72
+ }
73
+
74
+ /**
75
+ * Register (once) a metric-matched fallback face for `family`, derived from the
76
+ * dot font + `metrics`. Safe to call repeatedly and on every render. Returns
77
+ * the family name to put in the CSS stack, or `null` when it can't be built
78
+ * (no metrics, or no FontFace API / SSR) so the caller uses the generic
79
+ * `Fallback`.
80
+ */
81
+ export function ensureMetricFallback(family, metrics) {
82
+ const descriptors = fallbackDescriptors(metrics);
83
+ if (!descriptors) return null;
84
+ if (typeof FontFace === 'undefined' || typeof document === 'undefined') {
85
+ return null;
86
+ }
87
+ const name = metricFallbackFamily(family);
88
+ if (registered.has(name)) return name;
89
+ registered.add(name);
90
+ try {
91
+ const face = new FontFace(name, getBuffer(), descriptors);
92
+ face.load().then(() => document.fonts.add(face)).catch(() => registered.delete(name));
93
+ } catch {
94
+ registered.delete(name);
95
+ return null;
96
+ }
97
+ return name;
98
+ }
@@ -5,8 +5,10 @@
5
5
  // NEXT_PUBLIC_FONTDUE_IMAGE_HOST is set; see config.ts.
6
6
  //
7
7
  // NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS (comma-separated hostnames) should
8
- // mirror the transformation host's allowed-origins list. Sources on other
9
- // hosts e.g. the /logo endpoint a site serves from its own (possibly
8
+ // mirror the transformation host's allowed-origins list. A leading "*."
9
+ // wildcard matches any subdomain ("*.fontdue.xyz" "cdn.fontdue.xyz", not
10
+ // the apex or a lookalike); a scheme/port on an entry is ignored. Sources on
11
+ // other hosts — e.g. the /logo endpoint a site serves from its own (possibly
10
12
  // customer-owned) domain, which can't be allowlisted — are served as-is
11
13
  // rather than as transform URLs Cloudflare would refuse (ERROR 9401).
12
14
  //
@@ -14,6 +16,23 @@
14
16
  // reads at build time, so it must stay dependency-free, and the variables
15
17
  // have to be present when `next build` runs (not just at serve time).
16
18
 
19
+ // Reduce an allowlist entry to a bare hostname (or "*."-prefixed wildcard),
20
+ // tolerating an optional scheme, path, or port so "https://*.fontdue.xyz"
21
+ // behaves like "*.fontdue.xyz".
22
+ function originHostname(entry) {
23
+ return entry.trim().replace(/^https?:\/\//i, '').split('/')[0].split(':')[0];
24
+ }
25
+ function matchesOrigin(hostname, entry) {
26
+ const origin = originHostname(entry);
27
+ if (!origin) return false;
28
+ if (origin.startsWith('*.')) {
29
+ // ".fontdue.xyz" — endsWith plus the strict length check requires at
30
+ // least one label before the dot, so the apex and lookalikes don't match.
31
+ const suffix = origin.slice(1);
32
+ return hostname.endsWith(suffix) && hostname.length > suffix.length;
33
+ }
34
+ return origin === hostname;
35
+ }
17
36
  function transformable(src) {
18
37
  const origins = process.env.NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS;
19
38
  if (!origins) return true;
@@ -23,7 +42,7 @@ function transformable(src) {
23
42
  } catch {
24
43
  return false;
25
44
  }
26
- return origins.split(',').some(origin => origin.trim() === hostname);
45
+ return origins.split(',').some(entry => matchesOrigin(hostname, entry));
27
46
  }
28
47
  export default function fontdueImageLoader(_ref) {
29
48
  let {
@@ -1,2 +1 @@
1
- export { isMultiTenant, isValidDomain, fontdueEndpoint, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams, type FontdueEndpoint, } from './tenant.js';
2
- export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
1
+ export { __prepareFontdueRender, type FontdueEndpoint } from './tenant.js';
@@ -2,9 +2,17 @@
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, fontdueServerConfig, configureFontdueRender, prepareFontdueRender, currentFontdueEndpoint, generateStaticParams } from './tenant.js';
6
-
7
- // The per-render config store consumed by fontdue-js's own server-side
8
- // fetches. configureFontdueRender covers the common case; these are exported
9
- // for apps that need to set or inspect the config directly.
10
- export { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
5
+ // Single-tenant apps need NO per-render setup and nothing from this entrypoint:
6
+ // mounting <FontdueProvider> registers the ambient resolver that configures
7
+ // every server fetch your own (createFontdueFetch) and the embedded
8
+ // components' preloads, including the admin preview token (see
9
+ // ../components/FontdueProvider/index.server.tsx -> registerSingleTenantResolver).
10
+ //
11
+ // __prepareFontdueRender is the internal multi-tenant API (double-underscored)
12
+ // used only by the Fontdue-hosted next-template — not part of the foundry
13
+ // surface — and FontdueEndpoint is its return type. The remaining tenant/config
14
+ // helpers (isMultiTenant, isValidDomain, endpointForDomain, fontdueServerConfig,
15
+ // configureFontdueRender, setFontdueServerConfig) are deliberately not
16
+ // re-exported here; the modules that need them import them directly from
17
+ // './tenant.js' / '../relay/serverConfig.js'.
18
+ export { __prepareFontdueRender } from './tenant.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ // Registers the single-tenant ambient config resolver (env URL +
2
+ // draftMode()/cookies() preview token, resolved per fetch) so a single-tenant
3
+ // foundry that mounts <FontdueProvider> never has to call a per-render setup
4
+ // function. The FontdueProvider react-server entrypoint imports this module as
5
+ // a side effect — in the call-free model the app no longer imports
6
+ // 'fontdue-js/next' itself, so the provider mount is the only thing guaranteed
7
+ // to wire the resolver up.
8
+ //
9
+ // Why this lives apart from ./tenant.ts: it statically imports ONLY the
10
+ // browser-safe registerAmbientConfigResolver. The Next request APIs
11
+ // (next/headers, next/navigation) the resolver needs sit behind a lazy
12
+ // `import('./tenant.js')` in the resolver body, which runs only when a server
13
+ // fetch actually resolves config — i.e. only under Next. That keeps next/* off
14
+ // the provider entrypoint's eager import graph, so mounting <FontdueProvider>
15
+ // stays safe for any non-Next RSC consumer. (Today only Next resolves the
16
+ // react-server export condition, but the decoupling makes that a structural
17
+ // invariant rather than an incidental one.)
18
+ import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
19
+ registerAmbientConfigResolver(async () => {
20
+ // Loaded lazily so next/headers + next/navigation never enter the
21
+ // provider's static graph; config is read per fetch from Next's request
22
+ // context (draftMode()/cookies()), which also covers soft navigations that
23
+ // re-render only the page segment.
24
+ const {
25
+ isMultiTenant,
26
+ singleTenantUrl,
27
+ buildRenderConfig
28
+ } = await import('./tenant.js');
29
+ // Multi-tenant drives config through the React.cache slot
30
+ // (__prepareFontdueRender), which needs the per-request route param, so the
31
+ // resolver stays out of its way. The slot also wins the merge, so a
32
+ // multi-tenant __prepareFontdueRender call still overrides this when present.
33
+ if (isMultiTenant || !singleTenantUrl) return undefined;
34
+ return buildRenderConfig(new URL(singleTenantUrl).host);
35
+ });
@@ -7,7 +7,7 @@
7
7
  // receive the tenant in the URL, e.g.
8
8
  // POST /api/revalidate?domain=acme.fontdue.com
9
9
  // and only that tenant's cache is purged (pages and embed data share the
10
- // per-domain tag — see fontdueEndpoint/fontdueServerConfig in ./tenant).
10
+ // per-domain tag — see endpointForDomain/fontdueServerConfig in ./tenant).
11
11
  // Single-tenant deployments use the parameterless form and purge everything
12
12
  // carrying the 'graphql' tag.
13
13
 
@@ -1,5 +1,6 @@
1
1
  import { type FontdueServerConfig } from '../relay/serverConfig.js';
2
2
  export declare const isMultiTenant: boolean;
3
+ export declare const singleTenantUrl: string | undefined;
3
4
  export declare function isValidDomain(domain: string): boolean;
4
5
  export interface FontdueEndpoint {
5
6
  /** The site domain this endpoint resolves. */
@@ -14,13 +15,12 @@ export interface FontdueEndpoint {
14
15
  */
15
16
  tags: string[];
16
17
  }
17
- export declare function fontdueEndpoint(domain: string): FontdueEndpoint;
18
+ export declare function endpointForDomain(domain: string): FontdueEndpoint;
18
19
  export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
19
20
  export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
20
21
  interface RenderProps {
21
22
  params: Promise<Record<string, string | string[] | undefined>>;
22
23
  }
23
- export declare function prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
24
- export declare function currentFontdueEndpoint(): FontdueEndpoint;
25
- export declare function generateStaticParams(): Promise<never[]>;
24
+ export declare function buildRenderConfig(domain: string): Promise<FontdueServerConfig>;
25
+ export declare function __prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
26
26
  export {};
@@ -20,14 +20,17 @@
20
20
  //
21
21
  // The fontdue-js components embedded in pages fetch the same way: their
22
22
  // server-side preloads read the per-render config set by
23
- // configureFontdueRender, and in the browser they fetch the relative
24
- // /graphql on the page's own origin so multi-tenant mode needs no
25
- // NEXT_PUBLIC_FONTDUE_URL at all.
23
+ // __prepareFontdueRender (multi-tenant) or, in single-tenant apps, the ambient
24
+ // resolver that <FontdueProvider> registersno per-render setup call — and in
25
+ // the browser they fetch the relative /graphql on the page's own origin, so
26
+ // multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
26
27
 
27
- import { notFound } from 'next/navigation';
28
- import { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
28
+ import { notFound, unstable_rethrow } from 'next/navigation';
29
+ import { cookies, draftMode } from 'next/headers';
30
+ import { setFontdueServerConfig } from '../relay/serverConfig.js';
31
+ import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from '../preview/index.js';
29
32
  export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
30
- const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
33
+ export const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
31
34
  const internalOrigin = process.env.FONTDUE_ORIGIN;
32
35
  const proxySecret = process.env.FONTDUE_PROXY_SECRET;
33
36
 
@@ -39,7 +42,7 @@ export function isValidDomain(domain) {
39
42
  }
40
43
  // Where to fetch GraphQL for a given tenant domain, plus any headers needed
41
44
  // for the Fontdue server to resolve that tenant.
42
- export function fontdueEndpoint(domain) {
45
+ export function endpointForDomain(domain) {
43
46
  const tags = ['graphql', `graphql:${domain}`];
44
47
  if (!isMultiTenant) {
45
48
  return {
@@ -84,12 +87,11 @@ export function fontdueServerConfig(domain) {
84
87
  const {
85
88
  origin,
86
89
  headers
87
- } = fontdueEndpoint(domain);
90
+ } = endpointForDomain(domain);
88
91
  return {
89
92
  url: origin,
90
93
  headers,
91
- cacheTags: [`graphql:${domain}`],
92
- domain
94
+ cacheTags: [`graphql:${domain}`]
93
95
  };
94
96
  }
95
97
 
@@ -104,66 +106,95 @@ export function fontdueServerConfig(domain) {
104
106
  export function configureFontdueRender(domain) {
105
107
  if (!isValidDomain(domain)) return null;
106
108
  setFontdueServerConfig(fontdueServerConfig(domain));
107
- return fontdueEndpoint(domain);
109
+ return endpointForDomain(domain);
108
110
  }
109
111
 
110
112
  // The props every page, layout and generateMetadata receives. Any params
111
113
  // object is structurally compatible; only `domain` is read.
112
114
 
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.
115
+ // Build this render's server config for `domain` (origin, headers, per-site
116
+ // cache tags) and install it for the rest of the pass. When a logged-in admin
117
+ // is in preview (Next draft mode), fold the token into the headers and drop the
118
+ // cache tags, so every server fetch in the render — the app's own and the
119
+ // embedded components' preloads — reveals hidden fonts and stays live
120
+ // (uncached, never landing in a shared cache).
121
+ export async function buildRenderConfig(domain) {
122
+ const config = fontdueServerConfig(domain);
123
+ const previewHeaders = await readPreviewHeaders();
124
+ return previewHeaders ? {
125
+ ...config,
126
+ headers: {
127
+ ...config.headers,
128
+ ...previewHeaders
129
+ },
130
+ cacheTags: undefined
131
+ } : config;
132
+ }
133
+ async function applyRenderConfig(domain) {
134
+ setFontdueServerConfig(await buildRenderConfig(domain));
135
+ }
136
+
137
+ // Internal multi-tenant API (double-underscored, not part of the public
138
+ // foundry surface): the one line at the top of every page, layout and
139
+ // generateMetadata body in the multi-tenant [domain] route tree (the
140
+ // Fontdue-hosted service):
124
141
  //
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.
142
+ // await __prepareFontdueRender(props);
129
143
  //
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) {
144
+ // Reads the request's site from the route param, 404s anything that isn't a
145
+ // plain hostname, and configures the render (including preview) for that site.
146
+ // It runs per entry point: a soft navigation re-renders only the page segment
147
+ // with a fresh render store. Route handlers are not React renders — use the
148
+ // returned endpoint explicitly there.
149
+ export async function __prepareFontdueRender(props) {
134
150
  const {
135
151
  domain
136
152
  } = await props.params;
137
- const endpoint = typeof domain === 'string' ? configureFontdueRender(domain) : null;
138
- if (!endpoint) notFound();
139
- return endpoint;
153
+ if (typeof domain !== 'string' || !isValidDomain(domain)) notFound();
154
+ await applyRenderConfig(domain);
155
+ return endpointForDomain(domain);
140
156
  }
141
157
 
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.');
158
+ // The admin preview token for this render, as Authorization headers, or
159
+ // undefined when not previewing. Draft mode gates it: reading draftMode().
160
+ // isEnabled is static-safe (it returns false during static generation and only
161
+ // forces dynamic rendering when actually enabled), and the token cookie is only
162
+ // read in the preview branch.
163
+ //
164
+ // The catch handles two very different throws, and the difference is load-
165
+ // bearing for preview:
166
+ //
167
+ // 1. Next's dynamic-rendering bailout. When an admin is previewing, reading
168
+ // cookies() during a prerender/static pass throws a control-flow error
169
+ // (DynamicServerError / prerender interrupt) whose whole purpose is to
170
+ // tell Next "abandon the static render and go dynamic." This is the ONLY
171
+ // signal that takes a route in generateStaticParams off the full-route
172
+ // cache for the preview request. Swallowing it (as this used to) let Next
173
+ // serve the cached, token-less *public* render in preview, so the embedded
174
+ // components' own server preloads (createNetworkFetch) never saw the token
175
+ // — hidden fonts stayed hidden and @required(THROW) embeds like
176
+ // <BuyButton> crashed the server render (FD-712). unstable_rethrow re-
177
+ // throws exactly these control-flow errors so the route bails to a
178
+ // dynamic, token-bearing render. Public renders never hit this branch
179
+ // (draftMode().isEnabled is false → early return), so they stay static.
180
+ // 2. A genuine "no request scope" throw (older runtimes calling
181
+ // draftMode()/cookies() with no request at all): not a control-flow error,
182
+ // so unstable_rethrow is a no-op and we fall through to "not previewing".
183
+ async function readPreviewHeaders() {
184
+ try {
185
+ var _await$cookies$get;
186
+ if (!(await draftMode()).isEnabled) return undefined;
187
+ const token = (_await$cookies$get = (await cookies()).get(PREVIEW_TOKEN_COOKIE)) === null || _await$cookies$get === void 0 ? void 0 : _await$cookies$get.value;
188
+ return token ? previewAuthHeaders(token) : undefined;
189
+ } catch (error) {
190
+ unstable_rethrow(error);
191
+ return undefined;
155
192
  }
156
- return fontdueEndpoint(new URL(requireSingleTenantUrl()).host);
157
193
  }
158
194
 
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 [];
169
- }
195
+ // The single-tenant ambient resolver that lets a foundry skip per-render setup
196
+ // is registered from ./registerSingleTenantResolver.ts (imported as a side
197
+ // effect by the FontdueProvider react-server entrypoint), not here — so this
198
+ // module's static next/* imports stay off that entrypoint's eager graph. The
199
+ // resolver lazy-imports buildRenderConfig + singleTenantUrl + isMultiTenant
200
+ // from this module at fetch time.
@@ -0,0 +1,9 @@
1
+ export declare const PREVIEW_TOKEN_COOKIE = "fontdue_preview_token";
2
+ export declare const PREVIEW_MARKER_COOKIE = "fontdue_preview";
3
+ export declare const PREVIEW_ENDPOINT = "/api/preview";
4
+ export declare const DEFAULT_PREVIEW_TTL_MS: number;
5
+ export declare const PREVIEW_MARKER_GRACE_MS: number;
6
+ export declare const PREVIEW_HEADER = "fontdue-preview";
7
+ export declare function hasPreviewMarkerCookie(): boolean;
8
+ export declare function getPreviewExpiry(): number | undefined;
9
+ export declare function setPreviewMarkerCookie(on: boolean | number): void;
@@ -0,0 +1,117 @@
1
+ // Cookie + endpoint conventions for the admin preview contract.
2
+ //
3
+ // Kept in their own module (no Web/Node APIs) so the client-side toolbar can
4
+ // import them without pulling in the server-only route handler — see ./index.
5
+
6
+ // httpOnly cookie carrying the logged-in admin's short-lived admin token.
7
+ // Only ever read server-side, where it's forwarded as
8
+ // `Authorization: Bearer <token>` so GraphQL reveals hidden (unpublished) fonts.
9
+ export const PREVIEW_TOKEN_COOKIE = 'fontdue_preview_token';
10
+
11
+ // Readable twin of the token cookie. Lets the client toolbar tell preview is
12
+ // on without exposing the token itself.
13
+ export const PREVIEW_MARKER_COOKIE = 'fontdue_preview';
14
+
15
+ // Default path the toolbar POSTs (enter) / DELETEs (exit) to. Relative, so it
16
+ // hits the storefront's own origin. Override per app via
17
+ // FontdueConfig.preview.endpoint (and mount the route to match).
18
+ export const PREVIEW_ENDPOINT = '/api/preview';
19
+
20
+ // Canonical preview-token lifetime, in ms. The token is a stateless
21
+ // `Phoenix.Token` with a fixed 1-hour `max_age` (see the backend
22
+ // `Fontage.Token` `:admin_token` `verify` — `max_age: 60 * 60`). It is the
23
+ // source of truth; this constant only exists as a *fallback* for when the
24
+ // `createAdminToken` mutation hasn't yet been taught to return its real
25
+ // `expiresAt` (the backend half ships separately). Keep these two in lockstep.
26
+ export const DEFAULT_PREVIEW_TTL_MS = 60 * 60 * 1000;
27
+
28
+ // How long past the token's expiry the readable marker cookie should live.
29
+ // The marker outlives the token on purpose: once the token lapses, the toolbar
30
+ // must still find the marker so it can show the "preview expired" warning and
31
+ // offer "re-enter" — rather than the cookie silently vanishing and the toolbar
32
+ // snapping back to "not previewing" (which would hide the very problem from the
33
+ // admin). A few hours of grace comfortably covers a backgrounded tab.
34
+ export const PREVIEW_MARKER_GRACE_MS = 4 * 60 * 60 * 1000;
35
+
36
+ // Request header storefront fetches send to declare preview intent explicitly.
37
+ // The GraphQL server reveals hidden (unpublished) fonts only when this is
38
+ // "true" AND the request is an authenticated admin — so merely *being* logged
39
+ // in (an admin session cookie riding a storefront fetch) never leaks hidden
40
+ // fonts outside preview. "false" forces the public view even for an admin; an
41
+ // absent header (older fontdue-js clients) keeps the legacy "any admin sees
42
+ // hidden" behavior, so older integrations are unaffected. See the matching
43
+ // server logic in FontageWeb.Schema.Context.
44
+ export const PREVIEW_HEADER = 'fontdue-preview';
45
+
46
+ // The marker cookie's *value* now encodes the token's expiry as epoch-ms, e.g.
47
+ // `fontdue_preview=1718640000000`. Previously it was a bare `1`; that legacy
48
+ // value still parses (as 1ms after the epoch → always "expired"), so any stale
49
+ // `=1` cookie left from an older client degrades safely to the warning state
50
+ // rather than reading as a live preview. Empty / unset means "not previewing".
51
+
52
+ // Raw value of the marker cookie on the current document, or undefined when the
53
+ // cookie is absent or we're not in a browser. Browser-only and guarded so it
54
+ // stays inert during server rendering.
55
+ function readPreviewMarkerValue() {
56
+ if (typeof document === 'undefined') return undefined;
57
+ for (const part of document.cookie.split('; ')) {
58
+ const idx = part.indexOf('=');
59
+ if (idx === -1) continue;
60
+ if (part.slice(0, idx) !== PREVIEW_MARKER_COOKIE) continue;
61
+ const value = part.slice(idx + 1);
62
+ return value === '' ? undefined : value;
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ // Whether the readable preview marker cookie is present on the current
68
+ // document. Lets the admin toolbar and the client-side Relay network layer tell
69
+ // that an admin entered preview, so storefront fetches can send PREVIEW_HEADER
70
+ // "true" while the marker is set and "false" otherwise. Presence — not
71
+ // freshness — gates the header: a just-expired token still needs its render to
72
+ // stay out of the public cache until the admin re-enters or exits, and the
73
+ // server independently rejects a dead token, so a marker with a past expiry
74
+ // never actually leaks hidden fonts.
75
+ export function hasPreviewMarkerCookie() {
76
+ return readPreviewMarkerValue() !== undefined;
77
+ }
78
+
79
+ // Expiry (epoch-ms) stored in the marker cookie, or undefined when the marker
80
+ // is absent or its value isn't a finite number. The toolbar compares this to
81
+ // `Date.now()` to choose between the "previewing" and "preview expired" states.
82
+ export function getPreviewExpiry() {
83
+ const value = readPreviewMarkerValue();
84
+ if (value === undefined) return undefined;
85
+ const expiry = Number(value);
86
+ return Number.isFinite(expiry) ? expiry : undefined;
87
+ }
88
+
89
+ // Set or clear the readable preview marker cookie directly in the browser.
90
+ //
91
+ // The script-tag (CDN) embed has no server preview route to broker an httpOnly
92
+ // token through — and needs none: it is client-only, so its GraphQL fetches
93
+ // carry the admin's Fontdue session cookie cross-origin (SameSite=None) and the
94
+ // server reveals hidden fonts on `fontdue-preview: true` alone. This marker is
95
+ // what flips the client-side Relay layer to send that header (see
96
+ // hasPreviewMarkerCookie). It is NOT a credential — the admin session still
97
+ // gates the reveal, so a non-admin setting it just sees the public view.
98
+ //
99
+ // When entering (`on` truthy), `on` is the token's expiry as epoch-ms; the
100
+ // cookie stores it and is given a Max-Age that outlives the token by
101
+ // PREVIEW_MARKER_GRACE_MS so the toolbar can reach the "expired" warning state.
102
+ // Passing `false` clears the cookie. (A bare `true` keeps older call sites
103
+ // working by falling back to now + DEFAULT_PREVIEW_TTL_MS.)
104
+ //
105
+ // Secure is set on https (the marker stays JS-readable; Secure only restricts
106
+ // the transport) and omitted on http so it still persists in local dev.
107
+ export function setPreviewMarkerCookie(on) {
108
+ if (typeof document === 'undefined') return;
109
+ const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
110
+ if (on === false) {
111
+ document.cookie = `${PREVIEW_MARKER_COOKIE}=; Path=/; SameSite=Lax${secure}; Max-Age=0`;
112
+ return;
113
+ }
114
+ const expiresAt = typeof on === 'number' ? on : Date.now() + DEFAULT_PREVIEW_TTL_MS;
115
+ const maxAgeSeconds = Math.max(0, Math.ceil((expiresAt - Date.now() + PREVIEW_MARKER_GRACE_MS) / 1000));
116
+ document.cookie = `${PREVIEW_MARKER_COOKIE}=${expiresAt}; Path=/; SameSite=Lax${secure}; Max-Age=${maxAgeSeconds}`;
117
+ }