feat(app): defineNuxtPlugin + legacy plugin handling (#237)

Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
Daniel Roe 2021-06-18 18:16:51 +01:00 committed by GitHub
parent 36a3d285d8
commit f8435681d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 256 additions and 50 deletions

View File

@ -1,12 +1,10 @@
import head from '#app/plugins/head' import head from '#app/plugins/head'
import legacy from '#app/plugins/legacy'
import preload from '#app/plugins/preload.server' import preload from '#app/plugins/preload.server'
<%= utils.importSources(app.plugins.map(p => p.src)) %> <%= utils.importSources(app.plugins.map(p => p.src)) %>
const commonPlugins = [ const commonPlugins = [
head, head,
legacy,
<%= app.plugins.filter(p => !p.mode || p.mode === 'all').map(p => utils.importName(p.src)).join(',\n ') %> <%= app.plugins.filter(p => !p.mode || p.mode === 'all').map(p => utils.importName(p.src)).join(',\n ') %>
] ]

View File

@ -1,12 +1,14 @@
import { createSSRApp, createApp, nextTick } from 'vue' import { createSSRApp, createApp, nextTick } from 'vue'
import { createNuxt, applyPlugins } from '@nuxt/app' import { createNuxt, applyPlugins, normalizePlugins } from '@nuxt/app'
// @ts-ignore // @ts-ignore
import plugins from '#build/plugins' import _plugins from '#build/plugins'
// @ts-ignore // @ts-ignore
import App from '#build/app' import App from '#build/app'
let entry: Function let entry: Function
const plugins = normalizePlugins(_plugins)
if (process.server) { if (process.server) {
entry = async function createNuxtAppServer (ssrContext = {}) { entry = async function createNuxtAppServer (ssrContext = {}) {
const app = createApp(App) const app = createApp(App)

197
packages/app/src/legacy.ts Normal file
View File

@ -0,0 +1,197 @@
import type { IncomingMessage, ServerResponse } from 'http'
import type { App } from 'vue'
import type { Component } from '@vue/runtime-core'
import mockContext from 'unenv/runtime/mock/proxy'
import type { Nuxt } from './nuxt'
type Route = any
type Store = any
export type LegacyApp = App<Element> & {
$root: LegacyApp
constructor: LegacyApp
}
export interface LegacyContext {
// -> $config
$config: Record<string, any>
env: Record<string, any>
// -> app
app: Component
// -> unsupported
isClient: boolean
isServer: boolean
isStatic: boolean
// TODO: needs app implementation
isDev: boolean
isHMR: boolean
// -> unsupported
store: Store
// vue-router integration
route: Route
params: Route['params']
query: Route['query']
base: string /** TODO: */
payload: any /** TODO: */
from: Route /** TODO: */
// -> nuxt.payload.data
nuxtState: Record<string, any>
// TODO: needs app implementation
beforeNuxtRender (fn: (params: { Components: any, nuxtState: Record<string, any> }) => void): void
beforeSerialize (fn: (nuxtState: Record<string, any>) => void): void
// TODO: needs app implementation
enablePreview?: (previewData?: Record<string, any>) => void
$preview?: Record<string, any>
// -> ssrContext.{req,res}
req: IncomingMessage
res: ServerResponse
/** TODO: */
next?: (err?: any) => any
error (params: any): void
redirect (status: number, path: string, query?: Route['query']): void
redirect (path: string, query?: Route['query']): void
redirect (location: Location): void
redirect (status: number, location: Location): void
ssrContext?: {
// -> ssrContext.{req,res,url,runtimeConfig}
req: LegacyContext['req']
res: LegacyContext['res']
url: string
runtimeConfig: {
public: Record<string, any>
private: Record<string, any>
}
// -> unsupported
target: string
spa?: boolean
modern: boolean
fetchCounters: Record<string, number>
// TODO:
next: LegacyContext['next']
redirected: boolean
beforeRenderFns: Array<() => any>
beforeSerializeFns: Array<() => any>
// -> nuxt.payload
nuxt: {
serverRendered: boolean
// TODO:
layout: string
data: Array<Record<string, any>>
fetch: Array<Record<string, any>>
error: any
state: Array<Record<string, any>>
routePath: string
config: Record<string, any>
}
}
}
function mock (warning: string) {
console.warn(warning)
return mockContext
}
const unsupported = new Set<keyof LegacyContext | keyof LegacyContext['ssrContext']>([
'isClient',
'isServer',
'isStatic',
'store',
'target',
'spa',
'env',
'modern',
'fetchCounters'
])
const todo = new Set<keyof LegacyContext | keyof LegacyContext['ssrContext']>([
'isDev',
'isHMR',
// Routing handlers - needs implementation or deprecation
'base',
'payload',
'from',
'next',
'error',
'redirect',
'redirected',
// needs app implementation
'enablePreview',
'$preview',
'beforeNuxtRender',
'beforeSerialize'
])
const routerKeys: Array<keyof LegacyContext | keyof LegacyContext['ssrContext']> = ['route', 'params', 'query']
export const legacyPlugin = (nuxt: Nuxt) => {
nuxt._legacyContext = new Proxy(nuxt, {
get (nuxt, p: keyof LegacyContext | keyof LegacyContext['ssrContext']) {
// Unsupported keys
if (unsupported.has(p)) {
return mock(`Accessing ${p} is not supported in Nuxt3.`)
}
if (todo.has(p)) {
return mock(`Accessing ${p} is not yet supported in Nuxt3.`)
}
// vue-router implementation
if (routerKeys.includes(p)) {
if (!('$router' in nuxt)) {
return mock('vue-router is not being used in this project.')
}
switch (p) {
case 'route':
return nuxt.$router.currentRoute.value
case 'params':
case 'query':
return nuxt.$router.currentRoute.value[p]
}
}
if (p === '$config') {
// TODO: needs implementation
return mock('Accessing runtime config is not yet supported in Nuxt3.')
}
if (p === 'ssrContext') {
return nuxt._legacyContext
}
if (nuxt.ssrContext && p in nuxt.ssrContext) {
return nuxt.ssrContext[p]
}
if (p === 'nuxt') {
return nuxt.payload
}
if (p === 'nuxtState') {
return nuxt.payload.data
}
if (p in nuxt.app) {
return nuxt.app[p]
}
if (p in nuxt) {
return nuxt[p]
}
return mock(`Accessing ${p} is not supported in Nuxt3.`)
}
}) as unknown as LegacyContext
if (process.client) {
nuxt.hook('app:created', () => {
const legacyApp = { ...nuxt.app } as LegacyApp
legacyApp.$root = legacyApp
// @ts-ignore
// TODO: https://github.com/nuxt/framework/issues/244
legacyApp.constructor = legacyApp
window[`$${nuxt.globalName}`] = legacyApp
})
}
}

View File

@ -1,6 +1,7 @@
import { App, getCurrentInstance } from 'vue' import { App, getCurrentInstance } from 'vue'
import Hookable from 'hookable' import Hookable from 'hookable'
import { defineGetter } from './utils' import { defineGetter } from './utils'
import { legacyPlugin, LegacyContext } from './legacy'
export interface Nuxt { export interface Nuxt {
app: App app: App
@ -13,6 +14,7 @@ export interface Nuxt {
[key: string]: any [key: string]: any
_asyncDataPromises?: Record<string, Promise<any>> _asyncDataPromises?: Record<string, Promise<any>>
_legacyContext?: LegacyContext
ssrContext?: Record<string, any> ssrContext?: Record<string, any>
payload: { payload: {
@ -25,8 +27,13 @@ export interface Nuxt {
provide: (name: string, value: any) => void provide: (name: string, value: any) => void
} }
export const NuxtPluginIndicator = '__nuxt_plugin'
export interface Plugin { export interface Plugin {
(nuxt: Nuxt, provide?: Nuxt['provide']): Promise<void> | void (nuxt: Nuxt): Promise<void> | void
[NuxtPluginIndicator]?: true
}
export interface LegacyPlugin {
(context: LegacyContext, provide: Nuxt['provide']): Promise<void> | void
} }
export interface CreateOptions { export interface CreateOptions {
@ -66,7 +73,7 @@ export function createNuxt (options: CreateOptions) {
if (process.server) { if (process.server) {
nuxt.payload = { nuxt.payload = {
serverRendered: true // TODO: legacy serverRendered: true
} }
nuxt.ssrContext = nuxt.ssrContext || {} nuxt.ssrContext = nuxt.ssrContext || {}
@ -84,7 +91,7 @@ export function createNuxt (options: CreateOptions) {
export function applyPlugin (nuxt: Nuxt, plugin: Plugin) { export function applyPlugin (nuxt: Nuxt, plugin: Plugin) {
if (typeof plugin !== 'function') { return } if (typeof plugin !== 'function') { return }
return callWithNuxt(nuxt, () => plugin(nuxt, nuxt.provide)) return callWithNuxt(nuxt, () => plugin(nuxt))
} }
export async function applyPlugins (nuxt: Nuxt, plugins: Plugin[]) { export async function applyPlugins (nuxt: Nuxt, plugins: Plugin[]) {
@ -93,6 +100,33 @@ export async function applyPlugins (nuxt: Nuxt, plugins: Plugin[]) {
} }
} }
export function normalizePlugins (_plugins: Array<Plugin | LegacyPlugin>) {
let needsLegacyContext = false
const plugins = _plugins.map((plugin) => {
if (isLegacyPlugin(plugin)) {
needsLegacyContext = true
return (nuxt: Nuxt) => plugin(nuxt._legacyContext!, nuxt.provide)
}
return plugin
})
if (needsLegacyContext) {
plugins.unshift(legacyPlugin)
}
return plugins as Plugin[]
}
export function defineNuxtPlugin (plugin: Plugin) {
plugin[NuxtPluginIndicator] = true
return plugin
}
export function isLegacyPlugin (plugin: unknown): plugin is LegacyPlugin {
return !plugin[NuxtPluginIndicator]
}
let currentNuxtInstance: Nuxt | null let currentNuxtInstance: Nuxt | null
export const setNuxtInstance = (nuxt: Nuxt | null) => { export const setNuxtInstance = (nuxt: Nuxt | null) => {

View File

@ -1,8 +1,8 @@
import { createHead, renderHeadToString } from '@vueuse/head' import { createHead, renderHeadToString } from '@vueuse/head'
import type { Plugin } from '@nuxt/app' import { defineNuxtPlugin } from '@nuxt/app'
import { Head, Html, Body, Title, Meta, Link, Script, Style } from './head' import { Head, Html, Body, Title, Meta, Link, Script, Style } from './head'
export default <Plugin> function head (nuxt) { export default defineNuxtPlugin((nuxt) => {
const { app, ssrContext } = nuxt const { app, ssrContext } = nuxt
const head = createHead() const head = createHead()
@ -20,4 +20,4 @@ export default <Plugin> function head (nuxt) {
if (process.server) { if (process.server) {
ssrContext.head = () => renderHeadToString(head) ssrContext.head = () => renderHeadToString(head)
} }
} })

View File

@ -1,25 +0,0 @@
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

@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import type { Plugin } from '@nuxt/app' import { defineNuxtPlugin } from '@nuxt/app'
export default <Plugin> function logs ({ app }) { export default defineNuxtPlugin(({ app }) => {
// Only activate in development // Only activate in development
const logs = app.$nuxt.payload.logs || [] const logs = app.$nuxt.payload.logs || []
if (logs.length > 0) { if (logs.length > 0) {
@ -11,4 +11,4 @@ export default <Plugin> function logs ({ app }) {
delete app.$nuxt.payload.logs delete app.$nuxt.payload.logs
console.groupEnd && console.groupEnd() console.groupEnd && console.groupEnd()
} }
} })

View File

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

View File

@ -1,6 +1,6 @@
import type { Plugin } from '@nuxt/app' import { defineNuxtPlugin } from '@nuxt/app'
export default <Plugin> function progressbar ({ app }) { export default defineNuxtPlugin(({ app }) => {
const { $nuxt } = app const { $nuxt } = app
$nuxt.hook('app:mounted', () => { $nuxt.hook('app:mounted', () => {
const el = document.createElement('div') const el = document.createElement('div')
@ -41,4 +41,4 @@ export default <Plugin> function progressbar ({ app }) {
}, 500) }, 500)
}) })
}) })
} })

View File

@ -1,8 +1,8 @@
import { createVuex, defineStore, useStore } from 'vuex5/dist/vuex.esm' import { createVuex, defineStore, useStore } from 'vuex5/dist/vuex.esm'
import type { Plugin } from '@nuxt/app'
import { useHydration } from '../composables' import { useHydration } from '../composables'
import { defineNuxtPlugin } from '../nuxt'
export default <Plugin> function ({ app }) { export default defineNuxtPlugin(({ app }) => {
const vuex = createVuex({ }) const vuex = createVuex({ })
app.use(vuex) app.use(vuex)
@ -15,7 +15,7 @@ export default <Plugin> function ({ app }) {
// vuex.replaceStateTree(state) // vuex.replaceStateTree(state)
} }
) )
} })
export function createStore (arg1: any, arg2?: any) { export function createStore (arg1: any, arg2?: any) {
const store = defineStore(arg1, arg2) const store = defineStore(arg1, arg2)

View File

@ -6,12 +6,12 @@ import {
RouterLink RouterLink
} from 'vue-router' } from 'vue-router'
// @ts-ignore // @ts-ignore
import type { Plugin } from '@nuxt/app' import { defineNuxtPlugin } from '@nuxt/app'
import NuxtPage from './page.vue' import NuxtPage from './page.vue'
// @ts-ignore // @ts-ignore
import routes from '#build/routes' import routes from '#build/routes'
export default <Plugin> function router (nuxt) { export default defineNuxtPlugin((nuxt) => {
const { app } = nuxt const { app } = nuxt
app.component('NuxtPage', NuxtPage) app.component('NuxtPage', NuxtPage)
@ -50,4 +50,4 @@ export default <Plugin> function router (nuxt) {
// TODO // TODO
} }
}) })
} })