From 9e67d580057e6eafb7fa5817c775e78c062b4d4f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 25 Mar 2022 12:18:43 +0000 Subject: [PATCH] refactor(bridge): provide vue2 compat with a transform plugin (#3886) --- packages/bridge/src/app.ts | 65 ++++------------- packages/bridge/src/runtime/capi.plugin.mjs | 2 +- packages/bridge/src/runtime/vue2-bridge.d.ts | 63 +++++++++++++++-- packages/bridge/src/runtime/vue2-bridge.mjs | 2 +- packages/bridge/src/vite/vite.ts | 4 +- packages/bridge/src/vue-compat.ts | 74 ++++++++++++++++++++ packages/nitro/src/rollup/config.ts | 3 +- test/bridge.test.ts | 3 + test/fixtures/bridge/pages/index.vue | 2 + 9 files changed, 157 insertions(+), 61 deletions(-) create mode 100644 packages/bridge/src/vue-compat.ts diff --git a/packages/bridge/src/app.ts b/packages/bridge/src/app.ts index 48cd11d340..ac997f1c40 100644 --- a/packages/bridge/src/app.ts +++ b/packages/bridge/src/app.ts @@ -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 { resolve } from 'pathe' import { componentsTypeTemplate } from '../../nuxt3/src/components/templates' import { schemaTemplate } from '../../nuxt3/src/core/templates' import { distDir } from './dirs' +import { VueCompat } from './vue-compat' export function setupAppBridge (_options: any) { const nuxt = useNuxt() @@ -21,24 +22,13 @@ export function setupAppBridge (_options: any) { }) } - // Resolve vue2 builds - const vue2ESM = resolveModule('vue/dist/vue.runtime.esm.js', { paths: nuxt.options.modulesDir }) - const vue2CJS = resolveModule('vue/dist/vue.runtime.common.js', { paths: nuxt.options.modulesDir }) - nuxt.options.alias.vue2 = vue2ESM - nuxt.options.build.transpile.push('vue') + // Transpile core vue libraries + // TODO: resolve in vercel/nft + nuxt.options.build.transpile.push('vuex') // Transpile libs with modern syntax 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 nuxt.options.fetch.server = false nuxt.options.fetch.client = false @@ -70,43 +60,18 @@ export function setupAppBridge (_options: any) { }) // Alias vue to have identical vue3 exports - nuxt.options.alias['vue2-bridge'] = resolve(distDir, 'runtime/vue2-bridge.mjs') - for (const alias of [ - // 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`]) - ]) { - nuxt.options.alias[alias] = nuxt.options.alias['vue2-bridge'] - } + addWebpackPlugin(VueCompat.webpack({ + src: resolve(distDir, 'runtime/vue2-bridge.mjs') + })) + addVitePlugin(VueCompat.vite({ + src: resolve(distDir, 'runtime/vue2-bridge.mjs') + })) - // Ensure TS still recognises vue imports - nuxt.hook('prepare:types', ({ tsConfig }) => { - tsConfig.compilerOptions.paths.vue2 = ['vue'] - delete tsConfig.compilerOptions.paths.vue + nuxt.hook('prepare:types', ({ tsConfig, references }) => { + // Type 'vue' module with composition API exports + references.push({ path: resolve(distDir, 'runtime/vue2-bridge.d.ts') }) + // Enable Volar support with vue 2 compat mode // @ts-ignore tsConfig.vueCompilerOptions = { experimentalCompatMode: 2 diff --git a/packages/bridge/src/runtime/capi.plugin.mjs b/packages/bridge/src/runtime/capi.plugin.mjs index 2dad73bf6e..914fe4f01f 100644 --- a/packages/bridge/src/runtime/capi.plugin.mjs +++ b/packages/bridge/src/runtime/capi.plugin.mjs @@ -1,4 +1,4 @@ -import Vue from 'vue' // eslint-disable-line import/default +import Vue from 'vue' import VueCompositionAPI from '@vue/composition-api' import { defineNuxtPlugin } from '#app' diff --git a/packages/bridge/src/runtime/vue2-bridge.d.ts b/packages/bridge/src/runtime/vue2-bridge.d.ts index 695b76bacb..3dfdc1fd48 100644 --- a/packages/bridge/src/runtime/vue2-bridge.d.ts +++ b/packages/bridge/src/runtime/vue2-bridge.d.ts @@ -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 { Vue as default } +export {} diff --git a/packages/bridge/src/runtime/vue2-bridge.mjs b/packages/bridge/src/runtime/vue2-bridge.mjs index 43cce7412c..18c4e7b23f 100644 --- a/packages/bridge/src/runtime/vue2-bridge.mjs +++ b/packages/bridge/src/runtime/vue2-bridge.mjs @@ -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' diff --git a/packages/bridge/src/vite/vite.ts b/packages/bridge/src/vite/vite.ts index 765a17510b..1ec626dc30 100644 --- a/packages/bridge/src/vite/vite.ts +++ b/packages/bridge/src/vite/vite.ts @@ -58,9 +58,7 @@ async function bundle (nuxt: Nuxt, builder: any) { 'ufo', 'date-fns', 'nanoid', - 'vue', - 'vue2', - 'vue2-bridge' + 'vue' // TODO(Anthony): waiting for Vite's fix https://github.com/vitejs/vite/issues/5688 // ...nuxt.options.build.transpile.filter(i => typeof i === 'string'), // 'vue-demi' diff --git a/packages/bridge/src/vue-compat.ts b/packages/bridge/src/vue-compat.ts new file mode 100644 index 0000000000..39ff87c460 --- /dev/null +++ b/packages/bridge/src/vue-compat.ts @@ -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`]) +] diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index e46e36e9ba..288f7519c8 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -168,7 +168,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => { 'process.env.RUNTIME_CONFIG': devalue(nitroContext._nuxt.runtimeConfig), 'process.env.DEBUG': JSON.stringify(nitroContext._nuxt.dev), // Needed for vue 2 server build - 'commonjsGlobal.process.env.VUE_ENV': '"server"' + 'commonjsGlobal.process.env.VUE_ENV': '"server"', + 'global["process"].env.VUE_ENV': '"server"' } })) diff --git a/test/bridge.test.ts b/test/bridge.test.ts index 018920cd99..b911aa9f4c 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -12,6 +12,9 @@ describe('fixtures:bridge', async () => { it('render hello world', async () => { 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', () => { diff --git a/test/fixtures/bridge/pages/index.vue b/test/fixtures/bridge/pages/index.vue index 1dd1fcbdaf..eafa8acae0 100644 --- a/test/fixtures/bridge/pages/index.vue +++ b/test/fixtures/bridge/pages/index.vue @@ -6,6 +6,7 @@ Update + Rendered on server: {{ serverBuild }} @@ -15,4 +16,5 @@ const version = ref('2') const state = useState('test-state') state.value = '123' const updateState = () => { state.value = '456' } +const serverBuild = useState('server-build', () => getCurrentInstance().proxy.$isServer)