fontdue-js 3.0.0-alpha7 → 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.
@@ -0,0 +1,169 @@
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 { notFound } from 'next/navigation';
28
+ import { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
29
+ export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
30
+ const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
31
+ const internalOrigin = process.env.FONTDUE_ORIGIN;
32
+ const proxySecret = process.env.FONTDUE_PROXY_SECRET;
33
+
34
+ // Hostnames only: letters/digits/hyphens/dots, no path or port. Anything else
35
+ // is rejected before it can reach the GraphQL fetch or the filesystem cache.
36
+ const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
37
+ export function isValidDomain(domain) {
38
+ return domain.length <= 253 && DOMAIN_RE.test(domain);
39
+ }
40
+ // Where to fetch GraphQL for a given tenant domain, plus any headers needed
41
+ // for the Fontdue server to resolve that tenant.
42
+ export function fontdueEndpoint(domain) {
43
+ const tags = ['graphql', `graphql:${domain}`];
44
+ if (!isMultiTenant) {
45
+ return {
46
+ domain,
47
+ origin: requireSingleTenantUrl(),
48
+ headers: {},
49
+ tags
50
+ };
51
+ }
52
+ if (internalOrigin) {
53
+ return {
54
+ domain,
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
+ domain,
67
+ origin: `https://${domain}`,
68
+ headers: {},
69
+ tags
70
+ };
71
+ }
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;
77
+ }
78
+
79
+ // Per-render config for fontdue-js's own server-side fetches: same endpoint
80
+ // and headers as the app's fetches, plus the per-domain cache tag so the
81
+ // revalidate handler purges embed data (theme config, type testers, store)
82
+ // along with the pages. ('graphql' itself is added by the network layer.)
83
+ export function fontdueServerConfig(domain) {
84
+ const {
85
+ origin,
86
+ headers
87
+ } = fontdueEndpoint(domain);
88
+ return {
89
+ url: origin,
90
+ headers,
91
+ cacheTags: [`graphql:${domain}`],
92
+ domain
93
+ };
94
+ }
95
+
96
+ // The one call a data helper needs at the top of every page render:
97
+ // validates the request-derived domain (null means "treat as 404"), then
98
+ // points fontdue-js's server-side fetches at the tenant's endpoint for the
99
+ // rest of this render pass.
100
+ //
101
+ // Call it in a data helper every page goes through, not only in the root
102
+ // layout: an App Router soft navigation re-renders just the page segment,
103
+ // and the per-render config store starts empty on every pass.
104
+ export function configureFontdueRender(domain) {
105
+ if (!isValidDomain(domain)) return null;
106
+ setFontdueServerConfig(fontdueServerConfig(domain));
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 [];
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-alpha7";
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/dist/vite.js CHANGED
@@ -79,7 +79,9 @@ function getFontdueJsSubpaths() {
79
79
  for (const key of Object.keys(pkg.exports ?? {})) {
80
80
  if (!key.startsWith('./')) continue;
81
81
  if (key.endsWith('.css')) continue;
82
+ // Build-config and server-only entrypoints never reach a browser bundle.
82
83
  if (key === './vite') continue;
84
+ if (key === './next' || key.startsWith('./next/')) continue;
83
85
  out.push(`fontdue-js/${key.slice(2)}`);
84
86
  }
85
87
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.0.0-alpha7",
3
+ "version": "3.0.0-alpha9",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",
@@ -82,7 +82,10 @@
82
82
  },
83
83
  "exports": {
84
84
  ".": "./dist/index.js",
85
- "./server": "./dist/relay/serverConfig.js",
85
+ "./next": "./dist/next/index.js",
86
+ "./next/config": "./dist/next/config.js",
87
+ "./next/revalidate": "./dist/next/revalidate.js",
88
+ "./next/image-loader": "./dist/next/image-loader.js",
86
89
  "./fontdue.css": "./dist/fontdue.css",
87
90
  "./BuyButton": {
88
91
  "react-server": "./dist/components/BuyButton/index.server.js",
@@ -0,0 +1,6 @@
1
+ // Minimal declaration so src/next/revalidate.ts type-checks without `next`
2
+ // installed (it's the host app's dependency; this package only ever runs the
3
+ // import inside a Next.js server).
4
+ declare module 'next/cache' {
5
+ export function revalidateTag(tag: string): void;
6
+ }
@@ -0,0 +1,6 @@
1
+ // Minimal declaration so src/next/tenant.ts type-checks without `next`
2
+ // installed (it's the host app's dependency; this package only ever runs the
3
+ // import inside a Next.js server).
4
+ declare module 'next/navigation' {
5
+ export function notFound(): never;
6
+ }
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__'),