mirror of
https://github.com/nuxt/nuxt.git
synced 2024-12-04 19:37:18 +00:00
912ef25fce
Co-authored-by: Xin Du (Clark) <clark.duxin@gmail.com>
593 lines
15 KiB
JavaScript
593 lines
15 KiB
JavaScript
import Vue from 'vue'
|
|
|
|
// window.{{globals.loadedCallback}} hook
|
|
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
|
|
if (process.client) {
|
|
window.<%= globals.readyCallback %>Cbs = []
|
|
window.<%= globals.readyCallback %> = (cb) => {
|
|
window.<%= globals.readyCallback %>Cbs.push(cb)
|
|
}
|
|
}
|
|
|
|
export function empty() {}
|
|
|
|
export function globalHandleError(error) {
|
|
if (Vue.config.errorHandler) {
|
|
Vue.config.errorHandler(error)
|
|
}
|
|
}
|
|
|
|
export function interopDefault(promise) {
|
|
return promise.then(m => m.default || m)
|
|
}
|
|
|
|
export function applyAsyncData(Component, asyncData) {
|
|
if (
|
|
// For SSR, we once all this function without second param to just apply asyncData
|
|
// Prevent doing this for each SSR request
|
|
!asyncData && Component.options.__hasNuxtData
|
|
) {
|
|
return
|
|
}
|
|
|
|
const ComponentData = Component.options._originDataFn || Component.options.data || function () { return {} }
|
|
Component.options._originDataFn = ComponentData
|
|
|
|
Component.options.data = function () {
|
|
const data = ComponentData.call(this)
|
|
if (this.$ssrContext) {
|
|
asyncData = this.$ssrContext.asyncData[Component.cid]
|
|
}
|
|
return { ...data, ...asyncData }
|
|
}
|
|
|
|
Component.options.__hasNuxtData = true
|
|
|
|
if (Component._Ctor && Component._Ctor.options) {
|
|
Component._Ctor.options.data = Component.options.data
|
|
}
|
|
}
|
|
|
|
export function sanitizeComponent(Component) {
|
|
// If Component already sanitized
|
|
if (Component.options && Component._Ctor === Component) {
|
|
return Component
|
|
}
|
|
if (!Component.options) {
|
|
Component = Vue.extend(Component) // fix issue #6
|
|
Component._Ctor = Component
|
|
} else {
|
|
Component._Ctor = Component
|
|
Component.extendOptions = Component.options
|
|
}
|
|
// For debugging purpose
|
|
if (!Component.options.name && Component.options.__file) {
|
|
Component.options.name = Component.options.__file
|
|
}
|
|
return Component
|
|
}
|
|
|
|
export function getMatchedComponents(route, matches = false) {
|
|
return Array.prototype.concat.apply([], route.matched.map((m, index) => {
|
|
return Object.keys(m.components).map((key) => {
|
|
matches && matches.push(index)
|
|
return m.components[key]
|
|
})
|
|
}))
|
|
}
|
|
|
|
export function getMatchedComponentsInstances(route, matches = false) {
|
|
return Array.prototype.concat.apply([], route.matched.map((m, index) => {
|
|
return Object.keys(m.instances).map((key) => {
|
|
matches && matches.push(index)
|
|
return m.instances[key]
|
|
})
|
|
}))
|
|
}
|
|
|
|
export function flatMapComponents(route, fn) {
|
|
return Array.prototype.concat.apply([], route.matched.map((m, index) => {
|
|
return Object.keys(m.components).reduce((promises, key) => {
|
|
if (m.components[key]) {
|
|
promises.push(fn(m.components[key], m.instances[key], m, key, index))
|
|
} else {
|
|
delete m.components[key]
|
|
}
|
|
return promises
|
|
}, [])
|
|
}))
|
|
}
|
|
|
|
export function resolveRouteComponents(route) {
|
|
return Promise.all(
|
|
flatMapComponents(route, async (Component, _, match, key) => {
|
|
// If component is a function, resolve it
|
|
if (typeof Component === 'function' && !Component.options) {
|
|
Component = await Component()
|
|
}
|
|
match.components[key] = sanitizeComponent(Component)
|
|
return match.components[key]
|
|
})
|
|
)
|
|
}
|
|
|
|
export async function getRouteData(route) {
|
|
if (!route) {
|
|
return
|
|
}
|
|
// Make sure the components are resolved (code-splitting)
|
|
await resolveRouteComponents(route)
|
|
// Send back a copy of route with meta based on Component definition
|
|
return {
|
|
...route,
|
|
meta: getMatchedComponents(route).map((Component, index) => {
|
|
return { ...Component.options.meta, ...(route.matched[index] || {}).meta }
|
|
})
|
|
}
|
|
}
|
|
|
|
export async function setContext(app, context) {
|
|
// If context not defined, create it
|
|
if (!app.context) {
|
|
app.context = {
|
|
isStatic: process.static,
|
|
isDev: <%= isDev %>,
|
|
isHMR: false,
|
|
app,
|
|
<%= (store ? 'store: app.store,' : '') %>
|
|
payload: context.payload,
|
|
error: context.error,
|
|
base: '<%= router.base %>',
|
|
env: <%= JSON.stringify(env) %><%= isTest ? '// eslint-disable-line' : '' %>
|
|
}
|
|
// Only set once
|
|
if (context.req) {
|
|
app.context.req = context.req
|
|
}
|
|
if (context.res) {
|
|
app.context.res = context.res
|
|
}
|
|
if (context.ssrContext) {
|
|
app.context.ssrContext = context.ssrContext
|
|
}
|
|
app.context.redirect = (status, path, query) => {
|
|
if (!status) {
|
|
return
|
|
}
|
|
app.context._redirected = true
|
|
// if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
|
|
let pathType = typeof path
|
|
if (typeof status !== 'number' && (pathType === 'undefined' || pathType === 'object')) {
|
|
query = path || {}
|
|
path = status
|
|
pathType = typeof path
|
|
status = 302
|
|
}
|
|
if (pathType === 'object') {
|
|
path = app.router.resolve(path).route.fullPath
|
|
}
|
|
// "/absolute/route", "./relative/route" or "../relative/route"
|
|
if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(path)) {
|
|
app.context.next({
|
|
path: path,
|
|
query: query,
|
|
status: status
|
|
})
|
|
} else {
|
|
path = formatUrl(path, query)
|
|
if (process.server) {
|
|
app.context.next({
|
|
path: path,
|
|
status: status
|
|
})
|
|
}
|
|
if (process.client) {
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Location/replace
|
|
window.location.replace(path)
|
|
|
|
// Throw a redirect error
|
|
throw new Error('ERR_REDIRECT')
|
|
}
|
|
}
|
|
}
|
|
if (process.server) {
|
|
app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
|
|
}
|
|
if (process.client) {
|
|
app.context.nuxtState = window.<%= globals.context %>
|
|
}
|
|
}
|
|
|
|
// Dynamic keys
|
|
const [currentRouteData, fromRouteData] = await Promise.all([
|
|
getRouteData(context.route),
|
|
getRouteData(context.from)
|
|
])
|
|
|
|
if (context.route) {
|
|
app.context.route = currentRouteData
|
|
}
|
|
|
|
if (context.from) {
|
|
app.context.from = fromRouteData
|
|
}
|
|
|
|
app.context.next = context.next
|
|
app.context._redirected = false
|
|
app.context._errored = false
|
|
app.context.isHMR = !!context.isHMR
|
|
app.context.params = app.context.route.params || {}
|
|
app.context.query = app.context.route.query || {}
|
|
}
|
|
|
|
export function middlewareSeries(promises, appContext) {
|
|
if (!promises.length || appContext._redirected || appContext._errored) {
|
|
return Promise.resolve()
|
|
}
|
|
return promisify(promises[0], appContext)
|
|
.then(() => {
|
|
return middlewareSeries(promises.slice(1), appContext)
|
|
})
|
|
}
|
|
|
|
export function promisify(fn, context) {
|
|
let promise
|
|
if (fn.length === 2) {
|
|
<% if (isDev) { %>
|
|
console.warn('Callback-based asyncData, fetch or middleware calls are deprecated. ' +
|
|
'Please switch to promises or async/await syntax')
|
|
<% } %>
|
|
|
|
// fn(context, callback)
|
|
promise = new Promise((resolve) => {
|
|
fn(context, function (err, data) {
|
|
if (err) {
|
|
context.error(err)
|
|
}
|
|
data = data || {}
|
|
resolve(data)
|
|
})
|
|
})
|
|
} else {
|
|
promise = fn(context)
|
|
}
|
|
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) {
|
|
promise = Promise.resolve(promise)
|
|
}
|
|
return promise
|
|
}
|
|
|
|
// Imported from vue-router
|
|
export function getLocation(base, mode) {
|
|
let path = decodeURI(window.location.pathname)
|
|
if (mode === 'hash') {
|
|
return window.location.hash.replace(/^#\//, '')
|
|
}
|
|
if (base && path.indexOf(base) === 0) {
|
|
path = path.slice(base.length)
|
|
}
|
|
return (path || '/') + window.location.search + window.location.hash
|
|
}
|
|
|
|
export function urlJoin() {
|
|
return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/')
|
|
}
|
|
|
|
// Imported from path-to-regexp
|
|
|
|
/**
|
|
* Compile a string to a template function for the path.
|
|
*
|
|
* @param {string} str
|
|
* @param {Object=} options
|
|
* @return {!function(Object=, Object=)}
|
|
*/
|
|
export function compile(str, options) {
|
|
return tokensToFunction(parse(str, options))
|
|
}
|
|
|
|
export function getQueryDiff(toQuery, fromQuery) {
|
|
const diff = {}
|
|
const queries = { ...toQuery, ...fromQuery }
|
|
for (const k in queries) {
|
|
if (String(toQuery[k]) !== String(fromQuery[k])) {
|
|
diff[k] = true
|
|
}
|
|
}
|
|
return diff
|
|
}
|
|
|
|
export function normalizeError(err) {
|
|
let message
|
|
if (!(err.message || typeof err === 'string')) {
|
|
try {
|
|
message = JSON.stringify(err, null, 2)
|
|
} catch (e) {
|
|
message = `[${err.constructor.name}]`
|
|
}
|
|
} else {
|
|
message = err.message || err
|
|
}
|
|
return {
|
|
...err,
|
|
message: message,
|
|
statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The main path matching regexp utility.
|
|
*
|
|
* @type {RegExp}
|
|
*/
|
|
const PATH_REGEXP = new RegExp([
|
|
// Match escaped characters that would otherwise appear in future matches.
|
|
// This allows the user to escape special characters that won't transform.
|
|
'(\\\\.)',
|
|
// Match Express-style parameters and un-named parameters with a prefix
|
|
// and optional suffixes. Matches appear as:
|
|
//
|
|
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
|
|
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
|
|
// "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
|
|
'([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
|
|
].join('|'), 'g')
|
|
|
|
/**
|
|
* Parse a string for the raw tokens.
|
|
*
|
|
* @param {string} str
|
|
* @param {Object=} options
|
|
* @return {!Array}
|
|
*/
|
|
function parse(str, options) {
|
|
const tokens = []
|
|
let key = 0
|
|
let index = 0
|
|
let path = ''
|
|
const defaultDelimiter = (options && options.delimiter) || '/'
|
|
let res
|
|
|
|
while ((res = PATH_REGEXP.exec(str)) != null) {
|
|
const m = res[0]
|
|
const escaped = res[1]
|
|
const offset = res.index
|
|
path += str.slice(index, offset)
|
|
index = offset + m.length
|
|
|
|
// Ignore already escaped sequences.
|
|
if (escaped) {
|
|
path += escaped[1]
|
|
continue
|
|
}
|
|
|
|
const next = str[index]
|
|
const prefix = res[2]
|
|
const name = res[3]
|
|
const capture = res[4]
|
|
const group = res[5]
|
|
const modifier = res[6]
|
|
const asterisk = res[7]
|
|
|
|
// Push the current path onto the tokens.
|
|
if (path) {
|
|
tokens.push(path)
|
|
path = ''
|
|
}
|
|
|
|
const partial = prefix != null && next != null && next !== prefix
|
|
const repeat = modifier === '+' || modifier === '*'
|
|
const optional = modifier === '?' || modifier === '*'
|
|
const delimiter = res[2] || defaultDelimiter
|
|
const pattern = capture || group
|
|
|
|
tokens.push({
|
|
name: name || key++,
|
|
prefix: prefix || '',
|
|
delimiter: delimiter,
|
|
optional: optional,
|
|
repeat: repeat,
|
|
partial: partial,
|
|
asterisk: !!asterisk,
|
|
pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
|
|
})
|
|
}
|
|
|
|
// Match any characters still remaining.
|
|
if (index < str.length) {
|
|
path += str.substr(index)
|
|
}
|
|
|
|
// If the path exists, push it onto the end.
|
|
if (path) {
|
|
tokens.push(path)
|
|
}
|
|
|
|
return tokens
|
|
}
|
|
|
|
/**
|
|
* Prettier encoding of URI path segments.
|
|
*
|
|
* @param {string}
|
|
* @return {string}
|
|
*/
|
|
function encodeURIComponentPretty(str) {
|
|
return encodeURI(str).replace(/[/?#]/g, (c) => {
|
|
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
|
|
*
|
|
* @param {string}
|
|
* @return {string}
|
|
*/
|
|
function encodeAsterisk(str) {
|
|
return encodeURI(str).replace(/[?#]/g, (c) => {
|
|
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Expose a method for transforming tokens into the path function.
|
|
*/
|
|
function tokensToFunction(tokens) {
|
|
// Compile all the tokens into regexps.
|
|
const matches = new Array(tokens.length)
|
|
|
|
// Compile all the patterns before compilation.
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
if (typeof tokens[i] === 'object') {
|
|
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$')
|
|
}
|
|
}
|
|
|
|
return function (obj, opts) {
|
|
let path = ''
|
|
const data = obj || {}
|
|
const options = opts || {}
|
|
const encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
const token = tokens[i]
|
|
|
|
if (typeof token === 'string') {
|
|
path += token
|
|
|
|
continue
|
|
}
|
|
|
|
const value = data[token.name || 'pathMatch']
|
|
let segment
|
|
|
|
if (value == null) {
|
|
if (token.optional) {
|
|
// Prepend partial segment prefixes.
|
|
if (token.partial) {
|
|
path += token.prefix
|
|
}
|
|
|
|
continue
|
|
} else {
|
|
throw new TypeError('Expected "' + token.name + '" to be defined')
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
if (!token.repeat) {
|
|
throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
|
|
}
|
|
|
|
if (value.length === 0) {
|
|
if (token.optional) {
|
|
continue
|
|
} else {
|
|
throw new TypeError('Expected "' + token.name + '" to not be empty')
|
|
}
|
|
}
|
|
|
|
for (let j = 0; j < value.length; j++) {
|
|
segment = encode(value[j])
|
|
|
|
if (!matches[i].test(segment)) {
|
|
throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
|
|
}
|
|
|
|
path += (j === 0 ? token.prefix : token.delimiter) + segment
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
segment = token.asterisk ? encodeAsterisk(value) : encode(value)
|
|
|
|
if (!matches[i].test(segment)) {
|
|
throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
|
|
}
|
|
|
|
path += token.prefix + segment
|
|
}
|
|
|
|
return path
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape a regular expression string.
|
|
*
|
|
* @param {string} str
|
|
* @return {string}
|
|
*/
|
|
function escapeString(str) {
|
|
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
|
|
}
|
|
|
|
/**
|
|
* Escape the capturing group by escaping special characters and meaning.
|
|
*
|
|
* @param {string} group
|
|
* @return {string}
|
|
*/
|
|
function escapeGroup(group) {
|
|
return group.replace(/([=!:$/()])/g, '\\$1')
|
|
}
|
|
|
|
/**
|
|
* Format given url, append query to url query string
|
|
*
|
|
* @param {string} url
|
|
* @param {string} query
|
|
* @return {string}
|
|
*/
|
|
function formatUrl(url, query) {
|
|
let protocol
|
|
const index = url.indexOf('://')
|
|
if (index !== -1) {
|
|
protocol = url.substring(0, index)
|
|
url = url.substring(index + 3)
|
|
} else if (url.startsWith('//')) {
|
|
url = url.substring(2)
|
|
}
|
|
|
|
let parts = url.split('/')
|
|
let result = (protocol ? protocol + '://' : '//') + parts.shift()
|
|
|
|
let path = parts.filter(Boolean).join('/')
|
|
let hash
|
|
parts = path.split('#')
|
|
if (parts.length === 2) {
|
|
path = parts[0]
|
|
hash = parts[1]
|
|
}
|
|
|
|
result += path ? '/' + path : ''
|
|
|
|
if (query && JSON.stringify(query) !== '{}') {
|
|
result += (url.split('?').length === 2 ? '&' : '?') + formatQuery(query)
|
|
}
|
|
result += hash ? '#' + hash : ''
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Transform data object to query string
|
|
*
|
|
* @param {object} query
|
|
* @return {string}
|
|
*/
|
|
function formatQuery(query) {
|
|
return Object.keys(query).sort().map((key) => {
|
|
const val = query[key]
|
|
if (val == null) {
|
|
return ''
|
|
}
|
|
if (Array.isArray(val)) {
|
|
return val.slice().map(val2 => [key, '=', val2].join('')).join('&')
|
|
}
|
|
return key + '=' + val
|
|
}).filter(Boolean).join('&')
|
|
}
|