mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 23:22:02 +00:00
feat(nuxt): support vue runtime compiler (#4762)
This commit is contained in:
parent
7b5c755b38
commit
3fc9a75070
@ -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",
|
||||||
|
@ -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 })
|
||||||
|
@ -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: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
8
test/fixtures/runtime-compiler/.gitignore
vendored
Normal file
8
test/fixtures/runtime-compiler/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
5
test/fixtures/runtime-compiler/components/Helloworld.vue
vendored
Normal file
5
test/fixtures/runtime-compiler/components/Helloworld.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
template: '<div>hello, Helloworld.vue here ! </div>'
|
||||||
|
})
|
||||||
|
</script>
|
15
test/fixtures/runtime-compiler/components/Name.ts
vendored
Normal file
15
test/fixtures/runtime-compiler/components/Name.ts
vendored
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
35
test/fixtures/runtime-compiler/components/ShowTemplate.vue
vendored
Normal file
35
test/fixtures/runtime-compiler/components/ShowTemplate.vue
vendored
Normal 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>
|
8
test/fixtures/runtime-compiler/nuxt.config.ts
vendored
Normal file
8
test/fixtures/runtime-compiler/nuxt.config.ts
vendored
Normal 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'
|
||||||
|
})
|
10
test/fixtures/runtime-compiler/package.json
vendored
Normal file
10
test/fixtures/runtime-compiler/package.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "fixture-runtime-compiler",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxi build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nuxt": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
66
test/fixtures/runtime-compiler/pages/index.vue
vendored
Normal file
66
test/fixtures/runtime-compiler/pages/index.vue
vendored
Normal 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>
|
BIN
test/fixtures/runtime-compiler/public/favicon.ico
vendored
Normal file
BIN
test/fixtures/runtime-compiler/public/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
18
test/fixtures/runtime-compiler/server/api/full-component.get.ts
vendored
Normal file
18
test/fixtures/runtime-compiler/server/api/full-component.get.ts
vendored
Normal 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>'
|
||||||
|
}
|
||||||
|
})
|
7
test/fixtures/runtime-compiler/server/api/template.get.ts
vendored
Normal file
7
test/fixtures/runtime-compiler/server/api/template.get.ts
vendored
Normal 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>'
|
||||||
|
})
|
4
test/fixtures/runtime-compiler/tsconfig.json
vendored
Normal file
4
test/fixtures/runtime-compiler/tsconfig.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
59
test/runtime-compiler.test.ts
Normal file
59
test/runtime-compiler.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user