Skip to content

Commit 93751a4

Browse files
committed
fix(platform): defer Node-only config behind async thunks
Webpack bundles the top level of each platform's next.platform.config.mjs into the server output (because apps/site/mdx/plugins.mjs reads .mdx from it), which meant the Cloudflare build shipped `createRequire(...).resolve(...)` and `await getDeploymentId()` into the worker runtime — failing at request time with `m.resolve is not a function`. Moving the heavy, build-only pieces (`@opennextjs/cloudflare` imports, `require.resolve`, VERCEL_URL computation) behind async thunks keeps the module's top level free of Node-runtime-only code. Pair with the webpack `conditionNames` wiring so `#platform/*` actually resolves to the target variant at bundle time.
1 parent ed3b670 commit 93751a4

4 files changed

Lines changed: 80 additions & 52 deletions

File tree

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,53 @@
1-
import { createRequire } from 'node:module';
2-
import { relative } from 'node:path';
3-
4-
import { getDeploymentId } from '@opennextjs/cloudflare';
5-
6-
const require = createRequire(import.meta.url);
7-
81
/**
92
* Platform config contributed by the Cloudflare deployment target.
103
*
11-
* Consumed by `apps/site/next.config.mjs` via the platform-config loader.
12-
* Must export a default `{ nextConfig, aliases, images }` shape — any of
13-
* which may be omitted when the platform has nothing to contribute.
4+
* Consumed by `apps/site/next.config.mjs` via the `#platform/*` import
5+
* map. Heavy, Node-only bits (`@opennextjs/cloudflare`, `createRequire`,
6+
* `require.resolve`) live inside async thunks so that webpack — which
7+
* bundles the top level of this module into the server output when
8+
* `apps/site/mdx/plugins.mjs` reads `.mdx` — never drags them into the
9+
* worker runtime.
1410
*
1511
* @type {import('../site/next.platform.config').PlatformConfig}
1612
*/
1713
export default {
18-
nextConfig: {
19-
// Skew protection: Cloudflare routes requests by deploymentId so that
20-
// a client and the worker stay in sync across rolling deploys.
21-
deploymentId: await getDeploymentId(),
22-
},
2314
aliases: {
2415
'@platform/analytics': '@node-core/platform-cloudflare/analytics',
2516
'@platform/instrumentation':
2617
'@node-core/platform-cloudflare/instrumentation',
2718
},
28-
images: {
29-
// Route optimized images through Cloudflare's Images service via the
30-
// custom loader. `remotePatterns` do NOT apply here — Cloudflare
31-
// enforces allowed origins at the edge instead.
32-
loader: 'custom',
33-
// Next.js joins `loaderFile` onto its own cwd (apps/site), so pass a
34-
// path relative to that cwd rather than an absolute one. Resolving via
35-
// `require.resolve` avoids the `new URL(..., import.meta.url)` pattern,
36-
// which webpack rewrites as an asset reference and mangles at runtime.
37-
loaderFile: relative(
38-
process.cwd(),
39-
require.resolve('@node-core/platform-cloudflare/image-loader')
40-
),
41-
},
4219
mdx: {
4320
// Cloudflare workers can't load `shiki/wasm` via `WebAssembly.instantiate`
4421
// with custom imports (blocked for security), so fall back to the
4522
// JavaScript RegEx engine. Twoslash also needs a VFS we don't have here.
4623
wasm: false,
4724
twoslash: false,
4825
},
26+
nextConfig: async () => {
27+
const { getDeploymentId } = await import('@opennextjs/cloudflare');
28+
return {
29+
// Skew protection: Cloudflare routes requests by deploymentId so that
30+
// a client and the worker stay in sync across rolling deploys.
31+
deploymentId: await getDeploymentId(),
32+
};
33+
},
34+
images: async () => {
35+
const { createRequire } = await import('node:module');
36+
const { relative } = await import('node:path');
37+
const require = createRequire(import.meta.url);
38+
return {
39+
// Route optimized images through Cloudflare's Images service via the
40+
// custom loader. `remotePatterns` do NOT apply here — Cloudflare
41+
// enforces allowed origins at the edge instead.
42+
loader: 'custom',
43+
// Next.js joins `loaderFile` onto its own cwd (apps/site), so pass a
44+
// path relative to that cwd rather than an absolute one. Resolving via
45+
// `require.resolve` avoids the `new URL(..., import.meta.url)` pattern,
46+
// which webpack rewrites as an asset reference and mangles at runtime.
47+
loaderFile: relative(
48+
process.cwd(),
49+
require.resolve('@node-core/platform-cloudflare/image-loader')
50+
),
51+
};
52+
},
4953
};

apps/site/next.config.mjs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import createNextIntlPlugin from 'next-intl/plugin';
44

55
import platform from '#platform/next.platform.config';
66

