refactor(bridge): provide vue2 compat with a transform plugin (#3886)

This commit is contained in:
Daniel Roe 2022-03-25 12:18:43 +00:00 committed by GitHub
parent e34ed887f2
commit 9e67d58005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 61 deletions

View File

@ -1,9 +1,10 @@
import { useNuxt, resolveModule, addTemplate, resolveAlias, extendWebpackConfig } from '@nuxt/kit' import { useNuxt, addTemplate, resolveAlias, addWebpackPlugin, addVitePlugin } from '@nuxt/kit'
import { NuxtModule } from '@nuxt/schema' import { NuxtModule } from '@nuxt/schema'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { componentsTypeTemplate } from '../../nuxt3/src/components/templates' import { componentsTypeTemplate } from '../../nuxt3/src/components/templates'
import { schemaTemplate } from '../../nuxt3/src/core/templates' import { schemaTemplate } from '../../nuxt3/src/core/templates'
import { distDir } from './dirs' import { distDir } from './dirs'
import { VueCompat } from './vue-compat'
export function setupAppBridge (_options: any) { export function setupAppBridge (_options: any) {
const nuxt = useNuxt() const nuxt = useNuxt()
@ -21,24 +22,13 @@ export function setupAppBridge (_options: any) {
}) })
} }
// Resolve vue2 builds // Transpile core vue libraries
const vue2ESM = resolveModule('vue/dist/vue.runtime.esm.js', { paths: nuxt.options.modulesDir }) // TODO: resolve in vercel/nft
const vue2CJS = resolveModule('vue/dist/vue.runtime.common.js', { paths: nuxt.options.modulesDir }) nuxt.options.build.transpile.push('vuex')
nuxt.options.alias.vue2 = vue2ESM
nuxt.options.build.transpile.push('vue')
// Transpile libs with modern syntax // Transpile libs with modern syntax
nuxt.options.build.transpile.push('h3') nuxt.options.build.transpile.push('h3')
extendWebpackConfig((config) => {
(config.resolve.alias as any).vue2 = vue2CJS
}, { client: false })
nuxt.hook('vite:extendConfig', (config, { isServer }) => {
if (isServer && !nuxt.options.dev) {
(config.resolve.alias as any).vue2 = vue2CJS
}
})
// Disable legacy fetch polyfills // Disable legacy fetch polyfills
nuxt.options.fetch.server = false nuxt.options.fetch.server = false
nuxt.options.fetch.client = false nuxt.options.fetch.client = false
@ -70,43 +60,18 @@ export function setupAppBridge (_options: any) {
}) })
// Alias vue to have identical vue3 exports // Alias vue to have identical vue3 exports
nuxt.options.alias['vue2-bridge'] = resolve(distDir, 'runtime/vue2-bridge.mjs') addWebpackPlugin(VueCompat.webpack({
for (const alias of [ src: resolve(distDir, 'runtime/vue2-bridge.mjs')
// vue }))
'vue', addVitePlugin(VueCompat.vite({
// vue 3 helper packages src: resolve(distDir, 'runtime/vue2-bridge.mjs')
'@vue/shared', }))
'@vue/reactivity',
'@vue/runtime-core',
'@vue/runtime-dom',
// vue-demi
'vue-demi',
...[
// vue 2 dist files
'vue/dist/vue.common.dev',
'vue/dist/vue.common',
'vue/dist/vue.common.prod',
'vue/dist/vue.esm.browser',
'vue/dist/vue.esm.browser.min',
'vue/dist/vue.esm',
'vue/dist/vue',
'vue/dist/vue.min',
'vue/dist/vue.runtime.common.dev',
'vue/dist/vue.runtime.common',
'vue/dist/vue.runtime.common.prod',
'vue/dist/vue.runtime.esm',
'vue/dist/vue.runtime',
'vue/dist/vue.runtime.min'
].flatMap(m => [m, `${m}.js`])
]) {
nuxt.options.alias[alias] = nuxt.options.alias['vue2-bridge']
}
// Ensure TS still recognises vue imports nuxt.hook('prepare:types', ({ tsConfig, references }) => {
nuxt.hook('prepare:types', ({ tsConfig }) => { // Type 'vue' module with composition API exports
tsConfig.compilerOptions.paths.vue2 = ['vue'] references.push({ path: resolve(distDir, 'runtime/vue2-bridge.d.ts') })
delete tsConfig.compilerOptions.paths.vue
// Enable Volar support with vue 2 compat mode
// @ts-ignore // @ts-ignore
tsConfig.vueCompilerOptions = { tsConfig.vueCompilerOptions = {
experimentalCompatMode: 2 experimentalCompatMode: 2

View File

@ -1,4 +1,4 @@
import Vue from 'vue' // eslint-disable-line import/default import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api' import VueCompositionAPI from '@vue/composition-api'
import { defineNuxtPlugin } from '#app' import { defineNuxtPlugin } from '#app'

View File

@ -1,7 +1,60 @@
import Vue from 'vue' import * as VueCapi from '@vue/composition-api'
export * from '@vue/composition-api' declare module 'vue' {
export const EffectScope: typeof VueCapi['EffectScope']
export const computed: typeof VueCapi['computed']
export const createApp: typeof VueCapi['createApp']
export const createRef: typeof VueCapi['createRef']
export const customRef: typeof VueCapi['customRef']
export const defineAsyncComponent: typeof VueCapi['defineAsyncComponent']
export const defineComponent: typeof VueCapi['defineComponent']
export const del: typeof VueCapi['del']
export const effectScope: typeof VueCapi['effectScope']
export const getCurrentInstance: typeof VueCapi['getCurrentInstance']
export const getCurrentScope: typeof VueCapi['getCurrentScope']
export const h: typeof VueCapi['h']
export const inject: typeof VueCapi['inject']
export const isRaw: typeof VueCapi['isRaw']
export const isReactive: typeof VueCapi['isReactive']
export const isReadonly: typeof VueCapi['isReadonly']
export const isRef: typeof VueCapi['isRef']
export const markRaw: typeof VueCapi['markRaw']
export const nextTick: typeof VueCapi['nextTick']
export const onActivated: typeof VueCapi['onActivated']
export const onBeforeMount: typeof VueCapi['onBeforeMount']
export const onBeforeUnmount: typeof VueCapi['onBeforeUnmount']
export const onBeforeUpdate: typeof VueCapi['onBeforeUpdate']
export const onDeactivated: typeof VueCapi['onDeactivated']
export const onErrorCaptured: typeof VueCapi['onErrorCaptured']
export const onMounted: typeof VueCapi['onMounted']
export const onScopeDispose: typeof VueCapi['onScopeDispose']
export const onServerPrefetch: typeof VueCapi['onServerPrefetch']
export const onUnmounted: typeof VueCapi['onUnmounted']
export const onUpdated: typeof VueCapi['onUpdated']
export const provide: typeof VueCapi['provide']
export const proxyRefs: typeof VueCapi['proxyRefs']
export const reactive: typeof VueCapi['reactive']
export const readonly: typeof VueCapi['readonly']
export const ref: typeof VueCapi['ref']
export const set: typeof VueCapi['set']
export const shallowReactive: typeof VueCapi['shallowReactive']
export const shallowReadonly: typeof VueCapi['shallowReadonly']
export const shallowRef: typeof VueCapi['shallowRef']
export const toRaw: typeof VueCapi['toRaw']
export const toRef: typeof VueCapi['toRef']
export const toRefs: typeof VueCapi['toRefs']
export const triggerRef: typeof VueCapi['triggerRef']
export const unref: typeof VueCapi['unref']
export const useAttrs: typeof VueCapi['useAttrs']
export const useCSSModule: typeof VueCapi['useCSSModule']
export const useCssModule: typeof VueCapi['useCssModule']
export const useSlots: typeof VueCapi['useSlots']
export const warn: typeof VueCapi['warn']
export const watch: typeof VueCapi['watch']
export const watchEffect: typeof VueCapi['watchEffect']
export const watchPostEffect: typeof VueCapi['watchPostEffect']
export const watchSyncEffect: typeof VueCapi['watchSyncEffect']
export const isFunction: (fn: unknown) => boolean
}
export declare const isFunction: (fn: unknown) => boolean export {}
export { Vue as default }

View File

@ -1,4 +1,4 @@
import Vue from 'vue2' import Vue from 'vue'
export { EffectScope, computed, createApp, createRef, customRef, defineAsyncComponent, defineComponent, del, effectScope, getCurrentInstance, getCurrentScope, h, inject, isRaw, isReactive, isReadonly, isRef, markRaw, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onScopeDispose, onServerPrefetch, onUnmounted, onUpdated, provide, proxyRefs, reactive, readonly, ref, set, shallowReactive, shallowReadonly, shallowRef, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useCSSModule, useCssModule, useSlots, warn, watch, watchEffect, watchPostEffect, watchSyncEffect } from '@vue/composition-api' export { EffectScope, computed, createApp, createRef, customRef, defineAsyncComponent, defineComponent, del, effectScope, getCurrentInstance, getCurrentScope, h, inject, isRaw, isReactive, isReadonly, isRef, markRaw, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onScopeDispose, onServerPrefetch, onUnmounted, onUpdated, provide, proxyRefs, reactive, readonly, ref, set, shallowReactive, shallowReadonly, shallowRef, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useCSSModule, useCssModule, useSlots, warn, watch, watchEffect, watchPostEffect, watchSyncEffect } from '@vue/composition-api'

View File

@ -58,9 +58,7 @@ async function bundle (nuxt: Nuxt, builder: any) {
'ufo', 'ufo',
'date-fns', 'date-fns',
'nanoid', 'nanoid',
'vue', 'vue'
'vue2',
'vue2-bridge'
// TODO(Anthony): waiting for Vite's fix https://github.com/vitejs/vite/issues/5688 // TODO(Anthony): waiting for Vite's fix https://github.com/vitejs/vite/issues/5688
// ...nuxt.options.build.transpile.filter(i => typeof i === 'string'), // ...nuxt.options.build.transpile.filter(i => typeof i === 'string'),
// 'vue-demi' // 'vue-demi'

View File

@ -0,0 +1,74 @@
import { pathToFileURL } from 'url'
import MagicString from 'magic-string'
import { findStaticImports } from 'mlly'
import { parseQuery, parseURL } from 'ufo'
import { createUnplugin } from 'unplugin'
export const VueCompat = createUnplugin((opts: { src?: string }) => {
return {
name: 'nuxt-legacy-vue-transform',
enforce: 'post',
transformInclude (id) {
if (id.includes('vue2-bridge')) { return false }
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
const query = parseQuery(search)
// vue files
if (pathname.endsWith('.vue') && (query.type === 'script' || !search)) {
return true
}
// js files
if (pathname.match(/\.((c|m)?j|t)sx?/g)) {
return true
}
},
transform (code, id) {
if (id.includes('vue2-bridge')) { return }
const s = new MagicString(code)
const imports = findStaticImports(code).filter(i => i.type === 'static' && vueAliases.includes(i.specifier))
for (const i of imports) {
s.overwrite(i.start, i.end, i.code.replace(`"${i.specifier}"`, `"${opts.src}"`).replace(`'${i.specifier}'`, `'${opts.src}'`))
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: s.generateMap({ source: id, includeContent: true })
}
}
}
}
})
const vueAliases = [
// vue
'vue',
// vue 3 helper packages
'@vue/shared',
'@vue/reactivity',
'@vue/runtime-core',
'@vue/runtime-dom',
// vue-demi
'vue-demi',
...[
// vue 2 dist files
'vue/dist/vue.common.dev',
'vue/dist/vue.common',
'vue/dist/vue.common.prod',
'vue/dist/vue.esm.browser',
'vue/dist/vue.esm.browser.min',
'vue/dist/vue.esm',
'vue/dist/vue',
'vue/dist/vue.min',
'vue/dist/vue.runtime.common.dev',
'vue/dist/vue.runtime.common',
'vue/dist/vue.runtime.common.prod',
'vue/dist/vue.runtime.esm',
'vue/dist/vue.runtime',
'vue/dist/vue.runtime.min'
].flatMap(m => [m, `${m}.js`])
]

View File

@ -168,7 +168,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'process.env.RUNTIME_CONFIG': devalue(nitroContext._nuxt.runtimeConfig), 'process.env.RUNTIME_CONFIG': devalue(nitroContext._nuxt.runtimeConfig),
'process.env.DEBUG': JSON.stringify(nitroContext._nuxt.dev), 'process.env.DEBUG': JSON.stringify(nitroContext._nuxt.dev),
// Needed for vue 2 server build // Needed for vue 2 server build
'commonjsGlobal.process.env.VUE_ENV': '"server"' 'commonjsGlobal.process.env.VUE_ENV': '"server"',
'global["process"].env.VUE_ENV': '"server"'
} }
})) }))

View File

@ -12,6 +12,9 @@ describe('fixtures:bridge', async () => {
it('render hello world', async () => { it('render hello world', async () => {
expect(await $fetch('/')).to.contain('Hello Vue 2!') expect(await $fetch('/')).to.contain('Hello Vue 2!')
}) })
it('uses server Vue build', async () => {
expect(await $fetch('/')).to.contain('Rendered on server: true')
})
}) })
describe('navigate', () => { describe('navigate', () => {

View File

@ -6,6 +6,7 @@
Update Update
</button> </button>
</div> </div>
Rendered on server: {{ serverBuild }}
</div> </div>
</template> </template>
@ -15,4 +16,5 @@ const version = ref('2')
const state = useState('test-state') const state = useState('test-state')
state.value = '123' state.value = '123'
const updateState = () => { state.value = '456' } const updateState = () => { state.value = '456' }
const serverBuild = useState('server-build', () => getCurrentInstance().proxy.$isServer)
</script> </script>