utils.js 17 KB

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