From 3fc9a75070367a4bdd90e3a7fc7f38185b71da19 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Thu, 6 Apr 2023 13:51:32 +0200 Subject: [PATCH] feat(nuxt): support vue runtime compiler (#4762) --- package.json | 2 +- packages/nuxt/src/core/nitro.ts | 57 +++++++++++++-- packages/schema/src/config/app.ts | 2 +- packages/schema/src/config/experimental.ts | 6 ++ test/fixtures/runtime-compiler/.gitignore | 8 +++ .../components/Helloworld.vue | 5 ++ .../runtime-compiler/components/Name.ts | 15 ++++ .../components/ShowTemplate.vue | 35 ++++++++++ test/fixtures/runtime-compiler/nuxt.config.ts | 8 +++ test/fixtures/runtime-compiler/package.json | 10 +++ .../fixtures/runtime-compiler/pages/index.vue | 66 ++++++++++++++++++ .../runtime-compiler/public/favicon.ico | Bin 0 -> 15406 bytes .../server/api/full-component.get.ts | 18 +++++ .../server/api/template.get.ts | 7 ++ test/fixtures/runtime-compiler/tsconfig.json | 4 ++ test/runtime-compiler.test.ts | 59 ++++++++++++++++ 16 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/runtime-compiler/.gitignore create mode 100644 test/fixtures/runtime-compiler/components/Helloworld.vue create mode 100644 test/fixtures/runtime-compiler/components/Name.ts create mode 100644 test/fixtures/runtime-compiler/components/ShowTemplate.vue create mode 100644 test/fixtures/runtime-compiler/nuxt.config.ts create mode 100644 test/fixtures/runtime-compiler/package.json create mode 100644 test/fixtures/runtime-compiler/pages/index.vue create mode 100644 test/fixtures/runtime-compiler/public/favicon.ico create mode 100644 test/fixtures/runtime-compiler/server/api/full-component.get.ts create mode 100644 test/fixtures/runtime-compiler/server/api/template.get.ts create mode 100644 test/fixtures/runtime-compiler/tsconfig.json create mode 100644 test/runtime-compiler.test.ts diff --git a/package.json b/package.json index f9e8d6da7bf..89b96c211c3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "play": "pnpm nuxi dev playground", "play:build": "pnpm nuxi build playground", "play:preview": "pnpm nuxi preview playground", - "test:fixtures": "pnpm nuxi prepare test/fixtures/basic && JITI_ESM_RESOLVE=1 vitest run --dir test", + "test:fixtures": "pnpm nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/runtime-compiler && JITI_ESM_RESOLVE=1 vitest run --dir test", "test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures", "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", "test:types": "pnpm nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit", diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index c0114b2ea75..684ff3758ef 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -126,6 +126,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { 'nuxt/dist', 'nuxt3/dist', distDir + ], + traceInclude: [ + // force include files used in generated code from the runtime-compiler + ...(nuxt.options.experimental.runtimeVueCompiler && !nuxt.options.experimental.externalVue) + ? [ + ...nuxt.options.modulesDir.reduce((targets, path) => { + const serverRendererPath = resolve(path, 'vue/server-renderer/index.js') + if (existsSync(serverRendererPath)) { targets.push(serverRendererPath) } + return targets + }, []) + ] + : [] ] }, alias: { @@ -137,11 +149,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { vue: await resolvePath(`vue/dist/vue.cjs${nuxt.options.dev ? '' : '.prod'}.js`) }, // Vue 3 mocks - 'estree-walker': 'unenv/runtime/mock/proxy', - '@babel/parser': 'unenv/runtime/mock/proxy', - '@vue/compiler-core': 'unenv/runtime/mock/proxy', - '@vue/compiler-dom': 'unenv/runtime/mock/proxy', - '@vue/compiler-ssr': 'unenv/runtime/mock/proxy', + ...nuxt.options.experimental.runtimeVueCompiler || nuxt.options.experimental.externalVue + ? {} + : { + 'estree-walker': 'unenv/runtime/mock/proxy', + '@babel/parser': 'unenv/runtime/mock/proxy', + '@vue/compiler-core': 'unenv/runtime/mock/proxy', + '@vue/compiler-dom': 'unenv/runtime/mock/proxy', + '@vue/compiler-ssr': 'unenv/runtime/mock/proxy' + }, '@vue/devtools-api': 'vue-devtools-stub', // Paths @@ -231,6 +247,37 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { nuxt.callHook('prerender:routes', { routes }) }) + // Enable runtime compiler client side + if (nuxt.options.experimental.runtimeVueCompiler) { + nuxt.hook('vite:extendConfig', (config, { isClient }) => { + if (isClient) { + if (Array.isArray(config.resolve!.alias)) { + config.resolve!.alias.push({ + find: 'vue', + replacement: 'vue/dist/vue.esm-bundler' + }) + } else { + config.resolve!.alias = { + ...config.resolve!.alias, + vue: 'vue/dist/vue.esm-bundler' + } + } + } + }) + nuxt.hook('webpack:config', (configuration) => { + const clientConfig = configuration.find(config => config.name === 'client') + if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} } + if (Array.isArray(clientConfig!.resolve!.alias)) { + clientConfig!.resolve!.alias.push({ + name: 'vue', + alias: 'vue/dist/vue.esm-bundler' + }) + } else { + clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler' + } + }) + } + // Setup handlers const devMiddlewareHandler = dynamicEventHandler() nitro.options.devHandlers.unshift({ handler: devMiddlewareHandler }) diff --git a/packages/schema/src/config/app.ts b/packages/schema/src/config/app.ts index d3c3aede68c..c9a42f37db7 100644 --- a/packages/schema/src/config/app.ts +++ b/packages/schema/src/config/app.ts @@ -12,7 +12,7 @@ export default defineUntypedSchema({ * @see [documentation](https://vuejs.org/api/application.html#app-config-compileroptions) * @type {typeof import('@vue/compiler-core').CompilerOptions} */ - compilerOptions: {} + compilerOptions: {}, }, /** diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 2469dd26801..0817b672a1a 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -21,6 +21,12 @@ export default defineUntypedSchema({ */ externalVue: true, + // TODO: move to `vue.runtimeCompiler` in v3.5 + /** + * Include Vue compiler in runtime bundle. + */ + runtimeVueCompiler: false, + /** * Tree shakes contents of client-only components from server bundle. * @see https://github.com/nuxt/framework/pull/5750 diff --git a/test/fixtures/runtime-compiler/.gitignore b/test/fixtures/runtime-compiler/.gitignore new file mode 100644 index 00000000000..438cb0860d1 --- /dev/null +++ b/test/fixtures/runtime-compiler/.gitignore @@ -0,0 +1,8 @@ +node_modules +*.log* +.nuxt +.nitro +.cache +.output +.env +dist diff --git a/test/fixtures/runtime-compiler/components/Helloworld.vue b/test/fixtures/runtime-compiler/components/Helloworld.vue new file mode 100644 index 00000000000..f25b73707c5 --- /dev/null +++ b/test/fixtures/runtime-compiler/components/Helloworld.vue @@ -0,0 +1,5 @@ + diff --git a/test/fixtures/runtime-compiler/components/Name.ts b/test/fixtures/runtime-compiler/components/Name.ts new file mode 100644 index 00000000000..54b23cd97ef --- /dev/null +++ b/test/fixtures/runtime-compiler/components/Name.ts @@ -0,0 +1,15 @@ +export default defineNuxtComponent({ + props: ['template', 'name'], + + /** + * most of the time, vue compiler need at least a VNode, use h() to render the component + */ + render () { + return h({ + props: ['name'], + template: this.template + }, { + name: this.name + }) + } +}) diff --git a/test/fixtures/runtime-compiler/components/ShowTemplate.vue b/test/fixtures/runtime-compiler/components/ShowTemplate.vue new file mode 100644 index 00000000000..02456fa6ae1 --- /dev/null +++ b/test/fixtures/runtime-compiler/components/ShowTemplate.vue @@ -0,0 +1,35 @@ + + + diff --git a/test/fixtures/runtime-compiler/nuxt.config.ts b/test/fixtures/runtime-compiler/nuxt.config.ts new file mode 100644 index 00000000000..604986a3fcc --- /dev/null +++ b/test/fixtures/runtime-compiler/nuxt.config.ts @@ -0,0 +1,8 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + experimental: { + runtimeVueCompiler: true, + externalVue: false + }, + builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite' +}) diff --git a/test/fixtures/runtime-compiler/package.json b/test/fixtures/runtime-compiler/package.json new file mode 100644 index 00000000000..cf133dcd9b9 --- /dev/null +++ b/test/fixtures/runtime-compiler/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "name": "fixture-runtime-compiler", + "scripts": { + "build": "nuxi build" + }, + "dependencies": { + "nuxt": "workspace:*" + } +} diff --git a/test/fixtures/runtime-compiler/pages/index.vue b/test/fixtures/runtime-compiler/pages/index.vue new file mode 100644 index 00000000000..42fd3135f7c --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/index.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/public/favicon.ico b/test/fixtures/runtime-compiler/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d44088fe25f6dd6b3a0c9b0914c07b2cbfb063f1 GIT binary patch literal 15406 zcmeHOd2Ccg7=IprEB@iXupna9D0pFl8gJv7h^UF8hG@KEOiVQT2aUvkj7G#55rL(& z6vPTGN0D;NU9J{HjsQiVv`|X7`}TZq-?g9LH|v}3JGR|@-R^c}C)v!KneX_`_vX!f zGxMz=TqRs1j2Izc?h`(^RuCo%g5dL|$`v;V!a}T@FyX)Uae~l%qaaiu4YD9dsva}f z6!iPP?DdaQf(v|*%a8GRkv|H-s<9w0 zoyhpLp1&cua4g8j77fU+^&DY5(Jdb_S_9=#AA&Wv4aiHjNgQ9o_>K4_h_0UodiWeG zhn78@6@Pfu3(S6cxE*sNB`eYn!Rm2Y@e_vF_RpC8Lv;`0yL=hMnp?Bx9q9e$yL~X< z0PzMqeuGRe^14n6IaJa-SYm%T|*bU*;lN~rBO|Rn` zyaklD6>LrEPnMT9?NSR{>rxAz#@2pQj#zsqB-L17J3gtMJ0QH`83#_2C)ZpTPg~^E zx@pRYZg|_$#tY~d#I}9P=}dYrxgP4%pH$=MuiVV#l24YN}?;VDk* zMK|&2!INAC^Ow@*E%GPQQ>1YU^R4vY30AGNtsJy|)9)gIcKs3bqtcVK{~M-HTyi(V zt%uGIUVqnKt|Dn@Al8oOGDCHfcpE*a9ou;u`Ly!kUwGN8{gpFK79H{C85WsFtc$m^ z@lkb?rQNGulTSW8m&>OI@_U0d6)SGkq*p+q$XQ2;NDJZnVEOprVZITWatpzRmpVF9xlFXI!Fh9g!rDX@-o+%x-#ZD z?6oq=NSi;)G`XFvlHT|P=T?TAIy1q0YxQC?;(9(Krf}nA&;m!nkb2melG?Ei=xjaTxU!W;XKeOFXYJvY z&p9xO)fN0c+s>n&+YHn6Nwy2jrw2#Pj|mEarnZ;=PJ| zcwSC14|ZMImetOkT&?+_bsutICwa>0)g}qgm9y(WTyhu0{`?ry?{Fv%CZG3Hnfjxi z-^#I=56YV_`Js~8q;?B7}3X$#*c%c2b{yDL$fXS*rY$c^9u`^S%_|Yw z755>jJAQHT1c>8zOMbmBo&r5^3@9!khI6S{TfCSt`#TU9-45~Hl^~(t7+U_AyG_}e z?DUQ7vWix<oO6L(%Z0jgK-0@8>jS+m&rs9s^gz7%18MQ~WQQjK31UzaBZ2^`per zEsA+yF(()UA^Lr@OQ{cponlTXZiZq|%s3A0M>EcO!})uV2qW3u#7_9jscZqS7Wfy? CQ%)8D literal 0 HcmV?d00001 diff --git a/test/fixtures/runtime-compiler/server/api/full-component.get.ts b/test/fixtures/runtime-compiler/server/api/full-component.get.ts new file mode 100644 index 00000000000..26baa9d6c70 --- /dev/null +++ b/test/fixtures/runtime-compiler/server/api/full-component.get.ts @@ -0,0 +1,18 @@ +/** + * sometimes, CMS wants to give full control on components. This might not be a good practice. + * SO MAKE SURE TO SANITIZE ALL YOUR STRINGS + */ +export default defineEventHandler(() => { + return { + props: ['lastname', 'firstname'], + // don't forget to sanitize + setup: ` + const fullName = computed(() => props.lastname + ' ' + props.firstname); + + const count = ref(0); + + return {fullName, count} + `, + template: '
my name is {{ fullName }}, count: {{count}}. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api
' + } +}) diff --git a/test/fixtures/runtime-compiler/server/api/template.get.ts b/test/fixtures/runtime-compiler/server/api/template.get.ts new file mode 100644 index 00000000000..500bb1ff858 --- /dev/null +++ b/test/fixtures/runtime-compiler/server/api/template.get.ts @@ -0,0 +1,7 @@ +/** + * mock the behavior of nuxt retrieving data from an api + */ + +export default defineEventHandler(() => { + return '
Hello my name is : {{name}}, i am defined by ShowTemplate.vue and my template is retrieved from the API
' +}) diff --git a/test/fixtures/runtime-compiler/tsconfig.json b/test/fixtures/runtime-compiler/tsconfig.json new file mode 100644 index 00000000000..a746f2a70c2 --- /dev/null +++ b/test/fixtures/runtime-compiler/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} diff --git a/test/runtime-compiler.test.ts b/test/runtime-compiler.test.ts new file mode 100644 index 00000000000..b125a47d25b --- /dev/null +++ b/test/runtime-compiler.test.ts @@ -0,0 +1,59 @@ +import { fileURLToPath } from 'node:url' +import { isWindows } from 'std-env' +import { describe, it, expect } from 'vitest' +import { setup, $fetch } from '@nuxt/test-utils' +import { expectNoClientErrors, renderPage } from './utils' +const isWebpack = process.env.TEST_BUILDER === 'webpack' + +await setup({ + rootDir: fileURLToPath(new URL('./fixtures/runtime-compiler', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', + server: true, + browser: true, + setupTimeout: (isWindows ? 240 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite' + } +}) + +describe('test basic config', () => { + it('expect render page without any error or logs', async () => { + await expectNoClientErrors('/') + }) + + it('test HelloWorld.vue', async () => { + const html = await $fetch('/') + const { page } = await renderPage('/') + + expect(html).toContain('
hello, Helloworld.vue here !
') + expect(await page.locator('body').innerHTML()).toContain('
hello, Helloworld.vue here !
') + }) + + it('test Name.ts', async () => { + const html = await $fetch('/') + const { page } = await renderPage('/') + + expect(html).toContain('
I am the Name.ts component
') + expect(await page.locator('body').innerHTML()).toContain('
I am the Name.ts component
') + }) + + it('test ShowTemplate.ts', async () => { + const html = await $fetch('/') + const { page } = await renderPage('/') + + expect(html).toContain('
Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API
') + expect(await page.locator('body').innerHTML()).toContain('
Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API
') + }) + + it('test Interactive component.ts', async () => { + const html = await $fetch('/') + const { page } = await renderPage('/') + + expect(html).toContain('I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api') + expect(await page.locator('#interactive').innerHTML()).toContain('I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api') + const button = page.locator('#inc-interactive-count') + await button.click() + const count = page.locator('#interactive-count') + expect(await count.innerHTML()).toBe('1') + }) +})