refactor: move app to src with nuxt/app import

this refactor allows distributing app with esm modules instead of ts
This commit is contained in:
Pooya Parsa 2021-01-18 13:46:19 +01:00
parent a6f9fb4c7a
commit 454b8c332c
36 changed files with 682 additions and 5 deletions

View File

@ -0,0 +1,27 @@
import { createSSRApp, nextTick } from 'vue'
import { createNuxt, applyPlugins } from 'nuxt/app/nuxt'
import plugins from './plugins'
import clientPlugins from './plugins.client'
import App from '<%= app.main %>'
async function initApp () {
const app = createSSRApp(App)
const nuxt = createNuxt({ app })
await applyPlugins(nuxt, plugins)
await applyPlugins(nuxt, clientPlugins)
await app.$nuxt.hooks.callHook('app:created', app)
await app.$nuxt.hooks.callHook('app:beforeMount', app)
app.mount('#<%= globals.id %>')
await app.$nuxt.hooks.callHook('app:mounted', app)
await nextTick()
nuxt.isHydrating = false
}
initApp().catch((error) => {
console.error('Error while mounting app:', error) // eslint-disable-line no-console
})

View File

@ -0,0 +1,23 @@
import { createApp } from 'vue'
import { createNuxt, applyPlugins } from 'nuxt/app/nuxt'
import plugins from './plugins'
import serverPlugins from './plugins.server'
import App from '<%= app.main %>'
export default async function createNuxtAppServer (ssrContext = {}) {
const app = createApp(App)
const nuxt = createNuxt({ app, ssrContext })
await applyPlugins(nuxt, plugins)
await applyPlugins(nuxt, serverPlugins)
await app.$nuxt.hooks.callHook('app:created', app)
nuxt.hooks.hook('vue-renderer:done',
() => nuxt.hooks.callHook('app:rendered', app)
)
return app
}

View File

@ -0,0 +1,3 @@
<template>
<Nuxt />
</template>

View File

@ -0,0 +1,12 @@
import logs from 'nuxt/app/plugins/logs.client.dev'
import progress from 'nuxt/app/plugins/progress.client'
const plugins = [
progress
]
if (process.dev) {
plugins.push(logs)
}
export default plugins

View File

@ -0,0 +1,5 @@
import preload from 'nuxt/app/plugins/preload.server'
export default [
preload
]

View File

@ -0,0 +1,9 @@
import router from 'nuxt/app/plugins/router'
import vuex from 'nuxt/app/plugins/vuex'
import legacy from 'nuxt/app/plugins/legacy'
export default [
router,
vuex,
legacy
]

View File

