diff --git a/errors/serverless-publicRuntimeConfig.md b/errors/serverless-publicRuntimeConfig.md deleted file mode 100644 index 800a1e664049a26..000000000000000 --- a/errors/serverless-publicRuntimeConfig.md +++ /dev/null @@ -1,23 +0,0 @@ -# Using `publicRuntimeConfig` or `serverRuntimeConfig` with `target` set to `serverless` - -#### Why This Error Occurred - -In the `serverless` target environment `next.config.js` is not loaded, so we don't support `publicRuntimeConfig` or `serverRuntimeConfig`. - -#### Possible Ways to Fix It - -Use config option `env` to set **build time** variables like such: - -```js -// next.config.js -module.exports = { - env: { - special: 'value', - }, -} -``` - -```js -// pages/index.js -console.log(process.env.special) // value -``` diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index a518b7b9877c69c..c8385cfa4d818ee 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -67,6 +67,10 @@ export function createEntrypoints( const client: WebpackEntrypoints = {} const server: WebpackEntrypoints = {} + const hasRuntimeConfig = + Object.keys(config.publicRuntimeConfig).length > 0 || + Object.keys(config.serverRuntimeConfig).length > 0 + const defaultServerlessOptions = { absoluteAppPath: pages['/_app'], absoluteDocumentPath: pages['/_document'], @@ -77,6 +81,12 @@ export function createEntrypoints( generateEtags: config.generateEtags, canonicalBase: config.canonicalBase, basePath: config.experimental.basePath, + runtimeConfig: hasRuntimeConfig + ? JSON.stringify({ + publicRuntimeConfig: config.publicRuntimeConfig, + serverRuntimeConfig: config.serverRuntimeConfig, + }) + : '', } Object.keys(pages).forEach(page => { diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 9db7d8c93dbf6ea..d90d7ef0e69126c 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -22,6 +22,7 @@ export type ServerlessLoaderQuery = { generateEtags: string canonicalBase: string basePath: string + runtimeConfig: string } const nextServerlessLoader: loader.Loader = function() { @@ -37,6 +38,7 @@ const nextServerlessLoader: loader.Loader = function() { absoluteErrorPath, generateEtags, basePath, + runtimeConfig, }: ServerlessLoaderQuery = typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query @@ -50,6 +52,19 @@ const nextServerlessLoader: loader.Loader = function() { const escapedBuildId = escapeRegexp(buildId) const pageIsDynamicRoute = isDynamicRoute(page) + const runtimeConfigImports = runtimeConfig + ? ` + import { setConfig } from 'next/dist/next-server/lib/runtime-config' + ` + : '' + + const runtimeConfigSetter = runtimeConfig + ? ` + const runtimeConfig = ${runtimeConfig} + setConfig(runtimeConfig) + ` + : 'const runtimeConfig = {}' + const dynamicRouteImports = pageIsDynamicRoute ? ` import { getRouteMatcher } from 'next/dist/next-server/lib/router/utils/route-matcher'; @@ -123,9 +138,14 @@ const nextServerlessLoader: loader.Loader = function() { import initServer from 'next-plugin-loader?middleware=on-init-server!' import onError from 'next-plugin-loader?middleware=on-error-server!' ${rewriteImports} + ${runtimeConfigImports} ${dynamicRouteMatcher} ${handleRewrites} + ${ + /* this needs to be called before importing the API method */ + runtimeConfigSetter + } export default async (req, res) => { try { @@ -177,9 +197,16 @@ const nextServerlessLoader: loader.Loader = function() { import Document from '${absoluteDocumentPath}'; import Error from '${absoluteErrorPath}'; import App from '${absoluteAppPath}'; - import * as ComponentInfo from '${absolutePagePath}'; ${dynamicRouteImports} ${rewriteImports} + ${runtimeConfigImports} + + ${ + /* this needs to be called before importing the component */ + runtimeConfigSetter + } + + const ComponentInfo = require('${absolutePagePath}') const Component = ComponentInfo.default export default Component @@ -214,6 +241,7 @@ const nextServerlessLoader: loader.Loader = function() { canonicalBase: "${canonicalBase}", buildId: "${buildId}", assetPrefix: "${assetPrefix}", + runtimeConfig: runtimeConfig.publicRuntimeConfig || {}, ..._renderOpts } let _nextData = false diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index eee2087ff1f03e7..ad975b8414d6471 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -254,20 +254,6 @@ export default function loadConfig( : canonicalBase) || '' } - if ( - userConfig.target && - userConfig.target !== 'server' && - ((userConfig.publicRuntimeConfig && - Object.keys(userConfig.publicRuntimeConfig).length !== 0) || - (userConfig.serverRuntimeConfig && - Object.keys(userConfig.serverRuntimeConfig).length !== 0)) - ) { - // TODO: change error message tone to "Only compatible with [fat] server mode" - throw new Error( - 'Cannot use publicRuntimeConfig or serverRuntimeConfig with target=serverless https://err.sh/zeit/next.js/serverless-publicRuntimeConfig' - ) - } - if ( userConfig.experimental?.reactMode && !reactModes.includes(userConfig.experimental.reactMode) diff --git a/test/integration/handle-non-page-in-pages/test/index.test.js b/test/integration/handle-non-page-in-pages/test/index.test.js index 10cb24fc467b48c..b18c13ea451aa77 100644 --- a/test/integration/handle-non-page-in-pages/test/index.test.js +++ b/test/integration/handle-non-page-in-pages/test/index.test.js @@ -11,7 +11,7 @@ describe('Handle non-page in pages when target: serverless', () => { const { stderr } = await nextBuild(appDir, [], { stderr: true }) expect(stderr).toMatch( - /webpack build failed: found page without a React Component as default export in/ + /found page without a React Component as default export in/ ) expect(stderr).toMatch(/pages\/invalid/) }) diff --git a/test/integration/serverless-runtime-configs/pages/config.js b/test/integration/serverless-runtime-configs/pages/config.js new file mode 100644 index 000000000000000..45ace3c4a933763 --- /dev/null +++ b/test/integration/serverless-runtime-configs/pages/config.js @@ -0,0 +1,5 @@ +import getConfig from 'next/config' + +const config = getConfig() + +export default () =>

