diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8c217f62c4..40ceb8a2611 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
uses: ./.github/workflows/test.yml
continuous-release:
- if: github.repository == 'vuejs/core'
+ if: github.repository == 'vuejs/core' && github.ref_name != 'vapor'
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 00000000000..484ab7e5c61
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+results/*
diff --git a/benchmark/client/App.vue b/benchmark/client/App.vue
new file mode 100644
index 00000000000..428217aa971
--- /dev/null
+++ b/benchmark/client/App.vue
@@ -0,0 +1,144 @@
+
+
+
+ Vue.js ({{ isVapor ? 'Vapor' : 'Virtual DOM' }}) Benchmark
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmark/client/data.ts b/benchmark/client/data.ts
new file mode 100644
index 00000000000..2626bf13481
--- /dev/null
+++ b/benchmark/client/data.ts
@@ -0,0 +1,78 @@
+import { shallowRef } from 'vue/vapor'
+
+let ID = 1
+
+function _random(max: number) {
+ return Math.round(Math.random() * 1000) % max
+}
+
+export function buildData(count = 1000) {
+ const adjectives = [
+ 'pretty',
+ 'large',
+ 'big',
+ 'small',
+ 'tall',
+ 'short',
+ 'long',
+ 'handsome',
+ 'plain',
+ 'quaint',
+ 'clean',
+ 'elegant',
+ 'easy',
+ 'angry',
+ 'crazy',
+ 'helpful',
+ 'mushy',
+ 'odd',
+ 'unsightly',
+ 'adorable',
+ 'important',
+ 'inexpensive',
+ 'cheap',
+ 'expensive',
+ 'fancy',
+ ]
+ const colours = [
+ 'red',
+ 'yellow',
+ 'blue',
+ 'green',
+ 'pink',
+ 'brown',
+ 'purple',
+ 'brown',
+ 'white',
+ 'black',
+ 'orange',
+ ]
+ const nouns = [
+ 'table',
+ 'chair',
+ 'house',
+ 'bbq',
+ 'desk',
+ 'car',
+ 'pony',
+ 'cookie',
+ 'sandwich',
+ 'burger',
+ 'pizza',
+ 'mouse',
+ 'keyboard',
+ ]
+ const data = []
+ for (let i = 0; i < count; i++)
+ data.push({
+ id: ID++,
+ label: shallowRef(
+ adjectives[_random(adjectives.length)] +
+ ' ' +
+ colours[_random(colours.length)] +
+ ' ' +
+ nouns[_random(nouns.length)],
+ ),
+ })
+ return data
+}
diff --git a/benchmark/client/index.html b/benchmark/client/index.html
new file mode 100644
index 00000000000..c3ca4c53590
--- /dev/null
+++ b/benchmark/client/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Vue Vapor Benchmark
+
+
+
+
+
+
+
diff --git a/benchmark/client/index.ts b/benchmark/client/index.ts
new file mode 100644
index 00000000000..a12f727a101
--- /dev/null
+++ b/benchmark/client/index.ts
@@ -0,0 +1,5 @@
+if (import.meta.env.IS_VAPOR) {
+ import('./vapor')
+} else {
+ import('./vdom')
+}
diff --git a/benchmark/client/profiling.ts b/benchmark/client/profiling.ts
new file mode 100644
index 00000000000..82d950a0f61
--- /dev/null
+++ b/benchmark/client/profiling.ts
@@ -0,0 +1,94 @@
+/* eslint-disable no-console */
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-restricted-globals */
+
+import { nextTick } from 'vue/vapor'
+
+declare namespace globalThis {
+ let doProfile: boolean
+ let reactivity: boolean
+ let recordTime: boolean
+ let times: Record
+}
+
+globalThis.recordTime = true
+globalThis.doProfile = false
+globalThis.reactivity = false
+
+export const defer = () => new Promise(r => requestIdleCallback(r))
+
+const times: Record = (globalThis.times = {})
+
+export function wrap(
+ id: string,
+ fn: (...args: any[]) => any,
+): (...args: any[]) => Promise {
+ return async (...args) => {
+ if (!globalThis.recordTime) {
+ return fn(...args)
+ }
+
+ document.body.classList.remove('done')
+
+ const { doProfile } = globalThis
+ await nextTick()
+
+ doProfile && console.profile(id)
+ const start = performance.now()
+ fn(...args)
+
+ await nextTick()
+ let time: number
+ if (globalThis.reactivity) {
+ time = performance.measure(
+ 'flushJobs-measure',
+ 'flushJobs-start',
+ 'flushJobs-end',
+ ).duration
+ performance.clearMarks()
+ performance.clearMeasures()
+ } else {
+ time = performance.now() - start
+ }
+ const prevTimes = times[id] || (times[id] = [])
+ prevTimes.push(time)
+
+ const { min, max, median, mean, std } = compute(prevTimes)
+ const msg =
+ `${id}: min: ${min} / ` +
+ `max: ${max} / ` +
+ `median: ${median}ms / ` +
+ `mean: ${mean}ms / ` +
+ `time: ${time.toFixed(2)}ms / ` +
+ `std: ${std} ` +
+ `over ${prevTimes.length} runs`
+ doProfile && console.profileEnd(id)
+ console.log(msg)
+ const timeEl = document.getElementById('time')!
+ timeEl.textContent = msg
+
+ document.body.classList.add('done')
+ }
+}
+
+function compute(array: number[]) {
+ const n = array.length
+ const max = Math.max(...array)
+ const min = Math.min(...array)
+ const mean = array.reduce((a, b) => a + b) / n
+ const std = Math.sqrt(
+ array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+ )
+ const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+ return {
+ max: round(max),
+ min: round(min),
+ mean: round(mean),
+ std: round(std),
+ median: round(median),
+ }
+}
+
+function round(n: number) {
+ return +n.toFixed(2)
+}
diff --git a/benchmark/client/vapor.ts b/benchmark/client/vapor.ts
new file mode 100644
index 00000000000..70f1d4b1b3a
--- /dev/null
+++ b/benchmark/client/vapor.ts
@@ -0,0 +1,4 @@
+import { createVaporApp } from 'vue/vapor'
+import App from './App.vue'
+
+createVaporApp(App as any).mount('#app')
diff --git a/benchmark/client/vdom.ts b/benchmark/client/vdom.ts
new file mode 100644
index 00000000000..01433bca2ac
--- /dev/null
+++ b/benchmark/client/vdom.ts
@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/benchmark/index.js b/benchmark/index.js
new file mode 100644
index 00000000000..f1a37cdff79
--- /dev/null
+++ b/benchmark/index.js
@@ -0,0 +1,397 @@
+// @ts-check
+import path from 'node:path'
+import { parseArgs } from 'node:util'
+import { mkdir, rm, writeFile } from 'node:fs/promises'
+import Vue from '@vitejs/plugin-vue'
+import { build } from 'vite'
+import connect from 'connect'
+import sirv from 'sirv'
+import { launch } from 'puppeteer'
+import colors from 'picocolors'
+import { exec, getSha } from '../scripts/utils.js'
+import process from 'node:process'
+import readline from 'node:readline'
+
+// Thanks to https://github.com/krausest/js-framework-benchmark (Apache-2.0 license)
+const {
+ values: {
+ skipLib,
+ skipApp,
+ skipBench,
+ vdom,
+ noVapor,
+ port: portStr,
+ count: countStr,
+ warmupCount: warmupCountStr,
+ noHeadless,
+ devBuild,
+ },
+} = parseArgs({
+ allowNegative: true,
+ allowPositionals: true,
+ options: {
+ skipLib: {
+ type: 'boolean',
+ short: 'l',
+ },
+ skipApp: {
+ type: 'boolean',
+ short: 'a',
+ },
+ skipBench: {
+ type: 'boolean',
+ short: 'b',
+ },
+ noVapor: {
+ type: 'boolean',
+ },
+ vdom: {
+ type: 'boolean',
+ short: 'v',
+ },
+ port: {
+ type: 'string',
+ short: 'p',
+ default: '8193',
+ },
+ count: {
+ type: 'string',
+ short: 'c',
+ default: '30',
+ },
+ warmupCount: {
+ type: 'string',
+ short: 'w',
+ default: '5',
+ },
+ noHeadless: {
+ type: 'boolean',
+ },
+ devBuild: {
+ type: 'boolean',
+ short: 'd',
+ },
+ },
+})
+
+const port = +(/** @type {string}*/ (portStr))
+const count = +(/** @type {string}*/ (countStr))
+const warmupCount = +(/** @type {string}*/ (warmupCountStr))
+const sha = await getSha(true)
+
+if (!skipLib) {
+ await buildLib()
+}
+if (!skipApp) {
+ await rm('client/dist', { recursive: true }).catch(() => {})
+ vdom && (await buildApp(false))
+ !noVapor && (await buildApp(true))
+}
+const server = startServer()
+
+if (!skipBench) {
+ await benchmark()
+ server.close()
+}
+
+async function buildLib() {
+ console.info(colors.blue('Building lib...'))
+
+ /** @type {import('node:child_process').SpawnOptions} */
+ const options = {
+ cwd: path.resolve(import.meta.dirname, '..'),
+ stdio: 'inherit',
+ env: { ...process.env, BENCHMARK: 'true' },
+ }
+ const buildOptions = devBuild ? '-df' : '-pf'
+ const [{ ok }, { ok: ok2 }, { ok: ok3 }, { ok: ok4 }] = await Promise.all([
+ exec(
+ 'pnpm',
+ `run --silent build shared compiler-core compiler-dom compiler-vapor ${buildOptions} cjs`.split(
+ ' ',
+ ),
+ options,
+ ),
+ exec(
+ 'pnpm',
+ 'run --silent build compiler-sfc compiler-ssr -f cjs'.split(' '),
+ options,
+ ),
+ exec(
+ 'pnpm',
+ `run --silent build vue-vapor ${buildOptions} esm-browser`.split(' '),
+ options,
+ ),
+ exec(
+ 'pnpm',
+ `run --silent build vue ${buildOptions} esm-browser-runtime`.split(' '),
+ options,
+ ),
+ ])
+
+ if (!ok || !ok2 || !ok3 || !ok4) {
+ console.error('Failed to build')
+ process.exit(1)
+ }
+}
+
+/** @param {boolean} isVapor */
+async function buildApp(isVapor) {
+ console.info(
+ colors.blue(`\nBuilding ${isVapor ? 'Vapor' : 'Virtual DOM'} app...\n`),
+ )
+
+ if (!devBuild) process.env.NODE_ENV = 'production'
+ const CompilerSFC = await import(
+ '../packages/compiler-sfc/dist/compiler-sfc.cjs.js'
+ )
+ const prodSuffix = devBuild ? '.js' : '.prod.js'
+
+ /** @type {any} */
+ const TemplateCompiler = await import(
+ (isVapor
+ ? '../packages/compiler-vapor/dist/compiler-vapor.cjs'
+ : '../packages/compiler-dom/dist/compiler-dom.cjs') + prodSuffix
+ )
+ const runtimePath = path.resolve(
+ import.meta.dirname,
+ (isVapor
+ ? '../packages/vue-vapor/dist/vue-vapor.esm-browser'
+ : '../packages/vue/dist/vue.runtime.esm-browser') + prodSuffix,
+ )
+
+ const mode = isVapor ? 'vapor' : 'vdom'
+ await build({
+ root: './client',
+ base: `/${mode}`,
+ define: {
+ 'import.meta.env.IS_VAPOR': String(isVapor),
+ },
+ build: {
+ minify: !devBuild && 'terser',
+ outDir: path.resolve('./client/dist', mode),
+ rollupOptions: {
+ onwarn(log, handler) {
+ if (log.code === 'INVALID_ANNOTATION') return
+ handler(log)
+ },
+ },
+ },
+ resolve: {
+ alias: {
+ 'vue/vapor': runtimePath,
+ vue: runtimePath,
+ },
+ },
+ clearScreen: false,
+ plugins: [
+ Vue({
+ compiler: CompilerSFC,
+ template: { compiler: TemplateCompiler },
+ }),
+ ],
+ })
+}
+
+function startServer() {
+ const server = connect().use(sirv('./client/dist')).listen(port)
+ printPort(port)
+ process.on('SIGTERM', () => server.close())
+ return server
+}
+
+async function benchmark() {
+ console.info(colors.blue(`\nStarting benchmark...`))
+
+ const browser = await initBrowser()
+
+ await mkdir('results', { recursive: true }).catch(() => {})
+ if (!noVapor) {
+ await doBench(browser, true)
+ }
+ if (vdom) {
+ await doBench(browser, false)
+ }
+
+ await browser.close()
+}
+
+/**
+ *
+ * @param {import('puppeteer').Browser} browser
+ * @param {boolean} isVapor
+ */
+async function doBench(browser, isVapor) {
+ const mode = isVapor ? 'vapor' : 'vdom'
+ console.info('\n\nmode:', mode)
+
+ const page = await browser.newPage()
+ page.emulateCPUThrottling(4)
+ await page.goto(`http://localhost:${port}/${mode}`, {
+ waitUntil: 'networkidle0',
+ })
+
+ await forceGC()
+ const t = performance.now()
+
+ console.log('warmup run')
+ await eachRun(() => withoutRecord(benchOnce), warmupCount)
+
+ console.log('benchmark run')
+ await eachRun(benchOnce, count)
+
+ console.info(
+ 'Total time:',
+ colors.cyan(((performance.now() - t) / 1000).toFixed(2)),
+ 's',
+ )
+ const times = await getTimes()
+ const result =
+ /** @type {Record} */
+ Object.fromEntries(Object.entries(times).map(([k, v]) => [k, compute(v)]))
+
+ console.table(result)
+ await writeFile(
+ `results/benchmark-${sha}-${mode}.json`,
+ JSON.stringify(result, undefined, 2),
+ )
+ await page.close()
+ return result
+
+ async function benchOnce() {
+ await clickButton('run') // test: create rows
+ await clickButton('update') // partial update
+ await clickButton('swaprows') // swap rows
+ await select() // test: select row, remove row
+ await clickButton('clear') // clear rows
+
+ await withoutRecord(() => clickButton('run'))
+ await clickButton('add') // append rows to large table
+
+ await withoutRecord(() => clickButton('clear'))
+ await clickButton('runLots') // create many rows
+ await withoutRecord(() => clickButton('clear'))
+
+ // TODO replace all rows
+ }
+
+ function getTimes() {
+ return page.evaluate(() => /** @type {any} */ (globalThis).times)
+ }
+
+ async function forceGC() {
+ await page.evaluate(
+ `window.gc({type:'major',execution:'sync',flavor:'last-resort'})`,
+ )
+ }
+
+ /** @param {() => any} fn */
+ async function withoutRecord(fn) {
+ const currentRecordTime = await page.evaluate(() => globalThis.recordTime)
+ await page.evaluate(() => (globalThis.recordTime = false))
+ await fn()
+ await page.evaluate(
+ currentRecordTime => (globalThis.recordTime = currentRecordTime),
+ currentRecordTime,
+ )
+ }
+
+ /** @param {string} id */
+ async function clickButton(id) {
+ await page.click(`#${id}`)
+ await wait()
+ }
+
+ async function select() {
+ for (let i = 1; i <= 10; i++) {
+ await page.click(`tbody > tr:nth-child(2) > td:nth-child(2) > a`)
+ await page.waitForSelector(`tbody > tr:nth-child(2).danger`)
+ await page.click(`tbody > tr:nth-child(2) > td:nth-child(3) > button`)
+ await wait()
+ }
+ }
+
+ async function wait() {
+ await page.waitForSelector('.done')
+ }
+}
+
+/**
+ * @param {Function} bench
+ * @param {number} count
+ */
+async function eachRun(bench, count) {
+ for (let i = 0; i < count; i++) {
+ readline.cursorTo(process.stdout, 0)
+ readline.clearLine(process.stdout, 0)
+ process.stdout.write(`${i + 1}/${count}`)
+ await bench()
+ }
+ if (count === 0) {
+ process.stdout.write('0/0 (skip)')
+ }
+ process.stdout.write('\n')
+}
+
+async function initBrowser() {
+ const disableFeatures = [
+ 'Translate', // avoid translation popups
+ 'PrivacySandboxSettings4', // avoid privacy popup
+ 'IPH_SidePanelGenericMenuFeature', // bookmark popup see https://github.com/krausest/js-framework-benchmark/issues/1688
+ ]
+
+ const args = [
+ '--js-flags=--expose-gc', // needed for gc() function
+ '--no-default-browser-check',
+ '--disable-sync',
+ '--no-first-run',
+ '--ash-no-nudges',
+ '--disable-extensions',
+ `--disable-features=${disableFeatures.join(',')}`,
+ ]
+
+ const headless = !noHeadless
+ console.info('headless:', headless)
+ const browser = await launch({
+ headless: headless,
+ args,
+ })
+ console.log('browser version:', colors.blue(await browser.version()))
+
+ return browser
+}
+
+/** @param {number[]} array */
+function compute(array) {
+ const n = array.length
+ const max = Math.max(...array)
+ const min = Math.min(...array)
+ const mean = array.reduce((a, b) => a + b) / n
+ const std = Math.sqrt(
+ array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+ )
+ const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+ return {
+ max: round(max),
+ min: round(min),
+ mean: round(mean),
+ std: round(std),
+ median: round(median),
+ }
+}
+
+/** @param {number} n */
+function round(n) {
+ return +n.toFixed(2)
+}
+
+/** @param {number} port */
+function printPort(port) {
+ const vaporLink = !noVapor
+ ? `\nVapor: ${colors.blue(`http://localhost:${port}/vapor`)}`
+ : ''
+ const vdomLink = vdom
+ ? `\nvDom: ${colors.blue(`http://localhost:${port}/vdom`)}`
+ : ''
+ console.info(`\n\nServer started at`, vaporLink, vdomLink)
+}
diff --git a/benchmark/package.json b/benchmark/package.json
new file mode 100644
index 00000000000..81d55005c9e
--- /dev/null
+++ b/benchmark/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "benchmark",
+ "version": "0.0.0",
+ "author": "三咲智子 Kevin Deng ",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "dev": "pnpm start --devBuild --noHeadless --skipBench --vdom",
+ "start": "node index.js"
+ },
+ "dependencies": {
+ "@vitejs/plugin-vue": "https://pkg.pr.new/@vitejs/plugin-vue@e3c5ce5",
+ "connect": "^3.7.0",
+ "sirv": "^2.0.4",
+ "vite": "catalog:"
+ },
+ "devDependencies": {
+ "@types/connect": "^3.4.38",
+ "terser": "^5.33.0"
+ }
+}
diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json
new file mode 100644
index 00000000000..535fcf20696
--- /dev/null
+++ b/benchmark/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "lib": ["es2022", "dom"],
+ "allowJs": true,
+ "moduleDetection": "force",
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "types": ["node", "vite/client"],
+ "strict": true,
+ "noUnusedLocals": true,
+ "declaration": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "paths": {
+ "vue": ["../packages/vue/src"],
+ "@vue/*": ["../packages/*/src"]
+ }
+ },
+ "include": ["**/*"]
+}
diff --git a/eslint.config.js b/eslint.config.js
index b752b2e19f1..7282992910f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -126,6 +126,7 @@ export default tseslint.config(
files: [
'packages-private/template-explorer/**',
'packages-private/sfc-playground/**',
+ 'playground/**',
],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
@@ -149,6 +150,7 @@ export default tseslint.config(
'eslint.config.js',
'rollup*.config.js',
'scripts/**',
+ 'benchmark/*',
'./*.{js,ts}',
'packages/*/*.js',
'packages/vue/*/*.js',
diff --git a/package.json b/package.json
index 2478f3a1711..9e502ec604a 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,9 @@
"build-dts": "tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js",
"clean": "rimraf --glob packages/*/dist temp .eslintcache",
"size": "run-s \"size-*\" && node scripts/usage-size.js",
- "size-global": "node scripts/build.js vue runtime-dom -f global -p --size",
+ "size-global": "node scripts/build.js vue runtime-dom compiler-dom -f global -p --size",
"size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
- "size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
+ "size-esm": "node scripts/build.js runtime-shared runtime-dom runtime-core reactivity shared runtime-vapor -f esm-bundler",
"check": "tsc --incremental --noEmit",
"lint": "eslint --cache .",
"format": "prettier --write --cache .",
@@ -29,18 +29,17 @@
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
- "dev-compiler": "run-p \"dev template-explorer\" serve",
- "dev-sfc": "run-s dev-sfc-prepare dev-sfc-run",
- "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-all-cjs",
- "dev-sfc-serve": "vite packages-private/sfc-playground --host",
- "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
+ "dev-prepare-cjs": "node scripts/prepare-cjs.js || npm run build-all-cjs",
+ "dev-compiler": "run-p \"dev template-explorer\" serve open",
+ "dev-sfc": "run-s dev-prepare-cjs dev-sfc-run",
+ "dev-sfc-serve": "vite packages-private/sfc-playground",
+ "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if vapor\" \"dev vue -ipf vapor\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
+ "dev-vapor": "pnpm -C playground run dev",
"serve": "serve",
"open": "open http://localhost:3000/packages-private/template-explorer/local.html",
- "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-browser-esm build-ssr-esm build-sfc-playground-self",
+ "build-sfc-playground": "run-s build-all-cjs build-all-esm build-sfc-playground-self",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
- "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
- "build-browser-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler && node scripts/build.js vue -f esm-browser",
- "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
+ "build-all-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler+esm-browser+esm-bundler-runtime+esm-browser-runtime+vapor && node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground-self": "cd packages-private/sfc-playground && npm run build",
"preinstall": "npx only-allow pnpm",
"postinstall": "simple-git-hooks"
@@ -75,6 +74,7 @@
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^2.1.1",
+ "@vitest/ui": "^2.1.1",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
diff --git a/packages-private/sfc-playground/src/App.vue b/packages-private/sfc-playground/src/App.vue
index d163b1a3ee6..2d4069318ae 100644
--- a/packages-private/sfc-playground/src/App.vue
+++ b/packages-private/sfc-playground/src/App.vue
@@ -1,8 +1,15 @@
+
+
+ {{ msg }}
+
diff --git a/packages-private/sfc-playground/vite.config.ts b/packages-private/sfc-playground/vite.config.ts
index 2e77f1970a7..c1a40fd1ca9 100644
--- a/packages-private/sfc-playground/vite.config.ts
+++ b/packages-private/sfc-playground/vite.config.ts
@@ -53,6 +53,8 @@ function copyVuePlugin(): Plugin {
copyFile(`vue/dist/vue.esm-browser.prod.js`)
copyFile(`vue/dist/vue.runtime.esm-browser.js`)
copyFile(`vue/dist/vue.runtime.esm-browser.prod.js`)
+ copyFile(`vue/dist/vue.runtime-with-vapor.esm-browser.js`)
+ copyFile(`vue/dist/vue.runtime-with-vapor.esm-browser.prod.js`)
copyFile(`server-renderer/dist/server-renderer.esm-browser.js`)
},
}
diff --git a/packages-private/template-explorer/package.json b/packages-private/template-explorer/package.json
index e8bdf0bccbb..fafca266402 100644
--- a/packages-private/template-explorer/package.json
+++ b/packages-private/template-explorer/package.json
@@ -11,6 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
+ "@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.52.0",
"source-map-js": "^1.2.1"
}
diff --git a/packages-private/template-explorer/src/index.ts b/packages-private/template-explorer/src/index.ts
index 988712d623c..96619b5a311 100644
--- a/packages-private/template-explorer/src/index.ts
+++ b/packages-private/template-explorer/src/index.ts
@@ -1,15 +1,18 @@
import type * as m from 'monaco-editor'
+import type { CompilerError } from '@vue/compiler-dom'
+import { compile } from '@vue/compiler-dom'
import {
- type CompilerError,
type CompilerOptions,
- compile,
-} from '@vue/compiler-dom'
-import { compile as ssrCompile } from '@vue/compiler-ssr'
+ compile as vaporCompile,
+} from '@vue/compiler-vapor'
+// import { compile as ssrCompile } from '@vue/compiler-ssr'
+
import {
compilerOptions,
defaultOptions,
initOptions,
ssrMode,
+ vaporMode,
} from './options'
import { toRaw, watchEffect } from '@vue/runtime-dom'
import { SourceMapConsumer } from 'source-map-js'
@@ -77,10 +80,16 @@ window.init = () => {
console.clear()
try {
const errors: CompilerError[] = []
- const compileFn = ssrMode.value ? ssrCompile : compile
+ const compileFn = /* ssrMode.value ? ssrCompile : */ (
+ vaporMode.value ? vaporCompile : compile
+ ) as typeof vaporCompile
const start = performance.now()
const { code, ast, map } = compileFn(source, {
...compilerOptions,
+ prefixIdentifiers:
+ compilerOptions.prefixIdentifiers ||
+ compilerOptions.mode === 'module' ||
+ compilerOptions.ssr,
filename: 'ExampleTemplate.vue',
sourceMap: true,
onError: err => {
diff --git a/packages-private/template-explorer/src/options.ts b/packages-private/template-explorer/src/options.ts
index e3cc6173a8a..341e885c083 100644
--- a/packages-private/template-explorer/src/options.ts
+++ b/packages-private/template-explorer/src/options.ts
@@ -1,8 +1,9 @@
import { createApp, h, reactive, ref } from 'vue'
-import type { CompilerOptions } from '@vue/compiler-dom'
+import type { CompilerOptions } from '@vue/compiler-vapor'
import { BindingTypes } from '@vue/compiler-core'
export const ssrMode = ref(false)
+export const vaporMode = ref(true)
export const defaultOptions: CompilerOptions = {
mode: 'module',
@@ -39,11 +40,11 @@ const App = {
compilerOptions.prefixIdentifiers || compilerOptions.mode === 'module'
return [
- h('h1', `Vue 3 Template Explorer`),
+ h('h1', `Vue Template Explorer`),
h(
'a',
{
- href: `https://github.com/vuejs/core/tree/${__COMMIT__}`,
+ href: `https://github.com/vuejs/vue/tree/${__COMMIT__}`,
target: `_blank`,
},
`@${__COMMIT__}`,
@@ -222,6 +223,18 @@ const App = {
}),
h('label', { for: 'compat' }, 'v2 compat mode'),
]),
+
+ h('li', [
+ h('input', {
+ type: 'checkbox',
+ id: 'vapor',
+ checked: vaporMode.value,
+ onChange(e: Event) {
+ vaporMode.value = (e.target as HTMLInputElement).checked
+ },
+ }),
+ h('label', { for: 'vapor' }, 'vapor'),
+ ]),
]),
]),
]
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2d6df9d9010..bae13372a98 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -86,6 +86,13 @@ export interface Position {
column: number
}
+export type AllNode =
+ | ParentNode
+ | ExpressionNode
+ | TemplateChildNode
+ | AttributeNode
+ | DirectiveNode
+
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts
index 52fabeea896..f81f2691570 100644
--- a/packages/compiler-core/src/babelUtils.ts
+++ b/packages/compiler-core/src/babelUtils.ts
@@ -308,8 +308,8 @@ export const isFunctionType = (node: Node): node is Function => {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
-export const isStaticProperty = (node: Node): node is ObjectProperty =>
- node &&
+export const isStaticProperty = (node?: Node): node is ObjectProperty =>
+ !!node &&
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
!node.computed
@@ -510,3 +510,79 @@ export function unwrapTSNode(node: Node): Node {
return node
}
}
+
+export function isStaticNode(node: Node): boolean {
+ node = unwrapTSNode(node)
+
+ switch (node.type) {
+ case 'UnaryExpression': // void 0, !true
+ return isStaticNode(node.argument)
+
+ case 'LogicalExpression': // 1 > 2
+ case 'BinaryExpression': // 1 + 2
+ return isStaticNode(node.left) && isStaticNode(node.right)
+
+ case 'ConditionalExpression': {
+ // 1 ? 2 : 3
+ return (
+ isStaticNode(node.test) &&
+ isStaticNode(node.consequent) &&
+ isStaticNode(node.alternate)
+ )
+ }
+
+ case 'SequenceExpression': // (1, 2)
+ case 'TemplateLiteral': // `foo${1}`
+ return node.expressions.every(expr => isStaticNode(expr))
+
+ case 'ParenthesizedExpression': // (1)
+ return isStaticNode(node.expression)
+
+ case 'StringLiteral':
+ case 'NumericLiteral':
+ case 'BooleanLiteral':
+ case 'NullLiteral':
+ case 'BigIntLiteral':
+ return true
+ }
+ return false
+}
+
+export function isConstantNode(
+ node: Node,
+ onIdentifier: (name: string) => boolean,
+): boolean {
+ if (isStaticNode(node)) return true
+
+ node = unwrapTSNode(node)
+ switch (node.type) {
+ case 'Identifier':
+ return onIdentifier(node.name)
+ case 'RegExpLiteral':
+ return true
+ case 'ObjectExpression':
+ return node.properties.every(prop => {
+ // { bar() {} } object methods are not considered static nodes
+ if (prop.type === 'ObjectMethod') return false
+ // { ...{ foo: 1 } }
+ if (prop.type === 'SpreadElement')
+ return isConstantNode(prop.argument, onIdentifier)
+ // { foo: 1 }
+ return (
+ (!prop.computed || isConstantNode(prop.key, onIdentifier)) &&
+ isConstantNode(prop.value, onIdentifier)
+ )
+ })
+ case 'ArrayExpression':
+ return node.elements.every(element => {
+ // [1, , 3]
+ if (element === null) return true
+ // [1, ...[2, 3]]
+ if (element.type === 'SpreadElement')
+ return isConstantNode(element.argument, onIdentifier)
+ // [1, 2]
+ return isConstantNode(element, onIdentifier)
+ })
+ }
+ return false
+}
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 70116cfb61a..26d0bbef23d 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -105,22 +105,38 @@ const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
-export interface CodegenResult {
+export interface BaseCodegenResult {
code: string
preamble: string
- ast: RootNode
+ ast: unknown
map?: RawSourceMap
+ helpers?: Set | Set
+}
+
+export interface CodegenResult extends BaseCodegenResult {
+ ast: RootNode
+ helpers: Set
}
-enum NewlineType {
+export enum NewlineType {
+ /** Start with `\n` */
Start = 0,
+ /** Ends with `\n` */
End = -1,
+ /** No `\n` included */
None = -2,
+ /** Don't know, calc it */
Unknown = -3,
}
export interface CodegenContext
- extends Omit, 'bindingMetadata' | 'inline'> {
+ extends Omit<
+ Required,
+ | 'bindingMetadata'
+ | 'inline'
+ | 'vaporRuntimeModuleName'
+ | 'expressionPlugins'
+ > {
source: string
code: string
line: number
@@ -398,6 +414,7 @@ export function generate(
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
map: context.map ? context.map.toJSON() : undefined,
+ helpers: ast.helpers,
}
}
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index 29e5f681300..36ed73eab92 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -23,15 +23,19 @@ export {
} from './transform'
export {
generate,
+ NewlineType,
type CodegenContext,
type CodegenResult,
type CodegenSourceMapGenerator,
type RawSourceMap,
+ type BaseCodegenResult,
} from './codegen'
export {
ErrorCodes,
errorMessages,
createCompilerError,
+ defaultOnError,
+ defaultOnWarn,
type CoreCompilerError,
type CompilerError,
} from './errors'
@@ -52,6 +56,7 @@ export {
transformExpression,
processExpression,
stringifyExpression,
+ isLiteralWhitelisted,
} from './transforms/transformExpression'
export {
buildSlots,
@@ -75,4 +80,5 @@ export {
checkCompatEnabled,
warnDeprecation,
CompilerDeprecationTypes,
+ type CompilerCompatOptions,
} from './compat/compatConfig'
diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts
index 1de865f42eb..9983071609e 100644
--- a/packages/compiler-core/src/options.ts
+++ b/packages/compiler-core/src/options.ts
@@ -174,6 +174,12 @@ interface SharedTransformCodegenOptions {
* @default mode === 'module'
*/
prefixIdentifiers?: boolean
+ /**
+ * A list of parser plugins to enable for `@babel/parser`, which is used to
+ * parse expressions in bindings and interpolations.
+ * https://babeljs.io/docs/en/next/babel-parser#plugins
+ */
+ expressionPlugins?: ParserPlugin[]
/**
* Control whether generate SSR-optimized render functions instead.
* The resulting function must be attached to the component via the
@@ -272,12 +278,6 @@ export interface TransformOptions
* @default false
*/
cacheHandlers?: boolean
- /**
- * A list of parser plugins to enable for `@babel/parser`, which is used to
- * parse expressions in bindings and interpolations.
- * https://babeljs.io/docs/en/next/babel-parser#plugins
- */
- expressionPlugins?: ParserPlugin[]
/**
* SFC scoped styles ID
*/
diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts
index 9ae8897e674..9012c2701f7 100644
--- a/packages/compiler-core/src/transforms/transformExpression.ts
+++ b/packages/compiler-core/src/transforms/transformExpression.ts
@@ -44,7 +44,8 @@ import { parseExpression } from '@babel/parser'
import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
-const isLiteralWhitelisted = /*@__PURE__*/ makeMap('true,false,null,this')
+export const isLiteralWhitelisted: (key: string) => boolean =
+ /*@__PURE__*/ makeMap('true,false,null,this')
export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.INTERPOLATION) {
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b49d70bb2fb..b90a7018c8b 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -160,7 +160,7 @@ export const isMemberExpressionBrowser = (exp: ExpressionNode): boolean => {
export const isMemberExpressionNode: (
exp: ExpressionNode,
- context: TransformContext,
+ context: Pick,
) => boolean = __BROWSER__
? (NOOP as any)
: (exp, context) => {
@@ -185,7 +185,7 @@ export const isMemberExpressionNode: (
export const isMemberExpression: (
exp: ExpressionNode,
- context: TransformContext,
+ context: Pick,
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
const fnExpRE =
@@ -196,7 +196,7 @@ export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
export const isFnExpressionNode: (
exp: ExpressionNode,
- context: TransformContext,
+ context: Pick,
) => boolean = __BROWSER__
? (NOOP as any)
: (exp, context) => {
@@ -227,7 +227,7 @@ export const isFnExpressionNode: (
export const isFnExpression: (
exp: ExpressionNode,
- context: TransformContext,
+ context: Pick,
) => boolean = __BROWSER__ ? isFnExpressionBrowser : isFnExpressionNode
export function advancePositionWithClone(
@@ -279,6 +279,7 @@ export function assert(condition: boolean, msg?: string): void {
}
}
+/** find directive */
export function findDir(
node: ElementNode,
name: string | RegExp,
diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts
index b47624840ab..15641e531af 100644
--- a/packages/compiler-dom/src/errors.ts
+++ b/packages/compiler-dom/src/errors.ts
@@ -48,7 +48,7 @@ if (__TEST__) {
}
}
-export const DOMErrorMessages: { [code: number]: string } = {
+export const DOMErrorMessages: Record = {
[DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
[DOMErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
[DOMErrorCodes.X_V_TEXT_NO_EXPRESSION]: `v-text is missing expression.`,
@@ -60,4 +60,7 @@ export const DOMErrorMessages: { [code: number]: string } = {
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`,
[DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN]: ` expects exactly one child element or component.`,
[DOMErrorCodes.X_IGNORED_SIDE_EFFECT_TAG]: `Tags with side effect (
+ {{ foo }}
`)
expect(bindings).toStrictEqual({
__propsAliases: {
@@ -173,6 +174,7 @@ describe('sfc reactive props destructure', () => {
"foo:bar": { type: String, required: true, default: 'foo-bar' },
"onUpdate:modelValue": { type: Function, required: true }
},`)
+ expect(content).toMatch(`__props.foo`)
assertCode(content)
})
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index a9a638f5384..d6db266314e 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -46,6 +46,7 @@
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
+ "@vue/compiler-vapor": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index fee05beed96..798e7465071 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -2,6 +2,7 @@ import {
BindingTypes,
UNREF,
isFunctionType,
+ isStaticNode,
unwrapTSNode,
walkIdentifiers,
} from '@vue/compiler-dom'
@@ -28,7 +29,7 @@ import {
normalScriptDefaultVar,
processNormalScript,
} from './script/normalScript'
-import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
+import { genCssVarsCode, getCssVarsHelper } from './style/cssVars'
import {
type SFCTemplateCompileOptions,
compileTemplate,
@@ -125,6 +126,10 @@ export interface SFCScriptCompileOptions {
* Transform Vue SFCs into custom elements.
*/
customElement?: boolean | ((filename: string) => boolean)
+ /**
+ * Force to use of Vapor mode.
+ */
+ vapor?: boolean
}
export interface ImportBinding {
@@ -169,6 +174,7 @@ export function compileScript(
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
+ const vapor = sfc.vapor || options.vapor
let refBindings: string[] | undefined
@@ -753,7 +759,7 @@ export function compileScript(
// no need to do this when targeting SSR
!options.templateOptions?.ssr
) {
- ctx.helperImports.add(CSS_VARS_HELPER)
+ ctx.helperImports.add(getCssVarsHelper(vapor))
ctx.helperImports.add('unref')
ctx.s.prependLeft(
startOffset,
@@ -762,6 +768,7 @@ export function compileScript(
ctx.bindingMetadata,
scopeId,
!!options.isProd,
+ vapor,
)}\n`,
)
}
@@ -866,7 +873,7 @@ export function compileScript(
}
// inline render function mode - we are going to compile the template and
// inline it right here
- const { code, ast, preamble, tips, errors } = compileTemplate({
+ const { code, preamble, tips, errors, helpers } = compileTemplate({
filename,
ast: sfc.template.ast,
source: sfc.template.content,
@@ -911,7 +918,7 @@ export function compileScript(
// avoid duplicated unref import
// as this may get injected by the render function preamble OR the
// css vars codegen
- if (ast && ast.helpers.has(UNREF)) {
+ if (helpers && helpers.has(UNREF)) {
ctx.helperImports.delete('unref')
}
returned = code
@@ -931,7 +938,11 @@ export function compileScript(
`\n}\n\n`,
)
} else {
- ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
+ ctx.s.appendRight(
+ endOffset,
+ // vapor mode generates its own return when inlined
+ `\n${vapor ? `` : `return `}${returned}\n}\n\n`,
+ )
}
// 10. finalize default export
@@ -940,6 +951,9 @@ export function compileScript(
: `export default`
let runtimeOptions = ``
+ if (vapor) {
+ runtimeOptions += `\n vapor: true,`
+ }
if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) {
@@ -980,7 +994,7 @@ export function compileScript(
ctx.s.prependLeft(
startOffset,
`\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
- `defineComponent`,
+ vapor ? `defineVaporComponent` : `defineComponent`,
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`,
@@ -1254,40 +1268,3 @@ function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
return false
}
}
-
-function isStaticNode(node: Node): boolean {
- node = unwrapTSNode(node)
-
- switch (node.type) {
- case 'UnaryExpression': // void 0, !true
- return isStaticNode(node.argument)
-
- case 'LogicalExpression': // 1 > 2
- case 'BinaryExpression': // 1 + 2
- return isStaticNode(node.left) && isStaticNode(node.right)
-
- case 'ConditionalExpression': {
- // 1 ? 2 : 3
- return (
- isStaticNode(node.test) &&
- isStaticNode(node.consequent) &&
- isStaticNode(node.alternate)
- )
- }
-
- case 'SequenceExpression': // (1, 2)
- case 'TemplateLiteral': // `foo${1}`
- return node.expressions.every(expr => isStaticNode(expr))
-
- case 'ParenthesizedExpression': // (1)
- return isStaticNode(node.expression)
-
- case 'StringLiteral':
- case 'NumericLiteral':
- case 'BooleanLiteral':
- case 'NullLiteral':
- case 'BigIntLiteral':
- return true
- }
- return false
-}
diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts
index 322b1570e1a..bd69fb2cc9f 100644
--- a/packages/compiler-sfc/src/compileTemplate.ts
+++ b/packages/compiler-sfc/src/compileTemplate.ts
@@ -1,5 +1,5 @@
import {
- type CodegenResult,
+ type BaseCodegenResult,
type CompilerError,
type CompilerOptions,
type ElementNode,
@@ -24,24 +24,29 @@ import {
} from './template/transformSrcset'
import { generateCodeFrame, isObject } from '@vue/shared'
import * as CompilerDOM from '@vue/compiler-dom'
+import * as CompilerVapor from '@vue/compiler-vapor'
import * as CompilerSSR from '@vue/compiler-ssr'
import consolidate from '@vue/consolidate'
import { warnOnce } from './warn'
import { genCssVarsFromList } from './style/cssVars'
export interface TemplateCompiler {
- compile(source: string | RootNode, options: CompilerOptions): CodegenResult
+ compile(
+ source: string | RootNode,
+ options: CompilerOptions,
+ ): BaseCodegenResult
parse(template: string, options: ParserOptions): RootNode
}
export interface SFCTemplateCompileResults {
code: string
- ast?: RootNode
+ ast?: unknown
preamble?: string
source: string
tips: string[]
errors: (string | CompilerError)[]
map?: RawSourceMap
+ helpers?: Set
}
export interface SFCTemplateCompileOptions {
@@ -52,6 +57,7 @@ export interface SFCTemplateCompileOptions {
scoped?: boolean
slotted?: boolean
isProd?: boolean
+ vapor?: boolean
ssr?: boolean
ssrCssVars?: string[]
inMap?: RawSourceMap
@@ -168,6 +174,7 @@ function doCompileTemplate({
source,
ast: inAST,
ssr = false,
+ vapor = false,
ssrCssVars,
isProd = false,
compiler,
@@ -202,7 +209,12 @@ function doCompileTemplate({
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
- const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
+ const defaultCompiler = vapor
+ ? // TODO ssr
+ (CompilerVapor as TemplateCompiler)
+ : ssr
+ ? (CompilerSSR as TemplateCompiler)
+ : CompilerDOM
compiler = compiler || defaultCompiler
if (compiler !== defaultCompiler) {
@@ -227,25 +239,30 @@ function doCompileTemplate({
inAST = createRoot(template.children, inAST.source)
}
- let { code, ast, preamble, map } = compiler.compile(inAST || source, {
- mode: 'module',
- prefixIdentifiers: true,
- hoistStatic: true,
- cacheHandlers: true,
- ssrCssVars:
- ssr && ssrCssVars && ssrCssVars.length
- ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
- : '',
- scopeId: scoped ? longId : undefined,
- slotted,
- sourceMap: true,
- ...compilerOptions,
- hmr: !isProd,
- nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
- filename,
- onError: e => errors.push(e),
- onWarn: w => warnings.push(w),
- })
+ let { code, ast, preamble, map, helpers } = compiler.compile(
+ inAST || source,
+ {
+ mode: 'module',
+ prefixIdentifiers: true,
+ hoistStatic: true,
+ cacheHandlers: true,
+ ssrCssVars:
+ ssr && ssrCssVars && ssrCssVars.length
+ ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
+ : '',
+ scopeId: scoped ? longId : undefined,
+ slotted,
+ sourceMap: true,
+ ...compilerOptions,
+ hmr: !isProd,
+ nodeTransforms: nodeTransforms.concat(
+ compilerOptions.nodeTransforms || [],
+ ),
+ filename,
+ onError: e => errors.push(e),
+ onWarn: w => warnings.push(w),
+ },
+ )
// inMap should be the map produced by ./parse.ts which is a simple line-only
// mapping. If it is present, we need to adjust the final map and errors to
@@ -271,7 +288,16 @@ function doCompileTemplate({
return msg
})
- return { code, ast, preamble, source, errors, tips, map }
+ return {
+ code,
+ ast,
+ preamble,
+ source,
+ errors,
+ tips,
+ map,
+ helpers,
+ }
}
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts
index c8be865508f..739b455d066 100644
--- a/packages/compiler-sfc/src/parse.ts
+++ b/packages/compiler-sfc/src/parse.ts
@@ -84,6 +84,8 @@ export interface SFCDescriptor {
*/
slotted: boolean
+ vapor: boolean
+
/**
* compare with an existing descriptor to determine whether HMR should perform
* a reload vs. re-render.
@@ -137,6 +139,7 @@ export function parse(
customBlocks: [],
cssVars: [],
slotted: false,
+ vapor: false,
shouldForceReload: prevImports => hmrShouldReload(prevImports, descriptor),
}
@@ -171,6 +174,7 @@ export function parse(
source,
false,
) as SFCTemplateBlock)
+ descriptor.vapor ||= !!templateBlock.attrs.vapor
if (!templateBlock.attrs.src) {
templateBlock.ast = createRoot(node.children, source)
@@ -195,6 +199,7 @@ export function parse(
break
case 'script':
const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
+ descriptor.vapor ||= !!scriptBlock.attrs.vapor
const isSetup = !!scriptBlock.attrs.setup
if (isSetup && !descriptor.scriptSetup) {
descriptor.scriptSetup = scriptBlock
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
index 9a4880a1a54..ac5226168e4 100644
--- a/packages/compiler-sfc/src/script/defineProps.ts
+++ b/packages/compiler-sfc/src/script/defineProps.ts
@@ -79,6 +79,15 @@ export function processDefineProps(
)
}
ctx.propsTypeDecl = node.typeParameters.params[0]
+ // register bindings
+ const { props } = resolveTypeElements(ctx, ctx.propsTypeDecl)
+ if (props) {
+ for (const key in props) {
+ if (!(key in ctx.bindingMetadata)) {
+ ctx.bindingMetadata[key] = BindingTypes.PROPS
+ }
+ }
+ }
}
// handle props destructure
@@ -190,10 +199,6 @@ export function extractRuntimeProps(
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
- // register bindings
- if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
- ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
- }
}
let propsDecls = `{
diff --git a/packages/compiler-sfc/src/style/cssVars.ts b/packages/compiler-sfc/src/style/cssVars.ts
index 0397c7d790a..95a7d53c8ae 100644
--- a/packages/compiler-sfc/src/style/cssVars.ts
+++ b/packages/compiler-sfc/src/style/cssVars.ts
@@ -10,10 +10,14 @@ import {
import type { SFCDescriptor } from '../parse'
import type { PluginCreator } from 'postcss'
import hash from 'hash-sum'
-import { getEscapedCssVarName } from '@vue/shared'
+import { capitalize, getEscapedCssVarName } from '@vue/shared'
export const CSS_VARS_HELPER = `useCssVars`
+export function getCssVarsHelper(vapor: boolean | undefined): string {
+ return vapor ? `vapor${capitalize(CSS_VARS_HELPER)}` : CSS_VARS_HELPER
+}
+
export function genCssVarsFromList(
vars: string[],
id: string,
@@ -162,6 +166,7 @@ export function genCssVarsCode(
bindings: BindingMetadata,
id: string,
isProd: boolean,
+ vapor?: boolean,
) {
const varsExp = genCssVarsFromList(vars, id, isProd)
const exp = createSimpleExpression(varsExp, false)
@@ -182,7 +187,7 @@ export function genCssVarsCode(
})
.join('')
- return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
+ return `_${getCssVarsHelper(vapor)}(_ctx => (${transformedString}))`
}
//
+