@ -0,0 +1,2 @@
// TODO: Use webpack-virtual-modules
export default <%= app.templates.routes %>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
<div id="__nuxt">{{ APP }}</div>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Server error</title>
<meta charset="utf-8">
<meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" name=viewport>
<style>
.__nuxt-error-page{padding: 1rem;background:#f7f8fb;color:#47494e;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-family:sans-serif;font-weight:100!important;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;position:absolute;top:0;left:0;right:0;bottom:0}.__nuxt-error-page .error{max-width:450px}.__nuxt-error-page .title{font-size:24px;font-size:1.5rem;margin-top:15px;color:#47494e;margin-bottom:8px}.__nuxt-error-page .description{color:#7f828b;line-height:21px;margin-bottom:10px}.__nuxt-error-page a{color:#7f828b!important;text-decoration:none}.__nuxt-error-page .logo{position:fixed;left:12px;bottom:12px}
</style>
</head>
<body>
<div class="__nuxt-error-page">
<div class="error">
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="#DBE1EC" viewBox="0 0 48 48"><path d="M22 30h4v4h-4zm0-16h4v12h-4zm1.99-10C12.94 4 4 12.95 4 24s8.94 20 19.99 20S44 35.05 44 24 35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16z"/></svg>
<div class="title">Server error</div>
<div class="description">{{ message }}</div>
</div>
<div class="logo">
<a href="https://nuxtjs.org" target="_blank" rel="noopener">Nuxt.js</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@ -0,0 +1,6 @@
<template>
<div class="welcome">
<h1>Welcome to Nuxt 3</h1>
<p>You can start adding a <code>pages/index.vue</code> or <code>app.vue</code></p>
</div>
</template>

View File

@ -0,0 +1,38 @@
import { getCurrentInstance, reactive, isReactive } from 'vue'
import { useNuxt } from 'nuxt/app'
/**
* Returns a unique string suitable for syncing data between server and client.
* @param nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current instance
*/
export function useSSRRef (nuxt = useNuxt(), vm = getCurrentInstance()): string {
if (process.server) {
if (!vm.attrs['data-ssr-ref']) {
nuxt._refCtr = nuxt._refCtr || 1
vm.attrs['data-ssr-ref'] = String(nuxt._refCtr++)
}
return vm.attrs['data-ssr-ref'] as string
}
if (process.client) {
return vm.vnode.el?.dataset?.ssrRef || String(Math.random()) /* TODO: unique value for multiple calls */
}
}
/**
* Allows accessing reactive data that can be synced between server and client.
* @param nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current instance
*/
export function useData (nuxt = useNuxt(), vm = getCurrentInstance()): ReturnType<typeof reactive> {
const ssrRef = useSSRRef(nuxt, vm)
nuxt.payload.data = nuxt.payload.data || {}
if (!isReactive(nuxt.payload.data[ssrRef])) {
nuxt.payload.data[ssrRef] = reactive(nuxt.payload.data[ssrRef] || {})
}
return nuxt.payload.data[ssrRef]
}

View File

@ -0,0 +1,105 @@
import { Ref, toRef, onMounted, watch, getCurrentInstance, onUnmounted } from 'vue'
import { Nuxt, useNuxt } from 'nuxt/app'
import { httpFetch } from '../utils/fetch'
import { useData } from './data'
export type HTTPRequest = string | { method: string, url: string }
export type FetchRequest<T> = HTTPRequest | ((ctx: Nuxt) => HTTPRequest | Promise<T>)
export interface FetchOptions {
server?: boolean
defer?: boolean
fetcher?: Function
key?: string
}
export interface FetchObj<T> {
data: Ref<T>
fetch: Function
error?: any
}
export function useFetch (defaults?: FetchOptions) {
const nuxt = useNuxt()
const vm = getCurrentInstance()
let data = useData(nuxt, vm)
let dataRef = 1
const onMountedCbs = []
if (process.client) {
onMounted(() => {
onMountedCbs.forEach((cb) => { cb() })
onMountedCbs.splice(0, onMountedCbs.length)
})
onUnmounted(() => {
onMountedCbs.splice(0, onMountedCbs.length)
data = null
})
}
return async function fetch<T = any> (request: FetchRequest<T>, options?: FetchOptions): Promise<FetchObj<T>> {
options = {
server: true,
defer: false,
fetcher: httpFetch,
...defaults,
...options
}
const key = String(dataRef++)
const fetch = async () => {
const _request = typeof request === 'function'
? request(nuxt)
: request
if (_request instanceof Promise) {
// Let user resolve if request is promise
data[key] = await _request
} else if (_request && (typeof _request === 'string' || _request.url)) {
// Make HTTP request when request is string (url) or { url, ...opts }
data[key] = await options.fetcher(_request)
} else {
// Invalid request
throw new Error('Invalid fetch request: ' + _request)
}
}
const clientOnly = options.server === false
// Client side
if (process.client) {
// 1. Hydration (server: true): no fetch
// 2. Initial load (server: false): fetch on mounted
if (nuxt.isHydrating && !options.server) {
// Fetch on mounted (initial load or deferred fetch)
onMountedCbs.push(fetch)
} else if (!nuxt.isHydrating) {
if (options.defer) {
// 3. Navigation (defer: true): fetch on mounted
onMountedCbs.push(fetch)
} else {
// 4. Navigation (defer: false): await fetch
await fetch()
}
}
// Watch request
if (typeof request === 'function') {
watch(request, fetch)
}
}
// Server side
if (process.server && !clientOnly) {
await fetch()
}
return {
data: toRef<any, string>(data, key),
fetch
}
}
}

View File

@ -0,0 +1,23 @@
import { useNuxt } from 'nuxt/app'
/**
* Allows full control of the hydration cycle to set and receive data from the server.
* @param key a unique key to identify the data in the Nuxt payload
* @param get a function that returns the value to set the initial data
* @param set a function that will receive the data on the client-side
*/
export const useHydration = <T>(key: string, get: () => T, set: (value: T) => void) => {
const nuxt = useNuxt()
if (process.server) {
nuxt.hooks.hook('app:rendered', () => {
nuxt.payload[key] = get()
})
}
if (process.client) {
nuxt.hooks.hook('app:created', () => {
set(nuxt.payload[key])
})
}
}

View File

@ -0,0 +1,3 @@
export { useFetch } from './fetch'
export { useData } from './data'
export { useHydration } from './hydrate'

View File

@ -0,0 +1,10 @@
declare module NodeJS {
interface Process {
browser: boolean
client: boolean
mode: 'spa' | 'universal'
modern: boolean
server: boolean
static: boolean
}
}

View File

@ -0,0 +1,6 @@
declare module '~build/routes' {
import { RouteRecordRaw } from 'vue-router'
const _default: RouteRecordRaw[]
export default _default
}

View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import { Component } from 'vue'
const component: Component
export default component
}

