Nuxt/lib/app/utils.js

476 lines
13 KiB
JavaScript

import Vue from 'vue'
const noopData = () => ({})
// window.onNuxtReady(() => console.log('Ready')) hook
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
if (process.browser) {
window._nuxtReadyCbs = []
window.onNuxtReady = function (cb) {
window._nuxtReadyCbs.push(cb)
}
}
export function applyAsyncData(Component, asyncData) {
const ComponentData = Component.options.data || noopData
// Prevent calling this method for each request on SSR context
if (!asyncData && Component.options.hasAsyncData) {
return
}
Component.options.hasAsyncData = true
Component.options.data = function () {
const data = ComponentData.call(this)
if (this.$ssrContext) {
asyncData = this.$ssrContext.asyncData[Component.cid]
}
return { ...data, ...asyncData }
}
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) {
return [].concat.apply([], route.matched.map(function (m) {
return Object.keys(m.components).map(function (key) {
return m.components[key]
})
}))
}
export function getMatchedComponentsInstances(route) {
return [].concat.apply([], route.matched.map(function (m) {
return Object.keys(m.instances).map(function (key) {
return m.instances[key]
})
}))
}
function getRouteRecordWithParamNames (route) {
return route.matched.map(m => {
var paramNames = m.path.match(new RegExp(':[^\\/\\?]+', 'g'))
if (paramNames !== null) {
paramNames = paramNames.map(function (name) {
return name.substring(1)
})
}
return {
routeRecord: m,
paramNames: paramNames
}
})
}
export function getChangedComponentsInstances (to, from) {
var records = getRouteRecordWithParamNames(to)
var r = []
var parentChange = false
for (var i = 0; i < records.length; i++ ) {
var paramNames = records[i].paramNames
var instances = records[i].routeRecord.instances
instances = Object.keys(instances).map(function (key) {
return instances[key]
})
if (parentChange) {
r = [].concat(r, instances)
} else if (paramNames !== null) {
for (var pi in paramNames) {
var name = paramNames[pi]
if (to.params[name] !== from.params[name]) {
parentChange = true
r = [].concat(r, instances)
break
}
}
}
}
return r
}
export function flatMapComponents(route, fn) {
return Array.prototype.concat.apply([], route.matched.map(function (m, index) {
return Object.keys(m.components).map(function (key) {
return fn(m.components[key], m.instances[key], m, key, index)
})
}))
}
export async function resolveRouteComponents(route) {
await Promise.all(
flatMapComponents(route, async (Component, _, match, key) => {
// If component is a function, resolve it
if (typeof Component === 'function' && !Component.options) {
Component = await Component()
}
return match.components[key] = sanitizeComponent(Component)
})
)
}
async function getRouteData(route) {
// 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) => {
return Component.options.meta || {}
})
}
}
export async function setContext(app, context) {
const route = (context.to ? context.to : context.route)
// If context not defined, create it
if (!app.context) {
app.context = {
get isServer() {
console.warn('context.isServer has been deprecated, please use process.server instead.')
return process.server
},
get isClient() {
console.warn('context.isClient has been deprecated, please use process.client instead.')
return process.client
},
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) %>
}
// Only set once
if (context.req) app.context.req = context.req
if (context.res) app.context.res = context.res
app.context.redirect = function (status, path, query) {
if (!status) return
app.context._redirected = true // Used in middleware
// if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
if (typeof status === 'string' && (typeof path === 'undefined' || typeof path === 'object')) {
query = path || {}
path = status
status = 302
}
app.context.next({
path: path,
query: query,
status: status
})
}
if (process.server) app.context.beforeNuxtRender = (fn) => context.beforeRenderFns.push(fn)
if (process.client) app.context.nuxtState = window.__NUXT__
}
// Dynamic keys
app.context.next = context.next
app.context._redirected = false
app.context._errored = false
app.context.isHMR = !!context.isHMR
if (context.route) app.context.route = await getRouteData(context.route)
app.context.params = app.context.route.params || {}
app.context.query = app.context.route.query || {}
if (context.from) app.context.from = await getRouteData(context.from)
}
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) {
// 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) {
var path = 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 [].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))
}
/**
* 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) {
var tokens = []
var key = 0
var index = 0
var path = ''
var defaultDelimiter = options && options.delimiter || '/'
var res
while ((res = PATH_REGEXP.exec(str)) != null) {
var m = res[0]
var escaped = res[1]
var offset = res.index
path += str.slice(index, offset)
index = offset + m.length
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1]
continue
}
var next = str[index]
var prefix = res[2]
var name = res[3]
var capture = res[4]
var group = res[5]
var modifier = res[6]
var asterisk = res[7]
// Push the current path onto the tokens.
if (path) {
tokens.push(path)
path = ''
}
var partial = prefix != null && next != null && next !== prefix
var repeat = modifier === '+' || modifier === '*'
var optional = modifier === '?' || modifier === '*'
var delimiter = res[2] || defaultDelimiter
var 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, function (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, function (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.
var matches = new Array(tokens.length)
// Compile all the patterns before compilation.
for (var i = 0; i < tokens.length; i++) {
if (typeof tokens[i] === 'object') {
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$')
}
}
return function(obj, opts) {
var path = ''
var data = obj || {}
var options = opts || {}
var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i]
if (typeof token === 'string') {
path += token
continue
}
var value = data[token.name]
var 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 (var 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')
}