diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e5899448d137..9764456b3ab02 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,9 +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 -apps/site/redirects.json @nodejs/web-infra +platforms/cloudflare/wrangler.jsonc @nodejs/web-infra +platforms/cloudflare/open-next.config.ts @nodejs/web-infra +platforms/cloudflare/next.config.mjs @nodejs/web-infra +platforms/vercel/vercel.json @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 8a42b4675fcd8..86059722a54ef 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -50,12 +50,13 @@ 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 build:cloudflare + - name: Run Playwright tests - working-directory: apps/site + working-directory: platforms/cloudflare run: node --run playwright - env: - PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true - PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 - 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..1a1f062ae34c5 100644 --- a/.github/workflows/tmp-cloudflare-open-next-deploy.yml +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -50,19 +50,19 @@ 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 - run: node --run cloudflare:build:worker + working-directory: platforms/cloudflare + run: node --run build:cloudflare - name: Deploy open-next site - working-directory: apps/site - run: node --run cloudflare:deploy + working-directory: platforms/cloudflare + run: node --run deploy:cloudflare env: CF_WORKERS_SCRIPTS_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 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..6e17e91c15e5e 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -1,6 +1,7 @@ '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'; @@ -9,25 +10,8 @@ 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. - // - // 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..2b74b76ed0252 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -1,22 +1,30 @@ '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 { getImagesConfig } from './next.image.config.mjs'; +import { DEPLOY_TARGET, PLATFORM_ALIAS } from './next.platform.constants.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'); +/** + * 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`); - return openNextAdapter.getDeploymentId(); - } +const platformImages = await platform.images?.(); +const platformNextConfig = await platform.nextConfig?.(); - return undefined; -}; +// 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 = { @@ -27,9 +35,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, - // Vercel/Next.js Image Optimization Settings - images: getImagesConfig(), + images: getImagesConfig(platformImages), serverExternalPackages: ['twoslash'], + 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. @@ -81,8 +89,22 @@ const nextConfig = { // Faster Development Servers with Turbopack turbopackFileSystemCacheForDev: true, }, - deploymentId: await getDeploymentId(), + // Provide Turbopack Aliases for Platform Resolution + turbopack: { resolveAlias: turbopackPlatformAliases }, + // Provide Webpack Aliases for Platform Resolution. + webpack: ({ resolve, ...config }) => ({ + ...config, + resolve: { + ...resolve, + alias: { ...resolve.alias, ...webpackPlatformAliases }, + conditionNames: resolve.conditionNames + .concat(DEPLOY_TARGET) + .filter(Boolean), + }, + }), + ...platformNextConfig, }; 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 00cc9d12c9c94..8882bf26dcd27 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -5,15 +5,6 @@ */ export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; -/** - * This is used for telling Next.js if the Website is deployed on Vercel - * - * Can be used for conditionally enabling features that we know are Vercel only - * - * @see https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL_ENV - */ -export const VERCEL_ENV = process.env.VERCEL_ENV || undefined; - /** * This is used for telling Next.js to do a Static Export Build of the Website * @@ -38,20 +29,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.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..88b6e3c4f8408 --- /dev/null +++ b/apps/site/next.platform.config.d.ts @@ -0,0 +1,26 @@ +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 = { + images?: () => Promise; + mdx?: PlatformMdxConfig; + nextConfig?: () => Promise; +}; + +declare const config: PlatformConfig; + +export default config; diff --git a/apps/site/next.platform.constants.mjs b/apps/site/next.platform.constants.mjs new file mode 100644 index 0000000000000..b769808771de3 --- /dev/null +++ b/apps/site/next.platform.constants.mjs @@ -0,0 +1,22 @@ +'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' | '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/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/package.json b/apps/site/package.json index fef56aeaa7c66..36410e39c4669 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", @@ -37,10 +34,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/orama": "^3.1.18", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", @@ -48,16 +41,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", @@ -78,15 +68,11 @@ "vfile-matter": "~5.0.1" }, "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", "@types/mdast": "^4.0.4", "@types/mdx": "^2.0.13", @@ -107,8 +93,23 @@ "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" + }, + "peerDependencies": { + "@node-core/platform-cloudflare": "workspace:*", + "@node-core/platform-default": "workspace:*", + "@node-core/platform-vercel": "workspace:*" + }, + "peerDependenciesMeta": { + "@node-core/platform-cloudflare": { + "optional": true + }, + "@node-core/platform-vercel": { + "optional": true + }, + "@node-core/platform-default": { + "optional": true + } }, "imports": { "#site/*": [ diff --git a/apps/site/playwright.config.mjs b/apps/site/playwright.config.mjs new file mode 100644 index 0000000000000..31a38174ac0b4 --- /dev/null +++ b/apps/site/playwright.config.mjs @@ -0,0 +1,45 @@ +import { defineConfig, devices } from '@playwright/test'; + +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). + * + * @type {{ default: import('./playwright.platform.config.d.ts').PlatformPlaywrightConfig }} + */ +const { default: platform } = await import( + `${PLATFORM_ALIAS}/playwright.config.mjs` +); + +const isCI = !!process.env.CI; + +// https://playwright.dev/docs/test-configuration +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI ? [['html'], ['github']] : [['html']], + ...platform, + use: { + ...platform.use, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/apps/site/playwright.config.ts b/apps/site/playwright.config.ts deleted file mode 100644 index 523f40f644582..0000000000000 --- a/apps/site/playwright.config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { defineConfig, devices, type Config } from '@playwright/test'; - -import json from './package.json' with { type: 'json' }; - -const isCI = !!process.env.CI; - -// https://playwright.dev/docs/test-configuration -export default defineConfig({ - testDir: './tests/e2e', - fullyParallel: true, - forbidOnly: isCI, - retries: isCI ? 2 : 0, - workers: isCI ? 1 : undefined, - reporter: isCI ? [['html'], ['github']] : [['html']], - ...getWebServerConfig(), - use: { - baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000', - trace: 'on-first-retry', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - ], -}); - -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..1e551f564b88b --- /dev/null +++ b/apps/site/playwright.platform.config.d.ts @@ -0,0 +1,15 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +/** + * Shared Playwright platform-config contract consumed by + * `apps/site/playwright.config.mjs` and implemented by each + * `@node-core/platform-` package. + */ +export type PlatformPlaywrightConfig = Pick< + PlaywrightTestConfig, + 'webServer' | 'use' +>; + +declare const config: PlatformPlaywrightConfig; + +export default config; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 02158236da303..2ed19e23babd5 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -11,15 +11,12 @@ "module": "esnext", "moduleResolution": "Bundler", "customConditions": ["default"], + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, - "plugins": [ - { - "name": "next" - } - ], + "plugins": [{ "name": "next" }], "baseUrl": "." }, "mdx": { @@ -37,14 +34,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 47a5049c2b701..36833d3122de9 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", @@ -32,8 +32,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", @@ -51,8 +51,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", @@ -75,8 +75,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", @@ -129,7 +129,6 @@ "inputs": ["{pages}/**/*.{mdx,md}"], "outputs": ["public/blog-data.json"], "env": [ - "VERCEL_ENV", "VERCEL_URL", "TURBO_CACHE", "TURBO_TELEMETRY_DISABLED", @@ -137,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/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..bd167713c1a50 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/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. @@ -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.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 [`site/cloudflare/image-loader.ts`](../apps/site/cloudflare/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 [`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/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. @@ -75,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 e54940426bc72..9d8a6f7c4a0b2 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) @@ -127,7 +129,7 @@ We chose Next.js because it is: ``` nodejs.org/ ├── apps/ -│ └── site/ # Main website application +│ └── site/ # Main website application (platform-agnostic) │ ├── components/ # Website-specific React components │ ├── layouts/ # Page layout templates │ ├── pages/ # Content pages (Markdown/MDX) @@ -143,6 +145,14 @@ nodejs.org/ │ ├── snippets/ # Code snippets for download page │ └── 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 +│ # (worker entrypoint, image loader, +│ # open-next.config.ts, wrangler.jsonc) └── packages/ ├── ui-components/ # Reusable UI components │ ├── styles/ # Global stylesheets @@ -150,8 +160,7 @@ nodejs.org/ ├── i18n/ # Internationalization │ ├── locales/ # Translation files │ └── config.json # Locale configuration - ├── rehype-shiki/ # Syntax highlighting plugin - ... + └── rehype-shiki/ # Syntax highlighting plugin ``` ## Architecture Decisions @@ -296,14 +305,36 @@ Benefits: - **`pnpm build`**: Production build for Vercel - **`pnpm deploy`**: Export build for legacy servers - **`pnpm dev`**: Development server +- **`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.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)_ | [`@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.config.mjs`](../platforms/vercel/next.config.mjs) and [`platforms/cloudflare/next.config.mjs`](../platforms/cloudflare/next.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..1e53086e6a245 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,6 @@ "scripts": { "compile": "turbo compile", "build": "turbo build", - "cloudflare:deploy": "turbo cloudflare:deploy", - "cloudflare:preview": "turbo cloudflare:preview", "deploy": "turbo deploy", "dev": "turbo dev", "format": "turbo //#prettier:fix prettier:fix lint:fix", diff --git a/platforms/cloudflare/analytics.tsx b/platforms/cloudflare/analytics.tsx new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/platforms/cloudflare/analytics.tsx @@ -0,0 +1 @@ +export default () => null; diff --git a/apps/site/cloudflare/image-loader.ts b/platforms/cloudflare/image-loader.ts similarity index 87% rename from apps/site/cloudflare/image-loader.ts rename to platforms/cloudflare/image-loader.ts index 2137028e23db4..d883ff004ed71 100644 --- a/apps/site/cloudflare/image-loader.ts +++ b/platforms/cloudflare/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/instrumentation.ts b/platforms/cloudflare/instrumentation.ts new file mode 100644 index 0000000000000..605be34cb4a3d --- /dev/null +++ b/platforms/cloudflare/instrumentation.ts @@ -0,0 +1 @@ +export const register = () => {}; diff --git a/platforms/cloudflare/next.config.mjs b/platforms/cloudflare/next.config.mjs new file mode 100644 index 0000000000000..83815b5ec60bf --- /dev/null +++ b/platforms/cloudflare/next.config.mjs @@ -0,0 +1,44 @@ +/** + * Platform config contributed by the Cloudflare deployment target. + * + * @type {import('../../apps/site/next.platform.config').PlatformConfig} + */ +export default { + 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.ts') + ), + }; + }, +}; diff --git a/apps/site/open-next.config.ts b/platforms/cloudflare/open-next.config.ts similarity index 100% rename from apps/site/open-next.config.ts rename to platforms/cloudflare/open-next.config.ts diff --git a/platforms/cloudflare/package.json b/platforms/cloudflare/package.json new file mode 100644 index 0000000000000..b79a003e5beb7 --- /dev/null +++ b/platforms/cloudflare/package.json @@ -0,0 +1,42 @@ +{ + "name": "@node-core/platform-cloudflare", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./*": "./*" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodejs.org", + "directory": "platforms/cloudflare" + }, + "scripts": { + "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", + "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", + "@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:", + "cross-env": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": "24.x" + } +} diff --git a/platforms/cloudflare/playwright.config.mjs b/platforms/cloudflare/playwright.config.mjs new file mode 100644 index 0000000000000..45731269f4b94 --- /dev/null +++ b/platforms/cloudflare/playwright.config.mjs @@ -0,0 +1,20 @@ +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8787'; + +/** + * 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 { + use: { baseURL: BASE_URL }, + webServer: { + stdout: 'pipe', + command: 'pnpm --filter=@node-core/platform-cloudflare dev:cloudflare', + url: BASE_URL, + timeout: 60_000 * 3, + }, +}; diff --git a/platforms/cloudflare/tsconfig.json b/platforms/cloudflare/tsconfig.json new file mode 100644 index 0000000000000..378ecbb585105 --- /dev/null +++ b/platforms/cloudflare/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", "worker-entrypoint.ts"] +} diff --git a/apps/site/cloudflare/worker-entrypoint.ts b/platforms/cloudflare/worker-entrypoint.ts similarity index 91% rename from apps/site/cloudflare/worker-entrypoint.ts rename to platforms/cloudflare/worker-entrypoint.ts index bd40543b4b9dd..9ce832bf58f15 100644 --- a/apps/site/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 '../.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/apps/site/wrangler.jsonc b/platforms/cloudflare/wrangler.jsonc similarity index 77% rename from apps/site/wrangler.jsonc rename to platforms/cloudflare/wrangler.jsonc index b176578dd2eea..2a62adc83c739 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": "./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/default/analytics.tsx b/platforms/default/analytics.tsx new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/platforms/default/analytics.tsx @@ -0,0 +1 @@ +export default () => null; diff --git a/platforms/default/instrumentation.ts b/platforms/default/instrumentation.ts new file mode 100644 index 0000000000000..605be34cb4a3d --- /dev/null +++ b/platforms/default/instrumentation.ts @@ -0,0 +1 @@ +export const register = () => {}; diff --git a/platforms/default/next.config.mjs b/platforms/default/next.config.mjs new file mode 100644 index 0000000000000..6336c2f5d5ab2 --- /dev/null +++ b/platforms/default/next.config.mjs @@ -0,0 +1,13 @@ +/** + * Platform config contributed by the default deployment target. + * + * @type {import('../../apps/site/next.platform.config').PlatformConfig} + */ +export default { + 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/platforms/default/playwright.config.mjs b/platforms/default/playwright.config.mjs new file mode 100644 index 0000000000000..82fb1f33c3b9c --- /dev/null +++ b/platforms/default/playwright.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('../../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/analytics.tsx b/platforms/vercel/analytics.tsx new file mode 100644 index 0000000000000..6b78cb4512b3f --- /dev/null +++ b/platforms/vercel/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/instrumentation.ts b/platforms/vercel/instrumentation.ts new file mode 100644 index 0000000000000..3e7a06a7d37d2 --- /dev/null +++ b/platforms/vercel/instrumentation.ts @@ -0,0 +1,3 @@ +import { registerOTel } from '@vercel/otel'; + +export const register = () => registerOTel({ serviceName: 'nodejs-org' }); diff --git a/platforms/vercel/next.config.mjs b/platforms/vercel/next.config.mjs new file mode 100644 index 0000000000000..ef452793ecd51 --- /dev/null +++ b/platforms/vercel/next.config.mjs @@ -0,0 +1,30 @@ +/** + * Platform config contributed by the Vercel deployment target. + * + * @type {import('../../apps/site/next.platform.config').PlatformConfig} + */ +export default { + 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. 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: 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..189fbc8e71e93 --- /dev/null +++ b/platforms/vercel/package.json @@ -0,0 +1,42 @@ +{ + "name": "@node-core/platform-vercel", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./*": "./*" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodejs.org", + "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", + "playwright": "cross-env NEXT_PUBLIC_DEPLOY_TARGET=vercel pnpm --filter=@node-core/website playwright" + }, + "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", + "cross-env": "catalog:" + }, + "peerDependencies": { + "next": "catalog:", + "react": "catalog:" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": "24.x" + } +} diff --git a/platforms/vercel/playwright.config.mjs b/platforms/vercel/playwright.config.mjs new file mode 100644 index 0000000000000..3236d58b7d251 --- /dev/null +++ b/platforms/vercel/playwright.config.mjs @@ -0,0 +1,12 @@ +/** + * 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. + * + * @type {import('../../apps/site/playwright.platform.config').PlatformPlaywrightConfig} + */ +export default { + use: { baseURL: process.env.PLAYWRIGHT_BASE_URL }, +}; diff --git a/platforms/vercel/tsconfig.json b/platforms/vercel/tsconfig.json new file mode 100644 index 0000000000000..aa6f18311fe3f --- /dev/null +++ b/platforms/vercel/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/vercel.json b/platforms/vercel/vercel.json new file mode 100644 index 0000000000000..33374eaffa1c6 --- /dev/null +++ b/platforms/vercel/vercel.json @@ -0,0 +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/platform-vercel build:vercel", + "outputDirectory": "../../apps/site/.next", + "ignoreCommand": "[[ \"$VERCEL_GIT_COMMIT_REF\" =~ \"^dependabot/.*\" || \"$VERCEL_GIT_COMMIT_REF\" =~ \"^gh-readonly-queue/.*\" ]]" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 057903121005b..9176a052134f0 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,15 @@ 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-default': + specifier: workspace:* + version: link:../../platforms/default + '@node-core/platform-vercel': + specifier: workspace:* + version: link:../../platforms/vercel '@node-core/rehype-shiki': specifier: workspace:* version: link:../../packages/rehype-shiki @@ -99,18 +111,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/orama': specifier: ^3.1.18 version: 3.1.18 @@ -132,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 @@ -160,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 @@ -217,33 +208,21 @@ 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,9 +283,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: @@ -599,6 +575,108 @@ 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 + 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 + + 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) + 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) + 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': @@ -1154,9 +1232,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==} @@ -2255,10 +2330,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 +4462,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 +4549,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==} @@ -9628,11 +9691,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 @@ -10121,7 +10179,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': @@ -10591,9 +10649,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 +12809,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 +12861,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 +12923,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: {} @@ -13592,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 @@ -13618,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)) @@ -13648,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 @@ -13659,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 @@ -13670,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 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 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"]