View File

@ -0,0 +1,7 @@
import { Nuxt } from 'nuxt/app'
declare module 'vue' {
interface App {
$nuxt: Nuxt
}
}

View File

@ -0,0 +1,3 @@
interface Window {
__NUXT__?: Record<string, any>
}

View File

@ -0,0 +1,2 @@
export { useNuxt } from './nuxt/composables'
export * from './nuxt'

View File

@ -0,0 +1,37 @@
import { getCurrentInstance } from 'vue'
import type { Nuxt } from 'nuxt/app'
let currentNuxtInstance: Nuxt
export const setNuxtInstance = (nuxt: Nuxt) => {
currentNuxtInstance = nuxt
}
/**
* Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`.
* @param nuxt A Nuxt instance
* @param setup The function to call
*/
export async function callWithNuxt (nuxt: Nuxt, setup: () => any) {
setNuxtInstance(nuxt)
const p = setup()
setNuxtInstance(undefined)
await p
}
/**
* Returns the current Nuxt instance.
*/
export function useNuxt () {
const vm = getCurrentInstance()
if (!vm && !currentNuxtInstance) {
throw new Error('nuxt instance unavailable')
}
if (!vm) {
return currentNuxtInstance
}
return vm.appContext.app.$nuxt
}

View File

@ -0,0 +1,89 @@
import Hookable from 'hookable'
import type { App } from 'vue'
import { defineGetter } from '../utils'
import { callWithNuxt } from './composables'
export interface Nuxt {
app: App
globalName: string
hooks: Hookable
hook: Hookable['hook']
callHook: Hookable['callHook']
[key: string]: any
ssrContext?: Record<string, any>
payload: {
serverRendered?: true,
data?: object
rendered?: Function
[key: string]: any
}
provide: (name: string, value: any) => void
}
export interface Plugin {
(nuxt: Nuxt, provide?: Nuxt['provide']): Promise<void> | void
}
export interface CreateOptions {
app: Nuxt['app']
ssrContext?: Nuxt['ssrContext']
globalName?: Nuxt['globalName']
}
export function createNuxt (options: CreateOptions) {
const nuxt: Nuxt = {
app: undefined,
provide: undefined,
globalName: 'nuxt',
state: {},
payload: {},
isHydrating: process.client,
...options
} as any as Nuxt
nuxt.hooks = new Hookable()
nuxt.hook = nuxt.hooks.hook
nuxt.callHook = nuxt.hooks.callHook
nuxt.provide = (name: string, value: any) => {
const $name = '$' + name
defineGetter(nuxt.app, $name, value)
defineGetter(nuxt.app.config.globalProperties, $name, value)
}
nuxt.provide('nuxt', nuxt)
// Expose nuxt to the renderContext
if (nuxt.ssrContext) {
nuxt.ssrContext.nuxt = nuxt
}
if (process.server) {
nuxt.payload = {
serverRendered: true // TODO: legacy
}
// Expose to server renderer to create window.__NUXT__
nuxt.ssrContext.payload = nuxt.payload
}
if (process.client) {
nuxt.payload = window.__NUXT__ || {}
}
return nuxt
}
export function applyPlugin (nuxt: Nuxt, plugin: Plugin) {
return callWithNuxt(nuxt, () => plugin(nuxt))
}
export async function applyPlugins (nuxt: Nuxt, plugins: Plugin[]) {
for (const plugin of plugins) {
await applyPlugin(nuxt, plugin)
}
}

