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

feat(nuxt): support vue runtime compiler #4762

Merged
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f9cebae
feat(nuxt): support opt in option for runtime compiler
huang-julien May 2, 2022
87a8760
style: lint
huang-julien May 2, 2022
5d92902
fix(nuxt): set esmExternals and requireReturnsDefault on commonjs()
May 3, 2022
b3a2af0
fix(nuxt): add missing dep for @rollup/plugin-commonjs
huang-julien May 3, 2022
266b031
perf(nuxt): mock @vue/devtools-api in build
huang-julien May 3, 2022
9ece1d4
refactor: use nitro^0.4.x commonJS option to set dynamicRequireTargets
huang-julien May 6, 2022
40d72ae
feat(nuxt): enable runtime compiler client side in dev mode
huang-julien May 11, 2022
b91da5f
feat(nuxt): add webpack support on client build
May 12, 2022
62c9a30
refactor: move runtimeCompiler option into vue namespace
May 12, 2022
9260e17
feat(nuxt): allow usage of h() function by passing vue in the dynamic…
May 20, 2022
bc0e63d
Revert(nuxt): revert to avoid heavier final build
May 20, 2022
d3ad902
fix(nuxt): fix set vue esm for the runtime compiler
huang-julien Jul 14, 2022
3160ca5
feat: add vue to dynamicRequiretarget to avoid compile error
huang-julien Jul 14, 2022
99cff83
fix(nuxt): unmock estree and babel parser for runtime compiler
huang-julien Jul 22, 2022
c3226c4
style: lint
huang-julien Jul 22, 2022
067a3a1
refactor(nuxt): prefer externalVue instead of Rollup dynamicRequireTa…
huang-julien Sep 3, 2022
ac416a5
fix(nuxt): fix type
huang-julien Sep 3, 2022
0ba01f8
feat(nuxt): inline external vue/nuxt when runtimeCompiler true
huang-julien Sep 4, 2022
113f196
fix(nuxt): fix devtools stub due to merge
huang-julien Sep 4, 2022
4d8af8d
feat(nuxt): allow using runtimeCompiler with or without externalVue
Sep 6, 2022
fd3ad8a
fix(nuxt): force include vue/server-renderer/index.js
huang-julien Sep 13, 2022
a3fcf42
refactor(nuxt): use moduleDir to trace files
huang-julien Sep 13, 2022
7618e6a
fix: set back runtimeCOmpiler option due to merge
huang-julien Jan 29, 2023
fa67593
test(nuxt): retrieve fixtures test from nuxt-runtime-compiler
huang-julien Jan 29, 2023
c90891f
chore: update lock
huang-julien Feb 8, 2023
b331a6c
refactor: update tests
huang-julien Feb 8, 2023
f64d998
Merge branch 'main' into feat/nuxt-ssr-runtime-template-compiler
huang-julien Mar 12, 2023
d6da1f6
chore: update lock
huang-julien Mar 12, 2023
3cc4ad9
update chore and tests
huang-julien Mar 12, 2023
d1b2786
refactor: better tracing
huang-julien Mar 31, 2023
dfb05c1
Merge branch 'main' into feat/nuxt-ssr-runtime-template-compiler
danielroe Apr 3, 2023
7f07b70
test: log rendered html
danielroe Apr 3, 2023
28bbf9e
test: remove only
danielroe Apr 3, 2023
fbf267d
test: bump bundle size
danielroe Apr 3, 2023
a2c5d22
test: use path to runtime-compiler fixture
danielroe Apr 3, 2023
9438aed
test: bump bundle size
danielroe Apr 3, 2023
f3c5d11
perf: don't include server renderer when no externalVue
huang-julien Apr 3, 2023
e539b88
revert previous commit
huang-julien Apr 3, 2023
9258b94
refactor: simplify adding vue 3 mocks and omit with `externalVue`
danielroe Apr 3, 2023
3b6dd16
perf: use glob pattern for `dynamicRequireTargets`
danielroe Apr 3, 2023
866e89e
test: revert bundle size increase
danielroe Apr 3, 2023
3ccd6d5
fix: reverse condition
danielroe Apr 3, 2023
7caacf1
Merge remote-tracking branch 'origin/main' into feat/nuxt-ssr-runtime…
danielroe Apr 6, 2023
cb82f3c
fix: try without setting `dynamicRequireTargets`
danielroe Apr 6, 2023
6960253
refactor: use `experimental.runtimeVueCompiler` as option
danielroe Apr 6, 2023
c8ed188
chore: add comment
danielroe Apr 6, 2023
1ef1973
fix: disable by default
danielroe Apr 6, 2023
3f6acce
test: test runtime compiler in dev + webpack matrices as well
danielroe Apr 6, 2023
b74a152
fix: no need to trace server-renderer with externalVue
huang-julien Apr 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -19,7 +19,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
69 changes: 64 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.vue.runtimeCompiler)
? [
...nuxt.options.modulesDir.reduce<string[]>((targets, path) => {
const serverRendererPath = resolve(path, 'vue/server-renderer/index.js')
if (existsSync(serverRendererPath)) { targets.push(serverRendererPath) }
return targets
}, [])
]
danielroe marked this conversation as resolved.
Show resolved Hide resolved
: []
]
},
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.vue.runtimeCompiler || !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 All @@ -163,6 +179,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
rollupConfig: {
output: {},
plugins: []
},
commonJS: {
dynamicRequireTargets: (!nuxt.options.experimental.externalVue && nuxt.options.vue.runtimeCompiler)
? [
'node_modules/**/vue',
'node_modules/**/@vue/compiler-core',
'node_modules/**/@vue/compiler-dom',
'node_modules/**/@vue/compiler-ssr',
'node_modules/**/vue/server-renderer',
'node_modules/**/estree-walker'
]
: []
danielroe marked this conversation as resolved.
Show resolved Hide resolved
}
})

Expand Down Expand Up @@ -231,6 +259,37 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nuxt.callHook('prerender:routes', { routes })
})

// Enable runtime compiler client side
if (nuxt.options.vue.runtimeCompiler) {
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
7 changes: 6 additions & 1 deletion packages/schema/src/config/app.ts
Expand Up @@ -12,7 +12,12 @@ export default defineUntypedSchema({
* @see [documentation](https://vuejs.org/api/application.html#app-config-compileroptions)
* @type {typeof import('@vue/compiler-core').CompilerOptions}
*/
compilerOptions: {}
compilerOptions: {},

/**
* set vue runtime compiler bundle
*/
runtimeCompiler: false
},

/**
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>
9 changes: 9 additions & 0 deletions test/fixtures/runtime-compiler/nuxt.config.ts
@@ -0,0 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
vue: {
runtimeCompiler: true
},
experimental: {
externalVue: false
}
})
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"
}
54 changes: 54 additions & 0 deletions test/runtime-compiler.test.ts
@@ -0,0 +1,54 @@
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'

await setup({
rootDir: fileURLToPath(new URL('./fixtures/runtime-compiler', import.meta.url)),
server: true,
browser: true,
setupTimeout: (isWindows ? 240 : 120) * 1000
})

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