utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import Vue from 'vue'
  2. import { isSamePath as _isSamePath, joinURL, normalizeURL, withQuery, withoutTrailingSlash } from 'ufo'
  3. // window.{{globals.loadedCallback}} hook
  4. // Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
  5. if (process.client) {
  6. window.onNuxtReadyCbs = []
  7. window.onNuxtReady = (cb) => {
  8. window.onNuxtReadyCbs.push(cb)
  9. }
  10. }
  11. export function createGetCounter (counterObject, defaultKey = '') {
  12. return function getCounter (id = defaultKey) {
  13. if (counterObject[id] === undefined) {
  14. counterObject[id] = 0
  15. }
  16. return counterObject[id]++
  17. }
  18. }
  19. export function empty () {}
  20. export function globalHandleError (error) {
  21. if (Vue.config.errorHandler) {
  22. Vue.config.errorHandler(error)
  23. }
  24. }
  25. export function interopDefault (promise) {
  26. return promise.then(m => m.default || m)
  27. }
  28. export function hasFetch(vm) {
  29. return vm.$options && typeof vm.$options.fetch === 'function' && !vm.$options.fetch.length
  30. }
  31. export function purifyData(data) {
  32. if (process.env.NODE_ENV === 'production') {
  33. return data
  34. }
  35. return Object.entries(data).filter(
  36. ([key, value]) => {
  37. const valid = !(value instanceof Function) && !(value instanceof Promise)
  38. if (!valid) {
  39. console.warn(`${key} is not able to be stringified. This will break in a production environment.`)
  40. }
  41. return valid
  42. }
  43. ).reduce((obj, [key, value]) => {
  44. obj[key] = value
  45. return obj
  46. }, {})
  47. }
  48. export function getChildrenComponentInstancesUsingFetch(vm, instances = []) {
  49. const children = vm.$children || []
  50. for (const child of children) {
  51. if (child.$fetch) {
  52. instances.push(child)
  53. continue; // Don't get the children since it will reload the template
  54. }
  55. if (child.$children) {
  56. getChildrenComponentInstancesUsingFetch(child, instances)
  57. }
  58. }
  59. return instances
  60. }
  61. export function applyAsyncData (Component, asyncData) {
  62. if (
  63. // For SSR, we once all this function without second param to just apply asyncData
  64. // Prevent doing this for each SSR request
  65. !asyncData && Component.options.__hasNuxtData
  66. ) {
  67. return
  68. }
  69. const ComponentData = Component.options._originDataFn || Component.options.data || function () { return {} }
  70. Component.options._originDataFn = ComponentData
  71. Component.options.data = function () {
  72. const data = ComponentData.call(this, this)
  73. if (this.$ssrContext) {
  74. asyncData = this.$ssrContext.asyncData[Component.cid]
  75. }
  76. return { ...data, ...asyncData }
  77. }
  78. Component.options.__hasNuxtData = true
  79. if (Component._Ctor && Component._Ctor.options) {
  80. Component._Ctor.options.data = Component.options.data
  81. }
  82. }
  83. export function sanitizeComponent (Component) {
  84. // If Component already sanitized
  85. if (Component.options && Component._Ctor === Component) {
  86. return Component
  87. }
  88. if (!Component.options) {
  89. Component = Vue.extend(Component) // fix issue #6
  90. Component._Ctor = Component
  91. } else {
  92. Component._Ctor = Component
  93. Component.extendOptions = Component.options
  94. }
  95. // If no component name defined, set file path as name, (also fixes #5703)
  96. if (!Component.options.name && Component.options.__file) {
  97. Component.options.name = Component.options.__file
  98. }
  99. return Component
  100. }
  101. export function getMatchedComponents (route, matches = false, prop = 'components') {
  102. return Array.prototype.concat.apply([], route.matched.map((m, index) => {
  103. return Object.keys(m[prop]).map((key) => {
  104. matches && matches.push(index)
  105. return m[prop][key]
  106. })
  107. }))
  108. }
  109. export function getMatchedComponentsInstances (route, matches = false) {
  110. return getMatchedComponents(route, matches, 'instances')
  111. }
  112. export function flatMapComponents (route, fn) {
  113. return Array.prototype.concat.apply([], route.matched.map((m, index) => {
  114. return Object.keys(m.components).reduce((promises, key) => {
  115. if (m.components[key]) {
  116. promises.push(fn(m.components[key], m.instances[key], m, key, index))
  117. } else {
  118. delete m.components[key]
  119. }
  120. return promises
  121. }, [])
  122. }))
  123. }
  124. export function resolveRouteComponents (route, fn) {
  125. return Promise.all(
  126. flatMapComponents(route, async (Component, instance, match, key) => {
  127. // If component is a function, resolve it
  128. if (typeof Component === 'function' && !Component.options) {
  129. try {
  130. Component = await Component()
  131. } catch (error) {
  132. // Handle webpack chunk loading errors
  133. // This may be due to a new deployment or a network problem
  134. if (
  135. error &&
  136. error.name === 'ChunkLoadError' &&
  137. typeof window !== 'undefined' &&
  138. window.sessionStorage
  139. ) {
  140. const timeNow = Date.now()
  141. const previousReloadTime = parseInt(window.sessionStorage.getItem('nuxt-reload'))
  142. // check for previous reload time not to reload infinitely
  143. if (!previousReloadTime || previousReloadTime + 60000 < timeNow) {
  144. window.sessionStorage.setItem('nuxt-reload', timeNow)
  145. window.location.reload(true /* skip cache */)
  146. }
  147. }
  148. throw error
  149. }
  150. }
  151. match.components[key] = Component = sanitizeComponent(Component)
  152. return typeof fn === 'function' ? fn(Component, instance, match, key) : Component
  153. })
  154. )
  155. }
  156. export async function getRouteData (route) {
  157. if (!route) {
  158. return
  159. }
  160. // Make sure the components are resolved (code-splitting)
  161. await resolveRouteComponents(route)
  162. // Send back a copy of route with meta based on Component definition
  163. return {
  164. ...route,
  165. meta: getMatchedComponents(route).map((Component, index) => {
  166. return { ...Component.options.meta, ...(route.matched[index] || {}).meta }
  167. })
  168. }
  169. }
  170. export async function setContext (app, context) {
  171. // If context not defined, create it
  172. if (!app.context) {
  173. app.context = {
  174. isStatic: process.static,
  175. isDev: false,
  176. isHMR: false,
  177. app,
  178. payload: context.payload,
  179. error: context.error,
  180. base: app.router.options.base,
  181. env: {}
  182. }
  183. // Only set once
  184. if (context.ssrContext) {
  185. app.context.ssrContext = context.ssrContext
  186. }
  187. app.context.redirect = (status, path, query) => {
  188. if (!status) {
  189. return
  190. }
  191. app.context._redirected = true
  192. // if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
  193. let pathType = typeof path
  194. if (typeof status !== 'number' && (pathType === 'undefined' || pathType === 'object')) {
  195. query = path || {}
  196. path = status
  197. pathType = typeof path
  198. status = 302
  199. }
  200. if (pathType === 'object') {
  201. path = app.router.resolve(path).route.fullPath
  202. }
  203. // "/absolute/route", "./relative/route" or "../relative/route"
  204. if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(path)) {
  205. app.context.next({
  206. path,
  207. query,
  208. status
  209. })
  210. } else {
  211. path = withQuery(path, query)
  212. if (process.server) {
  213. app.context.next({
  214. path,
  215. status
  216. })
  217. }
  218. if (process.client) {
  219. // https://developer.mozilla.org/en-US/docs/Web/API/Location/replace
  220. window.location.replace(path)
  221. // Throw a redirect error
  222. throw new Error('ERR_REDIRECT')
  223. }
  224. }
  225. }
  226. if (process.server) {
  227. app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
  228. }
  229. if (process.client) {
  230. app.context.nuxtState = window.__NUXT__
  231. }
  232. }
  233. // Dynamic keys
  234. const [currentRouteData, fromRouteData] = await Promise.all([
  235. getRouteData(context.route),
  236. getRouteData(context.from)
  237. ])
  238. if (context.route) {
  239. app.context.route = currentRouteData
  240. }
  241. if (context.from) {
  242. app.context.from = fromRouteData
  243. }
  244. app.context.next = context.next
  245. app.context._redirected = false
  246. app.context._errored = false
  247. app.context.isHMR = false
  248. app.context.params = app.context.route.params || {}
  249. app.context.query = app.context.route.query || {}
  250. }
  251. export function middlewareSeries (promises, appContext) {
  252. if (!promises.length || appContext._redirected || appContext._errored) {
  253. return Promise.resolve()
  254. }
  255. return promisify(promises[0], appContext)
  256. .then(() => {
  257. return middlewareSeries(promises.slice(1), appContext)
  258. })
  259. }
  260. export function promisify (fn, context) {
  261. let promise
  262. if (fn.length === 2) {
  263. // fn(context, callback)
  264. promise = new Promise((resolve) => {
  265. fn(context, function (err, data) {
  266. if (err) {
  267. context.error(err)
  268. }
  269. data = data || {}
  270. resolve(data)
  271. })
  272. })
  273. } else {
  274. promise = fn(context)
  275. }
  276. if (promise && promise instanceof Promise && typeof promise.then === 'function') {
  277. return promise
  278. }
  279. return Promise.resolve(promise)
  280. }
  281. // Imported from vue-router
  282. export function getLocation (base, mode) {
  283. if (mode === 'hash') {
  284. return window.location.hash.replace(/^#\//, '')
  285. }
  286. base = decodeURI(base).slice(0, -1) // consideration is base is normalized with trailing slash
  287. let path = decodeURI(window.location.pathname)
  288. if (base && path.startsWith(base)) {
  289. path = path.slice(base.length)
  290. }
  291. const fullPath = (path || '/') + window.location.search + window.location.hash
  292. return normalizeURL(fullPath)
  293. }
  294. // Imported from path-to-regexp
  295. /**
  296. * Compile a string to a template function for the path.
  297. *
  298. * @param {string} str
  299. * @param {Object=} options
  300. * @return {!function(Object=, Object=)}
  301. */
  302. export function compile (str, options) {
  303. return tokensToFunction(parse(str, options), options)
  304. }
  305. export function getQueryDiff (toQuery, fromQuery) {
  306. const diff = {}
  307. const queries = { ...toQuery, ...fromQuery }
  308. for (const k in queries) {
  309. if (String(toQuery[k]) !== String(fromQuery[k])) {
  310. diff[k] = true
  311. }
  312. }
  313. return diff
  314. }
  315. export function normalizeError (err) {
  316. let message
  317. if (!(err.message || typeof err === 'string')) {
  318. try {
  319. message = JSON.stringify(err, null, 2)
  320. } catch (e) {
  321. message = `[${err.constructor.name}]`
  322. }
  323. } else {
  324. message = err.message || err
  325. }
  326. return {
  327. ...err,
  328. message,
  329. statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500)
  330. }
  331. }
  332. /**
  333. * The main path matching regexp utility.
  334. *
  335. * @type {RegExp}
  336. */
  337. const PATH_REGEXP = new RegExp([
  338. // Match escaped characters that would otherwise appear in future matches.
  339. // This allows the user to escape special characters that won't transform.
  340. '(\\\\.)',
  341. // Match Express-style parameters and un-named parameters with a prefix
  342. // and optional suffixes. Matches appear as:
  343. //
  344. // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
  345. // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
  346. // "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
  347. '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
  348. ].join('|'), 'g')
  349. /**
  350. * Parse a string for the raw tokens.
  351. *
  352. * @param {string} str
  353. * @param {Object=} options
  354. * @return {!Array}
  355. */
  356. function parse (str, options) {
  357. const tokens = []
  358. let key = 0
  359. let index = 0
  360. let path = ''
  361. const defaultDelimiter = (options && options.delimiter) || '/'
  362. let res
  363. while ((res = PATH_REGEXP.exec(str)) != null) {
  364. const m = res[0]
  365. const escaped = res[1]
  366. const offset = res.index
  367. path += str.slice(index, offset)
  368. index = offset + m.length
  369. // Ignore already escaped sequences.
  370. if (escaped) {
  371. path += escaped[1]
  372. continue
  373. }
  374. const next = str[index]
  375. const prefix = res[2]
  376. const name = res[3]
  377. const capture = res[4]
  378. const group = res[5]
  379. const modifier = res[6]
  380. const asterisk = res[7]
  381. // Push the current path onto the tokens.
  382. if (path) {
  383. tokens.push(path)
  384. path = ''
  385. }
  386. const partial = prefix != null && next != null && next !== prefix
  387. const repeat = modifier === '+' || modifier === '*'
  388. const optional = modifier === '?' || modifier === '*'
  389. const delimiter = res[2] || defaultDelimiter
  390. const pattern = capture || group
  391. tokens.push({
  392. name: name || key++,
  393. prefix: prefix || '',
  394. delimiter,
  395. optional,
  396. repeat,
  397. partial,
  398. asterisk: Boolean(asterisk),
  399. pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
  400. })
  401. }
  402. // Match any characters still remaining.
  403. if (index < str.length) {
  404. path += str.substr(index)
  405. }
  406. // If the path exists, push it onto the end.
  407. if (path) {
  408. tokens.push(path)
  409. }
  410. return tokens
  411. }
  412. /**
  413. * Prettier encoding of URI path segments.
  414. *
  415. * @param {string}
  416. * @return {string}
  417. */
  418. function encodeURIComponentPretty (str, slashAllowed) {
  419. const re = slashAllowed ? /[?#]/g : /[/?#]/g
  420. return encodeURI(str).replace(re, (c) => {
  421. return '%' + c.charCodeAt(0).toString(16).toUpperCase()
  422. })
  423. }
  424. /**
  425. * Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
  426. *
  427. * @param {string}
  428. * @return {string}
  429. */
  430. function encodeAsterisk (str) {
  431. return encodeURIComponentPretty(str, true)
  432. }
  433. /**
  434. * Escape a regular expression string.
  435. *
  436. * @param {string} str
  437. * @return {string}
  438. */
  439. function escapeString (str) {
  440. return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
  441. }
  442. /**
  443. * Escape the capturing group by escaping special characters and meaning.
  444. *
  445. * @param {string} group
  446. * @return {string}
  447. */
  448. function escapeGroup (group) {
  449. return group.replace(/([=!:$/()])/g, '\\$1')
  450. }
  451. /**
  452. * Expose a method for transforming tokens into the path function.
  453. */
  454. function tokensToFunction (tokens, options) {
  455. // Compile all the tokens into regexps.
  456. const matches = new Array(tokens.length)
  457. // Compile all the patterns before compilation.
  458. for (let i = 0; i < tokens.length; i++) {
  459. if (typeof tokens[i] === 'object') {
  460. matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options))
  461. }
  462. }
  463. return function (obj, opts) {
  464. let path = ''
  465. const data = obj || {}
  466. const options = opts || {}
  467. const encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
  468. for (let i = 0; i < tokens.length; i++) {
  469. const token = tokens[i]
  470. if (typeof token === 'string') {
  471. path += token
  472. continue
  473. }
  474. const value = data[token.name || 'pathMatch']
  475. let segment
  476. if (value == null) {
  477. if (token.optional) {
  478. // Prepend partial segment prefixes.
  479. if (token.partial) {
  480. path += token.prefix
  481. }
  482. continue
  483. } else {
  484. throw new TypeError('Expected "' + token.name + '" to be defined')
  485. }
  486. }
  487. if (Array.isArray(value)) {
  488. if (!token.repeat) {
  489. throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
  490. }
  491. if (value.length === 0) {
  492. if (token.optional) {
  493. continue
  494. } else {
  495. throw new TypeError('Expected "' + token.name + '" to not be empty')
  496. }
  497. }
  498. for (let j = 0; j < value.length; j++) {
  499. segment = encode(value[j])
  500. if (!matches[i].test(segment)) {
  501. throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
  502. }
  503. path += (j === 0 ? token.prefix : token.delimiter) + segment
  504. }
  505. continue
  506. }
  507. segment = token.asterisk ? encodeAsterisk(value) : encode(value)
  508. if (!matches[i].test(segment)) {
  509. throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
  510. }
  511. path += token.prefix + segment
  512. }
  513. return path
  514. }
  515. }
  516. /**
  517. * Get the flags for a regexp from the options.
  518. *
  519. * @param {Object} options
  520. * @return {string}
  521. */
  522. function flags (options) {
  523. return options && options.sensitive ? '' : 'i'
  524. }
  525. export function addLifecycleHook(vm, hook, fn) {
  526. if (!vm.$options[hook]) {
  527. vm.$options[hook] = []
  528. }
  529. if (!vm.$options[hook].includes(fn)) {
  530. vm.$options[hook].push(fn)
  531. }
  532. }
  533. export const urlJoin = joinURL
  534. export const stripTrailingSlash = withoutTrailingSlash
  535. export const isSamePath = _isSamePath
  536. export function setScrollRestoration (newVal) {
  537. try {
  538. window.history.scrollRestoration = newVal;
  539. } catch(e) {}
  540. }