diff --git a/.github/contributing.md b/.github/contributing.md index 2554582b887..f0ec46ee709 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -38,7 +38,6 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ### Pull Request Checklist - Vue core has two primary work branches: `main` and `minor`. - - If your pull request is a feature that adds new API surface, it should be submitted against the `minor` branch. - Otherwise, it should be submitted against the `main` branch. @@ -46,12 +45,10 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before - [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time. - If adding a new feature: - - Add accompanying test case. - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it. - If fixing a bug: - - If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`. - Provide a detailed description of the bug in the PR. Live demo preferred. - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `nr test-coverage`. @@ -69,9 +66,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before - The PR should fix the intended bug **only** and not introduce unrelated changes. This includes unnecessary refactors - a PR should focus on the fix and not code style, this makes it easier to trace changes in the future. - Consider the performance / size impact of the changes, and whether the bug being fixes justifies the cost. If the bug being fixed is a very niche edge case, we should try to minimize the size / perf cost to make it worthwhile. - - Is the code perf-sensitive (e.g. in "hot paths" like component updates or the vdom patch function?) - - If the branch is dev-only, performance is less of a concern. - Check how much extra bundle size the change introduces. @@ -265,7 +260,6 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set - `vue`: The public facing "full build" which includes both the runtime AND the compiler. - Private utility packages: - - `dts-test`: Contains type-only tests against generated dts files. - `sfc-playground`: The playground continuously deployed at https://play.vuejs.org. To run the playground locally, use [`nr dev-sfc`](#nr-dev-sfc). @@ -290,27 +284,39 @@ This is made possible via several configurations: ```mermaid flowchart LR + vue["vue"] compiler-sfc["@vue/compiler-sfc"] compiler-dom["@vue/compiler-dom"] + compiler-vapor["@vue/compiler-vapor"] compiler-core["@vue/compiler-core"] - vue["vue"] runtime-dom["@vue/runtime-dom"] + runtime-vapor["@vue/runtime-vapor"] runtime-core["@vue/runtime-core"] reactivity["@vue/reactivity"] subgraph "Runtime Packages" runtime-dom --> runtime-core + runtime-vapor --> runtime-core runtime-core --> reactivity end subgraph "Compiler Packages" compiler-sfc --> compiler-core compiler-sfc --> compiler-dom + compiler-sfc --> compiler-vapor compiler-dom --> compiler-core + compiler-vapor --> compiler-core end + vue --> compiler-sfc vue ---> compiler-dom vue --> runtime-dom + vue --> compiler-vapor + vue --> runtime-vapor + + %% Highlight class + classDef highlight stroke:#35eb9a,stroke-width:3px; + class compiler-vapor,runtime-vapor highlight; ``` There are some rules to follow when importing across package boundaries: diff --git a/.github/maintenance.md b/.github/maintenance.md index b1fb550dd7a..7b0c2a33626 100644 --- a/.github/maintenance.md +++ b/.github/maintenance.md @@ -48,7 +48,6 @@ Depending on the type of the PR, different considerations need to be taken into - Performance: if a refactor PR claims to improve performance, there should be benchmarks showcasing said performance unless the improvement is self-explanatory. - Code quality / stylistic PRs: we should be conservative on merging this type PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it. - - For PRs in this category that are approved, do not merge immediately. Group them before releasing a new minor, after all feature-oriented PRs are merged. ### Reviewing a Feature @@ -56,7 +55,6 @@ Depending on the type of the PR, different considerations need to be taken into - Feature PRs should always have clear context and explanation on why the feature should be added, ideally in the form of an RFC. If the PR doesn't explain what real-world problem it is solving, ask the contributor to clarify. - Decide if the feature should require an RFC process. The line isn't always clear, but a rough criteria is whether it is augmenting an existing API vs. adding a new API. Some examples: - - Adding a new built-in component or directive is "significant" and definitely requires an RFC. - Template syntax additions like adding a new `v-on` modifier or a new `v-bind` syntax sugar are "substantial". It would be nice to have an RFC for it, but a detailed explanation on the use case and reasoning behind the design directly in the PR itself can be acceptable. - Small, low-impact additions like exposing a new utility type or adding a new app config option can be self-explanatory, but should still provide enough context in the PR. @@ -70,7 +68,6 @@ Depending on the type of the PR, different considerations need to be taken into - Implementation: code style should be consistent with the rest of the codebase, follow common best practices. Prefer code that is boring but easy to understand over "clever" code. - Size: bundle size matters. We have a GitHub action that compares the size change for every PR. We should always aim to realize the desired changes with the smallest amount of code size increase. - - Sometimes we need to compare the size increase vs. perceived benefits to decide whether a change is justifiable. Also take extra care to make sure added code can be tree-shaken if not needed. - Make sure to put dev-only code in `__DEV__` branches so they are tree-shakable. @@ -80,7 +77,6 @@ Depending on the type of the PR, different considerations need to be taken into - Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in @vue/shared are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`. - Performance - - Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code. - Potential Breakage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8c217f62c4..6b69e4727e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: branches: - main - minor + - vapor jobs: test: @@ -16,7 +17,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/.github/workflows/test.yml b/.github/workflows/test.yml index 1122eb35573..1202ef9c8b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,6 +80,32 @@ jobs: - name: verify treeshaking run: node scripts/verify-treeshaking.js + e2e-vapor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup cache for Chromium binary + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: chromium-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + + - run: pnpm install + - run: node node_modules/puppeteer/install.mjs + + - name: Run e2e tests + run: pnpm run test-e2e-vapor + lint-and-test-dts: runs-on: ubuntu-latest env: diff --git a/.gitignore b/.gitignore index 9dd21f59bf6..973c062daf7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ TODOs.md dts-build/packages *.tsbuildinfo *.tgz +packages-private/benchmark/reference diff --git a/.vscode/settings.json b/.vscode/settings.json index 302428290b9..7907859bb86 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "vitest.disableWorkspaceWarning": true } diff --git a/eslint.config.js b/eslint.config.js index b752b2e19f1..1f5128ec72f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -106,7 +106,7 @@ export default tseslint.config( // Packages targeting DOM { - files: ['packages/{vue,vue-compat,runtime-dom}/**'], + files: ['packages/{vue,vue-compat,runtime-dom,runtime-vapor}/**'], rules: { 'no-restricted-globals': ['error', ...NodeGlobals], }, @@ -126,6 +126,7 @@ export default tseslint.config( files: [ 'packages-private/template-explorer/**', 'packages-private/sfc-playground/**', + 'packages-private/local-playground/**', ], rules: { 'no-restricted-globals': ['error', ...NodeGlobals], @@ -152,6 +153,8 @@ export default tseslint.config( './*.{js,ts}', 'packages/*/*.js', 'packages/vue/*/*.js', + 'packages-private/benchmark/*', + 'packages-private/e2e-utils/*', ], rules: { 'no-restricted-globals': 'off', diff --git a/package.json b/package.json index 08bdc5ac908..e05a475cb50 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,18 @@ "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 .", "format-check": "prettier --check --cache .", "test": "vitest", - "test-unit": "vitest --project unit", + "test-unit": "vitest --project unit --project unit-jsdom", "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e", + "test-e2e-vapor": "pnpm run prepare-e2e-vapor && vitest --project e2e-vapor", + "prepare-e2e-vapor": "node scripts/build.js -f cjs+esm-bundler+esm-bundler-runtime && pnpm run -C packages-private/vapor-e2e-test build", "test-dts": "run-s build-dts test-dts-only", "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json", "test-coverage": "vitest run --project unit --coverage", @@ -29,19 +31,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 || node scripts/build.js -f 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 esm-browser-vapor\" \"dev vue -ipf esm-browser-vapor\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", + "dev-vapor": "pnpm -C packages-private/local-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-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-sfc-playground-self": "cd packages-private/sfc-playground && npm run build", + "build-sfc-playground": "run-s build-sfc-deps build-sfc-playground-self", + "build-sfc-deps": "node scripts/build.js -f ~global+global-runtime", + "build-sfc-playground-self": "pnpm run -C packages-private/sfc-playground build", "preinstall": "npx only-allow pnpm", "postinstall": "simple-git-hooks" }, @@ -74,6 +74,7 @@ "@types/node": "^22.16.0", "@types/semver": "^7.7.0", "@types/serve-handler": "^6.1.4", + "@vitest/ui": "^3.0.2", "@vitest/coverage-v8": "^3.1.4", "@vitest/eslint-plugin": "^1.2.1", "@vue/consolidate": "1.0.0", diff --git a/packages-private/benchmark/.gitignore b/packages-private/benchmark/.gitignore new file mode 100644 index 00000000000..484ab7e5c61 --- /dev/null +++ b/packages-private/benchmark/.gitignore @@ -0,0 +1 @@ +results/* diff --git a/packages-private/benchmark/client/App.vue b/packages-private/benchmark/client/App.vue new file mode 100644 index 00000000000..c85deea53ea --- /dev/null +++ b/packages-private/benchmark/client/App.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages-private/benchmark/client/AppVapor.vue b/packages-private/benchmark/client/AppVapor.vue new file mode 100644 index 00000000000..0fd284da3f4 --- /dev/null +++ b/packages-private/benchmark/client/AppVapor.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages-private/benchmark/client/data.ts b/packages-private/benchmark/client/data.ts new file mode 100644 index 00000000000..ea5de1451d8 --- /dev/null +++ b/packages-private/benchmark/client/data.ts @@ -0,0 +1,78 @@ +import { shallowRef } from 'vue' + +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/packages-private/benchmark/client/index.html b/packages-private/benchmark/client/index.html new file mode 100644 index 00000000000..c3ca4c53590 --- /dev/null +++ b/packages-private/benchmark/client/index.html @@ -0,0 +1,17 @@ + + + + + + Vue Vapor Benchmark + + + +
+ + + diff --git a/packages-private/benchmark/client/index.ts b/packages-private/benchmark/client/index.ts new file mode 100644 index 00000000000..a12f727a101 --- /dev/null +++ b/packages-private/benchmark/client/index.ts @@ -0,0 +1,5 @@ +if (import.meta.env.IS_VAPOR) { + import('./vapor') +} else { + import('./vdom') +} diff --git a/packages-private/benchmark/client/profiling.ts b/packages-private/benchmark/client/profiling.ts new file mode 100644 index 00000000000..ee4f38b6090 --- /dev/null +++ b/packages-private/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' + +declare global { + var doProfile: boolean + var reactivity: boolean + var recordTime: boolean + var 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/packages-private/benchmark/client/vapor.ts b/packages-private/benchmark/client/vapor.ts new file mode 100644 index 00000000000..2574da9dea1 --- /dev/null +++ b/packages-private/benchmark/client/vapor.ts @@ -0,0 +1,4 @@ +import { createVaporApp } from 'vue' +import App from './AppVapor.vue' + +createVaporApp(App as any).mount('#app') diff --git a/packages-private/benchmark/client/vdom.ts b/packages-private/benchmark/client/vdom.ts new file mode 100644 index 00000000000..01433bca2ac --- /dev/null +++ b/packages-private/benchmark/client/vdom.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/packages-private/benchmark/index.js b/packages-private/benchmark/index.js new file mode 100644 index 00000000000..3af47eaf499 --- /dev/null +++ b/packages-private/benchmark/index.js @@ -0,0 +1,393 @@ +// @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, + noMinify, + reference, + }, +} = 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', + }, + noMinify: { + type: 'boolean', + }, + reference: { + type: 'boolean', + short: 'r', + }, + }, +}) + +const port = +(/** @type {string}*/ (portStr)) +const count = +(/** @type {string}*/ (countStr)) +const warmupCount = +(/** @type {string}*/ (warmupCountStr)) +const sha = await getSha(true) + +if (!skipLib && !reference) { + await buildLib() +} +if (!skipApp && !reference) { + 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 [{ ok }, { ok: ok2 }, { ok: ok3 }] = await Promise.all([ + exec( + 'pnpm', + `run --silent build shared compiler-core compiler-dom -pf cjs`.split(' '), + options, + ), + exec( + 'pnpm', + 'run --silent build compiler-sfc compiler-ssr compiler-vapor -f cjs'.split( + ' ', + ), + options, + ), + exec( + 'pnpm', + `run --silent build shared reactivity runtime-core runtime-dom runtime-vapor vue -f esm-bundler+esm-bundler-runtime`.split( + ' ', + ), + options, + ), + ]) + + if (!ok || !ok2 || !ok3) { + 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`), + ) + + process.env.NODE_ENV = 'production' + + const CompilerSFC = await import( + '../../packages/compiler-sfc/dist/compiler-sfc.cjs.js' + ) + + const runtimePath = path.resolve( + import.meta.dirname, + '../../packages/vue/dist/vue.runtime.esm-bundler.js', + ) + + const mode = isVapor ? 'vapor' : 'vdom' + await build({ + root: './client', + base: `/${mode}`, + define: { + 'import.meta.env.IS_VAPOR': String(isVapor), + }, + build: { + minify: !noMinify, + outDir: path.resolve('./client/dist', mode), + rollupOptions: { + onwarn(log, handler) { + if (log.code === 'INVALID_ANNOTATION') return + handler(log) + }, + }, + }, + resolve: { + alias: { + vue: runtimePath, + }, + }, + clearScreen: false, + plugins: [ + Vue({ + compiler: CompilerSFC, + }), + ], + }) +} + +function startServer() { + const server = connect() + .use(sirv(reference ? './reference' : './client/dist', { dev: true })) + .listen(port) + printPort() + 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 {boolean} isVapor + */ +function getURL(isVapor) { + return `http://localhost:${port}/${reference ? '' : isVapor ? 'vapor' : 'vdom'}/` +} + +/** + * + * @param {import('puppeteer').Browser} browser + * @param {boolean} isVapor + */ +async function doBench(browser, isVapor) { + const mode = reference ? `reference` : isVapor ? 'vapor' : 'vdom' + console.info('\n\nmode:', mode) + + const page = await browser.newPage() + page.emulateCPUThrottling(4) + await page.goto(getURL(isVapor), { + 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) > a`) + 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) +} + +function printPort() { + const vaporLink = !noVapor + ? `\n${reference ? `Reference` : `Vapor`}: ${colors.blue(getURL(true))}` + : '' + const vdomLink = vdom ? `\nvDom: ${colors.blue(getURL(false))}` : '' + console.info(`\n\nServer started at`, vaporLink, vdomLink) +} diff --git a/packages-private/benchmark/package.json b/packages-private/benchmark/package.json new file mode 100644 index 00000000000..e6eb08e9539 --- /dev/null +++ b/packages-private/benchmark/package.json @@ -0,0 +1,20 @@ +{ + "name": "benchmark", + "version": "0.0.0", + "author": "三咲智子 Kevin Deng ", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "pnpm start --noMinify --skipBench --vdom", + "start": "node index.js" + }, + "dependencies": { + "@vitejs/plugin-vue": "catalog:", + "connect": "^3.7.0", + "sirv": "^2.0.4", + "vite": "catalog:" + }, + "devDependencies": { + "@types/connect": "^3.4.38" + } +} diff --git a/packages-private/benchmark/tsconfig.json b/packages-private/benchmark/tsconfig.json new file mode 100644 index 00000000000..4a32149504f --- /dev/null +++ b/packages-private/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/runtime-with-vapor.ts"], + "@vue/*": ["../packages/*/src"] + } + }, + "include": ["**/*"] +} diff --git a/packages-private/local-playground/index.html b/packages-private/local-playground/index.html new file mode 100644 index 00000000000..a01ba96dc0b --- /dev/null +++ b/packages-private/local-playground/index.html @@ -0,0 +1,12 @@ + + + + + + Vue Vapor + + +
+ + + diff --git a/packages-private/local-playground/package.json b/packages-private/local-playground/package.json new file mode 100644 index 00000000000..37c9818a706 --- /dev/null +++ b/packages-private/local-playground/package.json @@ -0,0 +1,22 @@ +{ + "name": "playground", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "node ./setup/vite.js", + "build": "vite build -c vite.prod.config.ts", + "prepreview": "cd ../ && pnpm run build runtime-vapor -f esm-bundler", + "preview": "pnpm run build && vite preview -c vite.prod.config.ts" + }, + "dependencies": { + "@vueuse/core": "^11.1.0", + "vue": "workspace:*" + }, + "devDependencies": { + "@vitejs/plugin-vue": "catalog:", + "@vue/compiler-sfc": "workspace:*", + "vite": "catalog:", + "vite-hyper-config": "^0.4.0", + "vite-plugin-inspect": "^0.8.7" + } +} diff --git a/packages-private/local-playground/setup/dev.js b/packages-private/local-playground/setup/dev.js new file mode 100644 index 00000000000..72a7ed4a835 --- /dev/null +++ b/packages-private/local-playground/setup/dev.js @@ -0,0 +1,66 @@ +// @ts-check +import path from 'node:path' + +const resolve = (/** @type {string} */ p) => + path.resolve(import.meta.dirname, '../../../packages', p) + +/** + * @param {Object} [env] + * @param {boolean} [env.browser] + * @returns {import('vite').Plugin} + */ +export function DevPlugin({ browser = false } = {}) { + return { + name: 'dev-plugin', + config() { + return { + resolve: { + alias: { + vue: resolve('vue/src/runtime-with-vapor.ts'), + + '@vue/runtime-core': resolve('runtime-core/src'), + '@vue/runtime-dom': resolve('runtime-dom/src'), + '@vue/runtime-vapor': resolve('runtime-vapor/src'), + + '@vue/compiler-core': resolve('compiler-core/src'), + '@vue/compiler-dom': resolve('compiler-dom/src'), + '@vue/compiler-vapor': resolve('compiler-vapor/src'), + + '@vue/compiler-sfc': resolve('compiler-sfc/src'), + '@vue/compiler-ssr': resolve('compiler-ssr/src'), + + '@vue/reactivity': resolve('reactivity/src'), + '@vue/shared': resolve('shared/src'), + '@vue/runtime-shared': resolve('runtime-shared/src'), + }, + }, + define: { + __COMMIT__: `"__COMMIT__"`, + __VERSION__: `"0.0.0"`, + __DEV__: `true`, + // this is only used during Vue's internal tests + __TEST__: `false`, + // If the build is expected to run directly in the browser (global / esm builds) + __BROWSER__: String(browser), + __GLOBAL__: String(false), + __ESM_BUNDLER__: String(true), + __ESM_BROWSER__: String(false), + // is targeting Node (SSR)? + __NODE_JS__: String(false), + // need SSR-specific branches? + __SSR__: String(false), + __BENCHMARK__: 'false', + + // 2.x compat build + __COMPAT__: String(false), + + // feature flags + __FEATURE_SUSPENSE__: `true`, + __FEATURE_OPTIONS_API__: `true`, + __FEATURE_PROD_DEVTOOLS__: `false`, + __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `false`, + }, + } + }, + } +} diff --git a/packages-private/local-playground/setup/vite.js b/packages-private/local-playground/setup/vite.js new file mode 100644 index 00000000000..e6b9a927206 --- /dev/null +++ b/packages-private/local-playground/setup/vite.js @@ -0,0 +1,14 @@ +// @ts-check + +import { startVite } from 'vite-hyper-config' +import { DevPlugin } from './dev.js' + +startVite( + undefined, + { plugins: [DevPlugin()] }, + { + deps: { + inline: ['@vitejs/plugin-vue'], + }, + }, +) diff --git a/packages-private/local-playground/src/.gitignore b/packages-private/local-playground/src/.gitignore new file mode 100644 index 00000000000..2e1b9cfb780 --- /dev/null +++ b/packages-private/local-playground/src/.gitignore @@ -0,0 +1,5 @@ +* +!.gitignore +!App.vue +!main.ts +!style.css diff --git a/packages-private/local-playground/src/App.vue b/packages-private/local-playground/src/App.vue new file mode 100644 index 00000000000..b6124e39a35 --- /dev/null +++ b/packages-private/local-playground/src/App.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages-private/local-playground/src/main.ts b/packages-private/local-playground/src/main.ts new file mode 100644 index 00000000000..9d682d9ffb6 --- /dev/null +++ b/packages-private/local-playground/src/main.ts @@ -0,0 +1 @@ +import './_entry' diff --git a/packages-private/local-playground/src/style.css b/packages-private/local-playground/src/style.css new file mode 100644 index 00000000000..832e61618b0 --- /dev/null +++ b/packages-private/local-playground/src/style.css @@ -0,0 +1,6 @@ +.red { + color: red; +} +.green { + color: green; +} diff --git a/packages-private/local-playground/tsconfig.json b/packages-private/local-playground/tsconfig.json new file mode 100644 index 00000000000..8ed98192084 --- /dev/null +++ b/packages-private/local-playground/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "isolatedDeclarations": false, + "allowJs": true + }, + "include": ["./**/*", "../../packages/*/src"] +} diff --git a/packages-private/local-playground/vite.config.ts b/packages-private/local-playground/vite.config.ts new file mode 100644 index 00000000000..8b4b1a423f7 --- /dev/null +++ b/packages-private/local-playground/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import Inspect from 'vite-plugin-inspect' +import { DevPlugin } from './setup/dev' +import Vue from '@vitejs/plugin-vue' +import * as CompilerSFC from '@vue/compiler-sfc' + +export default defineConfig({ + clearScreen: false, + plugins: [ + Vue({ + compiler: CompilerSFC, + }), + DevPlugin(), + Inspect(), + ], + optimizeDeps: { + exclude: ['@vueuse/core'], + }, +}) diff --git a/packages-private/local-playground/vite.prod.config.ts b/packages-private/local-playground/vite.prod.config.ts new file mode 100644 index 00000000000..6bafb7a772a --- /dev/null +++ b/packages-private/local-playground/vite.prod.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import Vue from '@vitejs/plugin-vue' +import * as CompilerSFC from '@vue/compiler-sfc' + +export default defineConfig({ + build: { + modulePreload: false, + target: 'esnext', + minify: 'terser', + terserOptions: { + format: { comments: false }, + compress: { + pure_getters: true, + }, + }, + }, + clearScreen: false, + plugins: [ + Vue({ + compiler: CompilerSFC, + features: { + optionsAPI: false, + }, + }), + ], +}) diff --git a/packages-private/sfc-playground/src/App.vue b/packages-private/sfc-playground/src/App.vue index 455137ba6f6..be4b136012c 100644 --- a/packages-private/sfc-playground/src/App.vue +++ b/packages-private/sfc-playground/src/App.vue @@ -1,6 +1,12 @@ + + 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 08da34b173e..c546f4a298d 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.2", "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-private/tsconfig.json b/packages-private/tsconfig.json index 1c287a7500c..37a38f53fc6 100644 --- a/packages-private/tsconfig.json +++ b/packages-private/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "isolatedDeclarations": false }, - "include": ["."] + "include": [".", "../packages/vue/__tests__/e2e/e2eUtils.ts"] } diff --git a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts new file mode 100644 index 00000000000..3de8392e5e2 --- /dev/null +++ b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts @@ -0,0 +1,195 @@ +import path from 'node:path' +import { + E2E_TIMEOUT, + setupPuppeteer, +} from '../../../packages/vue/__tests__/e2e/e2eUtils' +import connect from 'connect' +import sirv from 'sirv' + +describe('e2e: todomvc', () => { + const { + page, + click, + isVisible, + count, + text, + value, + isChecked, + isFocused, + classList, + enterValue, + clearValue, + timeout, + } = setupPuppeteer() + + let server: any + const port = '8194' + beforeAll(() => { + server = connect() + .use(sirv(path.resolve(import.meta.dirname, '../dist'))) + .listen(port) + process.on('SIGTERM', () => server && server.close()) + }) + + afterAll(() => { + server.close() + }) + + async function removeItemAt(n: number) { + const item = (await page().$('.todo:nth-child(' + n + ')'))! + const itemBBox = (await item.boundingBox())! + await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10) + await click('.todo:nth-child(' + n + ') .destroy') + } + + test( + 'vapor', + async () => { + const baseUrl = `http://localhost:${port}/todomvc/` + await page().goto(baseUrl) + + expect(await isVisible('.main')).toBe(false) + expect(await isVisible('.footer')).toBe(false) + expect(await count('.filters .selected')).toBe(1) + expect(await text('.filters .selected')).toBe('All') + expect(await count('.todo')).toBe(0) + + await enterValue('.new-todo', 'test') + expect(await count('.todo')).toBe(1) + expect(await isVisible('.todo .edit')).toBe(false) + expect(await text('.todo label')).toBe('test') + expect(await text('.todo-count strong')).toBe('1') + expect(await isChecked('.todo .toggle')).toBe(false) + expect(await isVisible('.main')).toBe(true) + expect(await isVisible('.footer')).toBe(true) + expect(await isVisible('.clear-completed')).toBe(false) + expect(await value('.new-todo')).toBe('') + + await enterValue('.new-todo', 'test2') + expect(await count('.todo')).toBe(2) + expect(await text('.todo:nth-child(2) label')).toBe('test2') + expect(await text('.todo-count strong')).toBe('2') + + // toggle + await click('.todo .toggle') + expect(await count('.todo.completed')).toBe(1) + expect(await classList('.todo:nth-child(1)')).toContain('completed') + expect(await text('.todo-count strong')).toBe('1') + expect(await isVisible('.clear-completed')).toBe(true) + + await enterValue('.new-todo', 'test3') + expect(await count('.todo')).toBe(3) + expect(await text('.todo:nth-child(3) label')).toBe('test3') + expect(await text('.todo-count strong')).toBe('2') + + await enterValue('.new-todo', 'test4') + await enterValue('.new-todo', 'test5') + expect(await count('.todo')).toBe(5) + expect(await text('.todo-count strong')).toBe('4') + + // toggle more + await click('.todo:nth-child(4) .toggle') + await click('.todo:nth-child(5) .toggle') + expect(await count('.todo.completed')).toBe(3) + expect(await text('.todo-count strong')).toBe('2') + + // remove + await removeItemAt(1) + expect(await count('.todo')).toBe(4) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('2') + await removeItemAt(2) + expect(await count('.todo')).toBe(3) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('1') + + // remove all + await click('.clear-completed') + expect(await count('.todo')).toBe(1) + expect(await text('.todo label')).toBe('test2') + expect(await count('.todo.completed')).toBe(0) + expect(await text('.todo-count strong')).toBe('1') + expect(await isVisible('.clear-completed')).toBe(false) + + // prepare to test filters + await enterValue('.new-todo', 'test') + await enterValue('.new-todo', 'test') + await click('.todo:nth-child(2) .toggle') + await click('.todo:nth-child(3) .toggle') + + // active filter + await click('.filters li:nth-child(2) a') + await timeout(1) + expect(await count('.todo')).toBe(1) + expect(await count('.todo.completed')).toBe(0) + // add item with filter active + await enterValue('.new-todo', 'test') + expect(await count('.todo')).toBe(2) + + // completed filter + await click('.filters li:nth-child(3) a') + await timeout(1) + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(2) + + // filter on page load + await page().goto(`${baseUrl}#active`) + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(0) + expect(await text('.todo-count strong')).toBe('2') + + // completed on page load + await page().goto(`${baseUrl}#completed`) + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('2') + + // toggling with filter active + await click('.todo .toggle') + expect(await count('.todo')).toBe(1) + await click('.filters li:nth-child(2) a') + await timeout(1) + expect(await count('.todo')).toBe(3) + await click('.todo .toggle') + expect(await count('.todo')).toBe(2) + + // editing triggered by blur + await click('.filters li:nth-child(1) a') + await timeout(1) + await click('.todo:nth-child(1) label', { clickCount: 2 }) + expect(await count('.todo.editing')).toBe(1) + expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true) + await clearValue('.todo:nth-child(1) .edit') + await page().type('.todo:nth-child(1) .edit', 'edited!') + await click('.new-todo') // blur + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited!') + + // editing triggered by enter + await click('.todo label', { clickCount: 2 }) + await enterValue('.todo:nth-child(1) .edit', 'edited again!') + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited again!') + + // cancel + await click('.todo label', { clickCount: 2 }) + await clearValue('.todo:nth-child(1) .edit') + await page().type('.todo:nth-child(1) .edit', 'edited!') + await page().keyboard.press('Escape') + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited again!') + + // empty value should remove + await click('.todo label', { clickCount: 2 }) + await enterValue('.todo:nth-child(1) .edit', ' ') + expect(await count('.todo')).toBe(3) + + // toggle all + await click('.toggle-all+label') + expect(await count('.todo.completed')).toBe(3) + await click('.toggle-all+label') + expect(await count('.todo:not(.completed)')).toBe(3) + }, + E2E_TIMEOUT, + ) +}) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts new file mode 100644 index 00000000000..360f48085a1 --- /dev/null +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -0,0 +1,84 @@ +import path from 'node:path' +import { + E2E_TIMEOUT, + setupPuppeteer, +} from '../../../packages/vue/__tests__/e2e/e2eUtils' +import connect from 'connect' +import sirv from 'sirv' + +describe('vdom / vapor interop', () => { + const { page, click, text, enterValue } = setupPuppeteer() + + let server: any + const port = '8193' + beforeAll(() => { + server = connect() + .use(sirv(path.resolve(import.meta.dirname, '../dist'))) + .listen(port) + process.on('SIGTERM', () => server && server.close()) + }) + + afterAll(() => { + server.close() + }) + + test( + 'should work', + async () => { + const baseUrl = `http://localhost:${port}/interop/` + await page().goto(baseUrl) + + expect(await text('.vapor > h2')).toContain('Vapor component in VDOM') + + expect(await text('.vapor-prop')).toContain('hello') + + const t = await text('.vdom-slot-in-vapor-default') + expect(t).toContain('slot prop: slot prop') + expect(t).toContain('component prop: hello') + + await click('.change-vdom-slot-in-vapor-prop') + expect(await text('.vdom-slot-in-vapor-default')).toContain( + 'slot prop: changed', + ) + + expect(await text('.vdom-slot-in-vapor-test')).toContain('A test slot') + + await click('.toggle-vdom-slot-in-vapor') + expect(await text('.vdom-slot-in-vapor-test')).toContain( + 'fallback content', + ) + + await click('.toggle-vdom-slot-in-vapor') + expect(await text('.vdom-slot-in-vapor-test')).toContain('A test slot') + + expect(await text('.vdom > h2')).toContain('VDOM component in Vapor') + + expect(await text('.vdom-prop')).toContain('hello') + + const tt = await text('.vapor-slot-in-vdom-default') + expect(tt).toContain('slot prop: slot prop') + expect(tt).toContain('component prop: hello') + + await click('.change-vapor-slot-in-vdom-prop') + expect(await text('.vapor-slot-in-vdom-default')).toContain( + 'slot prop: changed', + ) + + expect(await text('.vapor-slot-in-vdom-test')).toContain('fallback') + + await click('.toggle-vapor-slot-in-vdom-default') + expect(await text('.vapor-slot-in-vdom-default')).toContain( + 'default slot fallback', + ) + + await click('.toggle-vapor-slot-in-vdom-default') + + await enterValue('input', 'bye') + expect(await text('.vapor-prop')).toContain('bye') + expect(await text('.vdom-slot-in-vapor-default')).toContain('bye') + expect(await text('.vdom-prop')).toContain('bye') + expect(await text('.vapor-slot-in-vdom-default')).toContain('bye') + }, + E2E_TIMEOUT, + ) +}) diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html new file mode 100644 index 00000000000..7dc205e5ab0 --- /dev/null +++ b/packages-private/vapor-e2e-test/index.html @@ -0,0 +1,2 @@ +VDOM / Vapor interop +Vapor TodoMVC diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue new file mode 100644 index 00000000000..772a6989dd7 --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/VaporComp.vue new file mode 100644 index 00000000000..88a60c782c0 --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/VaporComp.vue @@ -0,0 +1,50 @@ + + + diff --git a/packages-private/vapor-e2e-test/interop/VdomComp.vue b/packages-private/vapor-e2e-test/interop/VdomComp.vue new file mode 100644 index 00000000000..30ec1b2eeb5 --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/VdomComp.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages-private/vapor-e2e-test/interop/index.html b/packages-private/vapor-e2e-test/interop/index.html new file mode 100644 index 00000000000..79052a023ba --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/index.html @@ -0,0 +1,2 @@ + +
diff --git a/packages-private/vapor-e2e-test/interop/main.ts b/packages-private/vapor-e2e-test/interop/main.ts new file mode 100644 index 00000000000..d5d6d7dcf8c --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/main.ts @@ -0,0 +1,4 @@ +import { createApp, vaporInteropPlugin } from 'vue' +import App from './App.vue' + +createApp(App).use(vaporInteropPlugin).mount('#app') diff --git a/packages-private/vapor-e2e-test/package.json b/packages-private/vapor-e2e-test/package.json new file mode 100644 index 00000000000..66ea0457ec9 --- /dev/null +++ b/packages-private/vapor-e2e-test/package.json @@ -0,0 +1,18 @@ +{ + "name": "vapor-e2e-test", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build" + }, + "devDependencies": { + "@types/connect": "^3.4.38", + "@vitejs/plugin-vue": "catalog:", + "connect": "^3.7.0", + "sirv": "^2.0.4", + "vite": "catalog:", + "vue": "workspace:*" + } +} diff --git a/packages-private/vapor-e2e-test/todomvc/App.vue b/packages-private/vapor-e2e-test/todomvc/App.vue new file mode 100644 index 00000000000..910ada51093 --- /dev/null +++ b/packages-private/vapor-e2e-test/todomvc/App.vue @@ -0,0 +1,228 @@ + + + diff --git a/packages-private/vapor-e2e-test/todomvc/index.html b/packages-private/vapor-e2e-test/todomvc/index.html new file mode 100644 index 00000000000..79052a023ba --- /dev/null +++ b/packages-private/vapor-e2e-test/todomvc/index.html @@ -0,0 +1,2 @@ + +
diff --git a/packages-private/vapor-e2e-test/todomvc/main.ts b/packages-private/vapor-e2e-test/todomvc/main.ts new file mode 100644 index 00000000000..42497ab518d --- /dev/null +++ b/packages-private/vapor-e2e-test/todomvc/main.ts @@ -0,0 +1,5 @@ +import { createVaporApp } from 'vue' +import App from './App.vue' +import 'todomvc-app-css/index.css' + +createVaporApp(App).mount('#app') diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts new file mode 100644 index 00000000000..1e29a4dbd13 --- /dev/null +++ b/packages-private/vapor-e2e-test/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import Vue from '@vitejs/plugin-vue' +import * as CompilerSFC from 'vue/compiler-sfc' +import { resolve } from 'node:path' + +export default defineConfig({ + plugins: [ + Vue({ + compiler: CompilerSFC, + }), + ], + build: { + rollupOptions: { + input: { + interop: resolve(import.meta.dirname, 'interop/index.html'), + todomvc: resolve(import.meta.dirname, 'todomvc/index.html'), + }, + }, + }, +}) 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..6ede6bd0386 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -12,6 +12,7 @@ import type { Program, } from '@babel/types' import { walk } from 'estree-walker' +import { type BindingMetadata, BindingTypes } from './options' /** * Return value indicates whether the AST walked can be a constant @@ -308,8 +309,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 +511,77 @@ 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, bindings: BindingMetadata): boolean { + if (isStaticNode(node)) return true + + node = unwrapTSNode(node) + switch (node.type) { + case 'Identifier': + const type = bindings[node.name] + return type === BindingTypes.LITERAL_CONST + 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, bindings) + // { foo: 1 } + return ( + (!prop.computed || isConstantNode(prop.key, bindings)) && + isConstantNode(prop.value, bindings) + ) + }) + 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, bindings) + // [1, 2] + return isConstantNode(element, bindings) + }) + } + return false +} diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 6b4559fabb2..99020bcf1ae 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 @@ -400,6 +416,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..e54b0c3a498 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -17,21 +17,26 @@ export { createTransformContext, traverseNode, createStructuralDirectiveTransform, + getSelfName, type NodeTransform, type StructuralDirectiveTransform, type DirectiveTransform, } 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 +57,7 @@ export { transformExpression, processExpression, stringifyExpression, + isLiteralWhitelisted, } from './transforms/transformExpression' export { buildSlots, @@ -75,4 +81,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/transform.ts b/packages/compiler-core/src/transform.ts index 9d8fd842935..10121fb5d5c 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -123,6 +123,11 @@ export interface TransformContext filters?: Set } +export function getSelfName(filename: string): string | null { + const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/) + return nameMatch ? capitalize(camelize(nameMatch[1])) : null +} + export function createTransformContext( root: RootNode, { @@ -150,11 +155,10 @@ export function createTransformContext( compatConfig, }: TransformOptions, ): TransformContext { - const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/) const context: TransformContext = { // options filename, - selfName: nameMatch && capitalize(camelize(nameMatch[1])), + selfName: getSelfName(filename), prefixIdentifiers, hoistStatic, hmr, 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 ( + `) 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/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts index 265655e47ef..82b8cf98f11 100644 --- a/packages/compiler-sfc/__tests__/parse.spec.ts +++ b/packages/compiler-sfc/__tests__/parse.spec.ts @@ -381,6 +381,17 @@ h1 { color: red } }) }) + describe('vapor mode', () => { + test('on empty script', () => { + const { descriptor } = parse(``) + expect(descriptor.vapor).toBe(true) + }) + test('on template', () => { + const { descriptor } = parse(``) + expect(descriptor.vapor).toBe(true) + }) + }) + describe('warnings', () => { function assertWarning(errors: Error[], msg: string) { expect(errors.some(e => e.message.match(msg))).toBe(true) diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 45468177a94..55e12af299d 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 2e1e0ec34de..54ca260bdd6 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' @@ -18,6 +19,7 @@ import type { Declaration, ExportSpecifier, Identifier, + LVal, Node, ObjectPattern, Statement, @@ -129,6 +131,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 { @@ -173,6 +179,8 @@ 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 + const ssr = options.templateOptions?.ssr if (!scriptSetup) { if (!script) { @@ -540,7 +548,7 @@ export function compileScript( } // defineProps - const isDefineProps = processDefineProps(ctx, init, decl.id) + const isDefineProps = processDefineProps(ctx, init, decl.id as LVal) if (ctx.propsDestructureRestId) { setupBindings[ctx.propsDestructureRestId] = BindingTypes.SETUP_REACTIVE_CONST @@ -548,10 +556,10 @@ export function compileScript( // defineEmits const isDefineEmits = - !isDefineProps && processDefineEmits(ctx, init, decl.id) + !isDefineProps && processDefineEmits(ctx, init, decl.id as LVal) !isDefineEmits && - (processDefineSlots(ctx, init, decl.id) || - processDefineModel(ctx, init, decl.id)) + (processDefineSlots(ctx, init, decl.id as LVal) || + processDefineModel(ctx, init, decl.id as LVal)) if ( isDefineProps && @@ -747,7 +755,7 @@ export function compileScript( if ( sfc.cssVars.length && // no need to do this when targeting SSR - !options.templateOptions?.ssr + !ssr ) { ctx.helperImports.add(CSS_VARS_HELPER) ctx.helperImports.add('unref') @@ -858,12 +866,12 @@ export function compileScript( } else { // inline mode if (sfc.template && !sfc.template.src) { - if (options.templateOptions && options.templateOptions.ssr) { + if (ssr) { hasInlinedSsrRenderFn = true } // inline render function mode - we are going to compile the template and // inline it right here - const { code, ast, preamble, tips, errors, map } = compileTemplate({ + const { code, preamble, tips, errors, helpers, map } = compileTemplate({ filename, ast: sfc.template.ast, source: sfc.template.content, @@ -873,6 +881,7 @@ export function compileScript( scoped: sfc.styles.some(s => s.scoped), isProd: options.isProd, ssrCssVars: sfc.cssVars, + vapor, compilerOptions: { ...(options.templateOptions && options.templateOptions.compilerOptions), @@ -909,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 @@ -929,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 && !ssr ? `` : `return `}${returned}\n}\n\n`, + ) } // 10. finalize default export @@ -978,13 +991,17 @@ export function compileScript( ctx.s.prependLeft( startOffset, `\n${genDefaultAs} /*@__PURE__*/${ctx.helper( - `defineComponent`, + vapor && !ssr ? `defineVaporComponent` : `defineComponent`, )}({${def}${runtimeOptions}\n ${ hasAwait ? `async ` : `` }setup(${args}) {\n${exposeCall}`, ) ctx.s.appendRight(endOffset, `})`) } else { + // in TS, defineVaporComponent adds the option already + if (vapor) { + runtimeOptions += `\n __vapor: true,` + } if (defaultExport || definedOptions) { // without TS, can't rely on rest spread, so we use Object.assign // export default Object.assign(__default__, { ... }) @@ -1263,43 +1280,6 @@ function canNeverBeRef(node: Node, userReactiveImport?: string): boolean { } } -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 mergeSourceMaps( scriptMap: RawSourceMap, templateMap: RawSourceMap, diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index b043cf813d7..29d1853d2d6 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,11 @@ function doCompileTemplate({ const shortId = id.replace(/^data-v-/, '') const longId = `data-v-${shortId}` - const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM + const defaultCompiler = ssr + ? (CompilerSSR as TemplateCompiler) + : vapor + ? (CompilerVapor as TemplateCompiler) + : CompilerDOM compiler = compiler || defaultCompiler if (compiler !== defaultCompiler) { @@ -227,25 +238,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 +287,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..98b08a20815 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), } @@ -159,8 +162,9 @@ export function parse( ignoreEmpty && node.tag !== 'template' && isEmpty(node) && - !hasSrc(node) + !hasAttr(node, 'src') ) { + descriptor.vapor ||= hasAttr(node, 'vapor') return } switch (node.tag) { @@ -171,6 +175,7 @@ export function parse( source, false, ) as SFCTemplateBlock) + descriptor.vapor ||= !!templateBlock.attrs.vapor if (!templateBlock.attrs.src) { templateBlock.ast = createRoot(node.children, source) @@ -195,7 +200,8 @@ export function parse( break case 'script': const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock - const isSetup = !!scriptBlock.attrs.setup + descriptor.vapor ||= !!scriptBlock.attrs.vapor + const isSetup = !!(scriptBlock.attrs.setup || scriptBlock.attrs.vapor) if (isSetup && !descriptor.scriptSetup) { descriptor.scriptSetup = scriptBlock break @@ -404,13 +410,8 @@ function padContent( } } -function hasSrc(node: ElementNode) { - return node.props.some(p => { - if (p.type !== NodeTypes.ATTRIBUTE) { - return false - } - return p.name === 'src' - }) +function hasAttr(node: ElementNode, name: string) { + return node.props.some(p => p.type === NodeTypes.ATTRIBUTE && p.name === name) } /** 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-vapor/LICENSE b/packages/compiler-vapor/LICENSE new file mode 100644 index 00000000000..15f1f7e7a49 --- /dev/null +++ b/packages/compiler-vapor/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018-present, Yuxi (Evan) You + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/compiler-vapor/README.md b/packages/compiler-vapor/README.md new file mode 100644 index 00000000000..c2f11323d19 --- /dev/null +++ b/packages/compiler-vapor/README.md @@ -0,0 +1 @@ +# @vue/compiler-vapor diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap new file mode 100644 index 00000000000..b10a98d32cb --- /dev/null +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -0,0 +1,310 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compile > bindings 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + const x0 = _child(n0) + _renderEffect(() => _setText(x0, "count is " + _toDisplayString(_ctx.count) + ".")) + return n0 +}" +`; + +exports[`compile > custom directive > basic 1`] = ` +"import { resolveDirective as _resolveDirective, withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _directive_test = _resolveDirective("test") + const _directive_hello = _resolveDirective("hello") + const n0 = t0() + _withVaporDirectives(n0, [[_directive_test], [_directive_hello, void 0, void 0, { world: true }]]) + return n0 +}" +`; + +exports[`compile > custom directive > component 1`] = ` +"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const _component_Bar = _resolveComponent("Bar") + const _component_Comp = _resolveComponent("Comp") + const _directive_hello = _resolveDirective("hello") + const _directive_test = _resolveDirective("test") + const n4 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createIf(() => (true), () => { + const n3 = t0() + _setInsertionState(n3) + const n2 = _createComponentWithFallback(_component_Bar) + _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]]) + return n3 + }) + return n0 + } + }, true) + _withVaporDirectives(n4, [[_directive_test]]) + return n4 +}" +`; + +exports[`compile > directives > custom directive > basic 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > binding value 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > dynamic parameters 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > modifiers 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > modifiers w/o binding 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, void 0, void 0, { "foo-bar": true }]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > static parameters 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo"]]) + return n0 +}" +`; + +exports[`compile > directives > custom directive > static parameters and modifiers 1`] = ` +"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]]) + return n0 +}" +`; + +exports[`compile > directives > v-cloak > basic 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
test
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compile > directives > v-pre > basic 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
{{ bar }}
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + return n0 +}" +`; + +exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` +"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
{{ bar }}
") +const t1 = _template("
") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const _component_Comp = _resolveComponent("Comp") + const n0 = t0() + const n3 = t1() + const n2 = _child(n3) + _setInsertionState(n3, 0) + const n1 = _createComponentWithFallback(_component_Comp) + _renderEffect(() => { + _setProp(n3, "id", _ctx.foo) + _setText(n2, _toDisplayString(_ctx.bar)) + }) + return [n0, n3] +}" +`; + +exports[`compile > dynamic root 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const n0 = t0() + _setText(n0, _toDisplayString(1) + _toDisplayString(2)) + return n0 +}" +`; + +exports[`compile > dynamic root nodes and interpolation 1`] = ` +"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + const x0 = _child(n0) + n0.$evtclick = e => _ctx.handleClick(e) + _renderEffect(() => { + const _count = _ctx.count + _setProp(n0, "id", _count) + _setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count)) + }) + return n0 +}" +`; + +exports[`compile > execution order > basic 1`] = ` +"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + const x0 = _child(n0) + _renderEffect(() => { + _setProp(n0, "id", _ctx.foo) + _setText(x0, _toDisplayString(_ctx.bar)) + }) + return n0 +}" +`; + +exports[`compile > execution order > with v-once 1`] = ` +"import { child as _child, next as _next, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("

", true) + +export function render(_ctx) { + const n3 = t0() + const n0 = _child(n3) + const n1 = _next(n0) + const n2 = _nthChild(n3, 3) + const x0 = _child(n0) + _setText(x0, _toDisplayString(_ctx.foo)) + _renderEffect(() => { + _setText(n1, " " + _toDisplayString(_ctx.bar)) + _setText(n2, " " + _toDisplayString(_ctx.baz)) + }) + return n3 +}" +`; + +exports[`compile > expression parsing > interpolation 1`] = ` +" + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(a + b.value))) + return n0 +" +`; + +exports[`compile > expression parsing > v-bind 1`] = ` +" + const n0 = t0() + _renderEffect(() => { + const _key = key.value + _setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true) + }) + return n0 +" +`; + +exports[`compile > fragment 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("

") +const t1 = _template("") +const t2 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t1() + const n2 = t2() + return [n0, n1, n2] +}" +`; + +exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = ` +"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("
", true) + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n6 = t1() + const n5 = _next(_child(n6)) + const n7 = _nthChild(n6, 3) + const p0 = _next(n7) + const n4 = _child(p0) + _setInsertionState(n6, n5) + const n0 = _createComponentWithFallback(_component_Comp) + _setInsertionState(n6, n7) + const n1 = _createIf(() => (true), () => { + const n3 = t0() + return n3 + }) + _renderEffect(() => _setProp(n4, "disabled", _ctx.foo)) + return n6 +}" +`; + +exports[`compile > static + dynamic root 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const n0 = t0() + _setText(n0, _toDisplayString(1) + _toDisplayString(2) + "3" + _toDisplayString(4) + _toDisplayString(5) + "6" + _toDisplayString(7) + _toDisplayString(8) + "9" + 'A' + 'B') + return n0 +}" +`; + +exports[`compile > static template 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("

hello

", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/abbreviation.spec.ts b/packages/compiler-vapor/__tests__/abbreviation.spec.ts new file mode 100644 index 00000000000..6159c6ca5fd --- /dev/null +++ b/packages/compiler-vapor/__tests__/abbreviation.spec.ts @@ -0,0 +1,44 @@ +/** + * @vitest-environment jsdom + */ + +const parser: DOMParser = new DOMParser() + +function parseHTML(html: string): string { + return parser.parseFromString(html, 'text/html').body.innerHTML +} + +function checkAbbr( + template: string, + abbrevation: string, + expected: string, +): void { + // TODO do some optimzations to make sure template === abbrevation + expect(parseHTML(abbrevation)).toBe(expected) +} + +test('template abbreviation', () => { + checkAbbr('
hello
', '
hello', '
hello
') + checkAbbr( + '
hello
', + '
hello', + '
hello
', + ) + checkAbbr( + '
foo
', + '
foo', + '
foo
', + ) + checkAbbr( + '

', + '

', + '

', + ) + checkAbbr( + '

', + '

', + '

', + ) + + checkAbbr('hello', 'hello', 'hello') +}) diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts new file mode 100644 index 00000000000..178021d13dd --- /dev/null +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -0,0 +1,265 @@ +import { BindingTypes, type RootNode } from '@vue/compiler-dom' +import { type CompilerOptions, compile as _compile } from '../src' + +// TODO This is a temporary test case for initial implementation. +// Remove it once we have more comprehensive tests. +// DO NOT ADD MORE TESTS HERE. + +function compile(template: string | RootNode, options: CompilerOptions = {}) { + let { code } = _compile(template, { + ...options, + mode: 'module', + prefixIdentifiers: true, + }) + return code +} + +describe('compile', () => { + test('static template', () => { + const code = compile( + `
+

hello

+ + +
`, + ) + expect(code).matchSnapshot() + }) + + test('dynamic root', () => { + const code = compile(`{{ 1 }}{{ 2 }}`) + expect(code).matchSnapshot() + }) + + test('dynamic root nodes and interpolation', () => { + const code = compile( + ``, + ) + expect(code).matchSnapshot() + }) + + test('static + dynamic root', () => { + const code = compile( + `{{ 1 }}{{ 2 }}3{{ 4 }}{{ 5 }}6{{ 7 }}{{ 8 }}9{{ 'A' }}{{ 'B' }}`, + ) + expect(code).matchSnapshot() + }) + + test('fragment', () => { + const code = compile(`

`) + expect(code).matchSnapshot() + }) + + test('bindings', () => { + const code = compile(`
count is {{ count }}.
`, { + bindingMetadata: { + count: BindingTypes.SETUP_REF, + }, + }) + expect(code).matchSnapshot() + }) + + describe('directives', () => { + describe('v-pre', () => { + test('basic', () => { + const code = compile(`
{{ bar }}
\n`, { + bindingMetadata: { + foo: BindingTypes.SETUP_REF, + bar: BindingTypes.SETUP_REF, + }, + }) + + expect(code).toMatchSnapshot() + expect(code).contains( + JSON.stringify('
{{ bar }}
'), + ) + expect(code).not.contains('effect') + }) + + test('should not affect siblings after it', () => { + const code = compile( + `
{{ bar }}
\n` + + `
{{ bar }}
`, + { + bindingMetadata: { + foo: BindingTypes.SETUP_REF, + bar: BindingTypes.SETUP_REF, + }, + }, + ) + + expect(code).toMatchSnapshot() + // Waiting for TODO, There should be more here. + }) + }) + + describe('v-cloak', () => { + test('basic', () => { + const code = compile(`
test
`) + expect(code).toMatchSnapshot() + expect(code).not.contains('v-cloak') + }) + }) + + describe('custom directive', () => { + test('basic', () => { + const code = compile(`
`, { + bindingMetadata: { + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('binding value', () => { + const code = compile(`
`, { + bindingMetadata: { + msg: BindingTypes.SETUP_REF, + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('static parameters', () => { + const code = compile(`
`, { + bindingMetadata: { + msg: BindingTypes.SETUP_REF, + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('modifiers', () => { + const code = compile(`
`, { + bindingMetadata: { + msg: BindingTypes.SETUP_REF, + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('modifiers w/o binding', () => { + const code = compile(`
`, { + bindingMetadata: { + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('static parameters and modifiers', () => { + const code = compile(`
`, { + bindingMetadata: { + msg: BindingTypes.SETUP_REF, + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + + test('dynamic parameters', () => { + const code = compile(`
`, { + bindingMetadata: { + foo: BindingTypes.SETUP_REF, + vExample: BindingTypes.SETUP_CONST, + }, + }) + expect(code).matchSnapshot() + }) + }) + }) + + describe('expression parsing', () => { + test('interpolation', () => { + const code = compile(`{{ a + b }}`, { + inline: true, + bindingMetadata: { + b: BindingTypes.SETUP_REF, + }, + }) + expect(code).matchSnapshot() + expect(code).contains('a + b.value') + }) + + test('v-bind', () => { + const code = compile(`
`, { + inline: true, + bindingMetadata: { + key: BindingTypes.SETUP_REF, + foo: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(code).matchSnapshot() + expect(code).contains('const _key = key.value') + expect(code).contains('_key+1') + expect(code).contains( + '_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)', + ) + }) + + // TODO: add more test for expression parsing (v-on, v-slot, v-for) + }) + + describe('custom directive', () => { + test('basic', () => { + const code = compile(`
`) + expect(code).matchSnapshot() + }) + + test('component', () => { + const code = compile(` + +
+ +
+
+ `) + expect(code).matchSnapshot() + }) + }) + + describe('setInsertionState', () => { + test('next, child and nthChild should be above the setInsertionState', () => { + const code = compile(` +
+
+ +
+
+
+
+
+ `) + expect(code).toMatchSnapshot() + }) + }) + + describe('execution order', () => { + test('basic', () => { + const code = compile(`
{{ bar }}
`) + expect(code).matchSnapshot() + expect(code).contains( + `_setProp(n0, "id", _ctx.foo) + _setText(x0, _toDisplayString(_ctx.bar))`, + ) + }) + test('with v-once', () => { + const code = compile( + `
+ {{ foo }} + {{ bar }}
+ {{ baz }} +
`, + ) + expect(code).matchSnapshot() + expect(code).contains( + `_setText(n1, " " + _toDisplayString(_ctx.bar)) + _setText(n2, " " + _toDisplayString(_ctx.baz))`, + ) + }) + }) +}) diff --git a/packages/compiler-vapor/__tests__/scopeId.spec.ts b/packages/compiler-vapor/__tests__/scopeId.spec.ts new file mode 100644 index 00000000000..da17ef5552f --- /dev/null +++ b/packages/compiler-vapor/__tests__/scopeId.spec.ts @@ -0,0 +1,3 @@ +// import { compile } from '../src/compile' + +describe.todo('scopeId compiler support', () => {}) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap new file mode 100644 index 00000000000..7e157236bf9 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: expression > basic 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_ctx.a))) + return n0 +}" +`; + +exports[`compiler: expression > props 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString($props.foo))) + return n0 +}" +`; + +exports[`compiler: expression > props aliased 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString($props['bar']))) + return n0 +}" +`; + +exports[`compiler: expression > update expression 1`] = ` +"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n1 = t0() + const n0 = _child(n1) + const x1 = _child(n1) + _renderEffect(() => { + const _String = String + const _foo = _ctx.foo + _setProp(n1, "id", _String(_foo.id++)) + _setProp(n1, "foo", _foo) + _setProp(n1, "bar", _ctx.bar++) + _setText(n0, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar)) + _setText(x1, _toDisplayString(_String(_foo.id++)) + " " + _toDisplayString(_foo) + " " + _toDisplayString(_ctx.bar)) + }) + return n1 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap new file mode 100644 index 00000000000..4a8caa65948 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap @@ -0,0 +1,76 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: children transform > anchor insertion in middle 1`] = ` +"import { child as _child, next as _next, setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("
", true) + +export function render(_ctx) { + const n4 = t1() + const n3 = _next(_child(n4)) + _setInsertionState(n4, n3) + const n0 = _createIf(() => (1), () => { + const n2 = t0() + return n2 + }, null, true) + return n4 +}" +`; + +exports[`compiler: children transform > children & sibling references 1`] = ` +"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("

", true) + +export function render(_ctx) { + const n3 = t0() + const n0 = _child(n3) + const n1 = _next(n0) + const n2 = _next(n1) + const x0 = _child(n0) + const x2 = _child(n2) + _renderEffect(() => { + _setText(x0, _toDisplayString(_ctx.first)) + _setText(n1, " " + _toDisplayString(_ctx.second) + " " + _toDisplayString(_ctx.third) + " ") + _setText(x2, _toDisplayString(_ctx.forth)) + }) + return n3 +}" +`; + +exports[`compiler: children transform > efficient find 1`] = ` +"import { child as _child, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
x
x
", true) + +export function render(_ctx) { + const n1 = t0() + const n0 = _nthChild(n1, 2) + const x0 = _child(n0) + _renderEffect(() => _setText(x0, _toDisplayString(_ctx.msg))) + return n1 +}" +`; + +exports[`compiler: children transform > efficient traversal 1`] = ` +"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
x
", true) + +export function render(_ctx) { + const n3 = t0() + const p0 = _next(_child(n3)) + const p1 = _next(p0) + const p2 = _next(p1) + const n0 = _child(p0) + const n1 = _child(p1) + const n2 = _child(p2) + const x0 = _child(n0) + const x1 = _child(n1) + const x2 = _child(n2) + _renderEffect(() => { + const _msg = _ctx.msg + _setText(x0, _toDisplayString(_msg)) + _setText(x1, _toDisplayString(_msg)) + _setText(x2, _toDisplayString(_msg)) + }) + return n3 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap new file mode 100644 index 00000000000..7aa56aa9c2f --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -0,0 +1,463 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: element transform > component > cache v-on expression with unique handler name 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const _component_Bar = _resolveComponent("Bar") + const _on_bar = $event => (_ctx.handleBar($event)) + const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar }) + const _on_bar1 = () => _ctx.handler + const n1 = _createComponentWithFallback(_component_Bar, { onBar: () => _on_bar1 }) + return [n0, n1] +}" +`; + +exports[`compiler: element transform > component > do not resolve component from non-script-setup bindings 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const _component_Example = _resolveComponent("Example") + const n0 = _createComponentWithFallback(_component_Example, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > generate multi root component 1`] = ` +"import { createComponent as _createComponent, template as _template } from 'vue'; +const t0 = _template("123") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createComponent(_ctx.Comp) + const n1 = t0() + return [n0, n1] +}" +`; + +exports[`compiler: element transform > component > generate single root component 1`] = ` +"import { createComponent as _createComponent } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createComponent(_ctx.Comp, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > import + resolve component 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = ` +" + const n0 = _createComponent(Example, null, null, true) + return n0 +" +`; + +exports[`compiler: element transform > component > resolve component from setup bindings (inline) 1`] = ` +" + const n0 = _createComponent(_unref(Example), null, null, true) + return n0 +" +`; + +exports[`compiler: element transform > component > resolve component from setup bindings 1`] = ` +"import { createComponent as _createComponent } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createComponent(_ctx.Example, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > resolve implicitly self-referencing component 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Example__self = _resolveComponent("Example", true) + const n0 = _createComponentWithFallback(_component_Example__self, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = ` +" + const n0 = _createComponent(Foo.Example, null, null, true) + return n0 +" +`; + +exports[`compiler: element transform > component > resolve namespaced component from props bindings (non-inline) 1`] = ` +"import { createComponent as _createComponent } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createComponent(_ctx.Foo.Example, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > resolve namespaced component from setup bindings (inline const) 1`] = ` +" + const n0 = _createComponent(Foo.Example, null, null, true) + return n0 +" +`; + +exports[`compiler: element transform > component > resolve namespaced component from setup bindings 1`] = ` +"import { createComponent as _createComponent } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createComponent(_ctx.Foo.Example, null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > static props 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { + id: () => ("foo"), + class: () => ("bar") + }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-bind="obj" 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => (_ctx.obj) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-bind="obj" after static prop 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { + id: () => ("foo"), + $: [ + () => (_ctx.obj) + ] + }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-bind="obj" before static prop 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => (_ctx.obj), + { id: () => ("foo") } + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-bind="obj" between static props 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { + id: () => ("foo"), + $: [ + () => (_ctx.obj), + { class: () => ("bar") } + ] + }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-for on component should not mark as single root 1`] = ` +"import { createComponent as _createComponent, createFor as _createFor } from 'vue'; + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { + const n2 = _createComponent(_ctx.Comp) + return n2 + }, (item) => (item), 2) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-on expression is a function call 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const _on_bar = $event => (_ctx.handleBar($event)) + const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-on expression is inline statement 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const _on_bar = () => _ctx.handler + const n0 = _createComponentWithFallback(_component_Foo, { onBar: () => _on_bar }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > v-on="obj" 1`] = ` +"import { resolveComponent as _resolveComponent, toHandlers as _toHandlers, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => (_toHandlers(_ctx.obj)) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component dynamic event with once modifier 1`] = ` +"import { resolveComponent as _resolveComponent, toHandlerKey as _toHandlerKey, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => ({ [_toHandlerKey(_ctx.foo) + "Once"]: () => _ctx.bar }) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component event with once modifier 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { onFooOnce: () => _ctx.bar }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component with dynamic event arguments 1`] = ` +"import { resolveComponent as _resolveComponent, toHandlerKey as _toHandlerKey, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => ({ [_toHandlerKey(_ctx.foo-_ctx.bar)]: () => _ctx.bar }), + () => ({ [_toHandlerKey(_ctx.baz)]: () => _ctx.qux }) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component with dynamic prop arguments 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { $: [ + () => ({ [_ctx.foo-_ctx.bar]: _ctx.bar }), + () => ({ [_ctx.baz]: _ctx.qux }) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > dynamic component > capitalized version w/ static binding 1`] = ` +"import { resolveDynamicComponent as _resolveDynamicComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const n0 = _createComponentWithFallback(_resolveDynamicComponent("foo"), null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > dynamic component > dynamic binding 1`] = ` +"import { createDynamicComponent as _createDynamicComponent } from 'vue'; + +export function render(_ctx) { + const n0 = _createDynamicComponent(() => (_ctx.foo), null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > dynamic component > dynamic binding shorthand 1`] = ` +"import { createDynamicComponent as _createDynamicComponent } from 'vue'; + +export function render(_ctx) { + const n0 = _createDynamicComponent(() => (_ctx.is), null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > dynamic component > normal component with is prop 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_custom_input = _resolveComponent("custom-input") + const n0 = _createComponentWithFallback(_component_custom_input, { is: () => ("foo") }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > dynamic component > static binding 1`] = ` +"import { resolveDynamicComponent as _resolveDynamicComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const n0 = _createComponentWithFallback(_resolveDynamicComponent("foo"), null, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > empty template 1`] = ` +" +export function render(_ctx) { + return null +}" +`; + +exports[`compiler: element transform > invalid html nesting 1`] = ` +"import { insert as _insert, template as _template } from 'vue'; +const t0 = _template("
123
") +const t1 = _template("

") +const t2 = _template("
") + +export function render(_ctx) { + const n1 = t1() + const n3 = t2() + const n0 = t0() + const n2 = t2() + _insert(n0, n1) + _insert(n2, n3) + return [n1, n3] +}" +`; + +exports[`compiler: element transform > props + children 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: element transform > props merging: class 1`] = ` +"import { setClass as _setClass, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setClass(n0, ["foo", { bar: _ctx.isBar }])) + return n0 +}" +`; + +exports[`compiler: element transform > props merging: event handlers 1`] = ` +"import { withKeys as _withKeys, delegate as _delegate, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + _delegate(n0, "click", _withKeys(e => _ctx.a(e), ["foo"])) + _delegate(n0, "click", _withKeys(e => _ctx.b(e), ["bar"])) + return n0 +}" +`; + +exports[`compiler: element transform > props merging: style 1`] = ` +"import { setStyle as _setStyle, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _setStyle(n0, ["color: green", { color: 'red' }]) + return n0 +}" +`; + +exports[`compiler: element transform > static props 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: element transform > v-bind="obj" 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true)) + return n0 +}" +`; + +exports[`compiler: element transform > v-bind="obj" after static prop 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true)) + return n0 +}" +`; + +exports[`compiler: element transform > v-bind="obj" before static prop 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true)) + return n0 +}" +`; + +exports[`compiler: element transform > v-bind="obj" between static props 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true)) + return n0 +}" +`; + +exports[`compiler: element transform > v-on="obj" 1`] = ` +"import { setDynamicEvents as _setDynamicEvents, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicEvents(n0, _ctx.obj)) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap new file mode 100644 index 00000000000..a59b17d9429 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap @@ -0,0 +1,52 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: template ref transform > dynamic ref 1`] = ` +"import { renderEffect as _renderEffect, setRef as _setRef, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + let r0 + _renderEffect(() => r0 = _setRef(n0, _ctx.foo, r0)) + return n0 +}" +` + +exports[`compiler: template ref transform > ref + v-for 1`] = ` +"import { setRef as _setRef, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = _createFor(() => ([1,2,3]), (_block) => { + const n2 = t0() + _setRef(n2, "foo", void 0, true) + return [n2, () => {}] + }) + return n0 +}" +` + +exports[`compiler: template ref transform > ref + v-if 1`] = ` +"import { setRef as _setRef, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = _createIf(() => (true), () => { + const n2 = t0() + _setRef(n2, "foo") + return n2 + }) + return n0 +}" +` + +exports[`compiler: template ref transform > static ref 1`] = ` +"import { setRef as _setRef, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + _setRef(n0, "foo") + return n0 +}" +` diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap new file mode 100644 index 00000000000..6d62b9c4e35 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -0,0 +1,163 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: transform outlets > default slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > default slot outlet with fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createSlot("default", null, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > default slot outlet with props & fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createSlot("default", { foo: () => (_ctx.bar) }, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > default slot outlet with props 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", { + foo: () => ("bar"), + baz: () => (_ctx.qux), + fooBar: () => (_ctx.foo-_ctx.bar) + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > dynamically named slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot(() => (_ctx.foo + _ctx.bar), null) + return n0 +}" +`; + +exports[`compiler: transform outlets > dynamically named slot outlet with v-bind shorthand 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot(() => (_ctx.name), null) + return n0 +}" +`; + +exports[`compiler: transform outlets > error on unexpected custom directive on 1`] = ` +"import { resolveDirective as _resolveDirective, createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const _directive_foo = _resolveDirective("foo") + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > error on unexpected custom directive with v-show on 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > named slot outlet with fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createSlot("foo", null, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > named slot outlet with props & fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createSlot("foo", { foo: () => (_ctx.bar) }, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("foo", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with props 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("foo", { + foo: () => ("bar"), + baz: () => (_ctx.qux) + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with v-bind="obj" 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("foo", { + foo: () => ("bar"), + $: [ + () => (_ctx.obj), + { baz: () => (_ctx.qux) } + ] + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with v-on 1`] = ` +"import { createSlot as _createSlot, toHandlers as _toHandlers } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", { + onClick: () => _ctx.foo, + $: [ + () => (_toHandlers(_ctx.bar)), + { baz: () => (_ctx.qux) } + ] + }) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap new file mode 100644 index 00000000000..2d64e1ffe52 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -0,0 +1,85 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: template ref transform > dynamic ref 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + let r0 + _renderEffect(() => r0 = _setTemplateRef(n0, _ctx.foo, r0)) + return n0 +}" +`; + +exports[`compiler: template ref transform > function ref 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + let r0 + _renderEffect(() => { + const _foo = _ctx.foo + r0 = _setTemplateRef(n0, bar => { + _foo.value = bar + ;({ baz: _ctx.baz } = bar) + console.log(_foo.value, _ctx.baz) + }, r0) + }) + return n0 +}" +`; + +exports[`compiler: template ref transform > ref + v-for 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _setTemplateRef = _createTemplateRefSetter() + const n0 = _createFor(() => ([1,2,3]), (_for_item0) => { + const n2 = t0() + _setTemplateRef(n2, "foo", void 0, true) + return n2 + }, null, 4) + return n0 +}" +`; + +exports[`compiler: template ref transform > ref + v-if 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _setTemplateRef = _createTemplateRefSetter() + const n0 = _createIf(() => (true), () => { + const n2 = t0() + _setTemplateRef(n2, "foo") + return n2 + }) + return n0 +}" +`; + +exports[`compiler: template ref transform > static ref (inline mode) 1`] = ` +" + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + _setTemplateRef(n0, foo) + return n0 +" +`; + +exports[`compiler: template ref transform > static ref 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + _setTemplateRef(n0, "foo") + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap new file mode 100644 index 00000000000..f18f8520353 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: text transform > consecutive text 1`] = ` +"import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_ctx.msg))) + return n0 +}" +`; + +exports[`compiler: text transform > no consecutive text 1`] = ` +"import { setText as _setText, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const n0 = t0() + _setText(n0, "hello world") + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap new file mode 100644 index 00000000000..4ea0db55fe5 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -0,0 +1,658 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cache multiple access > cache variable used in both property shorthand and normal binding 1`] = ` +"import { setStyle as _setStyle, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _color = _ctx.color + _setStyle(n0, {color: _color}) + _setProp(n0, "id", _color) + }) + return n0 +}" +`; + +exports[`cache multiple access > dynamic key bindings with expressions 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _key = _ctx.key + _setDynamicProps(n0, [{ [_key+1]: _ctx.foo[_key+1]() }], true) + }) + return n0 +}" +`; + +exports[`cache multiple access > dynamic property access 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _obj = _ctx.obj + _setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar) + }) + return n0 +}" +`; + +exports[`cache multiple access > function calls with arguments 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + const n2 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + const _bar = _ctx.bar + const _foo_bar_baz = _foo[_bar(_ctx.baz)] + _setProp(n0, "id", _foo_bar_baz) + _setProp(n1, "id", _foo_bar_baz) + _setProp(n2, "id", _bar() + _foo) + }) + return [n0, n1, n2] +}" +`; + +exports[`cache multiple access > not cache variable and member expression with the same name 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "id", _ctx.bar + _ctx.obj.bar)) + return n0 +}" +`; + +exports[`cache multiple access > not cache variable in function expression 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [{ foo: bar => _ctx.foo = bar }], true)) + return n0 +}" +`; + +exports[`cache multiple access > not cache variable only used in property shorthand 1`] = ` +"import { setStyle as _setStyle, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setStyle(n0, {color: _ctx.color})) + return n0 +}" +`; + +exports[`cache multiple access > object property chain access 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _obj = _ctx.obj + const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar + _setProp(n0, "id", _obj_foo_baz_obj_bar) + _setProp(n1, "id", _obj_foo_baz_obj_bar) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > object property name substring cases 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _p = _ctx.p + const _p_title = _p.title + _setProp(n0, "id", _p_title + _p.titles + _p_title) + }) + return n0 +}" +`; + +exports[`cache multiple access > optional chaining 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _obj = _ctx.obj + _setProp(n0, "id", _obj?.foo + _obj?.bar) + }) + return n0 +}" +`; + +exports[`cache multiple access > repeated expression in expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + const n2 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + const _foo_bar = _foo + _ctx.bar + _setProp(n0, "id", _foo_bar) + _setProp(n1, "id", _foo_bar) + _setProp(n2, "id", _foo + _foo_bar) + }) + return [n0, n1, n2] +}" +`; + +exports[`cache multiple access > repeated expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo_bar = _ctx.foo + _ctx.bar + _setProp(n0, "id", _foo_bar) + _setProp(n1, "id", _foo_bar) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > repeated variable in expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + _setProp(n0, "id", _foo + _foo + _ctx.bar) + _setProp(n1, "id", _foo) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > repeated variables 1`] = ` +"import { setClass as _setClass, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + _setClass(n0, _foo) + _setClass(n1, _foo) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > variable name substring edge cases 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _title = _ctx.title + _setProp(n0, "id", _title + _ctx.titles + _title) + }) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "foo-bar", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier w/ innerHTML 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "innerHTML", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier w/ no expression 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "foo-bar", _ctx.fooBar)) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier w/ progress value 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "value", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier w/ textContent 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "textContent", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .attr modifier w/ value 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "value", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .camel modifier 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "fooBar", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > .camel modifier w/ dynamic arg 1`] = ` +"import { camelize as _camelize, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true)) + return n0 +}" +`; + +exports[`compiler v-bind > .camel modifier w/ no expression 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "fooBar", _ctx.fooBar)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) w/ innerHTML 1`] = ` +"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setHtml(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) w/ no expression 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.fooBar)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) w/ progress value 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "value", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) w/ textContent 1`] = ` +"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setText(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier (shorthand) w/ value 1`] = ` +"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setValue(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ dynamic arg 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ innerHTML 1`] = ` +"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setHtml(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ no expression 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "fooBar", _ctx.fooBar)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ progress value 1`] = ` +"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDOMProp(n0, "value", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ textContent 1`] = ` +"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setText(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > .prop modifier w/ value 1`] = ` +"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setValue(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > :innerHTML 1`] = ` +"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setHtml(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > :textContext 1`] = ` +"import { setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setText(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > :value 1`] = ` +"import { setValue as _setValue, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setValue(n0, _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > :value w/ progress 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "value", _ctx.foo)) + return n0 +}" +`; + +exports[`compiler v-bind > attributes must be set as attribute 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") +const t4 = _template("") +const t5 = _template("") +const t6 = _template("") + +export function render(_ctx) { + const n0 = t0() + const n1 = t1() + const n2 = t2() + const n3 = t3() + const n4 = t4() + const n5 = t5() + const n6 = t6() + _renderEffect(() => { + const _width = _ctx.width + const _height = _ctx.height + _setAttr(n0, "spellcheck", _ctx.spellcheck) + _setAttr(n0, "draggable", _ctx.draggable) + _setAttr(n0, "translate", _ctx.translate) + _setAttr(n0, "form", _ctx.form) + _setAttr(n1, "list", _ctx.list) + _setAttr(n2, "type", _ctx.type) + _setAttr(n3, "width", _width) + _setAttr(n3, "height", _height) + _setAttr(n4, "width", _width) + _setAttr(n4, "height", _height) + _setAttr(n5, "width", _width) + _setAttr(n5, "height", _height) + _setAttr(n6, "width", _width) + _setAttr(n6, "height", _height) + }) + return [n0, n1, n2, n3, n4, n5, n6] +}" +`; + +exports[`compiler v-bind > basic 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "id", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > dynamic arg 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _id = _ctx.id + const _title = _ctx.title + _setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true) + }) + return n0 +}" +`; + +exports[`compiler v-bind > dynamic arg w/ static attribute 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _id = _ctx.id + _setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true) + }) + return n0 +}" +`; + +exports[`compiler v-bind > no expression (shorthand) 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "camel-case", _ctx.camelCase)) + return n0 +}" +`; + +exports[`compiler v-bind > no expression 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "id", _ctx.id)) + return n0 +}" +`; + +exports[`compiler v-bind > number value 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { depth: () => (0) }, null, true) + return n0 +}" +`; + +exports[`compiler v-bind > should error if empty expression 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler v-bind > with constant value 1`] = ` +"import { setProp as _setProp, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _setProp(n0, "a", void 0) + _setProp(n0, "b", 1 > 2) + _setProp(n0, "c", 1 + 2) + _setProp(n0, "d", 1 ? 2 : 3) + _setProp(n0, "e", (2)) + _setProp(n0, "g", 1) + _setProp(n0, "i", true) + _setProp(n0, "j", null) + _setProp(n0, "k", _ctx.x) + _setProp(n0, "l", { foo: 1 }) + _setProp(n0, "m", { [_ctx.x]: 1 }) + _setProp(n0, "n", { ...{ foo: 1 } }) + _setProp(n0, "o", [1, , 3]) + _setProp(n0, "p", [1, ...[2, 3]]) + _setProp(n0, "q", [1, 2]) + _setProp(n0, "r", /\\s+/) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap new file mode 100644 index 00000000000..e3631f15890 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -0,0 +1,159 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: v-for > array de-structured value (with rest) 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value))) + return n2 + }, ([id, ...other], index) => (id)) + return n0 +}" +`; + +exports[`compiler: v-for > array de-structured value 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value[1] + _for_key0.value))) + return n2 + }, ([id, other], index) => (id)) + return n0 +}" +`; + +exports[`compiler: v-for > basic v-for 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + n2.$evtclick = () => (_ctx.remove(_for_item0.value)) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value))) + return n2 + }, (item) => (item.id)) + return n0 +}" +`; + +exports[`compiler: v-for > multi effect 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.items), (_for_item0, _for_key0) => { + const n2 = t0() + _renderEffect(() => { + _setProp(n2, "item", _for_item0.value) + _setProp(n2, "index", _for_key0.value) + }) + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-for > nested v-for 1`] = ` +"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template(" ") +const t1 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { + const n5 = t1() + _setInsertionState(n5) + const n2 = _createFor(() => (_for_item0.value), (_for_item1) => { + const n4 = t0() + const x4 = _child(n4) + _renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value))) + return n4 + }, null, 1) + return n5 + }) + return n0 +}" +`; + +exports[`compiler: v-for > object de-structured value (with rest) 1`] = ` +"import { getRestElement as _getRestElement, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value))) + return n2 + }, ({ id, ...other }, index) => (id)) + return n0 +}" +`; + +exports[`compiler: v-for > object de-structured value 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template(" ", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id) + _toDisplayString(_for_item0.value.value))) + return n2 + }, ({ id, value }) => (id)) + return n0 +}" +`; + +exports[`compiler: v-for > object value, key and index 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0, _for_index0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value + _for_key0.value + _for_index0.value))) + return n2 + }, (value, key, index) => (key)) + return n0 +}" +`; + +exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` +"import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_getDefaultValue(_for_item0.value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_for_item0.value.baz[0], _ctx.quux) + _ctx.quux))) + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-for > w/o value 1`] = ` +"import { createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
item
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap new file mode 100644 index 00000000000..ecf886d7cbb --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap @@ -0,0 +1,34 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`v-html > should convert v-html to innerHTML 1`] = ` +"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + _renderEffect(() => _setHtml(n0, _ctx.code)) + return n0 +}" +`; + +exports[`v-html > should raise error and ignore children when v-html is present 1`] = ` +"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setHtml(n0, _ctx.test)) + return n0 +}" +`; + +exports[`v-html > should raise error if has no expression 1`] = ` +"import { setHtml as _setHtml, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _setHtml(n0, "") + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap new file mode 100644 index 00000000000..c41dc9226c5 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -0,0 +1,162 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: v-if > basic v-if 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + const x2 = _child(n2) + _renderEffect(() => _setText(x2, _toDisplayString(_ctx.msg))) + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-if > comment between branches 1`] = ` +"import { createIf as _createIf, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("") +const t2 = _template("

") +const t3 = _template("") +const t4 = _template("fine") +const t5 = _template("
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n5 = t1() + const n6 = t2() + return [n5, n6] + }, () => { + const n10 = t3() + const n11 = t4() + return [n10, n11] + })) + const n13 = t5() + const x13 = _child(n13) + _renderEffect(() => _setText(x13, _toDisplayString(_ctx.text))) + return [n0, n13] +}" +`; + +exports[`compiler: v-if > dedupe same template 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
hello
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }) + const n3 = _createIf(() => (_ctx.ok), () => { + const n5 = t0() + return n5 + }) + return [n0, n3] +}" +`; + +exports[`compiler: v-if > template v-if 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("hello") +const t2 = _template("

", true) + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + const n3 = t1() + const n4 = t2() + const x4 = _child(n4) + _renderEffect(() => _setText(x4, _toDisplayString(_ctx.msg))) + return [n2, n3, n4] + }) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-else 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("

") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => { + const n4 = t1() + return n4 + }) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("

") +const t2 = _template("fine") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n4 = t1() + return n4 + }, () => { + const n7 = t2() + return n7 + })) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-else-if 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("

") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n4 = t1() + return n4 + })) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-if / v-else[-if] 1`] = ` +"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("foo") +const t1 = _template("bar") +const t2 = _template("baz") +const t3 = _template("
", true) + +export function render(_ctx) { + const n8 = t3() + _setInsertionState(n8) + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + return n2 + }) + _setInsertionState(n8) + const n3 = _createIf(() => (_ctx.bar), () => { + const n5 = t1() + return n5 + }, () => { + const n7 = t2() + return n7 + }) + return n8 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap new file mode 100644 index 00000000000..5ef064974c0 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -0,0 +1,242 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: vModel transform > component > v-model for component should generate modelModifiers 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo), + "onUpdate:modelValue": () => _value => (_ctx.foo = _value), + modelModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > component > v-model for component should work 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo), + "onUpdate:modelValue": () => _value => (_ctx.foo = _value) }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > component > v-model with arguments for component should generate modelModifiers 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { + foo: () => (_ctx.foo), + "onUpdate:foo": () => _value => (_ctx.foo = _value), + fooModifiers: () => ({ trim: true }), + bar: () => (_ctx.bar), + "onUpdate:bar": () => _value => (_ctx.bar = _value), + barModifiers: () => ({ number: true }) + }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > component > v-model with arguments for component should work 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { bar: () => (_ctx.foo), + "onUpdate:bar": () => _value => (_ctx.foo = _value) }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should generate modelModifiers 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { $: [ + () => ({ [_ctx.foo]: _ctx.foo, + ["onUpdate:" + _ctx.foo]: () => _value => (_ctx.foo = _value), + [_ctx.foo + "Modifiers"]: () => ({ trim: true }) }), + () => ({ [_ctx.bar]: _ctx.bar, + ["onUpdate:" + _ctx.bar]: () => _value => (_ctx.bar = _value), + [_ctx.bar + "Modifiers"]: () => ({ number: true }) }) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should work 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n0 = _createComponentWithFallback(_component_Comp, { $: [ + () => ({ [_ctx.arg]: _ctx.foo, + ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value) }) + ] }, null, true) + return n0 +}" +`; + +exports[`compiler: vModel transform > modifiers > .lazy 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { lazy: true }) + return n0 +}" +`; + +exports[`compiler: vModel transform > modifiers > .number 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { number: true }) + return n0 +}" +`; + +exports[`compiler: vModel transform > modifiers > .trim 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value), { trim: true }) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support input (checkbox) 1`] = ` +"import { applyCheckboxModel as _applyCheckboxModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyCheckboxModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support input (dynamic type) 1`] = ` +"import { applyDynamicModel as _applyDynamicModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support input (radio) 1`] = ` +"import { applyRadioModel as _applyRadioModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyRadioModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support input (text) 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support member expression 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + const n1 = t0() + const n2 = t0() + _applyTextModel(n0, () => (_ctx.setupRef.child), _value => (_ctx.setupRef.child = _value)) + _applyTextModel(n1, () => (_ctx.setupLet.child), _value => (_ctx.setupLet.child = _value)) + _applyTextModel(n2, () => (_ctx.setupMaybeRef.child), _value => (_ctx.setupMaybeRef.child = _value)) + return [n0, n1, n2] +}" +`; + +exports[`compiler: vModel transform > should support member expression w/ inline 1`] = ` +" + const n0 = t0() + const n1 = t0() + const n2 = t0() + _applyTextModel(n0, () => (setupRef.value.child), _value => (setupRef.value.child = _value)) + _applyTextModel(n1, () => (_unref(setupLet).child), _value => (_unref(setupLet).child = _value)) + _applyTextModel(n2, () => (_unref(setupMaybeRef).child), _value => (_unref(setupMaybeRef).child = _value)) + return [n0, n1, n2] +" +`; + +exports[`compiler: vModel transform > should support select 1`] = ` +"import { applySelectModel as _applySelectModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applySelectModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support simple expression 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support textarea 1`] = ` +"import { applyTextModel as _applyTextModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyTextModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support w/ dynamic v-bind 1`] = ` +"import { applyDynamicModel as _applyDynamicModel, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true)) + return n0 +}" +`; + +exports[`compiler: vModel transform > should support w/ dynamic v-bind 2`] = ` +"import { applyDynamicModel as _applyDynamicModel, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = t0() + _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap new file mode 100644 index 00000000000..dd00e552675 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap @@ -0,0 +1,472 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`v-on > complex member expression w/ prefixIdentifiers: true 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = e => _ctx.a['b' + _ctx.c](e) + return n0 +}" +`; + +exports[`v-on > dynamic arg 1`] = ` +"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, _ctx.event, e => _ctx.handler(e), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > dynamic arg with complex exp prefixing 1`] = ` +"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, _ctx.event(_ctx.foo), e => _ctx.handler(e), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > dynamic arg with prefixing 1`] = ` +"import { on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, _ctx.event, e => _ctx.handler(e), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > event modifier 1`] = ` +"import { withModifiers as _withModifiers, on as _on, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("") +const t1 = _template("
") +const t2 = _template("
") +const t3 = _template("") +_delegateEvents("click", "contextmenu", "mouseup", "keyup") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + const n1 = t1() + const n2 = t0() + const n3 = t2() + const n4 = t2() + const n5 = t0() + const n6 = t2() + const n7 = t3() + const n8 = t3() + const n9 = t3() + const n10 = t3() + const n11 = t3() + const n12 = t3() + const n13 = t3() + const n14 = t3() + const n15 = t3() + const n16 = t3() + const n17 = t3() + const n18 = t3() + const n19 = t3() + const n20 = t3() + const n21 = t3() + n0.$evtclick = _withModifiers(_ctx.handleEvent, ["stop"]) + _on(n1, "submit", _withModifiers(_ctx.handleEvent, ["prevent"])) + n2.$evtclick = _withModifiers(_ctx.handleEvent, ["stop","prevent"]) + n3.$evtclick = _withModifiers(_ctx.handleEvent, ["self"]) + _on(n4, "click", _ctx.handleEvent, { + capture: true + }) + _on(n5, "click", _ctx.handleEvent, { + once: true + }) + _on(n6, "scroll", _ctx.handleEvent, { + passive: true + }) + n7.$evtcontextmenu = _withModifiers(_ctx.handleEvent, ["right"]) + n8.$evtclick = _withModifiers(_ctx.handleEvent, ["left"]) + n9.$evtmouseup = _withModifiers(_ctx.handleEvent, ["middle"]) + n10.$evtcontextmenu = _withKeys(_withModifiers(_ctx.handleEvent, ["right"]), ["enter"]) + n11.$evtkeyup = _withKeys(_ctx.handleEvent, ["enter"]) + n12.$evtkeyup = _withKeys(_ctx.handleEvent, ["tab"]) + n13.$evtkeyup = _withKeys(_ctx.handleEvent, ["delete"]) + n14.$evtkeyup = _withKeys(_ctx.handleEvent, ["esc"]) + n15.$evtkeyup = _withKeys(_ctx.handleEvent, ["space"]) + n16.$evtkeyup = _withKeys(_ctx.handleEvent, ["up"]) + n17.$evtkeyup = _withKeys(_ctx.handleEvent, ["down"]) + n18.$evtkeyup = _withKeys(_ctx.handleEvent, ["left"]) + n19.$evtkeyup = _withModifiers(e => _ctx.submit(e), ["middle"]) + n20.$evtkeyup = _withModifiers(e => _ctx.submit(e), ["middle","self"]) + n21.$evtkeyup = _withKeys(_withModifiers(_ctx.handleEvent, ["self"]), ["enter"]) + return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20, n21] +}" +`; + +exports[`v-on > expression with type 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + n0.$evtclick = e => _ctx.handleClick(e) + return n0 +}" +`; + +exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = e => _ctx.foo(e) + return n0 +}" +`; + +exports[`v-on > inline statement w/ prefixIdentifiers: true 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = $event => (_ctx.foo($event)) + return n0 +}" +`; + +exports[`v-on > multiple inline statements w/ prefixIdentifiers: true 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = $event => {_ctx.foo($event);_ctx.bar()} + return n0 +}" +`; + +exports[`v-on > should NOT add a prefix to $event if the expression is a function expression 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = $event => {_ctx.i++;_ctx.foo($event)} + return n0 +}" +`; + +exports[`v-on > should NOT wrap as function if expression is already function expression (with Typescript) 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = (e: any): any => _ctx.foo(e) + return n0 +}" +`; + +exports[`v-on > should NOT wrap as function if expression is already function expression (with newlines) 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = + $event => { + _ctx.foo($event) + } + + return n0 +}" +`; + +exports[`v-on > should NOT wrap as function if expression is already function expression 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = $event => _ctx.foo($event) + return n0 +}" +`; + +exports[`v-on > should NOT wrap as function if expression is complex member expression 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = e => _ctx.a['b' + _ctx.c](e) + return n0 +}" +`; + +exports[`v-on > should delegate event 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = e => _ctx.test(e) + return n0 +}" +`; + +exports[`v-on > should handle multi-line statement 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = () => { +_ctx.foo(); +_ctx.bar() +} + return n0 +}" +`; + +exports[`v-on > should handle multiple inline statement 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = () => {_ctx.foo();_ctx.bar()} + return n0 +}" +`; + +exports[`v-on > should not prefix member expression 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = e => _ctx.foo.bar(e) + return n0 +}" +`; + +exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = ` +"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("keyup") + +export function render(_ctx) { + const n0 = t0() + n0.$evtkeyup = _withModifiers(e => _ctx.test(e), ["exact"]) + return n0 +}" +`; + +exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = ` +"import { withModifiers as _withModifiers, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click", "keyup") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = _withModifiers(e => _ctx.test(e), ["stop"]) + n0.$evtkeyup = _withKeys(e => _ctx.test(e), ["enter"]) + return n0 +}" +`; + +exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = ` +"import { withModifiers as _withModifiers, on as _on, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _on(n0, "click", _withModifiers(e => _ctx.test(e), ["stop","prevent"]), { + capture: true, + once: true + }) + return n0 +}" +`; + +exports[`v-on > should transform click.middle 1`] = ` +"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("mouseup") + +export function render(_ctx) { + const n0 = t0() + n0.$evtmouseup = _withModifiers(e => _ctx.test(e), ["middle"]) + return n0 +}" +`; + +exports[`v-on > should transform click.middle 2`] = ` +"import { withModifiers as _withModifiers, on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers(e => _ctx.test(e), ["middle"]), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > should transform click.right 1`] = ` +"import { withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("contextmenu") + +export function render(_ctx) { + const n0 = t0() + n0.$evtcontextmenu = _withModifiers(e => _ctx.test(e), ["right"]) + return n0 +}" +`; + +exports[`v-on > should transform click.right 2`] = ` +"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers(e => _ctx.test(e), ["right"]), ["right"]), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > should use delegate helper when have multiple events of same name 1`] = ` +"import { delegate as _delegate, withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + _delegate(n0, "click", e => _ctx.test(e)) + _delegate(n0, "click", _withModifiers(e => _ctx.test(e), ["stop"])) + return n0 +}" +`; + +exports[`v-on > should wrap as function if expression is inline statement 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx) { + const n0 = t0() + n0.$evtclick = () => (_ctx.i++) + return n0 +}" +`; + +exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = ` +"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + + _on(n0, _ctx.e, _withKeys(_withModifiers(e => _ctx.test(e), ["left"]), ["left"]), { + effect: true + }) + }) + return n0 +}" +`; + +exports[`v-on > should wrap in unref if identifier is setup-maybe-ref w/ inline: true 1`] = ` +" + const n0 = t0() + const n1 = t0() + const n2 = t0() + n0.$evtclick = () => (x.value=_unref(y)) + n1.$evtclick = () => (x.value++) + n2.$evtclick = () => ({ x: x.value } = _unref(y)) + return [n0, n1, n2] +" +`; + +exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = ` +"import { withModifiers as _withModifiers, withKeys as _withKeys, on as _on, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _on(n0, "keydown", _withKeys(_withModifiers(e => _ctx.test(e), ["stop","ctrl"]), ["a"]), { + capture: true + }) + return n0 +}" +`; + +exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = ` +"import { withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("keyup") + +export function render(_ctx) { + const n0 = t0() + n0.$evtkeyup = _withKeys(e => _ctx.test(e), ["left"]) + return n0 +}" +`; + +exports[`v-on > simple expression 1`] = ` +"import { delegateEvents as _delegateEvents, template as _template } from 'vue'; +const t0 = _template("
", true) +_delegateEvents("click") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + n0.$evtclick = _ctx.handleClick + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap new file mode 100644 index 00000000000..ab3ade45b60 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -0,0 +1,104 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: v-once > as root node 1`] = ` +"import { setProp as _setProp, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _setProp(n0, "id", _ctx.foo) + return n0 +}" +`; + +exports[`compiler: v-once > basic 1`] = ` +"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, setClass as _setClass, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n2 = t0() + const n0 = _child(n2) + const n1 = _next(n0) + _setText(n0, _toDisplayString(_ctx.msg) + " ") + _setClass(n1, _ctx.clz) + return n2 +}" +`; + +exports[`compiler: v-once > inside v-once 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: v-once > on component 1`] = ` +"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = t0() + _setInsertionState(n1) + const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true) + return n1 +}" +`; + +exports[`compiler: v-once > on nested plain element 1`] = ` +"import { child as _child, setProp as _setProp, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n1 = t0() + const n0 = _child(n1) + _setProp(n0, "id", _ctx.foo) + return n1 +}" +`; + +exports[`compiler: v-once > with v-for 1`] = ` +"import { createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { + const n2 = t0() + return n2 + }, null, 4) + return n0 +}" +`; + +exports[`compiler: v-once > with v-if 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.expr), () => { + const n2 = t0() + return n2 + }, null, true) + return n0 +}" +`; + +exports[`compiler: v-once > with v-if/else 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") +const t1 = _template("

") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.expr), () => { + const n2 = t0() + return n2 + }, () => { + const n4 = t1() + return n4 + }, true) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap new file mode 100644 index 00000000000..f595da5ef8f --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vShow.spec.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: v-show transform > simple expression 1`] = ` +"import { applyVShow as _applyVShow, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _applyVShow(n0, () => (_ctx.foo)) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap new file mode 100644 index 00000000000..f296d7257d0 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -0,0 +1,341 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: transform slot > dynamic slots name 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("foo") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + $: [ + () => ({ + name: _ctx.name, + fn: () => { + const n0 = t0() + return n0 + } + }) + ] + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = ` +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + $: [ + () => (_createForSlots(_ctx.list, (item) => ({ + name: item, + fn: (_slotProps0) => { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"]))) + return n0 + } + }))) + ] + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > dynamic slots name w/ v-for and provide absent key 1`] = ` +"import { resolveComponent as _resolveComponent, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("foo") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + $: [ + () => (_createForSlots(_ctx.list, (_, __, index) => ({ + name: index, + fn: () => { + const n0 = t0() + return n0 + } + }))) + ] + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("condition slot") +const t1 = _template("another condition") +const t2 = _template("else condition") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n6 = _createComponentWithFallback(_component_Comp, null, { + $: [ + () => (_ctx.condition + ? { + name: "condition", + fn: () => { + const n0 = t0() + return n0 + } + } + : _ctx.anotherCondition + ? { + name: "condition", + fn: (_slotProps0) => { + const n2 = t1() + return n2 + } + } + : { + name: "condition", + fn: () => { + const n4 = t2() + return n4 + } + }) + ] + }, true) + return n6 +}" +`; + +exports[`compiler: transform slot > implicit default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = t0() + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > named slots w/ implicit default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template("foo") +const t1 = _template("bar") +const t2 = _template("") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n4 = _createComponentWithFallback(_component_Comp, null, { + "one": () => { + const n0 = t0() + return n0 + }, + "default": () => { + const n2 = t1() + const n3 = t2() + return [n2, n3] + } + }, true) + return n4 +}" +`; + +exports[`compiler: transform slot > nested component slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_B = _resolveComponent("B") + const _component_A = _resolveComponent("A") + const n1 = _createComponentWithFallback(_component_A, null, { + "default": () => { + const n0 = _createComponentWithFallback(_component_B) + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > nested slots scoping 1`] = ` +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const _component_Inner = _resolveComponent("Inner") + const _component_Comp = _resolveComponent("Comp") + const n5 = _createComponentWithFallback(_component_Comp, null, { + "default": (_slotProps0) => { + const n1 = _createComponentWithFallback(_component_Inner, null, { + "default": (_slotProps1) => { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz))) + return n0 + } + }) + const n3 = t0() + _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz))) + return [n1, n3] + } + }, true) + return n5 +}" +`; + +exports[`compiler: transform slot > on component dynamically named slot 1`] = ` +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + $: [ + () => ({ + name: _ctx.named, + fn: (_slotProps0) => { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) + return n0 + } + }) + ] + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > on component named slot 1`] = ` +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "named": (_slotProps0) => { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > on-component default slot 1`] = ` +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": (_slotProps0) => { + const n0 = t0() + _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > quote slot name 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "nav-bar-title-before": () => { + return null + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > slot + v-if / v-else[-if] should not cause error 1`] = ` +"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const _component_Bar = _resolveComponent("Bar") + const n6 = t0() + _setInsertionState(n6) + const n0 = _createSlot("foo", null) + _setInsertionState(n6) + const n1 = _createIf(() => (true), () => { + const n3 = _createComponentWithFallback(_component_Foo) + return n3 + }, () => { + const n5 = _createComponentWithFallback(_component_Bar) + return n5 + }) + return n6 +}" +`; + +exports[`compiler: transform slot > with whitespace: 'preserve' > implicit default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" Header ") +const t1 = _template(" ") +const t2 = _template("

") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n4 = _createComponentWithFallback(_component_Comp, null, { + "header": () => { + const n0 = t0() + return n0 + }, + "default": () => { + const n2 = t1() + const n3 = t2() + return [n2, n3] + } + }, true) + return n4 +}" +`; + +exports[`compiler: transform slot > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" Header ") +const t1 = _template(" Default ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n5 = _createComponentWithFallback(_component_Comp, null, { + "header": () => { + const n0 = t0() + return n0 + }, + "default": () => { + const n3 = t1() + return n3 + } + }, true) + return n5 +}" +`; + +exports[`compiler: transform slot > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +const t0 = _template(" Header ") +const t1 = _template(" Footer ") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n5 = _createComponentWithFallback(_component_Comp, null, { + "header": () => { + const n0 = t0() + return n0 + }, + "footer": () => { + const n3 = t1() + return n3 + } + }, true) + return n5 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap new file mode 100644 index 00000000000..9a3b88acba3 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`v-text > should convert v-text to setText 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + const x0 = _child(n0) + _renderEffect(() => _setText(x0, _toDisplayString(_ctx.str))) + return n0 +}" +`; + +exports[`v-text > should raise error and ignore children when v-text is present 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + const x0 = _child(n0) + _renderEffect(() => _setText(x0, _toDisplayString(_ctx.test))) + return n0 +}" +`; + +exports[`v-text > should raise error if has no expression 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/_utils.ts b/packages/compiler-vapor/__tests__/transforms/_utils.ts new file mode 100644 index 00000000000..1b6e3f18ab1 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/_utils.ts @@ -0,0 +1,37 @@ +import type { RootNode } from '@vue/compiler-dom' +import { + type CompilerOptions, + type RootIRNode, + generate, + parse, + transform, +} from '../../src' + +export function makeCompile(options: CompilerOptions = {}) { + return ( + template: string, + overrideOptions: CompilerOptions = {}, + ): { + ast: RootNode + ir: RootIRNode + code: string + helpers: Set + } => { + const ast = parse(template, { + prefixIdentifiers: true, + ...options, + ...overrideOptions, + }) + const ir = transform(ast, { + prefixIdentifiers: true, + ...options, + ...overrideOptions, + }) + const { code, helpers } = generate(ir, { + prefixIdentifiers: true, + ...options, + ...overrideOptions, + }) + return { ast, ir, code, helpers } + } +} diff --git a/packages/compiler-vapor/__tests__/transforms/expression.spec.ts b/packages/compiler-vapor/__tests__/transforms/expression.spec.ts new file mode 100644 index 00000000000..5983bde67d1 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/expression.spec.ts @@ -0,0 +1,50 @@ +import { BindingTypes } from '@vue/compiler-dom' +import { + transformChildren, + transformElement, + transformText, + transformVBind, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithExpression = makeCompile({ + nodeTransforms: [transformElement, transformChildren, transformText], + directiveTransforms: { bind: transformVBind }, +}) + +describe('compiler: expression', () => { + test('basic', () => { + const { code } = compileWithExpression(`{{ a }}`) + expect(code).toMatchSnapshot() + expect(code).contains(`ctx.a`) + }) + + test('props', () => { + const { code } = compileWithExpression(`{{ foo }}`, { + bindingMetadata: { foo: BindingTypes.PROPS }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`$props.foo`) + }) + + test('props aliased', () => { + const { code } = compileWithExpression(`{{ foo }}`, { + bindingMetadata: { + foo: BindingTypes.PROPS_ALIASED, + __propsAliases: { foo: 'bar' } as any, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`$props['bar']`) + }) + + test('update expression', () => { + const { code } = compileWithExpression(` +
+ {{ String(foo.id++) }} {{ foo }} {{ bar }} +
+ `) + expect(code).toMatchSnapshot() + expect(code).contains(`_String(_foo.id++)`) + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts new file mode 100644 index 00000000000..2d8ae8c960d --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts @@ -0,0 +1,76 @@ +import { makeCompile } from './_utils' +import { + transformChildren, + transformElement, + transformText, + transformVIf, +} from '../../src' + +const compileWithElementTransform = makeCompile({ + nodeTransforms: [ + transformText, + transformVIf, + transformElement, + transformChildren, + ], +}) + +describe('compiler: children transform', () => { + test('children & sibling references', () => { + const { code, helpers } = compileWithElementTransform( + `
+

{{ first }}

+ {{ second }} + {{ third }} +

{{ forth }}

+
`, + ) + expect(code).toMatchSnapshot() + expect(Array.from(helpers)).containSubset([ + 'child', + 'toDisplayString', + 'renderEffect', + 'next', + 'setText', + 'template', + ]) + }) + + test('efficient traversal', () => { + const { code } = compileWithElementTransform( + `
+
x
+
{{ msg }}
+
{{ msg }}
+
{{ msg }}
+
`, + ) + expect(code).toMatchSnapshot() + }) + + test('efficient find', () => { + const { code } = compileWithElementTransform( + `
+
x
+
x
+
{{ msg }}
+
`, + ) + expect(code).contains(`const n0 = _nthChild(n1, 2)`) + expect(code).toMatchSnapshot() + }) + + test('anchor insertion in middle', () => { + const { code } = compileWithElementTransform( + `
+
+
+
+
`, + ) + // ensure the insertion anchor is generated before the insertion statement + expect(code).toMatch(`const n3 = _next(_child(n4))`) + expect(code).toMatch(`_setInsertionState(n4, n3)`) + expect(code).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts new file mode 100644 index 00000000000..a693db4ad39 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -0,0 +1,959 @@ +import { makeCompile } from './_utils' +import { + IRDynamicPropsKind, + IRNodeTypes, + transformChildren, + transformElement, + transformText, + transformVBind, + transformVFor, + transformVOn, +} from '../../src' +import { + type BindingMetadata, + BindingTypes, + NodeTypes, +} from '@vue/compiler-dom' + +const compileWithElementTransform = makeCompile({ + nodeTransforms: [ + transformVFor, + transformElement, + transformChildren, + transformText, + ], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + }, +}) + +describe('compiler: element transform', () => { + describe('component', () => { + test('import + resolve component', () => { + const { code, ir, helpers } = compileWithElementTransform(``) + expect(code).toMatchSnapshot() + expect(helpers).contains.all.keys('resolveComponent') + expect(helpers).contains.all.keys('createComponentWithFallback') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + id: 0, + tag: 'Foo', + asset: true, + root: true, + props: [[]], + }) + }) + + test('resolve implicitly self-referencing component', () => { + const { code, helpers } = compileWithElementTransform(``, { + filename: `/foo/bar/Example.vue?vue&type=template`, + }) + expect(code).toMatchSnapshot() + expect(code).toContain('_resolveComponent("Example", true)') + expect(helpers).toContain('resolveComponent') + }) + + test('resolve component from setup bindings', () => { + const { code, ir, helpers } = compileWithElementTransform(``, { + bindingMetadata: { + Example: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(code).toMatchSnapshot() + expect(helpers).not.toContain('resolveComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Example', + asset: false, + }) + }) + + test('resolve component from setup bindings (inline)', () => { + const { code, helpers } = compileWithElementTransform(``, { + inline: true, + bindingMetadata: { + Example: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`unref(Example)`) + expect(helpers).not.toContain('resolveComponent') + expect(helpers).toContain('unref') + }) + + test('resolve component from setup bindings (inline const)', () => { + const { code, helpers } = compileWithElementTransform(``, { + inline: true, + bindingMetadata: { + Example: BindingTypes.SETUP_CONST, + }, + }) + expect(code).toMatchSnapshot() + expect(helpers).not.toContain('resolveComponent') + }) + + test('resolve namespaced component from setup bindings', () => { + const { code, helpers } = compileWithElementTransform(``, { + bindingMetadata: { + Foo: BindingTypes.SETUP_MAYBE_REF, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`_ctx.Foo.Example`) + expect(helpers).not.toContain('resolveComponent') + }) + + test('resolve namespaced component from setup bindings (inline const)', () => { + const { code, helpers } = compileWithElementTransform(``, { + inline: true, + bindingMetadata: { + Foo: BindingTypes.SETUP_CONST, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`Foo.Example`) + expect(helpers).not.toContain('resolveComponent') + }) + + test('resolve namespaced component from props bindings (inline)', () => { + const { code, helpers } = compileWithElementTransform(``, { + inline: true, + bindingMetadata: { + Foo: BindingTypes.PROPS, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains(`Foo.Example`) + expect(helpers).not.toContain('resolveComponent') + }) + + test('resolve namespaced component from props bindings (non-inline)', () => { + const { code, helpers } = compileWithElementTransform(``, { + inline: false, + bindingMetadata: { + Foo: BindingTypes.PROPS, + }, + }) + expect(code).toMatchSnapshot() + expect(code).contains('_ctx.Foo.Example') + expect(helpers).not.toContain('resolveComponent') + }) + + test('do not resolve component from non-script-setup bindings', () => { + const bindingMetadata: BindingMetadata = { + Example: BindingTypes.SETUP_MAYBE_REF, + } + Object.defineProperty(bindingMetadata, '__isScriptSetup', { + value: false, + }) + const { code, ir, helpers } = compileWithElementTransform(``, { + bindingMetadata, + }) + expect(code).toMatchSnapshot() + expect(helpers).toContain('resolveComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + id: 0, + tag: 'Example', + asset: true, + }) + }) + + test('generate single root component', () => { + const { code } = compileWithElementTransform(``, { + bindingMetadata: { Comp: BindingTypes.SETUP_CONST }, + }) + expect(code).toMatchSnapshot() + expect(code).contains('_createComponent(_ctx.Comp, null, null, true)') + }) + + test('generate multi root component', () => { + const { code } = compileWithElementTransform(`123`, { + bindingMetadata: { Comp: BindingTypes.SETUP_CONST }, + }) + expect(code).toMatchSnapshot() + expect(code).contains('_createComponent(_ctx.Comp)') + }) + + test('v-for on component should not mark as single root', () => { + const { code } = compileWithElementTransform( + ``, + { + bindingMetadata: { Comp: BindingTypes.SETUP_CONST }, + }, + ) + expect(code).toMatchSnapshot() + expect(code).contains('_createComponent(_ctx.Comp)') + }) + + test('static props', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + + expect(code).toMatchSnapshot() + expect(code).contains(`{ + id: () => ("foo"), + class: () => ("bar") + }`) + + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + asset: true, + root: true, + props: [ + [ + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: true, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + ], + }, + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'class', + isStatic: true, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'bar', + isStatic: true, + }, + ], + }, + ], + ], + }) + }) + + test('v-bind="obj"', () => { + const { code, ir } = compileWithElementTransform(``) + expect(code).toMatchSnapshot() + expect(code).contains(`[ + () => (_ctx.obj) + ]`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj', isStatic: false }, + }, + ], + }) + }) + + test('v-bind="obj" after static prop', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`{ + id: () => ("foo"), + $: [ + () => (_ctx.obj) + ] + }`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + }, + ], + }) + }) + + test('v-bind="obj" before static prop', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`[ + () => (_ctx.obj), + { id: () => ("foo") } + ]`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + }, + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + ], + }) + }) + + test('v-bind="obj" between static props', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`{ + id: () => ("foo"), + $: [ + () => (_ctx.obj), + { class: () => ("bar") } + ] + }`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + }, + [{ key: { content: 'class' }, values: [{ content: 'bar' }] }], + ], + }) + }) + + test.todo('props merging: event handlers', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains('onClick: () => [_ctx.a, _ctx.b]') + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [ + { + key: { content: 'onClick', isStatic: true }, + values: [{ content: 'a' }, { content: 'b' }], + }, + ], + ], + }, + ]) + }) + + test.todo('props merging: style', () => { + const { code } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + }) + + test.todo('props merging: class', () => { + const { code } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + }) + + test('v-on="obj"', () => { + const { code, ir } = compileWithElementTransform(``) + expect(code).toMatchSnapshot() + expect(code).contains(`[ + () => (_toHandlers(_ctx.obj)) + ]`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + handler: true, + }, + ], + }) + }) + + test('v-on expression is inline statement', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`onBar: () => _on_bar`) + expect(code).contains(`const _on_bar = () => _ctx.handler`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [ + { + key: { content: 'bar' }, + handler: true, + values: [{ content: '_on_bar' }], + }, + ], + ], + }) + }) + + test('v-on expression is a function call', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`onBar: () => _on_bar`) + expect(code).contains( + `const _on_bar = $event => (_ctx.handleBar($event))`, + ) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [ + { + key: { content: 'bar' }, + handler: true, + values: [{ content: '_on_bar' }], + }, + ], + ], + }) + }) + + test('cache v-on expression with unique handler name', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains(`onBar: () => _on_bar`) + expect(code).contains( + `const _on_bar = $event => (_ctx.handleBar($event))`, + ) + expect(code).contains(`onBar: () => _on_bar1`) + expect(code).contains(`const _on_bar1 = () => _ctx.handler`) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + [ + { + key: { content: 'bar' }, + handler: true, + values: [{ content: '_on_bar' }], + }, + ], + ], + }) + + expect(ir.block.dynamic.children[1].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Bar', + props: [ + [ + { + key: { content: 'bar' }, + handler: true, + values: [{ content: '_on_bar1' }], + }, + ], + ], + }) + }) + }) + + describe('dynamic component', () => { + test('static binding', () => { + const { code, ir, helpers } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(helpers).toContain('resolveDynamicComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'component', + asset: true, + root: true, + props: [[]], + dynamic: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + }) + }) + + test('capitalized version w/ static binding', () => { + const { code, ir, helpers } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(helpers).toContain('resolveDynamicComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Component', + asset: true, + root: true, + props: [[]], + dynamic: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + }) + }) + + test('dynamic binding', () => { + const { code, ir, helpers } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(helpers).toContain('createDynamicComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'component', + asset: true, + root: true, + props: [[]], + dynamic: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: false, + }, + }) + }) + + test('dynamic binding shorthand', () => { + const { code, ir, helpers } = + compileWithElementTransform(``) + expect(code).toMatchSnapshot() + expect(helpers).toContain('createDynamicComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'component', + asset: true, + root: true, + props: [[]], + dynamic: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'is', + isStatic: false, + }, + }) + }) + + // #3934 + test('normal component with is prop', () => { + const { code, ir, helpers } = compileWithElementTransform( + ``, + { + isNativeTag: () => false, + }, + ) + expect(code).toMatchSnapshot() + expect(helpers).toContain('resolveComponent') + expect(helpers).not.toContain('resolveDynamicComponent') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'custom-input', + asset: true, + root: true, + props: [[{ key: { content: 'is' }, values: [{ content: 'foo' }] }]], + }) + }) + }) + + test('static props', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + + const template = '
' + expect(code).toMatchSnapshot() + expect(code).contains(JSON.stringify(template)) + expect(ir.template).toMatchObject([template]) + expect(ir.block.effect).lengthOf(0) + }) + + test('props + children', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + + const template = '
' + expect(code).toMatchSnapshot() + expect(code).contains(JSON.stringify(template)) + expect(ir.template).toMatchObject([template]) + expect(ir.block.effect).lengthOf(0) + }) + + test('v-bind="obj"', () => { + const { code, ir } = compileWithElementTransform(`
`) + expect(code).toMatchSnapshot() + expect(ir.block.effect).toMatchObject([ + { + expressions: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + ], + operations: [ + { + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + }, + ], + }, + ], + }, + ]) + expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true)') + }) + + test('v-bind="obj" after static prop', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.block.effect).toMatchObject([ + { + expressions: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + ], + operations: [ + { + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + }, + ], + }, + ], + }, + ]) + expect(code).contains( + '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true)', + ) + }) + + test('v-bind="obj" before static prop', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.block.effect).toMatchObject([ + { + expressions: [{ content: 'obj' }], + operations: [ + { + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + }, + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + ], + }, + ], + }, + ]) + expect(code).contains( + '_setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true)', + ) + }) + + test('v-bind="obj" between static props', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.block.effect).toMatchObject([ + { + expressions: [{ content: 'obj' }], + operations: [ + { + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + [{ key: { content: 'id' }, values: [{ content: 'foo' }] }], + { + kind: IRDynamicPropsKind.EXPRESSION, + value: { content: 'obj' }, + }, + [{ key: { content: 'class' }, values: [{ content: 'bar' }] }], + ], + }, + ], + }, + ]) + expect(code).contains( + '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true)', + ) + }) + + test('props merging: event handlers', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + expect(code).toMatchSnapshot() + + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + element: 0, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'click', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'a', + isStatic: false, + }, + keyOverride: undefined, + delegate: true, + effect: false, + }, + { + type: IRNodeTypes.SET_EVENT, + element: 0, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'click', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'b', + isStatic: false, + }, + keyOverride: undefined, + delegate: true, + effect: false, + }, + ]) + }) + + test('props merging: style', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + expect(code).toMatchSnapshot() + + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SET_PROP, + element: 0, + prop: { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'style', + isStatic: true, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'color: green', + isStatic: true, + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `{ color: 'red' }`, + isStatic: false, + }, + ], + }, + }, + ]) + }) + + test('props merging: class', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + + expect(code).toMatchSnapshot() + + expect(ir.block.effect).toMatchObject([ + { + expressions: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `{ bar: isBar }`, + isStatic: false, + }, + ], + operations: [ + { + type: IRNodeTypes.SET_PROP, + element: 0, + prop: { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'class', + isStatic: true, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `foo`, + isStatic: true, + }, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `{ bar: isBar }`, + isStatic: false, + }, + ], + }, + }, + ], + }, + ]) + }) + + test('v-on="obj"', () => { + const { code, ir } = compileWithElementTransform(`
`) + expect(code).toMatchSnapshot() + expect(ir.block.effect).toMatchObject([ + { + expressions: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + ], + operations: [ + { + type: IRNodeTypes.SET_DYNAMIC_EVENTS, + element: 0, + event: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'obj', + isStatic: false, + }, + }, + ], + }, + ]) + expect(code).contains('_setDynamicEvents(n0, _ctx.obj)') + }) + + test('component with dynamic prop arguments', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + { + kind: IRDynamicPropsKind.ATTRIBUTE, + key: { content: 'foo-bar' }, + values: [{ content: 'bar' }], + }, + { + kind: IRDynamicPropsKind.ATTRIBUTE, + key: { content: 'baz' }, + values: [{ content: 'qux' }], + }, + ], + }) + }) + + test('component with dynamic event arguments', () => { + const { code, ir } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Foo', + props: [ + { + kind: IRDynamicPropsKind.ATTRIBUTE, + key: { content: 'foo-bar' }, + values: [{ content: 'bar' }], + handler: true, + }, + { + kind: IRDynamicPropsKind.ATTRIBUTE, + key: { content: 'baz' }, + values: [{ content: 'qux' }], + handler: true, + }, + ], + }) + }) + + test('component event with once modifier', () => { + const { code } = compileWithElementTransform(``) + expect(code).toMatchSnapshot() + }) + + test('component dynamic event with once modifier', () => { + const { code } = compileWithElementTransform(``) + expect(code).toMatchSnapshot() + }) + + test('invalid html nesting', () => { + const { code, ir } = compileWithElementTransform( + `

123

+
`, + ) + expect(code).toMatchSnapshot() + expect(ir.template).toEqual(['
123
', '

', '
']) + expect(ir.block.dynamic).toMatchObject({ + children: [ + { id: 1, template: 1, children: [{ id: 0, template: 0 }] }, + { id: 3, template: 2, children: [{ id: 2, template: 2 }] }, + ], + }) + + expect(ir.block.operation).toMatchObject([ + { type: IRNodeTypes.INSERT_NODE, parent: 1, elements: [0] }, + { type: IRNodeTypes.INSERT_NODE, parent: 3, elements: [2] }, + ]) + }) + + test('empty template', () => { + const { code } = compileWithElementTransform('') + expect(code).toMatchSnapshot() + expect(code).contain('return null') + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts new file mode 100644 index 00000000000..389c665a12f --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -0,0 +1,280 @@ +import { ErrorCodes, NodeTypes } from '@vue/compiler-dom' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformSlotOutlet, + transformText, + transformVBind, + transformVOn, + transformVShow, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithSlotsOutlet = makeCompile({ + nodeTransforms: [ + transformText, + transformSlotOutlet, + transformElement, + transformChildren, + ], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + show: transformVShow, + }, +}) + +describe('compiler: transform outlets', () => { + test('default slot outlet', () => { + const { ir, code, helpers } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(helpers).toContain('createSlot') + expect(ir.block.effect).toEqual([]) + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + }) + }) + + test('statically named slot outlet', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + }) + }) + + test('dynamically named slot outlet', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo + bar', + isStatic: false, + }, + }) + }) + + test('dynamically named slot outlet with v-bind shorthand', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'name', + isStatic: false, + }, + }) + }) + + test('default slot outlet with props', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'default' }, + props: [ + [ + { key: { content: 'foo' }, values: [{ content: 'bar' }] }, + { key: { content: 'baz' }, values: [{ content: 'qux' }] }, + { key: { content: 'fooBar' }, values: [{ content: 'foo-bar' }] }, + ], + ], + }) + }) + + test('statically named slot outlet with props', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'foo' }, + props: [ + [ + { key: { content: 'foo' }, values: [{ content: 'bar' }] }, + { key: { content: 'baz' }, values: [{ content: 'qux' }] }, + ], + ], + }) + }) + + test('statically named slot outlet with v-bind="obj"', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'foo' }, + props: [ + [{ key: { content: 'foo' }, values: [{ content: 'bar' }] }], + { value: { content: 'obj', isStatic: false } }, + [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }], + ], + }) + }) + + test('statically named slot outlet with v-on', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + props: [ + [{ key: { content: 'click' }, values: [{ content: 'foo' }] }], + { value: { content: 'bar' }, handler: true }, + [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }], + ], + }) + }) + + test('default slot outlet with fallback', () => { + const { ir, code } = compileWithSlotsOutlet(`
`) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toBe('
') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'default' }, + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }) + }) + + test('named slot outlet with fallback', () => { + const { ir, code } = compileWithSlotsOutlet( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toBe('
') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'foo' }, + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }) + }) + + test('default slot outlet with props & fallback', () => { + const { ir, code } = compileWithSlotsOutlet( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toBe('
') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'default' }, + props: [[{ key: { content: 'foo' }, values: [{ content: 'bar' }] }]], + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }) + }) + + test('named slot outlet with props & fallback', () => { + const { ir, code } = compileWithSlotsOutlet( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toBe('
') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'foo' }, + props: [[{ key: { content: 'foo' }, values: [{ content: 'bar' }] }]], + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }) + }) + + test('error on unexpected custom directive on ', () => { + const onError = vi.fn() + const source = `` + const index = source.indexOf('v-foo') + const { code } = compileWithSlotsOutlet(source, { onError }) + expect(code).toMatchSnapshot() + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 5, + line: 1, + column: index + 6, + }, + }, + }) + }) + + test('error on unexpected custom directive with v-show on ', () => { + const onError = vi.fn() + const source = `` + const index = source.indexOf('v-show="ok"') + const { code } = compileWithSlotsOutlet(source, { onError }) + expect(code).toMatchSnapshot() + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 11, + line: 1, + column: index + 12, + }, + }, + }) + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts new file mode 100644 index 00000000000..2c883d10cc6 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -0,0 +1,183 @@ +import { BindingTypes } from '@vue/compiler-dom' +import { + DynamicFlag, + type ForIRNode, + IRNodeTypes, + type IfIRNode, + transformChildren, + transformElement, + transformTemplateRef, + transformVFor, + transformVIf, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithTransformRef = makeCompile({ + nodeTransforms: [ + transformVIf, + transformVFor, + transformTemplateRef, + transformElement, + transformChildren, + ], +}) + +describe('compiler: template ref transform', () => { + test('static ref', () => { + const { ir, code } = compileWithTransformRef(`
`) + + expect(ir.block.dynamic.children[0]).toMatchObject({ + id: 0, + flags: DynamicFlag.REFERENCED, + }) + expect(ir.template).toEqual(['
']) + expect(ir.block.operation).lengthOf(1) + expect(ir.block.operation[0]).toMatchObject({ + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 0, + value: { + content: 'foo', + isStatic: true, + loc: { + start: { line: 1, column: 10, offset: 9 }, + end: { line: 1, column: 15, offset: 14 }, + }, + }, + }) + expect(code).matchSnapshot() + expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') + expect(code).contains('_setTemplateRef(n0, "foo")') + }) + + test('static ref (inline mode)', () => { + const { code } = compileWithTransformRef(`
`, { + inline: true, + bindingMetadata: { foo: BindingTypes.SETUP_REF }, + }) + expect(code).matchSnapshot() + // pass the actual ref + expect(code).contains('_setTemplateRef(n0, foo)') + }) + + test('dynamic ref', () => { + const { ir, code } = compileWithTransformRef(`
`) + + expect(ir.block.dynamic.children[0]).toMatchObject({ + id: 0, + flags: DynamicFlag.REFERENCED, + }) + expect(ir.template).toEqual(['
']) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.DECLARE_OLD_REF, + id: 0, + }, + ]) + expect(ir.block.effect).toMatchObject([ + { + operations: [ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 0, + value: { + content: 'foo', + isStatic: false, + }, + }, + ], + }, + ]) + expect(code).matchSnapshot() + expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') + expect(code).contains('_setTemplateRef(n0, _ctx.foo, r0)') + }) + + test('function ref', () => { + const { ir, code } = compileWithTransformRef( + `
`, + ) + expect(ir.block.dynamic.children[0]).toMatchObject({ + id: 0, + flags: DynamicFlag.REFERENCED, + }) + expect(ir.template).toEqual(['
']) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.DECLARE_OLD_REF, + id: 0, + }, + ]) + expect(ir.block.effect).toMatchObject([ + { + operations: [ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 0, + value: { + isStatic: false, + }, + }, + ], + }, + ]) + expect(code).toMatchSnapshot() + expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') + expect(code).contains(`_setTemplateRef(n0, bar => { + _foo.value = bar + ;({ baz: _ctx.baz } = bar) + console.log(_foo.value, _ctx.baz) + }, r0)`) + }) + + test('ref + v-if', () => { + const { ir, code } = compileWithTransformRef( + `
`, + ) + + const op = ir.block.dynamic.children[0].operation as IfIRNode + expect(op.type).toBe(IRNodeTypes.IF) + + const { positive } = op + expect(positive.operation).toMatchObject([ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 2, + value: { + content: 'foo', + isStatic: true, + }, + effect: false, + }, + ]) + expect(code).matchSnapshot() + expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') + expect(code).contains('_setTemplateRef(n2, "foo")') + }) + + test('ref + v-for', () => { + const { ir, code } = compileWithTransformRef( + `
`, + ) + + const { render } = ir.block.dynamic.children[0].operation as ForIRNode + expect(render.operation).toMatchObject([ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 2, + value: { + content: 'foo', + isStatic: true, + }, + refFor: true, + effect: false, + }, + ]) + expect(code).matchSnapshot() + expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') + expect(code).contains('_setTemplateRef(n2, "foo", void 0, true)') + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts new file mode 100644 index 00000000000..20fa6d1fd00 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts @@ -0,0 +1,51 @@ +// TODO: add tests for this transform +import { NodeTypes } from '@vue/compiler-dom' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformText, + transformVBind, + transformVOn, +} from '../../src' + +import { makeCompile } from './_utils' + +const compileWithTextTransform = makeCompile({ + nodeTransforms: [transformElement, transformChildren, transformText], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + }, +}) + +describe('compiler: text transform', () => { + it('no consecutive text', () => { + const { code, ir, helpers } = compileWithTextTransform( + '{{ "hello world" }}', + ) + expect(code).toMatchSnapshot() + expect(helpers).contains.all.keys('setText', 'template') + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SET_TEXT, + element: 0, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: '"hello world"', + isStatic: false, + }, + ], + }, + ]) + }) + + it('consecutive text', () => { + const { code, ir, helpers } = compileWithTextTransform('{{ msg }}') + expect(code).toMatchSnapshot() + expect(helpers).contains.all.keys('setText', 'template') + expect(ir.block.operation).toMatchObject([]) + expect(ir.block.effect.length).toBe(1) + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts new file mode 100644 index 00000000000..e96186c275c --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -0,0 +1,846 @@ +import { BindingTypes, ErrorCodes, NodeTypes } from '@vue/compiler-dom' +import { + DynamicFlag, + IRNodeTypes, + transformChildren, + transformElement, + transformVBind, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithVBind = makeCompile({ + nodeTransforms: [transformElement, transformChildren], + directiveTransforms: { + bind: transformVBind, + }, +}) + +describe('compiler v-bind', () => { + test('basic', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.block.dynamic.children[0]).toMatchObject({ + id: 0, + flags: DynamicFlag.REFERENCED, + }) + expect(ir.template).toEqual(['
']) + expect(ir.block.effect).lengthOf(1) + expect(ir.block.effect[0].expressions).lengthOf(1) + expect(ir.block.effect[0].operations).lengthOf(1) + expect(ir.block.effect[0]).toMatchObject({ + expressions: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + }, + ], + operations: [ + { + type: IRNodeTypes.SET_PROP, + element: 0, + prop: { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: true, + loc: { + start: { line: 1, column: 13, offset: 12 }, + end: { line: 1, column: 15, offset: 14 }, + source: 'id', + }, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + loc: { + source: 'id', + start: { line: 1, column: 17, offset: 16 }, + end: { line: 1, column: 19, offset: 18 }, + }, + }, + ], + loc: { + start: { column: 6, line: 1, offset: 5 }, + end: { column: 20, line: 1, offset: 19 }, + source: 'v-bind:id="id"', + }, + runtimeCamelize: false, + }, + }, + ], + }) + + expect(code).matchSnapshot() + expect(code).contains('_setProp(n0, "id", _ctx.id') + }) + + test('no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_PROP, + prop: { + key: { + content: `id`, + isStatic: true, + loc: { + start: { line: 1, column: 13, offset: 12 }, + end: { line: 1, column: 15, offset: 14 }, + }, + }, + values: [ + { + content: `id`, + isStatic: false, + loc: { + start: { line: 1, column: 13, offset: 12 }, + end: { line: 1, column: 15, offset: 14 }, + }, + }, + ], + }, + }) + expect(code).contains('_setProp(n0, "id", _ctx.id)') + }) + + test('no expression (shorthand)', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_PROP, + prop: { + key: { + content: `camel-case`, + isStatic: true, + }, + values: [ + { + content: `camelCase`, + isStatic: false, + }, + ], + }, + }) + expect(code).contains('_setAttr(n0, "camel-case", _ctx.camelCase)') + }) + + test('dynamic arg', () => { + const { ir, code } = compileWithVBind( + `
`, + ) + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + [ + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + }, + ], + }, + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'title', + isStatic: false, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'title', + isStatic: false, + }, + ], + }, + ], + ], + }) + expect(code).contains( + '_setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true)', + ) + }) + + test('dynamic arg w/ static attribute', () => { + const { ir, code } = compileWithVBind( + `
`, + ) + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: 0, + props: [ + [ + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'id', + isStatic: false, + }, + ], + }, + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + values: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'bar', + isStatic: true, + }, + ], + }, + { + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'checked', + isStatic: true, + }, + }, + ], + ], + }) + expect(code).contains( + '_setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true)', + ) + }) + + test('should error if empty expression', () => { + const onError = vi.fn() + const { ir, code } = compileWithVBind(`
`, { + onError, + }) + + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_BIND_NO_EXPRESSION, + loc: { + start: { line: 1, column: 6 }, + end: { line: 1, column: 19 }, + }, + }) + expect(ir.template).toEqual(['
']) + + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('
')) + }) + + test('error on invalid argument for same-name shorthand', () => { + const onError = vi.fn() + compileWithVBind(`
`, { onError }) + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT, + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 18, + }, + }, + }) + }) + + test('.camel modifier', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: undefined, + }, + }) + + expect(code).matchSnapshot() + expect(code).contains('_setProp(n0, "fooBar", _ctx.id)') + }) + + test('.camel modifier w/ no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `fooBar`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: undefined, + }, + }) + expect(code).contains('renderEffect') + expect(code).contains('_setProp(n0, "fooBar", _ctx.fooBar)') + }) + + test('.camel modifier w/ dynamic arg', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_DYNAMIC_PROPS, + props: [ + [ + { + key: { + content: `foo`, + isStatic: false, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: true, + modifier: undefined, + }, + ], + ], + }) + + expect(code).matchSnapshot() + expect(code).contains('renderEffect') + expect(code).contains( + `_setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true)`, + ) + }) + + test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers') + + test('.prop modifier', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '.', + }, + }) + expect(code).contains('renderEffect') + expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.id)') + }) + + test('.prop modifier w/ no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `fooBar`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '.', + }, + }) + expect(code).contains('renderEffect') + expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)') + }) + + test('.prop modifier w/ dynamic arg', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_DYNAMIC_PROPS, + props: [ + [ + { + key: { + content: `fooBar`, + isStatic: false, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '.', + }, + ], + ], + }) + expect(code).contains('renderEffect') + expect(code).contains( + `_setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true)`, + ) + }) + + test.todo('.prop modifier w/ dynamic arg + prefixIdentifiers') + + test('.prop modifier (shorthand)', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '.', + }, + }) + expect(code).contains('renderEffect') + expect(code).contains(' _setDOMProp(n0, "fooBar", _ctx.id)') + }) + + test('.prop modifier (shorthand) w/ no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `fooBar`, + isStatic: true, + }, + values: [ + { + content: `fooBar`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '.', + }, + }) + expect(code).contains('renderEffect') + expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)') + }) + + test('.prop modifier w/ innerHTML', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setHtml(n0, _ctx.foo)') + }) + + test('.prop modifier (shorthand) w/ innerHTML', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setHtml(n0, _ctx.foo)') + }) + + test('.prop modifier w/ textContent', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setText(n0, _ctx.foo)') + }) + + test('.prop modifier (shorthand) w/ textContent', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setText(n0, _ctx.foo)') + }) + + test('.prop modifier w/ value', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setValue(n0, _ctx.foo)') + }) + + test('.prop modifier (shorthand) w/ value', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setValue(n0, _ctx.foo)') + }) + + test('.prop modifier w/ progress value', () => { + const { code } = compileWithVBind(``) + expect(code).matchSnapshot() + expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)') + }) + + test('.prop modifier (shorthand) w/ progress value', () => { + const { code } = compileWithVBind(``) + expect(code).matchSnapshot() + expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)') + }) + + test('.attr modifier', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `foo-bar`, + isStatic: true, + }, + values: [ + { + content: `id`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '^', + }, + }) + expect(code).contains('renderEffect') + expect(code).contains('_setAttr(n0, "foo-bar", _ctx.id)') + }) + + test('.attr modifier w/ no expression', () => { + const { ir, code } = compileWithVBind(`
`) + + expect(code).matchSnapshot() + expect(ir.block.effect[0].operations[0]).toMatchObject({ + prop: { + key: { + content: `foo-bar`, + isStatic: true, + }, + values: [ + { + content: `fooBar`, + isStatic: false, + }, + ], + runtimeCamelize: false, + modifier: '^', + }, + }) + + expect(code).contains('renderEffect') + expect(code).contains('_setAttr(n0, "foo-bar", _ctx.fooBar)') + }) + + test('.attr modifier w/ innerHTML', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n0, "innerHTML", _ctx.foo)') + }) + + test('.attr modifier w/ textContent', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n0, "textContent", _ctx.foo)') + }) + + test('.attr modifier w/ value', () => { + const { code } = compileWithVBind(`
`) + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n0, "value", _ctx.foo)') + }) + + test('.attr modifier w/ progress value', () => { + const { code } = compileWithVBind(``) + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n0, "value", _ctx.foo)') + }) + + test('attributes must be set as attribute', () => { + const { code } = compileWithVBind(` +
+ + `, () => + // h('textarea', { value: 'foo' }), + // ) + // mountWithHydration(``, () => + // h('textarea', { value: '' }), + // ) + // expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + // mountWithHydration(``, () => + // h('textarea', { value: 'bar' }), + // ) + // expect(`Hydration attribute mismatch`).toHaveBeenWarned() + // }) + + // // #11873 + // test('