View File

@ -0,0 +1,25 @@
import type { App } from 'vue'
import type { Plugin } from 'nuxt/app'
export type LegacyApp = App<Element> & {
$root: LegacyApp
}
// TODO: plugins second argument (inject)
// TODO: payload.serverRrendered
export default <Plugin> function legacy ({ app }) {
app.$nuxt.context = {}
if (process.client) {
const legacyApp = { ...app } as LegacyApp
legacyApp.$root = legacyApp
window[app.$nuxt.globalName] = legacyApp
}
if (process.server) {
const { ssrContext } = app.$nuxt
app.$nuxt.context.req = ssrContext.req
app.$nuxt.context.res = ssrContext.res
}
}

View File

@ -0,0 +1,14 @@
/* eslint-disable no-console */
import type { Plugin } from 'nuxt/app'
export default <Plugin> function logs ({ app }) {
// Only activate in development
const logs = app.$nuxt.payload.logs || []
if (logs.length > 0) {
const ssrLogStyle = 'background: #003C3C;border-radius: 0.5em;color: white;font-weight: bold;padding: 2px 0.5em;'
console.groupCollapsed && console.groupCollapsed('%cNuxt Server Logs', ssrLogStyle)
logs.forEach(logObj => (console[logObj.type] || console.log)(...logObj.args))
delete app.$nuxt.payload.logs
console.groupEnd && console.groupEnd()
}
}

View File

@ -0,0 +1,11 @@
import type { Plugin } from 'nuxt/app'
export default <Plugin> function preload ({ app }) {
app.mixin({
beforeCreate () {
const { _registeredComponents } = this.$nuxt.ssrContext
const { __moduleIdentifier } = this.$options
_registeredComponents.add(__moduleIdentifier)
}
})
}

View File

@ -0,0 +1,44 @@
import type { Plugin } from 'nuxt/app'
export default <Plugin> function progressbar ({ app }) {
const { $nuxt } = app
$nuxt.hooks.hook('app:mounted', () => {
const el = document.createElement('div')
el.id = 'nuxt-progress'
document.body.appendChild(el)
el.style.position = 'fixed'
el.style.backgroundColor = 'black'
el.style.height = '2px'
el.style.top = '0px'
el.style.left = '0px'
el.style.transition = 'width 0.1s, opacity 0.4s'
const duration = 3000
const progress = 10000 / Math.floor(duration)
let timeout
let interval
$nuxt.hooks.hook('page:start', () => {
if (timeout) { return }
timeout = setTimeout(() => {
let width = 10
el.style.opacity = '100%'
el.style.width = '10%'
interval = setInterval(() => {
if (width >= 100) { return }
width = Math.floor(width + progress)
el.style.width = `${width}%`
}, 100)
}, 200)
})
$nuxt.hooks.hook('page:finish', () => {
timeout && clearTimeout(timeout)
timeout = null
interval && clearInterval(interval)
interval = null
el.style.width = '100%'
el.style.opacity = '0%'
setTimeout(() => {
el.style.width = '0%'
}, 500)
})
})
}

View File

@ -0,0 +1,13 @@
<template>
<RouterView v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" :key="$route.path" />
</transition>
</RouterView>
</template>
<script>
export default {
name: 'NuxtChild'
}
</script>

View File

@ -0,0 +1,17 @@
<template>
<RouterView v-slot="{ Component }">
<transition name="page" mode="out-in">
<!-- <keep-alive> -->
<Suspense @pending="$nuxt.hooks.callHook('page:start')" @resolve="$nuxt.hooks.callHook('page:finish')">
<component :is="Component" :key="$route.path" />
</Suspense>
<!-- <keep-alive -->
</transition>
</RouterView>
</template>
<script>
export default {
name: 'NuxtPage'
}
</script>

