diff --git a/package.json b/package.json index f9e8d6da7b..89b96c211c 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 c0114b2ea7..684ff3758e 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 d3c3aede68..c9a42f37db 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 2469dd2680..0817b672a1 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 0000000000..438cb0860d --- /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 0000000000..f25b73707c --- /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 0000000000..54b23cd97e --- /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 0000000000..02456fa6ae --- /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 0000000000..604986a3fc --- /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 0000000000..cf133dcd9b --- /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 0000000000..42fd3135f7 --- /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 0000000000..d44088fe25 Binary files /dev/null and b/test/fixtures/runtime-compiler/public/favicon.ico differ 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 0000000000..26baa9d6c7 --- /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 0000000000..500bb1ff85 --- /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 0000000000..a746f2a70c --- /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 0000000000..b125a47d25 --- /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') + }) +})