diff --git a/examples/nuxt-example/.editorconfig b/examples/nuxt-example/.editorconfig new file mode 100644 index 00000000..5d126348 --- /dev/null +++ b/examples/nuxt-example/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/examples/nuxt-example/.nuxt/App.js b/examples/nuxt-example/.nuxt/App.js new file mode 100644 index 00000000..3d323a3a --- /dev/null +++ b/examples/nuxt-example/.nuxt/App.js @@ -0,0 +1,210 @@ +import Vue from 'vue' +import { decode, parsePath, withoutBase, withoutTrailingSlash, normalizeURL } from 'ufo' + +import { getMatchedComponentsInstances, getChildrenComponentInstancesUsingFetch, promisify, globalHandleError, urlJoin, sanitizeComponent } from './utils' +import NuxtError from './components/nuxt-error.vue' +import NuxtLoading from './components/nuxt-loading.vue' +import NuxtBuildIndicator from './components/nuxt-build-indicator' + +import _6f6c098b from '..\\layouts\\default.vue' + +const layouts = { "_default": sanitizeComponent(_6f6c098b) } + +export default { + render (h, props) { + const loadingEl = h('NuxtLoading', { ref: 'loading' }) + + const layoutEl = h(this.layout || 'nuxt') + const templateEl = h('div', { + domProps: { + id: '__layout' + }, + key: this.layoutName + }, [layoutEl]) + + const transitionEl = h('transition', { + props: { + name: 'layout', + mode: 'out-in' + }, + on: { + beforeEnter (el) { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + } + } + }, [templateEl]) + + return h('div', { + domProps: { + id: '__nuxt' + } + }, [ + loadingEl, + h(NuxtBuildIndicator), + transitionEl + ]) + }, + + data: () => ({ + isOnline: true, + + layout: null, + layoutName: '', + + nbFetching: 0 + }), + + beforeCreate () { + Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt) + }, + created () { + // Add this.$nuxt in child instances + this.$root.$options.$nuxt = this + + if (process.client) { + // add to window so we can listen when ready + window.$nuxt = this + + this.refreshOnlineStatus() + // Setup the listeners + window.addEventListener('online', this.refreshOnlineStatus) + window.addEventListener('offline', this.refreshOnlineStatus) + } + // Add $nuxt.error() + this.error = this.nuxt.error + // Add $nuxt.context + this.context = this.$options.context + }, + + async mounted () { + this.$loading = this.$refs.loading + }, + + watch: { + 'nuxt.err': 'errorChanged' + }, + + computed: { + isOffline () { + return !this.isOnline + }, + + isFetching () { + return this.nbFetching > 0 + }, + }, + + methods: { + refreshOnlineStatus () { + if (process.client) { + if (typeof window.navigator.onLine === 'undefined') { + // If the browser doesn't support connection status reports + // assume that we are online because most apps' only react + // when they now that the connection has been interrupted + this.isOnline = true + } else { + this.isOnline = window.navigator.onLine + } + } + }, + + async refresh () { + const pages = getMatchedComponentsInstances(this.$route) + + if (!pages.length) { + return + } + this.$loading.start() + + const promises = pages.map(async (page) => { + let p = [] + + // Old fetch + if (page.$options.fetch && page.$options.fetch.length) { + p.push(promisify(page.$options.fetch, this.context)) + } + + if (page.$options.asyncData) { + p.push( + promisify(page.$options.asyncData, this.context) + .then((newData) => { + for (const key in newData) { + Vue.set(page.$data, key, newData[key]) + } + }) + ) + } + + // Wait for asyncData & old fetch to finish + await Promise.all(p) + // Cleanup refs + p = [] + + if (page.$fetch) { + p.push(page.$fetch()) + } + // Get all component instance to call $fetch + for (const component of getChildrenComponentInstancesUsingFetch(page.$vnode.componentInstance)) { + p.push(component.$fetch()) + } + + return Promise.all(p) + }) + try { + await Promise.all(promises) + } catch (error) { + this.$loading.fail(error) + globalHandleError(error) + this.error(error) + } + this.$loading.finish() + }, + errorChanged () { + if (this.nuxt.err) { + if (this.$loading) { + if (this.$loading.fail) { + this.$loading.fail(this.nuxt.err) + } + if (this.$loading.finish) { + this.$loading.finish() + } + } + + let errorLayout = (NuxtError.options || NuxtError).layout; + + if (typeof errorLayout === 'function') { + errorLayout = errorLayout(this.context) + } + + this.nuxt.errPageReady = true + this.setLayout(errorLayout) + } + }, + + setLayout (layout) { + if(layout && typeof layout !== 'string') { + throw new Error('[nuxt] Avoid using non-string value as layout property.') + } + + if (!layout || !layouts['_' + layout]) { + layout = 'default' + } + this.layoutName = layout + this.layout = layouts['_' + layout] + return this.layout + }, + loadLayout (layout) { + if (!layout || !layouts['_' + layout]) { + layout = 'default' + } + return Promise.resolve(layouts['_' + layout]) + }, + }, + + components: { + NuxtLoading + } +} diff --git a/examples/nuxt-example/.nuxt/client.js b/examples/nuxt-example/.nuxt/client.js new file mode 100644 index 00000000..3cdc1dea --- /dev/null +++ b/examples/nuxt-example/.nuxt/client.js @@ -0,0 +1,891 @@ +import Vue from 'vue' +import fetch from 'unfetch' +import middleware from './middleware.js' +import { + applyAsyncData, + promisify, + middlewareSeries, + sanitizeComponent, + resolveRouteComponents, + getMatchedComponents, + getMatchedComponentsInstances, + flatMapComponents, + setContext, + getLocation, + compile, + getQueryDiff, + globalHandleError, + isSamePath, + urlJoin +} from './utils.js' +import { createApp, NuxtError } from './index.js' +import fetchMixin from './mixins/fetch.client' +import NuxtLink from './components/nuxt-link.client.js' // should be included after ./index.js + +// Fetch mixin +if (!Vue.__nuxt__fetch__mixin__) { + Vue.mixin(fetchMixin) + Vue.__nuxt__fetch__mixin__ = true +} + +// Component: +Vue.component(NuxtLink.name, NuxtLink) +Vue.component('NLink', NuxtLink) + +if (!global.fetch) { global.fetch = fetch } + +// Global shared references +let _lastPaths = [] +let app +let router + +// Try to rehydrate SSR data from window +const NUXT = window.__NUXT__ || {} + +const $config = NUXT.config || {} +if ($config._app) { + __webpack_public_path__ = urlJoin($config._app.cdnURL, $config._app.assetsPath) +} + +Object.assign(Vue.config, {"silent":false,"performance":true}) + +const logs = NUXT.logs || [] + if (logs.length > 0) { + const ssrLogStyle = 'background: #2E495E;border-radius: 0.5em;color: white;font-weight: bold;padding: 2px 0.5em;' + console.group && console.group ('%cNuxt SSR', ssrLogStyle) + logs.forEach(logObj => (console[logObj.type] || console.log)(...logObj.args)) + delete NUXT.logs + console.groupEnd && console.groupEnd() +} + +// Setup global Vue error handler +if (!Vue.config.$nuxt) { + const defaultErrorHandler = Vue.config.errorHandler + Vue.config.errorHandler = async (err, vm, info, ...rest) => { + // Call other handler if exist + let handled = null + if (typeof defaultErrorHandler === 'function') { + handled = defaultErrorHandler(err, vm, info, ...rest) + } + if (handled === true) { + return handled + } + + if (vm && vm.$root) { + const nuxtApp = Object.keys(Vue.config.$nuxt) + .find(nuxtInstance => vm.$root[nuxtInstance]) + + // Show Nuxt Error Page + if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') { + const currentApp = vm.$root[nuxtApp] + + // Load error layout + let layout = (NuxtError.options || NuxtError).layout + if (typeof layout === 'function') { + layout = layout(currentApp.context) + } + if (layout) { + await currentApp.loadLayout(layout).catch(() => {}) + } + currentApp.setLayout(layout) + + currentApp.error(err) + } + } + + if (typeof defaultErrorHandler === 'function') { + return handled + } + + // Log to console + if (process.env.NODE_ENV !== 'production') { + console.error(err) + } else { + console.error(err.message || err) + } + } + Vue.config.$nuxt = {} +} +Vue.config.$nuxt.$nuxt = true + +const errorHandler = Vue.config.errorHandler || console.error + +// Create and mount App +createApp(null, NUXT.config).then(mountApp).catch(errorHandler) + +function componentOption (component, key, ...args) { + if (!component || !component.options || !component.options[key]) { + return {} + } + const option = component.options[key] + if (typeof option === 'function') { + return option(...args) + } + return option +} + +function mapTransitions (toComponents, to, from) { + const componentTransitions = (component) => { + const transition = componentOption(component, 'transition', to, from) || {} + return (typeof transition === 'string' ? { name: transition } : transition) + } + + const fromComponents = from ? getMatchedComponents(from) : [] + const maxDepth = Math.max(toComponents.length, fromComponents.length) + + const mergedTransitions = [] + for (let i=0; i typeof toTransitions[key] !== 'undefined' && !key.toLowerCase().includes('leave')) + .forEach((key) => { transitions[key] = toTransitions[key] }) + + mergedTransitions.push(transitions) + } + return mergedTransitions +} + +async function loadAsyncComponents (to, from, next) { + // Check if route changed (this._routeChanged), only if the page is not an error (for validate()) + this._routeChanged = Boolean(app.nuxt.err) || from.name !== to.name + this._paramChanged = !this._routeChanged && from.path !== to.path + this._queryChanged = !this._paramChanged && from.fullPath !== to.fullPath + this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : []) + + if ((this._routeChanged || this._paramChanged) && this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + + try { + if (this._queryChanged) { + const Components = await resolveRouteComponents( + to, + (Component, instance) => ({ Component, instance }) + ) + // Add a marker on each component that it needs to refresh or not + const startLoader = Components.some(({ Component, instance }) => { + const watchQuery = Component.options.watchQuery + if (watchQuery === true) { + return true + } + if (Array.isArray(watchQuery)) { + return watchQuery.some(key => this._diffQuery[key]) + } + if (typeof watchQuery === 'function') { + return watchQuery.apply(instance, [to.query, from.query]) + } + return false + }) + + if (startLoader && this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + } + // Call next() + next() + } catch (error) { + const err = error || {} + const statusCode = err.statusCode || err.status || (err.response && err.response.status) || 500 + const message = err.message || '' + + // Handle chunk loading errors + // This may be due to a new deployment or a network problem + if (/^Loading( CSS)? chunk (\d)+ failed\./.test(message)) { + window.location.reload(true /* skip cache */) + return // prevent error page blinking for user + } + + this.error({ statusCode, message }) + this.$nuxt.$emit('routeChanged', to, from, err) + next() + } +} + +function applySSRData (Component, ssrData) { + if (NUXT.serverRendered && ssrData) { + applyAsyncData(Component, ssrData) + } + + Component._Ctor = Component + return Component +} + +// Get matched components +function resolveComponents (route) { + return flatMapComponents(route, async (Component, _, match, key, index) => { + // If component is not resolved yet, resolve it + if (typeof Component === 'function' && !Component.options) { + Component = await Component() + } + // Sanitize it and save it + const _Component = applySSRData(sanitizeComponent(Component), NUXT.data ? NUXT.data[index] : null) + match.components[key] = _Component + return _Component + }) +} + +function callMiddleware (Components, context, layout, renderState) { + let midd = [] + let unknownMiddleware = false + + // If layout is undefined, only call global middleware + if (typeof layout !== 'undefined') { + midd = [] // Exclude global middleware if layout defined (already called before) + layout = sanitizeComponent(layout) + if (layout.options.middleware) { + midd = midd.concat(layout.options.middleware) + } + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + } + + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + unknownMiddleware = true + this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + + if (unknownMiddleware) { + return + } + return middlewareSeries(midd, context, renderState) +} + +async function render (to, from, next, renderState) { + if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) { + return next() + } + // Handle first render on SPA mode + let spaFallback = false + if (to === from) { + _lastPaths = [] + spaFallback = true + } else { + const fromMatches = [] + _lastPaths = getMatchedComponents(from, fromMatches).map((Component, i) => { + return compile(from.matched[fromMatches[i]].path)(from.params) + }) + } + + // nextCalled is true when redirected + let nextCalled = false + const _next = (path) => { + if (from.path === path.path && this.$loading.finish) { + this.$loading.finish() + } + + if (from.path !== path.path && this.$loading.pause) { + this.$loading.pause() + } + + if (nextCalled) { + return + } + + nextCalled = true + next(path) + } + + // Update context + await setContext(app, { + route: to, + from, + error: (err) => { + if (renderState.aborted) { + return + } + app.nuxt.error.call(this, err) + }, + next: _next.bind(this) + }) + this._dateLastError = app.nuxt.dateErr + this._hadError = Boolean(app.nuxt.err) + + // Get route's matched components + const matches = [] + const Components = getMatchedComponents(to, matches) + + // If no Components matched, generate 404 + if (!Components.length) { + // Default layout + await callMiddleware.call(this, Components, app.context, undefined, renderState) + if (nextCalled) { + return + } + if (renderState.aborted) { + next(false) + return + } + + // Load layout for error page + const errorLayout = (NuxtError.options || NuxtError).layout + const layout = await this.loadLayout( + typeof errorLayout === 'function' + ? errorLayout.call(NuxtError, app.context) + : errorLayout + ) + + await callMiddleware.call(this, Components, app.context, layout, renderState) + if (nextCalled) { + return + } + if (renderState.aborted) { + next(false) + return + } + + // Show error page + app.context.error({ statusCode: 404, message: 'This page could not be found' }) + return next() + } + + // Update ._data and other properties if hot reloaded + Components.forEach((Component) => { + if (Component._Ctor && Component._Ctor.options) { + Component.options.asyncData = Component._Ctor.options.asyncData + Component.options.fetch = Component._Ctor.options.fetch + } + }) + + // Apply transitions + this.setTransitions(mapTransitions(Components, to, from)) + + try { + // Call middleware + await callMiddleware.call(this, Components, app.context, undefined, renderState) + if (nextCalled) { + return + } + if (renderState.aborted) { + next(false) + return + } + if (app.context._errored) { + return next() + } + + // Set layout + let layout = Components[0].options.layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + layout = await this.loadLayout(layout) + + // Call middleware for layout + await callMiddleware.call(this, Components, app.context, layout, renderState) + if (nextCalled) { + return + } + if (renderState.aborted) { + next(false) + return + } + if (app.context._errored) { + return next() + } + + // Call .validate() + let isValid = true + try { + for (const Component of Components) { + if (typeof Component.options.validate !== 'function') { + continue + } + + isValid = await Component.options.validate(app.context) + + if (!isValid) { + break + } + } + } catch (validationError) { + // ...If .validate() threw an error + this.error({ + statusCode: validationError.statusCode || '500', + message: validationError.message + }) + return next() + } + + // ...If .validate() returned false + if (!isValid) { + this.error({ statusCode: 404, message: 'This page could not be found' }) + return next() + } + + let instances + // Call asyncData & fetch hooks on components matched by the route. + await Promise.all(Components.map(async (Component, i) => { + // Check if only children route changed + Component._path = compile(to.matched[matches[i]].path)(to.params) + Component._dataRefresh = false + const childPathChanged = Component._path !== _lastPaths[i] + // Refresh component (call asyncData & fetch) when: + // Route path changed part includes current component + // Or route param changed part includes current component and watchParam is not `false` + // Or route query is changed and watchQuery returns `true` + if (this._routeChanged && childPathChanged) { + Component._dataRefresh = true + } else if (this._paramChanged && childPathChanged) { + const watchParam = Component.options.watchParam + Component._dataRefresh = watchParam !== false + } else if (this._queryChanged) { + const watchQuery = Component.options.watchQuery + if (watchQuery === true) { + Component._dataRefresh = true + } else if (Array.isArray(watchQuery)) { + Component._dataRefresh = watchQuery.some(key => this._diffQuery[key]) + } else if (typeof watchQuery === 'function') { + if (!instances) { + instances = getMatchedComponentsInstances(to) + } + Component._dataRefresh = watchQuery.apply(instances[i], [to.query, from.query]) + } + } + if (!this._hadError && this._isMounted && !Component._dataRefresh) { + return + } + + const promises = [] + + const hasAsyncData = ( + Component.options.asyncData && + typeof Component.options.asyncData === 'function' + ) + + const hasFetch = Boolean(Component.options.fetch) && Component.options.fetch.length + + const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45 + + // Call asyncData(context) + if (hasAsyncData) { + const promise = promisify(Component.options.asyncData, app.context) + + promise.then((asyncDataResult) => { + applyAsyncData(Component, asyncDataResult) + + if (this.$loading.increase) { + this.$loading.increase(loadingIncrease) + } + }) + promises.push(promise) + } + + // Check disabled page loading + this.$loading.manual = Component.options.loading === false + + // Call fetch(context) + if (hasFetch) { + let p = Component.options.fetch(app.context) + if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { + p = Promise.resolve(p) + } + p.then((fetchResult) => { + if (this.$loading.increase) { + this.$loading.increase(loadingIncrease) + } + }) + promises.push(p) + } + + return Promise.all(promises) + })) + + // If not redirected + if (!nextCalled) { + if (this.$loading.finish && !this.$loading.manual) { + this.$loading.finish() + } + + if (renderState.aborted) { + next(false) + return + } + next() + } + } catch (err) { + if (renderState.aborted) { + next(false) + return + } + const error = err || {} + if (error.message === 'ERR_REDIRECT') { + return this.$nuxt.$emit('routeChanged', to, from, error) + } + _lastPaths = [] + + globalHandleError(error) + + // Load error layout + let layout = (NuxtError.options || NuxtError).layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + await this.loadLayout(layout) + + this.error(error) + this.$nuxt.$emit('routeChanged', to, from, error) + next() + } +} + +// Fix components format in matched, it's due to code-splitting of vue-router +function normalizeComponents (to, ___) { + flatMapComponents(to, (Component, _, match, key) => { + if (typeof Component === 'object' && !Component.options) { + // Updated via vue-router resolveAsyncComponents() + Component = Vue.extend(Component) + Component._Ctor = Component + match.components[key] = Component + } + return Component + }) +} + +const routeMap = new WeakMap() +function getLayoutForNextPage (to, from, next) { + // Set layout + let hasError = Boolean(this.$options.nuxt.err) + if (this._hadError && this._dateLastError === this.$options.nuxt.dateErr) { + hasError = false + } + let layout = hasError + ? (NuxtError.options || NuxtError).layout + : to.matched[0].components.default.options.layout + + if (typeof layout === 'function') { + layout = layout(app.context) + } + + routeMap.set(to, layout); + + if (next) next(); +} + +function setLayoutForNextPage(to) { + const layout = routeMap.get(to) + routeMap.delete(to) + + const prevPageIsError = this._hadError && this._dateLastError === this.$options.nuxt.dateErr + + if (prevPageIsError) { + this.$options.nuxt.err = null + } + + this.setLayout(layout) +} + +function checkForErrors (app) { + // Hide error component if no error + if (app._hadError && app._dateLastError === app.$options.nuxt.dateErr) { + app.error() + } +} + +// When navigating on a different route but the same component is used, Vue.js +// Will not update the instance data, so we have to update $data ourselves +function fixPrepatch (to, ___) { + if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) { + return + } + + const instances = getMatchedComponentsInstances(to) + const Components = getMatchedComponents(to) + + let triggerScroll = false + + Vue.nextTick(() => { + instances.forEach((instance, i) => { + if (!instance || instance._isDestroyed) { + return + } + + if ( + instance.constructor._dataRefresh && + Components[i] === instance.constructor && + instance.$vnode.data.keepAlive !== true && + typeof instance.constructor.options.data === 'function' + ) { + const newData = instance.constructor.options.data.call(instance) + for (const key in newData) { + Vue.set(instance.$data, key, newData[key]) + } + + triggerScroll = true + } + }) + + if (triggerScroll) { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + } + + checkForErrors(this) + + // Hot reloading + setTimeout(() => hotReloadAPI(this), 100) + }) +} + +function nuxtReady (_app) { + window.onNuxtReadyCbs.forEach((cb) => { + if (typeof cb === 'function') { + cb(_app) + } + }) + // Special JSDOM + if (typeof window._onNuxtLoaded === 'function') { + window._onNuxtLoaded(_app) + } + // Add router hooks + router.afterEach((to, from) => { + // Wait for fixPrepatch + $data updates + Vue.nextTick(() => _app.$nuxt.$emit('routeChanged', to, from)) + }) +} + +const noopData = () => { return {} } +const noopFetch = () => {} + +// Special hot reload with asyncData(context) +function getNuxtChildComponents ($parent, $components = []) { + $parent.$children.forEach(($child) => { + if ($child.$vnode && $child.$vnode.data.nuxtChild && !$components.find(c =>(c.$options.__file === $child.$options.__file))) { + $components.push($child) + } + if ($child.$children && $child.$children.length) { + getNuxtChildComponents($child, $components) + } + }) + + return $components +} + +function hotReloadAPI(_app) { + if (!module.hot) return + + let $components = getNuxtChildComponents(_app.$nuxt, []) + + $components.forEach(addHotReload.bind(_app)) + + if (_app.context.isHMR) { + const Components = getMatchedComponents(router.currentRoute) + Components.forEach((Component) => { + Component.prototype.constructor = Component + }) + } +} + +function addHotReload ($component, depth) { + if ($component.$vnode.data._hasHotReload) return + $component.$vnode.data._hasHotReload = true + + var _forceUpdate = $component.$forceUpdate.bind($component.$parent) + + $component.$vnode.context.$forceUpdate = async () => { + let Components = getMatchedComponents(router.currentRoute) + let Component = Components[depth] + if (!Component) { + return _forceUpdate() + } + if (typeof Component === 'object' && !Component.options) { + // Updated via vue-router resolveAsyncComponents() + Component = Vue.extend(Component) + Component._Ctor = Component + } + this.error() + let promises = [] + const next = function (path) { + this.$loading.finish && this.$loading.finish() + router.push(path) + } + await setContext(app, { + route: router.currentRoute, + isHMR: true, + next: next.bind(this) + }) + const context = app.context + + if (this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + + callMiddleware.call(this, Components, context) + .then(() => { + // If layout changed + if (depth !== 0) { + return + } + + let layout = Component.options.layout || 'default' + if (typeof layout === 'function') { + layout = layout(context) + } + if (this.layoutName === layout) { + return + } + let promise = this.loadLayout(layout) + promise.then(() => { + this.setLayout(layout) + Vue.nextTick(() => hotReloadAPI(this)) + }) + return promise + }) + + .then(() => { + return callMiddleware.call(this, Components, context, this.layout) + }) + + .then(() => { + // Call asyncData(context) + let pAsyncData = promisify(Component.options.asyncData || noopData, context) + pAsyncData.then((asyncDataResult) => { + applyAsyncData(Component, asyncDataResult) + this.$loading.increase && this.$loading.increase(30) + }) + promises.push(pAsyncData) + + // Call fetch() + Component.options.fetch = Component.options.fetch || noopFetch + let pFetch = Component.options.fetch.length && Component.options.fetch(context) + if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) } + pFetch.then(() => this.$loading.increase && this.$loading.increase(30)) + promises.push(pFetch) + + return Promise.all(promises) + }) + .then(() => { + this.$loading.finish && this.$loading.finish() + _forceUpdate() + setTimeout(() => hotReloadAPI(this), 100) + }) + } +} + +async function mountApp (__app) { + // Set global variables + app = __app.app + router = __app.router + + // Create Vue instance + const _app = new Vue(app) + + // Load layout + const layout = NUXT.layout || 'default' + await _app.loadLayout(layout) + _app.setLayout(layout) + + // Mounts Vue app to DOM element + const mount = () => { + _app.$mount('#__nuxt') + + // Add afterEach router hooks + router.afterEach(normalizeComponents) + + router.beforeResolve(getLayoutForNextPage.bind(_app)) + router.afterEach(setLayoutForNextPage.bind(_app)) + + router.afterEach(fixPrepatch.bind(_app)) + + // Listen for first Vue update + Vue.nextTick(() => { + // Call window.{{globals.readyCallback}} callbacks + nuxtReady(_app) + + // Enable hot reloading + hotReloadAPI(_app) + }) + } + + // Resolve route components + const Components = await Promise.all(resolveComponents(app.context.route)) + + // Enable transitions + _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app) + if (Components.length) { + _app.setTransitions(mapTransitions(Components, router.currentRoute)) + _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params)) + } + + // Initialize error handler + _app.$loading = {} // To avoid error while _app.$nuxt does not exist + if (NUXT.error) { + _app.error(NUXT.error) + _app.nuxt.errPageReady = true + } + + // Add beforeEach router hooks + router.beforeEach(loadAsyncComponents.bind(_app)) + + // Each new invocation of render() aborts previous invocation + let renderState = null + const boundRender = render.bind(_app) + router.beforeEach((to, from, next) => { + if (renderState) { + renderState.aborted = true + } + renderState = { aborted: false } + boundRender(to, from, next, renderState) + }) + + // Fix in static: remove trailing slash to force hydration + // Full static, if server-rendered: hydrate, to allow custom redirect to generated page + + // Fix in static: remove trailing slash to force hydration + if (NUXT.serverRendered && isSamePath(NUXT.routePath, _app.context.route.path)) { + return mount() + } + + const clientFirstLayoutSet = () => { + getLayoutForNextPage.call(_app, router.currentRoute) + setLayoutForNextPage.call(_app, router.currentRoute) + } + + // First render on client-side + const clientFirstMount = () => { + normalizeComponents(router.currentRoute, router.currentRoute) + clientFirstLayoutSet() + checkForErrors(_app) + // Don't call fixPrepatch.call(_app, router.currentRoute, router.currentRoute) since it's first render + mount() + } + + // fix: force next tick to avoid having same timestamp when an error happen on spa fallback + await new Promise(resolve => setTimeout(resolve, 0)) + render.call(_app, router.currentRoute, router.currentRoute, (path) => { + // If not redirected + if (!path) { + clientFirstMount() + return + } + + // Add a one-time afterEach hook to + // mount the app wait for redirect and route gets resolved + const unregisterHook = router.afterEach((to, from) => { + unregisterHook() + clientFirstMount() + }) + + // Push the path and let route to be resolved + router.push(path, undefined, (err) => { + if (err) { + errorHandler(err) + } + }) + }, + { aborted: false }) +} diff --git a/examples/nuxt-example/.nuxt/components/index.js b/examples/nuxt-example/.nuxt/components/index.js new file mode 100644 index 00000000..c2ce4a94 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/index.js @@ -0,0 +1,34 @@ +export { default as NavBar } from '../..\\components\\NavBar.vue' +export { default as NuxtLogo } from '../..\\components\\NuxtLogo.vue' +export { default as Tutorial } from '../..\\components\\Tutorial.vue' + +// nuxt/nuxt.js#8607 +function wrapFunctional(options) { + if (!options || !options.functional) { + return options + } + + const propKeys = Array.isArray(options.props) ? options.props : Object.keys(options.props || {}) + + return { + render(h) { + const attrs = {} + const props = {} + + for (const key in this.$attrs) { + if (propKeys.includes(key)) { + props[key] = this.$attrs[key] + } else { + attrs[key] = this.$attrs[key] + } + } + + return h(options, { + on: this.$listeners, + attrs, + props, + scopedSlots: this.$scopedSlots, + }, this.$slots.default) + } + } +} diff --git a/examples/nuxt-example/.nuxt/components/nuxt-build-indicator.vue b/examples/nuxt-example/.nuxt/components/nuxt-build-indicator.vue new file mode 100644 index 00000000..913f5448 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-build-indicator.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/examples/nuxt-example/.nuxt/components/nuxt-child.js b/examples/nuxt-example/.nuxt/components/nuxt-child.js new file mode 100644 index 00000000..9db3ee27 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-child.js @@ -0,0 +1,121 @@ +export default { + name: 'NuxtChild', + functional: true, + props: { + nuxtChildKey: { + type: String, + default: '' + }, + keepAlive: Boolean, + keepAliveProps: { + type: Object, + default: undefined + } + }, + render (_, { parent, data, props }) { + const h = parent.$createElement + + data.nuxtChild = true + const _parent = parent + const transitions = parent.$nuxt.nuxt.transitions + const defaultTransition = parent.$nuxt.nuxt.defaultTransition + + let depth = 0 + while (parent) { + if (parent.$vnode && parent.$vnode.data.nuxtChild) { + depth++ + } + parent = parent.$parent + } + data.nuxtChildDepth = depth + const transition = transitions[depth] || defaultTransition + const transitionProps = {} + transitionsKeys.forEach((key) => { + if (typeof transition[key] !== 'undefined') { + transitionProps[key] = transition[key] + } + }) + + const listeners = {} + listenersKeys.forEach((key) => { + if (typeof transition[key] === 'function') { + listeners[key] = transition[key].bind(_parent) + } + }) + if (process.client) { + // Add triggerScroll event on beforeEnter (fix #1376) + const beforeEnter = listeners.beforeEnter + listeners.beforeEnter = (el) => { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + if (beforeEnter) { + return beforeEnter.call(_parent, el) + } + } + } + + // make sure that leave is called asynchronous (fix #5703) + if (transition.css === false) { + const leave = listeners.leave + + // only add leave listener when user didnt provide one + // or when it misses the done argument + if (!leave || leave.length < 2) { + listeners.leave = (el, done) => { + if (leave) { + leave.call(_parent, el) + } + + _parent.$nextTick(done) + } + } + } + + let routerView = h('routerView', data) + + if (props.keepAlive) { + routerView = h('keep-alive', { props: props.keepAliveProps }, [routerView]) + } + + return h('transition', { + props: transitionProps, + on: listeners + }, [routerView]) + } +} + +const transitionsKeys = [ + 'name', + 'mode', + 'appear', + 'css', + 'type', + 'duration', + 'enterClass', + 'leaveClass', + 'appearClass', + 'enterActiveClass', + 'enterActiveClass', + 'leaveActiveClass', + 'appearActiveClass', + 'enterToClass', + 'leaveToClass', + 'appearToClass' +] + +const listenersKeys = [ + 'beforeEnter', + 'enter', + 'afterEnter', + 'enterCancelled', + 'beforeLeave', + 'leave', + 'afterLeave', + 'leaveCancelled', + 'beforeAppear', + 'appear', + 'afterAppear', + 'appearCancelled' +] diff --git a/examples/nuxt-example/.nuxt/components/nuxt-error.vue b/examples/nuxt-example/.nuxt/components/nuxt-error.vue new file mode 100644 index 00000000..e71f7d01 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-error.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/examples/nuxt-example/.nuxt/components/nuxt-link.client.js b/examples/nuxt-example/.nuxt/components/nuxt-link.client.js new file mode 100644 index 00000000..1477cfd0 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-link.client.js @@ -0,0 +1,98 @@ +import Vue from 'vue' + +const requestIdleCallback = window.requestIdleCallback || + function (cb) { + const start = Date.now() + return setTimeout(function () { + cb({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) + }) + }, 1) + } + +const cancelIdleCallback = window.cancelIdleCallback || function (id) { + clearTimeout(id) +} + +const observer = window.IntersectionObserver && new window.IntersectionObserver((entries) => { + entries.forEach(({ intersectionRatio, target: link }) => { + if (intersectionRatio <= 0 || !link.__prefetch) { + return + } + link.__prefetch() + }) +}) + +export default { + name: 'NuxtLink', + extends: Vue.component('RouterLink'), + props: { + prefetch: { + type: Boolean, + default: true + }, + noPrefetch: { + type: Boolean, + default: false + } + }, + mounted () { + if (this.prefetch && !this.noPrefetch) { + this.handleId = requestIdleCallback(this.observe, { timeout: 2e3 }) + } + }, + beforeDestroy () { + cancelIdleCallback(this.handleId) + + if (this.__observed) { + observer.unobserve(this.$el) + delete this.$el.__prefetch + } + }, + methods: { + observe () { + // If no IntersectionObserver, avoid prefetching + if (!observer) { + return + } + // Add to observer + if (this.shouldPrefetch()) { + this.$el.__prefetch = this.prefetchLink.bind(this) + observer.observe(this.$el) + this.__observed = true + } + }, + shouldPrefetch () { + return this.getPrefetchComponents().length > 0 + }, + canPrefetch () { + const conn = navigator.connection + const hasBadConnection = this.$nuxt.isOffline || (conn && ((conn.effectiveType || '').includes('2g') || conn.saveData)) + + return !hasBadConnection + }, + getPrefetchComponents () { + const ref = this.$router.resolve(this.to, this.$route, this.append) + const Components = ref.resolved.matched.map(r => r.components.default) + + return Components.filter(Component => typeof Component === 'function' && !Component.options && !Component.__prefetched) + }, + prefetchLink () { + if (!this.canPrefetch()) { + return + } + // Stop observing this link (in case of internet connection changes) + observer.unobserve(this.$el) + const Components = this.getPrefetchComponents() + + for (const Component of Components) { + const componentOrPromise = Component() + if (componentOrPromise instanceof Promise) { + componentOrPromise.catch(() => {}) + } + Component.__prefetched = true + } + } + } +} diff --git a/examples/nuxt-example/.nuxt/components/nuxt-link.server.js b/examples/nuxt-example/.nuxt/components/nuxt-link.server.js new file mode 100644 index 00000000..f1230908 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-link.server.js @@ -0,0 +1,16 @@ +import Vue from 'vue' + +export default { + name: 'NuxtLink', + extends: Vue.component('RouterLink'), + props: { + prefetch: { + type: Boolean, + default: true + }, + noPrefetch: { + type: Boolean, + default: false + } + } +} diff --git a/examples/nuxt-example/.nuxt/components/nuxt-loading.vue b/examples/nuxt-example/.nuxt/components/nuxt-loading.vue new file mode 100644 index 00000000..19e6311b --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt-loading.vue @@ -0,0 +1,178 @@ + + + diff --git a/examples/nuxt-example/.nuxt/components/nuxt.js b/examples/nuxt-example/.nuxt/components/nuxt.js new file mode 100644 index 00000000..e401ed3d --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/nuxt.js @@ -0,0 +1,102 @@ +import Vue from 'vue' +import { compile } from '../utils' + +import NuxtError from './nuxt-error.vue' + +import NuxtChild from './nuxt-child' + +export default { + name: 'Nuxt', + components: { + NuxtChild, + NuxtError + }, + props: { + nuxtChildKey: { + type: String, + default: undefined + }, + keepAlive: Boolean, + keepAliveProps: { + type: Object, + default: undefined + }, + name: { + type: String, + default: 'default' + } + }, + errorCaptured (error) { + // if we receive and error while showing the NuxtError component + // capture the error and force an immediate update so we re-render + // without the NuxtError component + if (this.displayingNuxtError) { + this.errorFromNuxtError = error + this.$forceUpdate() + } + }, + computed: { + routerViewKey () { + // If nuxtChildKey prop is given or current route has children + if (typeof this.nuxtChildKey !== 'undefined' || this.$route.matched.length > 1) { + return this.nuxtChildKey || compile(this.$route.matched[0].path)(this.$route.params) + } + + const [matchedRoute] = this.$route.matched + + if (!matchedRoute) { + return this.$route.path + } + + const Component = matchedRoute.components.default + + if (Component && Component.options) { + const { options } = Component + + if (options.key) { + return (typeof options.key === 'function' ? options.key(this.$route) : options.key) + } + } + + const strict = /\/$/.test(matchedRoute.path) + return strict ? this.$route.path : this.$route.path.replace(/\/$/, '') + } + }, + beforeCreate () { + Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt) + }, + render (h) { + // if there is no error or + // error page has not been loaded yet on client + if (!this.nuxt.err || (process.client && !this.nuxt.errPageReady)) { + // Directly return nuxt child + return h('NuxtChild', { + key: this.routerViewKey, + props: this.$props + }) + } + + // if an error occurred within NuxtError show a simple + // error message instead to prevent looping + if (this.errorFromNuxtError) { + this.$nextTick(() => (this.errorFromNuxtError = false)) + + return h('div', {}, [ + h('h2', 'An error occurred while showing the error page'), + h('p', 'Unfortunately an error occurred and while showing the error page another error occurred'), + h('p', `Error details: ${this.errorFromNuxtError.toString()}`), + h('nuxt-link', { props: { to: '/' } }, 'Go back to home') + ]) + } + + // track if we are showing the NuxtError component + this.displayingNuxtError = true + this.$nextTick(() => (this.displayingNuxtError = false)) + + return h(NuxtError, { + props: { + error: this.nuxt.err + } + }) + } +} diff --git a/examples/nuxt-example/.nuxt/components/plugin.js b/examples/nuxt-example/.nuxt/components/plugin.js new file mode 100644 index 00000000..121bdcf1 --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/plugin.js @@ -0,0 +1,7 @@ +import Vue from 'vue' +import * as components from './index' + +for (const name in components) { + Vue.component(name, components[name]) + Vue.component('Lazy' + name, components[name]) +} diff --git a/examples/nuxt-example/.nuxt/components/readme.md b/examples/nuxt-example/.nuxt/components/readme.md new file mode 100644 index 00000000..8448575c --- /dev/null +++ b/examples/nuxt-example/.nuxt/components/readme.md @@ -0,0 +1,11 @@ +# Discovered Components + +This is an auto-generated list of components discovered by [nuxt/components](https://github.com/nuxt/components). + +You can directly use them in pages and other components without the need to import them. + +**Tip:** If a component is conditionally rendered with `v-if` and is big, it is better to use `Lazy` or `lazy-` prefix to lazy load. + +- `` | `` (components/NavBar.vue) +- `` | `` (components/NuxtLogo.vue) +- `` | `` (components/Tutorial.vue) diff --git a/examples/nuxt-example/.nuxt/empty.js b/examples/nuxt-example/.nuxt/empty.js new file mode 100644 index 00000000..a3ac0d84 --- /dev/null +++ b/examples/nuxt-example/.nuxt/empty.js @@ -0,0 +1 @@ +// This file is intentionally left empty for noop aliases diff --git a/examples/nuxt-example/.nuxt/index.js b/examples/nuxt-example/.nuxt/index.js new file mode 100644 index 00000000..e102393e --- /dev/null +++ b/examples/nuxt-example/.nuxt/index.js @@ -0,0 +1,228 @@ +import Vue from 'vue' + +import Meta from 'vue-meta' +import ClientOnly from 'vue-client-only' +import NoSsr from 'vue-no-ssr' +import { createRouter } from './router.js' +import NuxtChild from './components/nuxt-child.js' +import NuxtError from './components/nuxt-error.vue' +import Nuxt from './components/nuxt.js' +import App from './App.js' +import { setContext, getLocation, getRouteData, normalizeError } from './utils' + +/* Plugins */ + +import nuxt_plugin_plugin_9bcb59fa from 'nuxt_plugin_plugin_9bcb59fa' // Source: .\\components\\plugin.js (mode: 'all') + +// Component: +Vue.component(ClientOnly.name, ClientOnly) + +// TODO: Remove in Nuxt 3: +Vue.component(NoSsr.name, { + ...NoSsr, + render (h, ctx) { + if (process.client && !NoSsr._warned) { + NoSsr._warned = true + + console.warn(' has been deprecated and will be removed in Nuxt 3, please use instead') + } + return NoSsr.render(h, ctx) + } +}) + +// Component: +Vue.component(NuxtChild.name, NuxtChild) +Vue.component('NChild', NuxtChild) + +// Component NuxtLink is imported in server.js or client.js + +// Component: +Vue.component(Nuxt.name, Nuxt) + +Object.defineProperty(Vue.prototype, '$nuxt', { + get() { + const globalNuxt = this.$root ? this.$root.$options.$nuxt : null + if (process.client && !globalNuxt && typeof window !== 'undefined') { + return window.$nuxt + } + return globalNuxt + }, + configurable: true +}) + +Vue.use(Meta, {"keyName":"head","attribute":"data-n-head","ssrAttribute":"data-n-head-ssr","tagIDKeyName":"hid"}) + +const defaultTransition = {"name":"page","mode":"out-in","appear":false,"appearClass":"appear","appearActiveClass":"appear-active","appearToClass":"appear-to"} + +async function createApp(ssrContext, config = {}) { + const store = null + const router = await createRouter(ssrContext, config, { store }) + + // Create Root instance + + // here we inject the router and store to all child components, + // making them available everywhere as `this.$router` and `this.$store`. + const app = { + head: {"title":"nuxt-app-example","htmlAttrs":{"lang":"en"},"meta":[{"charset":"utf-8"},{"name":"viewport","content":"width=device-width, initial-scale=1"},{"hid":"description","name":"description","content":""},{"name":"format-detection","content":"telephone=no"}],"link":[{"rel":"icon","type":"image\u002Fx-icon","href":"\u002Ffavicon.ico"}],"style":[],"script":[]}, + + router, + nuxt: { + defaultTransition, + transitions: [defaultTransition], + setTransitions (transitions) { + if (!Array.isArray(transitions)) { + transitions = [transitions] + } + transitions = transitions.map((transition) => { + if (!transition) { + transition = defaultTransition + } else if (typeof transition === 'string') { + transition = Object.assign({}, defaultTransition, { name: transition }) + } else { + transition = Object.assign({}, defaultTransition, transition) + } + return transition + }) + this.$options.nuxt.transitions = transitions + return transitions + }, + + err: null, + errPageReady: false, + dateErr: null, + error (err) { + err = err || null + app.context._errored = Boolean(err) + err = err ? normalizeError(err) : null + let nuxt = app.nuxt // to work with @vue/composition-api, see https://github.com/nuxt/nuxt.js/issues/6517#issuecomment-573280207 + if (this) { + nuxt = this.nuxt || this.$options.nuxt + } + nuxt.dateErr = Date.now() + nuxt.err = err + nuxt.errPageReady = false + // Used in src/server.js + if (ssrContext) { + ssrContext.nuxt.error = err + } + return err + } + }, + ...App + } + + const next = ssrContext ? ssrContext.next : location => app.router.push(location) + // Resolve route + let route + if (ssrContext) { + route = router.resolve(ssrContext.url).route + } else { + const path = getLocation(router.options.base, router.options.mode) + route = router.resolve(path).route + } + + // Set context to app.context + await setContext(app, { + route, + next, + error: app.nuxt.error.bind(app), + payload: ssrContext ? ssrContext.payload : undefined, + req: ssrContext ? ssrContext.req : undefined, + res: ssrContext ? ssrContext.res : undefined, + beforeRenderFns: ssrContext ? ssrContext.beforeRenderFns : undefined, + beforeSerializeFns: ssrContext ? ssrContext.beforeSerializeFns : undefined, + ssrContext + }) + + function inject(key, value) { + if (!key) { + throw new Error('inject(key, value) has no key provided') + } + if (value === undefined) { + throw new Error(`inject('${key}', value) has no value provided`) + } + + key = '$' + key + // Add into app + app[key] = value + // Add into context + if (!app.context[key]) { + app.context[key] = value + } + + // Check if plugin not already installed + const installKey = '__nuxt_' + key + '_installed__' + if (Vue[installKey]) { + return + } + Vue[installKey] = true + // Call Vue.use() to install the plugin into vm + Vue.use(() => { + if (!Object.prototype.hasOwnProperty.call(Vue.prototype, key)) { + Object.defineProperty(Vue.prototype, key, { + get () { + return this.$root.$options[key] + } + }) + } + }) + } + + // Inject runtime config as $config + inject('config', config) + + // Add enablePreview(previewData = {}) in context for plugins + if (process.static && process.client) { + app.context.enablePreview = function (previewData = {}) { + app.previewData = Object.assign({}, previewData) + inject('preview', previewData) + } + } + // Plugin execution + + if (typeof nuxt_plugin_plugin_9bcb59fa === 'function') { + await nuxt_plugin_plugin_9bcb59fa(app.context, inject) + } + + // Lock enablePreview in context + if (process.static && process.client) { + app.context.enablePreview = function () { + console.warn('You cannot call enablePreview() outside a plugin.') + } + } + + // Wait for async component to be resolved first + await new Promise((resolve, reject) => { + // Ignore 404s rather than blindly replacing URL in browser + if (process.client) { + const { route } = router.resolve(app.context.route.fullPath) + if (!route.matched.length) { + return resolve() + } + } + router.replace(app.context.route.fullPath, resolve, (err) => { + // https://github.com/vuejs/vue-router/blob/v3.4.3/src/util/errors.js + if (!err._isRouter) return reject(err) + if (err.type !== 2 /* NavigationFailureType.redirected */) return resolve() + + // navigated to a different route in router guard + const unregister = router.afterEach(async (to, from) => { + if (process.server && ssrContext && ssrContext.url) { + ssrContext.url = to.fullPath + } + app.context.route = await getRouteData(to) + app.context.params = to.params || {} + app.context.query = to.query || {} + unregister() + resolve() + }) + }) + }) + + return { + app, + router + } +} + +export { createApp, NuxtError } diff --git a/examples/nuxt-example/.nuxt/jsonp.js b/examples/nuxt-example/.nuxt/jsonp.js new file mode 100644 index 00000000..702adf21 --- /dev/null +++ b/examples/nuxt-example/.nuxt/jsonp.js @@ -0,0 +1,82 @@ +const chunks = {} // chunkId => exports +const chunksInstalling = {} // chunkId => Promise +const failedChunks = {} + +function importChunk(chunkId, src) { + // Already installed + if (chunks[chunkId]) { + return Promise.resolve(chunks[chunkId]) + } + + // Failed loading + if (failedChunks[chunkId]) { + return Promise.reject(failedChunks[chunkId]) + } + + // Installing + if (chunksInstalling[chunkId]) { + return chunksInstalling[chunkId] + } + + // Set a promise in chunk cache + let resolve, reject + const promise = chunksInstalling[chunkId] = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + + // Clear chunk data from cache + delete chunks[chunkId] + + // Start chunk loading + const script = document.createElement('script') + script.charset = 'utf-8' + script.timeout = 120 + script.src = src + let timeout + + // Create error before stack unwound to get useful stacktrace later + const error = new Error() + + // Complete handlers + const onScriptComplete = script.onerror = script.onload = (event) => { + // Cleanups + clearTimeout(timeout) + delete chunksInstalling[chunkId] + + // Avoid mem leaks in IE + script.onerror = script.onload = null + + // Verify chunk is loaded + if (chunks[chunkId]) { + return resolve(chunks[chunkId]) + } + + // Something bad happened + const errorType = event && (event.type === 'load' ? 'missing' : event.type) + const realSrc = event && event.target && event.target.src + error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' + error.name = 'ChunkLoadError' + error.type = errorType + error.request = realSrc + failedChunks[chunkId] = error + reject(error) + } + + // Timeout + timeout = setTimeout(() => { + onScriptComplete({ type: 'timeout', target: script }) + }, 120000) + + // Append script + document.head.appendChild(script) + + // Return promise + return promise +} + +export function installJsonp() { + window.__NUXT_JSONP__ = function (chunkId, exports) { chunks[chunkId] = exports } + window.__NUXT_JSONP_CACHE__ = chunks + window.__NUXT_IMPORT__ = importChunk +} diff --git a/examples/nuxt-example/.nuxt/loading.html b/examples/nuxt-example/.nuxt/loading.html new file mode 100644 index 00000000..a6dcd1b5 --- /dev/null +++ b/examples/nuxt-example/.nuxt/loading.html @@ -0,0 +1,110 @@ + + + + +
Loading...
+ + diff --git a/examples/nuxt-example/.nuxt/middleware.js b/examples/nuxt-example/.nuxt/middleware.js new file mode 100644 index 00000000..343ef954 --- /dev/null +++ b/examples/nuxt-example/.nuxt/middleware.js @@ -0,0 +1,3 @@ +const middleware = {} + +export default middleware diff --git a/examples/nuxt-example/.nuxt/mixins/fetch.client.js b/examples/nuxt-example/.nuxt/mixins/fetch.client.js new file mode 100644 index 00000000..017e559e --- /dev/null +++ b/examples/nuxt-example/.nuxt/mixins/fetch.client.js @@ -0,0 +1,90 @@ +import Vue from 'vue' +import { hasFetch, normalizeError, addLifecycleHook, createGetCounter } from '../utils' + +const isSsrHydration = (vm) => vm.$vnode && vm.$vnode.elm && vm.$vnode.elm.dataset && vm.$vnode.elm.dataset.fetchKey +const nuxtState = window.__NUXT__ + +export default { + beforeCreate () { + if (!hasFetch(this)) { + return + } + + this._fetchDelay = typeof this.$options.fetchDelay === 'number' ? this.$options.fetchDelay : 200 + + Vue.util.defineReactive(this, '$fetchState', { + pending: false, + error: null, + timestamp: Date.now() + }) + + this.$fetch = $fetch.bind(this) + addLifecycleHook(this, 'created', created) + addLifecycleHook(this, 'beforeMount', beforeMount) + } +} + +function beforeMount() { + if (!this._hydrated) { + return this.$fetch() + } +} + +function created() { + if (!isSsrHydration(this)) { + return + } + + // Hydrate component + this._hydrated = true + this._fetchKey = this.$vnode.elm.dataset.fetchKey + const data = nuxtState.fetch[this._fetchKey] + + // If fetch error + if (data && data._error) { + this.$fetchState.error = data._error + return + } + + // Merge data + for (const key in data) { + Vue.set(this.$data, key, data[key]) + } +} + +function $fetch() { + if (!this._fetchPromise) { + this._fetchPromise = $_fetch.call(this) + .then(() => { delete this._fetchPromise }) + } + return this._fetchPromise +} + +async function $_fetch() { + this.$nuxt.nbFetching++ + this.$fetchState.pending = true + this.$fetchState.error = null + this._hydrated = false + let error = null + const startTime = Date.now() + + try { + await this.$options.fetch.call(this) + } catch (err) { + if (process.dev) { + console.error('Error in fetch():', err) + } + error = normalizeError(err) + } + + const delayLeft = this._fetchDelay - (Date.now() - startTime) + if (delayLeft > 0) { + await new Promise(resolve => setTimeout(resolve, delayLeft)) + } + + this.$fetchState.error = error + this.$fetchState.pending = false + this.$fetchState.timestamp = Date.now() + + this.$nextTick(() => this.$nuxt.nbFetching--) +} diff --git a/examples/nuxt-example/.nuxt/mixins/fetch.server.js b/examples/nuxt-example/.nuxt/mixins/fetch.server.js new file mode 100644 index 00000000..24a00686 --- /dev/null +++ b/examples/nuxt-example/.nuxt/mixins/fetch.server.js @@ -0,0 +1,69 @@ +import Vue from 'vue' +import { hasFetch, normalizeError, addLifecycleHook, purifyData, createGetCounter } from '../utils' + +async function serverPrefetch() { + if (!this._fetchOnServer) { + return + } + + // Call and await on $fetch + try { + await this.$options.fetch.call(this) + } catch (err) { + if (process.dev) { + console.error('Error in fetch():', err) + } + this.$fetchState.error = normalizeError(err) + } + this.$fetchState.pending = false + + // Define an ssrKey for hydration + this._fetchKey = this._fetchKey || this.$ssrContext.fetchCounters['']++ + + // Add data-fetch-key on parent element of Component + const attrs = this.$vnode.data.attrs = this.$vnode.data.attrs || {} + attrs['data-fetch-key'] = this._fetchKey + + // Add to ssrContext for window.__NUXT__.fetch + + if (this.$ssrContext.nuxt.fetch[this._fetchKey] !== undefined) { + console.warn(`Duplicate fetch key detected (${this._fetchKey}). This may lead to unexpected results.`) + } + + this.$ssrContext.nuxt.fetch[this._fetchKey] = + this.$fetchState.error ? { _error: this.$fetchState.error } : purifyData(this._data) +} + +export default { + created() { + if (!hasFetch(this)) { + return + } + + if (typeof this.$options.fetchOnServer === 'function') { + this._fetchOnServer = this.$options.fetchOnServer.call(this) !== false + } else { + this._fetchOnServer = this.$options.fetchOnServer !== false + } + + const defaultKey = this.$options._scopeId || this.$options.name || '' + const getCounter = createGetCounter(this.$ssrContext.fetchCounters, defaultKey) + + if (typeof this.$options.fetchKey === 'function') { + this._fetchKey = this.$options.fetchKey.call(this, getCounter) + } else { + const key = 'string' === typeof this.$options.fetchKey ? this.$options.fetchKey : defaultKey + this._fetchKey = key ? key + ':' + getCounter(key) : String(getCounter(key)) + } + + // Added for remove vue undefined warning while ssr + this.$fetch = () => {} // issue #8043 + Vue.util.defineReactive(this, '$fetchState', { + pending: true, + error: null, + timestamp: Date.now() + }) + + addLifecycleHook(this, 'serverPrefetch', serverPrefetch) + } +} diff --git a/examples/nuxt-example/.nuxt/router.js b/examples/nuxt-example/.nuxt/router.js new file mode 100644 index 00000000..142455ca --- /dev/null +++ b/examples/nuxt-example/.nuxt/router.js @@ -0,0 +1,53 @@ +import Vue from 'vue' +import Router from 'vue-router' +import { normalizeURL, decode } from 'ufo' +import { interopDefault } from './utils' +import scrollBehavior from './router.scrollBehavior.js' + +const _19757575 = () => interopDefault(import('..\\pages\\about.vue' /* webpackChunkName: "pages/about" */)) +const _392d503a = () => interopDefault(import('..\\pages\\index.vue' /* webpackChunkName: "pages/index" */)) + +const emptyFn = () => {} + +Vue.use(Router) + +export const routerOptions = { + mode: 'history', + base: '/', + linkActiveClass: 'nuxt-link-active', + linkExactActiveClass: 'nuxt-link-exact-active', + scrollBehavior, + + routes: [{ + path: "/about", + component: _19757575, + name: "about" + }, { + path: "/", + component: _392d503a, + name: "index" + }], + + fallback: false +} + +export function createRouter (ssrContext, config) { + const base = (config._app && config._app.basePath) || routerOptions.base + const router = new Router({ ...routerOptions, base }) + + // TODO: remove in Nuxt 3 + const originalPush = router.push + router.push = function push (location, onComplete = emptyFn, onAbort) { + return originalPush.call(this, location, onComplete, onAbort) + } + + const resolve = router.resolve.bind(router) + router.resolve = (to, current, append) => { + if (typeof to === 'string') { + to = normalizeURL(to) + } + return resolve(to, current, append) + } + + return router +} diff --git a/examples/nuxt-example/.nuxt/router.scrollBehavior.js b/examples/nuxt-example/.nuxt/router.scrollBehavior.js new file mode 100644 index 00000000..fd193781 --- /dev/null +++ b/examples/nuxt-example/.nuxt/router.scrollBehavior.js @@ -0,0 +1,82 @@ +import { getMatchedComponents, setScrollRestoration } from './utils' + +if (process.client) { + if ('scrollRestoration' in window.history) { + setScrollRestoration('manual') + + // reset scrollRestoration to auto when leaving page, allowing page reload + // and back-navigation from other pages to use the browser to restore the + // scrolling position. + window.addEventListener('beforeunload', () => { + setScrollRestoration('auto') + }) + + // Setting scrollRestoration to manual again when returning to this page. + window.addEventListener('load', () => { + setScrollRestoration('manual') + }) + } +} + +function shouldScrollToTop(route) { + const Pages = getMatchedComponents(route) + if (Pages.length === 1) { + const { options = {} } = Pages[0] + return options.scrollToTop !== false + } + return Pages.some(({ options }) => options && options.scrollToTop) +} + +export default function (to, from, savedPosition) { + // If the returned position is falsy or an empty object, will retain current scroll position + let position = false + const isRouteChanged = to !== from + + // savedPosition is only available for popstate navigations (back button) + if (savedPosition) { + position = savedPosition + } else if (isRouteChanged && shouldScrollToTop(to)) { + position = { x: 0, y: 0 } + } + + const nuxt = window.$nuxt + + if ( + // Initial load (vuejs/vue-router#3199) + !isRouteChanged || + // Route hash changes + (to.path === from.path && to.hash !== from.hash) + ) { + nuxt.$nextTick(() => nuxt.$emit('triggerScroll')) + } + + return new Promise((resolve) => { + // wait for the out transition to complete (if necessary) + nuxt.$once('triggerScroll', () => { + // coords will be used if no selector is provided, + // or if the selector didn't match any element. + if (to.hash) { + let hash = to.hash + // CSS.escape() is not supported with IE and Edge. + if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') { + hash = '#' + window.CSS.escape(hash.substr(1)) + } + try { + const el = document.querySelector(hash) + if (el) { + // scroll to anchor by returning the selector + position = { selector: hash } + // Respect any scroll-margin-top set in CSS when scrolling to anchor + const y = Number(getComputedStyle(el)['scroll-margin-top']?.replace('px', '')) + if (y) { + position.offset = { y } + } + } + } catch (e) { + console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).') + } + } + resolve(position) + }) + }) +} diff --git a/examples/nuxt-example/.nuxt/routes.json b/examples/nuxt-example/.nuxt/routes.json new file mode 100644 index 00000000..bd5321ec --- /dev/null +++ b/examples/nuxt-example/.nuxt/routes.json @@ -0,0 +1,16 @@ +[ + { + "name": "about", + "path": "/about", + "component": "C:\\\\Users\\\\linglingye\\\\JavaScript\\\\nuxt-app-example\\\\pages\\\\about.vue", + "chunkName": "pages/about", + "_name": "_19757575" + }, + { + "name": "index", + "path": "/", + "component": "C:\\\\Users\\\\linglingye\\\\JavaScript\\\\nuxt-app-example\\\\pages\\\\index.vue", + "chunkName": "pages/index", + "_name": "_392d503a" + } +] diff --git a/examples/nuxt-example/.nuxt/server.js b/examples/nuxt-example/.nuxt/server.js new file mode 100644 index 00000000..9a3f24da --- /dev/null +++ b/examples/nuxt-example/.nuxt/server.js @@ -0,0 +1,296 @@ +import Vue from 'vue' +import { joinURL, normalizeURL, withQuery } from 'ufo' +import fetch from 'node-fetch-native' +import middleware from './middleware.js' +import { + applyAsyncData, + middlewareSeries, + sanitizeComponent, + getMatchedComponents, + promisify +} from './utils.js' + import fetchMixin from './mixins/fetch.server' +import { createApp, NuxtError } from './index.js' +import NuxtLink from './components/nuxt-link.server.js' // should be included after ./index.js + + // Update serverPrefetch strategy + Vue.config.optionMergeStrategies.serverPrefetch = Vue.config.optionMergeStrategies.created + + // Fetch mixin + if (!Vue.__nuxt__fetch__mixin__) { + Vue.mixin(fetchMixin) + Vue.__nuxt__fetch__mixin__ = true + } + +if (!Vue.__original_use__) { + Vue.__original_use__ = Vue.use + Vue.__install_times__ = 0 + Vue.use = function (plugin, ...args) { + plugin.__nuxt_external_installed__ = Vue._installedPlugins.includes(plugin) + return Vue.__original_use__(plugin, ...args) + } + } + if (Vue.__install_times__ === 2) { + Vue.__install_times__ = 0 + Vue._installedPlugins = Vue._installedPlugins.filter(plugin => { + return plugin.__nuxt_external_installed__ === true + }) + } + Vue.__install_times__++ + + // Component: + Vue.component(NuxtLink.name, NuxtLink) + Vue.component('NLink', NuxtLink) + +if (!global.fetch) { global.fetch = fetch } + +const noopApp = () => new Vue({ render: h => h('div', { domProps: { id: '__nuxt' } }) }) + +const createNext = ssrContext => (opts) => { + // If static target, render on client-side + ssrContext.redirected = opts + if (ssrContext.target === 'static' || !ssrContext.res) { + ssrContext.nuxt.serverRendered = false + return + } + let fullPath = withQuery(opts.path, opts.query) + const $config = ssrContext.nuxt.config || {} + const routerBase = ($config._app && $config._app.basePath) || '/' + if (!fullPath.startsWith('http') && (routerBase !== '/' && !fullPath.startsWith(routerBase))) { + fullPath = joinURL(routerBase, fullPath) + } + // Avoid loop redirect + if (decodeURI(fullPath) === decodeURI(ssrContext.url)) { + ssrContext.redirected = false + return + } + ssrContext.res.writeHead(opts.status, { + Location: normalizeURL(fullPath) + }) + ssrContext.res.end() +} + +// This exported function will be called by `bundleRenderer`. +// This is where we perform data-prefetching to determine the +// state of our application before actually rendering it. +// Since data fetching is async, this function is expected to +// return a Promise that resolves to the app instance. +export default async (ssrContext) => { + // Create ssrContext.next for simulate next() of beforeEach() when wanted to redirect + ssrContext.redirected = false + ssrContext.next = createNext(ssrContext) + // Used for beforeNuxtRender({ Components, nuxtState }) + ssrContext.beforeRenderFns = [] + // for beforeSerialize(nuxtState) + ssrContext.beforeSerializeFns = [] + // Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__) + ssrContext.nuxt = { layout: 'default', data: [], fetch: { }, error: null , serverRendered: true, routePath: '' +} + + ssrContext.fetchCounters = { } + + // Remove query from url is static target + + // Public runtime config + ssrContext.nuxt.config = ssrContext.runtimeConfig.public +if (ssrContext.nuxt.config._app) { + __webpack_public_path__ = joinURL(ssrContext.nuxt.config._app.cdnURL, ssrContext.nuxt.config._app.assetsPath) +} +// Create the app definition and the instance (created for each request) +const { app, router } = await createApp(ssrContext, ssrContext.runtimeConfig.private) +const _app = new Vue(app) +// Add ssr route path to nuxt context so we can account for page navigation between ssr and csr +ssrContext.nuxt.routePath = app.context.route.path + + // Add meta infos (used in renderer.js) + ssrContext.meta = _app.$meta() + + // Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext) + ssrContext.asyncData = { } + + const beforeRender = async () => { + // Call beforeNuxtRender() methods + await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt }))) + + ssrContext.rendered = () => { + // Call beforeSerialize() hooks + ssrContext.beforeSerializeFns.forEach(fn => fn(ssrContext.nuxt)) + } +} + +const renderErrorPage = async () => { + // Don't server-render the page in static target + if (ssrContext.target === 'static') { + ssrContext.nuxt.serverRendered = false + } + + // Load layout for error page + const layout = (NuxtError.options || NuxtError).layout + const errLayout = typeof layout === 'function' ? layout.call(NuxtError, app.context) : layout + ssrContext.nuxt.layout = errLayout || 'default' + await _app.loadLayout(errLayout) + _app.setLayout(errLayout) + + await beforeRender() + return _app +} +const render404Page = () => { + app.context.error({ statusCode: 404, path: ssrContext.url, message: 'This page could not be found' }) + return renderErrorPage() +} + + const s = Date.now() + + // Components are already resolved by setContext -> getRouteData (app/utils.js) + const Components = getMatchedComponents(app.context.route) + + /* + ** Call global middleware (nuxt.config.js) + */ + let midd = [] + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + await middlewareSeries(midd, app.context) + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + /* + ** Set layout + */ + let layout = Components.length ? Components[0].options.layout : NuxtError.layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + await _app.loadLayout(layout) + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + layout = _app.setLayout(layout) + ssrContext.nuxt.layout = _app.layoutName + + /* + ** Call middleware (layout + pages) + */ + midd =[] + + layout = sanitizeComponent(layout) + if (layout.options.middleware) { + midd = midd.concat(layout.options.middleware) + } + + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + await middlewareSeries(midd, app.context) + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + /* + ** Call .validate() + */ + let isValid = true + try { + for (const Component of Components) { + if (typeof Component.options.validate !== 'function') { + continue + } + + isValid = await Component.options.validate(app.context) + + if (!isValid) { + break + } + } + } catch (validationError) { + // ...If .validate() threw an error + app.context.error({ + statusCode: validationError.statusCode || '500', + message: validationError.message + }) + return renderErrorPage() + } + + // ...If .validate() returned false + if (!isValid) { + // Render a 404 error page + return render404Page() + } + + // If no Components found, returns 404 + if (!Components.length) { + return render404Page() +} + + // Call asyncData & fetch hooks on components matched by the route. + const asyncDatas = await Promise.all(Components.map((Component) => { + const promises = [] + + // Call asyncData(context) + if (Component.options.asyncData && typeof Component.options.asyncData === 'function') { + const promise = promisify(Component.options.asyncData, app.context) + .then((asyncDataResult) => { + ssrContext.asyncData[Component.cid] = asyncDataResult + applyAsyncData(Component) + return asyncDataResult + }) + promises.push(promise) + } else { + promises.push(null) + } + + // Call fetch(context) + if (Component.options.fetch && Component.options.fetch.length) { + promises.push(Component.options.fetch(app.context)) + } else { + promises.push(null) + } + + return Promise.all(promises) +})) + + if (process.env.DEBUG && asyncDatas.length) console.debug('Data fetching ' + ssrContext.url + ': ' + (Date.now() - s) + 'ms') + + // datas are the first row of each + ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {}) + + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() +} +if (ssrContext.nuxt.error) { + return renderErrorPage() +} + +// Call beforeNuxtRender methods & add store state +await beforeRender() + +return _app +} diff --git a/examples/nuxt-example/.nuxt/utils.js b/examples/nuxt-example/.nuxt/utils.js new file mode 100644 index 00000000..4d271084 --- /dev/null +++ b/examples/nuxt-example/.nuxt/utils.js @@ -0,0 +1,637 @@ +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) + } + 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() + try { + 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 */) + } + } catch { + // don't throw an error if we have issues reading sessionStorage + } + } + + 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: true, + isHMR: false, + app, + + payload: context.payload, + error: context.error, + base: app.router.options.base, + env: {} + } + // 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/assign + window.location.assign(path) + + // Throw a redirect error + throw new Error('ERR_REDIRECT') + } + } + } + if (process.server) { + app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn) + app.context.beforeSerialize = fn => context.beforeSerializeFns.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 + } + + if (context.error) { + app.context.error = context.error + } + + app.context.next = context.next + app.context._redirected = false + app.context._errored = false + app.context.isHMR = Boolean(context.isHMR) + app.context.params = app.context.route.params || {} + app.context.query = app.context.route.query || {} +} + +export function middlewareSeries (promises, appContext, renderState) { + if (!promises.length || appContext._redirected || appContext._errored || (renderState && renderState.aborted)) { + return Promise.resolve() + } + return promisify(promises[0], appContext) + .then(() => { + return middlewareSeries(promises.slice(1), appContext, renderState) + }) +} + +export function promisify (fn, context) { + let promise + if (fn.length === 2) { + 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) + } + + 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) {} +} diff --git a/examples/nuxt-example/.nuxt/vetur/tags.json b/examples/nuxt-example/.nuxt/vetur/tags.json new file mode 100644 index 00000000..c8b534b3 --- /dev/null +++ b/examples/nuxt-example/.nuxt/vetur/tags.json @@ -0,0 +1,11 @@ +{ + "NavBar": { + "description": "Auto imported from components/NavBar.vue" + }, + "NuxtLogo": { + "description": "Auto imported from components/NuxtLogo.vue" + }, + "Tutorial": { + "description": "Auto imported from components/Tutorial.vue" + } +} diff --git a/examples/nuxt-example/.nuxt/views/app.template.html b/examples/nuxt-example/.nuxt/views/app.template.html new file mode 100644 index 00000000..3427d3ea --- /dev/null +++ b/examples/nuxt-example/.nuxt/views/app.template.html @@ -0,0 +1,9 @@ + + + + {{ HEAD }} + + + {{ APP }} + + diff --git a/examples/nuxt-example/.nuxt/views/error.html b/examples/nuxt-example/.nuxt/views/error.html new file mode 100644 index 00000000..082a41fc --- /dev/null +++ b/examples/nuxt-example/.nuxt/views/error.html @@ -0,0 +1,23 @@ + + + +Server error + + + + + +
+
+ +
Server error
+
{{ message }}
+
+ +
+ + diff --git a/examples/nuxt-example/README.md b/examples/nuxt-example/README.md new file mode 100644 index 00000000..0d7b4641 --- /dev/null +++ b/examples/nuxt-example/README.md @@ -0,0 +1,69 @@ +# nuxt-app-example + +## Build Setup + +```bash +# install dependencies +$ npm install + +# serve with hot reload at localhost:3000 +$ npm run dev + +# build for production and launch server +$ npm run build +$ npm run start + +# generate static project +$ npm run generate +``` + +For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org). + +## Special Directories + +You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality. + +### `assets` + +The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets). + +### `components` + +The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components). + +### `layouts` + +Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts). + + +### `pages` + +This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing). + +### `plugins` + +The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins). + +### `static` + +This directory contains your static files. Each file inside this directory is mapped to `/`. + +Example: `/static/robots.txt` is mapped as `/robots.txt`. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static). + +### `store` + +This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store). diff --git a/examples/nuxt-example/components/NavBar.vue b/examples/nuxt-example/components/NavBar.vue new file mode 100644 index 00000000..d29a68cb --- /dev/null +++ b/examples/nuxt-example/components/NavBar.vue @@ -0,0 +1,29 @@ +// components/NavBar.vue + + + + + diff --git a/examples/nuxt-example/components/NuxtLogo.vue b/examples/nuxt-example/components/NuxtLogo.vue new file mode 100644 index 00000000..970eba0e --- /dev/null +++ b/examples/nuxt-example/components/NuxtLogo.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/nuxt-example/components/Tutorial.vue b/examples/nuxt-example/components/Tutorial.vue new file mode 100644 index 00000000..6b96cf03 --- /dev/null +++ b/examples/nuxt-example/components/Tutorial.vue @@ -0,0 +1,52 @@ + + + + diff --git a/examples/nuxt-example/layouts/default.vue b/examples/nuxt-example/layouts/default.vue new file mode 100644 index 00000000..e6694f44 --- /dev/null +++ b/examples/nuxt-example/layouts/default.vue @@ -0,0 +1,32 @@ +// layouts/default.vue + + + + + diff --git a/examples/nuxt-example/nuxt.config.js b/examples/nuxt-example/nuxt.config.js new file mode 100644 index 00000000..8ce3d486 --- /dev/null +++ b/examples/nuxt-example/nuxt.config.js @@ -0,0 +1,50 @@ +export default { + // Global page headers: https://go.nuxtjs.dev/config-head + head: { + title: 'nuxt-app-example', + htmlAttrs: { + lang: 'en' + }, + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { hid: 'description', name: 'description', content: '' }, + { name: 'format-detection', content: 'telephone=no' } + ], + link: [ + { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } + ] + }, + + // Global CSS: https://go.nuxtjs.dev/config-css + css: [ + ], + + // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins + plugins: [ + ], + + // Auto import components: https://go.nuxtjs.dev/config-components + components: true, + + // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules + buildModules: [ + ], + + // Modules: https://go.nuxtjs.dev/config-modules + modules: [ + ], + + // Build Configuration: https://go.nuxtjs.dev/config-build + build: { + transpile: ['@azure/app-configuration-provider'], // Add the Azure package to transpile + loaders: { + js: { + transpileOnly: true, // Skip type-checking for JS files + } + }, + esbuild: { + target: 'esnext', // Make sure it's targeting modern JavaScript + } + } +} diff --git a/examples/nuxt-example/package.json b/examples/nuxt-example/package.json new file mode 100644 index 00000000..62902d8e --- /dev/null +++ b/examples/nuxt-example/package.json @@ -0,0 +1,26 @@ +{ + "name": "nuxt-app-example", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start", + "generate": "nuxt generate" + }, + "dependencies": { + "@azure/app-configuration-provider": "^1.1.3", + "core-js": "^3.25.3", + "nuxt": "^2.15.8", + "vue": "^2.7.10", + "vue-loader": "^17.4.2", + "vue-server-renderer": "^2.7.10", + "vue-template-compiler": "^2.7.10" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/preset-env": "^7.26.0", + "babel-loader": "^9.2.1" + } +} diff --git a/examples/nuxt-example/pages/about.vue b/examples/nuxt-example/pages/about.vue new file mode 100644 index 00000000..20ea6a43 --- /dev/null +++ b/examples/nuxt-example/pages/about.vue @@ -0,0 +1,20 @@ +// pages/about.vue + + + + + diff --git a/examples/nuxt-example/pages/index.vue b/examples/nuxt-example/pages/index.vue new file mode 100644 index 00000000..e0bbb5ae --- /dev/null +++ b/examples/nuxt-example/pages/index.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/examples/nuxt-example/static/favicon.ico b/examples/nuxt-example/static/favicon.ico new file mode 100644 index 00000000..d751f60c Binary files /dev/null and b/examples/nuxt-example/static/favicon.ico differ diff --git a/examples/nuxt-example/store/README.md b/examples/nuxt-example/store/README.md new file mode 100644 index 00000000..1972d277 --- /dev/null +++ b/examples/nuxt-example/store/README.md @@ -0,0 +1,10 @@ +# STORE + +**This directory is not required, you can delete it if you don't want to use it.** + +This directory contains your Vuex Store files. +Vuex Store option is implemented in the Nuxt.js framework. + +Creating a file in this directory automatically activates the option in the framework. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).