From 40d90f44cef1732a9e98dcd91b3f0690b9a4f486 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 13:07:30 -0300 Subject: [PATCH 01/24] 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 00cc9d12c9c94..a37d1e9f89f8a 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 fef56aeaa7c66..69c801909469c 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 47a5049c2b701..93dedc8ce9ddf 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", @@ -34,6 +35,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -53,6 +55,7 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -77,6 +80,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 a5ab910a916bbca993047e231c1199e104ba2e08 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 14:41:55 -0300 Subject: [PATCH 02/24] 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 | 21 +-- 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 | 151 +++++++++++------- 42 files changed, 591 insertions(+), 260 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 a37d1e9f89f8a..f30451082a9af 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 69c801909469c..b307b99f21491 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", @@ -33,24 +30,20 @@ "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/orama": "^3.1.18", + "@orama/core": "^1.2.19", + "@orama/ui": "^1.5.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "~4.2.2", "@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", @@ -80,11 +73,8 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20260418.1", "@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", - "@orama/core": "^1.2.19", "@playwright/test": "^1.58.2", "@sentry/cloudflare": "^10.49.0", "@testing-library/user-event": "~14.6.1", @@ -107,8 +97,7 @@ "tsx": "^4.21.0", "typescript": "catalog:", "typescript-eslint": "~8.57.2", - "user-agent-data-types": "0.4.2", - "wrangler": "^4.77.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 93dedc8ce9ddf..36833d3122de9 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", @@ -33,7 +32,6 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -53,7 +51,6 @@ "cache": false, "persistent": true, "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -78,7 +75,6 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", @@ -133,7 +129,6 @@ "inputs": ["{pages}/**/*.{mdx,md}"], "outputs": ["public/blog-data.json"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "TURBO_CACHE", "TURBO_TELEMETRY_DISABLED", @@ -141,39 +136,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 e54940426bc72..ff19d5e3ef3e5 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -38,7 +38,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) @@ -151,7 +153,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 @@ -296,6 +300,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 @@ -304,6 +322,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 057903121005b..c14e0399f3a7b 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,21 +105,12 @@ 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/orama': - specifier: ^3.1.18 - version: 3.1.18 + '@orama/core': + specifier: ^1.2.19 + version: 1.2.19 + '@orama/ui': + specifier: ^1.5.4 + version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-tabs': specifier: ^1.1.13 version: 1.1.13(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -132,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 @@ -217,33 +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)) - '@orama/core': - specifier: ^1.2.19 - version: 1.2.19 '@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 @@ -2255,10 +2302,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'} @@ -4391,11 +4434,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==} @@ -4483,9 +4521,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==} @@ -10591,9 +10626,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': {} @@ -12753,8 +12786,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 @@ -12807,8 +12838,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) @@ -12869,8 +12900,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 8c900fd771813ac617a12feb7d7d7c733f07bfaa Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 14:48:26 -0300 Subject: [PATCH 03/24] 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 eed7c1d7049a594bee9c50e8e2d9a4e821071fec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 18:56:38 +0000 Subject: [PATCH 04/24] 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 9a23e445b5e09b794fab8b1787fd99cbc58e2250 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:01:29 +0000 Subject: [PATCH 05/24] 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 b307b99f21491..6c72ee353dc76 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -50,7 +50,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 c14e0399f3a7b..301d3cf3677a9 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:' @@ -1201,9 +1204,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==} @@ -9663,11 +9663,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 @@ -10156,7 +10151,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 a349766c95f5623f6bbf529c44ea44a7564b2d56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:40:44 +0000 Subject: [PATCH 06/24] 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 fdcd1408af26d45509ddc06f3510050a57df11dd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:45:47 +0000 Subject: [PATCH 07/24] 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 43c26ec603bf64ad684cd1286b67a90f54ed7f5a Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 17:47:49 -0300 Subject: [PATCH 08/24] 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 6c72ee353dc76..71fd28b1d8f25 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -30,8 +30,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:*", @@ -99,6 +97,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 ff19d5e3ef3e5..d5982aa0ae344 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -307,13 +307,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 @@ -325,7 +325,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 301d3cf3677a9..8b80643ae3705 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 39b6bafbb6f13da9d3b61ff4ba7193ee36c0a684 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 17:49:43 -0300 Subject: [PATCH 09/24] 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 d5982aa0ae344..528eec4fb8044 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -129,22 +129,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 @@ -152,10 +157,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 0a3b89c148d8fa00eafd75189287903c70febc65 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:24:56 -0300 Subject: [PATCH 10/24] 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 71fd28b1d8f25..3e5438380aeac 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -118,7 +118,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 8ae0372a49cc29eba5e5e2b9d734b0f7a87873bd Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:43:46 -0300 Subject: [PATCH 11/24] 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 b39010b630f6bdd69a3fd138e349609c304f8d79 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 18:48:37 -0300 Subject: [PATCH 12/24] 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 dda62685cebe329124667bb27fab4b45680ec859 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 19:24:18 -0300 Subject: [PATCH 13/24] 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 d3f4233eb17938ebe686dfbdb657110347121d5e Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 23 Apr 2026 20:22:02 -0300 Subject: [PATCH 14/24] 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 092b64038b89630615df2b521515133aa4c8b0e6 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 24 Apr 2026 18:55:39 -0300 Subject: [PATCH 15/24] 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 528eec4fb8044..bd6a73fa24786 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -309,25 +309,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 8b80643ae3705..6664ab88c7927 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: @@ -649,6 +572,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 6a73da993000cc4b543b1a0256a2fa67bd5da56f Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 24 Apr 2026 18:57:18 -0300 Subject: [PATCH 16/24] 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, + }); + }); +}); From d0d3a18d4a61f2ca0c538857c587a183bf1e00f6 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sat, 25 Apr 2026 22:08:27 -0300 Subject: [PATCH 17/24] refactor(platform): flatten platform packages and route via #platform Collapse platforms/{vercel,cloudflare}/src/ into the package roots so analytics, instrumentation, image-loader, and the worker entrypoint sit at the package root. Both packages now expose a single flat exports map (`./*: ./*`); platform-cloudflare no longer needs an explicit `./image-loader` entry because the consumer (next.platform.config.mjs) calls `require.resolve(...image-loader.ts)` with the extension. Move apps/site/{next,playwright}.platform.config.mjs into apps/site/platform/ and update the apps/site `#platform/*` imports default branch to a single literal path (`./platform/*`). Import sites carry explicit extensions so Node resolves them without extension fallback: `#platform/next.platform.config.mjs`, `#platform/playwright.platform.config.mjs`. Update platforms/cloudflare/wrangler.jsonc `main` to the flattened worker path, and align both platform tsconfigs on `include: ["**/*.ts","**/*.tsx","**/*.mjs"]` now that there is no `src/` to scope to. Also: factor DEPLOY_TARGET out of next.constants.mjs into a small next.platform.constants.mjs (avoids dragging client-side env into build-time config), simplify transpilePackages derivation, drop the playwright.config.d.ts `Pick` (resolves `use` to `{}`) in favor of `PlaywrightTestConfig`, and refresh docs/technologies.md. Verified: standalone build, Vercel build (`NEXT_PUBLIC_DEPLOY_TARGET= vercel --conditions=vercel`), Cloudflare worker build, and `playwright --list` (18 tests) all green. Co-Authored-By: Claude Opus 4.7 --- apps/site/mdx/plugins.mjs | 6 +-- apps/site/next.config.mjs | 27 +++++++------ apps/site/next.constants.mjs | 14 ------- apps/site/next.platform.constants.mjs | 15 +++++++ apps/site/package.json | 13 ++----- .../{ => platform}/next.platform.config.mjs | 0 .../platform/playwright.platform.config.mjs | 12 ++++++ apps/site/playwright.config.ts | 9 ++--- apps/site/playwright.platform.config.d.ts | 10 ++--- apps/site/playwright.platform.config.mjs | 11 ------ apps/site/tsconfig.json | 3 +- docs/technologies.md | 39 ++++++++++--------- platforms/cloudflare/{src => }/analytics.tsx | 0 .../cloudflare/{src => }/image-loader.ts | 0 .../cloudflare/{src => }/instrumentation.ts | 0 platforms/cloudflare/next.platform.config.mjs | 2 +- platforms/cloudflare/package.json | 9 +---- .../cloudflare/playwright.platform.config.mjs | 6 ++- platforms/cloudflare/tsconfig.json | 8 +--- .../cloudflare/{src => }/worker-entrypoint.ts | 0 platforms/cloudflare/wrangler.jsonc | 2 +- .../__tests__/next.platform.config.test.mjs | 13 ------- platforms/vercel/{src => }/analytics.tsx | 0 platforms/vercel/{src => }/instrumentation.ts | 0 platforms/vercel/next.platform.config.mjs | 11 +++--- platforms/vercel/package.json | 7 +--- .../vercel/playwright.platform.config.mjs | 7 ++-- platforms/vercel/tsconfig.json | 7 +--- 28 files changed, 99 insertions(+), 132 deletions(-) create mode 100644 apps/site/next.platform.constants.mjs rename apps/site/{ => platform}/next.platform.config.mjs (100%) create mode 100644 apps/site/platform/playwright.platform.config.mjs delete mode 100644 apps/site/playwright.platform.config.mjs rename platforms/cloudflare/{src => }/analytics.tsx (100%) rename platforms/cloudflare/{src => }/image-loader.ts (100%) rename platforms/cloudflare/{src => }/instrumentation.ts (100%) rename platforms/cloudflare/{src => }/worker-entrypoint.ts (100%) delete mode 100644 platforms/vercel/__tests__/next.platform.config.test.mjs rename platforms/vercel/{src => }/analytics.tsx (100%) rename platforms/vercel/{src => }/instrumentation.ts (100%) diff --git a/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index 75c92d3ed9c59..e17b69b7263df 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -7,11 +7,7 @@ import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; import readingTime from 'remark-reading-time'; -// 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'; +import platform from '#platform/next.platform.config.mjs'; import remarkTableTitles from '../util/table'; diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 7349a457e06e4..dea459c6303d7 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -2,16 +2,20 @@ import createNextIntlPlugin from 'next-intl/plugin'; -import platform from '#platform/next.platform.config'; +import platform from '#platform/next.platform.config.mjs'; -import { - BASE_PATH, - ENABLE_STATIC_EXPORT, - DEPLOY_TARGET, -} from './next.constants.mjs'; +import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; +import { DEPLOY_TARGET } from './next.platform.constants.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; +const platformImages = await platform.images?.(); +const platformNextConfig = await platform.nextConfig?.(); + +const transpilePackages = DEPLOY_TARGET + ? [`@node-core/platform-${DEPLOY_TARGET}`] + : []; + /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -21,14 +25,9 @@ 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(await platform.images?.()), + images: getImagesConfig(platformImages), 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', - ], + transpilePackages, outputFileTracingIncludes: { // Twoslash needs TypeScript declarations to function, and, by default, Next.js // strips them for brevity. Therefore, they must be explicitly included. @@ -96,7 +95,7 @@ const nextConfig = { .filter(Boolean), }, }), - ...(await platform.nextConfig?.()), + ...platformNextConfig, }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index f30451082a9af..b1be612762b8d 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -5,20 +5,6 @@ */ export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; -/** - * Identifies the deployment platform the site is being built for. - * - * 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). - * - * 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 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.platform.constants.mjs b/apps/site/next.platform.constants.mjs new file mode 100644 index 0000000000000..1f7274905acea --- /dev/null +++ b/apps/site/next.platform.constants.mjs @@ -0,0 +1,15 @@ +'use strict'; + +/** + * Identifies the deployment platform the site is being built for. + * + * Set by the deployment wrapper at build time: `vercel.json`'s `build.env` + * sets `vercel`, `open-next.config.ts`'s `buildCommand` sets `cloudflare`. + * Unset for standalone builds (local dev, static export). + * + * 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 DEPLOY_TARGET = process.env.NEXT_PUBLIC_DEPLOY_TARGET; diff --git a/apps/site/package.json b/apps/site/package.json index 3e5438380aeac..c765cd522c0bf 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -119,15 +119,10 @@ "./*.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" + "#platform/*": { + "cloudflare": "@node-core/platform-cloudflare/*", + "vercel": "@node-core/platform-vercel/*", + "default": "./platform/*" } }, "engines": { diff --git a/apps/site/next.platform.config.mjs b/apps/site/platform/next.platform.config.mjs similarity index 100% rename from apps/site/next.platform.config.mjs rename to apps/site/platform/next.platform.config.mjs diff --git a/apps/site/platform/playwright.platform.config.mjs b/apps/site/platform/playwright.platform.config.mjs new file mode 100644 index 0000000000000..1f448eba37b59 --- /dev/null +++ b/apps/site/platform/playwright.platform.config.mjs @@ -0,0 +1,12 @@ +/** + * Default Playwright platform config used when no `DEPLOY_TARGET` is set — + * local dev against `next dev`, static export, generic hosting. Each + * platform contributes its own `baseURL` (with optional + * `PLAYWRIGHT_BASE_URL` override for CI), so consumers just spread + * `platform.use` into their `defineConfig` call. + * + * @type {import('../playwright.platform.config').PlatformPlaywrightConfig} + */ +export default { + use: { baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000' }, +}; diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.ts index 67ed01bde6856..fc5ef10725042 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.ts @@ -1,6 +1,6 @@ import { defineConfig, devices } from '@playwright/test'; -import platform from '#platform/playwright.platform.config'; +import platform from '#platform/playwright.platform.config.mjs'; const isCI = !!process.env.CI; @@ -12,12 +12,9 @@ export default defineConfig({ retries: isCI ? 2 : 0, workers: isCI ? 1 : undefined, reporter: isCI ? [['html'], ['github']] : [['html']], - ...(platform.webServer ? { webServer: platform.webServer } : {}), + ...platform, use: { - baseURL: - process.env.PLAYWRIGHT_BASE_URL || - platform.baseURL || - 'http://127.0.0.1:3000', + ...platform.use, trace: 'on-first-retry', }, projects: [ diff --git a/apps/site/playwright.platform.config.d.ts b/apps/site/playwright.platform.config.d.ts index ada0174396183..3a9f2f3f27f05 100644 --- a/apps/site/playwright.platform.config.d.ts +++ b/apps/site/playwright.platform.config.d.ts @@ -1,14 +1,14 @@ -import type { Config } from '@playwright/test'; +import type { PlaywrightTestConfig } 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']; -}; +export type PlatformPlaywrightConfig = Pick< + PlaywrightTestConfig, + 'webServer' | 'use' +>; declare const config: PlatformPlaywrightConfig; diff --git a/apps/site/playwright.platform.config.mjs b/apps/site/playwright.platform.config.mjs deleted file mode 100644 index 2ce9df7ea44f5..0000000000000 --- a/apps/site/playwright.platform.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 {import('./playwright.platform.config').PlatformPlaywrightConfig} - */ -export default {}; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 865ec44b098f5..5c8c00eb236f4 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -22,8 +22,7 @@ ], "baseUrl": ".", "paths": { - "@platform/analytics": ["./platform/analytics.tsx"], - "@platform/instrumentation": ["./platform/instrumentation.ts"] + "@platform/*": ["./platform/*"] } }, "mdx": { diff --git a/docs/technologies.md b/docs/technologies.md index bd6a73fa24786..c041ea8dd65ec 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -129,25 +129,28 @@ 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 -│ ├── vercel/ # Vercel deployment adapter +│ └── site/ # Main website application (platform-agnostic) +│ ├── 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. +│ ├── platform/ # No-op platform stubs (resolved via +│ │ # `@platform/*` when no DEPLOY_TARGET set) +│ ├── 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 +├── platforms/ # Deployment-target adapters +│ ├── vercel/ # Vercel adapter — @node-core/platform-vercel │ │ # (analytics, instrumentation, vercel.json) -│ └── cloudflare/ # Cloudflare deployment adapter +│ └── cloudflare/ # Cloudflare adapter — @node-core/platform-cloudflare │ # (worker entrypoint, image loader, │ # open-next.config.ts, wrangler.jsonc) └── packages/ diff --git a/platforms/cloudflare/src/analytics.tsx b/platforms/cloudflare/analytics.tsx similarity index 100% rename from platforms/cloudflare/src/analytics.tsx rename to platforms/cloudflare/analytics.tsx diff --git a/platforms/cloudflare/src/image-loader.ts b/platforms/cloudflare/image-loader.ts similarity index 100% rename from platforms/cloudflare/src/image-loader.ts rename to platforms/cloudflare/image-loader.ts diff --git a/platforms/cloudflare/src/instrumentation.ts b/platforms/cloudflare/instrumentation.ts similarity index 100% rename from platforms/cloudflare/src/instrumentation.ts rename to platforms/cloudflare/instrumentation.ts diff --git a/platforms/cloudflare/next.platform.config.mjs b/platforms/cloudflare/next.platform.config.mjs index 5d0871273100f..5549b8a1130e8 100644 --- a/platforms/cloudflare/next.platform.config.mjs +++ b/platforms/cloudflare/next.platform.config.mjs @@ -49,7 +49,7 @@ export default { // which webpack rewrites as an asset reference and mangles at runtime. loaderFile: relative( process.cwd(), - require.resolve('@node-core/platform-cloudflare/image-loader') + require.resolve('@node-core/platform-cloudflare/image-loader.ts') ), }; }, diff --git a/platforms/cloudflare/package.json b/platforms/cloudflare/package.json index 0e36a4360e393..661266c1423ec 100644 --- a/platforms/cloudflare/package.json +++ b/platforms/cloudflare/package.json @@ -4,12 +4,7 @@ "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", @@ -40,6 +35,6 @@ "typescript": "catalog:" }, "engines": { - "node": ">=20" + "node": "24.x" } } diff --git a/platforms/cloudflare/playwright.platform.config.mjs b/platforms/cloudflare/playwright.platform.config.mjs index b405cfb803b95..006c5cb3a08d9 100644 --- a/platforms/cloudflare/playwright.platform.config.mjs +++ b/platforms/cloudflare/playwright.platform.config.mjs @@ -1,3 +1,5 @@ +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8787'; + /** * Playwright overrides contributed by the Cloudflare deployment target. * @@ -8,11 +10,11 @@ * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} */ export default { - baseURL: 'http://127.0.0.1:8787', + use: { baseURL: BASE_URL }, webServer: { stdout: 'pipe', command: 'pnpm --filter=@node-core/platform-cloudflare cloudflare:preview', - url: 'http://127.0.0.1:8787', + url: BASE_URL, timeout: 60_000 * 3, }, }; diff --git a/platforms/cloudflare/tsconfig.json b/platforms/cloudflare/tsconfig.json index 95f515d1bbb7d..378ecbb585105 100644 --- a/platforms/cloudflare/tsconfig.json +++ b/platforms/cloudflare/tsconfig.json @@ -13,10 +13,6 @@ "isolatedModules": true, "jsx": "react-jsx" }, - "include": [ - "src", - "next.platform.config.mjs", - "playwright.platform.config.mjs" - ], - "exclude": ["src/worker-entrypoint.ts"] + "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"], + "exclude": ["node_modules", "worker-entrypoint.ts"] } diff --git a/platforms/cloudflare/src/worker-entrypoint.ts b/platforms/cloudflare/worker-entrypoint.ts similarity index 100% rename from platforms/cloudflare/src/worker-entrypoint.ts rename to platforms/cloudflare/worker-entrypoint.ts diff --git a/platforms/cloudflare/wrangler.jsonc b/platforms/cloudflare/wrangler.jsonc index 963fbaff24472..2a62adc83c739 100644 --- a/platforms/cloudflare/wrangler.jsonc +++ b/platforms/cloudflare/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "./node_modules/wrangler/config-schema.json", - "main": "./src/worker-entrypoint.ts", + "main": "./worker-entrypoint.ts", "name": "nodejs-website", "compatibility_date": "2024-11-07", "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], diff --git a/platforms/vercel/__tests__/next.platform.config.test.mjs b/platforms/vercel/__tests__/next.platform.config.test.mjs deleted file mode 100644 index 47d857a0e6122..0000000000000 --- a/platforms/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/platforms/vercel/src/analytics.tsx b/platforms/vercel/analytics.tsx similarity index 100% rename from platforms/vercel/src/analytics.tsx rename to platforms/vercel/analytics.tsx diff --git a/platforms/vercel/src/instrumentation.ts b/platforms/vercel/instrumentation.ts similarity index 100% rename from platforms/vercel/src/instrumentation.ts rename to platforms/vercel/instrumentation.ts diff --git a/platforms/vercel/next.platform.config.mjs b/platforms/vercel/next.platform.config.mjs index e216e33866df3..1deeacb58332a 100644 --- a/platforms/vercel/next.platform.config.mjs +++ b/platforms/vercel/next.platform.config.mjs @@ -25,15 +25,16 @@ export default { const VERCEL_URL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined; + const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL; 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 || '', - }, + // canonical env var. A manually-set `NEXT_PUBLIC_BASE_URL` wins. Only + // contribute the entry when a value is actually present so downstream + // truthiness vs. existence checks behave the same as if the var were + // never set. + env: NEXT_PUBLIC_BASE_URL ? { NEXT_PUBLIC_BASE_URL } : undefined, }; }, }; diff --git a/platforms/vercel/package.json b/platforms/vercel/package.json index 2df4c823eb071..2bb969abc3741 100644 --- a/platforms/vercel/package.json +++ b/platforms/vercel/package.json @@ -4,10 +4,7 @@ "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", @@ -36,6 +33,6 @@ "typescript": "catalog:" }, "engines": { - "node": ">=20" + "node": "24.x" } } diff --git a/platforms/vercel/playwright.platform.config.mjs b/platforms/vercel/playwright.platform.config.mjs index 657a046c5c621..3236d58b7d251 100644 --- a/platforms/vercel/playwright.platform.config.mjs +++ b/platforms/vercel/playwright.platform.config.mjs @@ -3,9 +3,10 @@ * * 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. + * the deployment. * * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} */ -export default {}; +export default { + use: { baseURL: process.env.PLAYWRIGHT_BASE_URL }, +}; diff --git a/platforms/vercel/tsconfig.json b/platforms/vercel/tsconfig.json index 5210f82773ae5..aa6f18311fe3f 100644 --- a/platforms/vercel/tsconfig.json +++ b/platforms/vercel/tsconfig.json @@ -13,9 +13,6 @@ "isolatedModules": true, "jsx": "react-jsx" }, - "include": [ - "src", - "next.platform.config.mjs", - "playwright.platform.config.mjs" - ] + "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"], + "exclude": ["node_modules"] } From a0cec9dd62bb573d701627e6c49c24f3b08bc0cb Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sat, 25 Apr 2026 22:15:06 -0300 Subject: [PATCH 18/24] fix(cloudflare): repair paths after platforms/cloudflare flatten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit worker-entrypoint.ts moved from src/ to the package root in the previous commit but the relative imports of the OpenNext-built worker still pointed at `../../../apps/site/.open-next/worker.js`, one segment too deep. wrangler dev (used by Playwright via cloudflare:preview) re-bundles the entrypoint and surfaced the regression — `opennextjs-cloudflare build` had silently tolerated it. Adjust both imports to `../../apps/site/.open-next/worker.js`. While here, update the cloudflare#cloudflare:build:worker turbo inputs glob from `src/**/*.ts` to `*.ts` so the cache key tracks the now-flat package. Also drop a duplicate `apps/site/redirects.json @nodejs/web-infra` line from CODEOWNERS that crept in when the platform entries were inserted between the original and a stray copy. Co-Authored-By: Claude Opus 4.7 --- .github/CODEOWNERS | 1 - platforms/cloudflare/turbo.json | 2 +- platforms/cloudflare/worker-entrypoint.ts | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4a0ebcfecd457..734b7c91e599f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -32,7 +32,6 @@ 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 LICENSE @nodejs/tsc diff --git a/platforms/cloudflare/turbo.json b/platforms/cloudflare/turbo.json index 8c1ffdc95a974..a04eafed7f22b 100644 --- a/platforms/cloudflare/turbo.json +++ b/platforms/cloudflare/turbo.json @@ -7,7 +7,7 @@ "inputs": [ "open-next.config.ts", "wrangler.jsonc", - "src/**/*.ts", + "*.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}", diff --git a/platforms/cloudflare/worker-entrypoint.ts b/platforms/cloudflare/worker-entrypoint.ts index f63ab60a6f586..9ce832bf58f15 100644 --- a/platforms/cloudflare/worker-entrypoint.ts +++ b/platforms/cloudflare/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 '../../apps/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 '../../apps/site/.open-next/worker.js'; From 130867d60e06c2ebcc16466e746cb26d0b9d99e7 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 00:08:53 -0300 Subject: [PATCH 19/24] refactor(platform): rename platform scripts and add default platform package Rename platform build/dev/deploy scripts to suffixed names (build:cloudflare, build:vercel, dev:cloudflare, dev:vercel, deploy:cloudflare) so they no longer collide with Turbo's ^build cascade. Add a platforms/default package as the standalone baseline (analytics noop, instrumentation, playwright, next config), matching the same shape as the cloudflare and vercel packages. Drop the per- platform turbo.json files in favor of a root lint:types task. Co-Authored-By: Claude Opus 4.7 --- .github/CODEOWNERS | 4 +- .../playwright-cloudflare-open-next.yml | 6 +-- .../tmp-cloudflare-open-next-deploy.yml | 9 +--- apps/site/mdx/plugins.mjs | 3 +- apps/site/next.config.mjs | 21 +++++---- apps/site/next.constants.mjs | 2 +- apps/site/next.platform.config.d.ts | 5 +++ apps/site/next.platform.constants.mjs | 4 +- apps/site/package.json | 11 +++-- apps/site/platform/next.platform.config.mjs | 26 ----------- apps/site/playwright.config.ts | 9 +++- apps/site/tsconfig.json | 14 +++--- docs/cloudflare-build-and-deployment.md | 15 ++++--- docs/technologies.md | 24 ++++++----- package.json | 3 -- ...xt.platform.config.mjs => next.config.mjs} | 13 ++---- platforms/cloudflare/open-next.config.ts | 3 +- platforms/cloudflare/package.json | 7 +-- ...tform.config.mjs => playwright.config.mjs} | 2 +- platforms/cloudflare/turbo.json | 43 ------------------- .../default}/analytics.tsx | 0 .../default}/instrumentation.ts | 0 platforms/default/next.config.mjs | 19 ++++++++ platforms/default/package.json | 29 +++++++++++++ .../default/playwright.config.mjs | 2 +- platforms/default/tsconfig.json | 18 ++++++++ ...xt.platform.config.mjs => next.config.mjs} | 13 ++---- platforms/vercel/package.json | 3 ++ ...tform.config.mjs => playwright.config.mjs} | 0 platforms/vercel/vercel.json | 8 +--- pnpm-lock.yaml | 28 ++++++++++++ turbo.json | 3 ++ 32 files changed, 182 insertions(+), 165 deletions(-) delete mode 100644 apps/site/platform/next.platform.config.mjs rename platforms/cloudflare/{next.platform.config.mjs => next.config.mjs} (79%) rename platforms/cloudflare/{playwright.platform.config.mjs => playwright.config.mjs} (87%) delete mode 100644 platforms/cloudflare/turbo.json rename {apps/site/platform => platforms/default}/analytics.tsx (100%) rename {apps/site/platform => platforms/default}/instrumentation.ts (100%) create mode 100644 platforms/default/next.config.mjs create mode 100644 platforms/default/package.json rename apps/site/platform/playwright.platform.config.mjs => platforms/default/playwright.config.mjs (82%) create mode 100644 platforms/default/tsconfig.json rename platforms/vercel/{next.platform.config.mjs => next.config.mjs} (71%) rename platforms/vercel/{playwright.platform.config.mjs => playwright.config.mjs} (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 734b7c91e599f..9764456b3ab02 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,9 +29,9 @@ apps/site/redirects.json @nodejs/web-infra apps/site/site.json @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/cloudflare/next.config.mjs @nodejs/web-infra platforms/vercel/vercel.json @nodejs/web-infra -platforms/vercel/next.platform.config.mjs @nodejs/web-infra +platforms/vercel/next.config.mjs @nodejs/web-infra # Critical Documents LICENSE @nodejs/tsc diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 76c76ccf7c48a..670697d58f190 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -52,17 +52,13 @@ jobs: - name: Build open-next site working-directory: platforms/cloudflare - run: node --run cloudflare:build:worker - env: - NEXT_PUBLIC_DEPLOY_TARGET: cloudflare - NODE_OPTIONS: --conditions=cloudflare + run: node --run build:cloudflare - name: Run Playwright tests working-directory: apps/site run: node --run playwright env: NEXT_PUBLIC_DEPLOY_TARGET: cloudflare - NODE_OPTIONS: --conditions=cloudflare - name: Upload Playwright test results if: always() diff --git a/.github/workflows/tmp-cloudflare-open-next-deploy.yml b/.github/workflows/tmp-cloudflare-open-next-deploy.yml index 42deb38324d5c..1a1f062ae34c5 100644 --- a/.github/workflows/tmp-cloudflare-open-next-deploy.yml +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -58,17 +58,12 @@ jobs: - name: Build open-next site working-directory: platforms/cloudflare - run: node --run cloudflare:build:worker - env: - NEXT_PUBLIC_DEPLOY_TARGET: cloudflare - NODE_OPTIONS: --conditions=cloudflare + run: node --run build:cloudflare - name: Deploy open-next site working-directory: platforms/cloudflare - run: node --run cloudflare:deploy + run: node --run deploy:cloudflare 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/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index e17b69b7263df..6e17e91c15e5e 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -1,14 +1,13 @@ 'use strict'; import rehypeShikiji from '@node-core/rehype-shiki/plugin'; +import platform from '@platform/next.config.mjs'; import remarkHeadings from '@vcarl/remark-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; import readingTime from 'remark-reading-time'; -import platform from '#platform/next.platform.config.mjs'; - import remarkTableTitles from '../util/table'; // Shiki is created out here to avoid an async rehype plugin diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index dea459c6303d7..c2c66cce25cb5 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -2,20 +2,22 @@ import createNextIntlPlugin from 'next-intl/plugin'; -import platform from '#platform/next.platform.config.mjs'; - import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; import { DEPLOY_TARGET } from './next.platform.constants.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; +// Loaded by Node directly (Next.js doesn't bundle `next.config.mjs`), so +// we resolve the active platform via a dynamic import keyed on +// `DEPLOY_TARGET` rather than a `@platform/*` alias (those only resolve +// inside Turbopack/webpack). +const { default: platform } = await import( + `@node-core/platform-${DEPLOY_TARGET}/next.config.mjs` +); + const platformImages = await platform.images?.(); const platformNextConfig = await platform.nextConfig?.(); -const transpilePackages = DEPLOY_TARGET - ? [`@node-core/platform-${DEPLOY_TARGET}`] - : []; - /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -27,7 +29,7 @@ const nextConfig = { basePath: BASE_PATH, images: getImagesConfig(platformImages), serverExternalPackages: ['twoslash'], - transpilePackages, + transpilePackages: [`@node-core/platform-${DEPLOY_TARGET}`], outputFileTracingIncludes: { // Twoslash needs TypeScript declarations to function, and, by default, Next.js // strips them for brevity. Therefore, they must be explicitly included. @@ -81,10 +83,7 @@ const nextConfig = { }, // Provide Turbopack Aliases for Platform Resolution turbopack: { resolveAlias: platform.aliases }, - // 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. + // Provide Webpack Aliases for Platform Resolution. webpack: ({ resolve, ...config }) => ({ ...config, resolve: { diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index b1be612762b8d..8882bf26dcd27 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -32,7 +32,7 @@ export const ENABLE_STATIC_EXPORT_LOCALE = * The full canonical URL of the deployed Website (used e.g. for the RSS feed). * * 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`, + * `NEXT_PUBLIC_BASE_URL` at build time by each platform's `next.config.mjs`, * keeping this module free of platform-specific branches. */ export const BASE_URL = diff --git a/apps/site/next.platform.config.d.ts b/apps/site/next.platform.config.d.ts index 7094cef1660cd..146ec140c10c3 100644 --- a/apps/site/next.platform.config.d.ts +++ b/apps/site/next.platform.config.d.ts @@ -8,6 +8,11 @@ type PlatformNextConfig = Pick; * Shared platform-config contract consumed by `apps/site/next.config.mjs` * and implemented by each `@node-core/platform-` package. * + * `aliases` are surfaced as Turbopack/webpack aliases so that + * `@platform/*` imports in bundled code (e.g. the `@analytics/` + * parallel-route slot, `instrumentation.ts`, `mdx/plugins.mjs`) resolve + * to the active platform's files. + * * `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 diff --git a/apps/site/next.platform.constants.mjs b/apps/site/next.platform.constants.mjs index 1f7274905acea..ffd85865d7d78 100644 --- a/apps/site/next.platform.constants.mjs +++ b/apps/site/next.platform.constants.mjs @@ -10,6 +10,6 @@ * 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} + * @type {'vercel' | 'cloudflare' | 'default'} */ -export const DEPLOY_TARGET = process.env.NEXT_PUBLIC_DEPLOY_TARGET; +export const DEPLOY_TARGET = process.env.NEXT_PUBLIC_DEPLOY_TARGET ?? 'default'; diff --git a/apps/site/package.json b/apps/site/package.json index c765cd522c0bf..fed39e2e15cd5 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -99,6 +99,7 @@ }, "peerDependencies": { "@node-core/platform-cloudflare": "workspace:*", + "@node-core/platform-default": "workspace:*", "@node-core/platform-vercel": "workspace:*" }, "peerDependenciesMeta": { @@ -107,6 +108,9 @@ }, "@node-core/platform-vercel": { "optional": true + }, + "@node-core/platform-default": { + "optional": true } }, "imports": { @@ -118,12 +122,7 @@ "./*/index.ts", "./*.mjs", "./*/index.mjs" - ], - "#platform/*": { - "cloudflare": "@node-core/platform-cloudflare/*", - "vercel": "@node-core/platform-vercel/*", - "default": "./platform/*" - } + ] }, "engines": { "node": "24.x" diff --git a/apps/site/platform/next.platform.config.mjs b/apps/site/platform/next.platform.config.mjs deleted file mode 100644 index 1ad7c451d1fb0..0000000000000 --- a/apps/site/platform/next.platform.config.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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. - * - * 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: { - '@platform/analytics': './platform/analytics.tsx', - '@platform/instrumentation': './platform/instrumentation.ts', - }, - 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/playwright.config.ts b/apps/site/playwright.config.ts index fc5ef10725042..9f715e01b8aa8 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.ts @@ -1,6 +1,13 @@ import { defineConfig, devices } from '@playwright/test'; -import platform from '#platform/playwright.platform.config.mjs'; +import { DEPLOY_TARGET } from './next.platform.constants.mjs'; + +// Playwright loads this config via Node, so resolve the active platform +// via a dynamic import keyed on `DEPLOY_TARGET` rather than a +// `@platform/*` alias (those only resolve inside Turbopack/webpack). +const { default: platform } = await import( + `@node-core/platform-${DEPLOY_TARGET}/playwright.config.mjs` +); const isCI = !!process.env.CI; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 5c8c00eb236f4..71e5b18bc3e49 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -11,18 +11,20 @@ "module": "esnext", "moduleResolution": "Bundler", "customConditions": ["default"], + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, - "plugins": [ - { - "name": "next" - } - ], + "plugins": [{ "name": "next" }], "baseUrl": ".", "paths": { - "@platform/*": ["./platform/*"] + "@platform/*": [ + "../../platforms/default/*", + "../../platforms/default/*.ts", + "../../platforms/default/*.tsx", + "../../platforms/default/*.mjs" + ] } }, "mdx": { diff --git a/docs/cloudflare-build-and-deployment.md b/docs/cloudflare-build-and-deployment.md index dad2a5bbc813f..bd167713c1a50 100644 --- a/docs/cloudflare-build-and-deployment.md +++ b/docs/cloudflare-build-and-deployment.md @@ -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 ([`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). +- `main`: Points to a custom worker entry point ([`platforms/cloudflare/worker-entrypoint.ts`](../platforms/cloudflare/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 ([`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)). +When deployed on the Cloudflare network a custom image loader is required. The Cloudflare platform config ([`platforms/cloudflare/next.config.mjs`](../platforms/cloudflare/next.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 [`platforms/cloudflare/src/image-loader.ts`](../platforms/cloudflare/src/image-loader.ts). +The custom loader can be found at [`platforms/cloudflare/image-loader.ts`](../platforms/cloudflare/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 [`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). +Instead of directly using the OpenNext-generated worker (`.open-next/worker.js`), the application uses a custom worker entry point at [`platforms/cloudflare/worker-entrypoint.ts`](../platforms/cloudflare/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. @@ -80,7 +80,8 @@ For more details, refer to the [Sentry Cloudflare guide](https://docs.sentry.io/ ## Scripts -Preview and deployment of the website targeting the Cloudflare network is implemented via the following two commands: +Build, preview, and deployment of the website targeting the Cloudflare network are implemented via the following commands: -- `pnpm cloudflare:preview` builds the website using the OpenNext Cloudflare adapter and runs the website locally in a server simulating the Cloudflare hosting (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)) -- `pnpm cloudflare:deploy` builds the website using the OpenNext Cloudflare adapter and deploys the website to the Cloudflare network (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)) +- `pnpm --filter=@node-core/platform-cloudflare build:cloudflare` builds the website using the OpenNext Cloudflare adapter +- `pnpm --filter=@node-core/platform-cloudflare dev:cloudflare` runs the website locally in a server simulating the Cloudflare hosting (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)) +- `pnpm --filter=@node-core/platform-cloudflare deploy:cloudflare` deploys the previously-built website to the Cloudflare network (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)) diff --git a/docs/technologies.md b/docs/technologies.md index c041ea8dd65ec..9d8a6f7c4a0b2 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -137,8 +137,6 @@ nodejs.org/ │ │ └── {locale}/ # Translated content │ ├── public/ # Static assets │ │ └── static/ # Images, documents, etc. -│ ├── platform/ # No-op platform stubs (resolved via -│ │ # `@platform/*` when no DEPLOY_TARGET set) │ ├── hooks/ # React hooks │ ├── providers/ # React context providers │ ├── types/ # TypeScript definitions @@ -148,6 +146,8 @@ nodejs.org/ │ └── tests/ # Test files │ └── e2e/ # End-to-end tests ├── platforms/ # Deployment-target adapters +│ ├── default/ # No-op fallback — @node-core/platform-default +│ │ # (resolved when NEXT_PUBLIC_DEPLOY_TARGET unset) │ ├── vercel/ # Vercel adapter — @node-core/platform-vercel │ │ # (analytics, instrumentation, vercel.json) │ └── cloudflare/ # Cloudflare adapter — @node-core/platform-cloudflare @@ -305,20 +305,22 @@ 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 +- **`pnpm --filter=@node-core/platform-cloudflare build:cloudflare`**: Build the website using the Cloudflare (OpenNext) adapter +- **`pnpm --filter=@node-core/platform-cloudflare dev:cloudflare`**: Local preview of the Cloudflare (OpenNext) worker build +- **`pnpm --filter=@node-core/platform-cloudflare deploy:cloudflare`**: Deploy the Cloudflare (OpenNext) worker build +- **`pnpm --filter=@node-core/platform-vercel build:vercel`**: Production website build with the Vercel deployment conditions applied #### 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`. +`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.config.mjs`. -| 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` | +| 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)_ | [`@node-core/platform-default`](../platforms/default) | Plain `pnpm dev` / `pnpm build` / `pnpm deploy` | -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. +Each adapter exports a default `{ nextConfig, aliases, images, mdx }` shape (any field optional). See [`platforms/vercel/next.config.mjs`](../platforms/vercel/next.config.mjs) and [`platforms/cloudflare/next.config.mjs`](../platforms/cloudflare/next.config.mjs) for reference. #### Vercel Integration diff --git a/package.json b/package.json index 668e872b50585..1e53086e6a245 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,6 @@ "scripts": { "compile": "turbo compile", "build": "turbo build", - "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/platforms/cloudflare/next.platform.config.mjs b/platforms/cloudflare/next.config.mjs similarity index 79% rename from platforms/cloudflare/next.platform.config.mjs rename to platforms/cloudflare/next.config.mjs index 5549b8a1130e8..1abf5fadd0a0e 100644 --- a/platforms/cloudflare/next.platform.config.mjs +++ b/platforms/cloudflare/next.config.mjs @@ -1,20 +1,15 @@ /** * Platform config contributed by the Cloudflare deployment target. * - * 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('../../apps/site/next.platform.config').PlatformConfig} */ export default { aliases: { - '@platform/analytics': '@node-core/platform-cloudflare/analytics', + '@platform/analytics': '@node-core/platform-cloudflare/analytics.tsx', '@platform/instrumentation': - '@node-core/platform-cloudflare/instrumentation', + '@node-core/platform-cloudflare/instrumentation.ts', + '@platform/next.config.mjs': + '@node-core/platform-cloudflare/next.config.mjs', }, mdx: { // Cloudflare workers can't load `shiki/wasm` via `WebAssembly.instantiate` diff --git a/platforms/cloudflare/open-next.config.ts b/platforms/cloudflare/open-next.config.ts index 5ef3a63965af1..03f1c01730dc2 100644 --- a/platforms/cloudflare/open-next.config.ts +++ b/platforms/cloudflare/open-next.config.ts @@ -20,8 +20,7 @@ const cloudflareConfig = defineCloudflareConfig({ const openNextConfig: OpenNextConfig = { ...cloudflareConfig, - buildCommand: - 'cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare NODE_OPTIONS=--conditions=cloudflare pnpm build --webpack', + buildCommand: 'pnpm build --webpack', cloudflare: { skewProtection: { enabled: true }, }, diff --git a/platforms/cloudflare/package.json b/platforms/cloudflare/package.json index 661266c1423ec..5d1dd4749e81a 100644 --- a/platforms/cloudflare/package.json +++ b/platforms/cloudflare/package.json @@ -12,9 +12,9 @@ "directory": "platforms/cloudflare" }, "scripts": { - "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", + "build:cloudflare": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm --filter=@node-core/website exec opennextjs-cloudflare build --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", + "deploy:cloudflare": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm --filter=@node-core/website exec opennextjs-cloudflare deploy --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", + "dev:cloudflare": "pnpm --filter=@node-core/website exec wrangler dev --config ../../platforms/cloudflare/wrangler.jsonc", "lint:types": "tsc --noEmit" }, "dependencies": { @@ -32,6 +32,7 @@ "@playwright/test": "^1.58.2", "@types/node": "catalog:", "@types/react": "catalog:", + "cross-env": "catalog:", "typescript": "catalog:" }, "engines": { diff --git a/platforms/cloudflare/playwright.platform.config.mjs b/platforms/cloudflare/playwright.config.mjs similarity index 87% rename from platforms/cloudflare/playwright.platform.config.mjs rename to platforms/cloudflare/playwright.config.mjs index 006c5cb3a08d9..45731269f4b94 100644 --- a/platforms/cloudflare/playwright.platform.config.mjs +++ b/platforms/cloudflare/playwright.config.mjs @@ -13,7 +13,7 @@ export default { use: { baseURL: BASE_URL }, webServer: { stdout: 'pipe', - command: 'pnpm --filter=@node-core/platform-cloudflare cloudflare:preview', + command: 'pnpm --filter=@node-core/platform-cloudflare dev:cloudflare', url: BASE_URL, timeout: 60_000 * 3, }, diff --git a/platforms/cloudflare/turbo.json b/platforms/cloudflare/turbo.json deleted file mode 100644 index a04eafed7f22b..0000000000000 --- a/platforms/cloudflare/turbo.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$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", - "*.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/platform/analytics.tsx b/platforms/default/analytics.tsx similarity index 100% rename from apps/site/platform/analytics.tsx rename to platforms/default/analytics.tsx diff --git a/apps/site/platform/instrumentation.ts b/platforms/default/instrumentation.ts similarity index 100% rename from apps/site/platform/instrumentation.ts rename to platforms/default/instrumentation.ts diff --git a/platforms/default/next.config.mjs b/platforms/default/next.config.mjs new file mode 100644 index 0000000000000..c99ed7f4d1cca --- /dev/null +++ b/platforms/default/next.config.mjs @@ -0,0 +1,19 @@ +/** + * Platform config contributed by the default deployment target. + * + * @type {import('../../apps/site/next.platform.config').PlatformConfig} + */ +export default { + aliases: { + '@platform/analytics': '@node-core/platform-default/analytics.tsx', + '@platform/instrumentation': + '@node-core/platform-default/instrumentation.ts', + '@platform/next.config.mjs': '@node-core/platform-default/next.config.mjs', + }, + mdx: { + // Defaults for local dev / static export / generic hosting. Platform + // packages override these via their own `next.config.mjs`. + wasm: true, + twoslash: true, + }, +}; diff --git a/platforms/default/package.json b/platforms/default/package.json new file mode 100644 index 0000000000000..96ad74ee12772 --- /dev/null +++ b/platforms/default/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-core/platform-default", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./*": "./*" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodejs.org", + "directory": "platforms/default" + }, + "scripts": { + "lint:types": "tsc --noEmit" + }, + "peerDependencies": { + "next": "catalog:", + "react": "catalog:" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": "24.x" + } +} diff --git a/apps/site/platform/playwright.platform.config.mjs b/platforms/default/playwright.config.mjs similarity index 82% rename from apps/site/platform/playwright.platform.config.mjs rename to platforms/default/playwright.config.mjs index 1f448eba37b59..82fb1f33c3b9c 100644 --- a/apps/site/platform/playwright.platform.config.mjs +++ b/platforms/default/playwright.config.mjs @@ -5,7 +5,7 @@ * `PLAYWRIGHT_BASE_URL` override for CI), so consumers just spread * `platform.use` into their `defineConfig` call. * - * @type {import('../playwright.platform.config').PlatformPlaywrightConfig} + * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} */ export default { use: { baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000' }, diff --git a/platforms/default/tsconfig.json b/platforms/default/tsconfig.json new file mode 100644 index 0000000000000..aa6f18311fe3f --- /dev/null +++ b/platforms/default/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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": ["**/*.ts", "**/*.tsx", "**/*.mjs"], + "exclude": ["node_modules"] +} diff --git a/platforms/vercel/next.platform.config.mjs b/platforms/vercel/next.config.mjs similarity index 71% rename from platforms/vercel/next.platform.config.mjs rename to platforms/vercel/next.config.mjs index 1deeacb58332a..2459fe60e134e 100644 --- a/platforms/vercel/next.platform.config.mjs +++ b/platforms/vercel/next.config.mjs @@ -1,19 +1,14 @@ /** * Platform config contributed by the Vercel deployment target. * - * 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('../../apps/site/next.platform.config').PlatformConfig} */ export default { aliases: { - '@platform/analytics': '@node-core/platform-vercel/analytics', - '@platform/instrumentation': '@node-core/platform-vercel/instrumentation', + '@platform/analytics': '@node-core/platform-vercel/analytics.tsx', + '@platform/instrumentation': + '@node-core/platform-vercel/instrumentation.ts', + '@platform/next.config.mjs': '@node-core/platform-vercel/next.config.mjs', }, mdx: { // Vercel supports the fast Oniguruma WASM engine and twoslash transforms, diff --git a/platforms/vercel/package.json b/platforms/vercel/package.json index 2bb969abc3741..3d93394bed657 100644 --- a/platforms/vercel/package.json +++ b/platforms/vercel/package.json @@ -12,6 +12,8 @@ "directory": "platforms/vercel" }, "scripts": { + "build:vercel": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website build", + "dev:vercel": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website dev", "lint:types": "tsc --noEmit" }, "dependencies": { @@ -30,6 +32,7 @@ "devDependencies": { "@playwright/test": "^1.58.2", "@types/react": "catalog:", + "cross-env": "catalog:", "typescript": "catalog:" }, "engines": { diff --git a/platforms/vercel/playwright.platform.config.mjs b/platforms/vercel/playwright.config.mjs similarity index 100% rename from platforms/vercel/playwright.platform.config.mjs rename to platforms/vercel/playwright.config.mjs diff --git a/platforms/vercel/vercel.json b/platforms/vercel/vercel.json index 5cf4bef4579bf..33374eaffa1c6 100644 --- a/platforms/vercel/vercel.json +++ b/platforms/vercel/vercel.json @@ -1,13 +1,7 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "installCommand": "pnpm install --prod --frozen-lockfile --filter=@node-core/website... --filter=@node-core/platform-vercel...", - "buildCommand": "pnpm --filter=@node-core/website build", + "buildCommand": "pnpm --filter=@node-core/platform-vercel build:vercel", "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 6664ab88c7927..73f3e2615583b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: '@node-core/platform-cloudflare': specifier: workspace:* version: link:../../platforms/cloudflare + '@node-core/platform-default': + specifier: workspace:* + version: link:../../platforms/default '@node-core/platform-vercel': specifier: workspace:* version: link:../../platforms/vercel @@ -605,6 +608,28 @@ importers: '@types/react': specifier: 'catalog:' version: 19.2.14 + cross-env: + specifier: 'catalog:' + version: 10.1.0 + typescript: + specifier: 'catalog:' + version: 5.9.3 + + platforms/default: + dependencies: + 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 @@ -645,6 +670,9 @@ importers: '@types/react': specifier: 'catalog:' version: 19.2.14 + cross-env: + specifier: 'catalog:' + version: 10.1.0 typescript: specifier: 'catalog:' version: 5.9.3 diff --git a/turbo.json b/turbo.json index e4f62db3943b8..20ef8f4db5e9b 100644 --- a/turbo.json +++ b/turbo.json @@ -17,6 +17,9 @@ "lint": { "dependsOn": ["^topo"] }, + "lint:types": { + "cache": false + }, "prettier": { "inputs": ["**/*.{js,mjs,ts,tsx,md,mdx,json,yml,css}"], "outputs": [".prettiercache"] From 3e24abffbbe582bc53ed5eafa8aa15b5f4309d90 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 00:33:21 -0300 Subject: [PATCH 20/24] refactor(platform): collapse @platform aliases into a single wildcard Replace the per-file `@platform/analytics`, `@platform/instrumentation`, and `@platform/next.config.mjs` aliases (duplicated across each platform package) with a single `'@platform' -> PLATFORM_ALIAS` mapping wired in the root `apps/site/next.config.mjs` for both Turbopack and webpack. Drop the `aliases` field from `PlatformConfig` and from each platform's `next.config.mjs`. Add `playwright` scripts to the cloudflare and vercel platform packages (env baked in via cross-env), simplify the cloudflare playwright workflow to invoke them, rename `playwright.config.ts` -> `playwright.config.mjs`, and move `cross-env` into `dependencies` on `@node-core/platform-vercel` so Vercel's `pnpm install --prod` keeps it. Co-Authored-By: Claude Opus 4.7 --- .../playwright-cloudflare-open-next.yml | 4 +-- apps/site/next.config.mjs | 30 ++++++++++++------- apps/site/next.platform.config.d.ts | 6 ---- apps/site/next.platform.constants.mjs | 7 +++++ ...wright.config.ts => playwright.config.mjs} | 14 +++++---- apps/site/playwright.platform.config.d.ts | 2 +- platforms/cloudflare/next.config.mjs | 7 ----- platforms/cloudflare/package.json | 3 +- platforms/default/next.config.mjs | 6 ---- platforms/vercel/next.config.mjs | 6 ---- platforms/vercel/package.json | 7 +++-- pnpm-lock.yaml | 6 ++-- 12 files changed, 46 insertions(+), 52 deletions(-) rename apps/site/{playwright.config.ts => playwright.config.mjs} (65%) diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 670697d58f190..86059722a54ef 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -55,10 +55,8 @@ jobs: run: node --run build:cloudflare - name: Run Playwright tests - working-directory: apps/site + working-directory: platforms/cloudflare run: node --run playwright - env: - NEXT_PUBLIC_DEPLOY_TARGET: cloudflare - name: Upload Playwright test results if: always() diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index c2c66cce25cb5..c0d2474f9463e 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -4,20 +4,28 @@ import createNextIntlPlugin from 'next-intl/plugin'; import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; import { getImagesConfig } from './next.image.config.mjs'; -import { DEPLOY_TARGET } from './next.platform.constants.mjs'; +import { DEPLOY_TARGET, PLATFORM_ALIAS } from './next.platform.constants.mjs'; import { redirects, rewrites } from './next.rewrites.mjs'; -// Loaded by Node directly (Next.js doesn't bundle `next.config.mjs`), so -// we resolve the active platform via a dynamic import keyed on -// `DEPLOY_TARGET` rather than a `@platform/*` alias (those only resolve -// inside Turbopack/webpack). -const { default: platform } = await import( - `@node-core/platform-${DEPLOY_TARGET}/next.config.mjs` -); +/** + * Loaded by Node directly (Next.js doesn't bundle `next.config.mjs`), so + * we resolve the active platform via a dynamic import keyed on + * `DEPLOY_TARGET` rather than a `@platform/*` alias (those only resolve + * inside Turbopack/webpack). + * + * @type {{ default: import('./next.platform.config.d.ts').PlatformConfig }} + */ +const { default: platform } = await import(`${PLATFORM_ALIAS}/next.config.mjs`); const platformImages = await platform.images?.(); const platformNextConfig = await platform.nextConfig?.(); +// Single wildcard alias: `@platform/` resolves to +// `@node-core/platform-${DEPLOY_TARGET}/` so each deploy target's +// files (analytics slot, instrumentation, MDX/Shiki config) are picked +// up automatically without per-file mappings. +const platformAliases = { '@platform': PLATFORM_ALIAS }; + /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -29,7 +37,7 @@ const nextConfig = { basePath: BASE_PATH, images: getImagesConfig(platformImages), serverExternalPackages: ['twoslash'], - transpilePackages: [`@node-core/platform-${DEPLOY_TARGET}`], + transpilePackages: [PLATFORM_ALIAS], outputFileTracingIncludes: { // Twoslash needs TypeScript declarations to function, and, by default, Next.js // strips them for brevity. Therefore, they must be explicitly included. @@ -82,13 +90,13 @@ const nextConfig = { turbopackFileSystemCacheForDev: true, }, // Provide Turbopack Aliases for Platform Resolution - turbopack: { resolveAlias: platform.aliases }, + turbopack: { resolveAlias: platformAliases }, // Provide Webpack Aliases for Platform Resolution. webpack: ({ resolve, ...config }) => ({ ...config, resolve: { ...resolve, - alias: { ...resolve.alias, ...platform.aliases }, + alias: { ...resolve.alias, ...platformAliases }, conditionNames: resolve.conditionNames .concat(DEPLOY_TARGET) .filter(Boolean), diff --git a/apps/site/next.platform.config.d.ts b/apps/site/next.platform.config.d.ts index 146ec140c10c3..88b6e3c4f8408 100644 --- a/apps/site/next.platform.config.d.ts +++ b/apps/site/next.platform.config.d.ts @@ -8,11 +8,6 @@ type PlatformNextConfig = Pick; * Shared platform-config contract consumed by `apps/site/next.config.mjs` * and implemented by each `@node-core/platform-` package. * - * `aliases` are surfaced as Turbopack/webpack aliases so that - * `@platform/*` imports in bundled code (e.g. the `@analytics/` - * parallel-route slot, `instrumentation.ts`, `mdx/plugins.mjs`) resolve - * to the active platform's files. - * * `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 @@ -21,7 +16,6 @@ type PlatformNextConfig = Pick; * worker runtime free of build-only code. */ export type PlatformConfig = { - aliases?: Record; images?: () => Promise; mdx?: PlatformMdxConfig; nextConfig?: () => Promise; diff --git a/apps/site/next.platform.constants.mjs b/apps/site/next.platform.constants.mjs index ffd85865d7d78..b769808771de3 100644 --- a/apps/site/next.platform.constants.mjs +++ b/apps/site/next.platform.constants.mjs @@ -13,3 +13,10 @@ * @type {'vercel' | 'cloudflare' | 'default'} */ export const DEPLOY_TARGET = process.env.NEXT_PUBLIC_DEPLOY_TARGET ?? 'default'; + +/** + * The alias for the platform. + * + * @type {string} + */ +export const PLATFORM_ALIAS = `@node-core/platform-${DEPLOY_TARGET}`; diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.mjs similarity index 65% rename from apps/site/playwright.config.ts rename to apps/site/playwright.config.mjs index 9f715e01b8aa8..31a38174ac0b4 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.mjs @@ -1,12 +1,16 @@ import { defineConfig, devices } from '@playwright/test'; -import { DEPLOY_TARGET } from './next.platform.constants.mjs'; +import { PLATFORM_ALIAS } from './next.platform.constants.mjs'; -// Playwright loads this config via Node, so resolve the active platform -// via a dynamic import keyed on `DEPLOY_TARGET` rather than a -// `@platform/*` alias (those only resolve inside Turbopack/webpack). +/** + * Playwright loads this config via Node, so resolve the active platform + * via a dynamic import keyed on `DEPLOY_TARGET` rather than a + * `@platform/*` alias (those only resolve inside Turbopack/webpack). + * + * @type {{ default: import('./playwright.platform.config.d.ts').PlatformPlaywrightConfig }} + */ const { default: platform } = await import( - `@node-core/platform-${DEPLOY_TARGET}/playwright.config.mjs` + `${PLATFORM_ALIAS}/playwright.config.mjs` ); const isCI = !!process.env.CI; diff --git a/apps/site/playwright.platform.config.d.ts b/apps/site/playwright.platform.config.d.ts index 3a9f2f3f27f05..1e551f564b88b 100644 --- a/apps/site/playwright.platform.config.d.ts +++ b/apps/site/playwright.platform.config.d.ts @@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; /** * Shared Playwright platform-config contract consumed by - * `apps/site/playwright.config.ts` and implemented by each + * `apps/site/playwright.config.mjs` and implemented by each * `@node-core/platform-` package. */ export type PlatformPlaywrightConfig = Pick< diff --git a/platforms/cloudflare/next.config.mjs b/platforms/cloudflare/next.config.mjs index 1abf5fadd0a0e..83815b5ec60bf 100644 --- a/platforms/cloudflare/next.config.mjs +++ b/platforms/cloudflare/next.config.mjs @@ -4,13 +4,6 @@ * @type {import('../../apps/site/next.platform.config').PlatformConfig} */ export default { - aliases: { - '@platform/analytics': '@node-core/platform-cloudflare/analytics.tsx', - '@platform/instrumentation': - '@node-core/platform-cloudflare/instrumentation.ts', - '@platform/next.config.mjs': - '@node-core/platform-cloudflare/next.config.mjs', - }, mdx: { // Cloudflare workers can't load `shiki/wasm` via `WebAssembly.instantiate` // with custom imports (blocked for security), so fall back to the diff --git a/platforms/cloudflare/package.json b/platforms/cloudflare/package.json index 5d1dd4749e81a..b79a003e5beb7 100644 --- a/platforms/cloudflare/package.json +++ b/platforms/cloudflare/package.json @@ -15,7 +15,8 @@ "build:cloudflare": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm --filter=@node-core/website exec opennextjs-cloudflare build --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", "deploy:cloudflare": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm --filter=@node-core/website exec opennextjs-cloudflare deploy --openNextConfigPath ../../platforms/cloudflare/open-next.config.ts --config ../../platforms/cloudflare/wrangler.jsonc", "dev:cloudflare": "pnpm --filter=@node-core/website exec wrangler dev --config ../../platforms/cloudflare/wrangler.jsonc", - "lint:types": "tsc --noEmit" + "lint:types": "tsc --noEmit", + "playwright": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=cloudflare pnpm --filter=@node-core/website playwright" }, "dependencies": { "@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.1", diff --git a/platforms/default/next.config.mjs b/platforms/default/next.config.mjs index c99ed7f4d1cca..6336c2f5d5ab2 100644 --- a/platforms/default/next.config.mjs +++ b/platforms/default/next.config.mjs @@ -4,12 +4,6 @@ * @type {import('../../apps/site/next.platform.config').PlatformConfig} */ export default { - aliases: { - '@platform/analytics': '@node-core/platform-default/analytics.tsx', - '@platform/instrumentation': - '@node-core/platform-default/instrumentation.ts', - '@platform/next.config.mjs': '@node-core/platform-default/next.config.mjs', - }, mdx: { // Defaults for local dev / static export / generic hosting. Platform // packages override these via their own `next.config.mjs`. diff --git a/platforms/vercel/next.config.mjs b/platforms/vercel/next.config.mjs index 2459fe60e134e..604186b16e8c5 100644 --- a/platforms/vercel/next.config.mjs +++ b/platforms/vercel/next.config.mjs @@ -4,12 +4,6 @@ * @type {import('../../apps/site/next.platform.config').PlatformConfig} */ export default { - aliases: { - '@platform/analytics': '@node-core/platform-vercel/analytics.tsx', - '@platform/instrumentation': - '@node-core/platform-vercel/instrumentation.ts', - '@platform/next.config.mjs': '@node-core/platform-vercel/next.config.mjs', - }, mdx: { // Vercel supports the fast Oniguruma WASM engine and twoslash transforms, // so keep parity with the default standalone config. diff --git a/platforms/vercel/package.json b/platforms/vercel/package.json index 3d93394bed657..189fbc8e71e93 100644 --- a/platforms/vercel/package.json +++ b/platforms/vercel/package.json @@ -14,7 +14,8 @@ "scripts": { "build:vercel": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website build", "dev:vercel": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website dev", - "lint:types": "tsc --noEmit" + "lint:types": "tsc --noEmit", + "playwright": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website playwright" }, "dependencies": { "@opentelemetry/api-logs": "~0.213.0", @@ -23,7 +24,8 @@ "@opentelemetry/sdk-logs": "~0.213.0", "@vercel/analytics": "~2.0.1", "@vercel/otel": "~2.1.1", - "@vercel/speed-insights": "~2.0.0" + "@vercel/speed-insights": "~2.0.0", + "cross-env": "catalog:" }, "peerDependencies": { "next": "catalog:", @@ -32,7 +34,6 @@ "devDependencies": { "@playwright/test": "^1.58.2", "@types/react": "catalog:", - "cross-env": "catalog:", "typescript": "catalog:" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73f3e2615583b..6d536a82adbc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -657,6 +657,9 @@ importers: '@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) + cross-env: + specifier: 'catalog:' + version: 10.1.0 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) @@ -670,9 +673,6 @@ importers: '@types/react': specifier: 'catalog:' version: 19.2.14 - cross-env: - specifier: 'catalog:' - version: 10.1.0 typescript: specifier: 'catalog:' version: 5.9.3 From 57b7c3855f1e93d4c2fe0fae7efd35aed3bd3c91 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 00:39:43 -0300 Subject: [PATCH 21/24] chore: tiny next.config.mjs simplification --- platforms/vercel/next.config.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/platforms/vercel/next.config.mjs b/platforms/vercel/next.config.mjs index 604186b16e8c5..ef452793ecd51 100644 --- a/platforms/vercel/next.config.mjs +++ b/platforms/vercel/next.config.mjs @@ -14,7 +14,6 @@ export default { const VERCEL_URL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined; - const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL; return { // Expose Vercel's auto-assigned deployment URL as a platform-agnostic @@ -23,7 +22,9 @@ export default { // contribute the entry when a value is actually present so downstream // truthiness vs. existence checks behave the same as if the var were // never set. - env: NEXT_PUBLIC_BASE_URL ? { NEXT_PUBLIC_BASE_URL } : undefined, + env: { + NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL || VERCEL_URL, + }, }; }, }; From c261b2c9838c174443d1e963dab8dd772e36efae Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 01:37:29 -0300 Subject: [PATCH 22/24] fix(platform): stop tsconfig paths from shadowing the @platform alias The `@platform/*` entry in `apps/site/tsconfig.json` was being picked up by `tsconfig-paths-webpack-plugin` before webpack's `resolve.alias`, which caused the Cloudflare worker bundle to include the default platform's MDX config (`wasm: true`) instead of cloudflare's (`wasm: false`). At runtime the worker hit `WebAssembly.instantiate(): Wasm code generation disallowed by embedder`. Drop the tsconfig path, declare a wildcard ambient module `@platform/*` for `lint:types`, and let webpack's alias be the sole source of truth for `@platform/*` resolution. The alias can now be the plain package specifier, so the `createRequire` + `dirname` workaround goes away too. Co-Authored-By: Claude Opus 4.7 --- apps/site/next.config.mjs | 4 ---- apps/site/next.platform.modules.d.ts | 9 +++++++++ apps/site/tsconfig.json | 10 +--------- 3 files changed, 10 insertions(+), 13 deletions(-) create mode 100644 apps/site/next.platform.modules.d.ts diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index c0d2474f9463e..47819788e2592 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -20,10 +20,6 @@ const { default: platform } = await import(`${PLATFORM_ALIAS}/next.config.mjs`); const platformImages = await platform.images?.(); const platformNextConfig = await platform.nextConfig?.(); -// Single wildcard alias: `@platform/` resolves to -// `@node-core/platform-${DEPLOY_TARGET}/` so each deploy target's -// files (analytics slot, instrumentation, MDX/Shiki config) are picked -// up automatically without per-file mappings. const platformAliases = { '@platform': PLATFORM_ALIAS }; /** @type {import('next').NextConfig} */ diff --git a/apps/site/next.platform.modules.d.ts b/apps/site/next.platform.modules.d.ts new file mode 100644 index 0000000000000..d00e62fbd52db --- /dev/null +++ b/apps/site/next.platform.modules.d.ts @@ -0,0 +1,9 @@ +/** + * Wildcard ambient module declaration for the `@platform/*` webpack alias. + * + * The alias is resolved at build time by `apps/site/next.config.mjs` to the + * active `@node-core/platform-` package. We declare it here (rather + * than via `tsconfig.json`'s `paths`) so that `tsconfig-paths-webpack-plugin` + * can't shadow the webpack alias and bundle the wrong platform's files. + */ +declare module '@platform/*'; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 71e5b18bc3e49..2ed19e23babd5 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -17,15 +17,7 @@ "jsx": "react-jsx", "incremental": true, "plugins": [{ "name": "next" }], - "baseUrl": ".", - "paths": { - "@platform/*": [ - "../../platforms/default/*", - "../../platforms/default/*.ts", - "../../platforms/default/*.tsx", - "../../platforms/default/*.mjs" - ] - } + "baseUrl": "." }, "mdx": { "checkMdx": true From aa7cfdd23541892f5be3acd898c0e3c44d105ff3 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 12:04:35 -0300 Subject: [PATCH 23/24] fix(platform): give Turbopack a wildcard `@platform/*` alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turbopack's `resolveAlias` doesn't do webpack-style prefix matching, so `{ '@platform': PLATFORM_ALIAS }` left every static `@platform/*` import unresolved on the Vercel build — `Module not found: Can't resolve '@platform/analytics'` etc. Webpack rejects the `/*` wildcard form, so the two bundlers need different shapes for the same mapping. Split the alias map: `'@platform/*': '/*'` for Turbopack, `'@platform': ''` for webpack. Co-Authored-By: Claude Opus 4.7 --- apps/site/next.config.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 47819788e2592..2b74b76ed0252 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -20,7 +20,11 @@ const { default: platform } = await import(`${PLATFORM_ALIAS}/next.config.mjs`); const platformImages = await platform.images?.(); const platformNextConfig = await platform.nextConfig?.(); -const platformAliases = { '@platform': PLATFORM_ALIAS }; +// Turbopack's `resolveAlias` requires explicit `*` wildcards; webpack's +// `resolve.alias` matches the bare prefix and the `/*` form is invalid, +// so the two bundlers need different shapes for the same mapping. +const turbopackPlatformAliases = { '@platform/*': `${PLATFORM_ALIAS}/*` }; +const webpackPlatformAliases = { '@platform': PLATFORM_ALIAS }; /** @type {import('next').NextConfig} */ const nextConfig = { @@ -86,13 +90,13 @@ const nextConfig = { turbopackFileSystemCacheForDev: true, }, // Provide Turbopack Aliases for Platform Resolution - turbopack: { resolveAlias: platformAliases }, + turbopack: { resolveAlias: turbopackPlatformAliases }, // Provide Webpack Aliases for Platform Resolution. webpack: ({ resolve, ...config }) => ({ ...config, resolve: { ...resolve, - alias: { ...resolve.alias, ...platformAliases }, + alias: { ...resolve.alias, ...webpackPlatformAliases }, conditionNames: resolve.conditionNames .concat(DEPLOY_TARGET) .filter(Boolean), From 24ef7e136e561685820ec6cfaeadb6c96f004079 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 26 Apr 2026 12:34:08 -0300 Subject: [PATCH 24/24] chore(deps): align orama and cloudflare deps after rebase - Add @orama/orama as a runtime dep (used by withSearch.tsx). - Move @orama/core to devDeps (only consumed via `import type`). - Drop unused @orama/ui from apps/site. - Drop @cloudflare/workers-types and @sentry/cloudflare from apps/site; they live in platforms/cloudflare which owns the worker entrypoint. --- apps/site/package.json | 6 ++---- pnpm-lock.yaml | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/site/package.json b/apps/site/package.json index fed39e2e15cd5..36410e39c4669 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -34,8 +34,7 @@ "@node-core/ui-components": "workspace:*", "@node-core/website-i18n": "workspace:*", "@nodevu/core": "0.3.0", - "@orama/core": "^1.2.19", - "@orama/ui": "^1.5.4", + "@orama/orama": "^3.1.18", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "~4.2.2", @@ -69,12 +68,11 @@ "vfile-matter": "~5.0.1" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20260418.1", "@eslint-react/eslint-plugin": "~3.0.0", "@next/eslint-plugin-next": "16.2.1", "@node-core/remark-lint": "workspace:*", + "@orama/core": "^1.2.19", "@playwright/test": "^1.58.2", - "@sentry/cloudflare": "^10.49.0", "@testing-library/user-event": "~14.6.1", "@types/mdast": "^4.0.4", "@types/mdx": "^2.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d536a82adbc0..9176a052134f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,12 +111,9 @@ importers: '@nodevu/core': specifier: 0.3.0 version: 0.3.0 - '@orama/core': - specifier: ^1.2.19 - version: 1.2.19 - '@orama/ui': - specifier: ^1.5.4 - version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.14)(react@19.2.4) + '@orama/orama': + specifier: ^3.1.18 + version: 3.1.18 '@radix-ui/react-tabs': specifier: ^1.1.13 version: 1.1.13(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -220,6 +217,9 @@ importers: '@node-core/remark-lint': specifier: workspace:* version: link:../../packages/remark-lint + '@orama/core': + specifier: ^1.2.19 + version: 1.2.19 '@playwright/test': specifier: ^1.58.2 version: 1.58.2 @@ -13644,7 +13644,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13670,10 +13670,11 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) @@ -13700,7 +13701,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13711,7 +13712,7 @@ snapshots: doctrine: 2.1.0 eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13722,6 +13723,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack