mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +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: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",
|
||||
|
@ -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<string[]>((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 })
|
||||
|
@ -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: {},
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
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