View File

@ -0,0 +1,58 @@
import { shallowRef } from 'vue'
import {
createRouter,
createWebHistory,
createMemoryHistory,
RouterLink
} from 'vue-router'
import type { Plugin } from 'nuxt/app'
import NuxtPage from './NuxtPage.vue'
import NuxtChild from './NuxtChild.vue'
import routes from '~build/routes'
export default <Plugin> function router (nuxt) {
const { app } = nuxt
// TODO: move this outside this plugin
if (!routes.length) {
return
}
app.component('NuxtPage', NuxtPage)
app.component('NuxtChild', NuxtChild)
app.component('NuxtLink', RouterLink)
const routerHistory = process.client
? createWebHistory()
: createMemoryHistory()
const router = createRouter({
history: routerHistory,
routes
})
app.use(router)
const previousRoute = shallowRef(router.currentRoute.value)
router.afterEach((_to, from) => {
previousRoute.value = from
})
Object.defineProperty(app.config.globalProperties, 'previousRoute', {
get: () => previousRoute.value
})
nuxt.hooks.hook('app:created', async () => {
if (process.server) {
router.push(nuxt.ssrContext.url)
}
try {
await router.isReady()
if (!router.currentRoute.value.matched.length) {
// TODO
}
} catch (err) {
// TODO
}
})
}

View File

@ -0,0 +1,24 @@
import { createVuex, defineStore, useStore } from 'vuex5/dist/vuex.esm'
import type { Plugin } from 'nuxt/app'
import { useHydration } from 'nuxt/app/composables'
export default <Plugin> function ({ app }) {
const vuex = createVuex({ })
app.use(vuex)
useHydration('vuex',
() => vuex.registry,
state => () => {
// eslint-disable-next-line no-console
console.log('vuex.replaceStateTree', state)
// vuex.replaceStateTree(state)
}
)
}
export function createStore (arg1, arg2) {
const store = defineStore(arg1, arg2)
return () => useStore(store)
}

View File

@ -0,0 +1,15 @@
// TODO: Move to a nuxt-contrib utility
import destr from 'destr'
// TODO: polyfill by env (nuxt) not by util
const _fetch = process.server ? require('node-fetch') : global.fetch
export async function httpFetch (path: string) {
const res = await _fetch(path)
if (!res.ok) {
throw new Error(res)
}
const data = await res.text()
return destr(data)
}

View File

@ -0,0 +1,3 @@
export function defineGetter<K extends string | number | symbol, V> (obj: Record<K, V>, key: K, val: V) {
Object.defineProperty(obj, key, { get: () => val })
}

View File

@ -1,7 +1,7 @@
import path from 'path'
import type { WatchOptions as ChokidarWatchOptions } from 'chokidar'
import type express from 'express'
import type { configHooksT } from 'hookable/types/types'
import type { configHooksT } from 'hookable'
import { APP_DIR } from 'src/index'
import ignore from 'ignore'
import capitalize from 'lodash/capitalize'
import env from 'std-env'
@ -172,7 +172,7 @@ export default (): CommonConfiguration => ({
modulesDir: [
'node_modules'
],
appDir: path.resolve(__dirname, '../../../app'),
appDir: APP_DIR,
dir: {
assets: 'assets',
app: 'app',

View File

@ -1,3 +1,6 @@
import { resolve } from 'path'
export * from './core'
export const APP_DIR = resolve(__dirname, 'app')
export const getBuilder = () => import('./builder')

View File

@ -114,8 +114,8 @@ function baseAlias (ctx: WebpackConfigContext) {
const { options, isServer } = ctx
ctx.alias = {
app: options.appDir,
'nuxt-build': options.buildDir,
'nuxt/app': options.appDir,
'~build': options.buildDir,
'vue-meta': require.resolve(`vue-meta${isServer ? '' : '/dist/vue-meta.esm.browser.js'}`),
...options.alias,
...ctx.alias