feat: configurable global name (#4012)

Co-authored-by: JuliaNeumann <jn.julianeumann@gmail.com>
This commit is contained in:
Jonas Galvez 2018-10-09 09:07:23 -03:00 committed by Pooya Parsa
parent c2fde1958a
commit a3dd7dad6b
27 changed files with 184 additions and 90 deletions

View File

@ -37,7 +37,7 @@ export default {
return h('div',{
domProps: {
id: '__nuxt'
id: '<%= globals.id %>'
}
}, [
<% if (loading) { %>loadingEl,<% } %>
@ -53,10 +53,13 @@ export default {
},
created () {
// Add this.$nuxt in child instances
Vue.prototype.$nuxt = this
Vue.prototype.<%= globals.nuxt %> = this
// add to window so we can listen when ready
if (typeof window !== 'undefined') {
window.$nuxt = this
window.<%= globals.nuxt %> = this
<% if (globals.nuxt !== '$nuxt') { %>
window.$nuxt = true
<% } %>
}
// Add $nuxt.error()
this.error = this.nuxt.error
@ -100,8 +103,8 @@ export default {
return resolvedLayouts[_layout]
})
.catch((e) => {
if (this.$nuxt) {
return this.$nuxt.error({ statusCode: 500, message: e.message })
if (this.<%= globals.nuxt %>) {
return this.<%= globals.nuxt %>.error({ statusCode: 500, message: e.message })
}
})
}

View File

@ -27,14 +27,15 @@ let router
<% if (store) { %>let store<% } %>
// Try to rehydrate SSR data from window
const NUXT = window.__NUXT__ || {}
const NUXT = window.<%= globals.context %> || {}
Object.assign(Vue.config, <%= serialize(vue.config) %>)
<% if (debug || mode === 'spa') { %>
// Setup global Vue error handler
const defaultErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (err, vm, info, ...rest) => {
if (!Vue.config.$nuxt) {
const defaultErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (err, vm, info, ...rest) => {
const nuxtError = {
statusCode: err.statusCode || err.name || 'Whoops!',
message: err.message || err.toString()
@ -49,10 +50,16 @@ Vue.config.errorHandler = (err, vm, info, ...rest) => {
return handled
}
if (vm && vm.$root) {
const nuxtApp = Object.keys(Vue.config.$nuxt)
.find(nuxtInstance => vm.$root[nuxtInstance])
// Show Nuxt Error Page
if (vm && vm.$root && vm.$root.$nuxt && vm.$root.$nuxt.error && info !== 'render function') {
vm.$root.$nuxt.error(nuxtError)
if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') {
vm.$root[nuxtApp].error(nuxtError)
}
}
if (typeof defaultErrorHandler === 'function') {
return handled
}
@ -63,7 +70,11 @@ Vue.config.errorHandler = (err, vm, info, ...rest) => {
} else {
console.error(err.message || nuxtError.message)
}
}
Vue.config.$nuxt = {}
}
Vue.config.$nuxt.<%= globals.nuxt %> = true
<% } %>
// Create and mount App
@ -142,7 +153,7 @@ async function loadAsyncComponents (to, from, next) {
err = err || {}
const statusCode = (err.statusCode || err.status || (err.response && err.response.status) || 500)
this.error({ statusCode, message: err.message })
this.$nuxt.$emit('routeChanged', to, from, err)
this.<%= globals.nuxt %>.$emit('routeChanged', to, from, err)
next(false)
}
}
@ -401,7 +412,7 @@ async function render (to, from, next) {
if (!error) {
error = {}
} else if (error.message === 'ERR_REDIRECT') {
return this.$nuxt.$emit('routeChanged', to, from, error)
return this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
}
_lastPaths = []
const errorResponseStatus = (error.response && error.response.status)
@ -417,7 +428,7 @@ async function render (to, from, next) {
await this.loadLayout(layout)
this.error(error)
this.$nuxt.$emit('routeChanged', to, from, error)
this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
next(false)
}
}
@ -489,19 +500,19 @@ function fixPrepatch(to, ___) {
}
function nuxtReady (_app) {
window._nuxtReadyCbs.forEach((cb) => {
window.<%= globals.readyCallback %>Cbs.forEach((cb) => {
if (typeof cb === 'function') {
cb(_app)
}
})
// Special JSDOM
if (typeof window._onNuxtLoaded === 'function') {
window._onNuxtLoaded(_app)
if (typeof window.<%= globals.loadedCallback %> === 'function') {
window.<%= globals.loadedCallback %>(_app)
}
// Add router hooks
router.afterEach((to, from) => {
// Wait for fixPrepatch + $data updates
Vue.nextTick(() => _app.$nuxt.$emit('routeChanged', to, from))
Vue.nextTick(() => _app.<%= globals.nuxt %>.$emit('routeChanged', to, from))
})
}
@ -523,7 +534,7 @@ function getNuxtChildComponents($parent, $components = []) {
function hotReloadAPI (_app) {
if (!module.hot) return
let $components = getNuxtChildComponents(_app.$nuxt, [])
let $components = getNuxtChildComponents(_app.<%= globals.nuxt %>, [])
$components.forEach(addHotReload.bind(_app))
}
@ -623,11 +634,11 @@ async function mountApp(__app) {
// Mounts Vue app to DOM element
const mount = () => {
_app.$mount('#__nuxt')
_app.$mount('#<%= globals.id %>')
// Listen for first Vue update
Vue.nextTick(() => {
// Call window.onNuxtReady callbacks
// Call window.{{globals.readyCallback}} callbacks
nuxtReady(_app)
<% if (isDev) { %>
// Enable hot reloading

View File

@ -5,8 +5,8 @@ export default {
render (h, { parent, data, props }) {
data.nuxtChild = true
const _parent = parent
const transitions = parent.$nuxt.nuxt.transitions
const defaultTransition = parent.$nuxt.nuxt.defaultTransition
const transitions = parent.<%= globals.nuxt %>.nuxt.transitions
const defaultTransition = parent.<%= globals.nuxt %>.nuxt.defaultTransition
let depth = 0
while (parent) {
@ -33,8 +33,8 @@ export default {
let beforeEnter = listeners.beforeEnter
listeners.beforeEnter = (el) => {
// Ensure to trigger scroll event after calling scrollBehavior
window.$nuxt.$nextTick(() => {
window.$nuxt.$emit('triggerScroll')
window.<%= globals.nuxt %>.$nextTick(() => {
window.<%= globals.nuxt %>.$emit('triggerScroll')
})
if (beforeEnter) return beforeEnter.call(_parent, el)
}

View File

@ -135,7 +135,7 @@ async function createApp (ssrContext) {
store[key] = app[key]
<% } %>
// Check if plugin not already installed
const installKey = '__nuxt_' + key + '_installed__'
const installKey = '__<%= globals.pluginPrefix %>_' + key + '_installed__'
if (Vue[installKey]) return
Vue[installKey] = true
// Call Vue.use() to install the plugin into vm
@ -153,8 +153,8 @@ async function createApp (ssrContext) {
<% if (store) { %>
if (process.client) {
// Replace store state before plugins execution
if (window.__NUXT__ && window.__NUXT__.state) {
store.replaceState(window.__NUXT__.state)
if (window.<%= globals.context %> && window.<%= globals.context %>.state) {
store.replaceState(window.<%= globals.context %>.state)
}
}
<% } %>

View File

@ -67,7 +67,7 @@ const scrollBehavior = function (to, from, savedPosition) {
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
window.$nuxt.$once('triggerScroll', () => {
window.<%= globals.nuxt %>.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {

View File

@ -46,7 +46,7 @@ export default async (ssrContext) => {
ssrContext.next = createNext(ssrContext)
// Used for beforeNuxtRender({ Components, nuxtState })
ssrContext.beforeRenderFns = []
// Nuxt object (window.__NUXT__)
// Nuxt object (window{{globals.context}}, defaults to window.__NUXT__)
ssrContext.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
// Create the app definition and the instance (created for each request)
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext)
@ -125,7 +125,6 @@ export default async (ssrContext) => {
await _app.loadLayout(layout)
if (ssrContext.nuxt.error) return renderErrorPage()
layout = _app.setLayout(layout)
// ...Set layout to __NUXT__
ssrContext.nuxt.layout = _app.layoutName
/*

View File

@ -2,12 +2,12 @@ import Vue from 'vue'
const noopData = () => ({})
// window.onNuxtReady(() => console.log('Ready')) hook
// window.{{globals.loadedCallback}} hook
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
if (process.client) {
window._nuxtReadyCbs = []
window.onNuxtReady = (cb) => {
window._nuxtReadyCbs.push(cb)
window.<%= globals.readyCallback %>Cbs = []
window.<%= globals.readyCallback %> = (cb) => {
window.<%= globals.readyCallback %>Cbs.push(cb)
}
}
@ -134,7 +134,6 @@ export async function setContext(app, context) {
if (!status) {
return
}
// Used in middleware
app.context._redirected = true
// if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
let pathType = typeof path
@ -171,8 +170,12 @@ export async function setContext(app, context) {
}
}
}
if (process.server) app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
if (process.client) app.context.nuxtState = window.__NUXT__
if (process.server) {
app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
}
if (process.client) {
app.context.nuxtState = window.<%= globals.context %>
}
}
// Dynamic keys
app.context.next = context.next

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -1,5 +1,5 @@
<style>
body, html, #__nuxt {
body, html, #<%= globals.id %> {
background: <%= options.background %>;
width: 100%;
height: 100%;

View File

@ -16,9 +16,19 @@ import Glob from 'glob'
import upath from 'upath'
import consola from 'consola'
import { r, wp, wChunk, createRoutes, parallel, sequence, relativeTo, waitFor } from '../common/utils'
import Options from '../common/options'
import {
r,
wp,
wChunk,
createRoutes,
parallel,
sequence,
relativeTo,
waitFor,
determineGlobals
} from '../common/utils'
import Options from '../common/options'
import PerfLoader from './webpack/utils/perf-loader'
import ClientWebpackConfig from './webpack/client'
import ServerWebpackConfig from './webpack/server'
@ -30,6 +40,7 @@ export default class Builder {
this.nuxt = nuxt
this.isStatic = false // Flag to know if the build is for a generated app
this.options = nuxt.options
this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)
// Fields that set on build
this.compilers = []
@ -214,6 +225,8 @@ export default class Builder {
head: this.options.head,
middleware: fsExtra.existsSync(path.join(this.options.srcDir, this.options.dir.middleware)),
store: this.options.store,
globalName: this.options.globalName,
globals: this.globals,
css: this.options.css,
plugins: this.plugins,
appPath: './App.js',

View File

@ -1,5 +1,6 @@
import path from 'path'
import fs from 'fs'
import _ from 'lodash'
import env from 'std-env'
const nuxtDir = fs.existsSync(path.resolve(__dirname, '..', 'package.json'))
@ -14,6 +15,17 @@ export default {
// Mode
mode: 'universal',
// Global name
globalName: `nuxt`,
globals: {
id: globalName => `__${globalName}`,
nuxt: globalName => `$${globalName}`,
context: globalName => `__${globalName.toUpperCase()}__`,
pluginPrefix: globalName => globalName,
readyCallback: globalName => `on${_.capitalize(globalName)}Ready`,
loadedCallback: globalName => `_on${_.capitalize(globalName)}Loaded`
},
// Server options
server: {
https: false,

View File

@ -43,6 +43,10 @@ Options.from = function (_options) {
options.extensions = [options.extensions]
}
options.globalName = (_.isString(options.globalName) && /^[a-zA-Z]+$/.test(options.globalName))
? options.globalName.toLowerCase()
: 'nuxt'
// Resolve rootDir
options.rootDir = hasValue(options.rootDir) ? path.resolve(options.rootDir) : process.cwd()

View File

@ -349,3 +349,15 @@ export const guardDir = function guardDir(options, key1, key2) {
throw new Error(errorMessage)
}
}
export const determineGlobals = function determineGlobals(globalName, globals) {
const _globals = {}
for (const global in globals) {
if (typeof globals[global] === 'function') {
_globals[global] = globals[global](globalName)
} else {
_globals[global] = globals[global]
}
}
return _globals
}

View File

@ -10,7 +10,7 @@ import connect from 'connect'
import launchMiddleware from 'launch-editor-middleware'
import consola from 'consola'
import { isUrl, timeout, waitFor } from '../common/utils'
import { isUrl, timeout, waitFor, determineGlobals } from '../common/utils'
import defaults from '../common/nuxt.config'
import MetaRenderer from './meta'
@ -23,6 +23,7 @@ export default class Renderer {
constructor(nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)
// Will be set by createRenderer
this.bundleRenderer = null
@ -311,7 +312,7 @@ export default class Renderer {
getPreloadFiles
} = await this.metaRenderer.render(context)
const APP =
`<div id="__nuxt">${this.resources.loadingHTML}</div>` + BODY_SCRIPTS
`<div id="${this.globals.id}">${this.resources.loadingHTML}</div>` + BODY_SCRIPTS
// Detect 404 errors
if (
@ -341,7 +342,7 @@ export default class Renderer {
let APP = await this.bundleRenderer.renderToString(context)
if (!context.nuxt.serverRendered) {
APP = '<div id="__nuxt"></div>'
APP = `<div id="${this.globals.id}"></div>`
}
const m = context.meta.inject()
let HEAD =
@ -361,7 +362,7 @@ export default class Renderer {
await this.nuxt.callHook('render:routeContext', context.nuxt)
const serializedSession = `window.__NUXT__=${devalue(context.nuxt)};`
const serializedSession = `window.${this.globals.context}=${devalue(context.nuxt)};`
const cspScriptSrcHashSet = new Set()
if (this.options.render.csp) {
@ -430,7 +431,7 @@ export default class Renderer {
const { window } = await jsdom.JSDOM.fromURL(url, options)
// If Nuxt could not be loaded (error from the server-side)
const nuxtExists = window.document.body.innerHTML.includes(
this.options.render.ssr ? 'window.__NUXT__' : '<div id="__nuxt">'
this.options.render.ssr ? `window.${this.globals.context}` : `<div id="${this.globals.id}">`
)
/* istanbul ignore if */
if (!nuxtExists) {
@ -439,8 +440,9 @@ export default class Renderer {
throw error
}
// Used by nuxt.js to say when the components are loaded and the app ready
const onNuxtLoaded = this.globals.loadedCallback
await timeout(new Promise((resolve) => {
window._onNuxtLoaded = () => resolve(window)
window[onNuxtLoaded] = () => resolve(window)
}), 20000, 'Components loading in renderAndGetWindow was not completed in 20s')
if (options.virtualConsole) {
// after window initialized successfully

View File

@ -91,5 +91,9 @@ export default {
static: {
maxAge: '1y'
}
},
globalName: 'noxxt',
globals: {
id: 'custom-nuxt-id'
}
}

View File

@ -21,4 +21,20 @@ describe('basic config defaults', () => {
expect(options.build.vendor).toBeUndefined()
expect(consola.warn).toHaveBeenCalledWith('vendor has been deprecated due to webpack4 optimization')
})
test('globalName uses nuxt as default if not set', () => {
const options = Options.from({})
expect(options.globalName).toEqual('nuxt')
})
test('globalName uses nuxt as default if set to something other than only letters', () => {
let options = Options.from({ globalName: '12foo4' })
expect(options.globalName).toEqual('nuxt')
options = Options.from({ globalName: 'foo bar' })
expect(options.globalName).toEqual('nuxt')
options = Options.from({ globalName: 'foo?' })
expect(options.globalName).toEqual('nuxt')
})
})

View File

@ -51,11 +51,22 @@ describe('with-config', () => {
expect(html.includes('::-webkit-input-placeholder')).toBe(true)
})
test('/test/ (custom globalName)', async () => {
const window = await nuxt.renderAndGetWindow(url('/test/'))
const html = window.document.body.innerHTML
expect(html.includes('id="custom-nuxt-id">')).toBe(true)
expect(html.includes('id="__nuxt">')).toBe(false)
expect(window.__NOXXT__).toBeDefined()
expect(window.__NUXT__).toBeUndefined()
expect(window.$noxxt).toBeDefined()
expect(window.$nuxt).toBeDefined() // for Vue Dev Tools detection
})
test('/test/ (router base)', async () => {
const window = await nuxt.renderAndGetWindow(url('/test/'))
const html = window.document.body.innerHTML
expect(window.__NUXT__.layout).toBe('default')
expect(window.__NOXXT__.layout).toBe('default')
expect(html.includes('<h1>Default layout</h1>')).toBe(true)
expect(html.includes('<h1>I have custom configurations</h1>')).toBe(true)
@ -65,7 +76,7 @@ describe('with-config', () => {
test('/test/about (custom layout)', async () => {
const window = await nuxt.renderAndGetWindow(url('/test/about'))
const html = window.document.body.innerHTML
expect(window.__NUXT__.layout).toBe('custom')
expect(window.__NOXXT__.layout).toBe('custom')
expect(html.includes('<h1>Custom layout</h1>')).toBe(true)
expect(html.includes('<h1>About page</h1>')).toBe(true)
})
@ -73,7 +84,7 @@ describe('with-config', () => {
test('/test/desktop (custom layout in desktop folder)', async () => {
const window = await nuxt.renderAndGetWindow(url('/test/desktop'))
const html = window.document.body.innerHTML
expect(window.__NUXT__.layout).toBe('desktop/default')
expect(window.__NOXXT__.layout).toBe('desktop/default')
expect(html.includes('<h1>Default desktop layout</h1>')).toBe(true)
expect(html.includes('<h1>Desktop page</h1>')).toBe(true)
})
@ -81,7 +92,7 @@ describe('with-config', () => {
test('/test/mobile (custom layout in mobile folder)', async () => {
const window = await nuxt.renderAndGetWindow(url('/test/mobile'))
const html = window.document.body.innerHTML
expect(window.__NUXT__.layout).toBe('mobile/default')
expect(window.__NOXXT__.layout).toBe('mobile/default')
expect(html.includes('<h1>Default mobile layout</h1>')).toBe(true)
expect(html.includes('<h1>Mobile page</h1>')).toBe(true)
})

View File

@ -19,11 +19,12 @@ export default class Browser {
await this.browser.close()
}
async page(url) {
async page(url, globalName = 'nuxt') {
if (!this.browser) throw new Error('Please call start() before page(url)')
const page = await this.browser.newPage()
await page.goto(url)
await page.waitForFunction('!!window.$nuxt')
page.$nuxtGlobalHandle = `window.$${globalName}`
await page.waitForFunction(`!!${page.$nuxtGlobalHandle}`)
page.html = () =>
page.evaluate(() => window.document.documentElement.outerHTML)
page.$text = selector => page.$eval(selector, el => el.textContent)
@ -37,21 +38,24 @@ export default class Browser {
(els, attr) => els.map(el => el.getAttribute(attr)),
attr
)
page.$nuxt = await page.evaluateHandle('window.$nuxt')
page.$nuxt = await page.evaluateHandle(page.$nuxtGlobalHandle)
page.nuxt = {
async navigate(path, waitEnd = true) {
const hook = page.evaluate(() => {
return new Promise(resolve =>
window.$nuxt.$once('routeChanged', resolve)
const hook = page.evaluate(`
new Promise(resolve =>
${page.$nuxtGlobalHandle}.$once('routeChanged', resolve)
).then(() => new Promise(resolve => setTimeout(resolve, 50)))
})
`)
await page.evaluate(
($nuxt, path) => $nuxt.$router.push(path),
page.$nuxt,
path
)
if (waitEnd) await hook
if (waitEnd) {
await hook
}
return { hook }
},
routeData() {