diff --git a/errors/missing-env-value.md b/errors/missing-env-value.md new file mode 100644 index 000000000000000..7c2d3865f4ef0b7 --- /dev/null +++ b/errors/missing-env-value.md @@ -0,0 +1,28 @@ +# Missing Env Value + +#### Why This Error Occurred + +One of your pages' config requested an env value that wasn't populated. + +```js +// pages/index.js +export const config = { + // this value isn't provided in `.env` + env: ['MISSING_KEY'], +} +``` + +``` +// .env (notice no `MISSING_KEY` provided here) +NOTION_KEY='...' +``` + +#### Possible Ways to Fix It + +Either remove the requested env value from the page's config, populate it in your `.env` file, or manually populate it in your environment before running `next dev` or `next build`. + +### Useful Links + +- [dotenv](https://npmjs.com/package/dotenv) +- [dotenv-expand](https://npmjs.com/package/dotenv-expand) +- [Environment Variables](https://en.wikipedia.org/wiki/Environment_variable) diff --git a/packages/create-next-app/templates/default/gitignore b/packages/create-next-app/templates/default/gitignore index 922d92a5745e872..20fccdd4b84d99e 100644 --- a/packages/create-next-app/templates/default/gitignore +++ b/packages/create-next-app/templates/default/gitignore @@ -17,9 +17,14 @@ # misc .DS_Store -.env* # debug npm-debug.log* yarn-debug.log* yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5f9f3f1ccc21785..196aff141ed9762 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -71,6 +71,7 @@ import { } from './utils' import getBaseWebpackConfig from './webpack-config' import { writeBuildId } from './write-build-id' +import { loadEnvConfig } from '../lib/load-env-config' const fsAccess = promisify(fs.access) const fsUnlink = promisify(fs.unlink) @@ -110,6 +111,9 @@ export default async function build(dir: string, conf = null): Promise { ) } + // attempt to load global env values so they are available in next.config.js + loadEnvConfig(dir) + const config = loadConfig(PHASE_PRODUCTION_BUILD, dir, conf) const { target } = config const buildId = await generateBuildId(config.generateBuildId, nanoid) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 1eb1f4548efcdcf..1660209d9bcbf15 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -707,6 +707,17 @@ export default async function getBaseWebpackConfig( // This plugin makes sure `output.filename` is used for entry chunks new ChunkNamesPlugin(), new webpack.DefinePlugin({ + ...(config.experimental.pageEnv + ? Object.keys(process.env).reduce( + (prev: { [key: string]: string }, key: string) => { + if (key.startsWith('NEXT_APP_')) { + prev[key] = process.env[key]! + } + return prev + }, + {} + ) + : {}), ...Object.keys(config.env).reduce((acc, key) => { if (/^(?:NODE_.+)|^(?:__.+)$/i.test(key)) { throw new Error( diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index e172619f152f327..865e419e64ebd92 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -181,6 +181,7 @@ const nextServerlessLoader: loader.Loader = function() { Object.assign({}, parsedUrl.query, params ), resolver, ${encodedPreviewProps}, + process.env, onError ) } catch (err) { @@ -257,6 +258,7 @@ const nextServerlessLoader: loader.Loader = function() { assetPrefix: "${assetPrefix}", runtimeConfig: runtimeConfig.publicRuntimeConfig || {}, previewProps: ${encodedPreviewProps}, + env: process.env, ..._renderOpts } let _nextData = false diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 40c740a29449bc5..8ce745a3a69208a 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -35,6 +35,7 @@ import loadConfig, { import { eventCliSession } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { normalizePagePath } from '../next-server/server/normalize-page-path' +import { loadEnvConfig } from '../lib/load-env-config' const copyFile = promisify(copyFileOrig) const mkdir = promisify(mkdirOrig) @@ -230,6 +231,7 @@ export default async function( dir, buildId, nextExport: true, + env: loadEnvConfig(dir), assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''), distDir, dev: false, diff --git a/packages/next/lib/find-pages-dir.ts b/packages/next/lib/find-pages-dir.ts index 572bae22616e4a6..a4acde163903835 100644 --- a/packages/next/lib/find-pages-dir.ts +++ b/packages/next/lib/find-pages-dir.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -const existsSync = (f: string): boolean => { +export const existsSync = (f: string): boolean => { try { fs.accessSync(f, fs.constants.F_OK) return true diff --git a/packages/next/lib/load-env-config.ts b/packages/next/lib/load-env-config.ts new file mode 100644 index 000000000000000..4178f956ae63bc7 --- /dev/null +++ b/packages/next/lib/load-env-config.ts @@ -0,0 +1,84 @@ +import fs from 'fs' +import path from 'path' +import chalk from 'chalk' +import dotenvExpand from 'next/dist/compiled/dotenv-expand' +import dotenv, { DotenvConfigOutput } from 'next/dist/compiled/dotenv' +import findUp from 'find-up' + +export type Env = { [key: string]: string } + +export function loadEnvConfig(dir: string, dev?: boolean): Env | false { + const packageJson = findUp.sync('package.json', { cwd: dir }) + + // only do new env loading if dotenv isn't installed since we + // can't check for an experimental flag in next.config.js + // since we want to load the env before loading next.config.js + if (packageJson) { + const { dependencies, devDependencies } = require(packageJson) + const allPackages = Object.keys({ + ...dependencies, + ...devDependencies, + }) + + if (allPackages.some(pkg => pkg === 'dotenv')) { + return false + } + } else { + // we should always have a package.json but disable in case we don't + return false + } + + const isTest = process.env.NODE_ENV === 'test' + const mode = isTest ? 'test' : dev ? 'development' : 'production' + const dotenvFiles = [ + `.env.${mode}.local`, + `.env.${mode}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + mode !== 'test' && `.env.local`, + '.env', + ].filter(Boolean) as string[] + + const combinedEnv: Env = { + ...(process.env as any), + } + + for (const envFile of dotenvFiles) { + // only load .env if the user provided has an env config file + const dotEnvPath = path.join(dir, envFile) + + try { + const contents = fs.readFileSync(dotEnvPath, 'utf8') + let result: DotenvConfigOutput = {} + result.parsed = dotenv.parse(contents) + + result = dotenvExpand(result) + + if (result.parsed) { + console.log(`> ${chalk.cyan.bold('Info:')} Loaded env from ${envFile}`) + } + + Object.assign(combinedEnv, result.parsed) + } catch (err) { + if (err.code !== 'ENOENT') { + console.log( + `> ${chalk.cyan.bold('Error: ')} Failed to load env from ${envFile}`, + err + ) + } + } + } + + // load global env values prefixed with `NEXT_APP_` to process.env + for (const key of Object.keys(combinedEnv)) { + if ( + key.startsWith('NEXT_APP_') && + typeof process.env[key] === 'undefined' + ) { + process.env[key] = combinedEnv[key] + } + } + + return combinedEnv +} diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index df10ab42355d81f..cda428cb9a05077 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -4,6 +4,7 @@ import { ComponentType } from 'react' import { format, URLFormatOptions, UrlObject } from 'url' import { ManifestItem } from '../server/load-components' import { NextRouter } from './router/router' +import { Env } from '../../lib/load-env-config' /** * Types used by both next and next-server @@ -186,6 +187,8 @@ export type NextApiRequest = IncomingMessage & { } body: any + + env: Env } /** diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index a6bbb234d67c790..5d7a6b166e6449e 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -8,6 +8,8 @@ import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils' import { decryptWithSecret, encryptWithSecret } from './crypto-utils' import { interopDefault } from './load-components' import { Params } from './router' +import { collectEnv } from './utils' +import { Env } from '../../lib/load-env-config' export type NextApiRequestCookies = { [key: string]: string } export type NextApiRequestQuery = { [key: string]: string | string[] } @@ -24,26 +26,23 @@ export async function apiResolver( params: any, resolverModule: any, apiContext: __ApiPreviewProps, + env: Env | false, onError?: ({ err }: { err: any }) => Promise ) { const apiReq = req as NextApiRequest const apiRes = res as NextApiResponse try { - let config: PageConfig = {} - let bodyParser = true if (!resolverModule) { res.statusCode = 404 res.end('Not Found') return } + const config: PageConfig = resolverModule.config || {} + const bodyParser = config.api?.bodyParser !== false + + apiReq.env = env ? collectEnv(req.url!, env, config.env) : {} - if (resolverModule.config) { - config = resolverModule.config - if (config.api && config.api.bodyParser === false) { - bodyParser = false - } - } // Parsing of cookies setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req)) // Parsing query string diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 69e73235c93ba28..09c722539d51a14 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -54,6 +54,7 @@ const defaultConfig: { [key: string]: any } = { workerThreads: false, basePath: '', sassOptions: {}, + pageEnv: false, }, future: { excludeDefaultMomentLocales: false, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index eeeb81c5737c7f1..c9311e325ec9bb8 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -61,6 +61,7 @@ import { setSprCache, } from './spr-cache' import { isBlockedPage } from './utils' +import { loadEnvConfig, Env } from '../../lib/load-env-config' const getCustomRouteMatcher = pathMatch(true) @@ -117,6 +118,7 @@ export default class Server { documentMiddlewareEnabled: boolean hasCssMode: boolean dev?: boolean + env: Env | false previewProps: __ApiPreviewProps customServer?: boolean ampOptimizerConfig?: { [key: string]: any } @@ -145,6 +147,8 @@ export default class Server { this.dir = resolve(dir) this.quiet = quiet const phase = this.currentPhase() + const env = loadEnvConfig(this.dir, dev) + this.nextConfig = loadConfig(phase, this.dir, conf) this.distDir = join(this.dir, this.nextConfig.distDir) this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH) @@ -171,6 +175,7 @@ export default class Server { staticMarkup, buildId: this.buildId, generateEtags, + env: this.nextConfig.experimental.pageEnv && env, previewProps: this.getPreviewProps(), customServer: customServer === true ? true : undefined, ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer, @@ -684,6 +689,7 @@ export default class Server { query, pageModule, this.renderOpts.previewProps, + this.renderOpts.env, this.onErrorMiddleware ) return true diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 9919c5ef4e7d875..787e538acb0cb73 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -38,6 +38,8 @@ import { tryGetPreviewData, __ApiPreviewProps } from './api-utils' import { getPageFiles } from './get-page-files' import { LoadComponentsReturnType, ManifestItem } from './load-components' import optimizeAmp from './optimize-amp' +import { collectEnv } from './utils' +import { Env } from '../../lib/load-env-config' import { UnwrapPromise } from '../../lib/coalesced-function' import { GetStaticProps, GetServerSideProps } from '../../types' @@ -154,6 +156,7 @@ export type RenderOptsPartial = { isDataReq?: boolean params?: ParsedUrlQuery previewProps: __ApiPreviewProps + env: Env | false } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial @@ -288,6 +291,7 @@ export async function renderToHTML( staticMarkup = false, ampPath = '', App, + env = {}, Document, pageConfig = {}, DocumentMiddleware, @@ -303,6 +307,8 @@ export async function renderToHTML( previewProps, } = renderOpts + const curEnv = env ? collectEnv(pathname, env, pageConfig.env) : {} + const callMiddleware = async (method: string, args: any[], props = false) => { let results: any = props ? {} : [] @@ -503,6 +509,7 @@ export async function renderToHTML( try { data = await getStaticProps!({ + env: curEnv, ...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined), ...(previewData !== false ? { preview: true, previewData: previewData } @@ -585,6 +592,7 @@ export async function renderToHTML( req, res, query, + env: curEnv, ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined), ...(previewData !== false ? { preview: true, previewData: previewData } diff --git a/packages/next/next-server/server/utils.ts b/packages/next/next-server/server/utils.ts index 452fa42091faf2d..9465f87e5d7daf6 100644 --- a/packages/next/next-server/server/utils.ts +++ b/packages/next/next-server/server/utils.ts @@ -1,4 +1,5 @@ import { BLOCKED_PAGES } from '../lib/constants' +import { Env } from '../../lib/load-env-config' export function isBlockedPage(pathname: string): boolean { return BLOCKED_PAGES.indexOf(pathname) !== -1 @@ -14,3 +15,28 @@ export function cleanAmpPath(pathname: string): string { pathname = pathname.replace(/\?$/, '') return pathname } + +export function collectEnv(page: string, env: Env, pageEnv?: string[]): Env { + const missingEnvKeys = new Set() + const collected = pageEnv + ? pageEnv.reduce((prev: Env, key): Env => { + if (typeof env[key] !== 'undefined') { + prev[key] = env[key]! + } else { + missingEnvKeys.add(key) + } + return prev + }, {}) + : {} + + if (missingEnvKeys.size > 0) { + console.warn( + `Missing env value${missingEnvKeys.size === 1 ? '' : 's'}: ${[ + ...missingEnvKeys, + ].join(', ')} for ${page}.\n` + + `Make sure to supply this value in either your .env file or in your environment.\n` + + `See here for more info: https://err.sh/next.js/missing-env-value` + ) + } + return collected +} diff --git a/packages/next/package.json b/packages/next/package.json index 96ef816cc8f4e9f..47a1fff2cc11053 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -166,6 +166,7 @@ "@types/content-type": "1.1.3", "@types/cookie": "0.3.2", "@types/cross-spawn": "6.0.0", + "@types/dotenv": "8.2.0", "@types/etag": "1.8.0", "@types/find-up": "2.1.1", "@types/fresh": "0.5.0", @@ -191,6 +192,8 @@ "arg": "4.1.0", "ast-types": "0.13.2", "babel-plugin-dynamic-import-node": "2.3.0", + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", "nanoid": "2.0.3", "resolve": "1.11.0", "taskr": "1.1.0", diff --git a/packages/next/taskfile-ncc.js b/packages/next/taskfile-ncc.js index de0b2bcda378280..a7ae3a96f4fc4ce 100644 --- a/packages/next/taskfile-ncc.js +++ b/packages/next/taskfile-ncc.js @@ -40,6 +40,11 @@ function writePackageManifest(packageName) { let typesFile = types || typings if (typesFile) { + // if they provide a types directory resolve it + if (extname(typesFile) === '') { + typesFile = join(typesFile, 'index.d.ts') + } + typesFile = require.resolve(join(packageName, typesFile)) } else { try { diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index dddbf07cdd2b890..9e1dcd5c9e2b318 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -41,6 +41,22 @@ export async function ncc_text_table(task, opts) { .target('dist/compiled/text-table') } +// eslint-disable-next-line camelcase +export async function ncc_dotenv(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('dotenv'))) + .ncc({ packageName: 'dotenv' }) + .target('dist/compiled/dotenv') +} + +// eslint-disable-next-line camelcase +export async function ncc_dotenv_expand(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('dotenv-expand'))) + .ncc({ packageName: 'dotenv-expand' }) + .target('dist/compiled/dotenv-expand') +} + export async function precompile(task) { await task.parallel([ 'ncc_unistore', @@ -48,6 +64,8 @@ export async function precompile(task) { 'ncc_arg', 'ncc_nanoid', 'ncc_text_table', + 'ncc_dotenv', + 'ncc_dotenv_expand', ]) } diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 08767b0d1956489..271e1b532d2781d 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -5,6 +5,7 @@ import React from 'react' import { ParsedUrlQuery } from 'querystring' import { IncomingMessage, ServerResponse } from 'http' +import { Env } from '../lib/load-env-config' import { NextPageContext, @@ -54,6 +55,7 @@ export type PageConfig = { */ bodyParser?: { sizeLimit?: number | string } | false } + env?: Array } export { @@ -70,6 +72,7 @@ export type GetStaticProps< params?: ParsedUrlQuery preview?: boolean previewData?: any + env: Env }) => Promise<{ props: P revalidate?: number | boolean @@ -87,6 +90,7 @@ export type GetServerSideProps< res: ServerResponse params?: ParsedUrlQuery query: ParsedUrlQuery + env: Env preview?: boolean previewData?: any }) => Promise<{ props: P }> diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index ec0f290ed7a033a..28d89be09371a8e 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -43,6 +43,12 @@ declare module 'next/dist/compiled/text-table' { export = textTable } +declare module 'next/dist/compiled/dotenv' { + import dotenv from 'dotenv' + + export = dotenv +} + declare module 'next/dist/compiled/arg/index.js' { function arg( spec: T, diff --git a/test/integration/env-config-errors/app/package.json b/test/integration/env-config-errors/app/package.json new file mode 100644 index 000000000000000..9f2c5a7558e76b9 --- /dev/null +++ b/test/integration/env-config-errors/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "env-config-errors" +} diff --git a/test/integration/env-config-errors/app/pages/api/hello.js b/test/integration/env-config-errors/app/pages/api/hello.js new file mode 100644 index 000000000000000..23d11b18a6fc8d7 --- /dev/null +++ b/test/integration/env-config-errors/app/pages/api/hello.js @@ -0,0 +1,5 @@ +export const config = { + env: ['NOTION_KEY', 'SENTRY_DSN', 'DATABASE_KEY', 'DATABASE_USER'], +} + +export default (req, res) => res.json(req.env) diff --git a/test/integration/env-config-errors/app/pages/index.js b/test/integration/env-config-errors/app/pages/index.js new file mode 100644 index 000000000000000..3a943794b2831f2 --- /dev/null +++ b/test/integration/env-config-errors/app/pages/index.js @@ -0,0 +1,14 @@ +export const config = { + env: ['NOTION_KEY', 'SENTRY_DSN', 'DATABASE_KEY', 'DATABASE_USER'], +} + +export async function getStaticProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + revalidate: 1, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config-errors/app/pages/ssp.js b/test/integration/env-config-errors/app/pages/ssp.js new file mode 100644 index 000000000000000..325dc7fc7743880 --- /dev/null +++ b/test/integration/env-config-errors/app/pages/ssp.js @@ -0,0 +1,13 @@ +export const config = { + env: ['NOTION_KEY', 'SENTRY_DSN', 'DATABASE_KEY', 'DATABASE_USER'], +} + +export async function getServerSideProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config-errors/test/index.test.js b/test/integration/env-config-errors/test/index.test.js new file mode 100644 index 000000000000000..899764abbd7650f --- /dev/null +++ b/test/integration/env-config-errors/test/index.test.js @@ -0,0 +1,150 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { join } from 'path' +import { + nextBuild, + findPort, + launchApp, + killApp, + nextStart, + renderViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '../app') +const envFile = join(appDir, '.env') +const nextConfig = join(appDir, 'next.config.js') +const nextConfigContent = ` + experimental: { + pageEnv: true + } +` + +let app +let appPort +let output = '' + +const envValues = ['NOTION_KEY', 'SENTRY_DSN', 'DATABASE_KEY', 'DATABASE_USER'] + +const writeEnv = () => + fs.writeFile(envFile, envValues.map(val => `${val}=value`).join('\n')) +const rmEnv = () => fs.remove(envFile) + +const runTests = (isDev = false) => { + const startApp = async () => { + output = '' + appPort = await findPort() + let method = isDev ? launchApp : nextStart + + app = await method(appDir, appPort, { + onStdout(msg) { + output += msg + }, + onStderr(msg) { + output += msg + }, + }) + } + + if (isDev) { + it('should warn for missing values on SSG page', async () => { + await startApp() + await renderViaHTTP(appPort, '/') + await killApp(app) + expect(output).toContain( + `Missing env values: ${envValues.join(', ')} for /` + ) + }) + + it('should not warn for missing values on SSG page', async () => { + await writeEnv() + await startApp() + await renderViaHTTP(appPort, '/') + await killApp(app) + await rmEnv() + expect(output).not.toContain( + `Missing env values: ${envValues.join(', ')} for /` + ) + }) + } + + it('should warn for missing values on server props page', async () => { + await startApp() + await renderViaHTTP(appPort, '/ssp') + await killApp(app) + expect(output).toContain( + `Missing env values: ${envValues.join(', ')} for /ssp` + ) + }) + + it('should not warn for missing values on server props page', async () => { + await writeEnv() + await startApp() + await renderViaHTTP(appPort, '/ssp') + await killApp(app) + await rmEnv() + expect(output).not.toContain( + `Missing env values: ${envValues.join(', ')} for /ssp` + ) + }) + + it('should warn for missing values on API route', async () => { + await startApp() + await renderViaHTTP(appPort, '/api/hello') + await killApp(app) + expect(output).toContain( + `Missing env values: ${envValues.join(', ')} for /api/hello` + ) + }) + + it('should not warn for missing values on API route', async () => { + await writeEnv() + await startApp() + await renderViaHTTP(appPort, '/api/hello') + await killApp(app) + await rmEnv() + expect(output).not.toContain( + `Missing env values: ${envValues.join(', ')} for /api/hello` + ) + }) +} + +describe('Env Config', () => { + afterEach(async () => { + await fs.remove(envFile) + try { + await killApp(app) + } catch (_) {} + }) + afterAll(() => fs.remove(nextConfig)) + + describe('dev mode', () => { + beforeAll(() => + fs.writeFile(nextConfig, `module.exports = { ${nextConfigContent} }`) + ) + runTests(true) + }) + + describe('server mode', () => { + beforeAll(async () => { + beforeAll(() => + fs.writeFile(nextConfig, `module.exports = { ${nextConfigContent} }`) + ) + await nextBuild(appDir) + }) + runTests() + }) + + describe('serverless mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { target: 'experimental-serverless-trace', ${nextConfigContent} }` + ) + await nextBuild(appDir) + }) + runTests() + }) +}) diff --git a/test/integration/env-config/app/.env b/test/integration/env-config/app/.env new file mode 100644 index 000000000000000..331912846cdfc1d --- /dev/null +++ b/test/integration/env-config/app/.env @@ -0,0 +1,10 @@ +PROCESS_ENV_KEY=env +ENV_FILE_KEY=env +ENV_FILE_LOCAL_OVERRIDE_TEST=env +ENV_FILE_DEVELOPMENT_OVERRIDE_TEST=env +ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST=env +ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST=env +ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST=env +ENV_FILE_TEST_OVERRIDE_TEST=env +ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST=env +NEXT_APP_TEST_DEST=another \ No newline at end of file diff --git a/test/integration/env-config/app/.env.development b/test/integration/env-config/app/.env.development new file mode 100644 index 000000000000000..556dd457c9470cf --- /dev/null +++ b/test/integration/env-config/app/.env.development @@ -0,0 +1,3 @@ +DEVELOPMENT_ENV_FILE_KEY=development +ENV_FILE_DEVELOPMENT_OVERRIDE_TEST=development +ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST=development \ No newline at end of file diff --git a/test/integration/env-config/app/.env.development.local b/test/integration/env-config/app/.env.development.local new file mode 100644 index 000000000000000..7666af047e62fb0 --- /dev/null +++ b/test/integration/env-config/app/.env.development.local @@ -0,0 +1,2 @@ +LOCAL_DEVELOPMENT_ENV_FILE_KEY=localdevelopment +ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST=localdevelopment \ No newline at end of file diff --git a/test/integration/env-config/app/.env.local b/test/integration/env-config/app/.env.local new file mode 100644 index 000000000000000..077c72362c54817 --- /dev/null +++ b/test/integration/env-config/app/.env.local @@ -0,0 +1,2 @@ +LOCAL_ENV_FILE_KEY=localenv +ENV_FILE_LOCAL_OVERRIDE_TEST=localenv \ No newline at end of file diff --git a/test/integration/env-config/app/.env.production b/test/integration/env-config/app/.env.production new file mode 100644 index 000000000000000..677a6066b195a63 --- /dev/null +++ b/test/integration/env-config/app/.env.production @@ -0,0 +1,3 @@ +PRODUCTION_ENV_FILE_KEY=production +ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST=production +ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST=production \ No newline at end of file diff --git a/test/integration/env-config/app/.env.production.local b/test/integration/env-config/app/.env.production.local new file mode 100644 index 000000000000000..01bd9dd821e2d7e --- /dev/null +++ b/test/integration/env-config/app/.env.production.local @@ -0,0 +1,2 @@ +LOCAL_PRODUCTION_ENV_FILE_KEY=localproduction +ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST=localproduction \ No newline at end of file diff --git a/test/integration/env-config/app/.env.test b/test/integration/env-config/app/.env.test new file mode 100644 index 000000000000000..fd4e537727b081f --- /dev/null +++ b/test/integration/env-config/app/.env.test @@ -0,0 +1,3 @@ +TEST_ENV_FILE_KEY=test +ENV_FILE_TEST_OVERRIDE_TEST=test +ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST=test \ No newline at end of file diff --git a/test/integration/env-config/app/.env.test.local b/test/integration/env-config/app/.env.test.local new file mode 100644 index 000000000000000..c613399451e30fe --- /dev/null +++ b/test/integration/env-config/app/.env.test.local @@ -0,0 +1,2 @@ +LOCAL_TEST_ENV_FILE_KEY=localtest +ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST=localtest \ No newline at end of file diff --git a/test/integration/env-config/app/package.json b/test/integration/env-config/app/package.json new file mode 100644 index 000000000000000..ffaa95af60f33d4 --- /dev/null +++ b/test/integration/env-config/app/package.json @@ -0,0 +1,4 @@ +{ + "name": "env-config", + "dependencies": {} +} diff --git a/test/integration/env-config/app/pages/api/all.js b/test/integration/env-config/app/pages/api/all.js new file mode 100644 index 000000000000000..9c6743b794eef03 --- /dev/null +++ b/test/integration/env-config/app/pages/api/all.js @@ -0,0 +1,25 @@ +export const config = { + env: [ + 'PROCESS_ENV_KEY', + 'ENV_FILE_KEY', + 'LOCAL_ENV_FILE_KEY', + 'ENV_FILE_LOCAL_OVERRIDE_TEST', + 'PRODUCTION_ENV_FILE_KEY', + 'LOCAL_PRODUCTION_ENV_FILE_KEY', + 'DEVELOPMENT_ENV_FILE_KEY', + 'LOCAL_DEVELOPMENT_ENV_FILE_KEY', + 'ENV_FILE_DEVELOPMENT_OVERRIDE_TEST', + 'ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST', + 'TEST_ENV_FILE_KEY', + 'LOCAL_TEST_ENV_FILE_KEY', + 'ENV_FILE_TEST_OVERRIDE_TEST', + 'ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST', + ], +} + +export default async (req, res) => { + // Only for testing, don't do this... + res.json(req.env) +} diff --git a/test/integration/env-config/app/pages/global.js b/test/integration/env-config/app/pages/global.js new file mode 100644 index 000000000000000..bb85ce9a0536272 --- /dev/null +++ b/test/integration/env-config/app/pages/global.js @@ -0,0 +1 @@ +export default () =>