{JSON.stringify(config)}

diff --git a/test/integration/serverless-runtime-configs/test/index.test.js b/test/integration/serverless-runtime-configs/test/index.test.js index d14e63635272157..81d79a75a2e9222 100644 --- a/test/integration/serverless-runtime-configs/test/index.test.js +++ b/test/integration/serverless-runtime-configs/test/index.test.js @@ -2,7 +2,15 @@ /* global jasmine */ import fs from 'fs-extra' import { join } from 'path' -import { nextBuild } from 'next-test-utils' +import { + nextBuild, + findPort, + nextStart, + killApp, + renderViaHTTP, +} from 'next-test-utils' +import cheerio from 'cheerio' +import webdriver from 'next-webdriver' const appDir = join(__dirname, '../') const nextConfigPath = join(appDir, 'next.config.js') @@ -14,7 +22,7 @@ describe('Serverless runtime configs', () => { beforeAll(() => cleanUp()) afterAll(() => cleanUp()) - it('should error on usage of publicRuntimeConfig', async () => { + it('should not error on usage of publicRuntimeConfig', async () => { await fs.writeFile( nextConfigPath, `module.exports = { @@ -25,13 +33,16 @@ describe('Serverless runtime configs', () => { }` ) - const { stderr } = await nextBuild(appDir, undefined, { stderr: true }) - expect(stderr).toMatch( + const { stderr, code } = await nextBuild(appDir, undefined, { + stderr: true, + }) + expect(code).toBe(0) + expect(stderr).not.toMatch( /Cannot use publicRuntimeConfig or serverRuntimeConfig/ ) }) - it('should error on usage of serverRuntimeConfig', async () => { + it('should not error on usage of serverRuntimeConfig', async () => { await fs.writeFile( nextConfigPath, `module.exports = { @@ -42,9 +53,63 @@ describe('Serverless runtime configs', () => { }` ) - const { stderr } = await nextBuild(appDir, undefined, { stderr: true }) - expect(stderr).toMatch( + const { stderr, code } = await nextBuild(appDir, undefined, { + stderr: true, + }) + expect(code).toBe(0) + expect(stderr).not.toMatch( /Cannot use publicRuntimeConfig or serverRuntimeConfig/ ) }) + + it('should support runtime configs in serverless mode', async () => { + await fs.writeFile( + nextConfigPath, + `module.exports = { + target: 'serverless', + serverRuntimeConfig: { + hello: 'world' + }, + publicRuntimeConfig: { + another: 'thing' + } + }` + ) + + await nextBuild(appDir, [], { stderr: true, stdout: true }) + const appPort = await findPort() + const app = await nextStart(appDir, appPort, { + onStdout: console.log, + onStderr: console.log, + }) + + const browser = await webdriver(appPort, '/config') + + const clientHTML = await browser.eval(`document.documentElement.innerHTML`) + const ssrHTML = await renderViaHTTP(appPort, '/config') + + await killApp(app) + await fs.remove(nextConfigPath) + + const ssr$ = cheerio.load(ssrHTML) + const client$ = cheerio.load(clientHTML) + + const ssrConfig = ssr$('#config').text() + const clientConfig = client$('#config').text() + + expect(JSON.parse(ssrConfig)).toEqual({ + publicRuntimeConfig: { + another: 'thing', + }, + serverRuntimeConfig: { + hello: 'world', + }, + }) + expect(JSON.parse(clientConfig)).toEqual({ + publicRuntimeConfig: { + another: 'thing', + }, + serverRuntimeConfig: {}, + }) + }) })