mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-26 23:52:06 +00:00
feat: runtime config and built-in dotenv support (#7312)
Co-authored-by: Sébastien Chopin <seb@nuxtjs.com> Co-authored-by: Alexander Lichter <manniL@gmx.net>
This commit is contained in:
parent
0c81c52c41
commit
0337932115
@ -16,6 +16,7 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-template-curly-in-string': 0,
|
||||
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }]
|
||||
},
|
||||
overrides: [{
|
||||
|
@ -770,6 +770,11 @@ export default class Builder {
|
||||
if (this.ignore.ignoreFile) {
|
||||
nuxtRestartWatch.push(this.ignore.ignoreFile)
|
||||
}
|
||||
|
||||
if (this.options._envConfig && this.options._envConfig.dotenv) {
|
||||
nuxtRestartWatch.push(this.options._envConfig.dotenv)
|
||||
}
|
||||
|
||||
// If default page displayed, watch for first page creation
|
||||
if (this._nuxtPages && this._defaultPage) {
|
||||
nuxtRestartWatch.push(path.join(this.options.srcDir, this.options.dir.pages))
|
||||
|
@ -54,5 +54,15 @@ export default {
|
||||
alias: 'h',
|
||||
type: 'boolean',
|
||||
description: 'Display this message'
|
||||
},
|
||||
processenv: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Disable reading from `process.env` and updating it with dotenv'
|
||||
},
|
||||
dotenv: {
|
||||
type: 'string',
|
||||
default: '.env',
|
||||
description: 'Specify path to dotenv file (default: `.env`). Use `false` to disable'
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,11 @@ export async function loadNuxtConfig (argv, configContext) {
|
||||
const options = await _loadNuxtConfig({
|
||||
rootDir,
|
||||
configFile,
|
||||
configContext
|
||||
configContext,
|
||||
envConfig: {
|
||||
dotenv: argv.dotenv === 'false' ? false : argv.dotenv,
|
||||
env: argv.processenv ? process.env : {}
|
||||
}
|
||||
})
|
||||
|
||||
// Nuxt Mode
|
||||
|
@ -27,6 +27,12 @@ exports[`cli/command builds help text 1`] = `
|
||||
--version, -v Display the Nuxt
|
||||
version
|
||||
--help, -h Display this message
|
||||
--no-processenv Disable reading from
|
||||
process.env and updating it with
|
||||
dotenv
|
||||
--dotenv Specify path to
|
||||
dotenv file (default: .env). Use
|
||||
false to disable
|
||||
--port, -p Port number on which
|
||||
to start the application
|
||||
--hostname, -H Hostname on which to
|
||||
|
@ -21,8 +21,8 @@ describe('cli/command', () => {
|
||||
const cmd = new Command({ options: allOptions })
|
||||
const minimistOptions = cmd._getMinimistOptions()
|
||||
|
||||
expect(minimistOptions.string.length).toBe(6)
|
||||
expect(minimistOptions.boolean.length).toBe(5)
|
||||
expect(minimistOptions.string.length).toBe(7)
|
||||
expect(minimistOptions.boolean.length).toBe(6)
|
||||
expect(minimistOptions.alias.c).toBe('config-file')
|
||||
expect(minimistOptions.default.c).toBe(common['config-file'].default)
|
||||
})
|
||||
@ -71,7 +71,13 @@ describe('cli/command', () => {
|
||||
expect(options.server.port).toBe(3001)
|
||||
expect(consola.fatal).toHaveBeenCalledWith('Provided hostname argument has no value') // hostname check
|
||||
expect(loadConfigSpy).toHaveBeenCalledTimes(1)
|
||||
expect(loadConfigSpy).toHaveBeenCalledWith(expect.any(Object), { command: 'test', dev: false })
|
||||
expect(loadConfigSpy).toHaveBeenCalledWith(expect.any(Object), {
|
||||
command: 'test',
|
||||
dev: false,
|
||||
env: expect.objectContaining({
|
||||
NODE_ENV: 'test'
|
||||
})
|
||||
})
|
||||
|
||||
loadConfigSpy.mockRestore()
|
||||
})
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@nuxt/utils": "2.12.1",
|
||||
"consola": "^2.12.1",
|
||||
"defu": "^2.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"esm": "^3.2.25",
|
||||
"std-env": "^2.2.1"
|
||||
},
|
||||
|
@ -77,5 +77,9 @@ export default () => ({
|
||||
editor: undefined,
|
||||
|
||||
// Hooks
|
||||
hooks: null
|
||||
hooks: null,
|
||||
|
||||
// runtimeConfig
|
||||
privateRuntimeConfig: {},
|
||||
publicRuntimeConfig: {}
|
||||
})
|
||||
|
@ -1,12 +1,15 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import defu from 'defu'
|
||||
import consola from 'consola'
|
||||
import dotenv from 'dotenv'
|
||||
import { clearRequireCache, scanRequireTree } from '@nuxt/utils'
|
||||
import esm from 'esm'
|
||||
import { defaultNuxtConfigFile } from './config'
|
||||
|
||||
export async function loadNuxtConfig ({
|
||||
rootDir = '.',
|
||||
envConfig = {},
|
||||
configFile = defaultNuxtConfigFile,
|
||||
configContext = {},
|
||||
configOverrides = {}
|
||||
@ -27,6 +30,23 @@ export async function loadNuxtConfig ({
|
||||
configFile = undefined
|
||||
}
|
||||
|
||||
// Load env
|
||||
envConfig = {
|
||||
dotenv: '.env',
|
||||
env: process.env,
|
||||
expand: true,
|
||||
...envConfig
|
||||
}
|
||||
|
||||
const env = loadEnv(envConfig, rootDir)
|
||||
|
||||
// Fill process.env so it is accessible in nuxt.config
|
||||
for (const key in env) {
|
||||
if (!key.startsWith('_') && envConfig.env[key] === undefined) {
|
||||
envConfig.env[key] = env[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (configFile) {
|
||||
// Clear cache
|
||||
clearRequireCache(configFile)
|
||||
@ -66,5 +86,85 @@ export async function loadNuxtConfig ({
|
||||
options.rootDir = rootDir
|
||||
}
|
||||
|
||||
// Load env to options._env
|
||||
options._env = env
|
||||
options._envConfig = envConfig
|
||||
if (configContext) { configContext.env = env }
|
||||
|
||||
// Expand and interpolate runtimeConfig from _env
|
||||
if (envConfig.expand) {
|
||||
for (const c of ['publicRuntimeConfig', 'privateRuntimeConfig']) {
|
||||
if (options[c]) {
|
||||
if (typeof options[c] === 'function') {
|
||||
options[c] = options[c](env)
|
||||
}
|
||||
expand(options[c], env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function loadEnv (envConfig, rootDir = process.cwd()) {
|
||||
const env = Object.create(null)
|
||||
|
||||
// Read dotenv
|
||||
if (envConfig.dotenv) {
|
||||
envConfig.dotenv = path.resolve(rootDir, envConfig.dotenv)
|
||||
if (fs.existsSync(envConfig.dotenv)) {
|
||||
const parsed = dotenv.parse(fs.readFileSync(envConfig.dotenv, 'utf-8'))
|
||||
Object.assign(env, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply process.env
|
||||
if (!envConfig.env._applied) {
|
||||
Object.assign(env, envConfig.env)
|
||||
envConfig.env._applied = true
|
||||
}
|
||||
|
||||
// Interpolate env
|
||||
if (envConfig.expand) {
|
||||
expand(env)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// Based on https://github.com/motdotla/dotenv-expand
|
||||
function expand (target, source = {}) {
|
||||
function getValue (key) {
|
||||
// Source value 'wins' over target value
|
||||
return source[key] !== undefined ? source[key] : (target[key] || '')
|
||||
}
|
||||
|
||||
function interpolate (value) {
|
||||
const matches = value.match(/(.?\${?(?:[a-zA-Z0-9_:]+)?}?)/g) || []
|
||||
return matches.reduce((newValue, match) => {
|
||||
const parts = /(.?)\${?([a-zA-Z0-9_:]+)?}?/g.exec(match)
|
||||
const prefix = parts[1]
|
||||
|
||||
let value, replacePart
|
||||
|
||||
if (prefix === '\\') {
|
||||
replacePart = parts[0]
|
||||
value = replacePart.replace('\\$', '$')
|
||||
} else {
|
||||
const key = parts[2]
|
||||
replacePart = parts[0].substring(prefix.length)
|
||||
|
||||
value = getValue(key)
|
||||
|
||||
// Resolve recursive interpolations
|
||||
value = interpolate(value)
|
||||
}
|
||||
|
||||
return newValue.replace(replacePart, value)
|
||||
}, value)
|
||||
}
|
||||
|
||||
for (const key in target) {
|
||||
target[key] = interpolate(getValue(key))
|
||||
}
|
||||
}
|
||||
|
@ -294,6 +294,8 @@ Object {
|
||||
"name": "page",
|
||||
},
|
||||
"plugins": Array [],
|
||||
"privateRuntimeConfig": Object {},
|
||||
"publicRuntimeConfig": Object {},
|
||||
"render": Object {
|
||||
"bundleRenderer": Object {
|
||||
"runInNewContext": false,
|
||||
|
@ -266,6 +266,8 @@ Object {
|
||||
"name": "page",
|
||||
},
|
||||
"plugins": Array [],
|
||||
"privateRuntimeConfig": Object {},
|
||||
"publicRuntimeConfig": Object {},
|
||||
"render": Object {
|
||||
"bundleRenderer": Object {
|
||||
"runInNewContext": undefined,
|
||||
@ -632,6 +634,8 @@ Object {
|
||||
"name": "page",
|
||||
},
|
||||
"plugins": Array [],
|
||||
"privateRuntimeConfig": Object {},
|
||||
"publicRuntimeConfig": Object {},
|
||||
"render": Object {
|
||||
"bundleRenderer": Object {
|
||||
"runInNewContext": undefined,
|
||||
|
@ -98,7 +98,7 @@ Vue.config.$nuxt.<%= globals.nuxt %> = true
|
||||
const errorHandler = Vue.config.errorHandler || console.error
|
||||
|
||||
// Create and mount App
|
||||
createApp().then(mountApp).catch(errorHandler)
|
||||
createApp(null, NUXT.config).then(mountApp).catch(errorHandler)
|
||||
|
||||
<% if (features.transitions) { %>
|
||||
function componentOption (component, key, ...args) {
|
||||
|
@ -66,7 +66,7 @@ const defaultTransition = <%=
|
||||
%><%= isTest ? '// eslint-disable-line' : '' %>
|
||||
<% } %>
|
||||
|
||||
async function createApp (ssrContext) {
|
||||
async function createApp(ssrContext, config = {}) {
|
||||
const router = await createRouter(ssrContext)
|
||||
|
||||
<% if (store) { %>
|
||||
@ -162,8 +162,7 @@ async function createApp (ssrContext) {
|
||||
ssrContext
|
||||
})
|
||||
|
||||
<% if (plugins.length) { %>
|
||||
const inject = function (key, value) {
|
||||
function inject(key, value) {
|
||||
if (!key) {
|
||||
throw new Error('inject(key, value) has no key provided')
|
||||
}
|
||||
@ -199,7 +198,9 @@ async function createApp (ssrContext) {
|
||||
}
|
||||
})
|
||||
}
|
||||
<% } %>
|
||||
|
||||
// Inject runtime config as $config
|
||||
inject('config', config)
|
||||
|
||||
<% if (store) { %>
|
||||
if (process.client) {
|
||||
|
@ -77,9 +77,10 @@ export default async (ssrContext) => {
|
||||
if (process.static && ssrContext.url) {
|
||||
ssrContext.url = ssrContext.url.split('?')[0]
|
||||
}
|
||||
|
||||
// Public runtime config
|
||||
ssrContext.nuxt.config = ssrContext.runtimeConfig.public
|
||||
// Create the app definition and the instance (created for each request)
|
||||
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext)
|
||||
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext, { ...ssrContext.runtimeConfig.public, ...ssrContext.runtimeConfig.private })
|
||||
const _app = new Vue(app)
|
||||
// Add ssr route path to nuxt context so we can account for page navigation between ssr and csr
|
||||
ssrContext.nuxt.routePath = app.context.route.path
|
||||
|
@ -291,6 +291,12 @@ export default class VueRenderer {
|
||||
renderContext.modern = modernMode === 'client' || isModernRequest(req, modernMode)
|
||||
}
|
||||
|
||||
// Set runtime config on renderContext
|
||||
renderContext.runtimeConfig = {
|
||||
private: renderContext.spa ? {} : { ...this.options.privateRuntimeConfig },
|
||||
public: { ...this.options.publicRuntimeConfig }
|
||||
}
|
||||
|
||||
// Call renderContext hook
|
||||
await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext)
|
||||
|
||||
|
@ -3,6 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'
|
||||
import VueMeta from 'vue-meta'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
import LRU from 'lru-cache'
|
||||
import devalue from '@nuxt/devalue'
|
||||
import { TARGETS, isModernRequest } from '@nuxt/utils'
|
||||
import BaseRenderer from './base'
|
||||
|
||||
@ -148,12 +149,16 @@ export default class SPARenderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize state (runtime config)
|
||||
let APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`
|
||||
|
||||
if (renderContext.staticAssetsBase) {
|
||||
// Full static, add window.__NUXT_STATIC__
|
||||
APP += `<script>window.__NUXT_STATIC__='${renderContext.staticAssetsBase}';window.${this.serverContext.globals.context}={spa:!0}</script>`
|
||||
APP += `<script>window.__NUXT_STATIC__='${renderContext.staticAssetsBase}'</script>`
|
||||
}
|
||||
APP += `<script>window.${this.serverContext.globals.context}=${devalue({
|
||||
config: renderContext.runtimeConfig.public,
|
||||
spa: true
|
||||
})}</script>`
|
||||
|
||||
// Prepare template params
|
||||
const templateParams = {
|
||||
|
45
test/dev/runtime-config.test.js
Normal file
45
test/dev/runtime-config.test.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { loadFixture, getPort, Nuxt } from '../utils'
|
||||
|
||||
let port
|
||||
const url = route => 'http://localhost:' + port + route
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('basic ssr', () => {
|
||||
beforeAll(async () => {
|
||||
const options = await loadFixture('runtime-config')
|
||||
nuxt = new Nuxt(options)
|
||||
await nuxt.ready()
|
||||
|
||||
port = await getPort()
|
||||
await nuxt.server.listen(port, '0.0.0.0')
|
||||
})
|
||||
|
||||
test('SSR payload', async () => {
|
||||
const window = await nuxt.server.renderAndGetWindow(url('/'))
|
||||
const payload = window.__NUXT__
|
||||
|
||||
expect(payload.config).toMatchObject({
|
||||
baseURL: '/api'
|
||||
})
|
||||
|
||||
expect(payload.data[0].serverConfig).toMatchObject({
|
||||
baseURL: 'https://google.com/api',
|
||||
API_SECRET: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
test('SPA payload ', async () => {
|
||||
const window = await nuxt.server.renderAndGetWindow(url('/?spa'))
|
||||
const payload = window.__NUXT__
|
||||
|
||||
expect(payload.config).toMatchObject({
|
||||
baseURL: '/api'
|
||||
})
|
||||
})
|
||||
|
||||
// Close server and ask nuxt to stop listening to file changes
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
})
|
@ -20,7 +20,7 @@ describe('nuxt minimal vue-app bundle size limit', () => {
|
||||
it('should stay within the size limit range', async () => {
|
||||
const filter = filename => filename === 'vue-app.nuxt.js'
|
||||
const legacyResourcesSize = await getResourcesSize(distDir, 'client', { filter })
|
||||
const LEGACY_JS_RESOURCES_KB_SIZE = 16.2
|
||||
const LEGACY_JS_RESOURCES_KB_SIZE = 16.5
|
||||
expect(legacyResourcesSize.uncompressed).toBeWithinSize(LEGACY_JS_RESOURCES_KB_SIZE)
|
||||
})
|
||||
})
|
||||
|
4
test/fixtures/runtime-config/.env
vendored
Normal file
4
test/fixtures/runtime-config/.env
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
BASE_URL=/api
|
||||
PUBLIC_URL=https://google.com
|
||||
SERVER_BASE_URL
|
||||
API_SECRET=1234
|
17
test/fixtures/runtime-config/nuxt.config.js
vendored
Normal file
17
test/fixtures/runtime-config/nuxt.config.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
export default {
|
||||
publicRuntimeConfig: {
|
||||
baseURL: process.env.BASE_URL
|
||||
},
|
||||
privateRuntimeConfig: {
|
||||
baseURL: '${PUBLIC_URL}${BASE_URL}',
|
||||
API_SECRET: ''
|
||||
},
|
||||
serverMiddleware: [
|
||||
(req, _, next) => {
|
||||
if (req.url.includes('?spa')) {
|
||||
req.spa = true
|
||||
}
|
||||
next()
|
||||
}
|
||||
]
|
||||
}
|
30
test/fixtures/runtime-config/pages/index.vue
vendored
Normal file
30
test/fixtures/runtime-config/pages/index.vue
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<pre v-text="JSON.stringify(config, null, 2)" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData ({ $config }) {
|
||||
return {
|
||||
serverConfig: $config
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
clientConfig: { please: 'wait' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
config () {
|
||||
return {
|
||||
client: this.clientConfig,
|
||||
server: this.serverConfig,
|
||||
$config: this.$config
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clientConfig = this.$config
|
||||
}
|
||||
}
|
||||
</script>
|
3
test/fixtures/runtime-config/runtime-config.test.js
vendored
Normal file
3
test/fixtures/runtime-config/runtime-config.test.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { buildFixture } from '../../utils/build'
|
||||
|
||||
buildFixture('runtime-config')
|
@ -2,6 +2,7 @@
|
||||
import path from 'path'
|
||||
import { defaultsDeep } from 'lodash'
|
||||
import { version as coreVersion } from '../../packages/core/package.json'
|
||||
import { loadNuxtConfig } from '../../packages/config/src/index'
|
||||
|
||||
export { Nuxt } from '../../packages/core/src/index'
|
||||
export { Builder } from '../../packages/builder/src/index'
|
||||
@ -13,25 +14,13 @@ export const version = `v${coreVersion}`
|
||||
|
||||
export const loadFixture = async function (fixture, overrides) {
|
||||
const rootDir = path.resolve(__dirname, '..', 'fixtures', fixture)
|
||||
let config = {}
|
||||
|
||||
try {
|
||||
config = await import(`../fixtures/${fixture}/nuxt.config`)
|
||||
config = config.default || config
|
||||
} catch (e) {
|
||||
// Ignore MODULE_NOT_FOUND
|
||||
if (e.code !== 'MODULE_NOT_FOUND') {
|
||||
throw e
|
||||
const config = await loadNuxtConfig({
|
||||
rootDir,
|
||||
configOverrides: {
|
||||
dev: false,
|
||||
test: true
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof config === 'function') {
|
||||
config = await config()
|
||||
}
|
||||
|
||||
config.rootDir = rootDir
|
||||
config.dev = false
|
||||
config.test = true
|
||||
})
|
||||
|
||||
// disable terser to speed-up fixture builds
|
||||
if (config.build) {
|
||||
|
15
yarn.lock
15
yarn.lock
@ -252,16 +252,16 @@
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@7.7.5", "@babel/parser@^7.7.0":
|
||||
version "7.7.5"
|
||||
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71"
|
||||
integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig==
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.8.6", "@babel/parser@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7"
|
||||
integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==
|
||||
|
||||
"@babel/parser@^7.7.0":
|
||||
version "7.7.5"
|
||||
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71"
|
||||
integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig==
|
||||
|
||||
"@babel/plugin-proposal-async-generator-functions@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
|
||||
@ -4951,6 +4951,11 @@ dot-prop@^5.2.0:
|
||||
dependencies:
|
||||
is-obj "^2.0.0"
|
||||
|
||||
dotenv@^8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||
|
||||
duplexer@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
||||
|
Loading…
Reference in New Issue
Block a user