Skip to content

Commit

Permalink
Update env config to .env.json and add dotenv loading
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk committed Feb 14, 2020
1 parent 455dc35 commit f0d817a
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const nextServerlessLoader: loader.Loader = function() {

const envImports = envConfig
? `
const { processEnv } = require('next/dist/lib/load-env-config')
const { processEnv } = require('next/dist/lib/process-env')
`
: ''

Expand Down
62 changes: 16 additions & 46 deletions packages/next/lib/load-env-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
import { processEnv } from './process-env'
import { existsSync } from './find-pages-dir'
import { ENV_CONFIG_FILE } from '../next-server/lib/constants'

type EnvironmentKey = {
description: string
required?: boolean // defaults to false
value?: string // the default value
defaultValue?: string // the default value
env?: {
development: Omit<EnvironmentKey, 'env'>
production: Omit<EnvironmentKey, 'env'>
Expand All @@ -19,52 +21,13 @@ export type EnvironmentConfig = {

export type Env = { [key: string]: string }

export function processEnv(_Env: EnvironmentConfig): Env {
const missingEnvItems = new Set()
const Env: Env = {}

for (const key of Object.keys(_Env)) {
const envItem = _Env[key]
let curValue: string | undefined = envItem.value
let isRequired = envItem.required

if (process.env[key]) {
curValue = process.env[key]
} else if (envItem.env) {
const nodeEnv = process.env.NODE_ENV
const subEnv = envItem.env[nodeEnv]

if (subEnv) {
if (typeof subEnv.required === 'boolean') {
isRequired = subEnv.required
}

if (subEnv.value) {
curValue = subEnv.value
}
}
}

if (curValue) {
Env[key] = curValue
} else if (isRequired) {
missingEnvItems.add(key)
}
}

if (missingEnvItems.size > 0) {
throw new Error(
`Required environment items from \`${ENV_CONFIG_FILE}\` are missing: ` +
`${[...missingEnvItems].join(', ')}`
)
}
return Env
}

// loads andy maybe populate env config items
export function loadEnvConfig(dir: string): Env
export function loadEnvConfig(dir: string, process: false): EnvironmentConfig
export function loadEnvConfig(dir: any, process?: any): any {
export function loadEnvConfig(
dir: string,
processItems: false
): EnvironmentConfig
export function loadEnvConfig(dir: any, processItems?: any): any {
const envPath = path.join(dir, ENV_CONFIG_FILE)

// don't use require so it's not cached in module cache
Expand All @@ -77,8 +40,15 @@ export function loadEnvConfig(dir: any, process?: any): any {
const _Env: EnvironmentConfig = JSON.parse(fs.readFileSync(envPath, 'utf8'))

// TODO: do we want to validate extra/invalid fields in env.json?
if (process === false) {
if (processItems === false) {
return _Env
}
// only load .env if the user provided has an env config file
const dotEnv = path.join(dir, '.env')
const result = dotenv.config({ path: dotEnv })

if (result.parsed) {
console.log(`Loaded .env from ${dotEnv}`)
}
return processEnv(_Env)
}
44 changes: 44 additions & 0 deletions packages/next/lib/process-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ENV_CONFIG_FILE } from '../next-server/lib/constants'
import { EnvironmentConfig, Env } from './load-env-config'

export function processEnv(_env: EnvironmentConfig): Env {
const missingEnvItems = new Set()
const env: Env = {}

for (const key of Object.keys(_env)) {
const envItem = _env[key]
let curValue: string | undefined = envItem.defaultValue
let isRequired = envItem.required

if (process.env[key]) {
curValue = process.env[key]
} else if (envItem.env) {
const nodeEnv = process.env.NODE_ENV
const subEnv = envItem.env[nodeEnv]

if (subEnv) {
if (typeof subEnv.required === 'boolean') {
isRequired = subEnv.required
}

if (subEnv.defaultValue) {
curValue = subEnv.defaultValue
}
}
}

if (curValue) {
env[key] = curValue
} else if (isRequired) {
missingEnvItems.add(key)
}
}

if (missingEnvItems.size > 0) {
throw new Error(
`Required environment items from \`${ENV_CONFIG_FILE}\` are missing: ` +
`${[...missingEnvItems].join(', ')}`
)
}
return env
}
2 changes: 1 addition & 1 deletion packages/next/next-server/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json'
export const SERVER_DIRECTORY = 'server'
export const SERVERLESS_DIRECTORY = 'serverless'
export const CONFIG_FILE = 'next.config.js'
export const ENV_CONFIG_FILE = 'env.json'
export const ENV_CONFIG_FILE = '.env.json'
export const BUILD_ID_FILE = 'BUILD_ID'
export const BLOCKED_PAGES = ['/_document', '/_app']
export const CLIENT_PUBLIC_FILES_PATH = 'public'
Expand Down
2 changes: 2 additions & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"css-loader": "3.3.0",
"cssnano-simple": "1.0.0",
"devalue": "2.0.1",
"dotenv": "8.2.0",
"escape-string-regexp": "2.0.0",
"etag": "1.8.1",
"file-loader": "4.2.0",
Expand Down Expand Up @@ -168,6 +169,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",
Expand Down
13 changes: 8 additions & 5 deletions test/integration/env-config-errors/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
/* global jasmine */
import fs from 'fs-extra'
import { join } from 'path'
import { ENV_CONFIG_FILE } from 'next/dist/next-server/lib/constants'
import { nextBuild, findPort, launchApp, killApp } from 'next-test-utils'

jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2

const appDir = join(__dirname, '..')
const envFile = join(appDir, 'env.json')
const envFile = join(appDir, ENV_CONFIG_FILE)
const nextConfig = join(appDir, 'next.config.js')

const build = async (isDev = false) => {
Expand Down Expand Up @@ -67,7 +68,9 @@ const runTests = (isDev = false) => {
console.log(output)

expect(output).toContain(
'Required environment items from `env.json` are missing: NOTION_KEY, SENTRY_DSN'
'Required environment items from `' +
ENV_CONFIG_FILE +
'` are missing: NOTION_KEY, SENTRY_DSN'
)
})

Expand All @@ -77,7 +80,7 @@ const runTests = (isDev = false) => {
JSON.stringify({
NOTION_KEY: {
description: 'Notion API key',
value: 'notion',
defaultValue: 'notion',
required: true,
},
APP_TITLE: {
Expand All @@ -88,7 +91,7 @@ const runTests = (isDev = false) => {
description: 'our sentry dsn',
env: {
[isDev ? 'development' : 'production']: {
value: 'sentry',
defaultValue: 'sentry',
required: true,
},
[!isDev ? 'development' : 'production']: {
Expand All @@ -104,7 +107,7 @@ const runTests = (isDev = false) => {
const output = await build(isDev)

expect(output).not.toContain(
'Required environment items from `env.json` are missing:'
'Required environment items from `' + ENV_CONFIG_FILE + '` are missing:'
)
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
"required": true,
"env": {
"development": {
"value": "abc"
"defaultValue": "abc"
},
"production": {
"value": "cba"
"defaultValue": "cba"
}
}
},
"APP_TITLE": {
"description": "The title of the current App",
"required": true,
"value": "hello"
"defaultValue": "hello"
},
"SENTRY_DSN": {
"description": "Our Sentry DSN for tracking oops",
Expand Down
14 changes: 12 additions & 2 deletions test/integration/env-config/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let app
let appPort
const appDir = join(__dirname, '..')
const nextConfig = join(appDir, 'next.config.js')
const dotenvFile = join(appDir, '.env')

const neededDevEnv = { NOTION_KEY: 'hello-notion' }
const expectedDevEnv = {
Expand Down Expand Up @@ -96,9 +97,18 @@ describe('Env Config', () => {
describe('dev mode', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort, { env: neededDevEnv })
await fs.writeFile(
dotenvFile,
Object.keys(neededDevEnv)
.map(key => `${key}=${neededDevEnv[key]}`)
.join('\n')
)
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await fs.remove(dotenvFile)
await killApp(app)
})
afterAll(() => killApp(app))

runTests(true)
})
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2518,6 +2518,13 @@
dependencies:
"@types/node" "*"

"@types/[email protected]":
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"
Expand Down Expand Up @@ -6178,6 +6185,11 @@ dot-prop@^5.0.0:
dependencies:
is-obj "^2.0.0"

dotenv@*, [email protected]:
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"
Expand Down

0 comments on commit f0d817a

Please sign in to comment.