Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for new env handling #10525

Merged
merged 41 commits into from Mar 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fb4c0de
Add initial support for new env config file
ijjk Feb 13, 2020
f369d2d
Fix serverless processEnv call when no env is provided
ijjk Feb 13, 2020
f12ac4c
Add missing await for test method
ijjk Feb 13, 2020
3e3878a
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 13, 2020
455dc35
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 14, 2020
f0d817a
Update env config to .env.json and add dotenv loading
ijjk Feb 14, 2020
97f7e56
ncc dotenv package
ijjk Feb 14, 2020
444c401
Update type
ijjk Feb 14, 2020
8ee3eab
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 17, 2020
e5b4ee0
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 20, 2020
31953f7
Update with new discussed behavior removing .env.json
ijjk Feb 21, 2020
30f1a52
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 21, 2020
266d095
Update hot-reloader createEntrypoints
ijjk Feb 21, 2020
9a6cd9e
Make sure .env is loaded before next.config.js
timneutkens Feb 24, 2020
66bf869
Add tests for all separate .env files
timneutkens Feb 24, 2020
78cb909
Remove comments
timneutkens Feb 24, 2020
cfb627e
Add override tests
timneutkens Feb 24, 2020
1c1b604
Add test for overriding env vars based on local environment
timneutkens Feb 24, 2020
25cf95c
Add support for .env.test
timneutkens Feb 24, 2020
6929e28
Apply suggestions from code review
ijjk Feb 24, 2020
e2f1877
Use chalk for env loaded message
ijjk Feb 24, 2020
d28de35
Remove constant as it’s not needed
timneutkens Feb 24, 2020
ac16529
Update test
ijjk Feb 24, 2020
129801e
Merge branch 'add/new-env' of github.com:ijjk/next.js into add/new-env
ijjk Feb 24, 2020
5a1580a
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 24, 2020
a0c709a
Update errsh, taskr, and CNA template ignores
ijjk Feb 24, 2020
f7a0030
Make sure to only consider undefined missing
ijjk Feb 24, 2020
ab97e46
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 24, 2020
e9401b3
Remove old .env ignore
ijjk Feb 24, 2020
cd0452d
Merge branch 'canary' into add/new-env
ijjk Feb 24, 2020
bdd50e3
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 25, 2020
69340a3
Update to not populate process.env with loaded env
ijjk Feb 25, 2020
dea3ac5
Merge branch 'add/new-env' of github.com:ijjk/next.js into add/new-env
ijjk Feb 25, 2020
0b67f2f
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 27, 2020
350a451
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Feb 28, 2020
9d086c1
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Mar 3, 2020
2eb53f0
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Mar 5, 2020
b2dee6e
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Mar 20, 2020
1eb2d2b
Add experimental flag and add loading of global env values
ijjk Mar 20, 2020
4bceac7
Merge remote-tracking branch 'upstream/canary' into add/new-env
ijjk Mar 20, 2020
67cf471
Merge branch 'canary' of github.com:zeit/next.js into ijjk-add/new-env
timneutkens Mar 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions 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)
7 changes: 6 additions & 1 deletion packages/create-next-app/templates/default/gitignore
Expand Up @@ -17,9 +17,14 @@

# misc
.DS_Store
.env*

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

ijjk marked this conversation as resolved.
Show resolved Hide resolved
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
4 changes: 4 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -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(
Expand Down
Expand Up @@ -181,6 +181,7 @@ const nextServerlessLoader: loader.Loader = function() {
Object.assign({}, parsedUrl.query, params ),
resolver,
${encodedPreviewProps},
process.env,
onError
)
} catch (err) {
Expand Down Expand Up @@ -257,6 +258,7 @@ const nextServerlessLoader: loader.Loader = function() {
assetPrefix: "${assetPrefix}",
runtimeConfig: runtimeConfig.publicRuntimeConfig || {},
previewProps: ${encodedPreviewProps},
env: process.env,
..._renderOpts
}
let _nextData = false
Expand Down
2 changes: 2 additions & 0 deletions packages/next/export/index.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -230,6 +231,7 @@ export default async function(
dir,
buildId,
nextExport: true,
env: loadEnvConfig(dir),
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
distDir,
dev: false,
Expand Down
2 changes: 1 addition & 1 deletion 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
Expand Down
84 changes: 84 additions & 0 deletions 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
}
3 changes: 3 additions & 0 deletions packages/next/next-server/lib/utils.ts
Expand Up @@ -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
Expand Down Expand Up @@ -186,6 +187,8 @@ export type NextApiRequest = IncomingMessage & {
}

body: any

env: Env
}

/**
Expand Down
15 changes: 7 additions & 8 deletions packages/next/next-server/server/api-utils.ts
Expand Up @@ -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[] }
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -54,6 +54,7 @@ const defaultConfig: { [key: string]: any } = {
workerThreads: false,
basePath: '',
sassOptions: {},
pageEnv: false,
},
future: {
excludeDefaultMomentLocales: false,
Expand Down
6 changes: 6 additions & 0 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -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)

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -684,6 +689,7 @@ export default class Server {
query,
pageModule,
this.renderOpts.previewProps,
this.renderOpts.env,
this.onErrorMiddleware
)
return true
Expand Down
8 changes: 8 additions & 0 deletions packages/next/next-server/server/render.tsx
Expand Up @@ -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'

Expand Down Expand Up @@ -154,6 +156,7 @@ export type RenderOptsPartial = {
isDataReq?: boolean
params?: ParsedUrlQuery
previewProps: __ApiPreviewProps
env: Env | false
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
Expand Down Expand Up @@ -288,6 +291,7 @@ export async function renderToHTML(
staticMarkup = false,
ampPath = '',
App,
env = {},
Document,
pageConfig = {},
DocumentMiddleware,
Expand All @@ -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 ? {} : []

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }
Expand Down
26 changes: 26 additions & 0 deletions 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
Expand All @@ -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
}