{process.env.NEXT_APP_TEST_DEST}

diff --git a/test/integration/env-config/app/pages/index.js b/test/integration/env-config/app/pages/index.js new file mode 100644 index 000000000000000..a276f47118a2d06 --- /dev/null +++ b/test/integration/env-config/app/pages/index.js @@ -0,0 +1,31 @@ +export const config = { + env: [ + 'PROCESS_ENV_KEY', + 'ENV_FILE_KEY', + 'LOCAL_ENV_FILE_KEY', + 'ENV_FILE_LOCAL_OVERRIDE_TEST', + 'PRODUCTION_ENV_FILE_KEY', + 'LOCAL_PRODUCTION_ENV_FILE_KEY', + 'DEVELOPMENT_ENV_FILE_KEY', + 'LOCAL_DEVELOPMENT_ENV_FILE_KEY', + 'ENV_FILE_DEVELOPMENT_OVERRIDE_TEST', + 'ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST', + 'TEST_ENV_FILE_KEY', + 'LOCAL_TEST_ENV_FILE_KEY', + 'ENV_FILE_TEST_OVERRIDE_TEST', + 'ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST', + ], +} + +export async function getStaticProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + revalidate: 1, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config/app/pages/next-config-loaded-env.js b/test/integration/env-config/app/pages/next-config-loaded-env.js new file mode 100644 index 000000000000000..51e8a5a85f4b597 --- /dev/null +++ b/test/integration/env-config/app/pages/next-config-loaded-env.js @@ -0,0 +1,16 @@ +export default () => ( +

+ {JSON.stringify({ + LOCAL_ENV_FILE_KEY: process.env.NC_LOCAL_ENV_FILE_KEY, + ENV_FILE_KEY: process.env.NC_ENV_FILE_KEY, + PRODUCTION_ENV_FILE_KEY: process.env.NC_PRODUCTION_ENV_FILE_KEY, + LOCAL_PRODUCTION_ENV_FILE_KEY: + process.env.NC_LOCAL_PRODUCTION_ENV_FILE_KEY, + DEVELOPMENT_ENV_FILE_KEY: process.env.NC_DEVELOPMENT_ENV_FILE_KEY, + TEST_ENV_FILE_KEY: process.env.NC_TEST_ENV_FILE_KEY, + LOCAL_TEST_ENV_FILE_KEY: process.env.NC_LOCAL_TEST_ENV_FILE_KEY, + LOCAL_DEVELOPMENT_ENV_FILE_KEY: + process.env.NC_LOCAL_DEVELOPMENT_ENV_FILE_KEY, + })} +

+) diff --git a/test/integration/env-config/app/pages/some-ssg.js b/test/integration/env-config/app/pages/some-ssg.js new file mode 100644 index 000000000000000..a276f47118a2d06 --- /dev/null +++ b/test/integration/env-config/app/pages/some-ssg.js @@ -0,0 +1,31 @@ +export const config = { + env: [ + 'PROCESS_ENV_KEY', + 'ENV_FILE_KEY', + 'LOCAL_ENV_FILE_KEY', + 'ENV_FILE_LOCAL_OVERRIDE_TEST', + 'PRODUCTION_ENV_FILE_KEY', + 'LOCAL_PRODUCTION_ENV_FILE_KEY', + 'DEVELOPMENT_ENV_FILE_KEY', + 'LOCAL_DEVELOPMENT_ENV_FILE_KEY', + 'ENV_FILE_DEVELOPMENT_OVERRIDE_TEST', + 'ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST', + 'TEST_ENV_FILE_KEY', + 'LOCAL_TEST_ENV_FILE_KEY', + 'ENV_FILE_TEST_OVERRIDE_TEST', + 'ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST', + ], +} + +export async function getStaticProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + revalidate: 1, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config/app/pages/some-ssp.js b/test/integration/env-config/app/pages/some-ssp.js new file mode 100644 index 000000000000000..5629fc90700a1e5 --- /dev/null +++ b/test/integration/env-config/app/pages/some-ssp.js @@ -0,0 +1,30 @@ +export const config = { + env: [ + 'PROCESS_ENV_KEY', + 'ENV_FILE_KEY', + 'LOCAL_ENV_FILE_KEY', + 'ENV_FILE_LOCAL_OVERRIDE_TEST', + 'PRODUCTION_ENV_FILE_KEY', + 'LOCAL_PRODUCTION_ENV_FILE_KEY', + 'DEVELOPMENT_ENV_FILE_KEY', + 'LOCAL_DEVELOPMENT_ENV_FILE_KEY', + 'ENV_FILE_DEVELOPMENT_OVERRIDE_TEST', + 'ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST', + 'TEST_ENV_FILE_KEY', + 'LOCAL_TEST_ENV_FILE_KEY', + 'ENV_FILE_TEST_OVERRIDE_TEST', + 'ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST', + ], +} + +export async function getServerSideProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config/app/pages/ssp.js b/test/integration/env-config/app/pages/ssp.js new file mode 100644 index 000000000000000..5629fc90700a1e5 --- /dev/null +++ b/test/integration/env-config/app/pages/ssp.js @@ -0,0 +1,30 @@ +export const config = { + env: [ + 'PROCESS_ENV_KEY', + 'ENV_FILE_KEY', + 'LOCAL_ENV_FILE_KEY', + 'ENV_FILE_LOCAL_OVERRIDE_TEST', + 'PRODUCTION_ENV_FILE_KEY', + 'LOCAL_PRODUCTION_ENV_FILE_KEY', + 'DEVELOPMENT_ENV_FILE_KEY', + 'LOCAL_DEVELOPMENT_ENV_FILE_KEY', + 'ENV_FILE_DEVELOPMENT_OVERRIDE_TEST', + 'ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST', + 'ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST', + 'TEST_ENV_FILE_KEY', + 'LOCAL_TEST_ENV_FILE_KEY', + 'ENV_FILE_TEST_OVERRIDE_TEST', + 'ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST', + ], +} + +export async function getServerSideProps({ env }) { + return { + // Do not pass any sensitive values here as they will + // be made PUBLICLY available in `pageProps` + props: { env }, + } +} + +export default ({ env }) =>

{JSON.stringify(env)}

diff --git a/test/integration/env-config/test/index.test.js b/test/integration/env-config/test/index.test.js new file mode 100644 index 000000000000000..6d060210105f6b6 --- /dev/null +++ b/test/integration/env-config/test/index.test.js @@ -0,0 +1,481 @@ +/* eslint-env jest */ +/* global jasmine */ +import url from 'url' +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import { + nextBuild, + nextStart, + renderViaHTTP, + findPort, + launchApp, + killApp, + fetchViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +let app +let appPort +const appDir = join(__dirname, '../app') +const nextConfig = join(appDir, 'next.config.js') + +const nextConfigContent = ` + experimental: { + pageEnv: true, + + async redirects() { + return [ + { + source: '/hello', + permanent: false, + destination: \`/\${process.env.NEXT_APP_TEST_DEST}\`, + } + ] + } + } +` + +const getEnvFromHtml = async path => { + const html = await renderViaHTTP(appPort, path) + return JSON.parse( + cheerio + .load(html)('p') + .text() + ) +} + +const runTests = (isDev, isServerless, isTestEnv) => { + // TODO: support runtime overrides in serverless output + if (!isServerless) { + describe('Process environment', () => { + it('should override .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.PROCESS_ENV_KEY).toEqual('processenvironment') + }) + }) + } + + it('should provide global env to next.config.js', async () => { + const res = await fetchViaHTTP(appPort, '/hello', undefined, { + redirect: 'manual', + }) + const { pathname } = url.parse(res.headers.get('location')) + + expect(res.status).toBe(307) + expect(pathname).toBe('/another') + }) + + it('should inline global values during build', async () => { + const html = await renderViaHTTP(appPort, '/global') + const $ = cheerio.load(html) + expect($('p').text()).toContain('another') + }) + + describe('Loads .env', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.ENV_FILE_KEY).toBe('env') + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.ENV_FILE_KEY).toBe('env') + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).ENV_FILE_KEY).toEqual('env') + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.ENV_FILE_KEY).toEqual('env') + // }) + }) + + if (!isTestEnv) { + describe('Loads .env.local', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.LOCAL_ENV_FILE_KEY).toBe('localenv') + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.LOCAL_ENV_FILE_KEY).toBe('localenv') + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).LOCAL_ENV_FILE_KEY).toEqual('localenv') + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.LOCAL_ENV_FILE_KEY).toEqual('localenv') + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_LOCAL_OVERRIDE_TEST).toEqual('localenv') + }) + }) + + describe('Loads .env.development', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.DEVELOPMENT_ENV_FILE_KEY).toBe( + isDev ? 'development' : undefined + ) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.DEVELOPMENT_ENV_FILE_KEY).toBe( + isDev ? 'development' : undefined + ) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).DEVELOPMENT_ENV_FILE_KEY).toEqual( + isDev ? 'development' : undefined + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.DEVELOPMENT_ENV_FILE_KEY).toEqual( + // isDev ? 'development' : undefined + // ) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_DEVELOPMENT_OVERRIDE_TEST).toEqual( + isDev ? 'development' : 'env' + ) + }) + }) + + describe('Loads .env.development.local', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.LOCAL_DEVELOPMENT_ENV_FILE_KEY).toBe( + isDev ? 'localdevelopment' : undefined + ) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.LOCAL_DEVELOPMENT_ENV_FILE_KEY).toBe( + isDev ? 'localdevelopment' : undefined + ) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).LOCAL_DEVELOPMENT_ENV_FILE_KEY).toEqual( + isDev ? 'localdevelopment' : undefined + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.LOCAL_DEVELOPMENT_ENV_FILE_KEY).toEqual( + // isDev ? 'localdevelopment' : undefined + // ) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env and .env.development', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST).toEqual( + isDev ? 'localdevelopment' : 'env' + ) + }) + }) + + describe('Loads .env.production', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.PRODUCTION_ENV_FILE_KEY).toBe( + isDev ? undefined : 'production' + ) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.PRODUCTION_ENV_FILE_KEY).toBe( + isDev ? undefined : 'production' + ) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).PRODUCTION_ENV_FILE_KEY).toEqual( + isDev ? undefined : 'production' + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.PRODUCTION_ENV_FILE_KEY).toEqual( + // isDev ? undefined : 'production' + // ) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST).toEqual( + isDev ? 'env' : 'production' + ) + }) + }) + + describe('Loads .env.production.local', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.LOCAL_PRODUCTION_ENV_FILE_KEY).toBe( + isDev ? undefined : 'localproduction' + ) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.LOCAL_PRODUCTION_ENV_FILE_KEY).toBe( + isDev ? undefined : 'localproduction' + ) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).LOCAL_PRODUCTION_ENV_FILE_KEY).toEqual( + isDev ? undefined : 'localproduction' + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.LOCAL_PRODUCTION_ENV_FILE_KEY).toEqual( + // isDev ? undefined : 'localproduction' + // ) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env and .env.production', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST).toEqual( + isDev ? 'env' : 'localproduction' + ) + }) + }) + } + + if (isTestEnv) { + describe('Loads .env.test', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.TEST_ENV_FILE_KEY).toBe(isDev ? 'test' : undefined) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.TEST_ENV_FILE_KEY).toBe(isDev ? 'test' : undefined) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).TEST_ENV_FILE_KEY).toEqual( + isDev ? 'test' : undefined + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.TEST_ENV_FILE_KEY).toEqual(isDev ? 'test' : undefined) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_TEST_OVERRIDE_TEST).toEqual(isDev ? 'test' : 'env') + }) + }) + + describe('Loads .env.test.local', () => { + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + expect(data.LOCAL_TEST_ENV_FILE_KEY).toBe( + isDev ? 'localtest' : undefined + ) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + expect(data.LOCAL_TEST_ENV_FILE_KEY).toBe( + isDev ? 'localtest' : undefined + ) + }) + + it('should provide env correctly for API routes', async () => { + const data = await renderViaHTTP(appPort, '/api/all') + expect(JSON.parse(data).LOCAL_TEST_ENV_FILE_KEY).toEqual( + isDev ? 'localtest' : undefined + ) + }) + + // TODO: uncomment once env is provided to next.config.js + // it('should provide env correctly through next.config.js', async () => { + // const data = await getEnvFromHtml('/next-config-loaded-env') + // expect(data.LOCAL_TEST_ENV_FILE_KEY).toEqual( + // isDev ? 'localtest' : undefined + // ) + // }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + }) + + it('should override env from .env and .env.test', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST).toEqual( + isDev ? 'localtest' : 'env' + ) + }) + + it('should not load .env.local', async () => { + const data = await getEnvFromHtml('/') + expect(data.LOCAL_ENV_FILE_KEY).toEqual(undefined) + }) + }) + } +} + +describe('Env Config', () => { + describe('dev mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { env: { NC_ENV_FILE_KEY: process.env.ENV_FILE_KEY, NC_LOCAL_ENV_FILE_KEY: process.env.LOCAL_ENV_FILE_KEY, NC_PRODUCTION_ENV_FILE_KEY: process.env.PRODUCTION_ENV_FILE_KEY, NC_LOCAL_PRODUCTION_ENV_FILE_KEY: process.env.LOCAL_PRODUCTION_ENV_FILE_KEY, NC_DEVELOPMENT_ENV_FILE_KEY: process.env.DEVELOPMENT_ENV_FILE_KEY, NC_LOCAL_DEVELOPMENT_ENV_FILE_KEY: process.env.LOCAL_DEVELOPMENT_ENV_FILE_KEY }, ${nextConfigContent} }` + ) + appPort = await findPort() + app = await launchApp(appDir, appPort, { + env: { + PROCESS_ENV_KEY: 'processenvironment', + }, + }) + }) + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + runTests(true, false, false) + }) + + describe('test environment', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { env: { NC_ENV_FILE_KEY: process.env.ENV_FILE_KEY, NC_LOCAL_ENV_FILE_KEY: process.env.LOCAL_ENV_FILE_KEY, NC_PRODUCTION_ENV_FILE_KEY: process.env.PRODUCTION_ENV_FILE_KEY, NC_LOCAL_PRODUCTION_ENV_FILE_KEY: process.env.LOCAL_PRODUCTION_ENV_FILE_KEY, NC_DEVELOPMENT_ENV_FILE_KEY: process.env.DEVELOPMENT_ENV_FILE_KEY, NC_LOCAL_DEVELOPMENT_ENV_FILE_KEY: process.env.LOCAL_DEVELOPMENT_ENV_FILE_KEY, NC_TEST_ENV_FILE_KEY: process.env.TEST_ENV_FILE_KEY, NC_LOCAL_TEST_ENV_FILE_KEY: process.env.LOCAL_TEST_ENV_FILE_KEY }, ${nextConfigContent} }` + ) + appPort = await findPort() + app = await launchApp(appDir, appPort, { + env: { + PROCESS_ENV_KEY: 'processenvironment', + NODE_ENV: 'test', + }, + }) + }) + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + runTests(true, false, true) + }) + + describe('server mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { env: { NC_ENV_FILE_KEY: process.env.ENV_FILE_KEY, NC_LOCAL_ENV_FILE_KEY: process.env.LOCAL_ENV_FILE_KEY, NC_PRODUCTION_ENV_FILE_KEY: process.env.PRODUCTION_ENV_FILE_KEY, NC_LOCAL_PRODUCTION_ENV_FILE_KEY: process.env.LOCAL_PRODUCTION_ENV_FILE_KEY, NC_DEVELOPMENT_ENV_FILE_KEY: process.env.DEVELOPMENT_ENV_FILE_KEY, NC_LOCAL_DEVELOPMENT_ENV_FILE_KEY: process.env.LOCAL_DEVELOPMENT_ENV_FILE_KEY }, ${nextConfigContent} }` + ) + const { code } = await nextBuild(appDir, [], { + env: { + PROCESS_ENV_KEY: 'processenvironment', + }, + }) + if (code !== 0) throw new Error(`Build failed with exit code ${code}`) + appPort = await findPort() + app = await nextStart(appDir, appPort, {}) + }) + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + runTests(false, false, false) + }) + + describe('serverless mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { target: 'experimental-serverless-trace', env: { NC_ENV_FILE_KEY: process.env.ENV_FILE_KEY, NC_LOCAL_ENV_FILE_KEY: process.env.LOCAL_ENV_FILE_KEY, NC_PRODUCTION_ENV_FILE_KEY: process.env.PRODUCTION_ENV_FILE_KEY, NC_LOCAL_PRODUCTION_ENV_FILE_KEY: process.env.LOCAL_PRODUCTION_ENV_FILE_KEY, NC_DEVELOPMENT_ENV_FILE_KEY: process.env.DEVELOPMENT_ENV_FILE_KEY, NC_LOCAL_DEVELOPMENT_ENV_FILE_KEY: process.env.LOCAL_DEVELOPMENT_ENV_FILE_KEY }, ${nextConfigContent} }` + ) + const { code } = await nextBuild(appDir, [], {}) + if (code !== 0) throw new Error(`Build failed with exit code ${code}`) + appPort = await findPort() + app = await nextStart(appDir, appPort, { + env: { + PROCESS_ENV_KEY: 'processenvironment', + }, + }) + }) + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + runTests(false, true, false) + }) +}) diff --git a/yarn.lock b/yarn.lock index 88ef538a57b6539..6e0f5a5a9d9c3ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2536,6 +2536,13 @@ dependencies: "@types/node" "*" +"@types/dotenv@8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053" + integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw== + dependencies: + dotenv "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -6213,6 +6220,16 @@ dot-prop@^5.0.0: dependencies: is-obj "^2.0.0" +dotenv-expand@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@*, dotenv@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"