7-
import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs';
7+
import {
8+
BASE_PATH,
9+
ENABLE_STATIC_EXPORT,
10+
DEPLOY_TARGET,
11+
} from './next.constants.mjs';
812
import { getImagesConfig } from './next.image.config.mjs';
913
import { redirects, rewrites } from './next.rewrites.mjs';
1014

@@ -17,7 +21,7 @@ const nextConfig = {
1721
// We allow the BASE_PATH to be overridden in case that the Website
1822
// is being built on a subdirectory (e.g. /nodejs-website)
1923
basePath: BASE_PATH,
20-
images: getImagesConfig(platform.images),
24+
images: getImagesConfig(await platform.images?.()),
2125
serverExternalPackages: ['twoslash'],
2226
// Transpile platform packages' TSX/TS sources when they're pulled in via
2327
// the `@platform/*` aliases from the active `next.platform.config.mjs`.
@@ -78,12 +82,21 @@ const nextConfig = {
7882
},
7983
// Provide Turbopack Aliases for Platform Resolution
8084
turbopack: { resolveAlias: platform.aliases },
81-
// Provide Webpack Aliases for Platform Resolution
85+
// Provide Webpack Aliases for Platform Resolution. The active deployment
86+
// target is also surfaced to the resolver via `conditionNames` so that
87+
// `#platform/*` subpath imports in `package.json` pick the matching
88+
// branch when webpack bundles server code.
8289
webpack: ({ resolve, ...config }) => ({
8390
...config,
84-
resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } },
91+
resolve: {
92+
...resolve,
93+
alias: { ...resolve.alias, ...platform.aliases },
94+
conditionNames: resolve.conditionNames
95+
.concat(DEPLOY_TARGET)
96+
.filter(Boolean),
97+
},
8598
}),
86-
...platform.nextConfig,
99+
...(await platform.nextConfig?.()),
87100
};
88101

89102
const withNextIntl = createNextIntlPlugin('./i18n.tsx');

apps/site/next.platform.config.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@ type PlatformNextConfig = Pick<NextConfig, 'deploymentId' | 'env'>;
77
/**
88
* Shared platform-config contract consumed by `apps/site/next.config.mjs`
99
* and implemented by each `@node-core/platform-<target>` package.
10+
*
11+
* `nextConfig` and `images` are async thunks so that platform modules
12+
* that depend on Node-only tooling (e.g. `@opennextjs/cloudflare`,
13+
* `require.resolve`) can keep those imports out of the module's
14+
* top-level. Webpack bundles the top level of this module into the
15+
* server output; deferring heavy work into function bodies keeps the
16+
* worker runtime free of build-only code.
1017
*/
1118
export type PlatformConfig = {
1219
aliases?: Record<string, string>;
13-
images?: NextConfig['images'];
20+
images?: () => Promise<NextConfig['images']>;
1421
mdx?: PlatformMdxConfig;
15-
nextConfig?: PlatformNextConfig;
22+
nextConfig?: () => Promise<PlatformNextConfig>;
1623
};
1724

1825
declare const config: PlatformConfig;
Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
/**
22
* Platform config contributed by the Vercel deployment target.
33
*
4-
* Consumed by `apps/site/next.config.mjs` via the platform-config loader.
5-
* Must export a default `{ nextConfig, aliases, images }` shape — any of
6-
* which may be omitted when the platform has nothing to contribute.
4+
* Consumed by `apps/site/next.config.mjs` via the `#platform/*` import
5+
* map. Heavy, Node-only bits live inside async thunks so that webpack —
6+
* which bundles the top level of this module into the server output
7+
* when `apps/site/mdx/plugins.mjs` reads `.mdx` — never drags them into
8+
* the Node server runtime (keeps bundles lean and parity with
9+
* Cloudflare's contract).
710
*
811
* @type {import('../site/next.platform.config').PlatformConfig}
912
*/
10-
const VERCEL_URL = process.env.VERCEL_URL
11-
? `https://${process.env.VERCEL_URL}`
12-
: undefined;
13-
1413
export default {
15-
nextConfig: {
16-
// Expose Vercel's auto-assigned deployment URL as a platform-agnostic
17-
// `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single
18-
// canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins.
19-
env: {
20-
NEXT_PUBLIC_BASE_URL:
21-
process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL || '',
22-
},
23-
},
2414
aliases: {
2515
'@platform/analytics': '@node-core/platform-vercel/analytics',
2616
'@platform/instrumentation': '@node-core/platform-vercel/instrumentation',
@@ -31,4 +21,18 @@ export default {
3121
wasm: true,
3222
twoslash: true,
3323
},
24+
nextConfig: async () => {
25+
const VERCEL_URL = process.env.VERCEL_URL
26+
? `https://${process.env.VERCEL_URL}`
27+
: undefined;
28+
return {
29+
// Expose Vercel's auto-assigned deployment URL as a platform-agnostic
30+
// `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single
31+
// canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins.
32+
env: {
33+
NEXT_PUBLIC_BASE_URL:
34+
process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL || '',
35+
},
36+
};
37+
},
3438
};

0 commit comments

Comments
 (0)