Add initial support for new env handling (#10525)

* Add initial support for new env config file

* Fix serverless processEnv call when no env is provided

* Add missing await for test method

* Update env config to .env.json and add dotenv loading

* ncc dotenv package

* Update type

* Update with new discussed behavior removing .env.json

* Update hot-reloader createEntrypoints

* Make sure .env is loaded before next.config.js

* Add tests for all separate .env files

* Remove comments

* Add override tests

* Add test for overriding env vars based on local environment

* Add support for .env.test

* Apply suggestions from code review

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>

* Use chalk for env loaded message

* Remove constant as it’s not needed

* Update test

* Update errsh, taskr, and CNA template ignores

* Make sure to only consider undefined missing

* Remove old .env ignore

* Update to not populate process.env with loaded env

* Add experimental flag and add loading of global env values

Co-authored-by: Tim Neutkens <timneutkens@me.com>
Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
This commit is contained in:
JJ Kasper 2020-03-26 07:32:41 -05:00 committed by GitHub
parent a391d328ae
commit d8155b22ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1103 additions and 10 deletions

View file

@ -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)

View file

@ -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

View file

@ -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<void> {
)
}
// 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)

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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
}

View file

@ -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
}
/**

View file

@ -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<void>
) {
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

View file

@ -54,6 +54,7 @@ const defaultConfig: { [key: string]: any } = {
workerThreads: false,
basePath: '',
sassOptions: {},
pageEnv: false,
},
future: {
excludeDefaultMomentLocales: false,

View file

@ -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

View file

@ -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 }

View file

@ -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
}

View file

@ -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",

View file

@ -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 {

View file

@ -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',
])
}

View file

@ -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<string>
}
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 }>

View file

@ -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<T extends arg.Spec>(
spec: T,

View file

@ -0,0 +1,3 @@
{
"name": "env-config-errors"
}

View file

@ -0,0 +1,5 @@
export const config = {
env: ['NOTION_KEY', 'SENTRY_DSN', 'DATABASE_KEY', 'DATABASE_USER'],
}
export default (req, res) => res.json(req.env)

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -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()
})
})

View file

@ -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

View file

@ -0,0 +1,3 @@
DEVELOPMENT_ENV_FILE_KEY=development
ENV_FILE_DEVELOPMENT_OVERRIDE_TEST=development
ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST=development

View file

@ -0,0 +1,2 @@
LOCAL_DEVELOPMENT_ENV_FILE_KEY=localdevelopment
ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST=localdevelopment

View file

@ -0,0 +1,2 @@
LOCAL_ENV_FILE_KEY=localenv
ENV_FILE_LOCAL_OVERRIDE_TEST=localenv

View file

@ -0,0 +1,3 @@
PRODUCTION_ENV_FILE_KEY=production
ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST=production
ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST=production

View file

@ -0,0 +1,2 @@
LOCAL_PRODUCTION_ENV_FILE_KEY=localproduction
ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST=localproduction

View file

@ -0,0 +1,3 @@
TEST_ENV_FILE_KEY=test
ENV_FILE_TEST_OVERRIDE_TEST=test
ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST=test

View file

@ -0,0 +1,2 @@
LOCAL_TEST_ENV_FILE_KEY=localtest
ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST=localtest

View file

@ -0,0 +1,4 @@
{
"name": "env-config",
"dependencies": {}
}

View file

@ -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)
}

View file

@ -0,0 +1 @@
export default () => <p>{process.env.NEXT_APP_TEST_DEST}</p>

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -0,0 +1,16 @@
export default () => (
<p>
{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,
})}
</p>
)

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -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 }) => <p>{JSON.stringify(env)}</p>

View file

@ -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)
})
})

View file

@ -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"