diff --git a/crates/next-build-test/nextConfig.json b/crates/next-build-test/nextConfig.json index 13142e8a57d2..6341127ae3a3 100644 --- a/crates/next-build-test/nextConfig.json +++ b/crates/next-build-test/nextConfig.json @@ -53,6 +53,8 @@ "productionBrowserSourceMaps": false, "optimizeFonts": true, "excludeDefaultMomentLocales": true, + "serverRuntimeConfig": {}, + "publicRuntimeConfig": {}, "reactProductionProfiling": false, "reactStrictMode": true, "httpAgentOptions": { diff --git a/docs/01-app/02-guides/environment-variables.mdx b/docs/01-app/02-guides/environment-variables.mdx index 83917914232f..809d4871cd99 100644 --- a/docs/01-app/02-guides/environment-variables.mdx +++ b/docs/01-app/02-guides/environment-variables.mdx @@ -228,6 +228,7 @@ This allows you to use a singular Docker image that can be promoted through mult **Good to know:** - You can run code on server startup using the [`register` function](/docs/app/guides/instrumentation). +- We do not recommend using the [`runtimeConfig`](/docs/pages/api-reference/config/next-config-js/runtime-configuration) option, as this does not work with the standalone output mode. Instead, we recommend [incrementally adopting](/docs/app/guides/migrating/app-router-migration) the App Router if you need this feature. ## Test Environment Variables diff --git a/docs/01-app/02-guides/self-hosting.mdx b/docs/01-app/02-guides/self-hosting.mdx index 9e161028cded..c09f87d05010 100644 --- a/docs/01-app/02-guides/self-hosting.mdx +++ b/docs/01-app/02-guides/self-hosting.mdx @@ -79,6 +79,7 @@ This allows you to use a singular Docker image that can be promoted through mult > **Good to know:** > > - You can run code on server startup using the [`register` function](/docs/app/guides/instrumentation). +> - We do not recommend using the [runtimeConfig](/docs/pages/api-reference/config/next-config-js/runtime-configuration) option, as this does not work with the standalone output mode. Instead, we recommend [incrementally adopting](/docs/app/guides/migrating/app-router-migration) the App Router. ## Caching and ISR diff --git a/docs/01-app/03-api-reference/05-config/01-next-config-js/output.mdx b/docs/01-app/03-api-reference/05-config/01-next-config-js/output.mdx index ee010ca94702..4d068fa1bba8 100644 --- a/docs/01-app/03-api-reference/05-config/01-next-config-js/output.mdx +++ b/docs/01-app/03-api-reference/05-config/01-next-config-js/output.mdx @@ -59,7 +59,7 @@ node .next/standalone/server.js > **Good to know**: > -> - `next.config.js` is read during `next build` and serialized into the `server.js` output file. +> - `next.config.js` is read during `next build` and serialized into the `server.js` output file. If the legacy [`serverRuntimeConfig` or `publicRuntimeConfig` options](/docs/pages/api-reference/config/next-config-js/runtime-configuration) are being used, the values will be specific to values at build time. > - If your project needs to listen to a specific port or hostname, you can define `PORT` or `HOSTNAME` environment variables before running `server.js`. For example, run `PORT=8080 HOSTNAME=0.0.0.0 node server.js` to start the server on `http://0.0.0.0:8080`. diff --git a/docs/02-pages/04-api-reference/04-config/01-next-config-js/runtime-configuration.mdx b/docs/02-pages/04-api-reference/04-config/01-next-config-js/runtime-configuration.mdx new file mode 100644 index 000000000000..879394ea38dd --- /dev/null +++ b/docs/02-pages/04-api-reference/04-config/01-next-config-js/runtime-configuration.mdx @@ -0,0 +1,60 @@ +--- +title: Runtime Config +description: Add client and server runtime configuration to your Next.js app. +--- + +> **Warning:** +> +> - **This feature is deprecated.** We recommend using [environment variables](/docs/pages/guides/environment-variables) instead, which also can support reading runtime values. +> - You can run code on server startup using the [`register` function](/docs/app/guides/instrumentation). +> - This feature does not work with [Automatic Static Optimization](/docs/pages/building-your-application/rendering/automatic-static-optimization), [Output File Tracing](/docs/pages/api-reference/config/next-config-js/output#automatically-copying-traced-files), or [React Server Components](/docs/app/getting-started/server-and-client-components). + +To add runtime configuration to your app, open `next.config.js` and add the `publicRuntimeConfig` and `serverRuntimeConfig` configs: + +```js filename="next.config.js" +module.exports = { + serverRuntimeConfig: { + // Will only be available on the server side + mySecret: 'secret', + secondSecret: process.env.SECOND_SECRET, // Pass through env variables + }, + publicRuntimeConfig: { + // Will be available on both server and client + staticFolder: '/static', + }, +} +``` + +Place any server-only runtime config under `serverRuntimeConfig`. + +Anything accessible to both client and server-side code should be under `publicRuntimeConfig`. + +> A page that relies on `publicRuntimeConfig` **must** use `getInitialProps` or `getServerSideProps` or your application must have a [Custom App](/docs/pages/building-your-application/routing/custom-app) with `getInitialProps` to opt-out of [Automatic Static Optimization](/docs/pages/building-your-application/rendering/automatic-static-optimization). Runtime configuration won't be available to any page (or component in a page) without being server-side rendered. + +To get access to the runtime configs in your app use `next/config`, like so: + +```jsx +import getConfig from 'next/config' +import Image from 'next/image' + +// Only holds serverRuntimeConfig and publicRuntimeConfig +const { serverRuntimeConfig, publicRuntimeConfig } = getConfig() +// Will only be available on the server-side +console.log(serverRuntimeConfig.mySecret) +// Will be available on both server-side and client-side +console.log(publicRuntimeConfig.staticFolder) + +function MyImage() { + return ( +
+ logo +
+ ) +} + +export default MyImage +``` diff --git a/packages/next/config.d.ts b/packages/next/config.d.ts new file mode 100644 index 000000000000..20c292fb467e --- /dev/null +++ b/packages/next/config.d.ts @@ -0,0 +1,3 @@ +import getConfig from './dist/shared/lib/runtime-config.external' +export * from './dist/shared/lib/runtime-config.external' +export default getConfig diff --git a/packages/next/config.js b/packages/next/config.js new file mode 100644 index 000000000000..668ee7c54f0e --- /dev/null +++ b/packages/next/config.js @@ -0,0 +1 @@ +module.exports = require('./dist/shared/lib/runtime-config.external') diff --git a/packages/next/index.d.ts b/packages/next/index.d.ts index ab2ddbddb0c5..8e7cf5d71b9b 100644 --- a/packages/next/index.d.ts +++ b/packages/next/index.d.ts @@ -4,6 +4,7 @@ /// /// /// +/// /// /// /// diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 81e24ed17548..e75691ceec0f 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1909,7 +1909,9 @@ export default async function build( } } - const { configFileName } = config + const { configFileName, publicRuntimeConfig, serverRuntimeConfig } = + config + const runtimeEnvConfig = { publicRuntimeConfig, serverRuntimeConfig } const sriEnabled = Boolean(config.experimental.sri?.algorithm) const nonStaticErrorPageSpan = staticCheckSpan.traceChild( @@ -1922,6 +1924,7 @@ export default async function build( (await worker.hasCustomGetInitialProps({ page: '/_error', distDir, + runtimeEnvConfig, checkingApp: false, sriEnabled, })) @@ -1935,6 +1938,7 @@ export default async function build( page: '/_error', distDir, configFileName, + runtimeEnvConfig, cacheComponents: isAppCacheComponentsEnabled, authInterrupts: isAuthInterruptsEnabled, httpAgentOptions: config.httpAgentOptions, @@ -1954,6 +1958,7 @@ export default async function build( ? worker.hasCustomGetInitialProps({ page: appPageToCheck, distDir, + runtimeEnvConfig, checkingApp: true, sriEnabled, }) @@ -1963,6 +1968,7 @@ export default async function build( ? worker.getDefinedNamedExports({ page: appPageToCheck, distDir, + runtimeEnvConfig, sriEnabled, }) : Promise.resolve([]) @@ -2149,6 +2155,7 @@ export default async function build( originalAppPath, distDir, configFileName, + runtimeEnvConfig, httpAgentOptions: config.httpAgentOptions, locales: config.i18n?.locales, defaultLocale: config.i18n?.defaultLocale, diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index 3aad16017f99..f1002b14b846 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -155,6 +155,12 @@ async function requestHandler( distDir: '', crossOrigin: nextConfig.crossOrigin ? nextConfig.crossOrigin : undefined, largePageDataBytes: nextConfig.experimental.largePageDataBytes, + // Only the `publicRuntimeConfig` key is exposed to the client side + // It'll be rendered as part of __NEXT_DATA__ on the client side + runtimeConfig: + Object.keys(nextConfig.publicRuntimeConfig).length > 0 + ? nextConfig.publicRuntimeConfig + : undefined, isExperimentalCompile: nextConfig.experimental.isExperimentalCompile, // `htmlLimitedBots` is passed to server as serialized config in string format diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 4c1e6420e7bc..680a25971213 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -567,6 +567,7 @@ export async function isPageStatic({ page, distDir, configFileName, + runtimeEnvConfig, httpAgentOptions, locales, defaultLocale, @@ -593,6 +594,7 @@ export async function isPageStatic({ cacheComponents: boolean authInterrupts: boolean configFileName: string + runtimeEnvConfig: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] locales?: readonly string[] defaultLocale?: string @@ -642,6 +644,9 @@ export async function isPageStatic({ const isPageStaticSpan = trace('is-page-static-utils', parentId) return isPageStaticSpan .traceAsyncFn(async (): Promise => { + ;( + require('../shared/lib/runtime-config.external') as typeof import('../shared/lib/runtime-config.external') + ).setConfig(runtimeEnvConfig) setHttpClientAndAgentOptions({ httpAgentOptions, }) @@ -950,14 +955,20 @@ export function reduceAppConfig( export async function hasCustomGetInitialProps({ page, distDir, + runtimeEnvConfig, checkingApp, sriEnabled, }: { page: string distDir: string + runtimeEnvConfig: any checkingApp: boolean sriEnabled: boolean }): Promise { + ;( + require('../shared/lib/runtime-config.external') as typeof import('../shared/lib/runtime-config.external') + ).setConfig(runtimeEnvConfig) + const { ComponentMod } = await loadComponents({ distDir, page: page, @@ -980,12 +991,17 @@ export async function hasCustomGetInitialProps({ export async function getDefinedNamedExports({ page, distDir, + runtimeEnvConfig, sriEnabled, }: { page: string distDir: string + runtimeEnvConfig: any sriEnabled: boolean }): Promise> { + ;( + require('../shared/lib/runtime-config.external') as typeof import('../shared/lib/runtime-config.external') + ).setConfig(runtimeEnvConfig) const { ComponentMod } = await loadComponents({ distDir, page: page, diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index 81fdc33f88e9..d63875b6b059 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -20,6 +20,7 @@ import { urlQueryToSearchParams, assign, } from '../shared/lib/router/utils/querystring' +import { setConfig } from '../shared/lib/runtime-config.external' import { getURL, loadGetInitialProps, ST } from '../shared/lib/utils' import type { NextWebVitalsMetric, NEXT_DATA } from '../shared/lib/utils' import { Portal } from './portal' @@ -211,6 +212,12 @@ export async function initialize(opts: { devClient?: any } = {}): Promise<{ // So, this is how we do it in the client side at runtime ;(self as any).__next_set_public_path__(`${prefix}/_next/`) //eslint-disable-line + // Initialize next/config with the environment configuration + setConfig({ + serverRuntimeConfig: {}, + publicRuntimeConfig: initialData.runtimeConfig || {}, + }) + asPath = getURL() // make sure not to attempt stripping basePath for 404s diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 0823be6f1302..eaa88b623f65 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -422,6 +422,12 @@ async function exportAppImpl( nextConfig.experimental.enablePrerenderSourceMaps === true, } + const { publicRuntimeConfig } = nextConfig + + if (Object.keys(publicRuntimeConfig).length > 0) { + renderOpts.runtimeConfig = publicRuntimeConfig + } + // We need this for server rendering the Link component. ;(globalThis as any).__NEXT_DATA__ = { nextExport: true, diff --git a/packages/next/src/export/types.ts b/packages/next/src/export/types.ts index 8a27f76b269e..795202731dd4 100644 --- a/packages/next/src/export/types.ts +++ b/packages/next/src/export/types.ts @@ -54,6 +54,7 @@ export interface ExportPageInput { ampValidatorPath?: string trailingSlash?: boolean buildExport?: boolean + serverRuntimeConfig: { [key: string]: any } subFolders?: boolean optimizeCss: any disableOptimizedLoading: any diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index c29ad8f1eae0..2a7f0d265aa0 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -50,6 +50,10 @@ import type { PagesRenderContext, PagesSharedContext } from '../server/render' import type { AppSharedContext } from '../server/app-render/app-render' import { MultiFileWriter } from '../lib/multi-file-writer' import { createRenderResumeDataCache } from '../server/resume-data-cache/resume-data-cache' + +const envConfig = + require('../shared/lib/runtime-config.external') as typeof import('../shared/lib/runtime-config.external') + ;(globalThis as any).__NEXT_DATA__ = { nextExport: true, } @@ -71,6 +75,7 @@ async function exportPageImpl( distDir, pagesDataDir, buildExport = false, + serverRuntimeConfig, subFolders = false, optimizeCss, disableOptimizedLoading, @@ -191,6 +196,11 @@ async function exportPageImpl( addRequestMeta(req, 'isLocaleDomain', true) } + envConfig.setConfig({ + serverRuntimeConfig, + publicRuntimeConfig: commonRenderOpts.runtimeConfig, + }) + const getHtmlFilename = (p: string) => subFolders ? `${p}${sep}index.html` : `${p}.html` @@ -406,6 +416,7 @@ export async function exportPages( ampValidatorPath: nextConfig.experimental.amp?.validator || undefined, trailingSlash: nextConfig.trailingSlash, + serverRuntimeConfig: nextConfig.serverRuntimeConfig, subFolders: nextConfig.trailingSlash && !options.buildExport, buildExport: options.buildExport, optimizeCss: nextConfig.experimental.optimizeCss, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 35f7ee4a76b0..2c5bd0438d00 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -57,6 +57,7 @@ import { UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../shared/lib/constants' import { isDynamicRoute } from '../shared/lib/router/utils' +import { setConfig } from '../shared/lib/runtime-config.external' import { execOnce } from '../shared/lib/utils' import { isBlockedPage } from './utils' import { getBotType, isBot } from '../shared/lib/router/utils/is-bot' @@ -474,7 +475,14 @@ export default abstract class Server< ? new LocaleRouteNormalizer(this.i18nProvider) : undefined - const { assetPrefix, generateEtags } = this.nextConfig + // Only serverRuntimeConfig needs the default + // publicRuntimeConfig gets it's default in client/index.js + const { + serverRuntimeConfig = {}, + publicRuntimeConfig, + assetPrefix, + generateEtags, + } = this.nextConfig this.buildId = this.getBuildId() // this is a hack to avoid Webpack knowing this is equal to this.minimalMode @@ -543,6 +551,12 @@ export default abstract class Server< ? this.nextConfig.crossOrigin : undefined, largePageDataBytes: this.nextConfig.experimental.largePageDataBytes, + // Only the `publicRuntimeConfig` key is exposed to the client side + // It'll be rendered as part of __NEXT_DATA__ on the client side + runtimeConfig: + Object.keys(publicRuntimeConfig).length > 0 + ? publicRuntimeConfig + : undefined, isExperimentalCompile: this.nextConfig.experimental.isExperimentalCompile, // `htmlLimitedBots` is passed to server as serialized config in string format @@ -569,6 +583,12 @@ export default abstract class Server< reactMaxHeadersLength: this.nextConfig.reactMaxHeadersLength, } + // Initialize next/config with the environment configuration + setConfig({ + serverRuntimeConfig, + publicRuntimeConfig, + }) + this.pagesManifest = this.getPagesManifest() this.appPathsManifest = this.getAppPathsManifest() this.appPathRoutes = this.getAppPathRoutes() diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index dc06321a7930..adfedff64112 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -653,6 +653,7 @@ export const configSchema: zod.ZodType = z.lazy(() => pageExtensions: z.array(z.string()).min(1).optional(), poweredByHeader: z.boolean().optional(), productionBrowserSourceMaps: z.boolean().optional(), + publicRuntimeConfig: z.record(z.string(), z.any()).optional(), reactProductionProfiling: z.boolean().optional(), reactStrictMode: z.boolean().nullable().optional(), reactMaxHeadersLength: z.number().nonnegative().int().optional(), @@ -685,6 +686,7 @@ export const configSchema: zod.ZodType = z.lazy(() => .catchall(z.any()) .optional(), serverExternalPackages: z.array(z.string()).optional(), + serverRuntimeConfig: z.record(z.string(), z.any()).optional(), skipMiddlewareUrlNormalize: z.boolean().optional(), skipTrailingSlashRedirect: z.boolean().optional(), staticPageGenerationTimeout: z.number().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 02f2867f1044..23e6391b7b53 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1115,6 +1115,20 @@ export interface NextConfig { */ reactMaxHeadersLength?: number + /** + * Add public (in browser) runtime configuration to your app + * + * @see [Runtime configuration](https://nextjs.org/docs/pages/api-reference/config/next-config-js/runtime-configuration + */ + publicRuntimeConfig?: { [key: string]: any } + + /** + * Add server runtime configuration to your app + * + * @see [Runtime configuration](https://nextjs.org/docs/pages/api-reference/config/next-config-js/runtime-configuration + */ + serverRuntimeConfig?: { [key: string]: any } + /** * Next.js enables HTTP Keep-Alive by default. * You may want to disable HTTP Keep-Alive for certain `fetch()` calls or globally. @@ -1353,6 +1367,8 @@ export const defaultConfig = Object.freeze({ i18n: null, productionBrowserSourceMaps: false, excludeDefaultMomentLocales: true, + serverRuntimeConfig: {}, + publicRuntimeConfig: {}, reactProductionProfiling: false, reactStrictMode: null, reactMaxHeadersLength: 6000, diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index e3febaca236c..df7f323082e5 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -804,7 +804,12 @@ export default class DevServer extends Server { // from waiting on them for the page to load in dev mode const __getStaticPaths = async () => { - const { configFileName, httpAgentOptions } = this.nextConfig + const { + configFileName, + publicRuntimeConfig, + serverRuntimeConfig, + httpAgentOptions, + } = this.nextConfig const { locales, defaultLocale } = this.nextConfig.i18n || {} const staticPathsWorker = this.getStaticPathsWorker() @@ -816,6 +821,8 @@ export default class DevServer extends Server { config: { pprConfig: this.nextConfig.experimental.ppr, configFileName, + publicRuntimeConfig, + serverRuntimeConfig, cacheComponents: Boolean( this.nextConfig.experimental.cacheComponents ), diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 72023e2e42a7..227933c08fe2 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -25,6 +25,8 @@ import type { AppRouteRouteModule } from '../route-modules/app-route/module' type RuntimeConfig = { pprConfig: ExperimentalPPRConfig | undefined configFileName: string + publicRuntimeConfig: { [key: string]: any } + serverRuntimeConfig: { [key: string]: any } cacheComponents: boolean } @@ -90,6 +92,9 @@ export async function loadStaticPaths({ }) // update work memory runtime-config + ;( + require('../../shared/lib/runtime-config.external') as typeof import('../../shared/lib/runtime-config.external') + ).setConfig(config) setHttpClientAndAgentOptions({ httpAgentOptions, }) diff --git a/packages/next/src/server/lib/router-utils/router-server-context.ts b/packages/next/src/server/lib/router-utils/router-server-context.ts index eaeafcc6eb56..568be2acae75 100644 --- a/packages/next/src/server/lib/router-utils/router-server-context.ts +++ b/packages/next/src/server/lib/router-utils/router-server-context.ts @@ -28,6 +28,8 @@ export type RouterServerContext = Record< parsedUrl?: UrlWithParsedQuery, setHeaders?: boolean ) => Promise + // current loaded public runtime config + publicRuntimeConfig?: NextConfigComplete['publicRuntimeConfig'] // exposing nextConfig for dev mode specifically nextConfig?: NextConfigComplete // whether running in custom server mode diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 5a93b9bb0c01..6d9df62d3bf9 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -243,6 +243,7 @@ function renderPageTree( export type RenderOptsPartial = { canonicalBase: string + runtimeConfig?: { [key: string]: any } assetPrefix?: string err?: Error | null nextExport?: boolean @@ -1490,6 +1491,7 @@ export async function renderToHTMLImpl( domainLocales, locale, locales, + runtimeConfig, } = renderOpts const htmlProps: HtmlProps = { __NEXT_DATA__: { @@ -1498,6 +1500,7 @@ export async function renderToHTMLImpl( query, // querystring parsed / passed by the user buildId: sharedContext.buildId, assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML + runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML nextExport: nextExport === true ? true : undefined, // If this is a page exported by `next export` autoExport: isAutoExport === true ? true : undefined, // If this is an auto exported page isFallback, diff --git a/packages/next/src/server/route-modules/pages/pages-handler.ts b/packages/next/src/server/route-modules/pages/pages-handler.ts index a113676e97c4..686fcc80b3de 100644 --- a/packages/next/src/server/route-modules/pages/pages-handler.ts +++ b/packages/next/src/server/route-modules/pages/pages-handler.ts @@ -204,6 +204,10 @@ export const getHandler = ({ query: hasStaticProps ? {} : originalQuery, }) + const publicRuntimeConfig: Record = + routerServerContext?.publicRuntimeConfig || + nextConfig.publicRuntimeConfig + const handleResponse = async (span?: Span) => { const responseGenerator: ResponseGenerator = async ({ previousCacheEntry, @@ -278,6 +282,12 @@ export const getHandler = ({ nextConfig.experimental.disableOptimizedLoading, largePageDataBytes: nextConfig.experimental.largePageDataBytes, + // Only the `publicRuntimeConfig` key is exposed to the client side + // It'll be rendered as part of __NEXT_DATA__ on the client side + runtimeConfig: + Object.keys(publicRuntimeConfig).length > 0 + ? publicRuntimeConfig + : undefined, isExperimentalCompile, diff --git a/packages/next/src/shared/lib/runtime-config.external.ts b/packages/next/src/shared/lib/runtime-config.external.ts new file mode 100644 index 000000000000..b4a48a362d63 --- /dev/null +++ b/packages/next/src/shared/lib/runtime-config.external.ts @@ -0,0 +1,9 @@ +let runtimeConfig: any + +export default () => { + return runtimeConfig +} + +export function setConfig(configValue: any): void { + runtimeConfig = configValue +} diff --git a/packages/next/src/shared/lib/utils.ts b/packages/next/src/shared/lib/utils.ts index 7a5ff10744ae..abc5d6da627f 100644 --- a/packages/next/src/shared/lib/utils.ts +++ b/packages/next/src/shared/lib/utils.ts @@ -91,6 +91,7 @@ export type NEXT_DATA = { query: ParsedUrlQuery buildId: string assetPrefix?: string + runtimeConfig?: { [key: string]: any } nextExport?: boolean autoExport?: boolean isFallback?: boolean diff --git a/test/integration/config-mjs/next.config.mjs b/test/integration/config-mjs/next.config.mjs index a26eb8fbda65..84b5831fc866 100644 --- a/test/integration/config-mjs/next.config.mjs +++ b/test/integration/config-mjs/next.config.mjs @@ -4,6 +4,12 @@ export default { maxInactiveAge: 1000 * 60 * 60, }, poweredByHeader: false, + serverRuntimeConfig: { + mySecret: 'secret', + }, + publicRuntimeConfig: { + staticFolder: '/static', + }, env: { customVar: 'hello', }, diff --git a/test/integration/config-mjs/pages/next-config.js b/test/integration/config-mjs/pages/next-config.js index da615fabbe42..945aeba52da3 100644 --- a/test/integration/config-mjs/pages/next-config.js +++ b/test/integration/config-mjs/pages/next-config.js @@ -1,5 +1,10 @@ +import getConfig from 'next/config' +const { serverRuntimeConfig, publicRuntimeConfig } = getConfig() + export default () => (
+

server-only: {serverRuntimeConfig.mySecret || '***'}

+

{publicRuntimeConfig.staticFolder}

{process.env.customVar}

) diff --git a/test/integration/config-mjs/test/index.test.ts b/test/integration/config-mjs/test/index.test.ts index 5d66d3f2c442..16a13738f99f 100644 --- a/test/integration/config-mjs/test/index.test.ts +++ b/test/integration/config-mjs/test/index.test.ts @@ -44,16 +44,32 @@ describe('Configuration', () => { expect(header).not.toBe('Next.js') }) + test('renders server config on the server only', async () => { + const $ = await get$('/next-config') + expect($('#server-only').text()).toBe('server-only: secret') + }) + + test('renders public config on the server only', async () => { + const $ = await get$('/next-config') + expect($('#server-and-client').text()).toBe('/static') + }) + test('correctly imports a package that defines `module` but no `main` in package.json', async () => { const $ = await get$('/module-only-content') expect($('#messageInAPackage').text()).toBe('OK') }) - it('should have env variables available on the client', async () => { + it('should have config available on the client', async () => { const browser = await webdriver(context.appPort, '/next-config') + const serverText = await browser.elementByCss('#server-only').text() + const serverClientText = await browser + .elementByCss('#server-and-client') + .text() const envValue = await browser.elementByCss('#env').text() + expect(serverText).toBe('server-only: ***') + expect(serverClientText).toBe('/static') expect(envValue).toBe('hello') await browser.close() }) diff --git a/test/integration/config/next.config.js b/test/integration/config/next.config.js index b0e531387538..e71caf99a7fe 100644 --- a/test/integration/config/next.config.js +++ b/test/integration/config/next.config.js @@ -4,6 +4,12 @@ module.exports = { maxInactiveAge: 1000 * 60 * 60, }, poweredByHeader: false, + serverRuntimeConfig: { + mySecret: 'secret', + }, + publicRuntimeConfig: { + staticFolder: '/static', + }, env: { customVar: 'hello', }, diff --git a/test/integration/config/pages/next-config.js b/test/integration/config/pages/next-config.js index da615fabbe42..945aeba52da3 100644 --- a/test/integration/config/pages/next-config.js +++ b/test/integration/config/pages/next-config.js @@ -1,5 +1,10 @@ +import getConfig from 'next/config' +const { serverRuntimeConfig, publicRuntimeConfig } = getConfig() + export default () => (
+

server-only: {serverRuntimeConfig.mySecret || '***'}

+

{publicRuntimeConfig.staticFolder}

{process.env.customVar}

) diff --git a/test/integration/config/test/index.test.js b/test/integration/config/test/index.test.js index a178283dc0f9..2bad399016e6 100644 --- a/test/integration/config/test/index.test.js +++ b/test/integration/config/test/index.test.js @@ -44,16 +44,32 @@ describe('Configuration', () => { expect(header).not.toBe('Next.js') }) + test('renders server config on the server only', async () => { + const $ = await get$('/next-config') + expect($('#server-only').text()).toBe('server-only: secret') + }) + + test('renders public config on the server only', async () => { + const $ = await get$('/next-config') + expect($('#server-and-client').text()).toBe('/static') + }) + test('correctly imports a package that defines `module` but no `main` in package.json', async () => { const $ = await get$('/module-only-content') expect($('#messageInAPackage').text()).toBe('OK') }) - it('should have env variables available on the client', async () => { + it('should have config available on the client', async () => { const browser = await webdriver(context.appPort, '/next-config') + const serverText = await browser.elementByCss('#server-only').text() + const serverClientText = await browser + .elementByCss('#server-and-client') + .text() const envValue = await browser.elementByCss('#env').text() + expect(serverText).toBe('server-only: ***') + expect(serverClientText).toBe('/static') expect(envValue).toBe('hello') await browser.close() }) diff --git a/test/integration/development-runtime-config/components/Layout.js b/test/integration/development-runtime-config/components/Layout.js new file mode 100644 index 000000000000..75cc04d8d65d --- /dev/null +++ b/test/integration/development-runtime-config/components/Layout.js @@ -0,0 +1,15 @@ +import getConfig from 'next/config' + +const { serverRuntimeConfig, publicRuntimeConfig } = getConfig() + +const Layout = ({ children }) => { + return ( +
+

{JSON.stringify(serverRuntimeConfig)}

+

{JSON.stringify(publicRuntimeConfig)}

+
{children}
+
+ ) +} + +export default Layout diff --git a/test/integration/development-runtime-config/pages/_app.js b/test/integration/development-runtime-config/pages/_app.js new file mode 100644 index 000000000000..f76402f3025c --- /dev/null +++ b/test/integration/development-runtime-config/pages/_app.js @@ -0,0 +1,11 @@ +import Layout from './../components/Layout' + +const App = ({ Component, pageProps }) => { + return ( + + + + ) +} + +export default App diff --git a/test/integration/development-runtime-config/pages/index.js b/test/integration/development-runtime-config/pages/index.js new file mode 100644 index 000000000000..f40492ac06d8 --- /dev/null +++ b/test/integration/development-runtime-config/pages/index.js @@ -0,0 +1 @@ +export default () => 'test' diff --git a/test/integration/development-runtime-config/pages/post/[pid].js b/test/integration/development-runtime-config/pages/post/[pid].js new file mode 100644 index 000000000000..d5aa21f76fe9 --- /dev/null +++ b/test/integration/development-runtime-config/pages/post/[pid].js @@ -0,0 +1,22 @@ +const Post = () => { + return ( +
+

POST

+
+ ) +} + +export const getStaticProps = async () => { + return { + props: {}, + } +} + +export const getStaticPaths = async () => { + return { + paths: ['/post/1', '/post/2'], + fallback: true, + } +} + +export default Post diff --git a/test/integration/development-runtime-config/test/index.test.js b/test/integration/development-runtime-config/test/index.test.js new file mode 100644 index 000000000000..12245e02d9d0 --- /dev/null +++ b/test/integration/development-runtime-config/test/index.test.js @@ -0,0 +1,77 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import { + findPort, + renderViaHTTP, + launchApp, + waitFor, + killApp, +} from 'next-test-utils' + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') + +const runApp = async (config) => { + const port = await findPort() + + let stderr = '' + const app = await launchApp(appDir, port, { + onStderr(err) { + stderr += err + }, + }) + + const html = await renderViaHTTP(port, '/post/1') + const $ = cheerio.load(html) + await waitFor(1000) + + await killApp(app) + await fs.remove(nextConfig) + + expect(stderr).not.toMatch( + /Cannot read property 'serverRuntimeConfig' of undefined/i + ) + expect(JSON.parse($('#server-runtime-config').text())).toEqual( + config.serverRuntimeConfig || {} + ) + expect(JSON.parse($('#public-runtime-config').text())).toEqual( + config.publicRuntimeConfig || {} + ) +} + +describe('should work with runtime-config in next.config.js', () => { + test('empty runtime-config', async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { + } + ` + ) + + await runApp({}) + }) + + test('with runtime-config', async () => { + const config = { + serverRuntimeConfig: { + mySecret: '**********', + }, + publicRuntimeConfig: { + staticFolder: '/static', + }, + } + + await fs.writeFile( + nextConfig, + ` + module.exports = ${JSON.stringify(config)} + ` + ) + + await runApp(config) + }) +}) diff --git a/test/production/export/index.test.ts b/test/production/export/index.test.ts index 7c649137f423..336de74d7711 100644 --- a/test/production/export/index.test.ts +++ b/test/production/export/index.test.ts @@ -185,7 +185,7 @@ describe('static export', () => { .elementByCss('#about-page p') .text() - expect(text).toBe('This is the About page') + expect(text).toBe('This is the About page foo') await browser.close() }) @@ -198,7 +198,7 @@ describe('static export', () => { .elementByCss('#about-page p') .text() - expect(text).toBe('This is the About page') + expect(text).toBe('This is the About page foo') await browser.close() }) @@ -396,7 +396,7 @@ describe('static export', () => { it('should render the about page', async () => { const html = await renderViaHTTP(port, '/about') - expect(html).toMatch(/This is the About page/) + expect(html).toMatch(/This is the About page foobar/) }) it('should render links correctly', async () => { @@ -456,7 +456,7 @@ describe('static export', () => { it('Should serve public files', async () => { const html = await renderViaHTTP(port, '/about') const data = await renderViaHTTP(port, '/about/data.txt') - expect(html).toMatch(/This is the About page/) + expect(html).toMatch(/This is the About page foobar/) expect(data).toBe('data') }) diff --git a/test/production/export/next.config.js b/test/production/export/next.config.js index ae6fca6d4790..65a64af92c9c 100644 --- a/test/production/export/next.config.js +++ b/test/production/export/next.config.js @@ -2,6 +2,12 @@ module.exports = (phase) => { return { output: 'export', distDir: 'out', + publicRuntimeConfig: { + foo: 'foo', + }, + serverRuntimeConfig: { + bar: 'bar', + }, trailingSlash: true, exportPathMap: function () { return { diff --git a/test/production/export/pages/about.js b/test/production/export/pages/about.js index 4db3d07cdaa5..409d9407ca92 100644 --- a/test/production/export/pages/about.js +++ b/test/production/export/pages/about.js @@ -1,12 +1,18 @@ import Link from 'next/link' +import getConfig from 'next/config' +const { publicRuntimeConfig, serverRuntimeConfig } = getConfig() -const About = () => ( +const About = ({ bar }) => (
Go Back
-

This is the About page

+

{`This is the About page ${publicRuntimeConfig.foo}${bar || ''}`}

) +About.getInitialProps = async (ctx) => { + return { bar: serverRuntimeConfig.bar } +} + export default About diff --git a/test/production/next-server-nft/next-server-nft.test.ts b/test/production/next-server-nft/next-server-nft.test.ts index 8650e16a783d..14a181d150bc 100644 --- a/test/production/next-server-nft/next-server-nft.test.ts +++ b/test/production/next-server-nft/next-server-nft.test.ts @@ -309,6 +309,7 @@ const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18 "/node_modules/next/dist/shared/lib/is-plain-object.js", "/node_modules/next/dist/shared/lib/is-thenable.js", "/node_modules/next/dist/shared/lib/no-fallback-error.external.js", + "/node_modules/next/dist/shared/lib/runtime-config.external.js", "/node_modules/next/dist/shared/lib/server-reference-info.js", "/node_modules/react/cjs/react.production.js", "/node_modules/react/index.js", diff --git a/test/production/pages-dir/production/fixture/pages/runtime-config.js b/test/production/pages-dir/production/fixture/pages/runtime-config.js new file mode 100644 index 000000000000..dae6b687bc25 --- /dev/null +++ b/test/production/pages-dir/production/fixture/pages/runtime-config.js @@ -0,0 +1,16 @@ +import getConfig from 'next/config' + +const page = () => { + const { publicRuntimeConfig, serverRuntimeConfig } = getConfig() + + return ( + <> + {publicRuntimeConfig &&

found public config

} + {serverRuntimeConfig &&

found server config

} + + ) +} + +page.getInitialProps = () => ({}) + +export default page diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts index 8e28e31f8cf6..7746950814ca 100644 --- a/test/production/pages-dir/production/test/index.test.ts +++ b/test/production/pages-dir/production/test/index.test.ts @@ -938,6 +938,19 @@ describe('Production Usage', () => { }) } + it('should have default runtime values when not defined', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + expect(html).toMatch(/found public config/) + expect(html).toMatch(/found server config/) + }) + + it('should not have runtimeConfig in __NEXT_DATA__', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + const $ = cheerio.load(html) + const script = $('#__NEXT_DATA__').html() + expect(script).not.toMatch(/runtimeConfig/) + }) + it('should add autoExport for auto pre-rendered pages', async () => { for (const page of ['/about']) { const html = await renderViaHTTP(next.appPort, page) diff --git a/test/production/typescript-basic/typechecking/index.ts b/test/production/typescript-basic/typechecking/index.ts index cf488af24c4d..69404ba3014e 100644 --- a/test/production/typescript-basic/typechecking/index.ts +++ b/test/production/typescript-basic/typechecking/index.ts @@ -4,6 +4,7 @@ import 'next/app' // import 'next/babel'; import 'next/cache' import 'next/client' +import 'next/config' import 'next/constants' import 'next/document' import 'next/dynamic' diff --git a/test/rspack-build-tests-manifest.json b/test/rspack-build-tests-manifest.json index b4f97ff073eb..9b2d949a30e0 100644 --- a/test/rspack-build-tests-manifest.json +++ b/test/rspack-build-tests-manifest.json @@ -12397,6 +12397,16 @@ "flakey": [], "runtimeError": false }, + "test/integration/development-runtime-config/test/index.test.js": { + "passed": [ + "should work with runtime-config in next.config.js empty runtime-config", + "should work with runtime-config in next.config.js with runtime-config" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/integration/disable-js/test/index.test.js": { "passed": [ "disabled runtime JS production mode should not have __NEXT_DATA__ script", @@ -20975,6 +20985,7 @@ "Production Usage Misc should handle already finished responses", "Production Usage Misc should have default runtime values when not defined", "Production Usage Misc should not add autoExport for non pre-rendered pages", + "Production Usage Misc should not have runtimeConfig in __NEXT_DATA__", "Production Usage Misc should reload the page on page script error", "Production Usage Misc should reload the page on page script error with prefetch", "Production Usage Runtime errors should call getInitialProps on _error page during a client side component error", diff --git a/test/rspack-dev-tests-manifest.json b/test/rspack-dev-tests-manifest.json index c73345de9787..b44d9aa47e1c 100644 --- a/test/rspack-dev-tests-manifest.json +++ b/test/rspack-dev-tests-manifest.json @@ -14145,6 +14145,16 @@ "flakey": [], "runtimeError": false }, + "test/integration/development-runtime-config/test/index.test.js": { + "passed": [ + "should work with runtime-config in next.config.js empty runtime-config", + "should work with runtime-config in next.config.js with runtime-config" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/integration/disable-js/test/index.test.js": { "passed": [ "disabled runtime JS development mode should have a script for each preload link", diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index b1437fd2e5d8..19d1eed4bf81 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -11002,6 +11002,16 @@ "flakey": [], "runtimeError": false }, + "test/integration/development-runtime-config/test/index.test.js": { + "passed": [ + "should work with runtime-config in next.config.js empty runtime-config", + "should work with runtime-config in next.config.js with runtime-config" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/integration/disable-js/test/index.test.js": { "passed": [ "disabled runtime JS production mode should not have __NEXT_DATA__ script", @@ -19406,6 +19416,7 @@ "Production Usage Misc should handle already finished responses", "Production Usage Misc should have default runtime values when not defined", "Production Usage Misc should not add autoExport for non pre-rendered pages", + "Production Usage Misc should not have runtimeConfig in __NEXT_DATA__", "Production Usage Misc should reload the page on page script error", "Production Usage Misc should reload the page on page script error with prefetch", "Production Usage Runtime errors should call getInitialProps on _error page during a client side component error", diff --git a/test/turbopack-dev-tests-manifest.json b/test/turbopack-dev-tests-manifest.json index 5be96a962eea..f97e93e183f2 100644 --- a/test/turbopack-dev-tests-manifest.json +++ b/test/turbopack-dev-tests-manifest.json @@ -14549,6 +14549,16 @@ "flakey": [], "runtimeError": false }, + "test/integration/development-runtime-config/test/index.test.js": { + "passed": [ + "should work with runtime-config in next.config.js empty runtime-config", + "should work with runtime-config in next.config.js with runtime-config" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/integration/disable-js/test/index.test.js": { "passed": [ "disabled runtime JS development mode should have a script for each preload link", diff --git a/turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/webpack-wrapper-strs-namespaces-large/input.js b/turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/webpack-wrapper-strs-namespaces-large/input.js index 770b95f55bcd..b1699cce82ad 100644 --- a/turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/webpack-wrapper-strs-namespaces-large/input.js +++ b/turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/webpack-wrapper-strs-namespaces-large/input.js @@ -2735,6 +2735,7 @@ module.exports = /******/ (function (modules) { const { processEnv } = __webpack_require__('4VNc') processEnv([]) + const runtimeConfig = {} const { parse } = __webpack_require__('bzos') const { parse: parseQs } = __webpack_require__('8xkj') const { renderToHTML } = __webpack_require__('KK5V') @@ -2821,6 +2822,7 @@ module.exports = /******/ (function (modules) { canonicalBase: '', buildId: 'xXPgpxsbhoEyVP2leRiec', assetPrefix: '', + runtimeConfig: runtimeConfig.publicRuntimeConfig || {}, previewProps: { previewModeId: '9e0f19a11fe1be22878bdb16da136d9e', previewModeSigningKey: