feat(nuxt): support vue runtime compiler (#4762)

This commit is contained in:
Julien Huang 2023-04-06 13:51:32 +02:00 committed by GitHub
parent 7b5c755b38
commit 3fc9a75070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 295 additions and 7 deletions

View File

@ -20,7 +20,7 @@
"play": "pnpm nuxi dev playground", "play": "pnpm nuxi dev playground",
"play:build": "pnpm nuxi build playground", "play:build": "pnpm nuxi build playground",
"play:preview": "pnpm nuxi preview 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:dev": "TEST_ENV=dev pnpm test:fixtures",
"test:fixtures:webpack": "TEST_BUILDER=webpack 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", "test:types": "pnpm nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit",

View File

@ -126,6 +126,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
'nuxt/dist', 'nuxt/dist',
'nuxt3/dist', 'nuxt3/dist',
distDir 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<string[]>((targets, path) => {
const serverRendererPath = resolve(path, 'vue/server-renderer/index.js')
if (existsSync(serverRendererPath)) { targets.push(serverRendererPath) }
return targets
}, [])
]
: []
] ]
}, },
alias: { 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: await resolvePath(`vue/dist/vue.cjs${nuxt.options.dev ? '' : '.prod'}.js`)
}, },
// Vue 3 mocks // Vue 3 mocks
...nuxt.options.experimental.runtimeVueCompiler || nuxt.options.experimental.externalVue
? {}
: {
'estree-walker': 'unenv/runtime/mock/proxy', 'estree-walker': 'unenv/runtime/mock/proxy',
'@babel/parser': 'unenv/runtime/mock/proxy', '@babel/parser': 'unenv/runtime/mock/proxy',
'@vue/compiler-core': 'unenv/runtime/mock/proxy', '@vue/compiler-core': 'unenv/runtime/mock/proxy',
'@vue/compiler-dom': 'unenv/runtime/mock/proxy', '@vue/compiler-dom': 'unenv/runtime/mock/proxy',
'@vue/compiler-ssr': 'unenv/runtime/mock/proxy', '@vue/compiler-ssr': 'unenv/runtime/mock/proxy'
},
'@vue/devtools-api': 'vue-devtools-stub', '@vue/devtools-api': 'vue-devtools-stub',
// Paths // Paths
@ -231,6 +247,37 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nuxt.callHook('prerender:routes', { routes }) 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 // Setup handlers
const devMiddlewareHandler = dynamicEventHandler() const devMiddlewareHandler = dynamicEventHandler()
nitro.options.devHandlers.unshift({ handler: devMiddlewareHandler }) nitro.options.devHandlers.unshift({ handler: devMiddlewareHandler })

View File

@ -12,7 +12,7 @@ export default defineUntypedSchema({
* @see [documentation](https://vuejs.org/api/application.html#app-config-compileroptions) * @see [documentation](https://vuejs.org/api/application.html#app-config-compileroptions)
* @type {typeof import('@vue/compiler-core').CompilerOptions} * @type {typeof import('@vue/compiler-core').CompilerOptions}
*/ */
compilerOptions: {} compilerOptions: {},
}, },
/** /**

View File

@ -21,6 +21,12 @@ export default defineUntypedSchema({
*/ */
externalVue: true, 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. * Tree shakes contents of client-only components from server bundle.
* @see https://github.com/nuxt/framework/pull/5750 * @see https://github.com/nuxt/framework/pull/5750

View File

@ -0,0 +1,8 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist

View File

@ -0,0 +1,5 @@
<script>
export default defineNuxtComponent({
template: '<div>hello, Helloworld.vue here ! </div>'
})
</script>

View File

@ -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
})
}
})

View File

@ -0,0 +1,35 @@
<template>
<component :is="showIt" :name="name" />
</template>
<script>
export default defineNuxtComponent({
props: {
template: {
required: true,
type: String
},
name: {
type: String,
default: () => '(missing name prop)'
}
},
setup (props) {
const showIt = h({
template: props.template,
props: {
name: {
type: String,
default: () => '(missing name prop)'
}
}
})
return {
showIt
}
}
})
</script>

View File

@ -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'
})

View File

@ -0,0 +1,10 @@
{
"private": true,
"name": "fixture-runtime-compiler",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
}
}

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import type { Component } from 'vue'
import Helloworld from '../components/Helloworld.vue'
const count = ref(0)
const compTemplate = computed(() => `
<div class='border'>
<div>hello i am defined in the setup of app.vue</div>
<div>This component template is in a computed refreshed on count</div>
count: <span class="count">${count.value}</span>.
I dont recommend you to do this for performance issue, prefer passing props for mutable data.
</div>`
)
const ComponentDefinedInSetup = computed(() => h({
template: compTemplate.value
}) as Component)
const { data, pending } = await useAsyncData('templates', async () => {
const [interactiveComponent, templateString] = await Promise.all([
$fetch('/api/full-component'),
$fetch('/api/template')
])
return {
interactiveComponent,
templateString
}
}, {})
const Interactive = h({
template: data.value?.interactiveComponent.template,
setup (props) {
// eslint-disable-next-line no-new-func
return new Function(
'ref',
'computed',
'props',
data.value?.interactiveComponent.setup ?? ''
)(ref, computed, props)
},
props: data.value?.interactiveComponent.props
}) as Component
</script>
<template>
<!-- Edit this file to play around with Nuxt but never commit changes! -->
<div>
<Helloworld id="hello-world" />
<ComponentDefinedInSetup id="component-defined-in-setup" />
<button id="increment-count" @click="count++">
{{ count }}
</button>
<template v-if="!pending">
<Name id="name" template="<div>I am the Name.ts component</div>" />
<show-template id="show-template" :template="data?.templateString ?? ''" name="John" />
<Interactive id="interactive" lastname="Doe" firstname="John" />
</template>
</div>
</template>
<style>
.border {
border: 1px solid burlywood;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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: '<div>my name is {{ fullName }}, <button id="inc-interactive-count" @click="count++">click here</button> count: <span id="interactive-count">{{count}}</span>. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api </div>'
}
})

View File

@ -0,0 +1,7 @@
/**
* mock the behavior of nuxt retrieving data from an api
*/
export default defineEventHandler(() => {
return '<div>Hello my name is : {{name}}, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>'
})

View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -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('<div id="hello-world">hello, Helloworld.vue here ! </div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="hello-world">hello, Helloworld.vue here ! </div>')
})
it('test Name.ts', async () => {
const html = await $fetch('/')
const { page } = await renderPage('/')
expect(html).toContain('<div id="name">I am the Name.ts component</div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="name">I am the Name.ts component</div>')
})
it('test ShowTemplate.ts', async () => {
const html = await $fetch('/')
const { page } = await renderPage('/')
expect(html).toContain('<div id="show-template">Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="show-template">Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>')
})
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')
})
})