fontdue-js 3.0.0-alpha6 → 3.0.0-alpha8

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 (48) hide show
  1. package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.d.ts +27 -0
  2. package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +72 -0
  3. package/dist/__tests__/nextAdapter.test.js +307 -0
  4. package/dist/components/BuyButton/index.js +8 -2
  5. package/dist/components/Cart/orderTracking.d.ts +10 -0
  6. package/dist/components/Cart/orderTracking.js +43 -0
  7. package/dist/components/CartButton/index.js +16 -4
  8. package/dist/components/CharacterViewer/index.js +8 -2
  9. package/dist/components/CustomerLoginForm/index.js +17 -9
  10. package/dist/components/FontdueProvider/index.d.ts +10 -1
  11. package/dist/components/FontdueProvider/index.js +1 -0
  12. package/dist/components/FontdueProvider/index.server.d.ts +2 -1
  13. package/dist/components/FontdueProvider/index.server.js +16 -0
  14. package/dist/components/NewsletterSignup/index.js +4 -1
  15. package/dist/components/TestFontsForm/index.js +4 -1
  16. package/dist/components/TypeTesters/index.js +8 -2
  17. package/dist/next/config.d.ts +45 -0
  18. package/dist/next/config.js +180 -0
  19. package/dist/next/image-loader.d.ts +7 -0
  20. package/dist/next/image-loader.js +39 -0
  21. package/dist/next/index.d.ts +2 -0
  22. package/dist/next/index.js +10 -0
  23. package/dist/next/revalidate.d.ts +1 -0
  24. package/dist/next/revalidate.js +37 -0
  25. package/dist/next/tenant.d.ts +18 -0
  26. package/dist/next/tenant.js +105 -0
  27. package/dist/relay/environment.js +10 -4
  28. package/dist/relay/loadSerializableQuery.d.ts +3 -1
  29. package/dist/relay/loadSerializableQuery.js +2 -2
  30. package/dist/relay/serverConfig.d.ts +10 -0
  31. package/dist/relay/serverConfig.js +38 -0
  32. package/dist/vite.js +2 -0
  33. package/package.json +5 -1
  34. package/types/next-cache.d.ts +6 -0
  35. package/dist/__generated__/TypeTesterStyleSelectData_viewer.graphql.d.ts +0 -42
  36. package/dist/__generated__/TypeTesterStyleSelectData_viewer.graphql.js +0 -166
  37. package/dist/__generated__/TypeTester_viewer.graphql.d.ts +0 -17
  38. package/dist/__generated__/TypeTester_viewer.graphql.js +0 -40
  39. package/dist/__generated__/TypeTesters_viewer.graphql.d.ts +0 -17
  40. package/dist/__generated__/TypeTesters_viewer.graphql.js +0 -40
  41. package/dist/components/FontdueContextProvider/index.server.d.ts +0 -4
  42. package/dist/components/FontdueContextProvider/index.server.js +0 -7
  43. package/dist/components/FontdueProvider/useAuxUIOwner.d.ts +0 -1
  44. package/dist/components/FontdueProvider/useAuxUIOwner.js +0 -28
  45. package/dist/components/TypeTester/TypeTesterStandalone.preload.d.ts +0 -14
  46. package/dist/components/TypeTester/TypeTesterStandalone.preload.js +0 -20
  47. package/dist/config.d.ts +0 -7
  48. package/dist/config.js +0 -31
@@ -2,6 +2,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
2
2
  import React from 'react';
3
3
  import FontdueProvider from './index.js';
4
4
  import loadFontdueProviderQueryImpl from '../../loadFontdueProviderQuery.js';
5
+ import { setFontdueServerConfig } from '../../relay/serverConfig.js';
5
6
  // Stub for the RSC export condition. The default `<FontdueProvider>` server
6
7
  // entrypoint (this file) awaits the query for consumers, so RSC users should
7
8
  // never call it manually. Re-exporting a throwing stub makes the mistake
