Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
feat(nuxt): support vue runtime compiler (#4762)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien committed Apr 6, 2023
1 parent 7b5c755 commit 3fc9a75
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
57 changes: 52 additions & 5 deletions packages/nuxt/src/core/nitro.ts
Expand Up @@ -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: {
Expand All @@ -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
Expand Down Expand Up @@ -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 })
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/config/app.ts
Expand Up @@ -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: {},
},

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/schema/src/config/experimental.ts
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/runtime-compiler/.gitignore
@@ -0,0 +1,8 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
5 changes: 5 additions & 0 deletions test/fixtures/runtime-compiler/components/Helloworld.vue
@@ -0,0 +1,5 @@
<script>
export default defineNuxtComponent({
template: '<div>hello, Helloworld.vue here ! </div>'
})
</script>
15 changes: 15 additions & 0 deletions 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
})
}
})
35 changes: 35 additions & 0 deletions test/fixtures/runtime-compiler/components/ShowTemplate.vue
@@ -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 changes: 8 additions & 0 deletions 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'
})
10 changes: 10 additions & 0 deletions test/fixtures/runtime-compiler/package.json
@@ -0,0 +1,10 @@
{
"private": true,
"name": "fixture-runtime-compiler",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
}
}
66 changes: 66 additions & 0 deletions test/fixtures/runtime-compiler/pages/index.vue
@@ -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.
18 changes: 18 additions & 0 deletions 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: '<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 changes: 7 additions & 0 deletions 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 '<div>Hello my name is : {{name}}, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>'
})
4 changes: 4 additions & 0 deletions test/fixtures/runtime-compiler/tsconfig.json
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
59 changes: 59 additions & 0 deletions 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('<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')
})
})

0 comments on commit 3fc9a75

Please sign in to comment.