Skip to content

Commit 641d0b9

Browse files
committed
refactor: split platform-specific code into platform-vercel and platform-cloudflare packages
Why: Upgrading Next.js or any Vercel dep was gated by OpenNext's compatibility window, platform-specific runtime branches (VERCEL_ENV, OPEN_NEXT_CLOUDFLARE, 'Cloudflare' in global) were scattered across the codebase, and apps/site carried deps that only one deployment used. This extracts all platform-specific integrations into dedicated workspace packages selected at build time via NEXT_PUBLIC_DEPLOY_TARGET: - @node-core/platform-vercel owns Vercel analytics, speed insights, and OTel instrumentation. - @node-core/platform-cloudflare owns the OpenNext config, Wrangler config, worker entrypoint (with Sentry), image loader, and the MDX flags needed to skip WASM/Twoslash on Cloudflare workers. Each adapter exports a next.platform.config.mjs with a { nextConfig, aliases, images, mdx } contract that apps/site merges into its Next.js config, MDX plugins, and Playwright config via dynamic import. A no-op apps/site/next.platform.config.mjs and apps/site/playwright.platform.config.mjs keep the standalone pnpm dev / pnpm build paths working when no target is set. Runtime platform detection (PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW, 'Cloudflare' in global, OPEN_NEXT_CLOUDFLARE branches) is replaced with NEXT_PUBLIC_DEPLOY_TARGET selection so the apps/site source tree has no platform conditionals left. Docs updated: docs/technologies.md documents the NEXT_PUBLIC_DEPLOY_TARGET contract; docs/cloudflare-build-and-deployment.md points at the new package paths; CODEOWNERS moved the Wrangler / OpenNext ownership to the new packages.
1 parent d7b6e7e commit 641d0b9

42 files changed

Lines changed: 583 additions & 254 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ turbo.json @nodejs/nodejs-website @nodejs/web-infra
2727
crowdin.yml @nodejs/web-infra
2828
apps/site/redirects.json @nodejs/web-infra
2929
apps/site/site.json @nodejs/web-infra
30-
apps/site/wrangler.jsonc @nodejs/web-infra
31-
apps/site/open-next.config.ts @nodejs/web-infra
30+
packages/platform-cloudflare/wrangler.jsonc @nodejs/web-infra
31+
packages/platform-cloudflare/open-next.config.ts @nodejs/web-infra
32+
packages/platform-cloudflare/next.platform.config.mjs @nodejs/web-infra
33+
packages/platform-vercel/next.platform.config.mjs @nodejs/web-infra
3234
apps/site/redirects.json @nodejs/web-infra
3335

3436
# Critical Documents

.github/workflows/playwright-cloudflare-open-next.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
working-directory: apps/site
5555
run: node --run playwright
5656
env:
57-
PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true
57+
NEXT_PUBLIC_DEPLOY_TARGET: cloudflare
5858
PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787
5959

6060
- name: Upload Playwright test results
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@platform/analytics';

apps/site/app/[locale]/layout.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ import { NextIntlClientProvider } from 'next-intl';
44

55
import BaseLayout from '#site/layouts/Base';
66
import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts';
7-
import BodyEnd from '#site/platform/body-end';
87
import { ThemeProvider } from '#site/providers/themeProvider';
98

10-
import type { FC, PropsWithChildren } from 'react';
9+
import type { FC, PropsWithChildren, ReactNode } from 'react';
1110

1211
import '#site/styles/index.css';
1312

1413
const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable);
1514

1615
type RootLayoutProps = PropsWithChildren<{
1716
params: Promise<{ locale: string }>;
17+
analytics: ReactNode;
1818
}>;
1919

20-
const RootLayout: FC<RootLayoutProps> = async ({ children, params }) => {
20+
const RootLayout: FC<RootLayoutProps> = async ({
21+
children,
22+
analytics,
23+
params,
24+
}) => {
2125
const { locale } = await params;
2226

2327
const { langDir, hrefLang } =
@@ -44,7 +48,7 @@ const RootLayout: FC<RootLayoutProps> = async ({ children, params }) => {
4448
href="https://social.lfx.dev/@nodejs"
4549
/>
4650

47-
<BodyEnd />
51+
{analytics}
4852
</body>
4953
</html>
5054
);

apps/site/eslint.config.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,7 @@ import baseConfig from '../../eslint.config.js';
66

77
export default baseConfig.concat([
88
{
9-
ignores: [
10-
'pages/en/blog/**/*.{md,mdx}/**',
11-
'public',
12-
'next-env.d.ts',
13-
// The worker entrypoint is bundled by wrangler, not tsc. Its imports
14-
// trigger a tsc crash (see tsconfig.json), so it is excluded from both
15-
// type checking and ESLint's type-aware linting.
16-
'cloudflare/worker-entrypoint.ts',
17-
],
9+
ignores: ['pages/en/blog/**/*.{md,mdx}/**', 'public', 'next-env.d.ts'],
1810
},
1911

2012
eslintReact.configs['recommended-typescript'],

apps/site/instrumentation.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
export async function register() {
2-
if (process.env.NEXT_PUBLIC_DEPLOY_TARGET === 'vercel') {
3-
const { registerOTel } = await import('@vercel/otel');
4-
registerOTel({ serviceName: 'nodejs-org' });
5-
}
6-
}
1+
export { register } from '@platform/instrumentation';

apps/site/mdx/plugins.mjs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@ import rehypeSlug from 'rehype-slug';
77
import remarkGfm from 'remark-gfm';
88
import readingTime from 'remark-reading-time';
99

10+
import { DEPLOY_TARGET } from '../next.constants.mjs';
1011
import remarkTableTitles from '../util/table';
1112

12-
// Shiki is created out here to avoid an async rehype plugin
13-
const singletonShiki = await rehypeShikiji({
14-
// We use the faster WASM engine on the server instead of the web-optimized version.
15-
//
16-
// Currently we fall back to the JavaScript RegEx engine
17-
// on Cloudflare workers because `shiki/wasm` requires loading via
18-
// `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support
19-
// for security reasons.
20-
wasm: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare',
13+
// Load MDX overrides contributed by the active deployment target. Keeps
14+
// this module free of platform-specific branches — each platform owns
15+
// its own `{ wasm, twoslash }` defaults via `next.platform.config.mjs`,
16+
// with the in-repo default config serving as the standalone fallback.
17+
const { default: platform } = DEPLOY_TARGET
18+
? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`)
19+
: await import('../next.platform.config.mjs');
2120

22-
// TODO(@avivkeller): Find a way to enable Twoslash w/ a VFS on Cloudflare
23-
twoslash: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare',
24-
});
21+
// Shiki is created out here to avoid an async rehype plugin
22+
const singletonShiki = await rehypeShikiji(platform.mdx);
2523

2624
/**
2725
* Provides all our Rehype Plugins that are used within MDX

apps/site/next.config.mjs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
23
import createNextIntlPlugin from 'next-intl/plugin';
34

45
import {
@@ -9,17 +10,16 @@ import {
910
import { getImagesConfig } from './next.image.config.mjs';
1011
import { redirects, rewrites } from './next.rewrites.mjs';
1112

12-
const getDeploymentId = async () => {
13-
if (DEPLOY_TARGET !== 'cloudflare') {
14-
return undefined;
15-
}
16-
17-
// If we're building for the Cloudflare deployment we want to set
18-
// an appropriate deploymentId (needed for skew protection)
19-
const openNextAdapter = await import('@opennextjs/cloudflare');
20-
21-
return openNextAdapter.getDeploymentId();
22-
};
13+
/**
14+
* Loads the deployment platform's `next.platform.config.mjs` — falling back
15+
* to the local no-op when no platform is active. Each platform package
16+
* (`@node-core/platform-<target>`) owns its own file and contributes
17+
* `{ nextConfig, aliases, images }`. Adding a new platform only means
18+
* creating a new `@node-core/platform-<target>` package.
19+
*/
20+
const { default: platform } = DEPLOY_TARGET
21+
? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`)
22+
: await import('./next.platform.config.mjs');
2323

2424
/** @type {import('next').NextConfig} */
2525
const nextConfig = {
@@ -30,9 +30,14 @@ const nextConfig = {
3030
// We allow the BASE_PATH to be overridden in case that the Website
3131
// is being built on a subdirectory (e.g. /nodejs-website)
3232
basePath: BASE_PATH,
33-
// Vercel/Next.js Image Optimization Settings
34-
images: getImagesConfig(),
33+
images: getImagesConfig(platform.images),
3534
serverExternalPackages: ['twoslash'],
35+
// Transpile platform packages' TSX/TS sources when they're pulled in via
36+
// the `@platform/*` aliases from the active `next.platform.config.mjs`.
37+
transpilePackages: [
38+
'@node-core/platform-vercel',
39+
'@node-core/platform-cloudflare',
40+
],
3641
outputFileTracingIncludes: {
3742
// Twoslash needs TypeScript declarations to function, and, by default, Next.js
3843
// strips them for brevity. Therefore, they must be explicitly included.
@@ -84,8 +89,16 @@ const nextConfig = {
8489
// Faster Development Servers with Turbopack
8590
turbopackFileSystemCacheForDev: true,
8691
},
87-
deploymentId: await getDeploymentId(),
92+
// Provide Turbopack Aliases for Platform Resolution
93+
turbopack: { resolveAlias: platform.aliases },
94+
// Provide Webpack Aliases for Platform Resolution
95+
webpack: ({ resolve, ...config }) => ({
96+
...config,
97+
resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } },
98+
}),
99+
...platform.nextConfig,
88100
};
89101

90102
const withNextIntl = createNextIntlPlugin('./i18n.tsx');
103+
91104
export default withNextIntl(nextConfig);

apps/site/next.constants.mjs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,14 @@ export const ENABLE_STATIC_EXPORT_LOCALE =
4343
process.env.NEXT_PUBLIC_STATIC_EXPORT_LOCALE === true;
4444

4545
/**
46-
* This is used for any place that requires the full canonical URL path for the Node.js Website (and its deployment), such as for example, the Node.js RSS Feed.
46+
* The full canonical URL of the deployed Website (used e.g. for the RSS feed).
4747
*
48-
* This variable can either come from the Vercel Deployment as `NEXT_PUBLIC_VERCEL_URL` or from the `NEXT_PUBLIC_BASE_URL` Environment Variable that is manually defined
49-
* by us if necessary. Otherwise it will fallback to the default Node.js Website URL.
50-
*
51-
* @TODO: We should get rid of needing to rely on `VERCEL_URL` for deployment URL.
52-
*
53-
* @see https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#framework-environment-variables
48+
* Platform-specific base URLs (such as Vercel's `VERCEL_URL`) are inlined into
49+
* `NEXT_PUBLIC_BASE_URL` at build time by each platform's `next.platform.config.mjs`,
50+
* keeping this module free of platform-specific branches.
5451
*/
55-
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
56-
? process.env.NEXT_PUBLIC_BASE_URL
57-
: process.env.VERCEL_URL
58-
? `https://${process.env.VERCEL_URL}`
59-
: 'https://nodejs.org';
52+
export const BASE_URL =
53+
process.env.NEXT_PUBLIC_BASE_URL || 'https://nodejs.org';
6054

6155
/**
6256
* This is used for any place that requires the Node.js distribution URL (which by default is nodejs.org/dist)

apps/site/next.image.config.mjs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DEPLOY_TARGET, ENABLE_STATIC_EXPORT } from './next.constants.mjs';
1+
import { ENABLE_STATIC_EXPORT } from './next.constants.mjs';
22

33
const remotePatterns = [
44
'https://avatars.githubusercontent.com/**',
@@ -8,18 +8,16 @@ const remotePatterns = [
88
'https://website-assets.oramasearch.com/**',
99
];
1010

11-
export const getImagesConfig = () => {
12-
if (DEPLOY_TARGET === 'cloudflare') {
13-
// If we're building for the Cloudflare deployment we want to use the custom cloudflare image loader
14-
//
15-
// Important: The custom loader ignores `remotePatterns` as those are configured as allowed source origins
16-
// (https://developers.cloudflare.com/images/transform-images/sources/)
17-
// in the Cloudflare dashboard itself instead (to the exact same values present in `remotePatterns` above).
18-
//
19-
return {
20-
loader: 'custom',
21-
loaderFile: './cloudflare/image-loader.ts',
22-
};
11+
/**
12+
* Returns the Next.js `images` configuration, preferring any platform-provided
13+
* override (e.g. Cloudflare's custom loader) over the default remotePatterns +
14+
* static-export unoptimized defaults.
15+
*
16+
* @param {import('next').NextConfig['images']} [platformImagesOverride]
17+
*/
18+
export const getImagesConfig = platformImagesOverride => {
19+
if (platformImagesOverride) {
20+
return platformImagesOverride;
2321
}
2422

2523
return {

0 commit comments

Comments
 (0)