You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
627 lines
16 KiB
627 lines
16 KiB
import Vue from 'vue' |
|
import { isSamePath as _isSamePath, joinURL, normalizeURL, withQuery, withoutTrailingSlash } from 'ufo' |
|
|
|
// window.{{globals.loadedCallback}} hook |
|
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading) |
|
if (process.client) { |
|
window.onNuxtReadyCbs = [] |
|
window.onNuxtReady = (cb) => { |
|
window.onNuxtReadyCbs.push(cb) |
|
} |
|
} |
|
|
|
export function createGetCounter (counterObject, defaultKey = '') { |
|
return function getCounter (id = defaultKey) { |
|
if (counterObject[id] === undefined) { |
|
counterObject[id] = 0 |
|
} |
|
return counterObject[id]++ |
|
} |
|
} |
|
|
|
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 hasFetch(vm) { |
|
return vm.$options && typeof vm.$options.fetch === 'function' && !vm.$options.fetch.length |
|
} |
|
export function purifyData(data) { |
|
if (process.env.NODE_ENV === 'production') { |
|
return data |
|
} |
|
|
|
return Object.entries(data).filter( |
|
([key, value]) => { |
|
const valid = !(value instanceof Function) && !(value instanceof Promise) |
|
if (!valid) { |
|
console.warn(`${key} is not able to be stringified. This will break in a production environment.`) |
|
} |
|
return valid |
|
} |
|
).reduce((obj, [key, value]) => { |
|
obj[key] = value |
|
return obj |
|
}, {}) |
|
} |
|
export function getChildrenComponentInstancesUsingFetch(vm, instances = []) { |
|
const children = vm.$children || [] |
|
for (const child of children) { |
|
if (child.$fetch) { |
|
instances.push(child) |
|
continue; // Don't get the children since it will reload the template |
|
} |
|
if (child.$children) { |
|
getChildrenComponentInstancesUsingFetch(child, instances) |
|
} |
|
} |
|
return instances |
|
} |
|
|
|
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, 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 |
|
} |
|
// If no component name defined, set file path as name, (also fixes #5703) |
|
if (!Component.options.name && Component.options.__file) { |
|
Component.options.name = Component.options.__file |
|
} |
|
return Component |
|
} |
|
|
|
export function getMatchedComponents (route, matches = false, prop = 'components') { |
|
return Array.prototype.concat.apply([], route.matched.map((m, index) => { |
|
return Object.keys(m[prop]).map((key) => { |
|
matches && matches.push(index) |
|
return m[prop][key] |
|
}) |
|
})) |
|
} |
|
|
|
export function getMatchedComponentsInstances (route, matches = false) { |
|
return getMatchedComponents(route, matches, 'instances') |
|
} |
|
|
|
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, fn) { |
|
return Promise.all( |
|
flatMapComponents(route, async (Component, instance, match, key) => { |
|
// If component is a function, resolve it |
|
if (typeof Component === 'function' && !Component.options) { |
|
try { |
|
Component = await Component() |
|
} catch (error) { |
|
// Handle webpack chunk loading errors |
|
// This may be due to a new deployment or a network problem |
|
if ( |
|
error && |
|
error.name === 'ChunkLoadError' && |
|
typeof window !== 'undefined' && |
|
window.sessionStorage |
|
) { |
|
const timeNow = Date.now() |
|
const previousReloadTime = parseInt(window.sessionStorage.getItem('nuxt-reload')) |
|
|
|
// check for previous reload time not to reload infinitely |
|
if (!previousReloadTime || previousReloadTime + 60000 < timeNow) { |
|
window.sessionStorage.setItem('nuxt-reload', timeNow) |
|
window.location.reload(true /* skip cache */) |
|
} |
|
} |
|
|
|
throw error |
|
} |
|
} |
|
match.components[key] = Component = sanitizeComponent(Component) |
|
return typeof fn === 'function' ? fn(Component, instance, match, key) : Component |
|
}) |
|
) |
|
} |
|
|
|
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: false, |
|
isHMR: false, |
|
app, |
|
store: app.store, |
|
payload: context.payload, |
|
error: context.error, |
|
base: app.router.options.base, |
|
env: {"VUE_APP_TITLE":"production"} |
|
} |
|
// 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, |
|
query, |
|
status |
|
}) |
|
} else { |
|
path = withQuery(path, query) |
|
if (process.server) { |
|
app.context.next({ |
|
path, |
|
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.__NUXT__ |
|
} |
|
} |
|
|
|
// 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 = false |
|
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) { |
|
// 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') { |
|
return promise |
|
} |
|
return Promise.resolve(promise) |
|
} |
|
|
|
// Imported from vue-router |
|
export function getLocation (base, mode) { |
|
if (mode === 'hash') { |
|
return window.location.hash.replace(/^#\//, '') |
|
} |
|
|
|
base = decodeURI(base).slice(0, -1) // consideration is base is normalized with trailing slash |
|
let path = decodeURI(window.location.pathname) |
|
|
|
if (base && path.startsWith(base)) { |
|
path = path.slice(base.length) |
|
} |
|
|
|
const fullPath = (path || '/') + window.location.search + window.location.hash |
|
|
|
return normalizeURL(fullPath) |
|
} |
|
|
|
// 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), 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, |
|
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, |
|
optional, |
|
repeat, |
|
partial, |
|
asterisk: Boolean(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, slashAllowed) { |
|
const re = slashAllowed ? /[?#]/g : /[/?#]/g |
|
return encodeURI(str).replace(re, (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 encodeURIComponentPretty(str, true) |
|
} |
|
|
|
/** |
|
* 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') |
|
} |
|
|
|
/** |
|
* Expose a method for transforming tokens into the path function. |
|
*/ |
|
function tokensToFunction (tokens, options) { |
|
// 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 + ')$', flags(options)) |
|
} |
|
} |
|
|
|
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 |
|
} |
|
} |
|
|
|
/** |
|
* Get the flags for a regexp from the options. |
|
* |
|
* @param {Object} options |
|
* @return {string} |
|
*/ |
|
function flags (options) { |
|
return options && options.sensitive ? '' : 'i' |
|
} |
|
|
|
export function addLifecycleHook(vm, hook, fn) { |
|
if (!vm.$options[hook]) { |
|
vm.$options[hook] = [] |
|
} |
|
if (!vm.$options[hook].includes(fn)) { |
|
vm.$options[hook].push(fn) |
|
} |
|
} |
|
|
|
export const urlJoin = joinURL |
|
|
|
export const stripTrailingSlash = withoutTrailingSlash |
|
|
|
export const isSamePath = _isSamePath |
|
|
|
export function setScrollRestoration (newVal) { |
|
try { |
|
window.history.scrollRestoration = newVal; |
|
} catch(e) {} |
|
}
|
|
|