Nuxt/packages/vue-app/template/utils.js

641 lines
17 KiB
JavaScript
Raw Normal View History

2017-05-04 07:57:10 +00:00
import Vue from 'vue'
import { isSamePath as _isSamePath, joinURL, normalizeURL, withQuery, withoutTrailingSlash } from 'ufo'
2016-11-07 01:34:58 +00:00
// window.{{globals.loadedCallback}} hook
2017-07-10 22:53:06 +00:00
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
2018-11-08 09:15:56 +00:00
if (process.client) {
window.<%= globals.readyCallback %>Cbs = []
window.<%= globals.readyCallback %> = (cb) => {
window.<%= globals.readyCallback %>Cbs.push(cb)
2017-07-10 22:53:06 +00:00
}
}
export function createGetCounter (counterObject, defaultKey = '') {
return function getCounter (id = defaultKey) {
if (counterObject[id] === undefined) {
counterObject[id] = 0
}
return counterObject[id]++
}
}
2019-09-10 09:51:14 +00:00
export function empty () {}
2019-09-10 09:51:14 +00:00
export function globalHandleError (error) {
if (Vue.config.errorHandler) {
Vue.config.errorHandler(error)
}
}
2019-09-10 09:51:14 +00:00
export function interopDefault (promise) {
2018-10-24 13:46:06 +00:00
return promise.then(m => m.default || m)
}
<% if (features.fetch) { %>
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
}
<% } %>
<% if (features.asyncData) { %>
2019-09-10 09:51:14 +00:00
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
2017-04-14 14:31:14 +00:00
Component.options.data = function () {
const data = ComponentData.call(this, this)
2017-08-29 18:53:50 +00:00
if (this.$ssrContext) {
asyncData = this.$ssrContext.asyncData[Component.cid]
}
2017-04-14 14:31:14 +00:00
return { ...data, ...asyncData }
}
Component.options.__hasNuxtData = true
2017-04-14 14:31:14 +00:00
if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.options.data
}
}
<% } %>
2017-04-14 14:31:14 +00:00
2019-09-10 09:51:14 +00:00
export function sanitizeComponent (Component) {
// If Component already sanitized
if (Component.options && Component._Ctor === Component) {
return Component
}
2017-05-04 07:57:10 +00:00
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)
2017-05-04 07:57:10 +00:00
if (!Component.options.name && Component.options.__file) {
Component.options.name = Component.options.__file
}
return Component
}
2019-09-10 09:51:14 +00:00
export function getMatchedComponents (route, matches = false, prop = 'components') {
2018-08-15 13:23:03 +00:00
return Array.prototype.concat.apply([], route.matched.map((m, index) => {
return Object.keys(m[prop]).map((key) => {
2018-01-24 06:33:27 +00:00
matches && matches.push(index)
return m[prop][key]
2016-11-07 01:34:58 +00:00
})
}))
}
2019-09-10 09:51:14 +00:00
export function getMatchedComponentsInstances (route, matches = false) {
return getMatchedComponents(route, matches, 'instances')
2016-11-22 23:27:07 +00:00
}
2019-09-10 09:51:14 +00:00
export function flatMapComponents (route, fn) {
2018-08-15 13:23:03 +00:00
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
}, [])
2016-11-07 01:34:58 +00:00
}))
}
2019-09-10 09:51:14 +00:00
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
})
)
}
2019-09-10 09:51:14 +00:00
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 }
})
2016-11-10 23:01:36 +00:00
}
}
2019-09-10 09:51:14 +00:00
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: app.router.options.base,
2018-10-24 13:46:06 +00:00
env: <%= JSON.stringify(env) %><%= isTest ? '// eslint-disable-line' : '' %>
}
// Only set once
<% if (!isFullStatic) { %>
if (context.req) {
app.context.req = context.req
}
if (context.res) {
app.context.res = context.res
}
<% } %>
if (context.ssrContext) {
app.context.ssrContext = context.ssrContext
}
2018-08-15 13:23:03 +00:00
app.context.redirect = (status, path, query) => {
if (!status) {
return
}
app.context._redirected = true
// if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
2017-12-21 04:55:32 +00:00
let pathType = typeof path
if (typeof status !== 'number' && (pathType === 'undefined' || pathType === 'object')) {
query = path || {}
path = status
2017-12-21 04:55:32 +00:00
pathType = typeof path
status = 302
}
2017-12-21 04:55:32 +00:00
if (pathType === 'object') {
path = app.router.resolve(path).route.fullPath
2017-12-21 04:55:32 +00:00
}
// "/absolute/route", "./relative/route" or "../relative/route"
if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(path)) {
2017-11-28 09:10:20 +00:00
app.context.next({
path,
query,
status
2017-11-28 09:10:20 +00:00
})
} else {
path = withQuery(path, query)
2017-11-28 09:10:20 +00:00
if (process.server) {
app.context.next({
path,
status
})
2017-11-28 09:10:20 +00:00
}
2018-11-08 09:15:56 +00:00
if (process.client) {
// https://developer.mozilla.org/en-US/docs/Web/API/Location/assign
window.location.assign(path)
// Throw a redirect error
throw new Error('ERR_REDIRECT')
2017-11-28 09:10:20 +00:00
}
}
}
if (process.server) {
app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
app.context.beforeSerialize = fn => context.beforeSerializeFns.push(fn)
}
2018-11-08 09:15:56 +00:00
if (process.client) {
app.context.nuxtState = window.<%= globals.context %>
}
2017-08-31 12:46:06 +00:00
}
// 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 = <% if(isDev) { %>Boolean(context.isHMR)<% } else { %>false<% } %>
app.context.params = app.context.route.params || {}
app.context.query = app.context.route.query || {}
2016-11-07 01:34:58 +00:00
}
<% if (features.middleware) { %>
2019-09-10 09:51:14 +00:00
export function middlewareSeries (promises, appContext) {
if (!promises.length || appContext._redirected || appContext._errored) {
2017-02-03 14:09:27 +00:00
return Promise.resolve()
}
2017-10-24 21:19:14 +00:00
return promisify(promises[0], appContext)
2018-10-24 13:46:06 +00:00
.then(() => {
return middlewareSeries(promises.slice(1), appContext)
})
2017-02-03 14:09:27 +00:00
}
<% } %>
2019-09-10 09:51:14 +00:00
export function promisify (fn, context) {
<% if (features.deprecations) { %>
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)
}
<% } else { %>
2019-09-10 09:51:14 +00:00
const promise = fn(context)
<% } %>
if (promise && promise instanceof Promise && typeof promise.then === 'function') {
return promise
}
return Promise.resolve(promise)
}
2016-11-07 01:34:58 +00:00
// Imported from vue-router
2019-09-10 09:51:14 +00:00
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)) {
2016-11-07 01:34:58 +00:00
path = path.slice(base.length)
}
const fullPath = (path || '/') + window.location.search + window.location.hash
return normalizeURL(fullPath)
2016-11-07 01:34:58 +00:00
}
2016-11-10 23:01:36 +00:00
2016-12-16 16:45:05 +00:00
// 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=)}
*/
2019-09-10 09:51:14 +00:00
export function compile (str, options) {
return tokensToFunction(parse(str, options), options)
2016-12-16 16:45:05 +00:00
}
2019-09-10 09:51:14 +00:00
export function getQueryDiff (toQuery, fromQuery) {
2017-11-06 17:30:37 +00:00
const diff = {}
const queries = { ...toQuery, ...fromQuery }
for (const k in queries) {
if (String(toQuery[k]) !== String(fromQuery[k])) {
diff[k] = true
}
}
return diff
}
2019-09-10 09:51:14 +00:00
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)
}
}
2016-12-16 16:45:05 +00:00
/**
* 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}
*/
2019-09-10 09:51:14 +00:00
function parse (str, options) {
2018-10-24 13:46:06 +00:00
const tokens = []
let key = 0
let index = 0
let path = ''
const defaultDelimiter = (options && options.delimiter) || '/'
let res
2016-12-16 16:45:05 +00:00
while ((res = PATH_REGEXP.exec(str)) != null) {
2018-10-24 13:46:06 +00:00
const m = res[0]
const escaped = res[1]
const offset = res.index
2016-12-16 16:45:05 +00:00
path += str.slice(index, offset)
index = offset + m.length
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1]
continue
}
2018-10-24 13:46:06 +00:00
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]
2016-12-16 16:45:05 +00:00
// Push the current path onto the tokens.
if (path) {
tokens.push(path)
path = ''
}
2018-10-24 13:46:06 +00:00
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
2016-12-16 16:45:05 +00:00
tokens.push({
name: name || key++,
prefix: prefix || '',
delimiter,
optional,
repeat,
partial,
asterisk: Boolean(asterisk),
2016-12-16 16:45:05 +00:00
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}
*/
2019-09-10 09:51:14 +00:00
function encodeURIComponentPretty (str, slashAllowed) {
const re = slashAllowed ? /[?#]/g : /[/?#]/g
return encodeURI(str).replace(re, (c) => {
2016-12-16 16:45:05 +00:00
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
*
* @param {string}
* @return {string}
*/
2019-09-10 09:51:14 +00:00
function encodeAsterisk (str) {
return encodeURIComponentPretty(str, true)
}
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
2019-09-10 09:51:14 +00:00
function escapeString (str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
2019-09-10 09:51:14 +00:00
function escapeGroup (group) {
return group.replace(/([=!:$/()])/g, '\\$1')
2016-12-16 16:45:05 +00:00
}
/**
* Expose a method for transforming tokens into the path function.
*/
function tokensToFunction (tokens, options) {
2016-12-16 16:45:05 +00:00
// Compile all the tokens into regexps.
2018-10-24 13:46:06 +00:00
const matches = new Array(tokens.length)
2016-12-16 16:45:05 +00:00
// Compile all the patterns before compilation.
2018-10-24 13:46:06 +00:00
for (let i = 0; i < tokens.length; i++) {
2016-12-16 16:45:05 +00:00
if (typeof tokens[i] === 'object') {
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options))
2016-12-16 16:45:05 +00:00
}
}
2018-10-24 13:46:06 +00:00
return function (obj, opts) {
let path = ''
const data = obj || {}
const options = opts || {}
const encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
2016-12-16 16:45:05 +00:00
2018-10-24 13:46:06 +00:00
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
2016-12-16 16:45:05 +00:00
if (typeof token === 'string') {
path += token
continue
}
const value = data[token.name || 'pathMatch']
2018-10-24 13:46:06 +00:00
let segment
2016-12-16 16:45:05 +00:00
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')
}
}
2018-10-24 13:46:06 +00:00
for (let j = 0; j < value.length; j++) {
2016-12-16 16:45:05 +00:00
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)
}
}
feat: options.target and full-static export (#6159) * feat: add options.target * fix(lint): lint * fix(test): update snapshots * fix(builder): default value for target * fix(test): fix test * fix(test): test fixing * fix: use this.options.target * fix: final test * Update packages/vue-renderer/src/renderer.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * feat: Add target option and update banner * fix(lint): fix * feat: Add warning when using serverMiddleware in static target * chore(utils): add TARGETS and MODES as constants * hotfix: lint * chore(module): add filename as alias of fileName * feat: introducing nuxt export and router/routes.json * hotfix: Fix the linting lord * chore(core): add comment for filename vs fileName * fix: use targets constant * chore: remove warning * fix: unit testing * wip: refactor and use TARGETS * fix: lint * feat: add target as alias for first arg value * fix: generate only for SPA * chore: explain to use nuxt static X * fix: render SPA fallback on redirect for static target * fix: lint issue * fix: only target is useful for now * wip * wip: nuxt static export is looking good * Update packages/generator/src/generator.js Co-Authored-By: Devon Rueckner <indirectlylit@users.noreply.github.com> * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * feat: add options.target * fix(lint): lint * fix(test): update snapshots * fix(builder): default value for target * fix(test): fix test * fix(test): test fixing * fix: use this.options.target * fix: final test * Update packages/vue-renderer/src/renderer.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * feat: Add target option and update banner * fix(lint): fix * feat: Add warning when using serverMiddleware in static target * chore(utils): add TARGETS and MODES as constants * hotfix: lint * chore(module): add filename as alias of fileName * feat: introducing nuxt export and router/routes.json * hotfix: Fix the linting lord * chore(core): add comment for filename vs fileName * fix: use targets constant * chore: remove warning * fix: unit testing * wip: refactor and use TARGETS * fix: lint * feat: add target as alias for first arg value * chore: explain to use nuxt static X * fix: render SPA fallback on redirect for static target * fix: lint issue * fix: only target is useful for now * wip * wip: nuxt static export is looking good * Update packages/generator/src/generator.js Co-Authored-By: Devon Rueckner <indirectlylit@users.noreply.github.com> * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * fix: duplicate imports * chore: don't server render if an error happens on static target * test: update unit and add export * lint: fix * lint: fix * fix: e2e test * fix: fallback only for static target * fix: dev test * feat: add generate.crawler * fix: full static is when generate.static is given * chore: improvements * fix: Add isFullStatic in nuxt/config.json * feat: handle fetch for full static * feat: router.prefetchPayloads for full static * chore: use fetch in async-data example * fix: add target only if given * fix: use created to have access to props in fetchOnServer * chore: add console.error in dev for easy debugging * feat: payload smart pre-fetching * fix: remove alias for target * fix: increment payloadFetchIndex is static set to false * chore: lint * chore: add serve command * chore: rename universal to server-side * fix: handle payloadPath on SPA fallback * fix: lint * chore lint again * feat: handle spa fallback * feat: support string for exclude * fix: fallback only if no extension or html * chore: use JSON.stringify() for static target * chore: lint again, dammit * chore: fix tests and remove too early return * fix: early return only for server target * fix: update tests * fix: unit tests * chore: add ssr option * chore: add logic for ssr option * fix: #6682 * chore(dx): add next command to run * fix: lint * fix: tests * chore: keep old behaviour for nuxt build in spa * fix: test again, oh boy * fix: alright this is good now * chore: add comment for spa fallback * chore: move routes.json to dot nuxt dir * chore: simplify check for promise * chore: unique lock id * chore: refactor isFullStatic * fix: dont set default in build context * chore: add test for serve * chore: update tests * hotfix: lint tests * chore(dx): improve message for bundling * feat: js payload extraction with jsonp * fix: keep serialized session script for legacy generate * fix: call to setPagePayload from fetchPayload * use devalue for payload chunks * feat: add initial load state chunk * feat: preload payload and state scripts * fix(vue-app): don't re-render the app if trailing slash on SSG * hotfix: remove console.log * chore(dx): add deploy infos for nuxt export Co-authored-by: Pooya Parsa <pyapar@gmail.com> * chore: handle fetching payload.js for nuxt state * chore(dx): error when using nuxt generate and static * chore: remove static option for clarity * chore: remove serverless target * hotfix: lint * hotfix: unit tests * chore: update legacy js resource * chore: remove query params from url in static target * fix: use globalName and urlJoin * chore: typo * feat: previewMode 👀 * chore: rename to enablePreview * fix: wait next tick to avoid error on spa * chore: try 1 sec * hotfix: test only for linux, wtf azure * refactor: static assets - generalize logic for modules need emit export static assets - allow customization for version, dir and base - serialization logic is only in ssr now * feat: smart state chunk creates * fix(client): ignore payload load error * perf: avoide payload loading for spa initial * perf: avoid loading failed chunks again * chore(cli): add simple compression for nuxt serve * test: update snapshots * fix version snapshot * fix(generator): set staticAssetsBase on context only for full static * fix tests * fix: honor shouldHashCspScriptSrc * chore(dx): add log for client-side fallback creation Co-authored-by: Xin Du (Clark) <clark.duxin@gmail.com> Co-authored-by: Alexander Lichter <manniL@gmx.net> Co-authored-by: Pooya Parsa <pooya@pi0.ir> Co-authored-by: Devon Rueckner <indirectlylit@users.noreply.github.com> Co-authored-by: Pooya Parsa <pyapar@gmail.com>
2020-05-07 19:08:01 +00:00
export const urlJoin = joinURL
export const stripTrailingSlash = withoutTrailingSlash
export const isSamePath = _isSamePath
export function setScrollRestoration (newVal) {
try {
window.history.scrollRestoration = newVal;
} catch(e) {}
}