diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e5899448d137..4a0ebcfecd457 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,8 +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/site/wrangler.jsonc @nodejs/web-infra -apps/site/open-next.config.ts @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 8a42b4675fcd8..76c76ccf7c48a 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -50,12 +50,19 @@ jobs: working-directory: apps/site run: node_modules/.bin/playwright install --with-deps + - 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 + - name: Run Playwright tests working-directory: apps/site run: node --run playwright env: - PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true - PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 + 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 192dc9d24b86a..42deb38324d5c 100644 --- a/.github/workflows/tmp-cloudflare-open-next-deploy.yml +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -50,20 +50,25 @@ 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: 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/site + 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 f461599c85893..6c783ceb3ee1f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ dist/ # Cloudflare Build Output apps/site/.open-next apps/site/.wrangler +platforms/cloudflare/.wrangler ## 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 5e1e440c5b974..acc8fcef6d154 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -1,15 +1,12 @@ 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 { ThemeProvider } from '#site/providers/themeProvider'; -import type { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; import '#site/styles/index.css'; @@ -17,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 } = @@ -46,12 +48,7 @@ const RootLayout: FC = async ({ children, params }) => { href="https://social.lfx.dev/@nodejs" /> - {VERCEL_ENV && ( - <> - - - - )} + {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 86097a48800b1..84f7384bb1493 100644 --- a/apps/site/instrumentation.ts +++ b/apps/site/instrumentation.ts @@ -1,7 +1 @@ -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 - 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 02166643aaf26..75c92d3ed9c59 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -7,27 +7,16 @@ import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; import readingTime from 'remark-reading-time'; -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'; -// 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; +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: !OPEN_NEXT_CLOUDFLARE, - - // TODO(@avivkeller): Find a way to enable Twoslash w/ a VFS on Cloudflare - twoslash: !OPEN_NEXT_CLOUDFLARE, -}); +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 40d1f89e87cd3..7349a457e06e4 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -1,23 +1,17 @@ '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 platform from '#platform/next.platform.config'; + +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'; -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(); - } - - return undefined; -}; - /** @type {import('next').NextConfig} */ const nextConfig = { // Full Support of React 18 SSR and Streaming @@ -27,9 +21,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(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`. + 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. @@ -81,8 +80,25 @@ 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. 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 }, + conditionNames: resolve.conditionNames + .concat(DEPLOY_TARGET) + .filter(Boolean), + }, + }), + ...(await platform.nextConfig?.()), }; const withNextIntl = createNextIntlPlugin('./i18n.tsx'); + export default withNextIntl(nextConfig); diff --git a/apps/site/next.constants.cloudflare.mjs b/apps/site/next.constants.cloudflare.mjs deleted file mode 100644 index a87b3ed2a5214..0000000000000 --- a/apps/site/next.constants.cloudflare.mjs +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -/** - * Whether the build process is targeting the Cloudflare open-next build or not. - * - * TODO: The `OPEN_NEXT_CLOUDFLARE` environment variable is being - * defined in the worker building script, ideally the open-next - * adapter should set it itself when it invokes the Next.js build - * process, once it does that remove the manual `OPEN_NEXT_CLOUDFLARE` - * definition in the package.json script. - */ -export const OPEN_NEXT_CLOUDFLARE = process.env.OPEN_NEXT_CLOUDFLARE; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index f621472d3c309..3e8f03aa02881 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 @@ -38,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. - * - * 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. + * The full canonical URL of the deployed Website (used e.g. for the RSS feed). * - * @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 519523ca2aafe..ddd9a19302ff5 100644 --- a/apps/site/next.image.config.mjs +++ b/apps/site/next.image.config.mjs @@ -1,4 +1,3 @@ -import { OPEN_NEXT_CLOUDFLARE } from './next.constants.cloudflare.mjs'; import { ENABLE_STATIC_EXPORT } from './next.constants.mjs'; const remotePatterns = [ @@ -9,18 +8,16 @@ const remotePatterns = [ 'https://website-assets.oramasearch.com/**', ]; -export const getImagesConfig = () => { - if (OPEN_NEXT_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.d.ts b/apps/site/next.platform.config.d.ts new file mode 100644 index 0000000000000..7094cef1660cd --- /dev/null +++ b/apps/site/next.platform.config.d.ts @@ -0,0 +1,27 @@ +import type { HighlighterOptions } from '@node-core/rehype-shiki'; +import type { NextConfig } from 'next'; + +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. + * + * `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?: () => Promise; + mdx?: PlatformMdxConfig; + nextConfig?: () => Promise; +}; + +declare const config: PlatformConfig; + +export default config; diff --git a/apps/site/next.platform.config.mjs b/apps/site/next.platform.config.mjs new file mode 100644 index 0000000000000..1ad7c451d1fb0 --- /dev/null +++ b/apps/site/next.platform.config.mjs @@ -0,0 +1,26 @@ +/** + * 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/package.json b/apps/site/package.json index 1fd73ab23c9be..cb79963acbd6e 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": "OPEN_NEXT_CLOUDFLARE=true 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", @@ -38,10 +35,6 @@ "@node-core/ui-components": "workspace:*", "@node-core/website-i18n": "workspace:*", "@nodevu/core": "0.3.0", - "@opentelemetry/api-logs": "~0.213.0", - "@opentelemetry/instrumentation": "~0.213.0", - "@opentelemetry/resources": "~1.30.1", - "@opentelemetry/sdk-logs": "~0.213.0", "@orama/core": "^1.2.19", "@orama/ui": "^1.5.4", "@radix-ui/react-tabs": "^1.1.13", @@ -50,16 +43,13 @@ "@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", "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", @@ -81,10 +71,8 @@ }, "devDependencies": { "@eslint-react/eslint-plugin": "~3.0.0", - "@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.1", "@next/eslint-plugin-next": "16.2.1", "@node-core/remark-lint": "workspace:*", - "@opennextjs/cloudflare": "^1.19.3", "@playwright/test": "^1.58.2", "@testing-library/user-event": "~14.6.1", "@types/mdast": "^4.0.4", @@ -106,10 +94,19 @@ "tsx": "^4.21.0", "typescript": "catalog:", "typescript-eslint": "~8.57.2", - "user-agent-data-types": "0.4.2", - "wrangler": "^4.77.0", - "@cloudflare/workers-types": "^4.20260418.1", - "@sentry/cloudflare": "^10.49.0" + "user-agent-data-types": "0.4.2" + }, + "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/*": [ @@ -120,7 +117,17 @@ "./*/index.ts", "./*.mjs", "./*/index.mjs" - ] + ], + "#platform/next.platform.config": { + "cloudflare": "@node-core/platform-cloudflare/next.platform.config", + "vercel": "@node-core/platform-vercel/next.platform.config", + "default": "./next.platform.config.mjs" + }, + "#platform/playwright.platform.config": { + "cloudflare": "@node-core/platform-cloudflare/playwright.platform.config", + "vercel": "@node-core/platform-vercel/playwright.platform.config", + "default": "./playwright.platform.config.mjs" + } }, "engines": { "node": "24.x" diff --git a/apps/site/platform/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/instrumentation.ts b/apps/site/platform/instrumentation.ts new file mode 100644 index 0000000000000..605be34cb4a3d --- /dev/null +++ b/apps/site/platform/instrumentation.ts @@ -0,0 +1 @@ +export const register = () => {}; diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.ts index 523f40f644582..67ed01bde6856 100644 --- a/apps/site/playwright.config.ts +++ b/apps/site/playwright.config.ts @@ -1,6 +1,6 @@ -import { defineConfig, devices, type Config } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; -import json from './package.json' with { type: 'json' }; +import platform from '#platform/playwright.platform.config'; const isCI = !!process.env.CI; @@ -12,9 +12,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 +35,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.d.ts b/apps/site/playwright.platform.config.d.ts new file mode 100644 index 0000000000000..ada0174396183 --- /dev/null +++ b/apps/site/playwright.platform.config.d.ts @@ -0,0 +1,15 @@ +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']; +}; + +declare const config: PlatformPlaywrightConfig; + +export default config; diff --git a/apps/site/playwright.platform.config.mjs b/apps/site/playwright.platform.config.mjs new file mode 100644 index 0000000000000..2ce9df7ea44f5 --- /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 {import('./playwright.platform.config').PlatformPlaywrightConfig} + */ +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 e08e9169e7a11..22e26b7dfaf98 100644 --- a/apps/site/turbo.json +++ b/apps/site/turbo.json @@ -7,8 +7,8 @@ "cache": false, "persistent": true, "env": [ - "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -34,8 +34,8 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -55,8 +55,8 @@ "cache": false, "persistent": true, "env": [ - "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -81,8 +81,8 @@ ], "outputs": [".next/**", "!.next/cache/**"], "env": [ - "VERCEL_ENV", "VERCEL_URL", + "NEXT_PUBLIC_DEPLOY_TARGET", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -137,7 +137,6 @@ "inputs": ["{pages}/**/*.{mdx,md}"], "outputs": ["public/blog-data.json"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "TURBO_CACHE", "TURBO_TELEMETRY_DISABLED", @@ -145,39 +144,6 @@ "ENABLE_EXPERIMENTAL_COREPACK" ] }, - "cloudflare:build:worker": { - "dependsOn": ["build:blog-data"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, - "cloudflare:preview": { - "dependsOn": ["cloudflare:build:worker"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, - "cloudflare:deploy": { - "dependsOn": ["cloudflare:build:worker"], - "inputs": [ - "{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}", - "{app,components,layouts,pages,styles}/**/*.css", - "{next-data,scripts,i18n}/**/*.{mjs,json}", - "{app,pages}/**/*.{mdx,md}", - "*.{md,mdx,json,ts,tsx,mjs,yml}" - ], - "outputs": [".open-next/**"] - }, "scripts:release-post": { "cache": false } diff --git a/apps/site/vercel.json b/apps/site/vercel.json deleted file mode 100644 index 9923ea6cf94e6..0000000000000 --- a/apps/site/vercel.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "installCommand": "pnpm install --prod --frozen-lockfile", - "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..dad2a5bbc813f 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`](../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 -There are two key configuration files related to Cloudflare deployments: +All Cloudflare-specific configuration lives in the [`@node-core/platform-cloudflare`](../platforms/cloudflare) package. The two key configuration files are: + +- [`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 @@ -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 ([`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. @@ -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 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. 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 [`site/cloudflare/image-loader.ts`](../apps/site/cloudflare/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 [`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 [`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 9c6113ab7dd8c..a820f8c499047 100644 --- a/docs/technologies.md +++ b/docs/technologies.md @@ -37,7 +37,9 @@ This document provides an overview of the technologies used in the Node.js websi - [VSCode Configuration](#vscode-configuration) - [Build and Deployment](#build-and-deployment) - [Multiple Build Targets](#multiple-build-targets) + - [Deploy Target Selection (`NEXT_PUBLIC_DEPLOY_TARGET`)](#deploy-target-selection-next_public_deploy_target) - [Vercel Integration](#vercel-integration) + - [Cloudflare Integration](#cloudflare-integration) - [Package Management](#package-management) - [Multi-package Workspace](#multi-package-workspace) - [Publishing Process](#publishing-process) @@ -120,22 +122,27 @@ We chose Next.js because it is: ``` nodejs.org/ ├── apps/ -│ └── site/ # Main website application -│ ├── components/ # Website-specific React components -│ ├── layouts/ # Page layout templates -│ ├── pages/ # Content pages (Markdown/MDX) -│ │ ├── en/ # English content (source) -│ │ └── {locale}/ # Translated content -│ ├── public/ # Static assets -│ │ └── static/ # Images, documents, etc. -│ ├── hooks/ # React hooks -│ ├── providers/ # React context providers -│ ├── types/ # TypeScript definitions -│ ├── next-data/ # Build-time data fetching -│ ├── scripts/ # Utility scripts -│ ├── snippets/ # Code snippets for download page -│ └── tests/ # Test files -│ └── e2e/ # End-to-end tests +│ ├── site/ # Main website application +│ │ ├── components/ # Website-specific React components +│ │ ├── layouts/ # Page layout templates +│ │ ├── pages/ # Content pages (Markdown/MDX) +│ │ │ ├── en/ # English content (source) +│ │ │ └── {locale}/ # Translated content +│ │ ├── public/ # Static assets +│ │ │ └── static/ # Images, documents, etc. +│ │ ├── hooks/ # React hooks +│ │ ├── providers/ # React context providers +│ │ ├── types/ # TypeScript definitions +│ │ ├── next-data/ # Build-time data fetching +│ │ ├── scripts/ # Utility scripts +│ │ ├── snippets/ # Code snippets for download page +│ │ └── tests/ # Test files +│ │ └── e2e/ # End-to-end tests +│ ├── vercel/ # Vercel deployment adapter +│ │ # (analytics, instrumentation, vercel.json) +│ └── cloudflare/ # Cloudflare deployment adapter +│ # (worker entrypoint, image loader, +│ # open-next.config.ts, wrangler.jsonc) └── packages/ ├── ui-components/ # Reusable UI components │ ├── styles/ # Global stylesheets @@ -143,8 +150,7 @@ nodejs.org/ ├── i18n/ # Internationalization │ ├── locales/ # Translation files │ └── config.json # Locale configuration - ├── rehype-shiki/ # Syntax highlighting plugin - ... + └── rehype-shiki/ # Syntax highlighting plugin ``` ## Architecture Decisions @@ -289,14 +295,34 @@ 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`](../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 [`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 [`platforms/cloudflare`](../platforms/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/platforms/cloudflare/next.platform.config.mjs b/platforms/cloudflare/next.platform.config.mjs new file mode 100644 index 0000000000000..5d0871273100f --- /dev/null +++ b/platforms/cloudflare/next.platform.config.mjs @@ -0,0 +1,56 @@ +/** + * 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/instrumentation': + '@node-core/platform-cloudflare/instrumentation', + }, + 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, + }, + 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: 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/open-next.config.ts b/platforms/cloudflare/open-next.config.ts similarity index 88% rename from apps/site/open-next.config.ts rename to platforms/cloudflare/open-next.config.ts index 03f1c01730dc2..5ef3a63965af1 100644 --- a/apps/site/open-next.config.ts +++ b/platforms/cloudflare/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 NODE_OPTIONS=--conditions=cloudflare pnpm build --webpack', cloudflare: { skewProtection: { enabled: true }, }, diff --git a/platforms/cloudflare/package.json b/platforms/cloudflare/package.json new file mode 100644 index 0000000000000..0e36a4360e393 --- /dev/null +++ b/platforms/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": "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", + "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": "catalog:", + "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/platforms/cloudflare/playwright.platform.config.mjs b/platforms/cloudflare/playwright.platform.config.mjs new file mode 100644 index 0000000000000..b405cfb803b95 --- /dev/null +++ b/platforms/cloudflare/playwright.platform.config.mjs @@ -0,0 +1,18 @@ +/** + * 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('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} + */ +export default { + baseURL: 'http://127.0.0.1:8787', + webServer: { + stdout: 'pipe', + command: 'pnpm --filter=@node-core/platform-cloudflare cloudflare:preview', + url: 'http://127.0.0.1:8787', + timeout: 60_000 * 3, + }, +}; diff --git a/platforms/cloudflare/src/analytics.tsx b/platforms/cloudflare/src/analytics.tsx new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/platforms/cloudflare/src/analytics.tsx @@ -0,0 +1 @@ +export default () => null; diff --git a/apps/site/cloudflare/image-loader.ts b/platforms/cloudflare/src/image-loader.ts similarity index 87% rename from apps/site/cloudflare/image-loader.ts rename to platforms/cloudflare/src/image-loader.ts index 2137028e23db4..d883ff004ed71 100644 --- a/apps/site/cloudflare/image-loader.ts +++ b/platforms/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/platforms/cloudflare/src/instrumentation.ts b/platforms/cloudflare/src/instrumentation.ts new file mode 100644 index 0000000000000..605be34cb4a3d --- /dev/null +++ b/platforms/cloudflare/src/instrumentation.ts @@ -0,0 +1 @@ +export const register = () => {}; diff --git a/apps/site/cloudflare/worker-entrypoint.ts b/platforms/cloudflare/src/worker-entrypoint.ts similarity index 90% rename from apps/site/cloudflare/worker-entrypoint.ts rename to platforms/cloudflare/src/worker-entrypoint.ts index bd40543b4b9dd..f63ab60a6f586 100644 --- a/apps/site/cloudflare/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 '../.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/platforms/cloudflare/tsconfig.json b/platforms/cloudflare/tsconfig.json new file mode 100644 index 0000000000000..95f515d1bbb7d --- /dev/null +++ b/platforms/cloudflare/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" + ], + "exclude": ["src/worker-entrypoint.ts"] +} diff --git a/platforms/cloudflare/turbo.json b/platforms/cloudflare/turbo.json new file mode 100644 index 0000000000000..8c1ffdc95a974 --- /dev/null +++ b/platforms/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/platforms/cloudflare/wrangler.jsonc similarity index 76% rename from apps/site/wrangler.jsonc rename to platforms/cloudflare/wrangler.jsonc index b176578dd2eea..963fbaff24472 100644 --- a/apps/site/wrangler.jsonc +++ b/platforms/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,14 @@ "head_sampling_rate": 1, }, "build": { + "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/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, + }); + }); +}); diff --git a/platforms/vercel/next.platform.config.mjs b/platforms/vercel/next.platform.config.mjs new file mode 100644 index 0000000000000..e216e33866df3 --- /dev/null +++ b/platforms/vercel/next.platform.config.mjs @@ -0,0 +1,39 @@ +/** + * 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', + }, + mdx: { + // Vercel supports the fast Oniguruma WASM engine and twoslash transforms, + // so keep parity with the default standalone config. + 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 || '', + }, + }; + }, +}; diff --git a/platforms/vercel/package.json b/platforms/vercel/package.json new file mode 100644 index 0000000000000..2df4c823eb071 --- /dev/null +++ b/platforms/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": "platforms/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": "catalog:", + "react": "catalog:" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=20" + } +} diff --git a/platforms/vercel/playwright.platform.config.mjs b/platforms/vercel/playwright.platform.config.mjs new file mode 100644 index 0000000000000..657a046c5c621 --- /dev/null +++ b/platforms/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('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} + */ +export default {}; diff --git a/platforms/vercel/src/analytics.tsx b/platforms/vercel/src/analytics.tsx new file mode 100644 index 0000000000000..6b78cb4512b3f --- /dev/null +++ b/platforms/vercel/src/analytics.tsx @@ -0,0 +1,11 @@ +import { Analytics } from '@vercel/analytics/react'; +import { SpeedInsights } from '@vercel/speed-insights/next'; + +const VercelAnalytics = () => ( + <> + + + +); + +export default VercelAnalytics; diff --git a/platforms/vercel/src/instrumentation.ts b/platforms/vercel/src/instrumentation.ts new file mode 100644 index 0000000000000..3e7a06a7d37d2 --- /dev/null +++ b/platforms/vercel/src/instrumentation.ts @@ -0,0 +1,3 @@ +import { registerOTel } from '@vercel/otel'; + +export const register = () => registerOTel({ serviceName: 'nodejs-org' }); diff --git a/platforms/vercel/tsconfig.json b/platforms/vercel/tsconfig.json new file mode 100644 index 0000000000000..5210f82773ae5 --- /dev/null +++ b/platforms/vercel/tsconfig.json @@ -0,0 +1,21 @@ +{ + "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" + ] +} diff --git a/platforms/vercel/vercel.json b/platforms/vercel/vercel.json new file mode 100644 index 0000000000000..5cf4bef4579bf --- /dev/null +++ b/platforms/vercel/vercel.json @@ -0,0 +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": "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 e50916229bc79..77d4e244e30f6 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 @@ -87,6 +90,12 @@ importers: '@mdx-js/mdx': specifier: ^3.1.1 version: 3.1.1 + '@node-core/platform-cloudflare': + specifier: workspace:* + version: link:../../platforms/cloudflare + '@node-core/platform-vercel': + specifier: workspace:* + version: link:../../platforms/vercel '@node-core/rehype-shiki': specifier: workspace:* version: link:../../packages/rehype-shiki @@ -99,18 +108,6 @@ importers: '@nodevu/core': specifier: 0.3.0 version: 0.3.0 - '@opentelemetry/api-logs': - specifier: ~0.213.0 - version: 0.213.0 - '@opentelemetry/instrumentation': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ~1.30.1 - version: 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': - specifier: ~0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.1) '@orama/core': specifier: ^1.2.19 version: 1.2.19 @@ -135,15 +132,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 @@ -163,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 @@ -220,30 +208,18 @@ importers: specifier: ~5.0.1 version: 5.0.1 devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20260418.1 - version: 4.20260422.1 '@eslint-react/eslint-plugin': specifier: ~3.0.0 version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': - specifier: ^0.0.1 - version: 0.0.1 '@next/eslint-plugin-next': specifier: 16.2.1 version: 16.2.1 '@node-core/remark-lint': specifier: workspace:* version: link:../../packages/remark-lint - '@opennextjs/cloudflare': - specifier: ^1.19.3 - version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) '@playwright/test': specifier: ^1.58.2 version: 1.58.2 - '@sentry/cloudflare': - specifier: ^10.49.0 - version: 10.49.0(@cloudflare/workers-types@4.20260422.1) '@testing-library/user-event': specifier: ~14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) @@ -304,9 +280,6 @@ importers: user-agent-data-types: specifier: 0.4.2 version: 0.4.2 - wrangler: - specifier: ^4.77.0 - version: 4.77.0(@cloudflare/workers-types@4.20260422.1) packages/i18n: devDependencies: @@ -596,6 +569,83 @@ importers: specifier: 4.21.0 version: 4.21.0 + platforms/cloudflare: + dependencies: + '@flarelabs-net/wrangler-build-time-fs-assets-polyfilling': + specifier: ^0.0.1 + version: 0.0.1 + '@opennextjs/cloudflare': + specifier: ^1.19.3 + version: 1.19.3(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(wrangler@4.77.0(@cloudflare/workers-types@4.20260422.1)) + '@sentry/cloudflare': + specifier: ^10.49.0 + version: 10.49.0(@cloudflare/workers-types@4.20260422.1) + next: + specifier: 'catalog:' + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 + wrangler: + specifier: ^4.77.0 + version: 4.77.0(@cloudflare/workers-types@4.20260422.1) + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260418.1 + version: 4.20260422.1 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/node': + specifier: 'catalog:' + version: 24.10.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 + + platforms/vercel: + dependencies: + '@opentelemetry/api-logs': + specifier: ~0.213.0 + version: 0.213.0 + '@opentelemetry/instrumentation': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ~1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': + specifier: ~0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@vercel/analytics': + specifier: ~2.0.1 + version: 2.0.1(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@vercel/otel': + specifier: ~2.1.1 + version: 2.1.1(@opentelemetry/api-logs@0.213.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)) + '@vercel/speed-insights': + specifier: ~2.0.0 + version: 2.0.0(next@16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + next: + specifier: 'catalog:' + version: 16.2.4(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 'catalog:' + version: 19.2.4 + devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages: '@actions/core@2.0.3': @@ -1151,9 +1201,6 @@ packages: '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.9.0': - resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} - '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} @@ -2252,10 +2299,6 @@ packages: resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -4388,11 +4431,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.10.9: - resolution: {integrity: sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==} - engines: {node: '>=6.0.0'} - hasBin: true - bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -4480,9 +4518,6 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - caniuse-lite@1.0.30001780: - resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} - caniuse-lite@1.0.30001784: resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} @@ -9625,11 +9660,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -10118,7 +10148,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.0 + '@emnapi/runtime': 1.9.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -10588,9 +10618,7 @@ snapshots: '@opentelemetry/api-logs@0.213.0': dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} @@ -12750,8 +12778,6 @@ snapshots: baseline-browser-mapping@2.10.13: {} - baseline-browser-mapping@2.10.9: {} - bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -12804,8 +12830,8 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.9 - caniuse-lite: 1.0.30001780 + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 electron-to-chromium: 1.5.321 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -12866,8 +12892,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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 71a45db007786..d8adee090dd40 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,12 +1,14 @@ packages: - packages/* - apps/* + - platforms/* catalog: '@types/node': ^24.10.1 '@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