From d7b6e7e5ce2d59eb09842130c11947e5ad0baf26 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 13:07:30 -0300 Subject: [PATCH 01/16] refactor: consolidate platform detection into NEXT_PUBLIC_DEPLOY_TARGET Platform detection in apps/site was spread across three different mechanisms: `VERCEL_ENV`, `OPEN_NEXT_CLOUDFLARE`, and a runtime `'Cloudflare' in global` check in the MDX plugin. Each one carried its own caveats (the global check blocked tree-shaking; the two env vars couldn't be compared to "is this build neutral / Vercel / Cloudflare?" uniformly), and Vercel analytics were imported eagerly in the root layout even on Cloudflare builds. Replace all three with a single `NEXT_PUBLIC_DEPLOY_TARGET` env var set by each deployment wrapper (`vercel.json` -> `vercel`, `open-next.config.ts` -> `cloudflare`). The `NEXT_PUBLIC_` prefix lets Next.js inline the value at build time so platform-specific branches are dead-code-eliminated from non-matching bundles. Extract the Vercel Analytics + SpeedInsights injection into a `platform/body-end` slot. The core layout renders `` and the slot's dynamic import resolves only on Vercel builds, so the Vercel modules no longer ship to Cloudflare. --- apps/site/app/[locale]/layout.tsx | 11 ++--------- apps/site/instrumentation.ts | 3 +-- apps/site/mdx/plugins.mjs | 10 ++-------- apps/site/next.config.mjs | 21 ++++++++++++--------- apps/site/next.constants.cloudflare.mjs | 12 ------------ apps/site/next.constants.mjs | 13 +++++++++---- apps/site/next.image.config.mjs | 5 ++--- apps/site/open-next.config.ts | 3 ++- apps/site/package.json | 2 +- apps/site/platform/body-end.tsx | 17 +++++++++++++++++ apps/site/platform/body-end.vercel.tsx | 11 +++++++++++ apps/site/turbo.json | 4 ++++ apps/site/vercel.json | 1 + docs/cloudflare-build-and-deployment.md | 2 +- 14 files changed, 65 insertions(+), 50 deletions(-) delete mode 100644 apps/site/next.constants.cloudflare.mjs create mode 100644 apps/site/platform/body-end.tsx create mode 100644 apps/site/platform/body-end.vercel.tsx diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index 5e1e440c5b974..44f6e8142f736 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -1,12 +1,10 @@ import { availableLocales, defaultLocale } from '@node-core/website-i18n'; -import { Analytics } from '@vercel/analytics/react'; -import { SpeedInsights } from '@vercel/speed-insights/next'; import classNames from 'classnames'; import { NextIntlClientProvider } from 'next-intl'; import BaseLayout from '#site/layouts/Base'; -import { VERCEL_ENV } from '#site/next.constants.mjs'; import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts'; +import BodyEnd from '#site/platform/body-end'; import { ThemeProvider } from '#site/providers/themeProvider'; import type { FC, PropsWithChildren } from 'react'; @@ -46,12 +44,7 @@ const RootLayout: FC = async ({ children, params }) => { href="https://social.lfx.dev/@nodejs" /> - {VERCEL_ENV && ( - <> - - - - )} + ); diff --git a/apps/site/instrumentation.ts b/apps/site/instrumentation.ts index 86097a48800b1..426884e92124e 100644 --- a/apps/site/instrumentation.ts +++ b/apps/site/instrumentation.ts @@ -1,6 +1,5 @@ export async function register() { - if (!('Cloudflare' in globalThis)) { - // Note: we don't need to set up the Vercel OTEL if the application is running on Cloudflare + if (process.env.NEXT_PUBLIC_DEPLOY_TARGET === 'vercel') { const { registerOTel } = await import('@vercel/otel'); registerOTel({ serviceName: 'nodejs-org' }); } diff --git a/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index 02166643aaf26..4a4c8258366ac 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -9,12 +9,6 @@ import readingTime from 'remark-reading-time'; import remarkTableTitles from '../util/table'; -// TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment -// variable for detection instead of current method, which will enable better -// tree-shaking. -// Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615 -const OPEN_NEXT_CLOUDFLARE = 'Cloudflare' in global; - // Shiki is created out here to avoid an async rehype plugin const singletonShiki = await rehypeShikiji({ // We use the faster WASM engine on the server instead of the web-optimized version. @@ -23,10 +17,10 @@ const singletonShiki = await rehypeShikiji({ // on Cloudflare workers because `shiki/wasm` requires loading via // `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support // for security reasons. - wasm: !OPEN_NEXT_CLOUDFLARE, + wasm: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare', // TODO(@avivkeller): Find a way to enable Twoslash w/ a VFS on Cloudflare - twoslash: !OPEN_NEXT_CLOUDFLARE, + twoslash: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare', }); /** diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 40d1f89e87cd3..c20134ec52a83 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -1,21 +1,24 @@ 'use strict'; import createNextIntlPlugin from 'next-intl/plugin'; -import { OPEN_NEXT_CLOUDFLARE } from './next.constants.cloudflare.mjs'; -import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { + BASE_PATH, + DEPLOY_TARGET, + ENABLE_STATIC_EXPORT, +} from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; const getDeploymentId = async () => { - if (OPEN_NEXT_CLOUDFLARE) { - // If we're building for the Cloudflare deployment we want to set - // an appropriate deploymentId (needed for skew protection) - const openNextAdapter = await import('@opennextjs/cloudflare'); - - return openNextAdapter.getDeploymentId(); + if (DEPLOY_TARGET !== 'cloudflare') { + return undefined; } - return undefined; + // If we're building for the Cloudflare deployment we want to set + // an appropriate deploymentId (needed for skew protection) + const openNextAdapter = await import('@opennextjs/cloudflare'); + + return openNextAdapter.getDeploymentId(); }; /** @type {import('next').NextConfig} */ diff --git a/apps/site/next.constants.cloudflare.mjs b/apps/site/next.constants.cloudflare.mjs deleted file mode 100644 index a87b3ed2a5214..0000000000000 --- a/apps/site/next.constants.cloudflare.mjs +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -/** - * Whether the build process is targeting the Cloudflare open-next build or not. - * - * TODO: The `OPEN_NEXT_CLOUDFLARE` environment variable is being - * defined in the worker building script, ideally the open-next - * adapter should set it itself when it invokes the Next.js build - * process, once it does that remove the manual `OPEN_NEXT_CLOUDFLARE` - * definition in the package.json script. - */ -export const OPEN_NEXT_CLOUDFLARE = process.env.OPEN_NEXT_CLOUDFLARE; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index f621472d3c309..e9ab137b828d7 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -6,13 +6,18 @@ export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; /** - * This is used for telling Next.js if the Website is deployed on Vercel + * Identifies the deployment platform the site is being built for. * - * Can be used for conditionally enabling features that we know are Vercel only + * Set by the deployment wrapper at build time: `vercel.json`'s `buildCommand` + * sets `vercel`, `open-next.config.ts`'s `buildCommand` sets `cloudflare`. + * Unset for standalone builds (local dev, static export). * - * @see https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL_ENV + * The `NEXT_PUBLIC_` prefix makes Next.js inline the value at build time, + * enabling dead-code elimination of platform-specific branches. + * + * @type {'vercel' | 'cloudflare' | undefined} */ -export const VERCEL_ENV = process.env.VERCEL_ENV || undefined; +export const DEPLOY_TARGET = process.env.NEXT_PUBLIC_DEPLOY_TARGET; /** * This is used for telling Next.js to do a Static Export Build of the Website diff --git a/apps/site/next.image.config.mjs b/apps/site/next.image.config.mjs index 519523ca2aafe..1097d7e0599c3 100644 --- a/apps/site/next.image.config.mjs +++ b/apps/site/next.image.config.mjs @@ -1,5 +1,4 @@ -import { OPEN_NEXT_CLOUDFLARE } from './next.constants.cloudflare.mjs'; -import { ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { DEPLOY_TARGET, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; const remotePatterns = [ 'https://avatars.githubusercontent.com/**', @@ -10,7 +9,7 @@ const remotePatterns = [ ]; export const getImagesConfig = () => { - if (OPEN_NEXT_CLOUDFLARE) { + if (DEPLOY_TARGET === 'cloudflare') { // If we're building for the Cloudflare deployment we want to use the custom cloudflare image loader // // Important: The custom loader ignores `remotePatterns` as those are configured as allowed source origins diff --git a/apps/site/open-next.config.ts b/apps/site/open-next.config.ts index 03f1c01730dc2..e627475ad60ad 100644 --- a/apps/site/open-next.config.ts +++ b/apps/site/open-next.config.ts @@ -20,7 +20,8 @@ const cloudflareConfig = defineCloudflareConfig({ const openNextConfig: OpenNextConfig = { ...cloudflareConfig, - buildCommand: 'pnpm build --webpack', + buildCommand: + 'cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm build --webpack', cloudflare: { skewProtection: { enabled: true }, }, diff --git a/apps/site/package.json b/apps/site/package.json index 1fd73ab23c9be..c908d4e77cff3 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -6,7 +6,7 @@ "build": "cross-env NODE_NO_WARNINGS=1 next build", "build:blog-data": "cross-env NODE_NO_WARNINGS=1 node ./scripts/blog-data/index.mjs", "build:blog-data:watch": "node --watch --watch-path=pages/en/blog ./scripts/blog-data/index.mjs", - "cloudflare:build:worker": "OPEN_NEXT_CLOUDFLARE=true opennextjs-cloudflare build", + "cloudflare:build:worker": "opennextjs-cloudflare build", "cloudflare:deploy": "opennextjs-cloudflare deploy", "cloudflare:preview": "wrangler dev", "predeploy": "node --run build:blog-data", diff --git a/apps/site/platform/body-end.tsx b/apps/site/platform/body-end.tsx new file mode 100644 index 0000000000000..64c44e365a56e --- /dev/null +++ b/apps/site/platform/body-end.tsx @@ -0,0 +1,17 @@ +/** + * Per-platform "body end" slot. Deployment targets can inject DOM at the + * end of the document body (analytics, tracking scripts, etc.) without + * adding platform-specific imports to the core layout. + * + * `NEXT_PUBLIC_DEPLOY_TARGET` is inlined by Next.js at build time, so on + * non-matching platforms the dynamic import is unreachable and tree-shaken + * out of the bundle — the Vercel modules never ship to Cloudflare builds. + */ +export default async function BodyEnd() { + if (process.env.NEXT_PUBLIC_DEPLOY_TARGET === 'vercel') { + const { default: VercelBodyEnd } = await import('./body-end.vercel'); + return ; + } + + return null; +} diff --git a/apps/site/platform/body-end.vercel.tsx b/apps/site/platform/body-end.vercel.tsx new file mode 100644 index 0000000000000..5db143af5e299 --- /dev/null +++ b/apps/site/platform/body-end.vercel.tsx @@ -0,0 +1,11 @@ +import { Analytics } from '@vercel/analytics/react'; +import { SpeedInsights } from '@vercel/speed-insights/next'; + +const VercelBodyEnd = () => ( + <> + + + +); + +export default VercelBodyEnd; diff --git a/apps/site/turbo.json b/apps/site/turbo.json index e08e9169e7a11..d9ab6c67b3af1 100644 --- a/apps/site/turbo.json +++ b/apps/site/turbo.json @@ -9,6 +9,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -36,6 +37,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -57,6 +59,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -83,6 +86,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", diff --git a/apps/site/vercel.json b/apps/site/vercel.json index 9923ea6cf94e6..37bf0655bfc6d 100644 --- a/apps/site/vercel.json +++ b/apps/site/vercel.json @@ -1,5 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "installCommand": "pnpm install --prod --frozen-lockfile", + "buildCommand": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm build", "ignoreCommand": "[[ \"$VERCEL_GIT_COMMIT_REF\" =~ \"^dependabot/.*\" || \"$VERCEL_GIT_COMMIT_REF\" =~ \"^gh-readonly-queue/.*\" ]]" } diff --git a/docs/cloudflare-build-and-deployment.md b/docs/cloudflare-build-and-deployment.md index 2327e71445fa6..ab1609640394a 100644 --- a/docs/cloudflare-build-and-deployment.md +++ b/docs/cloudflare-build-and-deployment.md @@ -49,7 +49,7 @@ Additionally, when deploying, an extra `CF_WORKERS_SCRIPTS_API_TOKEN` environmen ### Image loader -When deployed on the Cloudflare network a custom image loader is required. We set such loader in the Next.js config file when the `OPEN_NEXT_CLOUDFLARE` environment variable is set (which indicates that we're building the application for the Cloudflare deployment). +When deployed on the Cloudflare network a custom image loader is required. We set such loader in the Next.js config file when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (which indicates that we're building the application for the Cloudflare deployment; the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../apps/site/open-next.config.ts)). The custom loader can be found at [`site/cloudflare/image-loader.ts`](../apps/site/cloudflare/image-loader.ts). From 641d0b9354b9e6de02816c87c0cf7e9d43814783 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 14:41:55 -0300 Subject: [PATCH 02/16] 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. --- .github/CODEOWNERS | 6 +- .../playwright-cloudflare-open-next.yml | 2 +- apps/site/app/[locale]/@analytics/default.tsx | 1 + apps/site/app/[locale]/layout.tsx | 12 +- apps/site/eslint.config.js | 10 +- apps/site/instrumentation.ts | 7 +- apps/site/mdx/plugins.mjs | 22 ++- apps/site/next.config.mjs | 41 ++++-- apps/site/next.constants.mjs | 18 +-- apps/site/next.image.config.mjs | 24 ++- apps/site/next.platform.config.mjs | 26 ++++ apps/site/package.json | 19 +-- apps/site/platform/analytics.tsx | 1 + apps/site/platform/body-end.tsx | 17 --- apps/site/platform/instrumentation.ts | 1 + apps/site/playwright.config.ts | 43 +++--- apps/site/playwright.platform.config.mjs | 11 ++ apps/site/tsconfig.json | 17 +-- apps/site/turbo.json | 38 ----- docs/cloudflare-build-and-deployment.md | 15 +- docs/technologies.md | 26 +++- package.json | 5 +- .../next.platform.config.mjs | 49 ++++++ .../platform-cloudflare}/open-next.config.ts | 0 packages/platform-cloudflare/package.json | 45 ++++++ .../playwright.platform.config.d.ts | 10 ++ .../playwright.platform.config.mjs | 19 +++ .../platform-cloudflare/src/analytics.tsx | 1 + .../platform-cloudflare/src}/image-loader.ts | 5 +- .../src/instrumentation.ts | 1 + .../src}/worker-entrypoint.ts | 4 +- packages/platform-cloudflare/tsconfig.json | 23 +++ packages/platform-cloudflare/turbo.json | 43 ++++++ .../platform-cloudflare}/wrangler.jsonc | 15 +- .../platform-vercel/next.platform.config.mjs | 28 ++++ packages/platform-vercel/package.json | 41 ++++++ .../playwright.platform.config.d.ts | 10 ++ .../playwright.platform.config.mjs | 11 ++ .../platform-vercel/src/analytics.tsx | 4 +- .../platform-vercel/src/instrumentation.ts | 5 + packages/platform-vercel/tsconfig.json | 22 +++ pnpm-lock.yaml | 139 +++++++++++------- 42 files changed, 583 insertions(+), 254 deletions(-) create mode 100644 apps/site/app/[locale]/@analytics/default.tsx create mode 100644 apps/site/next.platform.config.mjs create mode 100644 apps/site/platform/analytics.tsx delete mode 100644 apps/site/platform/body-end.tsx create mode 100644 apps/site/platform/instrumentation.ts create mode 100644 apps/site/playwright.platform.config.mjs create mode 100644 packages/platform-cloudflare/next.platform.config.mjs rename {apps/site => packages/platform-cloudflare}/open-next.config.ts (100%) create mode 100644 packages/platform-cloudflare/package.json create mode 100644 packages/platform-cloudflare/playwright.platform.config.d.ts create mode 100644 packages/platform-cloudflare/playwright.platform.config.mjs create mode 100644 packages/platform-cloudflare/src/analytics.tsx rename {apps/site/cloudflare => packages/platform-cloudflare/src}/image-loader.ts (87%) create mode 100644 packages/platform-cloudflare/src/instrumentation.ts rename {apps/site/cloudflare => packages/platform-cloudflare/src}/worker-entrypoint.ts (90%) create mode 100644 packages/platform-cloudflare/tsconfig.json create mode 100644 packages/platform-cloudflare/turbo.json rename {apps/site => packages/platform-cloudflare}/wrangler.jsonc (71%) create mode 100644 packages/platform-vercel/next.platform.config.mjs create mode 100644 packages/platform-vercel/package.json create mode 100644 packages/platform-vercel/playwright.platform.config.d.ts create mode 100644 packages/platform-vercel/playwright.platform.config.mjs rename apps/site/platform/body-end.vercel.tsx => packages/platform-vercel/src/analytics.tsx (72%) create mode 100644 packages/platform-vercel/src/instrumentation.ts create mode 100644 packages/platform-vercel/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e5899448d137..a95e2867f4191 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,8 +27,10 @@ turbo.json @nodejs/nodejs-website @nodejs/web-infra crowdin.yml @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra apps/site/site.json @nodejs/web-infra -apps/site/wrangler.jsonc @nodejs/web-infra -apps/site/open-next.config.ts @nodejs/web-infra +packages/platform-cloudflare/wrangler.jsonc @nodejs/web-infra +packages/platform-cloudflare/open-next.config.ts @nodejs/web-infra +packages/platform-cloudflare/next.platform.config.mjs @nodejs/web-infra +packages/platform-vercel/next.platform.config.mjs @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra # Critical Documents diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 8a42b4675fcd8..25997c9803a41 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -54,7 +54,7 @@ jobs: working-directory: apps/site run: node --run playwright env: - PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true + NEXT_PUBLIC_DEPLOY_TARGET: cloudflare PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 - name: Upload Playwright test results diff --git a/apps/site/app/[locale]/@analytics/default.tsx b/apps/site/app/[locale]/@analytics/default.tsx new file mode 100644 index 0000000000000..a56c603a9c1d9 --- /dev/null +++ b/apps/site/app/[locale]/@analytics/default.tsx @@ -0,0 +1 @@ +export { default } from '@platform/analytics'; diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index 44f6e8142f736..acc8fcef6d154 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -4,10 +4,9 @@ import { NextIntlClientProvider } from 'next-intl'; import BaseLayout from '#site/layouts/Base'; import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts'; -import BodyEnd from '#site/platform/body-end'; import { ThemeProvider } from '#site/providers/themeProvider'; -import type { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; import '#site/styles/index.css'; @@ -15,9 +14,14 @@ const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable); type RootLayoutProps = PropsWithChildren<{ params: Promise<{ locale: string }>; + analytics: ReactNode; }>; -const RootLayout: FC = async ({ children, params }) => { +const RootLayout: FC = async ({ + children, + analytics, + params, +}) => { const { locale } = await params; const { langDir, hrefLang } = @@ -44,7 +48,7 @@ const RootLayout: FC = async ({ children, params }) => { href="https://social.lfx.dev/@nodejs" /> - + {analytics} ); diff --git a/apps/site/eslint.config.js b/apps/site/eslint.config.js index 67130ffb3ae42..986a3bf1bd172 100644 --- a/apps/site/eslint.config.js +++ b/apps/site/eslint.config.js @@ -6,15 +6,7 @@ import baseConfig from '../../eslint.config.js'; export default baseConfig.concat([ { - ignores: [ - 'pages/en/blog/**/*.{md,mdx}/**', - 'public', - 'next-env.d.ts', - // The worker entrypoint is bundled by wrangler, not tsc. Its imports - // trigger a tsc crash (see tsconfig.json), so it is excluded from both - // type checking and ESLint's type-aware linting. - 'cloudflare/worker-entrypoint.ts', - ], + ignores: ['pages/en/blog/**/*.{md,mdx}/**', 'public', 'next-env.d.ts'], }, eslintReact.configs['recommended-typescript'], diff --git a/apps/site/instrumentation.ts b/apps/site/instrumentation.ts index 426884e92124e..84f7384bb1493 100644 --- a/apps/site/instrumentation.ts +++ b/apps/site/instrumentation.ts @@ -1,6 +1 @@ -export async function register() { - if (process.env.NEXT_PUBLIC_DEPLOY_TARGET === 'vercel') { - const { registerOTel } = await import('@vercel/otel'); - registerOTel({ serviceName: 'nodejs-org' }); - } -} +export { register } from '@platform/instrumentation'; diff --git a/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index 4a4c8258366ac..b8af46fde8977 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -7,21 +7,19 @@ import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; import readingTime from 'remark-reading-time'; +import { DEPLOY_TARGET } from '../next.constants.mjs'; import remarkTableTitles from '../util/table'; -// Shiki is created out here to avoid an async rehype plugin -const singletonShiki = await rehypeShikiji({ - // We use the faster WASM engine on the server instead of the web-optimized version. - // - // Currently we fall back to the JavaScript RegEx engine - // on Cloudflare workers because `shiki/wasm` requires loading via - // `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support - // for security reasons. - wasm: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare', +// Load MDX overrides contributed by the active deployment target. Keeps +// this module free of platform-specific branches — each platform owns +// its own `{ wasm, twoslash }` defaults via `next.platform.config.mjs`, +// with the in-repo default config serving as the standalone fallback. +const { default: platform } = DEPLOY_TARGET + ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) + : await import('../next.platform.config.mjs'); - // TODO(@avivkeller): Find a way to enable Twoslash w/ a VFS on Cloudflare - twoslash: process.env.NEXT_PUBLIC_DEPLOY_TARGET !== 'cloudflare', -}); +// Shiki is created out here to avoid an async rehype plugin +const singletonShiki = await rehypeShikiji(platform.mdx); /** * Provides all our Rehype Plugins that are used within MDX diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index c20134ec52a83..aad1a7e20072b 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -1,4 +1,5 @@ 'use strict'; + import createNextIntlPlugin from 'next-intl/plugin'; import { @@ -9,17 +10,16 @@ import { import { getImagesConfig } from './next.image.config.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; -const getDeploymentId = async () => { - if (DEPLOY_TARGET !== 'cloudflare') { - return undefined; - } - - // If we're building for the Cloudflare deployment we want to set - // an appropriate deploymentId (needed for skew protection) - const openNextAdapter = await import('@opennextjs/cloudflare'); - - return openNextAdapter.getDeploymentId(); -}; +/** + * Loads the deployment platform's `next.platform.config.mjs` — falling back + * to the local no-op when no platform is active. Each platform package + * (`@node-core/platform-`) owns its own file and contributes + * `{ nextConfig, aliases, images }`. Adding a new platform only means + * creating a new `@node-core/platform-` package. + */ +const { default: platform } = DEPLOY_TARGET + ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) + : await import('./next.platform.config.mjs'); /** @type {import('next').NextConfig} */ const nextConfig = { @@ -30,9 +30,14 @@ const nextConfig = { // We allow the BASE_PATH to be overridden in case that the Website // is being built on a subdirectory (e.g. /nodejs-website) basePath: BASE_PATH, - // Vercel/Next.js Image Optimization Settings - images: getImagesConfig(), + images: getImagesConfig(platform.images), serverExternalPackages: ['twoslash'], + // Transpile platform packages' TSX/TS sources when they're pulled in via + // the `@platform/*` aliases from the active `next.platform.config.mjs`. + transpilePackages: [ + '@node-core/platform-vercel', + '@node-core/platform-cloudflare', + ], outputFileTracingIncludes: { // Twoslash needs TypeScript declarations to function, and, by default, Next.js // strips them for brevity. Therefore, they must be explicitly included. @@ -84,8 +89,16 @@ const nextConfig = { // Faster Development Servers with Turbopack turbopackFileSystemCacheForDev: true, }, - deploymentId: await getDeploymentId(), + // Provide Turbopack Aliases for Platform Resolution + turbopack: { resolveAlias: platform.aliases }, + // Provide Webpack Aliases for Platform Resolution + webpack: ({ resolve, ...config }) => ({ + ...config, + resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } }, + }), + ...platform.nextConfig, }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); + export default withNextIntl(nextConfig); diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index e9ab137b828d7..3e8f03aa02881 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -43,20 +43,14 @@ export const ENABLE_STATIC_EXPORT_LOCALE = process.env.NEXT_PUBLIC_STATIC_EXPORT_LOCALE === true; /** - * 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. + * The full canonical URL of the deployed Website (used e.g. for the RSS feed). * - * 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 - * by us if necessary. Otherwise it will fallback to the default Node.js Website URL. - * - * @TODO: We should get rid of needing to rely on `VERCEL_URL` for deployment URL. - * - * @see https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#framework-environment-variables + * Platform-specific base URLs (such as Vercel's `VERCEL_URL`) are inlined into + * `NEXT_PUBLIC_BASE_URL` at build time by each platform's `next.platform.config.mjs`, + * keeping this module free of platform-specific branches. */ -export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL - ? process.env.NEXT_PUBLIC_BASE_URL - : process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : 'https://nodejs.org'; +export const BASE_URL = + process.env.NEXT_PUBLIC_BASE_URL || 'https://nodejs.org'; /** * This is used for any place that requires the Node.js distribution URL (which by default is nodejs.org/dist) diff --git a/apps/site/next.image.config.mjs b/apps/site/next.image.config.mjs index 1097d7e0599c3..ddd9a19302ff5 100644 --- a/apps/site/next.image.config.mjs +++ b/apps/site/next.image.config.mjs @@ -1,4 +1,4 @@ -import { DEPLOY_TARGET, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { ENABLE_STATIC_EXPORT } from './next.constants.mjs'; const remotePatterns = [ 'https://avatars.githubusercontent.com/**', @@ -8,18 +8,16 @@ const remotePatterns = [ 'https://website-assets.oramasearch.com/**', ]; -export const getImagesConfig = () => { - if (DEPLOY_TARGET === 'cloudflare') { - // If we're building for the Cloudflare deployment we want to use the custom cloudflare image loader - // - // Important: The custom loader ignores `remotePatterns` as those are configured as allowed source origins - // (https://developers.cloudflare.com/images/transform-images/sources/) - // in the Cloudflare dashboard itself instead (to the exact same values present in `remotePatterns` above). - // - return { - loader: 'custom', - loaderFile: './cloudflare/image-loader.ts', - }; +/** + * Returns the Next.js `images` configuration, preferring any platform-provided + * override (e.g. Cloudflare's custom loader) over the default remotePatterns + + * static-export unoptimized defaults. + * + * @param {import('next').NextConfig['images']} [platformImagesOverride] + */ +export const getImagesConfig = platformImagesOverride => { + if (platformImagesOverride) { + return platformImagesOverride; } return { diff --git a/apps/site/next.platform.config.mjs b/apps/site/next.platform.config.mjs new file mode 100644 index 0000000000000..0bf1290a21545 --- /dev/null +++ b/apps/site/next.platform.config.mjs @@ -0,0 +1,26 @@ +import { fileURLToPath } from 'node:url'; + +/** + * Default (no-op) platform config used when no `DEPLOY_TARGET` is set — + * local dev, static export, generic hosting, etc. + * + * Platform deployments (Vercel, Cloudflare, …) provide their own + * `next.platform.config.mjs` that overrides these values. Keep this + * file free of any platform-specific code. + */ +export default { + aliases: { + '@platform/analytics': fileURLToPath( + new URL('./platform/analytics.tsx', import.meta.url) + ), + '@platform/instrumentation': fileURLToPath( + new URL('./platform/instrumentation.ts', import.meta.url) + ), + }, + mdx: { + // Defaults for local dev / static export / generic hosting. Platform + // packages override these via their own `next.platform.config.mjs`. + wasm: true, + twoslash: true, + }, +}; diff --git a/apps/site/package.json b/apps/site/package.json index c908d4e77cff3..60a098982f03e 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -6,9 +6,6 @@ "build": "cross-env NODE_NO_WARNINGS=1 next build", "build:blog-data": "cross-env NODE_NO_WARNINGS=1 node ./scripts/blog-data/index.mjs", "build:blog-data:watch": "node --watch --watch-path=pages/en/blog ./scripts/blog-data/index.mjs", - "cloudflare:build:worker": "opennextjs-cloudflare build", - "cloudflare:deploy": "opennextjs-cloudflare deploy", - "cloudflare:preview": "wrangler dev", "predeploy": "node --run build:blog-data", "deploy": "cross-env NEXT_PUBLIC_STATIC_EXPORT=true node --run build", "predev": "node --run build:blog-data", @@ -34,14 +31,12 @@ "dependencies": { "@heroicons/react": "~2.2.0", "@mdx-js/mdx": "^3.1.1", + "@node-core/platform-cloudflare": "workspace:*", + "@node-core/platform-vercel": "workspace:*", "@node-core/rehype-shiki": "workspace:*", "@node-core/ui-components": "workspace:*", "@node-core/website-i18n": "workspace:*", "@nodevu/core": "0.3.0", - "@opentelemetry/api-logs": "~0.213.0", - "@opentelemetry/instrumentation": "~0.213.0", - "@opentelemetry/resources": "~1.30.1", - "@opentelemetry/sdk-logs": "~0.213.0", "@orama/core": "^1.2.19", "@orama/ui": "^1.5.4", "@radix-ui/react-tabs": "^1.1.13", @@ -50,9 +45,6 @@ "@types/node": "catalog:", "@types/react": "catalog:", "@vcarl/remark-headings": "~0.1.0", - "@vercel/analytics": "~2.0.1", - "@vercel/otel": "~2.1.1", - "@vercel/speed-insights": "~2.0.0", "classnames": "catalog:", "cross-env": "catalog:", "feed": "~5.2.0", @@ -81,10 +73,8 @@ }, "devDependencies": { "@eslint-react/eslint-plugin": "~3.0.0", - "@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.1", "@next/eslint-plugin-next": "16.2.1", "@node-core/remark-lint": "workspace:*", - "@opennextjs/cloudflare": "^1.19.3", "@playwright/test": "^1.58.2", "@testing-library/user-event": "~14.6.1", "@types/mdast": "^4.0.4", @@ -106,10 +96,7 @@ "tsx": "^4.21.0", "typescript": "catalog:", "typescript-eslint": "~8.57.2", - "user-agent-data-types": "0.4.2", - "wrangler": "^4.77.0", - "@cloudflare/workers-types": "^4.20260418.1", - "@sentry/cloudflare": "^10.49.0" + "user-agent-data-types": "0.4.2" }, "imports": { "#site/*": [ diff --git a/apps/site/platform/analytics.tsx b/apps/site/platform/analytics.tsx new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/apps/site/platform/analytics.tsx @@ -0,0 +1 @@ +export default () => null; diff --git a/apps/site/platform/body-end.tsx b/apps/site/platform/body-end.tsx deleted file mode 100644 index 64c44e365a56e..0000000000000 --- a/apps/site/platform/body-end.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Per-platform "body end" slot. Deployment targets can inject DOM at the - * end of the document body (analytics, tracking scripts, etc.) without - * adding platform-specific imports to the core layout. - * - * `NEXT_PUBLIC_DEPLOY_TARGET` is inlined by Next.js at build time, so on - * non-matching platforms the dynamic import is unreachable and tree-shaken - * out of the bundle — the Vercel modules never ship to Cloudflare builds. - */ -export default async function BodyEnd() { - if (process.env.NEXT_PUBLIC_DEPLOY_TARGET === 'vercel') { - const { default: VercelBodyEnd } = await import('./body-end.vercel'); - return ; - } - - return null; -} diff --git a/apps/site/platform/instrumentation.ts b/apps/site/platform/instrumentation.ts new file mode 100644 index 0000000000000..a1c3920abc89d --- /dev/null +++ b/apps/site/platform/instrumentation.ts @@ -0,0 +1 @@ +export function register() {} diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.ts index 523f40f644582..c392c65a93579 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.ts @@ -1,6 +1,19 @@ -import { defineConfig, devices, type Config } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; -import json from './package.json' with { type: 'json' }; +import { DEPLOY_TARGET } from './next.constants.mjs'; + +/** + * Load Playwright overrides contributed by the active deployment target. + * + * Mirrors how `next.config.mjs` loads `next.platform.config` from the + * matching `@node-core/platform-` package. Each platform owns + * its own webServer / baseURL wiring so this file stays platform-neutral. + */ +const { default: platform } = DEPLOY_TARGET + ? await import( + `@node-core/platform-${DEPLOY_TARGET}/playwright.platform.config` + ) + : await import('./playwright.platform.config.mjs'); const isCI = !!process.env.CI; @@ -12,9 +25,12 @@ export default defineConfig({ retries: isCI ? 2 : 0, workers: isCI ? 1 : undefined, reporter: isCI ? [['html'], ['github']] : [['html']], - ...getWebServerConfig(), + ...(platform.webServer ? { webServer: platform.webServer } : {}), use: { - baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000', + baseURL: + process.env.PLAYWRIGHT_BASE_URL || + platform.baseURL || + 'http://127.0.0.1:3000', trace: 'on-first-retry', }, projects: [ @@ -32,22 +48,3 @@ export default defineConfig({ }, ], }); - -function getWebServerConfig(): Pick { - if (!json.scripts['cloudflare:preview']) { - throw new Error('cloudflare:preview script not defined'); - } - - if (process.env.PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW) { - return { - webServer: { - stdout: 'pipe', - command: '../../node_modules/.bin/turbo cloudflare:preview', - url: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000', - timeout: 60_000 * 3, - }, - }; - } - - return {}; -} diff --git a/apps/site/playwright.platform.config.mjs b/apps/site/playwright.platform.config.mjs new file mode 100644 index 0000000000000..5cef7bccd3aab --- /dev/null +++ b/apps/site/playwright.platform.config.mjs @@ -0,0 +1,11 @@ +/** + * Default (no-op) Playwright platform config used when no `DEPLOY_TARGET` + * is set — local dev against `next dev`, static export, generic hosting. + * + * Platform deployments (Vercel, Cloudflare, …) provide their own + * `playwright.platform.config.mjs` that overrides these values. Keep + * this file free of any platform-specific code. + * + * @type {{ baseURL?: string; webServer?: import('@playwright/test').Config['webServer'] }} + */ +export default {}; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 02158236da303..865ec44b098f5 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -20,7 +20,11 @@ "name": "next" } ], - "baseUrl": "." + "baseUrl": ".", + "paths": { + "@platform/analytics": ["./platform/analytics.tsx"], + "@platform/instrumentation": ["./platform/instrumentation.ts"] + } }, "mdx": { "checkMdx": true @@ -37,14 +41,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules", - ".next", - ".open-next", - // The worker entrypoint is bundled by wrangler (not tsc). Its imports of - // @sentry/cloudflare and .open-next/worker.js trigger an infinite-recursion - // crash in the TypeScript compiler (v5.9) during type resolution of - // @cloudflare/workers-types, so we exclude it from type checking. - "cloudflare/worker-entrypoint.ts" - ] + "exclude": ["node_modules", ".next", ".open-next"] } diff --git a/apps/site/turbo.json b/apps/site/turbo.json index d9ab6c67b3af1..22e26b7dfaf98 100644 --- a/apps/site/turbo.json +++ b/apps/site/turbo.json @@ -7,7 +7,6 @@ "cache": false, "persistent": true, "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -35,7 +34,6 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -57,7 +55,6 @@ "cache": false, "persistent": true, "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -84,7 +81,6 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -141,7 +137,6 @@ "inputs": ["{pages}/**/*.{mdx,md}"], "outputs": ["public/blog-data.json"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "TURBO_CACHE", "TURBO_TELEMETRY_DISABLED", @@ -149,39 +144,6 @@ "ENABLE_EXPERIMENTAL_COREPACK" ] }, - "cloudflare:build:worker": { - "dependsOn": ["build:blog-data"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, - "cloudflare:preview": { - "dependsOn": ["cloudflare:build:worker"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, - "cloudflare:deploy": { - "dependsOn": ["cloudflare:build:worker"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, "scripts:release-post": { "cache": false } diff --git a/docs/cloudflare-build-and-deployment.md b/docs/cloudflare-build-and-deployment.md index ab1609640394a..d021b409981d2 100644 --- a/docs/cloudflare-build-and-deployment.md +++ b/docs/cloudflare-build-and-deployment.md @@ -2,9 +2,14 @@ The Node.js Website can be built using the [OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare). Such build generates a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) that can be deployed on the [Cloudflare](https://www.cloudflare.com) network. +The build is gated on the `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` environment variable (set by the OpenNext `buildCommand`), which makes `apps/site` pull its Next.js, MDX, image-loader, and analytics overrides from [`@node-core/platform-cloudflare`](../packages/platform-cloudflare). See the [Deploy Target Selection](./technologies.md#deploy-target-selection-next_public_deploy_target) section of the Technologies document for the full platform-adapter contract. + ## Configurations -There are two key configuration files related to Cloudflare deployments: +All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../packages/platform-cloudflare) package. The two key configuration files are: + +- [`packages/platform-cloudflare/wrangler.jsonc`](../packages/platform-cloudflare/wrangler.jsonc) — the Wrangler configuration +- [`packages/platform-cloudflare/open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts) — the OpenNext adapter configuration ### Wrangler Configuration @@ -14,7 +19,7 @@ For more details, refer to the [Wrangler documentation](https://developers.cloud Key configurations include: -- `main`: Points to a custom worker entry point ([`site/cloudflare/worker-entrypoint.ts`](../apps/site/cloudflare/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). +- `main`: Points to a custom worker entry point ([`packages/platform-cloudflare/src/worker-entrypoint.ts`](../packages/platform-cloudflare/src/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). - `account_id`: Specifies the Cloudflare account ID. This is not required for local previews but is necessary for deployments. You can obtain an account ID for free by signing up at [dash.cloudflare.com](https://dash.cloudflare.com/login). - This is currently set to `fb4a2d0f103c6ff38854ac69eb709272`, which is the ID of a Cloudflare account controlled by Node.js, and used for testing. - `build`: Defines the build command to generate the Node.js filesystem polyfills required for the application to run on Cloudflare Workers. This uses the [`@flarelabs/wrangler-build-time-fs-assets-polyfilling`](https://github.com/flarelabs-net/wrangler-build-time-fs-assets-polyfilling) package. @@ -49,15 +54,15 @@ Additionally, when deploying, an extra `CF_WORKERS_SCRIPTS_API_TOKEN` environmen ### Image loader -When deployed on the Cloudflare network a custom image loader is required. We set such loader in the Next.js config file when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (which indicates that we're building the application for the Cloudflare deployment; the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../apps/site/open-next.config.ts)). +When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`packages/platform-cloudflare/next.platform.config.mjs`](../packages/platform-cloudflare/next.platform.config.mjs)) contributes it via the `images.loaderFile` field, which is merged into the shared Next.js config when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts)). -The custom loader can be found at [`site/cloudflare/image-loader.ts`](../apps/site/cloudflare/image-loader.ts). +The custom loader can be found at [`packages/platform-cloudflare/src/image-loader.ts`](../packages/platform-cloudflare/src/image-loader.ts). For more details on this see: https://developers.cloudflare.com/images/transform-images/integrate-with-frameworks/#global-loader ### Custom Worker Entry Point -Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`site/cloudflare/worker-entrypoint.ts`](../apps/site/cloudflare/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). +Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`packages/platform-cloudflare/src/worker-entrypoint.ts`](../packages/platform-cloudflare/src/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). The custom entry point imports the OpenNext-generated handler from `.open-next/worker.js` and re-exports the `DOQueueHandler` Durable Object needed by the application. diff --git a/docs/technologies.md b/docs/technologies.md index 9c6113ab7dd8c..cf8bfda8125c7 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -37,7 +37,9 @@ This document provides an overview of the technologies used in the Node.js websi - [VSCode Configuration](#vscode-configuration) - [Build and Deployment](#build-and-deployment) - [Multiple Build Targets](#multiple-build-targets) + - [Deploy Target Selection (`NEXT_PUBLIC_DEPLOY_TARGET`)](#deploy-target-selection-next_public_deploy_target) - [Vercel Integration](#vercel-integration) + - [Cloudflare Integration](#cloudflare-integration) - [Package Management](#package-management) - [Multi-package Workspace](#multi-package-workspace) - [Publishing Process](#publishing-process) @@ -144,7 +146,9 @@ nodejs.org/ │ ├── locales/ # Translation files │ └── config.json # Locale configuration ├── rehype-shiki/ # Syntax highlighting plugin - ... + ├── platform-vercel/ # Vercel platform adapter (analytics, instrumentation) + └── platform-cloudflare/ # Cloudflare platform adapter (worker entrypoint, + # image loader, open-next config, wrangler config) ``` ## Architecture Decisions @@ -289,6 +293,20 @@ Benefits: - **`pnpm build`**: Production build for Vercel - **`pnpm deploy`**: Export build for legacy servers - **`pnpm dev`**: Development server +- **`pnpm cloudflare:preview`**: Local preview of the Cloudflare (OpenNext) worker build +- **`pnpm cloudflare:deploy`**: Deploy the Cloudflare (OpenNext) worker build + +#### Deploy Target Selection (`NEXT_PUBLIC_DEPLOY_TARGET`) + +`NEXT_PUBLIC_DEPLOY_TARGET` selects which platform adapter contributes its Next.js config, MDX flags, image loader, analytics, and Playwright webServer. It is consumed at build time by [`apps/site/next.config.mjs`](../apps/site/next.config.mjs), [`apps/site/mdx/plugins.mjs`](../apps/site/mdx/plugins.mjs), and [`apps/site/playwright.config.ts`](../apps/site/playwright.config.ts) via a dynamic import of `@node-core/platform-${target}/next.platform.config`. + +| Value | Adapter | Set by | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `vercel` | [`@node-core/platform-vercel`](../packages/platform-vercel) | [`apps/site/vercel.json`](../apps/site/vercel.json) build env | +| `cloudflare` | [`@node-core/platform-cloudflare`](../packages/platform-cloudflare) | OpenNext `buildCommand` in [`open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts) | +| _(unset)_ | Falls back to the no-op defaults in [`apps/site/next.platform.config.mjs`](../apps/site/next.platform.config.mjs) and [`apps/site/playwright.platform.config.mjs`](../apps/site/playwright.platform.config.mjs) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | + +Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`packages/platform-vercel/next.platform.config.mjs`](../packages/platform-vercel/next.platform.config.mjs) and [`packages/platform-cloudflare/next.platform.config.mjs`](../packages/platform-cloudflare/next.platform.config.mjs) for reference. #### Vercel Integration @@ -297,6 +315,12 @@ Benefits: - Build-time dependencies must be in `dependencies`, not `devDependencies` - Sponsorship maintained by OpenJS Foundation +#### Cloudflare Integration + +- OpenNext adapter builds a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) artifact from the Next.js build +- All Cloudflare-specific files (Wrangler config, OpenNext config, custom worker entrypoint, image loader) live in [`packages/platform-cloudflare`](../packages/platform-cloudflare) +- See [Cloudflare build and deployment](./cloudflare-build-and-deployment.md) for details + ### Package Management #### Multi-package Workspace diff --git a/package.json b/package.json index dfa43455d7b47..668e872b50585 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "scripts": { "compile": "turbo compile", "build": "turbo build", - "cloudflare:deploy": "turbo cloudflare:deploy", - "cloudflare:preview": "turbo cloudflare:preview", + "cloudflare:build:worker": "turbo cloudflare:build:worker --filter=@node-core/platform-cloudflare", + "cloudflare:deploy": "turbo cloudflare:deploy --filter=@node-core/platform-cloudflare", + "cloudflare:preview": "turbo cloudflare:preview --filter=@node-core/platform-cloudflare", "deploy": "turbo deploy", "dev": "turbo dev", "format": "turbo //#prettier:fix prettier:fix lint:fix", diff --git a/packages/platform-cloudflare/next.platform.config.mjs b/packages/platform-cloudflare/next.platform.config.mjs new file mode 100644 index 0000000000000..7bfd123a2b975 --- /dev/null +++ b/packages/platform-cloudflare/next.platform.config.mjs @@ -0,0 +1,49 @@ +import { createRequire } from 'node:module'; +import { relative } from 'node:path'; + +import { getDeploymentId } from '@opennextjs/cloudflare'; + +const require = createRequire(import.meta.url); + +/** + * Platform config contributed by the Cloudflare deployment target. + * + * Consumed by `apps/site/next.config.mjs` via the platform-config loader. + * Must export a default `{ nextConfig, aliases, images }` shape — any of + * which may be omitted when the platform has nothing to contribute. + * + * @type {import('@node-core/platform-cloudflare/next.platform.config').PlatformConfig} + */ +export default { + nextConfig: { + // Skew protection: Cloudflare routes requests by deploymentId so that + // a client and the worker stay in sync across rolling deploys. + deploymentId: await getDeploymentId(), + }, + aliases: { + '@platform/analytics': '@node-core/platform-cloudflare/analytics', + '@platform/instrumentation': + '@node-core/platform-cloudflare/instrumentation', + }, + images: { + // Route optimized images through Cloudflare's Images service via the + // custom loader. `remotePatterns` do NOT apply here — Cloudflare + // enforces allowed origins at the edge instead. + loader: 'custom', + // Next.js joins `loaderFile` onto its own cwd (apps/site), so pass a + // path relative to that cwd rather than an absolute one. Resolving via + // `require.resolve` avoids the `new URL(..., import.meta.url)` pattern, + // which webpack rewrites as an asset reference and mangles at runtime. + loaderFile: relative( + process.cwd(), + require.resolve('@node-core/platform-cloudflare/image-loader') + ), + }, + mdx: { + // Cloudflare workers can't load `shiki/wasm` via `WebAssembly.instantiate` + // with custom imports (blocked for security), so fall back to the + // JavaScript RegEx engine. Twoslash also needs a VFS we don't have here. + wasm: false, + twoslash: false, + }, +}; diff --git a/apps/site/open-next.config.ts b/packages/platform-cloudflare/open-next.config.ts similarity index 100% rename from apps/site/open-next.config.ts rename to packages/platform-cloudflare/open-next.config.ts diff --git a/packages/platform-cloudflare/package.json b/packages/platform-cloudflare/package.json new file mode 100644 index 0000000000000..bde3f2e55b221 --- /dev/null +++ b/packages/platform-cloudflare/package.json @@ -0,0 +1,45 @@ +{ + "name": "@node-core/platform-cloudflare", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./analytics": "./src/analytics.tsx", + "./image-loader": "./src/image-loader.ts", + "./instrumentation": "./src/instrumentation.ts", + "./next.platform.config": "./next.platform.config.mjs", + "./playwright.platform.config": "./playwright.platform.config.mjs", + "./worker-entrypoint": "./src/worker-entrypoint.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodejs.org", + "directory": "packages/platform-cloudflare" + }, + "scripts": { + "cloudflare:build:worker": "cd ../../apps/site && opennextjs-cloudflare build --openNextConfigPath ../../packages/platform-cloudflare/open-next.config.ts --config ../../packages/platform-cloudflare/wrangler.jsonc", + "cloudflare:deploy": "cd ../../apps/site && opennextjs-cloudflare deploy --openNextConfigPath ../../packages/platform-cloudflare/open-next.config.ts --config ../../packages/platform-cloudflare/wrangler.jsonc", + "cloudflare:preview": "cd ../../apps/site && wrangler dev --config ../../packages/platform-cloudflare/wrangler.jsonc", + "lint:types": "tsc --noEmit" + }, + "dependencies": { + "@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.1", + "@opennextjs/cloudflare": "^1.19.3", + "@sentry/cloudflare": "^10.49.0", + "wrangler": "^4.77.0" + }, + "peerDependencies": { + "next": "16.2.4", + "react": "catalog:" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260418.1", + "@playwright/test": "^1.58.2", + "@types/node": "catalog:", + "@types/react": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/platform-cloudflare/playwright.platform.config.d.ts b/packages/platform-cloudflare/playwright.platform.config.d.ts new file mode 100644 index 0000000000000..046abfff37aa9 --- /dev/null +++ b/packages/platform-cloudflare/playwright.platform.config.d.ts @@ -0,0 +1,10 @@ +import type { Config } from '@playwright/test'; + +export type PlatformPlaywrightConfig = { + baseURL?: string; + webServer?: Config['webServer']; +}; + +declare const config: PlatformPlaywrightConfig; + +export default config; diff --git a/packages/platform-cloudflare/playwright.platform.config.mjs b/packages/platform-cloudflare/playwright.platform.config.mjs new file mode 100644 index 0000000000000..1a0cc1c3e3a53 --- /dev/null +++ b/packages/platform-cloudflare/playwright.platform.config.mjs @@ -0,0 +1,19 @@ +/** + * Playwright overrides contributed by the Cloudflare deployment target. + * + * Consumed by `apps/site/playwright.config.ts` via the platform-config + * loader. Spins up the wrangler preview so E2E runs against the + * OpenNext worker artifact rather than `next dev`. + * + * @type {import('./playwright.platform.config').PlatformPlaywrightConfig} + */ +export default { + baseURL: 'http://127.0.0.1:8787', + webServer: { + stdout: 'pipe', + command: + '../../node_modules/.bin/turbo cloudflare:preview --filter=@node-core/platform-cloudflare', + url: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8787', + timeout: 60_000 * 3, + }, +}; diff --git a/packages/platform-cloudflare/src/analytics.tsx b/packages/platform-cloudflare/src/analytics.tsx new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/packages/platform-cloudflare/src/analytics.tsx @@ -0,0 +1 @@ +export default () => null; diff --git a/apps/site/cloudflare/image-loader.ts b/packages/platform-cloudflare/src/image-loader.ts similarity index 87% rename from apps/site/cloudflare/image-loader.ts rename to packages/platform-cloudflare/src/image-loader.ts index 2137028e23db4..d883ff004ed71 100644 --- a/apps/site/cloudflare/image-loader.ts +++ b/packages/platform-cloudflare/src/image-loader.ts @@ -1,8 +1,7 @@ import type { ImageLoaderProps } from 'next/image'; -const normalizeSrc = (src: string) => { - return src.startsWith('/') ? src.slice(1) : src; -}; +const normalizeSrc = (src: string) => + src.startsWith('/') ? src.slice(1) : src; export default function cloudflareLoader({ src, diff --git a/packages/platform-cloudflare/src/instrumentation.ts b/packages/platform-cloudflare/src/instrumentation.ts new file mode 100644 index 0000000000000..a1c3920abc89d --- /dev/null +++ b/packages/platform-cloudflare/src/instrumentation.ts @@ -0,0 +1 @@ +export function register() {} diff --git a/apps/site/cloudflare/worker-entrypoint.ts b/packages/platform-cloudflare/src/worker-entrypoint.ts similarity index 90% rename from apps/site/cloudflare/worker-entrypoint.ts rename to packages/platform-cloudflare/src/worker-entrypoint.ts index bd40543b4b9dd..f63ab60a6f586 100644 --- a/apps/site/cloudflare/worker-entrypoint.ts +++ b/packages/platform-cloudflare/src/worker-entrypoint.ts @@ -11,7 +11,7 @@ import type { Request, } from '@cloudflare/workers-types'; -import { default as handler } from '../.open-next/worker.js'; +import { default as handler } from '../../../apps/site/.open-next/worker.js'; export default withSentry( (env: { @@ -50,4 +50,4 @@ export default withSentry( } ); -export { DOQueueHandler } from '../.open-next/worker.js'; +export { DOQueueHandler } from '../../../apps/site/.open-next/worker.js'; diff --git a/packages/platform-cloudflare/tsconfig.json b/packages/platform-cloudflare/tsconfig.json new file mode 100644 index 0000000000000..1189ad9fbc09a --- /dev/null +++ b/packages/platform-cloudflare/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "Bundler", + "customConditions": ["default"], + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": [ + "src", + "next.platform.config.mjs", + "playwright.platform.config.mjs", + "playwright.platform.config.d.ts" + ], + "exclude": ["src/worker-entrypoint.ts"] +} diff --git a/packages/platform-cloudflare/turbo.json b/packages/platform-cloudflare/turbo.json new file mode 100644 index 0000000000000..8c1ffdc95a974 --- /dev/null +++ b/packages/platform-cloudflare/turbo.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "cloudflare:build:worker": { + "dependsOn": ["@node-core/website#build:blog-data"], + "inputs": [ + "open-next.config.ts", + "wrangler.jsonc", + "src/**/*.ts", + "../../apps/site/{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", + "../../apps/site/{app,components,layouts,pages,styles}/**/*.css", + "../../apps/site/{next-data,scripts,i18n}/**/*.{mjs,json}", + "../../apps/site/{app,pages}/**/*.{mdx,md}", + "../../apps/site/*.{md,mdx,json,ts,tsx,mjs,yml}" + ], + "outputs": ["../../apps/site/.open-next/**"], + "env": [ + "NEXT_PUBLIC_DEPLOY_TARGET", + "NEXT_PUBLIC_BASE_URL", + "NEXT_PUBLIC_DIST_URL", + "NEXT_PUBLIC_DOCS_URL", + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_ORAMA_API_KEY", + "NEXT_PUBLIC_ORAMA_ENDPOINT", + "NEXT_PUBLIC_DATA_URL", + "NEXT_GITHUB_API_KEY" + ] + }, + "cloudflare:preview": { + "dependsOn": ["cloudflare:build:worker"], + "cache": false, + "persistent": true + }, + "cloudflare:deploy": { + "dependsOn": ["cloudflare:build:worker"], + "cache": false + }, + "lint:types": { + "cache": false + } + } +} diff --git a/apps/site/wrangler.jsonc b/packages/platform-cloudflare/wrangler.jsonc similarity index 71% rename from apps/site/wrangler.jsonc rename to packages/platform-cloudflare/wrangler.jsonc index b176578dd2eea..f3ec84ef5f325 100644 --- a/apps/site/wrangler.jsonc +++ b/packages/platform-cloudflare/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "./node_modules/wrangler/config-schema.json", - "main": "./cloudflare/worker-entrypoint.ts", + "main": "./src/worker-entrypoint.ts", "name": "nodejs-website", "compatibility_date": "2024-11-07", "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], @@ -8,7 +8,7 @@ "minify": true, "keep_names": false, "assets": { - "directory": ".open-next/assets", + "directory": "../../apps/site/.open-next/assets", "binding": "ASSETS", "run_worker_first": true, }, @@ -31,13 +31,16 @@ "head_sampling_rate": 1, }, "build": { + // Run the asset polyfiller from apps/site so that `pages`, `snippets`, and + // the `.open-next` output directory resolve against the Next.js app. + "cwd": "../../apps/site", "command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets", }, "alias": { - "node:fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "node:fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", - "fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "node:fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "node:fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", }, "r2_buckets": [ { diff --git a/packages/platform-vercel/next.platform.config.mjs b/packages/platform-vercel/next.platform.config.mjs new file mode 100644 index 0000000000000..d24ca1d083cbd --- /dev/null +++ b/packages/platform-vercel/next.platform.config.mjs @@ -0,0 +1,28 @@ +/** + * Platform config contributed by the Vercel deployment target. + * + * Consumed by `apps/site/next.config.mjs` via the platform-config loader. + * Must export a default `{ nextConfig, aliases, images }` shape — any of + * which may be omitted when the platform has nothing to contribute. + * + * @type {import('@node-core/platform-vercel/next.platform.config').PlatformConfig} + */ +const vercelDeploymentUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : undefined; + +export default { + nextConfig: { + // Expose Vercel's auto-assigned deployment URL as a platform-agnostic + // `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single + // canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins. + env: { + NEXT_PUBLIC_BASE_URL: + process.env.NEXT_PUBLIC_BASE_URL || vercelDeploymentUrl || '', + }, + }, + aliases: { + '@platform/analytics': '@node-core/platform-vercel/analytics', + '@platform/instrumentation': '@node-core/platform-vercel/instrumentation', + }, +}; diff --git a/packages/platform-vercel/package.json b/packages/platform-vercel/package.json new file mode 100644 index 0000000000000..05661584e9592 --- /dev/null +++ b/packages/platform-vercel/package.json @@ -0,0 +1,41 @@ +{ + "name": "@node-core/platform-vercel", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./analytics": "./src/analytics.tsx", + "./instrumentation": "./src/instrumentation.ts", + "./next.platform.config": "./next.platform.config.mjs", + "./playwright.platform.config": "./playwright.platform.config.mjs" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodejs.org", + "directory": "packages/platform-vercel" + }, + "scripts": { + "lint:types": "tsc --noEmit" + }, + "dependencies": { + "@opentelemetry/api-logs": "~0.213.0", + "@opentelemetry/instrumentation": "~0.213.0", + "@opentelemetry/resources": "~1.30.1", + "@opentelemetry/sdk-logs": "~0.213.0", + "@vercel/analytics": "~2.0.1", + "@vercel/otel": "~2.1.1", + "@vercel/speed-insights": "~2.0.0" + }, + "peerDependencies": { + "next": "16.2.4", + "react": "catalog:" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/platform-vercel/playwright.platform.config.d.ts b/packages/platform-vercel/playwright.platform.config.d.ts new file mode 100644 index 0000000000000..046abfff37aa9 --- /dev/null +++ b/packages/platform-vercel/playwright.platform.config.d.ts @@ -0,0 +1,10 @@ +import type { Config } from '@playwright/test'; + +export type PlatformPlaywrightConfig = { + baseURL?: string; + webServer?: Config['webServer']; +}; + +declare const config: PlatformPlaywrightConfig; + +export default config; diff --git a/packages/platform-vercel/playwright.platform.config.mjs b/packages/platform-vercel/playwright.platform.config.mjs new file mode 100644 index 0000000000000..29ca61b4e5d30 --- /dev/null +++ b/packages/platform-vercel/playwright.platform.config.mjs @@ -0,0 +1,11 @@ +/** + * Playwright overrides contributed by the Vercel deployment target. + * + * Vercel builds run on external preview URLs, so no local webServer is + * started — the CI workflow provides `PLAYWRIGHT_BASE_URL` pointing at + * the deployment. Left intentionally empty so `apps/site/playwright.config.ts` + * falls back to its default baseURL. + * + * @type {import('./playwright.platform.config').PlatformPlaywrightConfig} + */ +export default {}; diff --git a/apps/site/platform/body-end.vercel.tsx b/packages/platform-vercel/src/analytics.tsx similarity index 72% rename from apps/site/platform/body-end.vercel.tsx rename to packages/platform-vercel/src/analytics.tsx index 5db143af5e299..6b78cb4512b3f 100644 --- a/apps/site/platform/body-end.vercel.tsx +++ b/packages/platform-vercel/src/analytics.tsx @@ -1,11 +1,11 @@ import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/next'; -const VercelBodyEnd = () => ( +const VercelAnalytics = () => ( <> ); -export default VercelBodyEnd; +export default VercelAnalytics; diff --git a/packages/platform-vercel/src/instrumentation.ts b/packages/platform-vercel/src/instrumentation.ts new file mode 100644 index 0000000000000..b953218a3e1e9 --- /dev/null +++ b/packages/platform-vercel/src/instrumentation.ts @@ -0,0 +1,5 @@ +import { registerOTel } from '@vercel/otel'; + +export function register() { + registerOTel({ serviceName: 'nodejs-org' }); +} diff --git a/packages/platform-vercel/tsconfig.json b/packages/platform-vercel/tsconfig.json new file mode 100644 index 0000000000000..320f4a8035486 --- /dev/null +++ b/packages/platform-vercel/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "Bundler", + "customConditions": ["default"], + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": [ + "src", + "next.platform.config.mjs", + "playwright.platform.config.mjs", + "playwright.platform.config.d.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e50916229bc79..16d10577b7314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,12 @@ importers: '@mdx-js/mdx': specifier: ^3.1.1 version: 3.1.1 + '@node-core/platform-cloudflare': + specifier: workspace:* + version: link:../../packages/platform-cloudflare + '@node-core/platform-vercel': + specifier: workspace:* + version: link:../../packages/platform-vercel '@node-core/rehype-shiki': specifier: workspace:* version: link:../../packages/rehype-shiki @@ -99,18 +105,6 @@ importers: '@nodevu/core': specifier: 0.3.0 version: 0.3.0 - '@opentelemetry/api-logs': - specifier: ~0.213.0 - version: 0.213.0 - '@opentelemetry/instrumentation': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ~1.30.1 - version: 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) '@orama/core': specifier: ^1.2.19 version: 1.2.19 @@ -135,15 +129,6 @@ importers: '@vcarl/remark-headings': specifier: ~0.1.0 version: 0.1.0 - '@vercel/analytics': - specifier: ~2.0.1 - version: 2.0.1(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - '@vercel/otel': - specifier: ~2.1.1 - version: 2.1.1(@opentelemetry/api-logs@0.213.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)) - '@vercel/speed-insights': - specifier: ~2.0.0 - version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) classnames: specifier: 'catalog:' version: 2.5.1 @@ -220,30 +205,18 @@ importers: specifier: ~5.0.1 version: 5.0.1 devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20260418.1 - version: 4.20260422.1 '@eslint-react/eslint-plugin': specifier: ~3.0.0 version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': - specifier: ^0.0.1 - version: 0.0.1 '@next/eslint-plugin-next': specifier: 16.2.1 version: 16.2.1 '@node-core/remark-lint': specifier: workspace:* version: link:../../packages/remark-lint - '@opennextjs/cloudflare': - specifier: ^1.19.3 - version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) '@playwright/test': specifier: ^1.58.2 version: 1.58.2 - '@sentry/cloudflare': - specifier: ^10.49.0 - version: 10.49.0(@cloudflare/workers-types@4.20260422.1) '@testing-library/user-event': specifier: ~14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) @@ -304,12 +277,86 @@ importers: user-agent-data-types: specifier: 0.4.2 version: 0.4.2 + + packages/i18n: + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.9.3 + + packages/platform-cloudflare: + dependencies: + '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': + specifier: ^0.0.1 + version: 0.0.1 + '@opennextjs/cloudflare': + specifier: ^1.19.3 + version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) + '@sentry/cloudflare': + specifier: ^10.49.0 + version: 10.49.0(@cloudflare/workers-types@4.20260422.1) + next: + specifier: 16.2.4 + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 wrangler: specifier: ^4.77.0 version: 4.77.0(@cloudflare/workers-types@4.20260422.1) + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260418.1 + version: 4.20260422.1 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/node': + specifier: 'catalog:' + version: 24.10.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 - packages/i18n: + packages/platform-vercel: + dependencies: + '@opentelemetry/api-logs': + specifier: ~0.213.0 + version: 0.213.0 + '@opentelemetry/instrumentation': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ~1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@vercel/analytics': + specifier: ~2.0.1 + version: 2.0.1(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@vercel/otel': + specifier: ~2.1.1 + version: 2.1.1(@opentelemetry/api-logs@0.213.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)) + '@vercel/speed-insights': + specifier: ~2.0.0 + version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + next: + specifier: 16.2.4 + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 typescript: specifier: 'catalog:' version: 5.9.3 @@ -2252,10 +2299,6 @@ packages: resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -4388,11 +4431,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.10.9: - resolution: {integrity: sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==} - engines: {node: '>=6.0.0'} - hasBin: true - bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -4480,9 +4518,6 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - caniuse-lite@1.0.30001780: - resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} - caniuse-lite@1.0.30001784: resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} @@ -10588,9 +10623,7 @@ snapshots: '@opentelemetry/api-logs@0.213.0': dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} @@ -12750,8 +12783,6 @@ snapshots: baseline-browser-mapping@2.10.13: {} - baseline-browser-mapping@2.10.9: {} - bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -12804,8 +12835,8 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.9 - caniuse-lite: 1.0.30001780 + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 electron-to-chromium: 1.5.321 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -12866,8 +12897,6 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 - caniuse-lite@1.0.30001780: {} - caniuse-lite@1.0.30001784: {} case-sensitive-paths-webpack-plugin@2.4.0: {} From 1a61747e7a09f2efbce9bdbef1e56175dda4f99d Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 14:48:26 -0300 Subject: [PATCH 03/16] fix: use project-relative alias strings in default platform config Why: Turbopack's `resolveAlias` treats absolute paths as server-relative (prepending `./`) and rejects them with "server relative imports are not implemented yet". The `fileURLToPath(new URL(...))` pattern produced absolute paths that broke the plain `pnpm build` CI job. Project-relative strings resolve correctly in both Turbopack and webpack. --- apps/site/next.platform.config.mjs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/site/next.platform.config.mjs b/apps/site/next.platform.config.mjs index 0bf1290a21545..22384f7a9b0ac 100644 --- a/apps/site/next.platform.config.mjs +++ b/apps/site/next.platform.config.mjs @@ -1,5 +1,3 @@ -import { fileURLToPath } from 'node:url'; - /** * Default (no-op) platform config used when no `DEPLOY_TARGET` is set — * local dev, static export, generic hosting, etc. @@ -7,15 +5,15 @@ import { fileURLToPath } from 'node:url'; * Platform deployments (Vercel, Cloudflare, …) provide their own * `next.platform.config.mjs` that overrides these values. Keep this * file free of any platform-specific code. + * + * Alias values are project-relative strings (not absolute paths) so + * Turbopack resolves them correctly — Turbopack treats absolute paths + * as server-relative and rejects them. */ export default { aliases: { - '@platform/analytics': fileURLToPath( - new URL('./platform/analytics.tsx', import.meta.url) - ), - '@platform/instrumentation': fileURLToPath( - new URL('./platform/instrumentation.ts', import.meta.url) - ), + '@platform/analytics': './platform/analytics.tsx', + '@platform/instrumentation': './platform/instrumentation.ts', }, mdx: { // Defaults for local dev / static export / generic hosting. Platform From 78a6852d33ae772f984cb0267f90b2d9d9148de7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 18:56:38 +0000 Subject: [PATCH 04/16] fix(vercel): restore mdx defaults for shiki Assisted-by: Codex 5.3 Co-authored-by: Claudio Wunder --- .../__tests__/next.platform.config.test.mjs | 13 +++++++++++++ packages/platform-vercel/next.platform.config.mjs | 6 ++++++ 2 files changed, 19 insertions(+) create mode 100644 packages/platform-vercel/__tests__/next.platform.config.test.mjs diff --git a/packages/platform-vercel/__tests__/next.platform.config.test.mjs b/packages/platform-vercel/__tests__/next.platform.config.test.mjs new file mode 100644 index 0000000000000..47d857a0e6122 --- /dev/null +++ b/packages/platform-vercel/__tests__/next.platform.config.test.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +describe('platform-vercel next.platform.config', () => { + it('defines shiki mdx defaults for Vercel builds', async () => { + const { default: platform } = await import('../next.platform.config.mjs'); + + assert.deepEqual(platform.mdx, { + wasm: true, + twoslash: true, + }); + }); +}); diff --git a/packages/platform-vercel/next.platform.config.mjs b/packages/platform-vercel/next.platform.config.mjs index d24ca1d083cbd..f4ee425e95476 100644 --- a/packages/platform-vercel/next.platform.config.mjs +++ b/packages/platform-vercel/next.platform.config.mjs @@ -25,4 +25,10 @@ export default { '@platform/analytics': '@node-core/platform-vercel/analytics', '@platform/instrumentation': '@node-core/platform-vercel/instrumentation', }, + mdx: { + // Vercel supports the fast Oniguruma WASM engine and twoslash transforms, + // so keep parity with the default standalone config. + wasm: true, + twoslash: true, + }, }; From 517dfcb1284b9fc6372f6df7543101a4386d3601 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:01:29 +0000 Subject: [PATCH 05/16] chore(deps): catalog next versions across workspace Assisted-by: Codex 5.3 --- apps/site/package.json | 2 +- packages/platform-cloudflare/package.json | 2 +- packages/platform-vercel/package.json | 2 +- pnpm-lock.yaml | 19 +++++++------------ pnpm-workspace.yaml | 1 + 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/site/package.json b/apps/site/package.json index 60a098982f03e..bed312b5c2467 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -51,7 +51,7 @@ "github-slugger": "~2.0.0", "gray-matter": "~4.0.3", "mdast-util-to-string": "^4.0.0", - "next": "16.2.4", + "next": "catalog:", "next-intl": "~4.9.1", "next-themes": "~0.4.6", "postcss-calc": "~10.1.1", diff --git a/packages/platform-cloudflare/package.json b/packages/platform-cloudflare/package.json index bde3f2e55b221..f97b4b81c1f23 100644 --- a/packages/platform-cloudflare/package.json +++ b/packages/platform-cloudflare/package.json @@ -29,7 +29,7 @@ "wrangler": "^4.77.0" }, "peerDependencies": { - "next": "16.2.4", + "next": "catalog:", "react": "catalog:" }, "devDependencies": { diff --git a/packages/platform-vercel/package.json b/packages/platform-vercel/package.json index 05661584e9592..ec59bfd601fc1 100644 --- a/packages/platform-vercel/package.json +++ b/packages/platform-vercel/package.json @@ -27,7 +27,7 @@ "@vercel/speed-insights": "~2.0.0" }, "peerDependencies": { - "next": "16.2.4", + "next": "catalog:", "react": "catalog:" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16d10577b7314..e8d851233f58e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ catalogs: cross-env: specifier: ^10.0.0 version: 10.1.0 + next: + specifier: 16.2.4 + version: 16.2.4 react: specifier: ^19.2.4 version: 19.2.4 @@ -148,7 +151,7 @@ importers: specifier: ^4.0.0 version: 4.0.0 next: - specifier: 16.2.4 + specifier: 'catalog:' version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-intl: specifier: ~4.9.1 @@ -296,7 +299,7 @@ importers: specifier: ^10.49.0 version: 10.49.0(@cloudflare/workers-types@4.20260422.1) next: - specifier: 16.2.4 + specifier: 'catalog:' version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 'catalog:' @@ -345,7 +348,7 @@ importers: specifier: ~2.0.0 version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) next: - specifier: 16.2.4 + specifier: 'catalog:' version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 'catalog:' @@ -1198,9 +1201,6 @@ packages: '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.9.0': - resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} - '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} @@ -9660,11 +9660,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -10153,7 +10148,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.0 + '@emnapi/runtime': 1.9.1 optional: true '@img/sharp-win32-arm64@0.34.5': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 71a45db007786..826c636cd76b7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,7 @@ catalog: '@types/react': ^19.2.13 classnames: ~2.5.1 cross-env: ^10.0.0 + next: 16.2.4 react: ^19.2.4 tailwindcss: ~4.1.17 typescript: 5.9.3 From 6664f190dd1009e1dd2a9b17fe44c34322168c3d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:40:44 +0000 Subject: [PATCH 06/16] refactor(config): restrict platform nextConfig overrides Assisted-by: Codex 5.3 --- apps/site/next.config.mjs | 12 +++++++++- .../next.platform.config.d.ts | 22 +++++++++++++++++++ .../platform-vercel/next.platform.config.d.ts | 22 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/platform-cloudflare/next.platform.config.d.ts create mode 100644 packages/platform-vercel/next.platform.config.d.ts diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index aad1a7e20072b..cc85d4d13175b 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -21,6 +21,16 @@ const { default: platform } = DEPLOY_TARGET ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) : await import('./next.platform.config.mjs'); +/** + * Apply only an explicit allowlist of Next.js config keys from platform + * packages so critical core config (e.g. `webpack`, `turbopack`) cannot be + * overridden accidentally. + */ +const platformNextConfigOverrides = { + env: platform.nextConfig?.env, + deploymentId: platform.nextConfig?.deploymentId, +}; + /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -96,7 +106,7 @@ const nextConfig = { ...config, resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } }, }), - ...platform.nextConfig, + ...platformNextConfigOverrides, }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); diff --git a/packages/platform-cloudflare/next.platform.config.d.ts b/packages/platform-cloudflare/next.platform.config.d.ts new file mode 100644 index 0000000000000..d665c93b0ad2e --- /dev/null +++ b/packages/platform-cloudflare/next.platform.config.d.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from 'next'; + +type PlatformMdxConfig = { + wasm?: boolean; + twoslash?: boolean; +}; + +type PlatformNextConfig = { + deploymentId?: string; + env?: NextConfig['env']; +}; + +export type PlatformConfig = { + aliases?: Record; + images?: NextConfig['images']; + mdx?: PlatformMdxConfig; + nextConfig?: PlatformNextConfig; +}; + +declare const config: PlatformConfig; + +export default config; diff --git a/packages/platform-vercel/next.platform.config.d.ts b/packages/platform-vercel/next.platform.config.d.ts new file mode 100644 index 0000000000000..d665c93b0ad2e --- /dev/null +++ b/packages/platform-vercel/next.platform.config.d.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from 'next'; + +type PlatformMdxConfig = { + wasm?: boolean; + twoslash?: boolean; +}; + +type PlatformNextConfig = { + deploymentId?: string; + env?: NextConfig['env']; +}; + +export type PlatformConfig = { + aliases?: Record; + images?: NextConfig['images']; + mdx?: PlatformMdxConfig; + nextConfig?: PlatformNextConfig; +}; + +declare const config: PlatformConfig; + +export default config; From 0ac46dcc6e72c82fad758648741c52c241748b4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:45:47 +0000 Subject: [PATCH 07/16] refactor(types): enforce platform nextConfig via type-only constraints Assisted-by: Codex 5.3 --- apps/site/next.config.mjs | 12 +----------- .../platform-cloudflare/next.platform.config.d.ts | 14 +++++--------- packages/platform-vercel/next.platform.config.d.ts | 13 +++++-------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index cc85d4d13175b..aad1a7e20072b 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -21,16 +21,6 @@ const { default: platform } = DEPLOY_TARGET ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) : await import('./next.platform.config.mjs'); -/** - * Apply only an explicit allowlist of Next.js config keys from platform - * packages so critical core config (e.g. `webpack`, `turbopack`) cannot be - * overridden accidentally. - */ -const platformNextConfigOverrides = { - env: platform.nextConfig?.env, - deploymentId: platform.nextConfig?.deploymentId, -}; - /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -106,7 +96,7 @@ const nextConfig = { ...config, resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } }, }), - ...platformNextConfigOverrides, + ...platform.nextConfig, }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); diff --git a/packages/platform-cloudflare/next.platform.config.d.ts b/packages/platform-cloudflare/next.platform.config.d.ts index d665c93b0ad2e..6bcbd9719bdf9 100644 --- a/packages/platform-cloudflare/next.platform.config.d.ts +++ b/packages/platform-cloudflare/next.platform.config.d.ts @@ -1,14 +1,10 @@ import type { NextConfig } from 'next'; -type PlatformMdxConfig = { - wasm?: boolean; - twoslash?: boolean; -}; - -type PlatformNextConfig = { - deploymentId?: string; - env?: NextConfig['env']; -}; +type PlatformMdxConfig = Pick< + import('@node-core/rehype-shiki').HighlighterOptions, + 'wasm' | 'twoslash' +>; +type PlatformNextConfig = Pick; export type PlatformConfig = { aliases?: Record; diff --git a/packages/platform-vercel/next.platform.config.d.ts b/packages/platform-vercel/next.platform.config.d.ts index d665c93b0ad2e..461ef293fecd9 100644 --- a/packages/platform-vercel/next.platform.config.d.ts +++ b/packages/platform-vercel/next.platform.config.d.ts @@ -1,14 +1,11 @@ import type { NextConfig } from 'next'; -type PlatformMdxConfig = { - wasm?: boolean; - twoslash?: boolean; -}; +type PlatformMdxConfig = Pick< + import('@node-core/rehype-shiki').HighlighterOptions, + 'wasm' | 'twoslash' +>; -type PlatformNextConfig = { - deploymentId?: string; - env?: NextConfig['env']; -}; +type PlatformNextConfig = Pick; export type PlatformConfig = { aliases?: Record; From 4641a0c1a55b32caf4c4fd44c4ae5fadb16af27e Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 17:47:49 -0300 Subject: [PATCH 08/16] refactor: move platform workspaces from packages/ to apps/ and decouple site deps Platform packages are internal deployment wrappers, not publishable artifacts. Moving them out of packages/ keeps the publish workflow's globs untouched and matches the repo convention (apps/* = internal, packages/* = published). Also removes @node-core/platform-vercel and @node-core/platform-cloudflare from apps/site's dependencies. They're now declared as optional peer dependencies so: - A standalone install (no DEPLOY_TARGET) pulls in zero platform deps. - Vercel's installCommand scopes install to @node-core/website and @node-core/platform-vercel (skipping Cloudflare/OpenNext deps). - The Cloudflare deploy workflow scopes install to @node-core/website and @node-core/platform-cloudflare (skipping @vercel/* deps). Co-Authored-By: Claude Opus 4.7 --- .github/CODEOWNERS | 8 +- .../tmp-cloudflare-open-next-deploy.yml | 6 +- .../cloudflare}/next.platform.config.d.ts | 0 .../cloudflare}/next.platform.config.mjs | 0 .../cloudflare}/open-next.config.ts | 0 .../cloudflare}/package.json | 8 +- .../playwright.platform.config.d.ts | 0 .../playwright.platform.config.mjs | 0 .../cloudflare}/src/analytics.tsx | 0 .../cloudflare}/src/image-loader.ts | 0 .../cloudflare}/src/instrumentation.ts | 0 .../cloudflare}/src/worker-entrypoint.ts | 4 +- .../cloudflare}/tsconfig.json | 0 .../cloudflare}/turbo.json | 12 +-- .../cloudflare}/wrangler.jsonc | 12 +-- apps/site/package.json | 14 ++- apps/site/vercel.json | 2 +- .../__tests__/next.platform.config.test.mjs | 0 .../vercel}/next.platform.config.d.ts | 0 .../vercel}/next.platform.config.mjs | 0 .../vercel}/package.json | 2 +- .../vercel}/playwright.platform.config.d.ts | 0 .../vercel}/playwright.platform.config.mjs | 0 .../vercel}/src/analytics.tsx | 0 .../vercel}/src/instrumentation.ts | 0 .../vercel}/tsconfig.json | 0 docs/cloudflare-build-and-deployment.md | 16 ++-- docs/technologies.md | 14 +-- pnpm-lock.yaml | 92 +++++++++---------- 29 files changed, 100 insertions(+), 90 deletions(-) rename {packages/platform-cloudflare => apps/cloudflare}/next.platform.config.d.ts (100%) rename {packages/platform-cloudflare => apps/cloudflare}/next.platform.config.mjs (100%) rename {packages/platform-cloudflare => apps/cloudflare}/open-next.config.ts (100%) rename {packages/platform-cloudflare => apps/cloudflare}/package.json (65%) rename {packages/platform-cloudflare => apps/cloudflare}/playwright.platform.config.d.ts (100%) rename {packages/platform-cloudflare => apps/cloudflare}/playwright.platform.config.mjs (100%) rename {packages/platform-cloudflare => apps/cloudflare}/src/analytics.tsx (100%) rename {packages/platform-cloudflare => apps/cloudflare}/src/image-loader.ts (100%) rename {packages/platform-cloudflare => apps/cloudflare}/src/instrumentation.ts (100%) rename {packages/platform-cloudflare => apps/cloudflare}/src/worker-entrypoint.ts (90%) rename {packages/platform-cloudflare => apps/cloudflare}/tsconfig.json (100%) rename {packages/platform-cloudflare => apps/cloudflare}/turbo.json (68%) rename {packages/platform-cloudflare => apps/cloudflare}/wrangler.jsonc (80%) rename {packages/platform-vercel => apps/vercel}/__tests__/next.platform.config.test.mjs (100%) rename {packages/platform-vercel => apps/vercel}/next.platform.config.d.ts (100%) rename {packages/platform-vercel => apps/vercel}/next.platform.config.mjs (100%) rename {packages/platform-vercel => apps/vercel}/package.json (95%) rename {packages/platform-vercel => apps/vercel}/playwright.platform.config.d.ts (100%) rename {packages/platform-vercel => apps/vercel}/playwright.platform.config.mjs (100%) rename {packages/platform-vercel => apps/vercel}/src/analytics.tsx (100%) rename {packages/platform-vercel => apps/vercel}/src/instrumentation.ts (100%) rename {packages/platform-vercel => apps/vercel}/tsconfig.json (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a95e2867f4191..e781e2d73c6e5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,10 +27,10 @@ turbo.json @nodejs/nodejs-website @nodejs/web-infra crowdin.yml @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra apps/site/site.json @nodejs/web-infra -packages/platform-cloudflare/wrangler.jsonc @nodejs/web-infra -packages/platform-cloudflare/open-next.config.ts @nodejs/web-infra -packages/platform-cloudflare/next.platform.config.mjs @nodejs/web-infra -packages/platform-vercel/next.platform.config.mjs @nodejs/web-infra +apps/cloudflare/wrangler.jsonc @nodejs/web-infra +apps/cloudflare/open-next.config.ts @nodejs/web-infra +apps/cloudflare/next.platform.config.mjs @nodejs/web-infra +apps/vercel/next.platform.config.mjs @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra # Critical Documents diff --git a/.github/workflows/tmp-cloudflare-open-next-deploy.yml b/.github/workflows/tmp-cloudflare-open-next-deploy.yml index 192dc9d24b86a..d6a0b8b1dee37 100644 --- a/.github/workflows/tmp-cloudflare-open-next-deploy.yml +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -50,18 +50,18 @@ jobs: cache: 'pnpm' - name: Install packages - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --filter=@node-core/website... --filter=@node-core/platform-cloudflare... - name: Build blog data working-directory: apps/site run: node --run build:blog-data - name: Build open-next site - working-directory: apps/site + working-directory: apps/cloudflare run: node --run cloudflare:build:worker - name: Deploy open-next site - working-directory: apps/site + working-directory: apps/cloudflare run: node --run cloudflare:deploy env: CF_WORKERS_SCRIPTS_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/packages/platform-cloudflare/next.platform.config.d.ts b/apps/cloudflare/next.platform.config.d.ts similarity index 100% rename from packages/platform-cloudflare/next.platform.config.d.ts rename to apps/cloudflare/next.platform.config.d.ts diff --git a/packages/platform-cloudflare/next.platform.config.mjs b/apps/cloudflare/next.platform.config.mjs similarity index 100% rename from packages/platform-cloudflare/next.platform.config.mjs rename to apps/cloudflare/next.platform.config.mjs diff --git a/packages/platform-cloudflare/open-next.config.ts b/apps/cloudflare/open-next.config.ts similarity index 100% rename from packages/platform-cloudflare/open-next.config.ts rename to apps/cloudflare/open-next.config.ts diff --git a/packages/platform-cloudflare/package.json b/apps/cloudflare/package.json similarity index 65% rename from packages/platform-cloudflare/package.json rename to apps/cloudflare/package.json index f97b4b81c1f23..731983e3b6fec 100644 --- a/packages/platform-cloudflare/package.json +++ b/apps/cloudflare/package.json @@ -14,12 +14,12 @@ "repository": { "type": "git", "url": "https://github.com/nodejs/nodejs.org", - "directory": "packages/platform-cloudflare" + "directory": "apps/cloudflare" }, "scripts": { - "cloudflare:build:worker": "cd ../../apps/site && opennextjs-cloudflare build --openNextConfigPath ../../packages/platform-cloudflare/open-next.config.ts --config ../../packages/platform-cloudflare/wrangler.jsonc", - "cloudflare:deploy": "cd ../../apps/site && opennextjs-cloudflare deploy --openNextConfigPath ../../packages/platform-cloudflare/open-next.config.ts --config ../../packages/platform-cloudflare/wrangler.jsonc", - "cloudflare:preview": "cd ../../apps/site && wrangler dev --config ../../packages/platform-cloudflare/wrangler.jsonc", + "cloudflare:build:worker": "cd ../site && opennextjs-cloudflare build --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", + "cloudflare:deploy": "cd ../site && opennextjs-cloudflare deploy --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", + "cloudflare:preview": "cd ../site && wrangler dev --config ../cloudflare/wrangler.jsonc", "lint:types": "tsc --noEmit" }, "dependencies": { diff --git a/packages/platform-cloudflare/playwright.platform.config.d.ts b/apps/cloudflare/playwright.platform.config.d.ts similarity index 100% rename from packages/platform-cloudflare/playwright.platform.config.d.ts rename to apps/cloudflare/playwright.platform.config.d.ts diff --git a/packages/platform-cloudflare/playwright.platform.config.mjs b/apps/cloudflare/playwright.platform.config.mjs similarity index 100% rename from packages/platform-cloudflare/playwright.platform.config.mjs rename to apps/cloudflare/playwright.platform.config.mjs diff --git a/packages/platform-cloudflare/src/analytics.tsx b/apps/cloudflare/src/analytics.tsx similarity index 100% rename from packages/platform-cloudflare/src/analytics.tsx rename to apps/cloudflare/src/analytics.tsx diff --git a/packages/platform-cloudflare/src/image-loader.ts b/apps/cloudflare/src/image-loader.ts similarity index 100% rename from packages/platform-cloudflare/src/image-loader.ts rename to apps/cloudflare/src/image-loader.ts diff --git a/packages/platform-cloudflare/src/instrumentation.ts b/apps/cloudflare/src/instrumentation.ts similarity index 100% rename from packages/platform-cloudflare/src/instrumentation.ts rename to apps/cloudflare/src/instrumentation.ts diff --git a/packages/platform-cloudflare/src/worker-entrypoint.ts b/apps/cloudflare/src/worker-entrypoint.ts similarity index 90% rename from packages/platform-cloudflare/src/worker-entrypoint.ts rename to apps/cloudflare/src/worker-entrypoint.ts index f63ab60a6f586..9476169e669d7 100644 --- a/packages/platform-cloudflare/src/worker-entrypoint.ts +++ b/apps/cloudflare/src/worker-entrypoint.ts @@ -11,7 +11,7 @@ import type { Request, } from '@cloudflare/workers-types'; -import { default as handler } from '../../../apps/site/.open-next/worker.js'; +import { default as handler } from '../../site/.open-next/worker.js'; export default withSentry( (env: { @@ -50,4 +50,4 @@ export default withSentry( } ); -export { DOQueueHandler } from '../../../apps/site/.open-next/worker.js'; +export { DOQueueHandler } from '../../site/.open-next/worker.js'; diff --git a/packages/platform-cloudflare/tsconfig.json b/apps/cloudflare/tsconfig.json similarity index 100% rename from packages/platform-cloudflare/tsconfig.json rename to apps/cloudflare/tsconfig.json diff --git a/packages/platform-cloudflare/turbo.json b/apps/cloudflare/turbo.json similarity index 68% rename from packages/platform-cloudflare/turbo.json rename to apps/cloudflare/turbo.json index 8c1ffdc95a974..810960a02292a 100644 --- a/packages/platform-cloudflare/turbo.json +++ b/apps/cloudflare/turbo.json @@ -8,13 +8,13 @@ "open-next.config.ts", "wrangler.jsonc", "src/**/*.ts", - "../../apps/site/{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "../../apps/site/{app,components,layouts,pages,styles}/**/*.css", - "../../apps/site/{next-data,scripts,i18n}/**/*.{mjs,json}", - "../../apps/site/{app,pages}/**/*.{mdx,md}", - "../../apps/site/*.{md,mdx,json,ts,tsx,mjs,yml}" + "../site/{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", + "../site/{app,components,layouts,pages,styles}/**/*.css", + "../site/{next-data,scripts,i18n}/**/*.{mjs,json}", + "../site/{app,pages}/**/*.{mdx,md}", + "../site/*.{md,mdx,json,ts,tsx,mjs,yml}" ], - "outputs": ["../../apps/site/.open-next/**"], + "outputs": ["../site/.open-next/**"], "env": [ "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_BASE_URL", diff --git a/packages/platform-cloudflare/wrangler.jsonc b/apps/cloudflare/wrangler.jsonc similarity index 80% rename from packages/platform-cloudflare/wrangler.jsonc rename to apps/cloudflare/wrangler.jsonc index f3ec84ef5f325..90ccce6e97616 100644 --- a/packages/platform-cloudflare/wrangler.jsonc +++ b/apps/cloudflare/wrangler.jsonc @@ -8,7 +8,7 @@ "minify": true, "keep_names": false, "assets": { - "directory": "../../apps/site/.open-next/assets", + "directory": "../site/.open-next/assets", "binding": "ASSETS", "run_worker_first": true, }, @@ -33,14 +33,14 @@ "build": { // Run the asset polyfiller from apps/site so that `pages`, `snippets`, and // the `.open-next` output directory resolve against the Next.js app. - "cwd": "../../apps/site", + "cwd": "../site", "command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets", }, "alias": { - "node:fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "node:fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", - "fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "node:fs": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "node:fs/promises": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "fs": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "fs/promises": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", }, "r2_buckets": [ { diff --git a/apps/site/package.json b/apps/site/package.json index bed312b5c2467..cc3490d9072ba 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -31,8 +31,6 @@ "dependencies": { "@heroicons/react": "~2.2.0", "@mdx-js/mdx": "^3.1.1", - "@node-core/platform-cloudflare": "workspace:*", - "@node-core/platform-vercel": "workspace:*", "@node-core/rehype-shiki": "workspace:*", "@node-core/ui-components": "workspace:*", "@node-core/website-i18n": "workspace:*", @@ -98,6 +96,18 @@ "typescript-eslint": "~8.57.2", "user-agent-data-types": "0.4.2" }, + "peerDependencies": { + "@node-core/platform-cloudflare": "workspace:*", + "@node-core/platform-vercel": "workspace:*" + }, + "peerDependenciesMeta": { + "@node-core/platform-cloudflare": { + "optional": true + }, + "@node-core/platform-vercel": { + "optional": true + } + }, "imports": { "#site/*": [ "./*", diff --git a/apps/site/vercel.json b/apps/site/vercel.json index 37bf0655bfc6d..2359ac19dd2b7 100644 --- a/apps/site/vercel.json +++ b/apps/site/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "installCommand": "pnpm install --prod --frozen-lockfile", + "installCommand": "pnpm install --prod --frozen-lockfile --filter=@node-core/website... --filter=@node-core/platform-vercel...", "buildCommand": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm build", "ignoreCommand": "[[ \"$VERCEL_GIT_COMMIT_REF\" =~ \"^dependabot/.*\" || \"$VERCEL_GIT_COMMIT_REF\" =~ \"^gh-readonly-queue/.*\" ]]" } diff --git a/packages/platform-vercel/__tests__/next.platform.config.test.mjs b/apps/vercel/__tests__/next.platform.config.test.mjs similarity index 100% rename from packages/platform-vercel/__tests__/next.platform.config.test.mjs rename to apps/vercel/__tests__/next.platform.config.test.mjs diff --git a/packages/platform-vercel/next.platform.config.d.ts b/apps/vercel/next.platform.config.d.ts similarity index 100% rename from packages/platform-vercel/next.platform.config.d.ts rename to apps/vercel/next.platform.config.d.ts diff --git a/packages/platform-vercel/next.platform.config.mjs b/apps/vercel/next.platform.config.mjs similarity index 100% rename from packages/platform-vercel/next.platform.config.mjs rename to apps/vercel/next.platform.config.mjs diff --git a/packages/platform-vercel/package.json b/apps/vercel/package.json similarity index 95% rename from packages/platform-vercel/package.json rename to apps/vercel/package.json index ec59bfd601fc1..513ebee856d34 100644 --- a/packages/platform-vercel/package.json +++ b/apps/vercel/package.json @@ -12,7 +12,7 @@ "repository": { "type": "git", "url": "https://github.com/nodejs/nodejs.org", - "directory": "packages/platform-vercel" + "directory": "apps/vercel" }, "scripts": { "lint:types": "tsc --noEmit" diff --git a/packages/platform-vercel/playwright.platform.config.d.ts b/apps/vercel/playwright.platform.config.d.ts similarity index 100% rename from packages/platform-vercel/playwright.platform.config.d.ts rename to apps/vercel/playwright.platform.config.d.ts diff --git a/packages/platform-vercel/playwright.platform.config.mjs b/apps/vercel/playwright.platform.config.mjs similarity index 100% rename from packages/platform-vercel/playwright.platform.config.mjs rename to apps/vercel/playwright.platform.config.mjs diff --git a/packages/platform-vercel/src/analytics.tsx b/apps/vercel/src/analytics.tsx similarity index 100% rename from packages/platform-vercel/src/analytics.tsx rename to apps/vercel/src/analytics.tsx diff --git a/packages/platform-vercel/src/instrumentation.ts b/apps/vercel/src/instrumentation.ts similarity index 100% rename from packages/platform-vercel/src/instrumentation.ts rename to apps/vercel/src/instrumentation.ts diff --git a/packages/platform-vercel/tsconfig.json b/apps/vercel/tsconfig.json similarity index 100% rename from packages/platform-vercel/tsconfig.json rename to apps/vercel/tsconfig.json diff --git a/docs/cloudflare-build-and-deployment.md b/docs/cloudflare-build-and-deployment.md index d021b409981d2..3f5cc90d9f0d9 100644 --- a/docs/cloudflare-build-and-deployment.md +++ b/docs/cloudflare-build-and-deployment.md @@ -2,14 +2,14 @@ The Node.js Website can be built using the [OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare). Such build generates a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) that can be deployed on the [Cloudflare](https://www.cloudflare.com) network. -The build is gated on the `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` environment variable (set by the OpenNext `buildCommand`), which makes `apps/site` pull its Next.js, MDX, image-loader, and analytics overrides from [`@node-core/platform-cloudflare`](../packages/platform-cloudflare). See the [Deploy Target Selection](./technologies.md#deploy-target-selection-next_public_deploy_target) section of the Technologies document for the full platform-adapter contract. +The build is gated on the `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` environment variable (set by the OpenNext `buildCommand`), which makes `apps/site` pull its Next.js, MDX, image-loader, and analytics overrides from [`@node-core/platform-cloudflare`](../apps/cloudflare). See the [Deploy Target Selection](./technologies.md#deploy-target-selection-next_public_deploy_target) section of the Technologies document for the full platform-adapter contract. ## Configurations -All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../packages/platform-cloudflare) package. The two key configuration files are: +All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../apps/cloudflare) package. The two key configuration files are: -- [`packages/platform-cloudflare/wrangler.jsonc`](../packages/platform-cloudflare/wrangler.jsonc) — the Wrangler configuration -- [`packages/platform-cloudflare/open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts) — the OpenNext adapter configuration +- [`apps/cloudflare/wrangler.jsonc`](../apps/cloudflare/wrangler.jsonc) — the Wrangler configuration +- [`apps/cloudflare/open-next.config.ts`](../apps/cloudflare/open-next.config.ts) — the OpenNext adapter configuration ### Wrangler Configuration @@ -19,7 +19,7 @@ For more details, refer to the [Wrangler documentation](https://developers.cloud Key configurations include: -- `main`: Points to a custom worker entry point ([`packages/platform-cloudflare/src/worker-entrypoint.ts`](../packages/platform-cloudflare/src/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). +- `main`: Points to a custom worker entry point ([`apps/cloudflare/src/worker-entrypoint.ts`](../apps/cloudflare/src/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). - `account_id`: Specifies the Cloudflare account ID. This is not required for local previews but is necessary for deployments. You can obtain an account ID for free by signing up at [dash.cloudflare.com](https://dash.cloudflare.com/login). - This is currently set to `fb4a2d0f103c6ff38854ac69eb709272`, which is the ID of a Cloudflare account controlled by Node.js, and used for testing. - `build`: Defines the build command to generate the Node.js filesystem polyfills required for the application to run on Cloudflare Workers. This uses the [`@flarelabs/wrangler-build-time-fs-assets-polyfilling`](https://github.com/flarelabs-net/wrangler-build-time-fs-assets-polyfilling) package. @@ -54,15 +54,15 @@ Additionally, when deploying, an extra `CF_WORKERS_SCRIPTS_API_TOKEN` environmen ### Image loader -When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`packages/platform-cloudflare/next.platform.config.mjs`](../packages/platform-cloudflare/next.platform.config.mjs)) contributes it via the `images.loaderFile` field, which is merged into the shared Next.js config when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts)). +When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`apps/cloudflare/next.platform.config.mjs`](../apps/cloudflare/next.platform.config.mjs)) contributes it via the `images.loaderFile` field, which is merged into the shared Next.js config when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../apps/cloudflare/open-next.config.ts)). -The custom loader can be found at [`packages/platform-cloudflare/src/image-loader.ts`](../packages/platform-cloudflare/src/image-loader.ts). +The custom loader can be found at [`apps/cloudflare/src/image-loader.ts`](../apps/cloudflare/src/image-loader.ts). For more details on this see: https://developers.cloudflare.com/images/transform-images/integrate-with-frameworks/#global-loader ### Custom Worker Entry Point -Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`packages/platform-cloudflare/src/worker-entrypoint.ts`](../packages/platform-cloudflare/src/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). +Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`apps/cloudflare/src/worker-entrypoint.ts`](../apps/cloudflare/src/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). The custom entry point imports the OpenNext-generated handler from `.open-next/worker.js` and re-exports the `DOQueueHandler` Durable Object needed by the application. diff --git a/docs/technologies.md b/docs/technologies.md index cf8bfda8125c7..54e80de79634f 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -300,13 +300,13 @@ Benefits: `NEXT_PUBLIC_DEPLOY_TARGET` selects which platform adapter contributes its Next.js config, MDX flags, image loader, analytics, and Playwright webServer. It is consumed at build time by [`apps/site/next.config.mjs`](../apps/site/next.config.mjs), [`apps/site/mdx/plugins.mjs`](../apps/site/mdx/plugins.mjs), and [`apps/site/playwright.config.ts`](../apps/site/playwright.config.ts) via a dynamic import of `@node-core/platform-${target}/next.platform.config`. -| Value | Adapter | Set by | -| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `vercel` | [`@node-core/platform-vercel`](../packages/platform-vercel) | [`apps/site/vercel.json`](../apps/site/vercel.json) build env | -| `cloudflare` | [`@node-core/platform-cloudflare`](../packages/platform-cloudflare) | OpenNext `buildCommand` in [`open-next.config.ts`](../packages/platform-cloudflare/open-next.config.ts) | -| _(unset)_ | Falls back to the no-op defaults in [`apps/site/next.platform.config.mjs`](../apps/site/next.platform.config.mjs) and [`apps/site/playwright.platform.config.mjs`](../apps/site/playwright.platform.config.mjs) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | +| Value | Adapter | Set by | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `vercel` | [`@node-core/platform-vercel`](../apps/vercel) | [`apps/site/vercel.json`](../apps/site/vercel.json) build env | +| `cloudflare` | [`@node-core/platform-cloudflare`](../apps/cloudflare) | OpenNext `buildCommand` in [`open-next.config.ts`](../apps/cloudflare/open-next.config.ts) | +| _(unset)_ | Falls back to the no-op defaults in [`apps/site/next.platform.config.mjs`](../apps/site/next.platform.config.mjs) and [`apps/site/playwright.platform.config.mjs`](../apps/site/playwright.platform.config.mjs) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | -Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`packages/platform-vercel/next.platform.config.mjs`](../packages/platform-vercel/next.platform.config.mjs) and [`packages/platform-cloudflare/next.platform.config.mjs`](../packages/platform-cloudflare/next.platform.config.mjs) for reference. +Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`apps/vercel/next.platform.config.mjs`](../apps/vercel/next.platform.config.mjs) and [`apps/cloudflare/next.platform.config.mjs`](../apps/cloudflare/next.platform.config.mjs) for reference. #### Vercel Integration @@ -318,7 +318,7 @@ Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any #### Cloudflare Integration - OpenNext adapter builds a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) artifact from the Next.js build -- All Cloudflare-specific files (Wrangler config, OpenNext config, custom worker entrypoint, image loader) live in [`packages/platform-cloudflare`](../packages/platform-cloudflare) +- All Cloudflare-specific files (Wrangler config, OpenNext config, custom worker entrypoint, image loader) live in [`apps/cloudflare`](../apps/cloudflare) - See [Cloudflare build and deployment](./cloudflare-build-and-deployment.md) for details ### Package Management diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8d851233f58e..49581359c0467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,43 @@ importers: specifier: ~8.57.2 version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + apps/cloudflare: + dependencies: + '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': + specifier: ^0.0.1 + version: 0.0.1 + '@opennextjs/cloudflare': + specifier: ^1.19.3 + version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) + '@sentry/cloudflare': + specifier: ^10.49.0 + version: 10.49.0(@cloudflare/workers-types@4.20260422.1) + next: + specifier: 'catalog:' + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 + wrangler: + specifier: ^4.77.0 + version: 4.77.0(@cloudflare/workers-types@4.20260422.1) + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260418.1 + version: 4.20260422.1 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/node': + specifier: 'catalog:' + version: 24.10.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 + apps/site: dependencies: '@heroicons/react': @@ -92,10 +129,10 @@ importers: version: 3.1.1 '@node-core/platform-cloudflare': specifier: workspace:* - version: link:../../packages/platform-cloudflare + version: link:../cloudflare '@node-core/platform-vercel': specifier: workspace:* - version: link:../../packages/platform-vercel + version: link:../vercel '@node-core/rehype-shiki': specifier: workspace:* version: link:../../packages/rehype-shiki @@ -281,50 +318,7 @@ importers: specifier: 0.4.2 version: 0.4.2 - packages/i18n: - devDependencies: - typescript: - specifier: 'catalog:' - version: 5.9.3 - - packages/platform-cloudflare: - dependencies: - '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': - specifier: ^0.0.1 - version: 0.0.1 - '@opennextjs/cloudflare': - specifier: ^1.19.3 - version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) - '@sentry/cloudflare': - specifier: ^10.49.0 - version: 10.49.0(@cloudflare/workers-types@4.20260422.1) - next: - specifier: 'catalog:' - version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: - specifier: 'catalog:' - version: 19.2.4 - wrangler: - specifier: ^4.77.0 - version: 4.77.0(@cloudflare/workers-types@4.20260422.1) - devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20260418.1 - version: 4.20260422.1 - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 - '@types/node': - specifier: 'catalog:' - version: 24.10.1 - '@types/react': - specifier: 'catalog:' - version: 19.2.14 - typescript: - specifier: 'catalog:' - version: 5.9.3 - - packages/platform-vercel: + apps/vercel: dependencies: '@opentelemetry/api-logs': specifier: ~0.213.0 @@ -364,6 +358,12 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/i18n: + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/rehype-shiki: dependencies: '@shikijs/core': From 23bfde765945518b1af7f2c962990b6975db184e Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 17:49:43 -0300 Subject: [PATCH 09/16] docs: update Repository Structure to list apps/vercel and apps/cloudflare The platform adapter sub-trees moved out of packages/ in the previous commit. This refreshes the tree diagram so docs aren't misleading for new contributors. Co-Authored-By: Claude Opus 4.7 --- docs/technologies.md | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/technologies.md b/docs/technologies.md index 54e80de79634f..4b09b81f78051 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -122,22 +122,27 @@ We chose Next.js because it is: ``` nodejs.org/ ├── apps/ -│ └── site/ # Main website application -│ ├── components/ # Website-specific React components -│ ├── layouts/ # Page layout templates -│ ├── pages/ # Content pages (Markdown/MDX) -│ │ ├── en/ # English content (source) -│ │ └── {locale}/ # Translated content -│ ├── public/ # Static assets -│ │ └── static/ # Images, documents, etc. -│ ├── hooks/ # React hooks -│ ├── providers/ # React context providers -│ ├── types/ # TypeScript definitions -│ ├── next-data/ # Build-time data fetching -│ ├── scripts/ # Utility scripts -│ ├── snippets/ # Code snippets for download page -│ └── tests/ # Test files -│ └── e2e/ # End-to-end tests +│ ├── site/ # Main website application +│ │ ├── components/ # Website-specific React components +│ │ ├── layouts/ # Page layout templates +│ │ ├── pages/ # Content pages (Markdown/MDX) +│ │ │ ├── en/ # English content (source) +│ │ │ └── {locale}/ # Translated content +│ │ ├── public/ # Static assets +│ │ │ └── static/ # Images, documents, etc. +│ │ ├── hooks/ # React hooks +│ │ ├── providers/ # React context providers +│ │ ├── types/ # TypeScript definitions +│ │ ├── next-data/ # Build-time data fetching +│ │ ├── scripts/ # Utility scripts +│ │ ├── snippets/ # Code snippets for download page +│ │ └── tests/ # Test files +│ │ └── e2e/ # End-to-end tests +│ ├── vercel/ # Vercel deployment adapter +│ │ # (analytics, instrumentation, vercel.json) +│ └── cloudflare/ # Cloudflare deployment adapter +│ # (worker entrypoint, image loader, +│ # open-next.config.ts, wrangler.jsonc) └── packages/ ├── ui-components/ # Reusable UI components │ ├── styles/ # Global stylesheets @@ -145,10 +150,7 @@ nodejs.org/ ├── i18n/ # Internationalization │ ├── locales/ # Translation files │ └── config.json # Locale configuration - ├── rehype-shiki/ # Syntax highlighting plugin - ├── platform-vercel/ # Vercel platform adapter (analytics, instrumentation) - └── platform-cloudflare/ # Cloudflare platform adapter (worker entrypoint, - # image loader, open-next config, wrangler config) + └── rehype-shiki/ # Syntax highlighting plugin ``` ## Architecture Decisions From 87f739f98d1eb26b60687b540517a9566f1decc3 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:24:56 -0300 Subject: [PATCH 10/16] refactor(platform): resolve via #platform import map and unify type contract Replace the template-literal `await import()` pattern in next.config.mjs, mdx/plugins.mjs, and playwright.config.ts with a static `import from '#platform/...'` resolved by Node's subpath imports. Each platform's build command sets `NODE_OPTIONS=--conditions=` so the imports map selects the matching @node-core/platform-* package; without the flag, the site's local no-op file is used. Consolidate the duplicated PlatformConfig / PlatformPlaywrightConfig types into apps/site, the contract owner. Platform .mjs files JSDoc the site types via relative path; platform-side .d.ts duplicates are removed. Also: arrow-function instrumentation hooks; prune the redundant PLAYWRIGHT_BASE_URL env from the Cloudflare Playwright workflow and config (kept in the Vercel workflow where it carries the dynamic preview URL). --- .../playwright-cloudflare-open-next.yml | 2 +- apps/cloudflare/next.platform.config.mjs | 2 +- apps/cloudflare/open-next.config.ts | 2 +- .../cloudflare/playwright.platform.config.mjs | 4 ++-- apps/cloudflare/src/instrumentation.ts | 2 +- apps/cloudflare/tsconfig.json | 3 +-- apps/site/mdx/plugins.mjs | 15 ++++++--------- apps/site/next.config.mjs | 19 +++---------------- .../next.platform.config.d.ts | 10 ++++++---- apps/site/next.platform.config.mjs | 2 ++ apps/site/package.json | 12 +++++++++++- apps/site/platform/instrumentation.ts | 2 +- apps/site/playwright.config.ts | 15 +-------------- .../playwright.platform.config.d.ts | 5 +++++ apps/site/playwright.platform.config.mjs | 2 +- apps/site/vercel.json | 2 +- apps/vercel/next.platform.config.d.ts | 19 ------------------- apps/vercel/next.platform.config.mjs | 6 +++--- apps/vercel/playwright.platform.config.d.ts | 10 ---------- apps/vercel/playwright.platform.config.mjs | 2 +- apps/vercel/src/instrumentation.ts | 4 +--- apps/vercel/tsconfig.json | 3 +-- 22 files changed, 50 insertions(+), 93 deletions(-) rename apps/{cloudflare => site}/next.platform.config.d.ts (53%) rename apps/{cloudflare => site}/playwright.platform.config.d.ts (56%) delete mode 100644 apps/vercel/next.platform.config.d.ts delete mode 100644 apps/vercel/playwright.platform.config.d.ts diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 25997c9803a41..1ea6426b8d0c0 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -55,7 +55,7 @@ jobs: run: node --run playwright env: NEXT_PUBLIC_DEPLOY_TARGET: cloudflare - PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 + NODE_OPTIONS: --conditions=cloudflare - name: Upload Playwright test results if: always() diff --git a/apps/cloudflare/next.platform.config.mjs b/apps/cloudflare/next.platform.config.mjs index 7bfd123a2b975..2b7b03c0c7e01 100644 --- a/apps/cloudflare/next.platform.config.mjs +++ b/apps/cloudflare/next.platform.config.mjs @@ -12,7 +12,7 @@ const require = createRequire(import.meta.url); * Must export a default `{ nextConfig, aliases, images }` shape — any of * which may be omitted when the platform has nothing to contribute. * - * @type {import('@node-core/platform-cloudflare/next.platform.config').PlatformConfig} + * @type {import('../site/next.platform.config').PlatformConfig} */ export default { nextConfig: { diff --git a/apps/cloudflare/open-next.config.ts b/apps/cloudflare/open-next.config.ts index e627475ad60ad..5ef3a63965af1 100644 --- a/apps/cloudflare/open-next.config.ts +++ b/apps/cloudflare/open-next.config.ts @@ -21,7 +21,7 @@ const cloudflareConfig = defineCloudflareConfig({ const openNextConfig: OpenNextConfig = { ...cloudflareConfig, buildCommand: - 'cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm build --webpack', + 'cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare NODE_OPTIONS=--conditions=cloudflare pnpm build --webpack', cloudflare: { skewProtection: { enabled: true }, }, diff --git a/apps/cloudflare/playwright.platform.config.mjs b/apps/cloudflare/playwright.platform.config.mjs index 1a0cc1c3e3a53..011777c5b329b 100644 --- a/apps/cloudflare/playwright.platform.config.mjs +++ b/apps/cloudflare/playwright.platform.config.mjs @@ -5,7 +5,7 @@ * loader. Spins up the wrangler preview so E2E runs against the * OpenNext worker artifact rather than `next dev`. * - * @type {import('./playwright.platform.config').PlatformPlaywrightConfig} + * @type {import('../site/playwright.platform.config').PlatformPlaywrightConfig} */ export default { baseURL: 'http://127.0.0.1:8787', @@ -13,7 +13,7 @@ export default { stdout: 'pipe', command: '../../node_modules/.bin/turbo cloudflare:preview --filter=@node-core/platform-cloudflare', - url: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8787', + url: 'http://127.0.0.1:8787', timeout: 60_000 * 3, }, }; diff --git a/apps/cloudflare/src/instrumentation.ts b/apps/cloudflare/src/instrumentation.ts index a1c3920abc89d..605be34cb4a3d 100644 --- a/apps/cloudflare/src/instrumentation.ts +++ b/apps/cloudflare/src/instrumentation.ts @@ -1 +1 @@ -export function register() {} +export const register = () => {}; diff --git a/apps/cloudflare/tsconfig.json b/apps/cloudflare/tsconfig.json index 1189ad9fbc09a..95f515d1bbb7d 100644 --- a/apps/cloudflare/tsconfig.json +++ b/apps/cloudflare/tsconfig.json @@ -16,8 +16,7 @@ "include": [ "src", "next.platform.config.mjs", - "playwright.platform.config.mjs", - "playwright.platform.config.d.ts" + "playwright.platform.config.mjs" ], "exclude": ["src/worker-entrypoint.ts"] } diff --git a/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index b8af46fde8977..75c92d3ed9c59 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -7,16 +7,13 @@ import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; import readingTime from 'remark-reading-time'; -import { DEPLOY_TARGET } from '../next.constants.mjs'; -import remarkTableTitles from '../util/table'; +// MDX overrides contributed by the active deployment target. Resolved via +// the `#platform/next.platform.config` import map in `package.json`; each +// platform owns its own `{ wasm, twoslash }` defaults and the in-repo +// default file acts as the standalone fallback. +import platform from '#platform/next.platform.config'; -// Load MDX overrides contributed by the active deployment target. Keeps -// this module free of platform-specific branches — each platform owns -// its own `{ wasm, twoslash }` defaults via `next.platform.config.mjs`, -// with the in-repo default config serving as the standalone fallback. -const { default: platform } = DEPLOY_TARGET - ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) - : await import('../next.platform.config.mjs'); +import remarkTableTitles from '../util/table'; // Shiki is created out here to avoid an async rehype plugin const singletonShiki = await rehypeShikiji(platform.mdx); diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index aad1a7e20072b..386dca2df0297 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -2,25 +2,12 @@ import createNextIntlPlugin from 'next-intl/plugin'; -import { - BASE_PATH, - DEPLOY_TARGET, - ENABLE_STATIC_EXPORT, -} from './next.constants.mjs'; +import platform from '#platform/next.platform.config'; + +import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; -/** - * Loads the deployment platform's `next.platform.config.mjs` — falling back - * to the local no-op when no platform is active. Each platform package - * (`@node-core/platform-`) owns its own file and contributes - * `{ nextConfig, aliases, images }`. Adding a new platform only means - * creating a new `@node-core/platform-` package. - */ -const { default: platform } = DEPLOY_TARGET - ? await import(`@node-core/platform-${DEPLOY_TARGET}/next.platform.config`) - : await import('./next.platform.config.mjs'); - /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming diff --git a/apps/cloudflare/next.platform.config.d.ts b/apps/site/next.platform.config.d.ts similarity index 53% rename from apps/cloudflare/next.platform.config.d.ts rename to apps/site/next.platform.config.d.ts index 6bcbd9719bdf9..3b6449e5f942a 100644 --- a/apps/cloudflare/next.platform.config.d.ts +++ b/apps/site/next.platform.config.d.ts @@ -1,11 +1,13 @@ +import type { HighlighterOptions } from '@node-core/rehype-shiki'; import type { NextConfig } from 'next'; -type PlatformMdxConfig = Pick< - import('@node-core/rehype-shiki').HighlighterOptions, - 'wasm' | 'twoslash' ->; +type PlatformMdxConfig = Pick; type PlatformNextConfig = Pick; +/** + * Shared platform-config contract consumed by `apps/site/next.config.mjs` + * and implemented by each `@node-core/platform-` package. + */ export type PlatformConfig = { aliases?: Record; images?: NextConfig['images']; diff --git a/apps/site/next.platform.config.mjs b/apps/site/next.platform.config.mjs index 22384f7a9b0ac..1ad7c451d1fb0 100644 --- a/apps/site/next.platform.config.mjs +++ b/apps/site/next.platform.config.mjs @@ -9,6 +9,8 @@ * Alias values are project-relative strings (not absolute paths) so * Turbopack resolves them correctly — Turbopack treats absolute paths * as server-relative and rejects them. + * + * @type {import('./next.platform.config').PlatformConfig} */ export default { aliases: { diff --git a/apps/site/package.json b/apps/site/package.json index cc3490d9072ba..cb79963acbd6e 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -117,7 +117,17 @@ "./*/index.ts", "./*.mjs", "./*/index.mjs" - ] + ], + "#platform/next.platform.config": { + "cloudflare": "@node-core/platform-cloudflare/next.platform.config", + "vercel": "@node-core/platform-vercel/next.platform.config", + "default": "./next.platform.config.mjs" + }, + "#platform/playwright.platform.config": { + "cloudflare": "@node-core/platform-cloudflare/playwright.platform.config", + "vercel": "@node-core/platform-vercel/playwright.platform.config", + "default": "./playwright.platform.config.mjs" + } }, "engines": { "node": "24.x" diff --git a/apps/site/platform/instrumentation.ts b/apps/site/platform/instrumentation.ts index a1c3920abc89d..605be34cb4a3d 100644 --- a/apps/site/platform/instrumentation.ts +++ b/apps/site/platform/instrumentation.ts @@ -1 +1 @@ -export function register() {} +export const register = () => {}; diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.ts index c392c65a93579..67ed01bde6856 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.ts @@ -1,19 +1,6 @@ import { defineConfig, devices } from '@playwright/test'; -import { DEPLOY_TARGET } from './next.constants.mjs'; - -/** - * Load Playwright overrides contributed by the active deployment target. - * - * Mirrors how `next.config.mjs` loads `next.platform.config` from the - * matching `@node-core/platform-` package. Each platform owns - * its own webServer / baseURL wiring so this file stays platform-neutral. - */ -const { default: platform } = DEPLOY_TARGET - ? await import( - `@node-core/platform-${DEPLOY_TARGET}/playwright.platform.config` - ) - : await import('./playwright.platform.config.mjs'); +import platform from '#platform/playwright.platform.config'; const isCI = !!process.env.CI; diff --git a/apps/cloudflare/playwright.platform.config.d.ts b/apps/site/playwright.platform.config.d.ts similarity index 56% rename from apps/cloudflare/playwright.platform.config.d.ts rename to apps/site/playwright.platform.config.d.ts index 046abfff37aa9..ada0174396183 100644 --- a/apps/cloudflare/playwright.platform.config.d.ts +++ b/apps/site/playwright.platform.config.d.ts @@ -1,5 +1,10 @@ import type { Config } from '@playwright/test'; +/** + * Shared Playwright platform-config contract consumed by + * `apps/site/playwright.config.ts` and implemented by each + * `@node-core/platform-` package. + */ export type PlatformPlaywrightConfig = { baseURL?: string; webServer?: Config['webServer']; diff --git a/apps/site/playwright.platform.config.mjs b/apps/site/playwright.platform.config.mjs index 5cef7bccd3aab..2ce9df7ea44f5 100644 --- a/apps/site/playwright.platform.config.mjs +++ b/apps/site/playwright.platform.config.mjs @@ -6,6 +6,6 @@ * `playwright.platform.config.mjs` that overrides these values. Keep * this file free of any platform-specific code. * - * @type {{ baseURL?: string; webServer?: import('@playwright/test').Config['webServer'] }} + * @type {import('./playwright.platform.config').PlatformPlaywrightConfig} */ export default {}; diff --git a/apps/site/vercel.json b/apps/site/vercel.json index 2359ac19dd2b7..c7ede6c737bc9 100644 --- a/apps/site/vercel.json +++ b/apps/site/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "installCommand": "pnpm install --prod --frozen-lockfile --filter=@node-core/website... --filter=@node-core/platform-vercel...", - "buildCommand": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm build", + "buildCommand": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel NODE_OPTIONS=--conditions=vercel pnpm build", "ignoreCommand": "[[ \"$VERCEL_GIT_COMMIT_REF\" =~ \"^dependabot/.*\" || \"$VERCEL_GIT_COMMIT_REF\" =~ \"^gh-readonly-queue/.*\" ]]" } diff --git a/apps/vercel/next.platform.config.d.ts b/apps/vercel/next.platform.config.d.ts deleted file mode 100644 index 461ef293fecd9..0000000000000 --- a/apps/vercel/next.platform.config.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextConfig } from 'next'; - -type PlatformMdxConfig = Pick< - import('@node-core/rehype-shiki').HighlighterOptions, - 'wasm' | 'twoslash' ->; - -type PlatformNextConfig = Pick; - -export type PlatformConfig = { - aliases?: Record; - images?: NextConfig['images']; - mdx?: PlatformMdxConfig; - nextConfig?: PlatformNextConfig; -}; - -declare const config: PlatformConfig; - -export default config; diff --git a/apps/vercel/next.platform.config.mjs b/apps/vercel/next.platform.config.mjs index f4ee425e95476..af3397a0b9aad 100644 --- a/apps/vercel/next.platform.config.mjs +++ b/apps/vercel/next.platform.config.mjs @@ -5,9 +5,9 @@ * Must export a default `{ nextConfig, aliases, images }` shape — any of * which may be omitted when the platform has nothing to contribute. * - * @type {import('@node-core/platform-vercel/next.platform.config').PlatformConfig} + * @type {import('../site/next.platform.config').PlatformConfig} */ -const vercelDeploymentUrl = process.env.VERCEL_URL +const VERCEL_URL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined; @@ -18,7 +18,7 @@ export default { // canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins. env: { NEXT_PUBLIC_BASE_URL: - process.env.NEXT_PUBLIC_BASE_URL || vercelDeploymentUrl || '', + process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL || '', }, }, aliases: { diff --git a/apps/vercel/playwright.platform.config.d.ts b/apps/vercel/playwright.platform.config.d.ts deleted file mode 100644 index 046abfff37aa9..0000000000000 --- a/apps/vercel/playwright.platform.config.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Config } from '@playwright/test'; - -export type PlatformPlaywrightConfig = { - baseURL?: string; - webServer?: Config['webServer']; -}; - -declare const config: PlatformPlaywrightConfig; - -export default config; diff --git a/apps/vercel/playwright.platform.config.mjs b/apps/vercel/playwright.platform.config.mjs index 29ca61b4e5d30..e31d681f2d2e8 100644 --- a/apps/vercel/playwright.platform.config.mjs +++ b/apps/vercel/playwright.platform.config.mjs @@ -6,6 +6,6 @@ * the deployment. Left intentionally empty so `apps/site/playwright.config.ts` * falls back to its default baseURL. * - * @type {import('./playwright.platform.config').PlatformPlaywrightConfig} + * @type {import('../site/playwright.platform.config').PlatformPlaywrightConfig} */ export default {}; diff --git a/apps/vercel/src/instrumentation.ts b/apps/vercel/src/instrumentation.ts index b953218a3e1e9..3e7a06a7d37d2 100644 --- a/apps/vercel/src/instrumentation.ts +++ b/apps/vercel/src/instrumentation.ts @@ -1,5 +1,3 @@ import { registerOTel } from '@vercel/otel'; -export function register() { - registerOTel({ serviceName: 'nodejs-org' }); -} +export const register = () => registerOTel({ serviceName: 'nodejs-org' }); diff --git a/apps/vercel/tsconfig.json b/apps/vercel/tsconfig.json index 320f4a8035486..5210f82773ae5 100644 --- a/apps/vercel/tsconfig.json +++ b/apps/vercel/tsconfig.json @@ -16,7 +16,6 @@ "include": [ "src", "next.platform.config.mjs", - "playwright.platform.config.mjs", - "playwright.platform.config.d.ts" + "playwright.platform.config.mjs" ] } From 35a58e3e894e9d7782b534d63acddd5aea4140a0 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:43:46 -0300 Subject: [PATCH 11/16] fix(ci): build open-next in a dedicated step and skip turbo for wrangler Turbo's persistent-task output buffering swallows wrangler dev's readiness signal in CI, making Playwright's webServer probe time out at 180s even though the preview eventually comes up. Move the OpenNext build to a dedicated CI step so Playwright's webServer can invoke wrangler dev directly, keeping stdout attached to the CI log and removing the dependsOn chain indirection. --- .github/workflows/playwright-cloudflare-open-next.yml | 7 +++++++ apps/cloudflare/playwright.platform.config.mjs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 1ea6426b8d0c0..e6a43ed4b5cfc 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -50,6 +50,13 @@ jobs: working-directory: apps/site run: node_modules/.bin/playwright install --with-deps + - name: Build open-next site + working-directory: apps/cloudflare + run: node --run cloudflare:build:worker + env: + NEXT_PUBLIC_DEPLOY_TARGET: cloudflare + NODE_OPTIONS: --conditions=cloudflare + - name: Run Playwright tests working-directory: apps/site run: node --run playwright diff --git a/apps/cloudflare/playwright.platform.config.mjs b/apps/cloudflare/playwright.platform.config.mjs index 011777c5b329b..dc30d278eecf1 100644 --- a/apps/cloudflare/playwright.platform.config.mjs +++ b/apps/cloudflare/playwright.platform.config.mjs @@ -12,7 +12,7 @@ export default { webServer: { stdout: 'pipe', command: - '../../node_modules/.bin/turbo cloudflare:preview --filter=@node-core/platform-cloudflare', + '../../node_modules/.bin/wrangler dev --config ../cloudflare/wrangler.jsonc', url: 'http://127.0.0.1:8787', timeout: 60_000 * 3, }, From ed3b670de519b7b7cefaaead80152f45ff7de3fb Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:48:37 -0300 Subject: [PATCH 12/16] fix(ci): invoke cloudflare:preview via pnpm filter so wrangler resolves wrangler's bin is hoisted to apps/cloudflare/node_modules/.bin under pnpm, not repo-root node_modules/.bin, so the previous direct path failed with ENOENT in CI. Delegating to the package script via `pnpm --filter` resolves wrangler from the right workspace and keeps stdout attached (no turbo buffering). --- apps/cloudflare/playwright.platform.config.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/cloudflare/playwright.platform.config.mjs b/apps/cloudflare/playwright.platform.config.mjs index dc30d278eecf1..992eaf822854b 100644 --- a/apps/cloudflare/playwright.platform.config.mjs +++ b/apps/cloudflare/playwright.platform.config.mjs @@ -11,8 +11,7 @@ export default { baseURL: 'http://127.0.0.1:8787', webServer: { stdout: 'pipe', - command: - '../../node_modules/.bin/wrangler dev --config ../cloudflare/wrangler.jsonc', + command: 'pnpm --filter=@node-core/platform-cloudflare cloudflare:preview', url: 'http://127.0.0.1:8787', timeout: 60_000 * 3, }, From 93751a4b1dc81b1b44fa0a4c1309affd54a6ade2 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 19:24:18 -0300 Subject: [PATCH 13/16] fix(platform): defer Node-only config behind async thunks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/cloudflare/next.platform.config.mjs | 62 +++++++++++++----------- apps/site/next.config.mjs | 23 +++++++-- apps/site/next.platform.config.d.ts | 11 ++++- apps/vercel/next.platform.config.mjs | 36 ++++++++------ 4 files changed, 80 insertions(+), 52 deletions(-) diff --git a/apps/cloudflare/next.platform.config.mjs b/apps/cloudflare/next.platform.config.mjs index 2b7b03c0c7e01..56007e4415bac 100644 --- a/apps/cloudflare/next.platform.config.mjs +++ b/apps/cloudflare/next.platform.config.mjs @@ -1,44 +1,21 @@ -import { createRequire } from 'node:module'; -import { relative } from 'node:path'; - -import { getDeploymentId } from '@opennextjs/cloudflare'; - -const require = createRequire(import.meta.url); - /** * Platform config contributed by the Cloudflare deployment target. * - * Consumed by `apps/site/next.config.mjs` via the platform-config loader. - * Must export a default `{ nextConfig, aliases, images }` shape — any of - * which may be omitted when the platform has nothing to contribute. + * Consumed by `apps/site/next.config.mjs` via the `#platform/*` import + * map. Heavy, Node-only bits (`@opennextjs/cloudflare`, `createRequire`, + * `require.resolve`) live inside async thunks so that webpack — which + * bundles the top level of this module into the server output when + * `apps/site/mdx/plugins.mjs` reads `.mdx` — never drags them into the + * worker runtime. * * @type {import('../site/next.platform.config').PlatformConfig} */ export default { - nextConfig: { - // Skew protection: Cloudflare routes requests by deploymentId so that - // a client and the worker stay in sync across rolling deploys. - deploymentId: await getDeploymentId(), - }, aliases: { '@platform/analytics': '@node-core/platform-cloudflare/analytics', '@platform/instrumentation': '@node-core/platform-cloudflare/instrumentation', }, - images: { - // Route optimized images through Cloudflare's Images service via the - // custom loader. `remotePatterns` do NOT apply here — Cloudflare - // enforces allowed origins at the edge instead. - loader: 'custom', - // Next.js joins `loaderFile` onto its own cwd (apps/site), so pass a - // path relative to that cwd rather than an absolute one. Resolving via - // `require.resolve` avoids the `new URL(..., import.meta.url)` pattern, - // which webpack rewrites as an asset reference and mangles at runtime. - loaderFile: relative( - process.cwd(), - require.resolve('@node-core/platform-cloudflare/image-loader') - ), - }, mdx: { // Cloudflare workers can't load `shiki/wasm` via `WebAssembly.instantiate` // with custom imports (blocked for security), so fall back to the @@ -46,4 +23,31 @@ export default { wasm: false, twoslash: false, }, + nextConfig: async () => { + const { getDeploymentId } = await import('@opennextjs/cloudflare'); + return { + // Skew protection: Cloudflare routes requests by deploymentId so that + // a client and the worker stay in sync across rolling deploys. + deploymentId: await getDeploymentId(), + }; + }, + images: async () => { + const { createRequire } = await import('node:module'); + const { relative } = await import('node:path'); + const require = createRequire(import.meta.url); + return { + // Route optimized images through Cloudflare's Images service via the + // custom loader. `remotePatterns` do NOT apply here — Cloudflare + // enforces allowed origins at the edge instead. + loader: 'custom', + // Next.js joins `loaderFile` onto its own cwd (apps/site), so pass a + // path relative to that cwd rather than an absolute one. Resolving via + // `require.resolve` avoids the `new URL(..., import.meta.url)` pattern, + // which webpack rewrites as an asset reference and mangles at runtime. + loaderFile: relative( + process.cwd(), + require.resolve('@node-core/platform-cloudflare/image-loader') + ), + }; + }, }; diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 386dca2df0297..7349a457e06e4 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -4,7 +4,11 @@ import createNextIntlPlugin from 'next-intl/plugin'; import platform from '#platform/next.platform.config'; -import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { + BASE_PATH, + ENABLE_STATIC_EXPORT, + DEPLOY_TARGET, +} from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; @@ -17,7 +21,7 @@ const nextConfig = { // We allow the BASE_PATH to be overridden in case that the Website // is being built on a subdirectory (e.g. /nodejs-website) basePath: BASE_PATH, - images: getImagesConfig(platform.images), + images: getImagesConfig(await platform.images?.()), serverExternalPackages: ['twoslash'], // Transpile platform packages' TSX/TS sources when they're pulled in via // the `@platform/*` aliases from the active `next.platform.config.mjs`. @@ -78,12 +82,21 @@ const nextConfig = { }, // Provide Turbopack Aliases for Platform Resolution turbopack: { resolveAlias: platform.aliases }, - // Provide Webpack Aliases for Platform Resolution + // Provide Webpack Aliases for Platform Resolution. The active deployment + // target is also surfaced to the resolver via `conditionNames` so that + // `#platform/*` subpath imports in `package.json` pick the matching + // branch when webpack bundles server code. webpack: ({ resolve, ...config }) => ({ ...config, - resolve: { ...resolve, alias: { ...resolve.alias, ...platform.aliases } }, + resolve: { + ...resolve, + alias: { ...resolve.alias, ...platform.aliases }, + conditionNames: resolve.conditionNames + .concat(DEPLOY_TARGET) + .filter(Boolean), + }, }), - ...platform.nextConfig, + ...(await platform.nextConfig?.()), }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); diff --git a/apps/site/next.platform.config.d.ts b/apps/site/next.platform.config.d.ts index 3b6449e5f942a..7094cef1660cd 100644 --- a/apps/site/next.platform.config.d.ts +++ b/apps/site/next.platform.config.d.ts @@ -7,12 +7,19 @@ type PlatformNextConfig = Pick; /** * Shared platform-config contract consumed by `apps/site/next.config.mjs` * and implemented by each `@node-core/platform-` package. + * + * `nextConfig` and `images` are async thunks so that platform modules + * that depend on Node-only tooling (e.g. `@opennextjs/cloudflare`, + * `require.resolve`) can keep those imports out of the module's + * top-level. Webpack bundles the top level of this module into the + * server output; deferring heavy work into function bodies keeps the + * worker runtime free of build-only code. */ export type PlatformConfig = { aliases?: Record; - images?: NextConfig['images']; + images?: () => Promise; mdx?: PlatformMdxConfig; - nextConfig?: PlatformNextConfig; + nextConfig?: () => Promise; }; declare const config: PlatformConfig; diff --git a/apps/vercel/next.platform.config.mjs b/apps/vercel/next.platform.config.mjs index af3397a0b9aad..15528ff543cc0 100644 --- a/apps/vercel/next.platform.config.mjs +++ b/apps/vercel/next.platform.config.mjs @@ -1,26 +1,16 @@ /** * Platform config contributed by the Vercel deployment target. * - * Consumed by `apps/site/next.config.mjs` via the platform-config loader. - * Must export a default `{ nextConfig, aliases, images }` shape — any of - * which may be omitted when the platform has nothing to contribute. + * Consumed by `apps/site/next.config.mjs` via the `#platform/*` import + * map. Heavy, Node-only bits live inside async thunks so that webpack — + * which bundles the top level of this module into the server output + * when `apps/site/mdx/plugins.mjs` reads `.mdx` — never drags them into + * the Node server runtime (keeps bundles lean and parity with + * Cloudflare's contract). * * @type {import('../site/next.platform.config').PlatformConfig} */ -const VERCEL_URL = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : undefined; - export default { - nextConfig: { - // Expose Vercel's auto-assigned deployment URL as a platform-agnostic - // `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single - // canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins. - env: { - NEXT_PUBLIC_BASE_URL: - process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL || '', - }, - }, aliases: { '@platform/analytics': '@node-core/platform-vercel/analytics', '@platform/instrumentation': '@node-core/platform-vercel/instrumentation', @@ -31,4 +21,18 @@ export default { wasm: true, twoslash: true, }, + nextConfig: async () => { + const VERCEL_URL = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : undefined; + return { + // Expose Vercel's auto-assigned deployment URL as a platform-agnostic + // `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single + // canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins. + env: { + NEXT_PUBLIC_BASE_URL: + process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL || '', + }, + }; + }, }; From ecf659dc54c5d7696b3af2dabbd3e646263062f7 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 20:22:02 -0300 Subject: [PATCH 14/16] chore(cloudflare): drop cd-prefixed scripts and ignore wrangler state dir Replace `cd ../site && ...` in the three cloudflare scripts with `pnpm --filter=@node-core/website exec ...` so pnpm sets cwd transparently (per Dario's review feedback). Ignore apps/cloudflare/.wrangler, which is where wrangler v4 writes its state dir relative to the config file. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + apps/cloudflare/package.json | 6 +++--- apps/cloudflare/wrangler.jsonc | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index f461599c85893..be4a634d56b72 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ dist/ # Cloudflare Build Output apps/site/.open-next apps/site/.wrangler +apps/cloudflare/.wrangler ## Playwright test-results diff --git a/apps/cloudflare/package.json b/apps/cloudflare/package.json index 731983e3b6fec..275afdb50f220 100644 --- a/apps/cloudflare/package.json +++ b/apps/cloudflare/package.json @@ -17,9 +17,9 @@ "directory": "apps/cloudflare" }, "scripts": { - "cloudflare:build:worker": "cd ../site && opennextjs-cloudflare build --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", - "cloudflare:deploy": "cd ../site && opennextjs-cloudflare deploy --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", - "cloudflare:preview": "cd ../site && wrangler dev --config ../cloudflare/wrangler.jsonc", + "cloudflare:build:worker": "pnpm --filter=@node-core/website exec opennextjs-cloudflare build --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", + "cloudflare:deploy": "pnpm --filter=@node-core/website exec opennextjs-cloudflare deploy --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", + "cloudflare:preview": "pnpm --filter=@node-core/website exec wrangler dev --config ../cloudflare/wrangler.jsonc", "lint:types": "tsc --noEmit" }, "dependencies": { diff --git a/apps/cloudflare/wrangler.jsonc b/apps/cloudflare/wrangler.jsonc index 90ccce6e97616..a345ec103715f 100644 --- a/apps/cloudflare/wrangler.jsonc +++ b/apps/cloudflare/wrangler.jsonc @@ -31,8 +31,6 @@ "head_sampling_rate": 1, }, "build": { - // Run the asset polyfiller from apps/site so that `pages`, `snippets`, and - // the `.open-next` output directory resolve against the Next.js app. "cwd": "../site", "command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets", }, From 62528e882a5f9aa316e463bb9afda0d6df2df303 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 24 Apr 2026 18:55:39 -0300 Subject: [PATCH 15/16] refactor(platform): relocate deploy-target apps under platforms/ Move apps/{vercel,cloudflare} to platforms/{vercel,cloudflare} and apps/site/vercel.json to platforms/vercel/vercel.json. Rebase all relative path references across workflows, CODEOWNERS, docs, and configs so tooling resolves from the new locations. vercel.json now declares outputDirectory (../../apps/site/.next) so Vercel can find the Next.js build when its Root Directory is set to platforms/vercel, and uses build.env for NEXT_PUBLIC_DEPLOY_TARGET and NODE_OPTIONS=--conditions=vercel instead of cross-env (which is not in the --prod install tree). Add NEXT_PUBLIC_DEPLOY_TARGET and NODE_OPTIONS=--conditions=cloudflare to the Cloudflare deploy workflow's build and deploy step env blocks so the outer opennextjs-cloudflare process resolves imports under the cloudflare condition (matching the Playwright workflow). Co-Authored-By: Claude Opus 4.7 --- .github/CODEOWNERS | 9 +- .../playwright-cloudflare-open-next.yml | 2 +- .../tmp-cloudflare-open-next-deploy.yml | 9 +- .gitignore | 2 +- .../__tests__/next.platform.config.test.mjs | 13 -- docs/cloudflare-build-and-deployment.md | 16 +- docs/technologies.md | 16 +- .../cloudflare/next.platform.config.mjs | 7 +- .../cloudflare/open-next.config.ts | 0 {apps => platforms}/cloudflare/package.json | 8 +- .../cloudflare/playwright.platform.config.mjs | 2 +- .../cloudflare/src/analytics.tsx | 0 .../cloudflare/src/image-loader.ts | 0 .../cloudflare/src/instrumentation.ts | 0 .../cloudflare/src/worker-entrypoint.ts | 4 +- {apps => platforms}/cloudflare/tsconfig.json | 0 {apps => platforms}/cloudflare/turbo.json | 12 +- {apps => platforms}/cloudflare/wrangler.jsonc | 12 +- .../vercel/next.platform.config.mjs | 3 +- {apps => platforms}/vercel/package.json | 2 +- .../vercel/playwright.platform.config.mjs | 2 +- {apps => platforms}/vercel/src/analytics.tsx | 0 .../vercel/src/instrumentation.ts | 0 {apps => platforms}/vercel/tsconfig.json | 0 {apps/site => platforms/vercel}/vercel.json | 9 +- pnpm-lock.yaml | 158 +++++++++--------- pnpm-workspace.yaml | 1 + 27 files changed, 146 insertions(+), 141 deletions(-) delete mode 100644 apps/vercel/__tests__/next.platform.config.test.mjs rename {apps => platforms}/cloudflare/next.platform.config.mjs (94%) rename {apps => platforms}/cloudflare/open-next.config.ts (100%) rename {apps => platforms}/cloudflare/package.json (76%) rename {apps => platforms}/cloudflare/playwright.platform.config.mjs (85%) rename {apps => platforms}/cloudflare/src/analytics.tsx (100%) rename {apps => platforms}/cloudflare/src/image-loader.ts (100%) rename {apps => platforms}/cloudflare/src/instrumentation.ts (100%) rename {apps => platforms}/cloudflare/src/worker-entrypoint.ts (90%) rename {apps => platforms}/cloudflare/tsconfig.json (100%) rename {apps => platforms}/cloudflare/turbo.json (68%) rename {apps => platforms}/cloudflare/wrangler.jsonc (78%) rename {apps => platforms}/vercel/next.platform.config.mjs (94%) rename {apps => platforms}/vercel/package.json (96%) rename {apps => platforms}/vercel/playwright.platform.config.mjs (80%) rename {apps => platforms}/vercel/src/analytics.tsx (100%) rename {apps => platforms}/vercel/src/instrumentation.ts (100%) rename {apps => platforms}/vercel/tsconfig.json (100%) rename {apps/site => platforms/vercel}/vercel.json (58%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e781e2d73c6e5..4a0ebcfecd457 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,10 +27,11 @@ turbo.json @nodejs/nodejs-website @nodejs/web-infra crowdin.yml @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra apps/site/site.json @nodejs/web-infra -apps/cloudflare/wrangler.jsonc @nodejs/web-infra -apps/cloudflare/open-next.config.ts @nodejs/web-infra -apps/cloudflare/next.platform.config.mjs @nodejs/web-infra -apps/vercel/next.platform.config.mjs @nodejs/web-infra +platforms/cloudflare/wrangler.jsonc @nodejs/web-infra +platforms/cloudflare/open-next.config.ts @nodejs/web-infra +platforms/cloudflare/next.platform.config.mjs @nodejs/web-infra +platforms/vercel/vercel.json @nodejs/web-infra +platforms/vercel/next.platform.config.mjs @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra # Critical Documents diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index e6a43ed4b5cfc..76c76ccf7c48a 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -51,7 +51,7 @@ jobs: run: node_modules/.bin/playwright install --with-deps - name: Build open-next site - working-directory: apps/cloudflare + working-directory: platforms/cloudflare run: node --run cloudflare:build:worker env: NEXT_PUBLIC_DEPLOY_TARGET: cloudflare diff --git a/.github/workflows/tmp-cloudflare-open-next-deploy.yml b/.github/workflows/tmp-cloudflare-open-next-deploy.yml index d6a0b8b1dee37..42deb38324d5c 100644 --- a/.github/workflows/tmp-cloudflare-open-next-deploy.yml +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -57,13 +57,18 @@ jobs: run: node --run build:blog-data - name: Build open-next site - working-directory: apps/cloudflare + working-directory: platforms/cloudflare run: node --run cloudflare:build:worker + env: + NEXT_PUBLIC_DEPLOY_TARGET: cloudflare + NODE_OPTIONS: --conditions=cloudflare - name: Deploy open-next site - working-directory: apps/cloudflare + working-directory: platforms/cloudflare run: node --run cloudflare:deploy env: + NEXT_PUBLIC_DEPLOY_TARGET: cloudflare + NODE_OPTIONS: --conditions=cloudflare CF_WORKERS_SCRIPTS_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: fb4a2d0f103c6ff38854ac69eb709272 diff --git a/.gitignore b/.gitignore index be4a634d56b72..6c783ceb3ee1f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ dist/ # Cloudflare Build Output apps/site/.open-next apps/site/.wrangler -apps/cloudflare/.wrangler +platforms/cloudflare/.wrangler ## Playwright test-results diff --git a/apps/vercel/__tests__/next.platform.config.test.mjs b/apps/vercel/__tests__/next.platform.config.test.mjs deleted file mode 100644 index 47d857a0e6122..0000000000000 --- a/apps/vercel/__tests__/next.platform.config.test.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -describe('platform-vercel next.platform.config', () => { - it('defines shiki mdx defaults for Vercel builds', async () => { - const { default: platform } = await import('../next.platform.config.mjs'); - - assert.deepEqual(platform.mdx, { - wasm: true, - twoslash: true, - }); - }); -}); diff --git a/docs/cloudflare-build-and-deployment.md b/docs/cloudflare-build-and-deployment.md index 3f5cc90d9f0d9..dad2a5bbc813f 100644 --- a/docs/cloudflare-build-and-deployment.md +++ b/docs/cloudflare-build-and-deployment.md @@ -2,14 +2,14 @@ The Node.js Website can be built using the [OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare). Such build generates a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) that can be deployed on the [Cloudflare](https://www.cloudflare.com) network. -The build is gated on the `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` environment variable (set by the OpenNext `buildCommand`), which makes `apps/site` pull its Next.js, MDX, image-loader, and analytics overrides from [`@node-core/platform-cloudflare`](../apps/cloudflare). See the [Deploy Target Selection](./technologies.md#deploy-target-selection-next_public_deploy_target) section of the Technologies document for the full platform-adapter contract. +The build is gated on the `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` environment variable (set by the OpenNext `buildCommand`), which makes `apps/site` pull its Next.js, MDX, image-loader, and analytics overrides from [`@node-core/platform-cloudflare`](../platforms/cloudflare). See the [Deploy Target Selection](./technologies.md#deploy-target-selection-next_public_deploy_target) section of the Technologies document for the full platform-adapter contract. ## Configurations -All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../apps/cloudflare) package. The two key configuration files are: +All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../platforms/cloudflare) package. The two key configuration files are: -- [`apps/cloudflare/wrangler.jsonc`](../apps/cloudflare/wrangler.jsonc) — the Wrangler configuration -- [`apps/cloudflare/open-next.config.ts`](../apps/cloudflare/open-next.config.ts) — the OpenNext adapter configuration +- [`platforms/cloudflare/wrangler.jsonc`](../platforms/cloudflare/wrangler.jsonc) — the Wrangler configuration +- [`platforms/cloudflare/open-next.config.ts`](../platforms/cloudflare/open-next.config.ts) — the OpenNext adapter configuration ### Wrangler Configuration @@ -19,7 +19,7 @@ For more details, refer to the [Wrangler documentation](https://developers.cloud Key configurations include: -- `main`: Points to a custom worker entry point ([`apps/cloudflare/src/worker-entrypoint.ts`](../apps/cloudflare/src/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). +- `main`: Points to a custom worker entry point ([`platforms/cloudflare/src/worker-entrypoint.ts`](../platforms/cloudflare/src/worker-entrypoint.ts)) that wraps the OpenNext-generated worker (see [Custom Worker Entry Point](#custom-worker-entry-point) and [Sentry](#sentry) below). - `account_id`: Specifies the Cloudflare account ID. This is not required for local previews but is necessary for deployments. You can obtain an account ID for free by signing up at [dash.cloudflare.com](https://dash.cloudflare.com/login). - This is currently set to `fb4a2d0f103c6ff38854ac69eb709272`, which is the ID of a Cloudflare account controlled by Node.js, and used for testing. - `build`: Defines the build command to generate the Node.js filesystem polyfills required for the application to run on Cloudflare Workers. This uses the [`@flarelabs/wrangler-build-time-fs-assets-polyfilling`](https://github.com/flarelabs-net/wrangler-build-time-fs-assets-polyfilling) package. @@ -54,15 +54,15 @@ Additionally, when deploying, an extra `CF_WORKERS_SCRIPTS_API_TOKEN` environmen ### Image loader -When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`apps/cloudflare/next.platform.config.mjs`](../apps/cloudflare/next.platform.config.mjs)) contributes it via the `images.loaderFile` field, which is merged into the shared Next.js config when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../apps/cloudflare/open-next.config.ts)). +When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`platforms/cloudflare/next.platform.config.mjs`](../platforms/cloudflare/next.platform.config.mjs)) contributes it via the `images.loaderFile` field, which is merged into the shared Next.js config when `NEXT_PUBLIC_DEPLOY_TARGET=cloudflare` (the variable is set by the OpenNext `buildCommand` in [`open-next.config.ts`](../platforms/cloudflare/open-next.config.ts)). -The custom loader can be found at [`apps/cloudflare/src/image-loader.ts`](../apps/cloudflare/src/image-loader.ts). +The custom loader can be found at [`platforms/cloudflare/src/image-loader.ts`](../platforms/cloudflare/src/image-loader.ts). For more details on this see: https://developers.cloudflare.com/images/transform-images/integrate-with-frameworks/#global-loader ### Custom Worker Entry Point -Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`apps/cloudflare/src/worker-entrypoint.ts`](../apps/cloudflare/src/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). +Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`platforms/cloudflare/src/worker-entrypoint.ts`](../platforms/cloudflare/src/worker-entrypoint.ts). This allows customizing the worker's behavior before requests are handled (currently used to integrate [Sentry](#sentry) error monitoring). The custom entry point imports the OpenNext-generated handler from `.open-next/worker.js` and re-exports the `DOQueueHandler` Durable Object needed by the application. diff --git a/docs/technologies.md b/docs/technologies.md index 4b09b81f78051..a820f8c499047 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -302,25 +302,25 @@ Benefits: `NEXT_PUBLIC_DEPLOY_TARGET` selects which platform adapter contributes its Next.js config, MDX flags, image loader, analytics, and Playwright webServer. It is consumed at build time by [`apps/site/next.config.mjs`](../apps/site/next.config.mjs), [`apps/site/mdx/plugins.mjs`](../apps/site/mdx/plugins.mjs), and [`apps/site/playwright.config.ts`](../apps/site/playwright.config.ts) via a dynamic import of `@node-core/platform-${target}/next.platform.config`. -| Value | Adapter | Set by | -| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `vercel` | [`@node-core/platform-vercel`](../apps/vercel) | [`apps/site/vercel.json`](../apps/site/vercel.json) build env | -| `cloudflare` | [`@node-core/platform-cloudflare`](../apps/cloudflare) | OpenNext `buildCommand` in [`open-next.config.ts`](../apps/cloudflare/open-next.config.ts) | -| _(unset)_ | Falls back to the no-op defaults in [`apps/site/next.platform.config.mjs`](../apps/site/next.platform.config.mjs) and [`apps/site/playwright.platform.config.mjs`](../apps/site/playwright.platform.config.mjs) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | +| Value | Adapter | Set by | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `vercel` | [`@node-core/platform-vercel`](../platforms/vercel) | [`platforms/vercel/vercel.json`](../platforms/vercel/vercel.json) build env | +| `cloudflare` | [`@node-core/platform-cloudflare`](../platforms/cloudflare) | OpenNext `buildCommand` in [`open-next.config.ts`](../platforms/cloudflare/open-next.config.ts) | +| _(unset)_ | Falls back to the no-op defaults in [`apps/site/next.platform.config.mjs`](../apps/site/next.platform.config.mjs) and [`apps/site/playwright.platform.config.mjs`](../apps/site/playwright.platform.config.mjs) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | -Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`apps/vercel/next.platform.config.mjs`](../apps/vercel/next.platform.config.mjs) and [`apps/cloudflare/next.platform.config.mjs`](../apps/cloudflare/next.platform.config.mjs) for reference. +Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`platforms/vercel/next.platform.config.mjs`](../platforms/vercel/next.platform.config.mjs) and [`platforms/cloudflare/next.platform.config.mjs`](../platforms/cloudflare/next.platform.config.mjs) for reference. #### Vercel Integration - Automatic deployments for branches (ignoring automated branches) -- Custom install + ignore scripts ([see `vercel.json`](../apps/site/vercel.json)) +- Custom install + ignore scripts ([see `vercel.json`](../platforms/vercel/vercel.json)) - Build-time dependencies must be in `dependencies`, not `devDependencies` - Sponsorship maintained by OpenJS Foundation #### Cloudflare Integration - OpenNext adapter builds a [Cloudflare Worker](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) artifact from the Next.js build -- All Cloudflare-specific files (Wrangler config, OpenNext config, custom worker entrypoint, image loader) live in [`apps/cloudflare`](../apps/cloudflare) +- All Cloudflare-specific files (Wrangler config, OpenNext config, custom worker entrypoint, image loader) live in [`platforms/cloudflare`](../platforms/cloudflare) - See [Cloudflare build and deployment](./cloudflare-build-and-deployment.md) for details ### Package Management diff --git a/apps/cloudflare/next.platform.config.mjs b/platforms/cloudflare/next.platform.config.mjs similarity index 94% rename from apps/cloudflare/next.platform.config.mjs rename to platforms/cloudflare/next.platform.config.mjs index 56007e4415bac..5d0871273100f 100644 --- a/apps/cloudflare/next.platform.config.mjs +++ b/platforms/cloudflare/next.platform.config.mjs @@ -8,7 +8,7 @@ * `apps/site/mdx/plugins.mjs` reads `.mdx` — never drags them into the * worker runtime. * - * @type {import('../site/next.platform.config').PlatformConfig} + * @type {import('../../apps/site/next.platform.config').PlatformConfig} */ export default { aliases: { @@ -25,16 +25,19 @@ export default { }, nextConfig: async () => { const { getDeploymentId } = await import('@opennextjs/cloudflare'); + return { // Skew protection: Cloudflare routes requests by deploymentId so that // a client and the worker stay in sync across rolling deploys. - deploymentId: await getDeploymentId(), + deploymentId: getDeploymentId(), }; }, images: async () => { const { createRequire } = await import('node:module'); const { relative } = await import('node:path'); + const require = createRequire(import.meta.url); + return { // Route optimized images through Cloudflare's Images service via the // custom loader. `remotePatterns` do NOT apply here — Cloudflare diff --git a/apps/cloudflare/open-next.config.ts b/platforms/cloudflare/open-next.config.ts similarity index 100% rename from apps/cloudflare/open-next.config.ts rename to platforms/cloudflare/open-next.config.ts diff --git a/apps/cloudflare/package.json b/platforms/cloudflare/package.json similarity index 76% rename from apps/cloudflare/package.json rename to platforms/cloudflare/package.json index 275afdb50f220..0e36a4360e393 100644 --- a/apps/cloudflare/package.json +++ b/platforms/cloudflare/package.json @@ -14,12 +14,12 @@ "repository": { "type": "git", "url": "https://github.com/nodejs/nodejs.org", - "directory": "apps/cloudflare" + "directory": "platforms/cloudflare" }, "scripts": { - "cloudflare:build:worker": "pnpm --filter=@node-core/website exec opennextjs-cloudflare build --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", - "cloudflare:deploy": "pnpm --filter=@node-core/website exec opennextjs-cloudflare deploy --openNextConfigPath ../cloudflare/open-next.config.ts --config ../cloudflare/wrangler.jsonc", - "cloudflare:preview": "pnpm --filter=@node-core/website exec wrangler dev --config ../cloudflare/wrangler.jsonc", + "cloudflare:build:worker": "pnpm --filter=@node-core/website exec opennextjs-cloudflare build --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", + "cloudflare:deploy": "pnpm --filter=@node-core/website exec opennextjs-cloudflare deploy --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", + "cloudflare:preview": "pnpm --filter=@node-core/website exec wrangler dev --config ../../platforms/cloudflare/wrangler.jsonc", "lint:types": "tsc --noEmit" }, "dependencies": { diff --git a/apps/cloudflare/playwright.platform.config.mjs b/platforms/cloudflare/playwright.platform.config.mjs similarity index 85% rename from apps/cloudflare/playwright.platform.config.mjs rename to platforms/cloudflare/playwright.platform.config.mjs index 992eaf822854b..b405cfb803b95 100644 --- a/apps/cloudflare/playwright.platform.config.mjs +++ b/platforms/cloudflare/playwright.platform.config.mjs @@ -5,7 +5,7 @@ * loader. Spins up the wrangler preview so E2E runs against the * OpenNext worker artifact rather than `next dev`. * - * @type {import('../site/playwright.platform.config').PlatformPlaywrightConfig} + * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} */ export default { baseURL: 'http://127.0.0.1:8787', diff --git a/apps/cloudflare/src/analytics.tsx b/platforms/cloudflare/src/analytics.tsx similarity index 100% rename from apps/cloudflare/src/analytics.tsx rename to platforms/cloudflare/src/analytics.tsx diff --git a/apps/cloudflare/src/image-loader.ts b/platforms/cloudflare/src/image-loader.ts similarity index 100% rename from apps/cloudflare/src/image-loader.ts rename to platforms/cloudflare/src/image-loader.ts diff --git a/apps/cloudflare/src/instrumentation.ts b/platforms/cloudflare/src/instrumentation.ts similarity index 100% rename from apps/cloudflare/src/instrumentation.ts rename to platforms/cloudflare/src/instrumentation.ts diff --git a/apps/cloudflare/src/worker-entrypoint.ts b/platforms/cloudflare/src/worker-entrypoint.ts similarity index 90% rename from apps/cloudflare/src/worker-entrypoint.ts rename to platforms/cloudflare/src/worker-entrypoint.ts index 9476169e669d7..f63ab60a6f586 100644 --- a/apps/cloudflare/src/worker-entrypoint.ts +++ b/platforms/cloudflare/src/worker-entrypoint.ts @@ -11,7 +11,7 @@ import type { Request, } from '@cloudflare/workers-types'; -import { default as handler } from '../../site/.open-next/worker.js'; +import { default as handler } from '../../../apps/site/.open-next/worker.js'; export default withSentry( (env: { @@ -50,4 +50,4 @@ export default withSentry( } ); -export { DOQueueHandler } from '../../site/.open-next/worker.js'; +export { DOQueueHandler } from '../../../apps/site/.open-next/worker.js'; diff --git a/apps/cloudflare/tsconfig.json b/platforms/cloudflare/tsconfig.json similarity index 100% rename from apps/cloudflare/tsconfig.json rename to platforms/cloudflare/tsconfig.json diff --git a/apps/cloudflare/turbo.json b/platforms/cloudflare/turbo.json similarity index 68% rename from apps/cloudflare/turbo.json rename to platforms/cloudflare/turbo.json index 810960a02292a..8c1ffdc95a974 100644 --- a/apps/cloudflare/turbo.json +++ b/platforms/cloudflare/turbo.json @@ -8,13 +8,13 @@ "open-next.config.ts", "wrangler.jsonc", "src/**/*.ts", - "../site/{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "../site/{app,components,layouts,pages,styles}/**/*.css", - "../site/{next-data,scripts,i18n}/**/*.{mjs,json}", - "../site/{app,pages}/**/*.{mdx,md}", - "../site/*.{md,mdx,json,ts,tsx,mjs,yml}" + "../../apps/site/{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", + "../../apps/site/{app,components,layouts,pages,styles}/**/*.css", + "../../apps/site/{next-data,scripts,i18n}/**/*.{mjs,json}", + "../../apps/site/{app,pages}/**/*.{mdx,md}", + "../../apps/site/*.{md,mdx,json,ts,tsx,mjs,yml}" ], - "outputs": ["../site/.open-next/**"], + "outputs": ["../../apps/site/.open-next/**"], "env": [ "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_BASE_URL", diff --git a/apps/cloudflare/wrangler.jsonc b/platforms/cloudflare/wrangler.jsonc similarity index 78% rename from apps/cloudflare/wrangler.jsonc rename to platforms/cloudflare/wrangler.jsonc index a345ec103715f..963fbaff24472 100644 --- a/apps/cloudflare/wrangler.jsonc +++ b/platforms/cloudflare/wrangler.jsonc @@ -8,7 +8,7 @@ "minify": true, "keep_names": false, "assets": { - "directory": "../site/.open-next/assets", + "directory": "../../apps/site/.open-next/assets", "binding": "ASSETS", "run_worker_first": true, }, @@ -31,14 +31,14 @@ "head_sampling_rate": 1, }, "build": { - "cwd": "../site", + "cwd": "../../apps/site", "command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets", }, "alias": { - "node:fs": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "node:fs/promises": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", - "fs": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", - "fs/promises": "../site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "node:fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "node:fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", + "fs": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts", + "fs/promises": "../../apps/site/.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts", }, "r2_buckets": [ { diff --git a/apps/vercel/next.platform.config.mjs b/platforms/vercel/next.platform.config.mjs similarity index 94% rename from apps/vercel/next.platform.config.mjs rename to platforms/vercel/next.platform.config.mjs index 15528ff543cc0..e216e33866df3 100644 --- a/apps/vercel/next.platform.config.mjs +++ b/platforms/vercel/next.platform.config.mjs @@ -8,7 +8,7 @@ * the Node server runtime (keeps bundles lean and parity with * Cloudflare's contract). * - * @type {import('../site/next.platform.config').PlatformConfig} + * @type {import('../../apps/site/next.platform.config').PlatformConfig} */ export default { aliases: { @@ -25,6 +25,7 @@ export default { const VERCEL_URL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined; + return { // Expose Vercel's auto-assigned deployment URL as a platform-agnostic // `NEXT_PUBLIC_BASE_URL` so `apps/site` consumers can read a single diff --git a/apps/vercel/package.json b/platforms/vercel/package.json similarity index 96% rename from apps/vercel/package.json rename to platforms/vercel/package.json index 513ebee856d34..2df4c823eb071 100644 --- a/apps/vercel/package.json +++ b/platforms/vercel/package.json @@ -12,7 +12,7 @@ "repository": { "type": "git", "url": "https://github.com/nodejs/nodejs.org", - "directory": "apps/vercel" + "directory": "platforms/vercel" }, "scripts": { "lint:types": "tsc --noEmit" diff --git a/apps/vercel/playwright.platform.config.mjs b/platforms/vercel/playwright.platform.config.mjs similarity index 80% rename from apps/vercel/playwright.platform.config.mjs rename to platforms/vercel/playwright.platform.config.mjs index e31d681f2d2e8..657a046c5c621 100644 --- a/apps/vercel/playwright.platform.config.mjs +++ b/platforms/vercel/playwright.platform.config.mjs @@ -6,6 +6,6 @@ * the deployment. Left intentionally empty so `apps/site/playwright.config.ts` * falls back to its default baseURL. * - * @type {import('../site/playwright.platform.config').PlatformPlaywrightConfig} + * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} */ export default {}; diff --git a/apps/vercel/src/analytics.tsx b/platforms/vercel/src/analytics.tsx similarity index 100% rename from apps/vercel/src/analytics.tsx rename to platforms/vercel/src/analytics.tsx diff --git a/apps/vercel/src/instrumentation.ts b/platforms/vercel/src/instrumentation.ts similarity index 100% rename from apps/vercel/src/instrumentation.ts rename to platforms/vercel/src/instrumentation.ts diff --git a/apps/vercel/tsconfig.json b/platforms/vercel/tsconfig.json similarity index 100% rename from apps/vercel/tsconfig.json rename to platforms/vercel/tsconfig.json diff --git a/apps/site/vercel.json b/platforms/vercel/vercel.json similarity index 58% rename from apps/site/vercel.json rename to platforms/vercel/vercel.json index c7ede6c737bc9..5cf4bef4579bf 100644 --- a/apps/site/vercel.json +++ b/platforms/vercel/vercel.json @@ -1,6 +1,13 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "installCommand": "pnpm install --prod --frozen-lockfile --filter=@node-core/website... --filter=@node-core/platform-vercel...", - "buildCommand": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel NODE_OPTIONS=--conditions=vercel pnpm build", + "buildCommand": "pnpm --filter=@node-core/website build", + "outputDirectory": "../../apps/site/.next", + "build": { + "env": { + "NEXT_PUBLIC_DEPLOY_TARGET": "vercel", + "NODE_OPTIONS": "--conditions=vercel" + } + }, "ignoreCommand": "[[ \"$VERCEL_GIT_COMMIT_REF\" =~ \"^dependabot/.*\" || \"$VERCEL_GIT_COMMIT_REF\" =~ \"^gh-readonly-queue/.*\" ]]" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49581359c0467..77d4e244e30f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,43 +82,6 @@ importers: specifier: ~8.57.2 version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - apps/cloudflare: - dependencies: - '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': - specifier: ^0.0.1 - version: 0.0.1 - '@opennextjs/cloudflare': - specifier: ^1.19.3 - version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) - '@sentry/cloudflare': - specifier: ^10.49.0 - version: 10.49.0(@cloudflare/workers-types@4.20260422.1) - next: - specifier: 'catalog:' - version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: - specifier: 'catalog:' - version: 19.2.4 - wrangler: - specifier: ^4.77.0 - version: 4.77.0(@cloudflare/workers-types@4.20260422.1) - devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20260418.1 - version: 4.20260422.1 - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 - '@types/node': - specifier: 'catalog:' - version: 24.10.1 - '@types/react': - specifier: 'catalog:' - version: 19.2.14 - typescript: - specifier: 'catalog:' - version: 5.9.3 - apps/site: dependencies: '@heroicons/react': @@ -129,10 +92,10 @@ importers: version: 3.1.1 '@node-core/platform-cloudflare': specifier: workspace:* - version: link:../cloudflare + version: link:../../platforms/cloudflare '@node-core/platform-vercel': specifier: workspace:* - version: link:../vercel + version: link:../../platforms/vercel '@node-core/rehype-shiki': specifier: workspace:* version: link:../../packages/rehype-shiki @@ -318,46 +281,6 @@ importers: specifier: 0.4.2 version: 0.4.2 - apps/vercel: - dependencies: - '@opentelemetry/api-logs': - specifier: ~0.213.0 - version: 0.213.0 - '@opentelemetry/instrumentation': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ~1.30.1 - version: 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) - '@vercel/analytics': - specifier: ~2.0.1 - version: 2.0.1(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - '@vercel/otel': - specifier: ~2.1.1 - version: 2.1.1(@opentelemetry/api-logs@0.213.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)) - '@vercel/speed-insights': - specifier: ~2.0.0 - version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - next: - specifier: 'catalog:' - version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: - specifier: 'catalog:' - version: 19.2.4 - devDependencies: - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 - '@types/react': - specifier: 'catalog:' - version: 19.2.14 - typescript: - specifier: 'catalog:' - version: 5.9.3 - packages/i18n: devDependencies: typescript: @@ -646,6 +569,83 @@ importers: specifier: 4.21.0 version: 4.21.0 + platforms/cloudflare: + dependencies: + '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': + specifier: ^0.0.1 + version: 0.0.1 + '@opennextjs/cloudflare': + specifier: ^1.19.3 + version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) + '@sentry/cloudflare': + specifier: ^10.49.0 + version: 10.49.0(@cloudflare/workers-types@4.20260422.1) + next: + specifier: 'catalog:' + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 + wrangler: + specifier: ^4.77.0 + version: 4.77.0(@cloudflare/workers-types@4.20260422.1) + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260418.1 + version: 4.20260422.1 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/node': + specifier: 'catalog:' + version: 24.10.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 + + platforms/vercel: + dependencies: + '@opentelemetry/api-logs': + specifier: ~0.213.0 + version: 0.213.0 + '@opentelemetry/instrumentation': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ~1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@vercel/analytics': + specifier: ~2.0.1 + version: 2.0.1(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@vercel/otel': + specifier: ~2.1.1 + version: 2.1.1(@opentelemetry/api-logs@0.213.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)) + '@vercel/speed-insights': + specifier: ~2.0.0 + version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + next: + specifier: 'catalog:' + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 + devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages: '@actions/core@2.0.3': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 826c636cd76b7..d8adee090dd40 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - packages/* - apps/* + - platforms/* catalog: '@types/node': ^24.10.1 From 68b5524a051eb2855abacade37428db0c399abd0 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 24 Apr 2026 18:57:18 -0300 Subject: [PATCH 16/16] fix(platform): restore platform-vercel next config test under platforms/ The prior commit accidentally dropped this test during the directory rename rather than moving it alongside the other platforms/vercel files. Co-Authored-By: Claude Opus 4.7 --- .../vercel/__tests__/next.platform.config.test.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 platforms/vercel/__tests__/next.platform.config.test.mjs diff --git a/platforms/vercel/__tests__/next.platform.config.test.mjs b/platforms/vercel/__tests__/next.platform.config.test.mjs new file mode 100644 index 0000000000000..47d857a0e6122 --- /dev/null +++ b/platforms/vercel/__tests__/next.platform.config.test.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +describe('platform-vercel next.platform.config', () => { + it('defines shiki mdx defaults for Vercel builds', async () => { + const { default: platform } = await import('../next.platform.config.mjs'); + + assert.deepEqual(platform.mdx, { + wasm: true, + twoslash: true, + }); + }); +});