@@ -14,11 +15,26 @@ export function loadFontdueProviderQuery() {
14
15
 
15
16
  // RSC entry. When no preloadedQuery is passed, fetch one server-side so the
16
17
  // Next.js App Router path stays "drop the provider in your layout, done."
18
+ //
19
+ // `serverConfig` (and, as a shorthand, the `url` prop) is written to the
20
+ // per-render server config store before anything fetches, so every Fontdue
21
+ // server component below the provider — and the provider's own query —
22
+ // fetches against it. It is intentionally NOT forwarded to the client
23
+ // component: it can carry internal headers (e.g. proxy auth) that must not
24
+ // be serialized into the RSC payload. Note a soft navigation re-renders only
25
+ // the page segment, not a layout-hosted provider — pages that contain
26
+ // Fontdue components should call setFontdueServerConfig themselves.
17
27
  export default async function FontdueProviderServer(_ref) {
18
28
  let {
19
29
  preloadedQuery,
30
+ serverConfig,
20
31
  ...rest
21
32
  } = _ref;
33
+ if (serverConfig || rest.url) {
34
+ setFontdueServerConfig(serverConfig ?? {
35
+ url: rest.url
36
+ });
37
+ }
22
38
  const resolved = preloadedQuery ?? (await loadFontdueProviderQueryImpl());
23
39
  return /*#__PURE__*/React.createElement(FontdueProvider, _extends({}, rest, {
24
40
  preloadedQuery: resolved
@@ -182,7 +182,10 @@ export function NewsletterSignupPreloadedQueryRenderer(_ref3) {
182
182
  preloadedQuery,
183
183
  ...props
184
184
  } = _ref3;
185
- const queryRef = useSerializablePreloadedQuery(preloadedQuery);
185
+ // The query node lets the hook commit the payload into the store, so
186
+ // usePreloadedQuery resolves synchronously during SSR instead of
187
+ // refetching (the response cache only exists in the browser).
188
+ const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', query);
186
189
  const data = usePreloadedQuery(query, queryRef);
187
190
  return /*#__PURE__*/React.createElement(NewsletterSignupComponent, _extends({
188
191
  data: data
@@ -202,7 +202,10 @@ export function TestFontsFormPreloadedQueryRenderer(_ref4) {
202
202
  preloadedQuery,
203
203
  ...rest
204
204
  } = _ref4;
205
- const queryRef = useSerializablePreloadedQuery(preloadedQuery);
205
+ // The query node lets the hook commit the payload into the store, so
206
+ // usePreloadedQuery resolves synchronously during SSR instead of
207
+ // refetching (the response cache only exists in the browser).
208
+ const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', query);
206
209
  const data = usePreloadedQuery(query, queryRef);
207
210
  return /*#__PURE__*/React.createElement(TestFontsFormComponent, _extends({
208
211
  data: data
@@ -191,7 +191,10 @@ export function TypeTestersPreloadedIDQueryRenderer(_ref3) {
191
191
  preloadedQuery,
192
192
  ...rest
193
193
  } = _ref3;
194
- const queryRef = useSerializablePreloadedQuery(preloadedQuery);
194
+ // The query node lets the hook commit the payload into the store, so
195
+ // usePreloadedQuery resolves synchronously during SSR instead of
196
+ // refetching (the response cache only exists in the browser).
197
+ const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', idQuery);
195
198
  const data = usePreloadedQuery(idQuery, queryRef);
196
199
  return /*#__PURE__*/React.createElement(TypeTestersComponent, _extends({}, data, rest));
197
200
  }
@@ -219,7 +222,10 @@ export function TypeTestersPreloadedSlugQueryRenderer(_ref5) {
219
222
  preloadedQuery,
220
223
  ...rest
221
224
  } = _ref5;
222
- const queryRef = useSerializablePreloadedQuery(preloadedQuery);
225
+ // The query node lets the hook commit the payload into the store, so
226
+ // usePreloadedQuery resolves synchronously during SSR instead of
227
+ // refetching (the response cache only exists in the browser).
228
+ const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', slugQuery);
223
229
  const data = usePreloadedQuery(slugQuery, queryRef);
224
230
  return /*#__PURE__*/React.createElement(TypeTestersComponent, _extends({
225
231
  collection: ((_data$viewer$slug = data.viewer.slug) === null || _data$viewer$slug === void 0 ? void 0 : _data$viewer$slug.collection) ?? null
@@ -0,0 +1,45 @@
1
+ interface RouteHas {
2
+ type: 'header' | 'query' | 'cookie' | 'host';
3
+ key?: string;
4
+ value?: string;
5
+ }
6
+ interface Rewrite {
7
+ source: string;
8
+ destination: string;
9
+ has?: RouteHas[];
10
+ missing?: RouteHas[];
11
+ }
12
+ interface RewriteGroups {
13
+ beforeFiles: Rewrite[];
14
+ afterFiles: Rewrite[];
15
+ fallback: Rewrite[];
16
+ }
17
+ type RewritesResult = Rewrite[] | Partial<RewriteGroups>;
18
+ interface NextConfigLike {
19
+ rewrites?: () => Promise<RewritesResult> | RewritesResult;
20
+ images?: {
21
+ remotePatterns?: unknown[];
22
+ [key: string]: unknown;
23
+ };
24
+ [key: string]: unknown;
25
+ }
26
+ export declare function tenantRewrites(): Rewrite[];
27
+ export declare function withFontdue<C extends NextConfigLike>(nextConfig?: C): C & {
28
+ rewrites(): Promise<{
29
+ beforeFiles: Rewrite[];
30
+ afterFiles: Rewrite[];
31
+ fallback: Rewrite[];
32
+ }>;
33
+ images: {
34
+ remotePatterns: unknown[];
35
+ dangerouslyAllowSVG: boolean;
36
+ loader: "custom";
37
+ loaderFile: string;
38
+ } | {
39
+ remotePatterns: unknown[];
40
+ dangerouslyAllowSVG: boolean;
41
+ unoptimized: boolean;
42
+ };
43
+ htmlLimitedBots: RegExp;
44
+ };
45
+ export {};
@@ -0,0 +1,180 @@
1
+ // withFontdue(nextConfig): next.config wrapper that installs everything a
2
+ // Fontdue storefront needs — host→path tenant rewrites, image settings, and
3
+ // workarounds for Next behaviors that would otherwise break Fontdue pages.
4
+ // Import it from next.config.mjs (this package is ESM):
5
+ //
6
+ // import { withFontdue } from 'fontdue-js/next/config';
7
+ // export default withFontdue({ /* your config */ });
8
+ //
9
+ // This module is evaluated at config-load time, so it must not import React,
10
+ // Relay, or anything else from the component tree.
11
+
12
+ import { relative } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ // Minimal structural types so this module doesn't need `next` installed to
16
+ // type-check; the shapes match next/dist/lib/load-custom-routes.
17
+
18
+ // Every page lives under the /[domain]/... route tree so Next renders and
19
+ // caches each tenant's pages independently. These rewrites turn the request's
20
+ // host into that leading path segment:
21
+ //
22
+ // acme.fontdue.com/fonts/foo → /acme.fontdue.com/fonts/foo (internal)
23
+ //
24
+ // In single-tenant mode the domain is constant (from NEXT_PUBLIC_FONTDUE_URL)
25
+ // so the app behaves exactly like a plain single-site Next app.
26
+ //
27
+ // This is done with config rewrites rather than middleware on purpose:
28
+ // middleware rewrites bypass the ISR page cache on self-hosted next start,
29
+ // turning every request into a full render. beforeFiles rewrites go through
30
+ // the normal routing layer and keep per-tenant ISR working. (beforeFiles also
31
+ // runs before app routes are matched, so the internal /[domain] paths can't
32
+ // be reached directly with a mismatching Host — /evil.com on acme's domain
33
+ // becomes /acme.fontdue.com/evil.com, which 404s.)
34
+ //
35
+ // X-Forwarded-Host (set by the Fontdue proxy in front of this service) wins
36
+ // over Host. The hostname charset is constrained in the patterns; anything
37
+ // else falls through to a 404.
38
+ //
39
+ // beforeFiles rules CHAIN: each rule is evaluated in order against the
40
+ // already-rewritten path, so a rewrite must produce a path no later rule can
41
+ // match. Tenant domains always contain a dot, so the catch-all path rule
42
+ // refuses any path whose first segment contains a dot — that makes the
43
+ // rewritten /acme.fontdue.com/... inert. robots.txt and sitemap.xml (dotted
44
+ // first segments we DO want to serve) get their own explicit rules, which run
45
+ // first. Side effect: a page slug containing a dot can't be routed at the
46
+ // top level.
47
+ const TENANT_HOST = '(?<tenant>[a-zA-Z0-9][a-zA-Z0-9.-]*)';
48
+ const DOTLESS_PATH = '/:path((?!api/|_next/|favicon\\.ico)(?![^/]*\\.[^/]*(?:/|$)).*)';
49
+
50
+ // One rule set rewriting onto `dest` (either a fixed /domain or the /:tenant
51
+ // capture from `has`). `conditions` is {has?, missing?}.
52
+ function rewriteRules(dest) {
53
+ let conditions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
54
+ return [{
55
+ source: '/',
56
+ destination: dest,
57
+ ...conditions
58
+ }, {
59
+ source: '/robots.txt',
60
+ destination: `${dest}/robots.txt`,
61
+ ...conditions
62
+ }, {
63
+ source: '/sitemap.xml',
64
+ destination: `${dest}/sitemap.xml`,
65
+ ...conditions
66
+ }, {
67
+ source: DOTLESS_PATH,
68
+ destination: `${dest}/:path`,
69
+ ...conditions
70
+ }];
71
+ }
72
+ export function tenantRewrites() {
73
+ const fontdueUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
74
+ const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
75
+ if (!isMultiTenant) {
76
+ return rewriteRules(`/${new URL(fontdueUrl).host}`);
77
+ }
78
+ const forwardedHost = {
79
+ type: 'header',
80
+ key: 'x-forwarded-host',
81
+ value: `${TENANT_HOST}(:.*)?`
82
+ };
83
+ const noForwardedHost = {
84
+ type: 'header',
85
+ key: 'x-forwarded-host'
86
+ };
87
+ const host = {
88
+ type: 'host',
89
+ value: TENANT_HOST
90
+ };
91
+ return [...rewriteRules('/:tenant', {
92
+ has: [forwardedHost]
93
+ }),
94
+ // Host-based rules only apply when X-Forwarded-Host is absent, so the
95
+ // two sets can't both rewrite one request.
96
+ ...rewriteRules('/:tenant', {
97
+ has: [host],
98
+ missing: [noForwardedHost]
99
+ })];
100
+ }
101
+ async function resolveRewrites(rewrites) {
102
+ const result = (await (rewrites === null || rewrites === void 0 ? void 0 : rewrites())) ?? [];
103
+ // A plain array from rewrites() is afterFiles, per Next's contract.
104
+ if (Array.isArray(result)) {
105
+ return {
106
+ beforeFiles: [],
107
+ afterFiles: result,
108
+ fallback: []
109
+ };
110
+ }
111
+ return {
112
+ beforeFiles: result.beforeFiles ?? [],
113
+ afterFiles: result.afterFiles ?? [],
114
+ fallback: result.fallback ?? []
115
+ };
116
+ }
117
+ export function withFontdue() {
118
+ let nextConfig = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
119
+ const fontdueUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
120
+ const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
121
+ if (!isMultiTenant && !fontdueUrl) {
122
+ throw new Error('Set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
123
+ }
124
+ const userImages = nextConfig.images ?? {};
125
+ return {
126
+ ...nextConfig,
127
+ async rewrites() {
128
+ const user = await resolveRewrites(nextConfig.rewrites);
129
+ return {
130
+ // App rules run first, against the public (pre-tenant) path; because
131
+ // beforeFiles rules chain, a dotless path they produce is then
132
+ // prefixed by the tenant rules like any direct request. afterFiles
133
+ // and fallback rules run after the tenant prefix is applied, so
134
+ // their sources must match the internal /[domain]/... form.
135
+ beforeFiles: [...user.beforeFiles, ...tenantRewrites()],
136
+ afterFiles: user.afterFiles,
137
+ fallback: user.fallback
138
+ };
139
+ },
140
+ images: {
141
+ // With a Cloudflare image transformation host configured, optimization
142
+ // moves to its edge (image-loader.ts) and the in-process optimizer —
143
+ // and sharp — stay out of the deployment entirely. The host must be in
144
+ // the env at build time (the loader is inlined into the client bundle)
145
+ // as well as at serve time (this config runs again under next start).
146
+ //
147
+ // Without one, in multi-tenant dev the GraphQL origin is typically a
148
+ // container or tunnel from which tenant hostnames don't resolve
149
+ // publicly, so the optimizer can't fetch the originals — serve them
150
+ // unoptimized there.
151
+ ...(process.env.NEXT_PUBLIC_FONTDUE_IMAGE_HOST ? {
152
+ loader: 'custom',
153
+ // Next resolves loaderFile against the project root and rejects
154
+ // absolute paths, so point into this package relative to cwd.
155
+ loaderFile: relative(process.cwd(), fileURLToPath(new URL('./image-loader.js', import.meta.url)))
156
+ } : {
157
+ unoptimized: isMultiTenant && process.env.NODE_ENV !== 'production'
158
+ }),
159
+ dangerouslyAllowSVG: true,
160
+ ...userImages,
161
+ remotePatterns: [...(fontdueUrl ? [{
162
+ protocol: 'https',
163
+ hostname: new URL(fontdueUrl).hostname
164
+ }] : []), {
165
+ protocol: 'https',
166
+ hostname: '*.fontdue.com'
167
+ },
168
+ // Multi-tenant: logos and images can live on any tenant custom
169
+ // domain. The URLs all come from the Fontdue CMS, not user input.
170
+ ...(isMultiTenant ? [{
171
+ protocol: 'https',
172
+ hostname: '**'
173
+ }] : []), ...(userImages.remotePatterns ?? [])]
174
+ },
175
+ // Treat every user agent as HTML-limited so metadata rendering blocks
176
+ // the response. Streamed metadata locks in a 200 status before
177
+ // generateMetadata's notFound() can produce a real 404.
178
+ htmlLimitedBots: nextConfig.htmlLimitedBots ?? /.*/
179
+ };
180
+ }
@@ -0,0 +1,7 @@
1
+ interface ImageLoaderProps {
2
+ src: string;
3
+ width: number;
4
+ quality?: number;
5
+ }
6
+ export default function fontdueImageLoader({ src, width, quality, }: ImageLoaderProps): string;
7
+ export {};
@@ -0,0 +1,39 @@
1
+ // Custom next/image loader that serves images through a Cloudflare image
2
+ // transformation host (developers.cloudflare.com/images/transform-images)
3
+ // instead of the in-process Next optimizer, so the deployment needs neither
4
+ // the /_next/image endpoint nor sharp. withFontdue activates it when
5
+ // NEXT_PUBLIC_FONTDUE_IMAGE_HOST is set; see config.ts.
6
+ //
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
10
+ // customer-owned) domain, which can't be allowlisted — are served as-is
11
+ // rather than as transform URLs Cloudflare would refuse (ERROR 9401).
12
+ //
13
+ // Next bundles this file into the client build and inlines the NEXT_PUBLIC_
14
+ // reads at build time, so it must stay dependency-free, and the variables
15
+ // have to be present when `next build` runs (not just at serve time).
16
+
17
+ function transformable(src) {
18
+ const origins = process.env.NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS;
19
+ if (!origins) return true;
20
+ let hostname;
21
+ try {
22
+ hostname = new URL(src).hostname;
23
+ } catch {
24
+ return false;
25
+ }
26
+ return origins.split(',').some(origin => origin.trim() === hostname);
27
+ }
28
+ export default function fontdueImageLoader(_ref) {
29
+ let {
30
+ src,
31
+ width,
32
+ quality
33
+ } = _ref;
34
+ const host = process.env.NEXT_PUBLIC_FONTDUE_IMAGE_HOST;
35
+ // No transform host, a local asset it couldn't fetch, or a source outside
36
+ // its allowed origins: serve the original, like `unoptimized`.
37
+ if (!host || src.startsWith('/') || !transformable(src)) return src;
38
+ return `https://${host}/cdn-cgi/image/width=${width},quality=${quality ?? 75},format=auto/${src}`;
39
+ }
@@ -0,0 +1,2 @@
1
+ export { isMultiTenant, isValidDomain, fontdueEndpoint, fallbackSiteUrl, fontdueServerConfig, configureFontdueRender, type FontdueEndpoint, } from './tenant.js';
2
+ export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
@@ -0,0 +1,10 @@
1
+ // Server-side entrypoint for Next.js apps (the App Router / RSC adapter).
2
+ // The config-time wrapper lives in 'fontdue-js/next/config' and the deploy
3
+ // hook route handler in 'fontdue-js/next/revalidate'.
4
+
5
+ export { isMultiTenant, isValidDomain, fontdueEndpoint, fallbackSiteUrl, fontdueServerConfig, configureFontdueRender } 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';
@@ -0,0 +1 @@
1
+ export declare function POST(request: Request): Promise<Response>;
@@ -0,0 +1,37 @@
1
+ // Route handler for Fontdue's deploy hook: re-export it from
2
+ // app/api/revalidate/route.ts:
3
+ //
4
+ // export { POST } from 'fontdue-js/next/revalidate';
5
+ //
6
+ // Fontdue calls it when a site's content changes. Multi-tenant deployments
7
+ // receive the tenant in the URL, e.g.
8
+ // POST /api/revalidate?domain=acme.fontdue.com
9
+ // and only that tenant's cache is purged (pages and embed data share the
10
+ // per-domain tag — see fontdueEndpoint/fontdueServerConfig in ./tenant).
11
+ // Single-tenant deployments use the parameterless form and purge everything
12
+ // carrying the 'graphql' tag.
13
+
14
+ import { revalidateTag } from 'next/cache';
15
+ import { isMultiTenant, isValidDomain } from './tenant.js';
16
+ export async function POST(request) {
17
+ var _URL$searchParams$get;
18
+ const domain = (_URL$searchParams$get = new URL(request.url).searchParams.get('domain')) === null || _URL$searchParams$get === void 0 ? void 0 : _URL$searchParams$get.toLowerCase();
19
+ if (isMultiTenant) {
20
+ if (!domain || !isValidDomain(domain)) {
21
+ return Response.json({
22
+ revalidated: false,
23
+ error: 'Missing or invalid ?domain='
24
+ }, {
25
+ status: 400
26
+ });
27
+ }
28
+ revalidateTag(`graphql:${domain}`);
29
+ } else {
30
+ revalidateTag('graphql');
31
+ }
32
+ return Response.json({
33
+ revalidated: true,
34
+ domain,
35
+ now: Date.now()
36
+ });
37
+ }
@@ -0,0 +1,18 @@
1
+ import { type FontdueServerConfig } from '../relay/serverConfig.js';
2
+ export declare const isMultiTenant: boolean;
3
+ export declare function isValidDomain(domain: string): boolean;
4
+ export interface FontdueEndpoint {
5
+ /** Base URL the app's own GraphQL fetches should target. */
6
+ origin: string;
7
+ /** Headers the Fontdue server needs to resolve the tenant. */
8
+ headers: Record<string, string>;
9
+ /**
10
+ * Cache tags for the app's own fetch calls (pass as `next: { tags }`), so
11
+ * the revalidate handler (see ./revalidate) can purge one site at a time.
12
+ */
13
+ tags: string[];
14
+ }
15
+ export declare function fontdueEndpoint(domain: string): FontdueEndpoint;
16
+ export declare function fallbackSiteUrl(domain: string): string;
17
+ export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
18
+ export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
@@ -0,0 +1,105 @@
1
+ // Tenant/mode resolution for Next.js apps. An app using this adapter runs in
2
+ // one of two modes:
3
+ //
4
+ // - Single-tenant (default): NEXT_PUBLIC_FONTDUE_URL points at one Fontdue
5
+ // site and every request renders that site.
6
+ //
7
+ // - Multi-tenant (FONTDUE_MULTI_TENANT=1): the tenant is derived per request
8
+ // from the (forwarded) Host header, and one deployment serves every
9
+ // tenant. The rewrites installed by withFontdue (see ./config) turn each
10
+ // request into the /[domain]/... route tree so pages are rendered and
11
+ // cached per domain.
12
+ //
13
+ // In multi-tenant mode, GraphQL is fetched from FONTDUE_ORIGIN (the internal
14
+ // Fontdue server, e.g. http://localhost:4000 when running next to it) with
15
+ // the tenant's domain forwarded via X-Forwarded-Host, authenticated by the
16
+ // FONTDUE_PROXY_SECRET shared secret (the Fontdue server refuses to trust
17
+ // X-Forwarded-Host without it). Without FONTDUE_ORIGIN it falls back to
18
+ // fetching the tenant's public URL directly, which is useful for local
19
+ // development against live sites.
20
+ //
21
+ // The fontdue-js components embedded in pages fetch the same way: their
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.
26
+
27
+ import { setFontdueServerConfig } from '../relay/serverConfig.js';
28
+ export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
29
+ const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
30
+ const internalOrigin = process.env.FONTDUE_ORIGIN;
31
+ const proxySecret = process.env.FONTDUE_PROXY_SECRET;
32
+
33
+ // Hostnames only: letters/digits/hyphens/dots, no path or port. Anything else
34
+ // is rejected before it can reach the GraphQL fetch or the filesystem cache.
35
+ const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
36
+ export function isValidDomain(domain) {
37
+ return domain.length <= 253 && DOMAIN_RE.test(domain);
38
+ }
39
+ // Where to fetch GraphQL for a given tenant domain, plus any headers needed
40
+ // for the Fontdue server to resolve that tenant.
41
+ export function fontdueEndpoint(domain) {
42
+ const tags = ['graphql', `graphql:${domain}`];
43
+ 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
+ return {
48
+ origin: singleTenantUrl,
49
+ headers: {},
50
+ tags
51
+ };
52
+ }
53
+ if (internalOrigin) {
54
+ return {
55
+ origin: internalOrigin,
56
+ headers: {
57
+ 'x-forwarded-host': domain,
58
+ ...(proxySecret ? {
59
+ 'x-fontdue-proxy-secret': proxySecret
60
+ } : {})
61
+ },
62
+ tags
63
+ };
64
+ }
65
+ return {
66
+ origin: `https://${domain}`,
67
+ headers: {},
68
+ tags
69
+ };
70
+ }
71
+
72
+ // metadataBase / sitemap fallback when the site URL setting is empty.
73
+ export function fallbackSiteUrl(domain) {
74
+ return isMultiTenant ? `https://${domain}` : 'http://localhost:3000';
75
+ }
76
+
77
+ // Per-render config for fontdue-js's own server-side fetches: same endpoint
78
+ // and headers as the app's fetches, plus the per-domain cache tag so the
79
+ // revalidate handler purges embed data (theme config, type testers, store)
80
+ // along with the pages. ('graphql' itself is added by the network layer.)
81
+ export function fontdueServerConfig(domain) {
82
+ const {
83
+ origin,
84
+ headers
85
+ } = fontdueEndpoint(domain);
86
+ return {
87
+ url: origin,
88
+ headers,
89
+ cacheTags: [`graphql:${domain}`]
90
+ };
91
+ }
92
+
93
+ // The one call a data helper needs at the top of every page render:
94
+ // validates the request-derived domain (null means "treat as 404"), then
95
+ // points fontdue-js's server-side fetches at the tenant's endpoint for the
96
+ // rest of this render pass.
97
+ //
98
+ // Call it in a data helper every page goes through, not only in the root
99
+ // layout: an App Router soft navigation re-renders just the page segment,
100
+ // and the per-render config store starts empty on every pass.
101
+ export function configureFontdueRender(domain) {
102
+ if (!isValidDomain(domain)) return null;
103
+ setFontdueServerConfig(fontdueServerConfig(domain));
104
+ return fontdueEndpoint(domain);
105
+ }
@@ -1,9 +1,10 @@
1
1
  import { Environment, Network, RecordSource, Store, QueryResponseCache } from 'relay-runtime';
2
2
  import { handlePossibleCorsError } from '../corsError.js';
3
+ import { getFontdueServerConfig } from './serverConfig.js';
3
4
 
4
5
  // `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
5
6
  // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
6
- const version = "3.0.0-alpha6";
7
+ const version = "3.0.0-alpha8";
7
8
  const IS_SERVER = typeof window === typeof undefined;
8
9
 
9
10
  // Read env from either process.env (Node/Next.js) or import.meta.env (Vite/Astro).
@@ -37,9 +38,13 @@ const CACHE_TTL = 10 * 1000; // 10 seconds, to resolve preloaded results
37
38
 
38
39
  export function createNetworkFetch(options) {
39
40
  return async function networkFetch(request, variables) {
40
- const base = (options === null || options === void 0 ? void 0 : options.url) ?? FONTDUE_URL;
41
+ // Per-render server config (set via setFontdueServerConfig or the
42
+ // FontdueProvider server entrypoint). Resolved per call, not per
43
+ // createNetworkFetch, because module-level fetchers outlive renders.
44
+ const serverConfig = IS_SERVER ? getFontdueServerConfig() : undefined;
45
+ const base = (options === null || options === void 0 ? void 0 : options.url) ?? (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.url) ?? FONTDUE_URL;
41
46
  if (IS_SERVER && (base == null || base === '')) {
42
- throw new Error('fontdue-js: no Fontdue URL configured for server-side fetch. ' + 'Set FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL in your environment, ' + 'or pass `url` explicitly to loadSerializableQuery (or its component-level wrappers).');
47
+ throw new Error('fontdue-js: no Fontdue URL configured for server-side fetch. ' + 'Set FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL in your environment, ' + 'pass `url` to loadSerializableQuery, or call setFontdueServerConfig ' + '(or render <FontdueProvider url=…>) earlier in the server render.');
43
48
  }
44
49
  const url = `${base ?? ''}/graphql`;
45
50
  for (let attempt = 0; attempt <= 2; attempt++) {
@@ -48,6 +53,7 @@ export function createNetworkFetch(options) {
48
53
  method: 'POST',
49
54
  credentials: 'include',
50
55
  headers: {
56
+ ...(serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.headers),
51
57
  Accept: 'application/json',
52
58
  'Content-Type': 'application/json',
53
59
  'fontdue-stripe-integration': (options === null || options === void 0 ? void 0 : options.stripeIntegration) ?? STRIPE_INTEGRATION ?? 'dynamic',
@@ -59,7 +65,7 @@ export function createNetworkFetch(options) {
59
65
  }),
60
66
  // @ts-ignore
61
67
  next: {
62
- tags: ['graphql', `operation:${request.name}`]
68
+ tags: ['graphql', ...((serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.cacheTags) ?? []), `operation:${request.name}`]
63
69
  }
64
70
  });
65
71
  const json = await resp.json();
@@ -43,5 +43,7 @@ export interface SerializablePreloadedQuery<TQuery extends OperationType> {
43
43
  type RequireAllWithNull<T> = {
44
44
  [K in keyof T]-?: T[K] | null;
45
45
  };
46
- export default function loadSerializableQuery<TQuery extends OperationType>(query: GraphQLTaggedNode, variables: RequireAllWithNull<VariablesOf<TQuery>>): Promise<SerializablePreloadedQuery<TQuery>>;
46
+ export default function loadSerializableQuery<TQuery extends OperationType>(query: GraphQLTaggedNode, variables: RequireAllWithNull<VariablesOf<TQuery>>, options?: {
47
+ url?: string;
48
+ }): Promise<SerializablePreloadedQuery<TQuery>>;
47
49
  export {};
@@ -40,9 +40,9 @@ import { createNetworkFetch } from './environment.js';
40
40
  // Call into raw network fetch to get serializable GraphQL query response
41
41
  // This response will be sent to the client to "warm" the QueryResponseCache
42
42
  // to avoid the client fetches.
43
- export default async function loadSerializableQuery(query, variables) {
43
+ export default async function loadSerializableQuery(query, variables, options) {
44
44
  if (!('params' in query)) throw new Error('Params not found in query, is it a fragment instead of a query?');
45
- const fetcher = createNetworkFetch();
45
+ const fetcher = createNetworkFetch(options);
46
46
  const response = await fetcher(query.params, variables);
47
47
  return {
48
48
  params: query.params,
@@ -0,0 +1,10 @@
1
+ export interface FontdueServerConfig {
2
+ /** Base URL for server-side GraphQL fetches, e.g. https://acme.fontdue.com */
3
+ url?: string;
4
+ /** Extra headers sent with every server-side GraphQL fetch. */
5
+ headers?: Record<string, string>;
6
+ /** Extra Next.js fetch cache tags applied to every server-side GraphQL fetch. */
7
+ cacheTags?: string[];
8
+ }
9
+ export declare function setFontdueServerConfig(config: FontdueServerConfig): void;
10
+ export declare function getFontdueServerConfig(): FontdueServerConfig | undefined;