Merge remote-tracking branch 'github/master' into changing_unwrap_ref

# Conflicts:
#	packages/reactivity/src/ref.ts
#	packages/runtime-core/__tests__/apiTemplateRef.spec.ts
#	packages/runtime-core/src/apiWatch.ts
This commit is contained in:
pikax 2020-04-08 21:21:04 +01:00
commit 5ae74144f2
339 changed files with 26645 additions and 8965 deletions

View File

@ -1,37 +1,63 @@
version: 2 version: 2
defaults: &defaults
docker:
- image: vuejs/ci
step_restore_cache: &restore_cache
restore_cache:
keys:
- v1-dependencies-{{ checksum "yarn.lock" }}-1
- v1-dependencies-
step_install_deps: &install_deps
run:
name: Install Dependencies
command: yarn --frozen-lockfile
step_save_cache: &save_cache
save_cache:
paths:
- node_modules
- packages/compiler-core/node_modules
- packages/compiler-sfc/node_modules
- packages/vue/node_modules
- ~/.cache/yarn
key: v1-dependencies-{{ checksum "yarn.lock" }}-1
jobs: jobs:
build: test:
docker: <<: *defaults
- image: vuejs/ci
working_directory: ~/repo
steps: steps:
- checkout - checkout
- *restore_cache
- *install_deps
- *save_cache
- run: yarn ls-lint
- run: yarn test --ci --runInBand
- restore_cache: test-dts:
keys: <<: *defaults
- v1-dependencies-{{ checksum "yarn.lock" }} steps:
- v1-dependencies- - checkout
- *restore_cache
- *install_deps
- *save_cache
- run: yarn test-dts
- run: check-size:
name: Install Dependencies <<: *defaults
command: yarn --frozen-lockfile steps:
- checkout
- *restore_cache
- *install_deps
- *save_cache
- run: yarn size
- save_cache: workflows:
paths: version: 2
- node_modules ci:
- ~/.cache/yarn jobs:
key: v1-dependencies-{{ checksum "yarn.lock" }} - test
- test-dts
- run: - check-size
name: Run Tests
command: yarn test --ci --runInBand
- run:
name: Test d.ts
command: yarn test-dts
- run:
name: Check size
command: yarn size

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
open_collective: vuejs
patreon: evanyou

View File

@ -19,10 +19,12 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
- Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch. - Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch.
- If adding a new feature: - If adding a new feature:
- Add accompanying test case. - 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. - 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 bug: - If fixing 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)`. - 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. - 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 `yarn test --coverage`. - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `yarn test --coverage`.
@ -41,7 +43,7 @@ You will need [Node.js](http://nodejs.org) **version 10+**, and [Yarn](https://y
After cloning the repo, run: After cloning the repo, run:
``` bash ```bash
$ yarn # install the dependencies of the project $ yarn # install the dependencies of the project
``` ```
@ -60,7 +62,7 @@ The `build` script builds all public packages (packages without `private: true`
Packages to build can be specified with fuzzy matching: Packages to build can be specified with fuzzy matching:
``` bash ```bash
# build runtime-core only # build runtime-core only
yarn build runtime-core yarn build runtime-core
@ -68,38 +70,64 @@ yarn build runtime-core
yarn build runtime --all yarn build runtime --all
``` ```
#### Build Formats
By default, each package will be built in multiple distribution formats as specified in the `buildOptions.formats` field in its `package.json`. These can be overwritten via the `-f` flag. The following formats are supported: By default, each package will be built in multiple distribution formats as specified in the `buildOptions.formats` field in its `package.json`. These can be overwritten via the `-f` flag. The following formats are supported:
- **`global`**: for direct use via `<script>` in the browser. The global variable exposed is specified via the `buildOptions.name` field in a package's `package.json`. - **`global`**:
- **`esm-bundler`**: for use with bundlers like `webpack`, `rollup` and `parcel`.
- **`esm`**: for usage via native ES modules imports (in browser via `<script type="module">`, or via Node.js native ES modules support in the future) - For direct use via `<script>` in the browser. The global variable exposed is specified via the `buildOptions.name` field in a package's `package.json`.
- **`cjs`**: for use in Node.js via `require()`. - Note: global builds are not [UMD](https://github.com/umdjs/umd) builds. Instead they are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE).
- **`esm-bundler`**:
- Leaves prod/dev branches with `process.env.NODE_ENV` guards (to be replaced by bundler)
- Does not ship a minified build (to be done together with the rest of the code after bundling)
- For use with bundlers like `webpack`, `rollup` and `parcel`.
- Imports dependencies (e.g. `@vue/runtime-core`, `@vue/runtime-compiler`)
- Imported dependencies are also `esm-bundler` builds and will in turn import their dependencies (e.g. `@vue/runtime-core` imports `@vue/reactivity`)
- This means you **can** install/import these deps without ending up with different instances of these dependencies
- **`esm`**:
- For usage via native ES modules imports (in browser via `<script type="module">`, or via Node.js native ES modules support in the future)
- Inlines all dependencies - i.e. it's a single ES module with no imports from other files
- This means you **must** import everything from this file and this file only to ensure you are getting the same instance of code.
- Hard-coded prod/dev branches, and the prod build is pre-minified (you will have to use different paths/aliases for dev/prod)
- **`cjs`**:
- For use in Node.js server-side rendering via `require()`.
- The dev/prod files are pre-built, but are dynamically required based on `process.env.NODE_ENV` in `index.js`, which is the default entry when you do `require('vue')`.
For example, to build `runtime-core` with the global build only: For example, to build `runtime-core` with the global build only:
``` bash ```bash
yarn build runtime-core -f global yarn build runtime-core -f global
``` ```
Multiple formats can be specified as a comma-separated list: Multiple formats can be specified as a comma-separated list:
``` bash ```bash
yarn build runtime-core -f esm,cjs yarn build runtime-core -f esm,cjs
``` ```
#### Build with Source Maps
Use the `--sourcemap` or `-s` flag to build with source maps. Note this will make the build much slower.
#### Build with Type Declarations #### Build with Type Declarations
The `--types` flag will generate type declarations during the build and in addition: The `--types` or `-t` flag will generate type declarations during the build and in addition:
- Roll the declarations into a single `.dts` file for each package; - Roll the declarations into a single `.d.ts` file for each package;
- Generate an API report in `<projectRoot>/temp/<packageName>.api.md`. This report contains potential warnings emitted by [api-extractor](https://api-extractor.com/). - Generate an API report in `<projectRoot>/temp/<packageName>.api.md`. This report contains potential warnings emitted by [api-extractor](https://api-extractor.com/).
- Generate an API model json in `<projectRoot>/temp/<packageName>.api.md`. This file can be used to generate a Markdown version of the exported APIs. - Generate an API model json in `<projectRoot>/temp/<packageName>.api.json`. This file can be used to generate a Markdown version of the exported APIs.
### `yarn dev` ### `yarn dev`
The `dev` script bundles a target package (default: `vue`) in a specified format (default: `global`) in dev mode and watches for changes. This is useful when you want to load up a build in an HTML page for quick debugging: The `dev` script bundles a target package (default: `vue`) in a specified format (default: `global`) in dev mode and watches for changes. This is useful when you want to load up a build in an HTML page for quick debugging:
``` bash ```bash
$ yarn dev $ yarn dev
> rollup v1.19.4 > rollup v1.19.4
@ -110,11 +138,13 @@ $ yarn dev
- The `dev` script supports specifying build format via the `-f` flag just like the `build` script. - The `dev` script supports specifying build format via the `-f` flag just like the `build` script.
- The `dev` script also supports the `-s` flag for generating source maps, but it will make rebuilds slower.
### `yarn test` ### `yarn test`
The `yarn test` script simply calls the `jest` binary, so all [Jest CLI Options](https://jestjs.io/docs/en/cli) can be used. Some examples: The `yarn test` script simply calls the `jest` binary, so all [Jest CLI Options](https://jestjs.io/docs/en/cli) can be used. Some examples:
``` bash ```bash
# run all tests # run all tests
$ yarn test $ yarn test
@ -149,11 +179,13 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set
- `compiler-dom`: Compiler with additional plugins specifically targeting the browser. - `compiler-dom`: Compiler with additional plugins specifically targeting the browser.
- `compiler-ssr`: Compiler that produces render functions optimized for server-side rendering.
- `template-explorer`: A development tool for debugging compiler output. You can run `yarn dev template-explorer` and open its `index.html` to get a repl of template compilation based on current source code. - `template-explorer`: A development tool for debugging compiler output. You can run `yarn dev template-explorer` and open its `index.html` to get a repl of template compilation based on current source code.
A [live version](https://vue-next-template-explorer.netlify.com) of the template explorer is also available, which can be used for providing reproductions for compiler bugs. You can also pick the deployment for a specific commit from the [deploy logs](https://app.netlify.com/sites/vue-next-template-explorer/deploys). A [live version](https://vue-next-template-explorer.netlify.com) of the template explorer is also available, which can be used for providing reproductions for compiler bugs. You can also pick the deployment for a specific commit from the [deploy logs](https://app.netlify.com/sites/vue-next-template-explorer/deploys).
- `shared`: **Private.** Platform-agnostic internal utilities shared across multiple packages. This package is private and not published. - `shared`: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).
- `vue`: The public facing "full build" which includes both the runtime AND the compiler. - `vue`: The public facing "full build" which includes both the runtime AND the compiler.
@ -161,20 +193,19 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set
The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed: The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed:
``` js ```js
import { h } from '@vue/runtime-core' import { h } from '@vue/runtime-core'
``` ```
This is made possible via several configurations: This is made possible via several configurations:
- For TypeScript, `compilerOptions.path` in `tsconfig.json` - For TypeScript, `compilerOptions.path` in `tsconfig.json`
- For Jest, `moduleNameMapping` in `jest.config.js` - For Jest, `moduleNameMapper` in `jest.config.js`
- For plain Node.js, they are linked using [Yarn Workspaces](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/). - For plain Node.js, they are linked using [Yarn Workspaces](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/).
### Package Dependencies ### Package Dependencies
``` ```
+---------------------+ +---------------------+
| | | |
| @vue/compiler-sfc | | @vue/compiler-sfc |

16
.github/issue-template.md vendored Normal file
View File

@ -0,0 +1,16 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://new-issue.vuejs.org/?repo=vuejs/vue-next
If your issue was not created using the app above, it will be closed immediately.
中文用户请注意:
请使用上面的链接来创建新的 issue。如果不是用上述工具创建的 issue 会被自动关闭。
-->
<!--
Love vuejs? Please consider supporting us via Patreon or OpenCollective:
👉 https://www.patreon.com/evanyou
👉 https://opencollective.com/vuejs/donate
-->

7
.ls-lint.yml Normal file
View File

@ -0,0 +1,7 @@
ls:
packages/*/{src,__tests__}:
.js: kebab-case
.ts: camelCase | PascalCase
.d.ts: camelCase
.spec.ts: camelCase | PascalCase
.mock.ts: camelCase

537
CHANGELOG.md Normal file
View File

@ -0,0 +1,537 @@
# [3.0.0-alpha.11](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.10...v3.0.0-alpha.11) (2020-04-04)
### Bug Fixes
* **compiler:** fix pre tag whitespace handling ([7f30cb5](https://github.com/vuejs/vue-next/commit/7f30cb577257ad5765261bbffa3cae862259fcab)), closes [#908](https://github.com/vuejs/vue-next/issues/908)
* **compiler-core/slots:** should support on-component named slots ([a022b63](https://github.com/vuejs/vue-next/commit/a022b63605820c97923413ee457ba1fb69a5221e))
* **compiler-sfc:** always use offset for template block sourcemaps ([#911](https://github.com/vuejs/vue-next/issues/911)) ([db50009](https://github.com/vuejs/vue-next/commit/db5000935306214b31e33865cd57935e80e27d41))
* **inject:** allow default value to be `undefined` ([#894](https://github.com/vuejs/vue-next/issues/894)) ([94562da](https://github.com/vuejs/vue-next/commit/94562daea70fde33a340bb7b57746523c3660a8e)), closes [#892](https://github.com/vuejs/vue-next/issues/892)
* **portal:** portal should always remove its children when unmounted ([16cd8ee](https://github.com/vuejs/vue-next/commit/16cd8eee7839cc4613f17642bf37b39f7bdf1fce))
* **reactivity:** scheduled effect should not execute if stopped ([0764c33](https://github.com/vuejs/vue-next/commit/0764c33d3da8c06d472893a4e451e33394726a42)), closes [#910](https://github.com/vuejs/vue-next/issues/910)
* **runtime-core:** support attr merging on child with root level comments ([e42cb54](https://github.com/vuejs/vue-next/commit/e42cb543947d4286115b6adae6e8a5741d909f14)), closes [#904](https://github.com/vuejs/vue-next/issues/904)
* **runtime-dom:** v-cloak should be removed after compile on the root element ([#893](https://github.com/vuejs/vue-next/issues/893)) ([0ed147d](https://github.com/vuejs/vue-next/commit/0ed147d33610b86af72cbadcc4b32e6069bcaf08)), closes [#890](https://github.com/vuejs/vue-next/issues/890)
* **runtome-dom:** properly support creating customized built-in element ([b1d0b04](https://github.com/vuejs/vue-next/commit/b1d0b046afb1e8f4640d8d80b6eeaf9f89e892f7))
* **transition:** warn only when there is more than one rendered child ([#903](https://github.com/vuejs/vue-next/issues/903)) ([37b1dc8](https://github.com/vuejs/vue-next/commit/37b1dc8242608b072d14fd2a5e52f5d40829ea52))
* **types:** allow use PropType with Function ([#915](https://github.com/vuejs/vue-next/issues/915)) ([026eb72](https://github.com/vuejs/vue-next/commit/026eb729f3d1566e95f2f4253d76c20e86d1ec9b)), closes [#748](https://github.com/vuejs/vue-next/issues/748)
* **types:** export missing types from runtime-core ([#889](https://github.com/vuejs/vue-next/issues/889)) ([412ec86](https://github.com/vuejs/vue-next/commit/412ec86128fa33fa41ce435c493fd8275a785fea))
* **types/reactivity:** add generics constraint for markNonReactive ([f3b6559](https://github.com/vuejs/vue-next/commit/f3b6559408fb42ff6dc0c67001c9c67093f2b059)), closes [#917](https://github.com/vuejs/vue-next/issues/917)
### Code Refactoring
* **runtime-core:** adjust attr fallthrough behavior ([21bcdec](https://github.com/vuejs/vue-next/commit/21bcdec9435700cac98868a36716b49a7766c48d))
* rename `<portal>` to `<teleport>` ([eee5095](https://github.com/vuejs/vue-next/commit/eee50956924d7d2c916cdb8b99043da616e53af5))
* **runtime-core:** rename `createAsyncComponent` to `defineAsyncComponent` ([#888](https://github.com/vuejs/vue-next/issues/888)) ([ebc5873](https://github.com/vuejs/vue-next/commit/ebc587376ca1fb4bb8a20d4137332740605753c8))
### Features
* **asyncComponent:** retry support ([c01930e](https://github.com/vuejs/vue-next/commit/c01930e60b4abf481900cdfcc2ba422890c41656))
* **compiler-core:** export `transformElement` from compiler-core ([#907](https://github.com/vuejs/vue-next/issues/907)) ([20f4965](https://github.com/vuejs/vue-next/commit/20f4965b45d410a2fe95310ecf7293b2b7f46f36))
* **compiler-core:** support v-is ([b8ffbff](https://github.com/vuejs/vue-next/commit/b8ffbffaf771c259848743cf4eb1a5ea31795aaa))
* **portal:** hydration support for portal disabled mode ([b74bab2](https://github.com/vuejs/vue-next/commit/b74bab216c3be68ab046451cf5e5b5bec5f19483))
* **portal:** SSR support for multi portal shared target ([e866434](https://github.com/vuejs/vue-next/commit/e866434f0c54498dd0fc47d48287a1d0ada36388))
* **portal:** SSR support for portal disabled prop ([9ed9bf3](https://github.com/vuejs/vue-next/commit/9ed9bf3687a770aebc265839065832761e6bafa1))
* **portal:** support disabled prop ([8ce3da0](https://github.com/vuejs/vue-next/commit/8ce3da0104db9bdd89929724c6d841ac3dfb7336))
* **portal:** support multiple portal appending to same target ([aafb880](https://github.com/vuejs/vue-next/commit/aafb880a0a9e023b62cf8fb3ae269b31f22ac84e))
* **reactivity:** add effect to public api ([#909](https://github.com/vuejs/vue-next/issues/909)) ([6fba241](https://github.com/vuejs/vue-next/commit/6fba2418507d9c65891e8d14bd63736adb377556))
* **runtime-core:** config.performance tracing support ([e93e426](https://github.com/vuejs/vue-next/commit/e93e426bfad13f40c8f1d80b8f45ac5d0926c2fc))
* **runtime-core:** emits validation and warnings ([c7c3a6a](https://github.com/vuejs/vue-next/commit/c7c3a6a3bef6275be8f9f8873358421017bb5386))
* **runtime-core:** failed component resolution should fallback to native element ([cb31eb4](https://github.com/vuejs/vue-next/commit/cb31eb4d0a0afdd2abf9e3897d9aac447dd0264b))
* **runtime-core:** support app.config.globalProperties ([27873db](https://github.com/vuejs/vue-next/commit/27873dbe1c09ac6a058d815949a4e13831513fd0))
* **runtime-core:** type and attr fallthrough support for emits option ([bf473a6](https://github.com/vuejs/vue-next/commit/bf473a64eacab21d734d556c66cc190aa4ff902d))
* **templateRef:** should work with direct reactive property ([449ab03](https://github.com/vuejs/vue-next/commit/449ab039feb10df7179898b13ecc45028a043002)), closes [#901](https://github.com/vuejs/vue-next/issues/901)
* **templateRef:** support template ref for all vnode types ([55b364d](https://github.com/vuejs/vue-next/commit/55b364decc903a6c7fccd1cdcdcfc79948c848a2))
### BREAKING CHANGES
* **runtime-core:** attribute fallthrough behavior has been adjusted
according to https://github.com/vuejs/rfcs/pull/154
* `<portal>` has been renamed to `<teleport>`.
`target` prop is also renamed to `to`, so the new usage will be:
```html
<Teleport to="#modal-layer" :disabled="isMobile">
<div class="modal">
hello
</div>
</Teleport>
```
The primary reason for the renaming is to avoid potential naming
conflict with [native portals](https://wicg.github.io/portals/).
* **asyncComponent:** async component `error` and `loading` options have been
renamed to `errorComponent` and `loadingComponent` respectively.
* **runtime-core:** `createAsyncComponent` has been renamed to `defineAsyncComponent` for consistency with `defineComponent`.
# [3.0.0-alpha.10](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.9...v3.0.0-alpha.10) (2020-03-24)
### Bug Fixes
* fix option merge global mixins presence check ([10ad965](https://github.com/vuejs/vue-next/commit/10ad965100a88e28cb528690f2e09070fefc8872))
* **compiler-core:** assign patchFlag for template v-if fragment ([a1da9c2](https://github.com/vuejs/vue-next/commit/a1da9c28a0a7030124b1deb9369685760c67be47)), closes [#850](https://github.com/vuejs/vue-next/issues/850)
* **compiler-core:** support interpolation in RCDATA mode (e.g. textarea) ([0831b98](https://github.com/vuejs/vue-next/commit/0831b98eac344d9bdfd6f6e922902adb91ea180a))
* **keep-alive:** should update re-activated component with latest props ([1237387](https://github.com/vuejs/vue-next/commit/123738727a0af54fd632bf838dc3aa024722ee41))
* **reactivity:** should not observe frozen objects ([1b2149d](https://github.com/vuejs/vue-next/commit/1b2149dbb2dd224d01e90c1a9332bfe67aa465ce)), closes [#867](https://github.com/vuejs/vue-next/issues/867)
* **reactivity:** should not trigger map keys iteration when keys did not change ([45ba06a](https://github.com/vuejs/vue-next/commit/45ba06ac5f49876b4f05e5996f595b2c4a761f47)), closes [#877](https://github.com/vuejs/vue-next/issues/877)
* **runtime-core:** fix boolean props validation ([3b282e7](https://github.com/vuejs/vue-next/commit/3b282e7e3c96786af0a5ff61822882d1ed3f4db3))
* **runtime-dom:** invalid lineGradient svg tag ([#863](https://github.com/vuejs/vue-next/issues/863)) ([d425818](https://github.com/vuejs/vue-next/commit/d425818901428ff919a0179fc910410cbcfa119b)), closes [#862](https://github.com/vuejs/vue-next/issues/862)
* **TransitionGroup:** ignore comment node when warn (fix[#869](https://github.com/vuejs/vue-next/issues/869)) ([#875](https://github.com/vuejs/vue-next/issues/875)) ([0dba5d4](https://github.com/vuejs/vue-next/commit/0dba5d44e60d33b909f4e4d05663c7ddf746a1f2))
* do not drop SFC runtime behavior code in global builds ([4c1a193](https://github.com/vuejs/vue-next/commit/4c1a193617bee8ace6fad289b78e9d2557cb081e)), closes [#873](https://github.com/vuejs/vue-next/issues/873)
* dynamic component fallback to native element ([f529dbd](https://github.com/vuejs/vue-next/commit/f529dbde236e9eaedbded78e926951a189234f9c)), closes [#870](https://github.com/vuejs/vue-next/issues/870)
* **runtime-core:** fix component proxy props presence check ([b3890a9](https://github.com/vuejs/vue-next/commit/b3890a93e39342fd16e5fd72c59f361fc211309c)), closes [#864](https://github.com/vuejs/vue-next/issues/864)
* **suspense:** clear effects on suspense resolve ([ebc1ca8](https://github.com/vuejs/vue-next/commit/ebc1ca8eff82789987c09a9f6a934898b00153ff))
* **transition:** fix duration prop validation ([0dc2478](https://github.com/vuejs/vue-next/commit/0dc24785699101fa24d2a68786feaaac8a887520)), closes [#868](https://github.com/vuejs/vue-next/issues/868)
### Features
* **asyncComponent:** SSR/hydration support for async component ([cba2f1a](https://github.com/vuejs/vue-next/commit/cba2f1aadbd0d4ae246040ecd5a91d8dd4e8fd1a))
* **runtime-core:** async component support ([c3bb316](https://github.com/vuejs/vue-next/commit/c3bb3169f497fc834654d8ae700f18b1a6613127))
* **runtime-core:** support `config.optionMergeStrategies` ([528621b](https://github.com/vuejs/vue-next/commit/528621ba41b1d7113940077574217d01d182b35f))
* add hook for transforming h's arguments ([#851](https://github.com/vuejs/vue-next/issues/851)) ([b7d1e0f](https://github.com/vuejs/vue-next/commit/b7d1e0fa2ffe4561a589580eca6e92171c311347))
### Performance Improvements
* **transform-vif:** don't need to createBlock for a component ([#853](https://github.com/vuejs/vue-next/issues/853)) ([a3601e9](https://github.com/vuejs/vue-next/commit/a3601e9fa73d10f524ed3bdf3ae44df8847c1230))
# [3.0.0-alpha.9](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.8...v3.0.0-alpha.9) (2020-03-16)
### Bug Fixes
* **build:** remove __RUNTIME_COMPILE__ flag ([206640a](https://github.com/vuejs/vue-next/commit/206640a2d859a9ce9c19f22e201692f15a8d1da3)), closes [#817](https://github.com/vuejs/vue-next/issues/817)
* **compiler-core:** fix property shorthand detection ([586e5bb](https://github.com/vuejs/vue-next/commit/586e5bb8003916ba6be9b3394087df80328657f4)), closes [#845](https://github.com/vuejs/vue-next/issues/845)
* **compiler-ssr:** fix input w/ v-bind="obj" codegen ([3b40fc5](https://github.com/vuejs/vue-next/commit/3b40fc56dba56a5c1085582d11f3287e9317a151))
* **compiler-ssr:** should pass necessary tag names for dynamic v-bind ([a46f3b3](https://github.com/vuejs/vue-next/commit/a46f3b354d451a857df750a318bd0536338008cd))
* **runtime-core:** always set invalid vnode type ([#820](https://github.com/vuejs/vue-next/issues/820)) ([28a9bee](https://github.com/vuejs/vue-next/commit/28a9beed1624de9812e0f4ce9b63f7f3ed2c6db8))
* **runtime-core:** empty boolean props ([#844](https://github.com/vuejs/vue-next/issues/844)) ([c7ae269](https://github.com/vuejs/vue-next/commit/c7ae2699724bd5206ce7d2db73b86c1ef5947641)), closes [#843](https://github.com/vuejs/vue-next/issues/843)
* **runtime-core:** pass instance proxy as data() argument ([#828](https://github.com/vuejs/vue-next/issues/828)) ([d9dd1d8](https://github.com/vuejs/vue-next/commit/d9dd1d8a0ac81d7d463e0788bb2e75b2d4866db6))
* **runtime-dom:** patch xlink attribute ([#842](https://github.com/vuejs/vue-next/issues/842)) ([d318576](https://github.com/vuejs/vue-next/commit/d318576d74f8756e471942ff44d2af2a4661d775))
* simplify and use correct ctx in withCtx ([4dc8ffc](https://github.com/vuejs/vue-next/commit/4dc8ffc3788c38aff3e4c0f271d0ca111f723140))
* **runtime-core:** pass prev value to hostPatchProp ([#809](https://github.com/vuejs/vue-next/issues/809)) ([cd34603](https://github.com/vuejs/vue-next/commit/cd34603864142d5468486ec3f379679b22014a1b)), closes [#808](https://github.com/vuejs/vue-next/issues/808)
* **runtime-core:** should allow empty string and 0 as valid vnode key ([#807](https://github.com/vuejs/vue-next/issues/807)) ([54a0e93](https://github.com/vuejs/vue-next/commit/54a0e93c276f95a35b3bd6510a7f52d967fd3b7f))
* **types:** app.component should accept defineComponent return type ([#822](https://github.com/vuejs/vue-next/issues/822)) ([1e9d131](https://github.com/vuejs/vue-next/commit/1e9d1319c3f66a0a7430a4f6ac7b508486894b6b)), closes [#730](https://github.com/vuejs/vue-next/issues/730)
### Code Refactoring
* **runtime-core:** adjust patchProp value arguments order ([ca5f39e](https://github.com/vuejs/vue-next/commit/ca5f39ee3501a1d9cacdb74108318c15ee7c0abb))
### Features
* **compiler-core:** wrap slot functions with render context ([ecd7ce6](https://github.com/vuejs/vue-next/commit/ecd7ce60d5234a7a0dbc11add6a690c3f9ff0617))
* **compiler-sfc:** add ssr option ([3b2d236](https://github.com/vuejs/vue-next/commit/3b2d23671409f8ac358252311bf5212882fa985a))
* **runtime-core:** add special property to get class component options ([#821](https://github.com/vuejs/vue-next/issues/821)) ([dd17fa1](https://github.com/vuejs/vue-next/commit/dd17fa1c9071b9685c379e1b12102214b757cf35))
* **runtime-core:** implement RFC-0020 ([bb7fa3d](https://github.com/vuejs/vue-next/commit/bb7fa3dabce73de63d016c75f1477e7d8bed8858))
* **runtime-core:** set context for manual slot functions as well ([8a58dce](https://github.com/vuejs/vue-next/commit/8a58dce6034944b18c2e507b5d9ab8177f60e269))
* **server-renderer:** render suspense in vnode mode ([#727](https://github.com/vuejs/vue-next/issues/727)) ([589aeb4](https://github.com/vuejs/vue-next/commit/589aeb402c58f463cc32d5e7728b56614bc9bf33))
* **ssr:** compiler-ssr support for Suspense ([80c625d](https://github.com/vuejs/vue-next/commit/80c625dce33610e53c953e9fb8fde26e3e10e358))
* **ssr:** hide comment anchors during hydration in dev mode ([cad5bcc](https://github.com/vuejs/vue-next/commit/cad5bcce40b9f2aaa520ccbd377cd5419650e55f))
* **ssr:** improve fragment mismatch handling ([60ed4e7](https://github.com/vuejs/vue-next/commit/60ed4e7e0821a2932660b87fbf8d5ca953e0e073))
* **ssr:** support getSSRProps for vnode directives ([c450ede](https://github.com/vuejs/vue-next/commit/c450ede12d1a93a70271a2fe7fcb6f8efcf1cd4c))
* **ssr/suspense:** suspense hydration ([a3cc970](https://github.com/vuejs/vue-next/commit/a3cc970030579f2c55d893d6e83bbc05324adad4))
* **types:** export `ErrorTypes` ([#840](https://github.com/vuejs/vue-next/issues/840)) ([760c3e0](https://github.com/vuejs/vue-next/commit/760c3e0fd67f6360995cdbb125f9eae4e024f3af))
### Reverts
* Revert "refactor(directives): remove binding.instance" ([2370166](https://github.com/vuejs/vue-next/commit/23701666cb487e55d05b74d66990361051715ba4))
### BREAKING CHANGES
* **runtime-core:** data no longer supports object format (per RFC-0020)
* **runtime-core:** `RendererOptions.patchProp` arguments order has changed
The `prevValue` and `nextValue` position has been swapped to keep it
consistent with other functions in the renderer implementation. This
only affects custom renderers using the `createRenderer` API.
# [3.0.0-alpha.8](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.7...v3.0.0-alpha.8) (2020-03-06)
### Bug Fixes
* **directives:** ignore invalid directive hooks ([7971b04](https://github.com/vuejs/vue-next/commit/7971b0468c81483dd7026204518f7c03187d13c4)), closes [#795](https://github.com/vuejs/vue-next/issues/795)
* **portal:** fix portal placeholder text ([4397528](https://github.com/vuejs/vue-next/commit/439752822c175c737e58896e0f365f2b02bab577))
* **reactivity:** allow effect trigger inside no-track execution contexts ([274f81c](https://github.com/vuejs/vue-next/commit/274f81c5db83f0f77e1aba3240b2134a2474a72f)), closes [#804](https://github.com/vuejs/vue-next/issues/804)
* **reactivity:** Map/Set identity methods should work even if raw value contains reactive entries ([cc69fd7](https://github.com/vuejs/vue-next/commit/cc69fd72e3f9ef3572d2be40af71d22232e1b9af)), closes [#799](https://github.com/vuejs/vue-next/issues/799)
* **reactivity:** should not trigger length dependency on Array delete ([a306658](https://github.com/vuejs/vue-next/commit/a3066581f3014aae31f2d96b96428100f1674166)), closes [#774](https://github.com/vuejs/vue-next/issues/774)
* **runtime-core:** ensure inhertied attrs update on optimized child root ([6810d14](https://github.com/vuejs/vue-next/commit/6810d1402e214a12fa274ff5fb7475bad002d1b1)), closes [#677](https://github.com/vuejs/vue-next/issues/677) [#784](https://github.com/vuejs/vue-next/issues/784)
* **slots:** fix conditional slot ([3357ff4](https://github.com/vuejs/vue-next/commit/3357ff438c6ff0d4fea67923724dd3cb99ff2756)), closes [#787](https://github.com/vuejs/vue-next/issues/787)
* **ssr:** fix ssr on-the-fly compilation + slot fallback branch helper injection ([3be3785](https://github.com/vuejs/vue-next/commit/3be3785f945253918469da456a14a2d9381bcbd0))
### Code Refactoring
* **runtime-core:** adjust attr fallthrough behavior ([e1660f4](https://github.com/vuejs/vue-next/commit/e1660f4338fbf4d2a434e13193a58e00f844379b)), closes [#749](https://github.com/vuejs/vue-next/issues/749)
* **runtime-core:** revert setup() result reactive conversion ([e67f655](https://github.com/vuejs/vue-next/commit/e67f655b2687042fcc74dc0993581405abed56de))
### Features
* **compiler-core:** switch to @babel/parser for expression parsing ([8449a97](https://github.com/vuejs/vue-next/commit/8449a9727c942b6049c9e577c7c15b43fdca2867))
* **compiler-ssr:** compile portal ([#775](https://github.com/vuejs/vue-next/issues/775)) ([d8ed0e7](https://github.com/vuejs/vue-next/commit/d8ed0e7fbf9bbe734667eb94e809235e79e431eb))
* **ssr:** hydration mismatch handling ([91269da](https://github.com/vuejs/vue-next/commit/91269da52c30abf6c50312555b715f5360224bb0))
### BREAKING CHANGES
* **runtime-core:** adjust attr fallthrough behavior
Updated per pending RFC https://github.com/vuejs/rfcs/pull/137
- Implicit fallthrough now by default only applies for a whitelist
of attributes (class, style, event listeners, a11y attributes, and
data attributes).
- Fallthrough is now applied regardless of whether the component has
* **runtime-core:** revert setup() result reactive conversion
Revert 6b10f0c & a840e7d. The motivation of the original change was
avoiding unnecessary deep conversions, but that can be achieved by
explicitly marking values non-reactive via `markNonReactive`.
Removing the reactive conversion behavior leads to an usability
issue in that plain objects containing refs (which is what most
composition functions will return), when exposed as a nested
property from `setup()`, will not unwrap the refs in templates. This
goes against the "no .value in template" intuition and the only
workaround requires users to manually wrap it again with `reactive()`.
So in this commit we are reverting to the previous behavior where
objects returned from `setup()` are implicitly wrapped with
`reactive()` for deep ref unwrapping.
# [3.0.0-alpha.7](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.6...v3.0.0-alpha.7) (2020-02-26)
### Bug Fixes
* **renderSlot:** set slot render as a STABLE_FRAGMENT ([#776](https://github.com/vuejs/vue-next/issues/776)) ([8cb0b83](https://github.com/vuejs/vue-next/commit/8cb0b8308801159177ec16ab5a3e23672c4c1d00)), closes [#766](https://github.com/vuejs/vue-next/issues/766)
* **runtime-core:** fix slot fallback + slots typing ([4a5b91b](https://github.com/vuejs/vue-next/commit/4a5b91bd1faec76bbaa0522b095f4a07ca88a9e5)), closes [#773](https://github.com/vuejs/vue-next/issues/773)
* **runtime-core:** make watchEffect ignore deep option ([#765](https://github.com/vuejs/vue-next/issues/765)) ([19a799c](https://github.com/vuejs/vue-next/commit/19a799c28b149b14e85d9e2081fa65ed58d108ba))
* **runtime-core:** set appContext.provides to Object.create(null) ([#781](https://github.com/vuejs/vue-next/issues/781)) ([04f83fa](https://github.com/vuejs/vue-next/commit/04f83fa6810e07915e98b94c954ff0c1859aaa49))
* **template-explorer:** rename watch -> watchEffect ([#780](https://github.com/vuejs/vue-next/issues/780)) ([59393dd](https://github.com/vuejs/vue-next/commit/59393dd75766720330cb69e22086c97a392dbbe4))
* **template-ref:** fix string template refs inside slots ([3eab143](https://github.com/vuejs/vue-next/commit/3eab1438432a3bab15ccf2f6092fc3e4355f3cdd))
* **types:** ref value type unwrapping should happen at creation time ([d4c6957](https://github.com/vuejs/vue-next/commit/d4c6957e2d8ac7920a649f3a3576689cd5e1099f))
* **types:** shallowRef should not unwrap value type ([3206e5d](https://github.com/vuejs/vue-next/commit/3206e5dfe58fd0e93644d13929558d71c5171888))
### Code Refactoring
* **directives:** remove binding.instance ([52cc7e8](https://github.com/vuejs/vue-next/commit/52cc7e823148289b3dcdcb6b521984ab815fce79))
### BREAKING CHANGES
* **directives:** custom directive bindings no longer expose instance
This is a rarely used property that creates extra complexity in
ensuring it points to the correct instance. From a design
perspective, a custom directive should be scoped to the element and
data it is bound to and should not have access to the entire
instance in the first place.
# [3.0.0-alpha.6](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.5...v3.0.0-alpha.6) (2020-02-22)
### Bug Fixes
* **compiler-core:** should alias name in helperString ([#743](https://github.com/vuejs/vue-next/issues/743)) ([7b987d9](https://github.com/vuejs/vue-next/commit/7b987d9450fc7befcd0946a0d53991d27ed299ec)), closes [#740](https://github.com/vuejs/vue-next/issues/740)
* **compiler-dom:** properly stringify class/style bindings when hoisting static strings ([1b9b235](https://github.com/vuejs/vue-next/commit/1b9b235663b75db040172d2ffbee1dd40b4db032))
* **reactivity:** should trigger all effects when array length is mutated ([#754](https://github.com/vuejs/vue-next/issues/754)) ([5fac655](https://github.com/vuejs/vue-next/commit/5fac65589b4455b98fd4e2f9eb3754f0acde97bb))
* **sfc:** inherit parent scopeId on child root ([#756](https://github.com/vuejs/vue-next/issues/756)) ([9547c2b](https://github.com/vuejs/vue-next/commit/9547c2b93d6d8f469314cfe055960746a3e3acbe))
* **types:** improve ref typing, close [#759](https://github.com/vuejs/vue-next/issues/759) ([627b9df](https://github.com/vuejs/vue-next/commit/627b9df4a293ae18071009d9cac7a5e995d40716))
* **types:** update setup binding unwrap types for 6b10f0c ([a840e7d](https://github.com/vuejs/vue-next/commit/a840e7ddf0b470b5da27b7b2b8b5fcf39a7197a2)), closes [#738](https://github.com/vuejs/vue-next/issues/738)
### Code Refactoring
* preserve refs in reactive arrays ([775a7c2](https://github.com/vuejs/vue-next/commit/775a7c2b414ca44d4684badb29e8e80ff6b5d3dd)), closes [#737](https://github.com/vuejs/vue-next/issues/737)
### Features
* **reactivity:** expose unref and shallowRef ([e9024bf](https://github.com/vuejs/vue-next/commit/e9024bf1b7456b9cf9b913c239502593364bc773))
* **runtime-core:** add watchEffect API ([99a2e18](https://github.com/vuejs/vue-next/commit/99a2e18c9711d3d1f79f8c9c59212880efd058b9))
### Performance Improvements
* **effect:** optimize effect trigger for array length mutation ([#761](https://github.com/vuejs/vue-next/issues/761)) ([76c7f54](https://github.com/vuejs/vue-next/commit/76c7f5426919f9d29a303263bc54a1e42a66e94b))
* **reactivity:** only trigger all effects on Array length mutation if new length is shorter than old length ([33622d6](https://github.com/vuejs/vue-next/commit/33622d63600ba0f18ba4dae97bda882c918b5f7d))
### BREAKING CHANGES
* **runtime-core:** replace `watch(fn, options?)` with `watchEffect`
The `watch(fn, options?)` signature has been replaced by the new
`watchEffect` API, which has the same usage and behavior. `watch`
now only supports the `watch(source, cb, options?)` signature.
* **reactivity:** reactive arrays no longer unwraps contained refs
When reactive arrays contain refs, especially a mix of refs and
plain values, Array prototype methods will fail to function
properly - e.g. sort() or reverse() will overwrite the ref's value
instead of moving it (see #737).
Ensuring correct behavior for all possible Array methods while
retaining the ref unwrapping behavior is exceedingly complicated; In
addition, even if Vue handles the built-in methods internally, it
would still break when the user attempts to use a 3rd party utility
function (e.g. lodash) on a reactive array containing refs.
After this commit, similar to other collection types like Map and
Set, Arrays will no longer automatically unwrap contained refs.
The usage of mixed refs and plain values in Arrays should be rare in
practice. In cases where this is necessary, the user can create a
computed property that performs the unwrapping.
# [3.0.0-alpha.5](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.4...v3.0.0-alpha.5) (2020-02-18)
### Bug Fixes
* **compiler:** fix v-for fragment openBlock argument ([12fcf9a](https://github.com/vuejs/vue-next/commit/12fcf9ab953acdbb8706b549c7e63f69482a495a))
* **compiler-core:** fix keep-alive when used in templates ([ade07c6](https://github.com/vuejs/vue-next/commit/ade07c64a1f98c0958e80db0458c699c21998f64)), closes [#715](https://github.com/vuejs/vue-next/issues/715)
* **compiler-core:** only check is prop on `<component>` ([78c4f32](https://github.com/vuejs/vue-next/commit/78c4f321cd0902a117c599ac705dda294fa198ed))
* **compiler-core:** relax error on unknown entities ([730d329](https://github.com/vuejs/vue-next/commit/730d329f794caf1ea2cc47628f8d74ef2d07f96e)), closes [#663](https://github.com/vuejs/vue-next/issues/663)
* **compiler-core:** should apply text transform to if branches ([e0f3c6b](https://github.com/vuejs/vue-next/commit/e0f3c6b352ab35adcad779ef0ac9670acf3d7b37)), closes [#725](https://github.com/vuejs/vue-next/issues/725)
* **compiler-core:** should not hoist element with cached + merged event handlers ([5455e8e](https://github.com/vuejs/vue-next/commit/5455e8e69a59cd1ff72330b1aed9c8e6aedc4b36))
* **compiler-dom:** fix duplicated transforms ([9e51297](https://github.com/vuejs/vue-next/commit/9e51297702f975ced1cfebad9a46afc46f0593bb))
* **compiler-sfc:** handle empty nodes with src attribute ([#695](https://github.com/vuejs/vue-next/issues/695)) ([2d56dfd](https://github.com/vuejs/vue-next/commit/2d56dfdc4fcf824bba4c0166ca5471258c4f883b))
* **compiler-ssr:** import helpers from correct packages ([8f6b669](https://github.com/vuejs/vue-next/commit/8f6b6690a2011846446804267ec49073996c3800))
* **computed:** support arrow function usage for computed option ([2fb7a63](https://github.com/vuejs/vue-next/commit/2fb7a63943d9d995248cb6d2d4fb5f22ff2ac000)), closes [#733](https://github.com/vuejs/vue-next/issues/733)
* **reactivity:** avoid cross-component dependency leaks in setup() ([d9d63f2](https://github.com/vuejs/vue-next/commit/d9d63f21b1e6f99f2fb63d736501095b131e5ad9))
* **reactivity:** effect should handle self dependency mutations ([e8e6772](https://github.com/vuejs/vue-next/commit/e8e67729cb7649d736be233b2a5e00768dd6f4ba))
* **reactivity:** trigger iteration effect on Map.set ([e1c9153](https://github.com/vuejs/vue-next/commit/e1c9153b9ed71f9b2e1ad4f9018c51d239e7dcd0)), closes [#709](https://github.com/vuejs/vue-next/issues/709)
* **runtime-core:** ensure renderCache always exists ([8383e54](https://github.com/vuejs/vue-next/commit/8383e5450e4f9679ac8a284f1c3960e3ee5b5211))
* **runtime-core:** fix keep-alive tree-shaking ([5b43764](https://github.com/vuejs/vue-next/commit/5b43764eacb59ff6ebba3195a55af4ac0cf253bb))
* **runtime-core:** fix ShapeFlags tree shaking ([0f67aa7](https://github.com/vuejs/vue-next/commit/0f67aa7da50d6ffc543754a42f1e677af11f9173))
* **runtime-core:** handle component updates with only class/style bindings ([35d91f4](https://github.com/vuejs/vue-next/commit/35d91f4e18ccb72cbf39a86fe8f39060f0bf075e))
* **runtime-core:** render context set should not unwrap reactive values ([27fbfbd](https://github.com/vuejs/vue-next/commit/27fbfbdb8beffc96134c931425f33178c23a72db))
* **runtime-core:** rework vnode hooks handling ([cfadb98](https://github.com/vuejs/vue-next/commit/cfadb98011e188114bb822ee6f678cd09ddac7e3)), closes [#684](https://github.com/vuejs/vue-next/issues/684)
* **runtime-core:** should not return early on text patchFlag ([778f3a5](https://github.com/vuejs/vue-next/commit/778f3a5e886a1a1136bc8b00b849370d7c4041be))
* **runtime-core/scheduler:** avoid duplicate updates of child component ([8a87074](https://github.com/vuejs/vue-next/commit/8a87074df013fdbb0e88f34074c2605e4af2937c))
* **runtime-core/scheduler:** invalidate job ([#717](https://github.com/vuejs/vue-next/issues/717)) ([fe9da2d](https://github.com/vuejs/vue-next/commit/fe9da2d0e4f9b338252b1b62941ee9ead71f0346))
* **runtime-core/watch:** trigger watcher with undefined as initial value ([#687](https://github.com/vuejs/vue-next/issues/687)) ([5742a0b](https://github.com/vuejs/vue-next/commit/5742a0b826fe77d2310acb530667adb758822f66)), closes [#683](https://github.com/vuejs/vue-next/issues/683)
* **runtime-dom/ssr:** properly handle xlink and boolean attributes ([e6e2c58](https://github.com/vuejs/vue-next/commit/e6e2c58234cab46fa530c383c0f7ae1cb3494da3))
* **ssr:** avoid hard-coded ssr checks in cjs builds ([bc07e95](https://github.com/vuejs/vue-next/commit/bc07e95ca84686bfa43798a444a3220581b183d8))
* **ssr:** fix class/style rendering + ssrRenderComponent export name ([688ad92](https://github.com/vuejs/vue-next/commit/688ad9239105625f7b63ac43181dfb2e9d1d4720))
* **ssr:** render components returning render function from setup ([#720](https://github.com/vuejs/vue-next/issues/720)) ([4669215](https://github.com/vuejs/vue-next/commit/4669215ca2f82d90a1bd730613259f3167e199cd))
* **transition-group:** handle multiple move-classes ([#679](https://github.com/vuejs/vue-next/issues/679)) ([5495c70](https://github.com/vuejs/vue-next/commit/5495c70c4a3f740ef4ac575ffee5466ca747cca1)), closes [#678](https://github.com/vuejs/vue-next/issues/678)
* **types:** app.component should accept defineComponent return type ([57ee5df](https://github.com/vuejs/vue-next/commit/57ee5df364f03816e548f4f3bf05edc7a089c362)), closes [#730](https://github.com/vuejs/vue-next/issues/730)
* **types:** ensure correct oldValue typing based on lazy option ([c6a9787](https://github.com/vuejs/vue-next/commit/c6a9787941ca99877d268182a5bb57fcf8b80b75)), closes [#719](https://github.com/vuejs/vue-next/issues/719)
* **v-on:** transform click.right and click.middle modifiers ([028f748](https://github.com/vuejs/vue-next/commit/028f748c32f80842be39897fdacc37f6700f00a7)), closes [#735](https://github.com/vuejs/vue-next/issues/735)
* remove effect from public API ([4bc4cb9](https://github.com/vuejs/vue-next/commit/4bc4cb970f7a65177948c5d817bb43ecb0324636)), closes [#712](https://github.com/vuejs/vue-next/issues/712)
* **v-model:** should use dynamic directive on input with dynamic v-bind ([1f2de9e](https://github.com/vuejs/vue-next/commit/1f2de9e232409b09c97b67d0824d1450beed6eb1))
### Code Refactoring
* **watch:** adjust watch API behavior ([9571ede](https://github.com/vuejs/vue-next/commit/9571ede84bb6949e13c25807cc8f016ace29dc8a))
### Features
* **compiler:** mark hoisted trees with patchFlag ([175f8aa](https://github.com/vuejs/vue-next/commit/175f8aae8d009e044e3674f7647bf1397f3a794a))
* **compiler:** warn invalid children for transition and keep-alive ([4cc39e1](https://github.com/vuejs/vue-next/commit/4cc39e14a297f42230f5aac5ec08e3c98902b98d))
* **compiler-core:** support mode: cjs in codegen ([04da2a8](https://github.com/vuejs/vue-next/commit/04da2a82e8fbde2b60b2392bc4bdcc5e61113202))
* **compiler-core/v-on:** support [@vnode-xxx](https://github.com/vnode-xxx) usage for vnode hooks ([571ed42](https://github.com/vuejs/vue-next/commit/571ed4226be618dcc9f95e4c2da8d82d7d2f7750))
* **compiler-dom:** handle constant expressions when stringifying static content ([8b7c162](https://github.com/vuejs/vue-next/commit/8b7c162125cb72068727a76ede8afa2896251db0))
* **compiler-dom/runtime-dom:** stringify eligible static trees ([27913e6](https://github.com/vuejs/vue-next/commit/27913e661ac551f580bd5fd42b49fe55cbe8dbb8))
* **reactivity:** add shallowReactive function ([#689](https://github.com/vuejs/vue-next/issues/689)) ([7f38c1e](https://github.com/vuejs/vue-next/commit/7f38c1e0ff5a7591f67ed21aa3a2944db2e72a27))
* **runtime-core/reactivity:** expose shallowReactive ([#711](https://github.com/vuejs/vue-next/issues/711)) ([21944c4](https://github.com/vuejs/vue-next/commit/21944c4a42a65f20245794fa5f07add579b7121f))
* **server-renderer:** support on-the-fly template compilation ([#707](https://github.com/vuejs/vue-next/issues/707)) ([6d10a6c](https://github.com/vuejs/vue-next/commit/6d10a6c77242aec98103f15d6cb672ba63c18abf))
* **ssr:** render portals ([#714](https://github.com/vuejs/vue-next/issues/714)) ([e495fa4](https://github.com/vuejs/vue-next/commit/e495fa4a1872d03ed59252e7ed5dd2b708adb7ae))
* **ssr:** support portal hydration ([70dc3e3](https://github.com/vuejs/vue-next/commit/70dc3e3ae74f08d53243e6f078794c16f359e272))
* **ssr:** useSSRContext ([fd03149](https://github.com/vuejs/vue-next/commit/fd031490fb89b7c0d1d478b586151a24324101a3))
### Performance Improvements
* prevent renderer hot functions being inlined by minifiers ([629ee75](https://github.com/vuejs/vue-next/commit/629ee75588fc2ca4ab2b3786046f788d3547b6bc))
* **reactivity:** better computed tracking ([#710](https://github.com/vuejs/vue-next/issues/710)) ([8874b21](https://github.com/vuejs/vue-next/commit/8874b21a7e2383a8bb6c15a7095c1853aa5ae705))
### BREAKING CHANGES
* **watch:** `watch` behavior has been adjusted.
- When using the `watch(source, callback, options?)` signature, the
callback now fires lazily by default (consistent with 2.x
behavior).
Note that the `watch(effect, options?)` signature is still eager,
since it must invoke the `effect` immediately to collect
dependencies.
- The `lazy` option has been replaced by the opposite `immediate`
option, which defaults to `false`. (It's ignored when using the
effect signature)
- Due to the above changes, the `watch` option in Options API now
behaves exactly the same as 2.x.
- When using the effect signature or `{ immediate: true }`, the
initial execution is now performed synchronously instead of
deferred until the component is mounted. This is necessary for
certain use cases to work properly with `async setup()` and
Suspense.
The side effect of this is the immediate watcher invocation will
no longer have access to the mounted DOM. However, the watcher can
be initiated inside `onMounted` to retain previous behavior.
# [3.0.0-alpha.4](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.3...v3.0.0-alpha.4) (2020-01-27)
### Bug Fixes
* **reactivity:** Array methods relying on identity should work with raw values ([aefb7d2](https://github.com/vuejs/vue-next/commit/aefb7d282ed716923ca1a288a63a83a94af87ebc))
* **runtime-core:** instance should not expose non-declared props ([2884831](https://github.com/vuejs/vue-next/commit/2884831065e16ccf5bd3ae1ee95116803ee3b18c))
* **runtime-dom:** should not access document in non-browser env ([48152bc](https://github.com/vuejs/vue-next/commit/48152bc88ea817ae23e2987dce99d64b426366c1)), closes [#657](https://github.com/vuejs/vue-next/issues/657)
* **v-model/emit:** update:camelCase events should trigger kebab case equivalent ([2837ce8](https://github.com/vuejs/vue-next/commit/2837ce842856d51dfbb55e3fa4a36a352446fb54)), closes [#656](https://github.com/vuejs/vue-next/issues/656)
### Code Refactoring
* adjust `createApp` related API signatures ([c07751f](https://github.com/vuejs/vue-next/commit/c07751fd3605f301dc0f02fd2a48acc7ba7a0397))
* remove implicit reactive() call on renderContext ([6b10f0c](https://github.com/vuejs/vue-next/commit/6b10f0cd1da942c1d96746672b5f595df7d125b5))
### Performance Improvements
* **ssr:** avoid unnecessary async overhead ([297282a](https://github.com/vuejs/vue-next/commit/297282a81259289bfed207d0c9393337aea70117))
### BREAKING CHANGES
* object returned from `setup()` are no longer implicitly
passed to `reactive()`.
The renderContext is the object returned by `setup()` (or a new object
if no setup() is present). Before this change, it was implicitly passed
to `reactive()` for ref unwrapping. But this has the side effect of
unnecessary deep reactive conversion on properties that should not be
made reactive (e.g. computed return values and injected non-reactive
objects), and can lead to performance issues.
This change removes the `reactive()` call and instead performs a
shallow ref unwrapping at the render proxy level. The breaking part is
when the user returns an object with a plain property from `setup()`,
e.g. `return { count: 0 }`, this property will no longer trigger
updates when mutated by a in-template event handler. Instead, explicit
refs are required.
This also means that any objects not explicitly made reactive in
`setup()` will remain non-reactive. This can be desirable when
exposing heavy external stateful objects on `this`.
* `createApp` API has been adjusted.
- `createApp()` now accepts the root component, and optionally a props
object to pass to the root component.
- `app.mount()` now accepts a single argument (the root container)
- `app.unmount()` no longer requires arguments.
New behavior looks like the following:
``` js
const app = createApp(RootComponent)
app.mount('#app')
app.unmount()
```
# [3.0.0-alpha.3](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.2...v3.0.0-alpha.3) (2020-01-22)
### Bug Fixes
* Suspense should include into dynamic children ([#653](https://github.com/vuejs/vue-next/issues/653)) ([ec63623](https://github.com/vuejs/vue-next/commit/ec63623fe8d395e1cd759f27b90b1ccc1b616931)), closes [#649](https://github.com/vuejs/vue-next/issues/649)
* **compiler-core:** avoid override user keys when injecting branch key ([#630](https://github.com/vuejs/vue-next/issues/630)) ([aca2c2a](https://github.com/vuejs/vue-next/commit/aca2c2a81e2793befce516378a02afd1e4da3d3d))
* **compiler-core:** force `<svg>` into blocks for correct runtime isSVG ([f2ac28b](https://github.com/vuejs/vue-next/commit/f2ac28b31e9f1e8ebcd68ca9a1e8ea29653b0916))
* **compiler-sfc:** only transform relative asset URLs ([#628](https://github.com/vuejs/vue-next/issues/628)) ([c71ca35](https://github.com/vuejs/vue-next/commit/c71ca354b9368135b55676c5817cebffaf3fd9c5))
* **dom:** fix `<svg>` and `<foreignObject>` mount and updates ([4f06eeb](https://github.com/vuejs/vue-next/commit/4f06eebc1c2a29d0e4165c6e87f849732ec2cd0f))
* **runtime-core:** condition for parent node check should be any different nodes ([c35fea3](https://github.com/vuejs/vue-next/commit/c35fea3d608acbb571ace6693284061e1cadf7ba)), closes [#622](https://github.com/vuejs/vue-next/issues/622)
* **runtime-core:** isSVG check should also apply for patch branch ([035b656](https://github.com/vuejs/vue-next/commit/035b6560f7eb64ce940ed0d06e19086ad9a3890f)), closes [#639](https://github.com/vuejs/vue-next/issues/639)
* **runtime-core:** should not warn unused attrs when accessed via setup context ([751d838](https://github.com/vuejs/vue-next/commit/751d838fb963e580a40df2d84840ba2198480185)), closes [#625](https://github.com/vuejs/vue-next/issues/625)
* **transition:** handle multiple transition classes ([#638](https://github.com/vuejs/vue-next/issues/638)) ([#645](https://github.com/vuejs/vue-next/issues/645)) ([98d50d8](https://github.com/vuejs/vue-next/commit/98d50d874dcb32a246216b936e442e5b95ab4825))
### Features
* **runtime-core:** emit now returns array of return values from all triggered handlers ([e81c8a3](https://github.com/vuejs/vue-next/commit/e81c8a32c7b66211cbaecffa93efd4629ec45ad9)), closes [#635](https://github.com/vuejs/vue-next/issues/635)
* **runtime-core:** support app.unmount(container) ([#601](https://github.com/vuejs/vue-next/issues/601)) ([04ac6c4](https://github.com/vuejs/vue-next/commit/04ac6c467a4122877c204d7494c86f89498d2dc6)), closes [#593](https://github.com/vuejs/vue-next/issues/593)
# [3.0.0-alpha.2](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.1...v3.0.0-alpha.2) (2020-01-13)
### Bug Fixes
* **compiler/v-on:** handle multiple statements in v-on handler (close [#572](https://github.com/vuejs/vue-next/issues/572)) ([137893a](https://github.com/vuejs/vue-next/commit/137893a4fdd3d2b901adca31e30d916df925b108))
* **compiler/v-slot:** handle implicit default slot mixed with named slots ([2ac4b72](https://github.com/vuejs/vue-next/commit/2ac4b723e010082488b5be64af73e41c9677a28d))
* **reactivity:** should delete observe value ([#598](https://github.com/vuejs/vue-next/issues/598)) ([63a6563](https://github.com/vuejs/vue-next/commit/63a656310676e3927b2e57d813fd6300c0a42590)), closes [#597](https://github.com/vuejs/vue-next/issues/597)
* **runtime-core:** allow classes to be passed as plugins ([#588](https://github.com/vuejs/vue-next/issues/588)) ([8f616a8](https://github.com/vuejs/vue-next/commit/8f616a89c580bc211540d5e4d60488ff24d024cc))
* **runtime-core:** should preserve props casing when component has no declared props ([bb6a346](https://github.com/vuejs/vue-next/commit/bb6a346996ce0bf05596c605ba5ddbe0743ef84b)), closes [#583](https://github.com/vuejs/vue-next/issues/583)
* **runtime-core/renderer:** fix v-if toggle inside blocks ([2e9726e](https://github.com/vuejs/vue-next/commit/2e9726e6a219d546cd28e4ed42be64719708f047)), closes [#604](https://github.com/vuejs/vue-next/issues/604) [#607](https://github.com/vuejs/vue-next/issues/607)
* **runtime-core/vnode:** should not render boolean values in vnode children (close [#574](https://github.com/vuejs/vue-next/issues/574)) ([84dc5a6](https://github.com/vuejs/vue-next/commit/84dc5a686275528733977ea1570e0a892ba3e177))
* **types:** components options should accept components defined with defineComponent ([#602](https://github.com/vuejs/vue-next/issues/602)) ([74baea1](https://github.com/vuejs/vue-next/commit/74baea108aa93377c4959f9a6b8bc8f9548700ba))
* **watch:** remove recorded effect on manual stop ([#590](https://github.com/vuejs/vue-next/issues/590)) ([453e688](https://github.com/vuejs/vue-next/commit/453e6889da22e7224b638261a32438bdf5c62e41))
# [3.0.0-alpha.1](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.0...v3.0.0-alpha.1) (2020-01-02)
### Bug Fixes
* **runtime-core:** pass options to plugins ([#561](https://github.com/vuejs/vue-next/issues/561)) ([4d20981](https://github.com/vuejs/vue-next/commit/4d20981eb069b20e1627916b977aedb2d68eca86))
* **sfc:** treat custom block content as raw text ([d6275a3](https://github.com/vuejs/vue-next/commit/d6275a3c310e6e9426f897afe35ff6cdb125c023))
* mounting new children ([7d436ab](https://github.com/vuejs/vue-next/commit/7d436ab59a30562a049e199ae579df7ac8066829))
* **core:** clone mounted hoisted vnodes on patch ([47a6a84](https://github.com/vuejs/vue-next/commit/47a6a846311203fa59584486265f5da387afa51d))
* **fragment:** perform direct remove when removing fragments ([2fdb499](https://github.com/vuejs/vue-next/commit/2fdb499bd96b4d1a8a7a1964d59e8dc5dacd9d22))
### Features
* **hmr:** root instance reload ([eda495e](https://github.com/vuejs/vue-next/commit/eda495efd824f17095728a4d2a6db85ca874e5ca))
### Performance Improvements
* **compiler-core:** simplify `advancePositionWithMutation` ([#564](https://github.com/vuejs/vue-next/issues/564)) ([ad2a0bd](https://github.com/vuejs/vue-next/commit/ad2a0bde988de743d4abc62b681b6a4888545a51))
# [3.0.0-alpha.0](https://github.com/vuejs/vue-next/compare/a8522cf48c09efbb2063f129cf1bea0dae09f10a...v3.0.0-alpha.0) (2019-12-20)
For changes between 2.x and 3.0 up to this release, please refer to merged RFCs [here](https://github.com/vuejs/rfcs/pulls?q=is%3Apr+is%3Amerged+label%3A3.x).

View File

@ -1,32 +1,14 @@
# vue-next [![CircleCI](https://circleci.com/gh/vuejs/vue-next.svg?style=svg&circle-token=fb883a2d0a73df46e80b2e79fd430959d8f2b488)](https://circleci.com/gh/vuejs/vue-next) # vue-next [![CircleCI](https://circleci.com/gh/vuejs/vue-next.svg?style=svg&circle-token=fb883a2d0a73df46e80b2e79fd430959d8f2b488)](https://circleci.com/gh/vuejs/vue-next)
## Status: Pre-Alpha. ## Status: Alpha.
We have achieved most of the architectural goals and new features planned for v3: The current codebase has reached feature parity with v2.x (except for features explicitly removed by RFCs), and has landed all the changes proposed in [merged RFCs](https://github.com/vuejs/rfcs/pulls?q=is%3Apr+is%3Amerged+label%3A3.x). We are ready to advance to beta phase once we resolve a number of pending RFCs.
- Compiler There is a simple webpack-based setup with Single-File Component support available [here](https://github.com/vuejs/vue-next-webpack-preview).
- [x] Modular architecture
- [x] "Block tree" optimization
- [x] More aggressive static tree hoisting
- [x] Source map support
- [x] Built-in identifier prefixing (aka "stripWith")
- [x] Built-in pretty-printing
- [x] Lean ~10kb brotli-compressed browser build after dropping source map and identifier prefixing
- Runtime Please note that there could still be undocumented behavior inconsistencies with 2.x. When you run into such a case, please make sure to first check if the behavior difference has already been proposed in an existing RFC. If the inconsistency is not part of an RFC, then it's likely unintended, and an issue should be opened (please make sure to use the [issue helper](https://new-issue.vuejs.org/?repo=vuejs/vue-next) when opening new issues).
- [x] Significantly faster
- [x] Simultaneous Composition API + Options API support, **with typings**
- [x] Proxy-based change detection
- [x] Fragments
- [x] Portals
- [x] Suspense w/ `async setup()`
## Major TODOs: In addition, the current implementation requires native ES2015+ in the runtime environment and does not support IE11 (yet). The IE11 compatible build will be worked on after we have reached RC stage.
- [ ] SFC compiler
- [ ] Server-side rendering
Also note that the current implementation requires native ES2015+ in the runtime environment and does not support IE11 (yet).
## Contribution ## Contribution

View File

@ -5,8 +5,10 @@ module.exports = {
__TEST__: true, __TEST__: true,
__VERSION__: require('./package.json').version, __VERSION__: require('./package.json').version,
__BROWSER__: false, __BROWSER__: false,
__BUNDLER__: false, __BUNDLER__: true,
__RUNTIME_COMPILE__: true, __RUNTIME_COMPILE__: true,
__GLOBAL__: false,
__NODE_JS__: true,
__FEATURE_OPTIONS__: true, __FEATURE_OPTIONS__: true,
__FEATURE_SUSPENSE__: true __FEATURE_SUSPENSE__: true
}, },
@ -16,13 +18,20 @@ module.exports = {
'packages/*/src/**/*.ts', 'packages/*/src/**/*.ts',
'!packages/runtime-test/src/utils/**', '!packages/runtime-test/src/utils/**',
'!packages/template-explorer/**', '!packages/template-explorer/**',
'!packages/size-check/**' '!packages/size-check/**',
'!packages/runtime-core/src/profiling.ts'
], ],
watchPathIgnorePatterns: ['/node_modules/'], watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
moduleNameMapper: { moduleNameMapper: {
'^@vue/(.*?)$': '<rootDir>/packages/$1/src' '^@vue/(.*?)$': '<rootDir>/packages/$1/src',
vue: '<rootDir>/packages/vue/src'
}, },
rootDir: __dirname, rootDir: __dirname,
testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.[jt]s?(x)'] testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.[jt]s?(x)'],
testPathIgnorePatterns: process.env.SKIP_E2E
? // ignore example tests on netlify builds since they don't contribute
// to coverage and can cause netlify builds to fail
['/node_modules/', '/examples/__tests__']
: ['/node_modules/']
} }

View File

@ -1,26 +1,29 @@
{ {
"private": true, "private": true,
"version": "3.0.0-alpha.0", "version": "3.0.0-alpha.11",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"size-runtime": "node scripts/build.js runtime-dom size-check -p -f global", "size": "node scripts/build.js vue runtime-dom size-check -p -f global",
"size-compiler": "node scripts/build.js compiler-dom -p -f global",
"size": "yarn size-runtime && yarn size-compiler",
"lint": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"", "lint": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"",
"ls-lint": "ls-lint",
"test": "node scripts/build.js vue -f global -d && jest", "test": "node scripts/build.js vue -f global -d && jest",
"test-dts": "node scripts/build.js reactivity runtime-core runtime-dom -t -f esm && tsd", "test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && tsd",
"release": "node scripts/release.js" "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-compiler": "npm-run-all --parallel \"dev template-explorer\" serve",
"serve": "serve",
"open": "open http://localhost:5000/packages/template-explorer/local.html"
}, },
"types": "test-dts/index.d.ts", "types": "test-dts/index.d.ts",
"tsd": { "tsd": {
"directory": "test-dts" "directory": "test-dts"
}, },
"gitHooks": { "gitHooks": {
"pre-commit": "lint-staged", "pre-commit": "ls-lint && lint-staged",
"commit-msg": "node scripts/verifyCommit.js" "commit-msg": "node scripts/verifyCommit.js"
}, },
"lint-staged": { "lint-staged": {
@ -33,29 +36,39 @@
"git add" "git add"
] ]
}, },
"engines": {
"node": ">=10.0.0"
},
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "^7.3.9", "@microsoft/api-extractor": "^7.3.9",
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-json": "^4.0.0", "@rollup/plugin-json": "^4.0.0",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-replace": "^2.2.1", "@rollup/plugin-replace": "^2.2.1",
"@types/jest": "^24.0.21", "@types/jest": "^25.1.4",
"@types/puppeteer": "^2.0.0", "@types/puppeteer": "^2.0.0",
"brotli": "^1.3.2", "brotli": "^1.3.2",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"conventional-changelog-cli": "^2.0.31",
"csstype": "^2.6.8",
"enquirer": "^2.3.2", "enquirer": "^2.3.2",
"execa": "^2.0.4", "execa": "^2.0.4",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"jest": "^24.9.0", "jest": "^25.2.3",
"lint-staged": "^9.2.3", "lint-staged": "^9.2.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"npm-run-all": "^4.1.5",
"prettier": "~1.14.0", "prettier": "~1.14.0",
"puppeteer": "^2.0.0", "puppeteer": "^2.0.0",
"rollup": "^1.19.4", "rollup": "^2.2.0",
"rollup-plugin-terser": "^5.1.1", "rollup-plugin-terser": "^5.3.0",
"rollup-plugin-typescript2": "^0.24.0", "rollup-plugin-typescript2": "^0.27.0",
"semver": "^6.3.0", "semver": "^6.3.0",
"ts-jest": "^24.0.2", "serve": "^11.3.0",
"ts-jest": "^25.2.1",
"tsd": "^0.11.0", "tsd": "^0.11.0",
"typescript": "^3.7.0", "typescript": "^3.8.3",
"yorkie": "^2.0.0" "yorkie": "^2.0.0",
"@ls-lint/ls-lint": "^1.8.0"
} }
} }

View File

@ -2,8 +2,8 @@
exports[`compiler: codegen ArrayExpression 1`] = ` exports[`compiler: codegen ArrayExpression 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return [ return [
foo, foo,
bar(baz) bar(baz)
@ -14,22 +14,18 @@ return function render() {
exports[`compiler: codegen CacheExpression 1`] = ` exports[`compiler: codegen CacheExpression 1`] = `
" "
export default function render() { export function render(_ctx, _cache) {
const _ctx = this
const _cache = _ctx.$cache
return _cache[1] || (_cache[1] = foo) return _cache[1] || (_cache[1] = foo)
}" }"
`; `;
exports[`compiler: codegen CacheExpression w/ isVNode: true 1`] = ` exports[`compiler: codegen CacheExpression w/ isVNode: true 1`] = `
" "
export default function render() { export function render(_ctx, _cache) {
const _ctx = this
const _cache = _ctx.$cache
return _cache[1] || ( return _cache[1] || (
setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = foo, _cache[1] = foo,
setBlockTracking(1), _setBlockTracking(1),
_cache[1] _cache[1]
) )
}" }"
@ -37,8 +33,8 @@ export default function render() {
exports[`compiler: codegen ConditionalExpression 1`] = ` exports[`compiler: codegen ConditionalExpression 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return ok return ok
? foo() ? foo()
: orNot : orNot
@ -50,8 +46,8 @@ return function render() {
exports[`compiler: codegen Element (callExpression + objectExpression + TemplateChildNode[]) 1`] = ` exports[`compiler: codegen Element (callExpression + objectExpression + TemplateChildNode[]) 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return _createVNode(\\"div\\", { return _createVNode(\\"div\\", {
id: \\"foo\\", id: \\"foo\\",
[prop]: bar, [prop]: bar,
@ -63,23 +59,16 @@ return function render() {
}" }"
`; `;
exports[`compiler: codegen SequenceExpression 1`] = ` exports[`compiler: codegen assets + temps 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return (foo, bar(baz))
}
}"
`;
exports[`compiler: codegen assets 1`] = `
"
return function render() {
with (this) {
const _component_Foo = _resolveComponent(\\"Foo\\") const _component_Foo = _resolveComponent(\\"Foo\\")
const _component_bar_baz = _resolveComponent(\\"bar-baz\\") const _component_bar_baz = _resolveComponent(\\"bar-baz\\")
const _component_barbaz = _resolveComponent(\\"barbaz\\") const _component_barbaz = _resolveComponent(\\"barbaz\\")
const _directive_my_dir = _resolveDirective(\\"my_dir\\") const _directive_my_dir_0 = _resolveDirective(\\"my_dir_0\\")
const _directive_my_dir_1 = _resolveDirective(\\"my_dir_1\\")
let _temp0, _temp1, _temp2
return null return null
} }
@ -88,8 +77,8 @@ return function render() {
exports[`compiler: codegen comment 1`] = ` exports[`compiler: codegen comment 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return _createCommentVNode(\\"foo\\") return _createCommentVNode(\\"foo\\")
} }
}" }"
@ -97,18 +86,18 @@ return function render() {
exports[`compiler: codegen compound expression 1`] = ` exports[`compiler: codegen compound expression 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return _ctx.foo + _toString(bar) return _ctx.foo + _toDisplayString(bar) + nested
} }
}" }"
`; `;
exports[`compiler: codegen forNode 1`] = ` exports[`compiler: codegen forNode 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return (foo, bar) return (_openBlock(true), _createBlock(_Fragment, null, _renderList(), 1))
} }
}" }"
`; `;
@ -116,8 +105,8 @@ return function render() {
exports[`compiler: codegen function mode preamble 1`] = ` exports[`compiler: codegen function mode preamble 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, resolveDirective: _resolveDirective } = _Vue const { createVNode: _createVNode, resolveDirective: _resolveDirective } = _Vue
return null return null
@ -126,10 +115,9 @@ return function render() {
`; `;
exports[`compiler: codegen function mode preamble w/ prefixIdentifiers: true 1`] = ` exports[`compiler: codegen function mode preamble w/ prefixIdentifiers: true 1`] = `
"const { createVNode, resolveDirective } = Vue "const { createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this
return null return null
}" }"
`; `;
@ -139,8 +127,8 @@ exports[`compiler: codegen hoists 1`] = `
const _hoisted_1 = hello const _hoisted_1 = hello
const _hoisted_2 = { id: \\"foo\\" } const _hoisted_2 = { id: \\"foo\\" }
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return null return null
} }
}" }"
@ -148,44 +136,59 @@ return function render() {
exports[`compiler: codegen ifNode 1`] = ` exports[`compiler: codegen ifNode 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return (foo, bar) return foo
? bar
: baz
} }
}" }"
`; `;
exports[`compiler: codegen interpolation 1`] = ` exports[`compiler: codegen interpolation 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return _toString(hello) return _toDisplayString(hello)
} }
}" }"
`; `;
exports[`compiler: codegen module mode preamble 1`] = ` exports[`compiler: codegen module mode preamble 1`] = `
"import { createVNode, resolveDirective } from \\"vue\\" "import { createVNode as _createVNode, resolveDirective as _resolveDirective } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this
return null return null
}" }"
`; `;
exports[`compiler: codegen prefixIdentifiers: true should inject _ctx statement 1`] = ` exports[`compiler: codegen module mode preamble w/ optimizeBindings: true 1`] = `
" "import { createVNode, resolveDirective } from \\"vue\\"
return function render() {
const _ctx = this // Binding optimization for webpack code-split
const _createVNode = createVNode, _resolveDirective = resolveDirective
export function render(_ctx, _cache) {
return null return null
}" }"
`; `;
exports[`compiler: codegen static text 1`] = ` exports[`compiler: codegen static text 1`] = `
" "
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
return \\"hello\\" return \\"hello\\"
} }
}" }"
`; `;
exports[`compiler: codegen temps 1`] = `
"
return function render(_ctx, _cache) {
with (_ctx) {
let _temp0, _temp1, _temp2
return null
}
}"
`;

View File

@ -3,21 +3,23 @@
exports[`compiler: integration tests function mode 1`] = ` exports[`compiler: integration tests function mode 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment, renderList: _renderList, createTextVNode: _createTextVNode } = _Vue const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList } = _Vue
return (_openBlock(), _createBlock(\\"div\\", { return (_openBlock(), _createBlock(\\"div\\", {
id: \\"foo\\", id: \\"foo\\",
class: bar.baz class: bar.baz
}, [ }, [
_createTextVNode(_toString(world.burn()) + \\" \\", 1 /* TEXT */), _createTextVNode(_toDisplayString(world.burn()) + \\" \\", 1 /* TEXT */),
(_openBlock(), ok ok
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: _createBlock(_Fragment, { key: 1 }, [\\"no\\"])), : (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (value, index) => { _createTextVNode(\\"no\\")
], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */) _createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
])) ]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */)) ], 2 /* CLASS */))
@ -26,21 +28,22 @@ return function render() {
`; `;
exports[`compiler: integration tests function mode w/ prefixIdentifiers: true 1`] = ` exports[`compiler: integration tests function mode w/ prefixIdentifiers: true 1`] = `
"const { toString, openBlock, createVNode, createBlock, createCommentVNode, Fragment, renderList, createTextVNode } = Vue "const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"div\\", {
return (openBlock(), createBlock(\\"div\\", {
id: \\"foo\\", id: \\"foo\\",
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
createTextVNode(toString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */), _createTextVNode(_toDisplayString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(openBlock(), (_ctx.ok) (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
(openBlock(false), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => { _createTextVNode(\\"no\\")
return (openBlock(), createBlock(\\"div\\", null, [ ], 64 /* STABLE_FRAGMENT */)),
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */) (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
])) ]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */)) ], 2 /* CLASS */))
@ -48,21 +51,22 @@ return function render() {
`; `;
exports[`compiler: integration tests module mode 1`] = ` exports[`compiler: integration tests module mode 1`] = `
"import { toString, openBlock, createVNode, createBlock, createCommentVNode, Fragment, renderList, createTextVNode } from \\"vue\\" "import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createTextVNode as _createTextVNode, Fragment as _Fragment, renderList as _renderList } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"div\\", {
return (openBlock(), createBlock(\\"div\\", {
id: \\"foo\\", id: \\"foo\\",
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
createTextVNode(toString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */), _createTextVNode(_toDisplayString(_ctx.world.burn()) + \\" \\", 1 /* TEXT */),
(openBlock(), (_ctx.ok) (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
(openBlock(false), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => { _createTextVNode(\\"no\\")
return (openBlock(), createBlock(\\"div\\", null, [ ], 64 /* STABLE_FRAGMENT */)),
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */) (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
])) ]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */)) ], 2 /* CLASS */))

View File

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = `
"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
_pushScopeId(\\"test\\")
const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -1 /* HOISTED */)
const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -1 /* HOISTED */)
_popScopeId()
export const render = _withId(function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */),
_hoisted_2
]))
})"
`;
exports[`scopeId compiler support should wrap default slot 1`] = `
"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, {
default: _withId(() => [
_createVNode(\\"div\\")
]),
_: 1
}))
})"
`;
exports[`scopeId compiler support should wrap dynamic slots 1`] = `
"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, _createSlots({ _: 1 }, [
(_ctx.ok)
? {
name: \\"foo\\",
fn: _withId(() => [
_createVNode(\\"div\\")
])
}
: undefined,
_renderList(_ctx.list, (i) => {
return {
name: i,
fn: _withId(() => [
_createVNode(\\"div\\")
])
}
})
]), 1024 /* DYNAMIC_SLOTS */))
})"
`;
exports[`scopeId compiler support should wrap named slots 1`] = `
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
const _component_Child = _resolveComponent(\\"Child\\")
return (_openBlock(), _createBlock(_component_Child, null, {
foo: _withId(({ msg }) => [
_createTextVNode(_toDisplayString(msg), 1 /* TEXT */)
]),
bar: _withId(() => [
_createVNode(\\"div\\")
]),
_: 1
}))
})"
`;
exports[`scopeId compiler support should wrap render function 1`] = `
"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\"
const _withId = _withScopeId(\\"test\\")
export const render = _withId(function render(_ctx, _cache) {
return (_openBlock(), _createBlock(\\"div\\"))
})"
`;

View File

@ -9,20 +9,28 @@ import {
createArrayExpression, createArrayExpression,
createCompoundExpression, createCompoundExpression,
createInterpolation, createInterpolation,
createSequenceExpression,
createCallExpression, createCallExpression,
createConditionalExpression, createConditionalExpression,
IfCodegenNode,
ForCodegenNode, ForCodegenNode,
createCacheExpression createCacheExpression,
createTemplateLiteral,
createBlockStatement,
createIfStatement,
createAssignmentExpression,
IfConditionalExpression,
createVNodeCall,
VNodeCall,
DirectiveArguments
} from '../src' } from '../src'
import { import {
CREATE_VNODE, CREATE_VNODE,
TO_STRING, TO_DISPLAY_STRING,
RESOLVE_DIRECTIVE, RESOLVE_DIRECTIVE,
helperNameMap, helperNameMap,
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
CREATE_COMMENT CREATE_COMMENT,
FRAGMENT,
RENDER_LIST
} from '../src/runtimeHelpers' } from '../src/runtimeHelpers'
import { createElementWithCodegen } from './testUtils' import { createElementWithCodegen } from './testUtils'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
@ -37,6 +45,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
imports: [], imports: [],
hoists: [], hoists: [],
cached: 0, cached: 0,
temps: 0,
codegenNode: createSimpleExpression(`null`, false), codegenNode: createSimpleExpression(`null`, false),
loc: locStub, loc: locStub,
...options ...options
@ -49,11 +58,33 @@ describe('compiler: codegen', () => {
helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE] helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
}) })
const { code } = generate(root, { mode: 'module' }) const { code } = generate(root, { mode: 'module' })
expect(code).toMatch(
`import { ${helperNameMap[CREATE_VNODE]} as _${
helperNameMap[CREATE_VNODE]
}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${
helperNameMap[RESOLVE_DIRECTIVE]
} } from "vue"`
)
expect(code).toMatchSnapshot()
})
test('module mode preamble w/ optimizeBindings: true', () => {
const root = createRoot({
helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
})
const { code } = generate(root, { mode: 'module', optimizeBindings: true })
expect(code).toMatch( expect(code).toMatch(
`import { ${helperNameMap[CREATE_VNODE]}, ${ `import { ${helperNameMap[CREATE_VNODE]}, ${
helperNameMap[RESOLVE_DIRECTIVE] helperNameMap[RESOLVE_DIRECTIVE]
} } from "vue"` } } from "vue"`
) )
expect(code).toMatch(
`const _${helperNameMap[CREATE_VNODE]} = ${
helperNameMap[CREATE_VNODE]
}, _${helperNameMap[RESOLVE_DIRECTIVE]} = ${
helperNameMap[RESOLVE_DIRECTIVE]
}`
)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -83,17 +114,20 @@ describe('compiler: codegen', () => {
}) })
expect(code).not.toMatch(`const _Vue = Vue`) expect(code).not.toMatch(`const _Vue = Vue`)
expect(code).toMatch( expect(code).toMatch(
`const { ${helperNameMap[CREATE_VNODE]}, ${ `const { ${helperNameMap[CREATE_VNODE]}: _${
helperNameMap[CREATE_VNODE]
}, ${helperNameMap[RESOLVE_DIRECTIVE]}: _${
helperNameMap[RESOLVE_DIRECTIVE] helperNameMap[RESOLVE_DIRECTIVE]
} } = Vue` } } = Vue`
) )
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('assets', () => { test('assets + temps', () => {
const root = createRoot({ const root = createRoot({
components: [`Foo`, `bar-baz`, `barbaz`], components: [`Foo`, `bar-baz`, `barbaz`],
directives: [`my_dir`] directives: [`my_dir_0`, `my_dir_1`],
temps: 3
}) })
const { code } = generate(root, { mode: 'function' }) const { code } = generate(root, { mode: 'function' })
expect(code).toMatch( expect(code).toMatch(
@ -110,10 +144,16 @@ describe('compiler: codegen', () => {
}("barbaz")\n` }("barbaz")\n`
) )
expect(code).toMatch( expect(code).toMatch(
`const _directive_my_dir = _${ `const _directive_my_dir_0 = _${
helperNameMap[RESOLVE_DIRECTIVE] helperNameMap[RESOLVE_DIRECTIVE]
}("my_dir")\n` }("my_dir_0")\n`
) )
expect(code).toMatch(
`const _directive_my_dir_1 = _${
helperNameMap[RESOLVE_DIRECTIVE]
}("my_dir_1")\n`
)
expect(code).toMatch(`let _temp0, _temp1, _temp2`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -138,9 +178,12 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('prefixIdentifiers: true should inject _ctx statement', () => { test('temps', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true }) const root = createRoot({
expect(code).toMatch(`const _ctx = this\n`) temps: 3
})
const { code } = generate(root)
expect(code).toMatch(`let _temp0, _temp1, _temp2`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -164,7 +207,7 @@ describe('compiler: codegen', () => {
codegenNode: createInterpolation(`hello`, locStub) codegenNode: createInterpolation(`hello`, locStub)
}) })
) )
expect(code).toMatch(`return _${helperNameMap[TO_STRING]}(hello)`) expect(code).toMatch(`return _${helperNameMap[TO_DISPLAY_STRING]}(hello)`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -193,11 +236,15 @@ describe('compiler: codegen', () => {
type: NodeTypes.INTERPOLATION, type: NodeTypes.INTERPOLATION,
loc: locStub, loc: locStub,
content: createSimpleExpression(`bar`, false, locStub) content: createSimpleExpression(`bar`, false, locStub)
} },
// nested compound
createCompoundExpression([` + `, `nested`])
]) ])
}) })
) )
expect(code).toMatch(`return _ctx.foo + _${helperNameMap[TO_STRING]}(bar)`) expect(code).toMatch(
`return _ctx.foo + _${helperNameMap[TO_DISPLAY_STRING]}(bar) + nested`
)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -208,14 +255,15 @@ describe('compiler: codegen', () => {
type: NodeTypes.IF, type: NodeTypes.IF,
loc: locStub, loc: locStub,
branches: [], branches: [],
codegenNode: createSequenceExpression([ codegenNode: createConditionalExpression(
createSimpleExpression('foo', false), createSimpleExpression('foo', false),
createSimpleExpression('bar', false) createSimpleExpression('bar', false),
]) as IfCodegenNode createSimpleExpression('baz', false)
) as IfConditionalExpression
} }
}) })
) )
expect(code).toMatch(`return (foo, bar)`) expect(code).toMatch(/return foo\s+\? bar\s+: baz/)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -230,21 +278,30 @@ describe('compiler: codegen', () => {
keyAlias: undefined, keyAlias: undefined,
objectIndexAlias: undefined, objectIndexAlias: undefined,
children: [], children: [],
codegenNode: createSequenceExpression([ parseResult: {} as any,
createSimpleExpression('foo', false), codegenNode: {
createSimpleExpression('bar', false) type: NodeTypes.VNODE_CALL,
]) as ForCodegenNode tag: FRAGMENT,
isBlock: true,
isForBlock: true,
props: undefined,
children: createCallExpression(RENDER_LIST),
patchFlag: '1',
dynamicProps: undefined,
directives: undefined,
loc: locStub
} as ForCodegenNode
} }
}) })
) )
expect(code).toMatch(`return (foo, bar)`) expect(code).toMatch(`openBlock(true)`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('Element (callExpression + objectExpression + TemplateChildNode[])', () => { test('Element (callExpression + objectExpression + TemplateChildNode[])', () => {
const { code } = generate( const { code } = generate(
createRoot({ createRoot({
codegenNode: createElementWithCodegen([ codegenNode: createElementWithCodegen(
// string // string
`"div"`, `"div"`,
// ObjectExpression // ObjectExpression
@ -275,7 +332,7 @@ describe('compiler: codegen', () => {
), ),
// ChildNode[] // ChildNode[]
[ [
createElementWithCodegen([ createElementWithCodegen(
`"p"`, `"p"`,
createObjectExpression( createObjectExpression(
[ [
@ -287,11 +344,11 @@ describe('compiler: codegen', () => {
], ],
locStub locStub
) )
]) )
], ],
// flag // flag
PatchFlags.FULL_PROPS + '' PatchFlags.FULL_PROPS + ''
]) )
}) })
) )
expect(code).toMatch(` expect(code).toMatch(`
@ -321,19 +378,6 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('SequenceExpression', () => {
const { code } = generate(
createRoot({
codegenNode: createSequenceExpression([
createSimpleExpression(`foo`, false),
createCallExpression(`bar`, [`baz`])
])
})
)
expect(code).toMatch(`return (foo, bar(baz))`)
expect(code).toMatchSnapshot()
})
test('ConditionalExpression', () => { test('ConditionalExpression', () => {
const { code } = generate( const { code } = generate(
createRoot({ createRoot({
@ -372,7 +416,6 @@ describe('compiler: codegen', () => {
prefixIdentifiers: true prefixIdentifiers: true
} }
) )
expect(code).toMatch(`const _cache = _ctx.$cache`)
expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`) expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -392,17 +435,309 @@ describe('compiler: codegen', () => {
prefixIdentifiers: true prefixIdentifiers: true
} }
) )
expect(code).toMatch(`const _cache = _ctx.$cache`)
expect(code).toMatch( expect(code).toMatch(
` `
_cache[1] || ( _cache[1] || (
setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = foo, _cache[1] = foo,
setBlockTracking(1), _setBlockTracking(1),
_cache[1] _cache[1]
) )
`.trim() `.trim()
) )
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('TemplateLiteral', () => {
const { code } = generate(
createRoot({
codegenNode: createCallExpression(`_push`, [
createTemplateLiteral([
`foo`,
createCallExpression(`_renderAttr`, ['id', 'foo']),
`bar`
])
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
_push(\`foo\${_renderAttr(id, foo)}bar\`)
}"
`)
})
describe('IfStatement', () => {
test('if', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`ok`)])
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
ok()
}
}"
`)
})
test('if/else', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createBlockStatement([createCallExpression('bar')])
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else {
bar()
}
}"
`)
})
test('if/else-if', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createIfStatement(
createSimpleExpression('bar', false),
createBlockStatement([createCallExpression(`bar`)])
)
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else if (bar) {
bar()
}
}"
`)
})
test('if/else-if/else', () => {
const { code } = generate(
createRoot({
codegenNode: createBlockStatement([
createIfStatement(
createSimpleExpression('foo', false),
createBlockStatement([createCallExpression(`foo`)]),
createIfStatement(
createSimpleExpression('bar', false),
createBlockStatement([createCallExpression(`bar`)]),
createBlockStatement([createCallExpression('baz')])
)
)
])
}),
{ ssr: true, mode: 'module' }
)
expect(code).toMatchInlineSnapshot(`
"
export function ssrRender(_ctx, _push, _parent) {
if (foo) {
foo()
} else if (bar) {
bar()
} else {
baz()
}
}"
`)
})
})
test('AssignmentExpression', () => {
const { code } = generate(
createRoot({
codegenNode: createAssignmentExpression(
createSimpleExpression(`foo`, false),
createSimpleExpression(`bar`, false)
)
})
)
expect(code).toMatchInlineSnapshot(`
"
return function render(_ctx, _cache) {
with (_ctx) {
return foo = bar
}
}"
`)
})
describe('VNodeCall', () => {
function genCode(node: VNodeCall) {
return generate(
createRoot({
codegenNode: node
})
).code.match(/with \(_ctx\) \{\s+([^]+)\s+\}\s+\}$/)![1]
}
const mockProps = createObjectExpression([
createObjectProperty(`foo`, createSimpleExpression(`bar`, true))
])
const mockChildren = createCompoundExpression(['children'])
const mockDirs = createArrayExpression([
createArrayExpression([`foo`, createSimpleExpression(`bar`, false)])
]) as DirectiveArguments
test('tag only', () => {
expect(genCode(createVNodeCall(null, `"div"`))).toMatchInlineSnapshot(`
"return _createVNode(\\"div\\")
"
`)
expect(genCode(createVNodeCall(null, FRAGMENT))).toMatchInlineSnapshot(`
"return _createVNode(_Fragment)
"
`)
})
test('with props', () => {
expect(genCode(createVNodeCall(null, `"div"`, mockProps)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", { foo: \\"bar\\" })
"
`)
})
test('with children, no props', () => {
expect(genCode(createVNodeCall(null, `"div"`, undefined, mockChildren)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", null, children)
"
`)
})
test('with children + props', () => {
expect(genCode(createVNodeCall(null, `"div"`, mockProps, mockChildren)))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", { foo: \\"bar\\" }, children)
"
`)
})
test('with patchFlag and no children/props', () => {
expect(genCode(createVNodeCall(null, `"div"`, undefined, undefined, '1')))
.toMatchInlineSnapshot(`
"return _createVNode(\\"div\\", null, null, 1)
"
`)
})
test('as block', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
undefined,
true
)
)
).toMatchInlineSnapshot(`
"return (_openBlock(), _createBlock(\\"div\\", { foo: \\"bar\\" }, children))
"
`)
})
test('as for block', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
undefined,
true,
true
)
)
).toMatchInlineSnapshot(`
"return (_openBlock(true), _createBlock(\\"div\\", { foo: \\"bar\\" }, children))
"
`)
})
test('with directives', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
mockDirs
)
)
).toMatchInlineSnapshot(`
"return _withDirectives(_createVNode(\\"div\\", { foo: \\"bar\\" }, children), [
[foo, bar]
])
"
`)
})
test('block + directives', () => {
expect(
genCode(
createVNodeCall(
null,
`"div"`,
mockProps,
mockChildren,
undefined,
undefined,
mockDirs,
true
)
)
).toMatchInlineSnapshot(`
"return _withDirectives((_openBlock(), _createBlock(\\"div\\", { foo: \\"bar\\" }, children)), [
[foo, bar]
])
"
`)
})
})
}) })

View File

@ -44,7 +44,7 @@ describe('compiler: integration tests', () => {
return res return res
} }
test('function mode', async () => { test('function mode', () => {
const { code, map } = compile(source, { const { code, map } = compile(source, {
sourceMap: true, sourceMap: true,
filename: `foo.vue` filename: `foo.vue`
@ -54,7 +54,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`]) expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source]) expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap) const consumer = new SourceMapConsumer(map as RawSourceMap)
expect( expect(
consumer.originalPositionFor(getPositionInCode(code, `id`)) consumer.originalPositionFor(getPositionInCode(code, `id`))
@ -109,7 +109,7 @@ describe('compiler: integration tests', () => {
).toMatchObject(getPositionInCode(source, `value + index`)) ).toMatchObject(getPositionInCode(source, `value + index`))
}) })
test('function mode w/ prefixIdentifiers: true', async () => { test('function mode w/ prefixIdentifiers: true', () => {
const { code, map } = compile(source, { const { code, map } = compile(source, {
sourceMap: true, sourceMap: true,
filename: `foo.vue`, filename: `foo.vue`,
@ -120,7 +120,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`]) expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source]) expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap) const consumer = new SourceMapConsumer(map as RawSourceMap)
expect( expect(
consumer.originalPositionFor(getPositionInCode(code, `id`)) consumer.originalPositionFor(getPositionInCode(code, `id`))
@ -184,7 +184,7 @@ describe('compiler: integration tests', () => {
).toMatchObject(getPositionInCode(source, `value + index`)) ).toMatchObject(getPositionInCode(source, `value + index`))
}) })
test('module mode', async () => { test('module mode', () => {
const { code, map } = compile(source, { const { code, map } = compile(source, {
mode: 'module', mode: 'module',
sourceMap: true, sourceMap: true,
@ -195,7 +195,7 @@ describe('compiler: integration tests', () => {
expect(map!.sources).toEqual([`foo.vue`]) expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source]) expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap) const consumer = new SourceMapConsumer(map as RawSourceMap)
expect( expect(
consumer.originalPositionFor(getPositionInCode(code, `id`)) consumer.originalPositionFor(getPositionInCode(code, `id`))

View File

@ -1,5 +1,5 @@
import { ParserOptions } from '../src/options' import { ParserOptions } from '../src/options'
import { parse, TextModes } from '../src/parse' import { baseParse, TextModes } from '../src/parse'
import { ErrorCodes } from '../src/errors' import { ErrorCodes } from '../src/errors'
import { import {
CommentNode, CommentNode,
@ -16,7 +16,7 @@ import {
describe('compiler: parse', () => { describe('compiler: parse', () => {
describe('Text', () => { describe('Text', () => {
test('simple text', () => { test('simple text', () => {
const ast = parse('some text') const ast = baseParse('some text')
const text = ast.children[0] as TextNode const text = ast.children[0] as TextNode
expect(text).toStrictEqual({ expect(text).toStrictEqual({
@ -31,7 +31,7 @@ describe('compiler: parse', () => {
}) })
test('simple text with invalid end tag', () => { test('simple text with invalid end tag', () => {
const ast = parse('some text</div>', { const ast = baseParse('some text</div>', {
onError: () => {} onError: () => {}
}) })
const text = ast.children[0] as TextNode const text = ast.children[0] as TextNode
@ -48,7 +48,7 @@ describe('compiler: parse', () => {
}) })
test('text with interpolation', () => { test('text with interpolation', () => {
const ast = parse('some {{ foo + bar }} text') const ast = baseParse('some {{ foo + bar }} text')
const text1 = ast.children[0] as TextNode const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode const text2 = ast.children[2] as TextNode
@ -73,7 +73,7 @@ describe('compiler: parse', () => {
}) })
test('text with interpolation which has `<`', () => { test('text with interpolation which has `<`', () => {
const ast = parse('some {{ a<b && c>d }} text') const ast = baseParse('some {{ a<b && c>d }} text')
const text1 = ast.children[0] as TextNode const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode const text2 = ast.children[2] as TextNode
@ -98,7 +98,7 @@ describe('compiler: parse', () => {
}) })
test('text with mix of tags and interpolations', () => { test('text with mix of tags and interpolations', () => {
const ast = parse('some <span>{{ foo < bar + foo }} text</span>') const ast = baseParse('some <span>{{ foo < bar + foo }} text</span>')
const text1 = ast.children[0] as TextNode const text1 = ast.children[0] as TextNode
const text2 = (ast.children[1] as ElementNode).children![1] as TextNode const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
@ -123,7 +123,7 @@ describe('compiler: parse', () => {
}) })
test('lonly "<" don\'t separate nodes', () => { test('lonly "<" don\'t separate nodes', () => {
const ast = parse('a < b', { const ast = baseParse('a < b', {
onError: err => { onError: err => {
if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) { if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
throw err throw err
@ -144,7 +144,7 @@ describe('compiler: parse', () => {
}) })
test('lonly "{{" don\'t separate nodes', () => { test('lonly "{{" don\'t separate nodes', () => {
const ast = parse('a {{ b', { const ast = baseParse('a {{ b', {
onError: error => { onError: error => {
if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) { if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
throw error throw error
@ -166,7 +166,7 @@ describe('compiler: parse', () => {
test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => { test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn() const spy = jest.fn()
const ast = parse('&ampersand;', { const ast = baseParse('&ampersand;', {
namedCharacterReferences: { amp: '&' }, namedCharacterReferences: { amp: '&' },
onError: spy onError: spy
}) })
@ -195,7 +195,7 @@ describe('compiler: parse', () => {
test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => { test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn() const spy = jest.fn()
const ast = parse( const ast = baseParse(
'<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>', '<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
{ {
namedCharacterReferences: { amp: '&', 'amp;': '&' }, namedCharacterReferences: { amp: '&', 'amp;': '&' },
@ -248,7 +248,7 @@ describe('compiler: parse', () => {
test('Some control character reference should be replaced.', () => { test('Some control character reference should be replaced.', () => {
const spy = jest.fn() const spy = jest.fn()
const ast = parse('&#x86;', { onError: spy }) const ast = baseParse('&#x86;', { onError: spy })
const text = ast.children[0] as TextNode const text = ast.children[0] as TextNode
expect(text).toStrictEqual({ expect(text).toStrictEqual({
@ -275,7 +275,7 @@ describe('compiler: parse', () => {
describe('Interpolation', () => { describe('Interpolation', () => {
test('simple interpolation', () => { test('simple interpolation', () => {
const ast = parse('{{message}}') const ast = baseParse('{{message}}')
const interpolation = ast.children[0] as InterpolationNode const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({ expect(interpolation).toStrictEqual({
@ -300,7 +300,7 @@ describe('compiler: parse', () => {
}) })
test('it can have tag-like notation', () => { test('it can have tag-like notation', () => {
const ast = parse('{{ a<b }}') const ast = baseParse('{{ a<b }}')
const interpolation = ast.children[0] as InterpolationNode const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({ expect(interpolation).toStrictEqual({
@ -325,7 +325,7 @@ describe('compiler: parse', () => {
}) })
test('it can have tag-like notation (2)', () => { test('it can have tag-like notation (2)', () => {
const ast = parse('{{ a<b }}{{ c>d }}') const ast = baseParse('{{ a<b }}{{ c>d }}')
const interpolation1 = ast.children[0] as InterpolationNode const interpolation1 = ast.children[0] as InterpolationNode
const interpolation2 = ast.children[1] as InterpolationNode const interpolation2 = ast.children[1] as InterpolationNode
@ -371,7 +371,7 @@ describe('compiler: parse', () => {
}) })
test('it can have tag-like notation (3)', () => { test('it can have tag-like notation (3)', () => {
const ast = parse('<div>{{ "</div>" }}</div>') const ast = baseParse('<div>{{ "</div>" }}</div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
const interpolation = element.children[0] as InterpolationNode const interpolation = element.children[0] as InterpolationNode
@ -398,7 +398,7 @@ describe('compiler: parse', () => {
}) })
test('custom delimiters', () => { test('custom delimiters', () => {
const ast = parse('<p>{msg}</p>', { const ast = baseParse('<p>{msg}</p>', {
delimiters: ['{', '}'] delimiters: ['{', '}']
}) })
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
@ -428,7 +428,7 @@ describe('compiler: parse', () => {
describe('Comment', () => { describe('Comment', () => {
test('empty comment', () => { test('empty comment', () => {
const ast = parse('<!---->') const ast = baseParse('<!---->')
const comment = ast.children[0] as CommentNode const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({ expect(comment).toStrictEqual({
@ -443,7 +443,7 @@ describe('compiler: parse', () => {
}) })
test('simple comment', () => { test('simple comment', () => {
const ast = parse('<!--abc-->') const ast = baseParse('<!--abc-->')
const comment = ast.children[0] as CommentNode const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({ expect(comment).toStrictEqual({
@ -458,7 +458,7 @@ describe('compiler: parse', () => {
}) })
test('two comments', () => { test('two comments', () => {
const ast = parse('<!--abc--><!--def-->') const ast = baseParse('<!--abc--><!--def-->')
const comment1 = ast.children[0] as CommentNode const comment1 = ast.children[0] as CommentNode
const comment2 = ast.children[1] as CommentNode const comment2 = ast.children[1] as CommentNode
@ -485,7 +485,7 @@ describe('compiler: parse', () => {
describe('Element', () => { describe('Element', () => {
test('simple div', () => { test('simple div', () => {
const ast = parse('<div>hello</div>') const ast = baseParse('<div>hello</div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -516,7 +516,7 @@ describe('compiler: parse', () => {
}) })
test('empty', () => { test('empty', () => {
const ast = parse('<div></div>') const ast = baseParse('<div></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -526,7 +526,6 @@ describe('compiler: parse', () => {
tagType: ElementTypes.ELEMENT, tagType: ElementTypes.ELEMENT,
codegenNode: undefined, codegenNode: undefined,
props: [], props: [],
isSelfClosing: false, isSelfClosing: false,
children: [], children: [],
loc: { loc: {
@ -538,7 +537,7 @@ describe('compiler: parse', () => {
}) })
test('self closing', () => { test('self closing', () => {
const ast = parse('<div/>after') const ast = baseParse('<div/>after')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -560,7 +559,7 @@ describe('compiler: parse', () => {
}) })
test('void element', () => { test('void element', () => {
const ast = parse('<img>after', { const ast = baseParse('<img>after', {
isVoidTag: tag => tag === 'img' isVoidTag: tag => tag === 'img'
}) })
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
@ -583,8 +582,26 @@ describe('compiler: parse', () => {
}) })
}) })
test('template element with directives', () => {
const ast = baseParse('<template v-if="ok"></template>')
const element = ast.children[0]
expect(element).toMatchObject({
type: NodeTypes.ELEMENT,
tagType: ElementTypes.TEMPLATE
})
})
test('template element without directives', () => {
const ast = baseParse('<template></template>')
const element = ast.children[0]
expect(element).toMatchObject({
type: NodeTypes.ELEMENT,
tagType: ElementTypes.ELEMENT
})
})
test('native element with `isNativeTag`', () => { test('native element with `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>', { const ast = baseParse('<div></div><comp></comp><Comp></Comp>', {
isNativeTag: tag => tag === 'div' isNativeTag: tag => tag === 'div'
}) })
@ -608,7 +625,7 @@ describe('compiler: parse', () => {
}) })
test('native element without `isNativeTag`', () => { test('native element without `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>') const ast = baseParse('<div></div><comp></comp><Comp></Comp>')
expect(ast.children[0]).toMatchObject({ expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -629,8 +646,57 @@ describe('compiler: parse', () => {
}) })
}) })
test('v-is without `isNativeTag`', () => {
const ast = baseParse(
`<div></div><div v-is="'foo'"></div><Comp></Comp>`,
{
isNativeTag: tag => tag === 'div'
}
)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('v-is with `isNativeTag`', () => {
const ast = baseParse(`<div></div><div v-is="'foo'"></div><Comp></Comp>`)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('custom element', () => { test('custom element', () => {
const ast = parse('<div></div><comp></comp>', { const ast = baseParse('<div></div><comp></comp>', {
isNativeTag: tag => tag === 'div', isNativeTag: tag => tag === 'div',
isCustomElement: tag => tag === 'comp' isCustomElement: tag => tag === 'comp'
}) })
@ -649,7 +715,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with no value', () => { test('attribute with no value', () => {
const ast = parse('<div id></div>') const ast = baseParse('<div id></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -682,7 +748,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with empty value, double quote', () => { test('attribute with empty value, double quote', () => {
const ast = parse('<div id=""></div>') const ast = baseParse('<div id=""></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -723,7 +789,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with empty value, single quote', () => { test('attribute with empty value, single quote', () => {
const ast = parse("<div id=''></div>") const ast = baseParse("<div id=''></div>")
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -764,7 +830,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with value, double quote', () => { test('attribute with value, double quote', () => {
const ast = parse('<div id=">\'"></div>') const ast = baseParse('<div id=">\'"></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -805,7 +871,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with value, single quote', () => { test('attribute with value, single quote', () => {
const ast = parse("<div id='>\"'></div>") const ast = baseParse("<div id='>\"'></div>")
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -846,7 +912,7 @@ describe('compiler: parse', () => {
}) })
test('attribute with value, unquoted', () => { test('attribute with value, unquoted', () => {
const ast = parse('<div id=a/></div>') const ast = baseParse('<div id=a/></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -887,7 +953,7 @@ describe('compiler: parse', () => {
}) })
test('multiple attributes', () => { test('multiple attributes', () => {
const ast = parse('<div id=a class="c" inert style=\'\'></div>') const ast = baseParse('<div id=a class="c" inert style=\'\'></div>')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({ expect(element).toStrictEqual({
@ -974,7 +1040,7 @@ describe('compiler: parse', () => {
}) })
test('directive with no value', () => { test('directive with no value', () => {
const ast = parse('<div v-if/>') const ast = baseParse('<div v-if/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -992,7 +1058,7 @@ describe('compiler: parse', () => {
}) })
test('directive with value', () => { test('directive with value', () => {
const ast = parse('<div v-if="a"/>') const ast = baseParse('<div v-if="a"/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1020,7 +1086,7 @@ describe('compiler: parse', () => {
}) })
test('directive with argument', () => { test('directive with argument', () => {
const ast = parse('<div v-on:click/>') const ast = baseParse('<div v-on:click/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1057,7 +1123,7 @@ describe('compiler: parse', () => {
}) })
test('directive with a modifier', () => { test('directive with a modifier', () => {
const ast = parse('<div v-on.enter/>') const ast = baseParse('<div v-on.enter/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1075,7 +1141,7 @@ describe('compiler: parse', () => {
}) })
test('directive with two modifiers', () => { test('directive with two modifiers', () => {
const ast = parse('<div v-on.enter.exact/>') const ast = baseParse('<div v-on.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1093,7 +1159,7 @@ describe('compiler: parse', () => {
}) })
test('directive with argument and modifiers', () => { test('directive with argument and modifiers', () => {
const ast = parse('<div v-on:click.enter.exact/>') const ast = baseParse('<div v-on:click.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1130,7 +1196,7 @@ describe('compiler: parse', () => {
}) })
test('v-bind shorthand', () => { test('v-bind shorthand', () => {
const ast = parse('<div :a=b />') const ast = baseParse('<div :a=b />')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1178,7 +1244,7 @@ describe('compiler: parse', () => {
}) })
test('v-bind shorthand with modifier', () => { test('v-bind shorthand with modifier', () => {
const ast = parse('<div :a.sync=b />') const ast = baseParse('<div :a.sync=b />')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1226,7 +1292,7 @@ describe('compiler: parse', () => {
}) })
test('v-on shorthand', () => { test('v-on shorthand', () => {
const ast = parse('<div @a=b />') const ast = baseParse('<div @a=b />')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1274,7 +1340,7 @@ describe('compiler: parse', () => {
}) })
test('v-on shorthand with modifier', () => { test('v-on shorthand with modifier', () => {
const ast = parse('<div @a.enter=b />') const ast = baseParse('<div @a.enter=b />')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1322,7 +1388,7 @@ describe('compiler: parse', () => {
}) })
test('v-slot shorthand', () => { test('v-slot shorthand', () => {
const ast = parse('<Comp #a="{ b }" />') const ast = baseParse('<Comp #a="{ b }" />')
const directive = (ast.children[0] as ElementNode).props[0] const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({ expect(directive).toStrictEqual({
@ -1369,7 +1435,7 @@ describe('compiler: parse', () => {
}) })
test('v-pre', () => { test('v-pre', () => {
const ast = parse( const ast = baseParse(
`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` + `<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
`<div :id="foo"><Comp/>{{ bar }}</div>` `<div :id="foo"><Comp/>{{ bar }}</div>`
) )
@ -1451,7 +1517,7 @@ describe('compiler: parse', () => {
}) })
test('end tags are case-insensitive.', () => { test('end tags are case-insensitive.', () => {
const ast = parse('<div>hello</DIV>after') const ast = baseParse('<div>hello</DIV>after')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode const text = element.children[0] as TextNode
@ -1468,14 +1534,14 @@ describe('compiler: parse', () => {
}) })
test('self closing single tag', () => { test('self closing single tag', () => {
const ast = parse('<div :class="{ some: condition }" />') const ast = baseParse('<div :class="{ some: condition }" />')
expect(ast.children).toHaveLength(1) expect(ast.children).toHaveLength(1)
expect(ast.children[0]).toMatchObject({ tag: 'div' }) expect(ast.children[0]).toMatchObject({ tag: 'div' })
}) })
test('self closing multiple tag', () => { test('self closing multiple tag', () => {
const ast = parse( const ast = baseParse(
`<div :class="{ some: condition }" />\n` + `<div :class="{ some: condition }" />\n` +
`<p v-bind:style="{ color: 'red' }"/>` `<p v-bind:style="{ color: 'red' }"/>`
) )
@ -1488,7 +1554,7 @@ describe('compiler: parse', () => {
}) })
test('valid html', () => { test('valid html', () => {
const ast = parse( const ast = baseParse(
`<div :class="{ some: condition }">\n` + `<div :class="{ some: condition }">\n` +
` <p v-bind:style="{ color: 'red' }"/>\n` + ` <p v-bind:style="{ color: 'red' }"/>\n` +
` <!-- a comment with <html> inside it -->\n` + ` <!-- a comment with <html> inside it -->\n` +
@ -1513,11 +1579,11 @@ describe('compiler: parse', () => {
test('invalid html', () => { test('invalid html', () => {
expect(() => { expect(() => {
parse(`<div>\n<span>\n</div>\n</span>`) baseParse(`<div>\n<span>\n</div>\n</span>`)
}).toThrow('End tag was not found. (3:1)') }).toThrow('Element is missing end tag.')
const spy = jest.fn() const spy = jest.fn()
const ast = parse(`<div>\n<span>\n</div>\n</span>`, { const ast = baseParse(`<div>\n<span>\n</div>\n</span>`, {
onError: spy onError: spy
}) })
@ -1527,8 +1593,8 @@ describe('compiler: parse', () => {
code: ErrorCodes.X_MISSING_END_TAG, code: ErrorCodes.X_MISSING_END_TAG,
loc: { loc: {
start: { start: {
offset: 13, offset: 6,
line: 3, line: 2,
column: 1 column: 1
} }
} }
@ -1552,7 +1618,7 @@ describe('compiler: parse', () => {
}) })
test('parse with correct location info', () => { test('parse with correct location info', () => {
const [foo, bar, but, baz] = parse( const [foo, bar, but, baz] = baseParse(
` `
foo foo
is {{ bar }} but {{ baz }}`.trim() is {{ bar }} but {{ baz }}`.trim()
@ -1588,7 +1654,7 @@ foo
describe('namedCharacterReferences option', () => { describe('namedCharacterReferences option', () => {
test('use the given map', () => { test('use the given map', () => {
const ast: any = parse('&amp;&cups;', { const ast: any = baseParse('&amp;&cups;', {
namedCharacterReferences: { namedCharacterReferences: {
'cups;': '\u222A\uFE00' // UNION with serifs 'cups;': '\u222A\uFE00' // UNION with serifs
}, },
@ -1603,18 +1669,18 @@ foo
describe('whitespace management', () => { describe('whitespace management', () => {
it('should remove whitespaces at start/end inside an element', () => { it('should remove whitespaces at start/end inside an element', () => {
const ast = parse(`<div> <span/> </div>`) const ast = baseParse(`<div> <span/> </div>`)
expect((ast.children[0] as ElementNode).children.length).toBe(1) expect((ast.children[0] as ElementNode).children.length).toBe(1)
}) })
it('should remove whitespaces w/ newline between elements', () => { it('should remove whitespaces w/ newline between elements', () => {
const ast = parse(`<div/> \n <div/> \n <div/>`) const ast = baseParse(`<div/> \n <div/> \n <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true) expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
}) })
it('should remove whitespaces adjacent to comments', () => { it('should remove whitespaces adjacent to comments', () => {
const ast = parse(`<div/> \n <!--foo--> <div/>`) const ast = baseParse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@ -1622,7 +1688,7 @@ foo
}) })
it('should remove whitespaces w/ newline between comments and elements', () => { it('should remove whitespaces w/ newline between comments and elements', () => {
const ast = parse(`<div/> \n <!--foo--> \n <div/>`) const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@ -1630,7 +1696,7 @@ foo
}) })
it('should NOT remove whitespaces w/ newline between interpolations', () => { it('should NOT remove whitespaces w/ newline between interpolations', () => {
const ast = parse(`{{ foo }} \n {{ bar }}`) const ast = baseParse(`{{ foo }} \n {{ bar }}`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION) expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
expect(ast.children[1]).toMatchObject({ expect(ast.children[1]).toMatchObject({
@ -1641,7 +1707,7 @@ foo
}) })
it('should NOT remove whitespaces w/o newline between elements', () => { it('should NOT remove whitespaces w/o newline between elements', () => {
const ast = parse(`<div/> <div/> <div/>`) const ast = baseParse(`<div/> <div/> <div/>`)
expect(ast.children.length).toBe(5) expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([ expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT, NodeTypes.ELEMENT,
@ -1653,7 +1719,7 @@ foo
}) })
it('should condense consecutive whitespaces in text', () => { it('should condense consecutive whitespaces in text', () => {
const ast = parse(` foo \n bar baz `) const ast = baseParse(` foo \n bar baz `)
expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `) expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
}) })
}) })
@ -1833,7 +1899,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 11, line: 1, column: 12 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1846,7 +1912,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 12, line: 1, column: 13 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -1861,11 +1927,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 29, line: 1, column: 30 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 29, line: 1, column: 30 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1878,11 +1944,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -1897,7 +1963,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1910,7 +1976,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1925,7 +1991,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 12, line: 1, column: 13 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1938,7 +2004,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 13, line: 1, column: 14 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -1951,7 +2017,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -1962,7 +2028,7 @@ foo
errors: [ errors: [
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 32, line: 1, column: 33 } loc: { offset: 0, line: 1, column: 1 }
}, },
{ {
type: ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT, type: ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
@ -1975,7 +2041,7 @@ foo
errors: [ errors: [
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 28, line: 1, column: 29 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -1990,11 +2056,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 14, line: 1, column: 15 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2007,11 +2073,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2024,11 +2090,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 17, line: 1, column: 18 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 17, line: 1, column: 18 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2041,11 +2107,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 18, line: 1, column: 19 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 18, line: 1, column: 19 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2062,11 +2128,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2079,11 +2145,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2096,11 +2162,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 22, line: 1, column: 23 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2113,11 +2179,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2130,11 +2196,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2147,11 +2213,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 21, line: 1, column: 22 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2168,11 +2234,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2189,11 +2255,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 24, line: 1, column: 25 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2210,11 +2276,11 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 23, line: 1, column: 24 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -2292,7 +2358,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 27, line: 1, column: 28 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
}, },
@ -2429,7 +2495,7 @@ foo
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 19, line: 1, column: 20 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -2594,17 +2660,6 @@ foo
] ]
} }
], ],
UNKNOWN_NAMED_CHARACTER_REFERENCE: [
{
code: '<template>&unknown;</template>',
errors: [
{
type: ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
X_INVALID_END_TAG: [ X_INVALID_END_TAG: [
{ {
code: '<template></div></template>', code: '<template></div></template>',
@ -2651,7 +2706,7 @@ foo
errors: [ errors: [
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 10, line: 1, column: 11 }
} }
] ]
}, },
@ -2660,11 +2715,11 @@ foo
errors: [ errors: [
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 10, line: 1, column: 11 }
}, },
{ {
type: ErrorCodes.X_MISSING_END_TAG, type: ErrorCodes.X_MISSING_END_TAG,
loc: { offset: 15, line: 1, column: 16 } loc: { offset: 0, line: 1, column: 1 }
} }
] ]
} }
@ -2716,7 +2771,7 @@ foo
), ),
() => { () => {
const spy = jest.fn() const spy = jest.fn()
const ast = parse(code, { const ast = baseParse(code, {
getNamespace: (tag, parent) => { getNamespace: (tag, parent) => {
const ns = parent ? parent.ns : Namespaces.HTML const ns = parent ? parent.ns : Namespaces.HTML
if (ns === Namespaces.HTML) { if (ns === Namespaces.HTML) {

View File

@ -0,0 +1,97 @@
import { baseCompile } from '../src/compile'
import {
WITH_SCOPE_ID,
PUSH_SCOPE_ID,
POP_SCOPE_ID
} from '../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { genFlagText } from './testUtils'
describe('scopeId compiler support', () => {
test('should only work in module mode', () => {
expect(() => {
baseCompile(``, { scopeId: 'test' })
}).toThrow(`"scopeId" option is only supported in module mode`)
})
test('should wrap render function', () => {
const { ast, code } = baseCompile(`<div/>`, {
mode: 'module',
scopeId: 'test'
})
expect(ast.helpers).toContain(WITH_SCOPE_ID)
expect(code).toMatch(`const _withId = _withScopeId("test")`)
expect(code).toMatch(`export const render = _withId(function render(`)
expect(code).toMatchSnapshot()
})
test('should wrap default slot', () => {
const { code } = baseCompile(`<Child><div/></Child>`, {
mode: 'module',
scopeId: 'test'
})
expect(code).toMatch(`default: _withId(() => [`)
expect(code).toMatchSnapshot()
})
test('should wrap named slots', () => {
const { code } = baseCompile(
`<Child>
<template #foo="{ msg }">{{ msg }}</template>
<template #bar><div/></template>
</Child>
`,
{
mode: 'module',
scopeId: 'test'
}
)
expect(code).toMatch(`foo: _withId(({ msg }) => [`)
expect(code).toMatch(`bar: _withId(() => [`)
expect(code).toMatchSnapshot()
})
test('should wrap dynamic slots', () => {
const { code } = baseCompile(
`<Child>
<template #foo v-if="ok"><div/></template>
<template v-for="i in list" #[i]><div/></template>
</Child>
`,
{
mode: 'module',
scopeId: 'test'
}
)
expect(code).toMatch(/name: "foo",\s+fn: _withId\(/)
expect(code).toMatch(/name: i,\s+fn: _withId\(/)
expect(code).toMatchSnapshot()
})
test('should push scopeId for hoisted nodes', () => {
const { ast, code } = baseCompile(
`<div><div>hello</div>{{ foo }}<div>world</div></div>`,
{
mode: 'module',
scopeId: 'test',
hoistStatic: true
}
)
expect(ast.helpers).toContain(PUSH_SCOPE_ID)
expect(ast.helpers).toContain(POP_SCOPE_ID)
expect(ast.hoists.length).toBe(2)
expect(code).toMatch(
[
`_pushScopeId("test")`,
`const _hoisted_1 = _createVNode("div", null, "hello", ${genFlagText(
PatchFlags.HOISTED
)})`,
`const _hoisted_2 = _createVNode("div", null, "world", ${genFlagText(
PatchFlags.HOISTED
)})`,
`_popScopeId()`
].join('\n')
)
expect(code).toMatchSnapshot()
})
})

View File

@ -4,9 +4,8 @@ import {
locStub, locStub,
Namespaces, Namespaces,
ElementTypes, ElementTypes,
PlainElementCodegenNode VNodeCall
} from '../src' } from '../src'
import { CREATE_VNODE } from '../src/runtimeHelpers'
import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared' import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared'
const leadingBracketRE = /^\[/ const leadingBracketRE = /^\[/
@ -39,7 +38,11 @@ export function createObjectMatcher(obj: Record<string, any>) {
} }
export function createElementWithCodegen( export function createElementWithCodegen(
args: PlainElementCodegenNode['arguments'] tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag'],
dynamicProps?: VNodeCall['dynamicProps']
): ElementNode { ): ElementNode {
return { return {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -51,10 +54,16 @@ export function createElementWithCodegen(
props: [], props: [],
children: [], children: [],
codegenNode: { codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
loc: locStub, tag,
callee: CREATE_VNODE, props,
arguments: args children,
patchFlag,
dynamicProps,
directives: undefined,
isBlock: false,
isForBlock: false,
loc: locStub
} }
} }
} }

View File

@ -1,19 +1,17 @@
import { parse } from '../src/parse' import { baseParse } from '../src/parse'
import { transform, NodeTransform } from '../src/transform' import { transform, NodeTransform } from '../src/transform'
import { import {
ElementNode, ElementNode,
NodeTypes, NodeTypes,
DirectiveNode, DirectiveNode,
ExpressionNode ExpressionNode,
VNodeCall
} from '../src/ast' } from '../src/ast'
import { ErrorCodes, createCompilerError } from '../src/errors' import { ErrorCodes, createCompilerError } from '../src/errors'
import { import {
TO_STRING, TO_DISPLAY_STRING,
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT, FRAGMENT,
RENDER_SLOT, RENDER_SLOT,
WITH_DIRECTIVES,
CREATE_COMMENT CREATE_COMMENT
} from '../src/runtimeHelpers' } from '../src/runtimeHelpers'
import { transformIf } from '../src/transforms/vIf' import { transformIf } from '../src/transforms/vIf'
@ -26,7 +24,7 @@ import { PatchFlags } from '@vue/shared'
describe('compiler: transform', () => { describe('compiler: transform', () => {
test('context state', () => { test('context state', () => {
const ast = parse(`<div>hello {{ world }}</div>`) const ast = baseParse(`<div>hello {{ world }}</div>`)
// manually store call arguments because context is mutable and shared // manually store call arguments because context is mutable and shared
// across calls // across calls
@ -72,7 +70,7 @@ describe('compiler: transform', () => {
}) })
test('context.replaceNode', () => { test('context.replaceNode', () => {
const ast = parse(`<div/><span/>`) const ast = baseParse(`<div/><span/>`)
const plugin: NodeTransform = (node, context) => { const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
// change the node to <p> // change the node to <p>
@ -106,7 +104,7 @@ describe('compiler: transform', () => {
}) })
test('context.removeNode', () => { test('context.removeNode', () => {
const ast = parse(`<span/><div>hello</div><span/>`) const ast = baseParse(`<span/><div>hello</div><span/>`)
const c1 = ast.children[0] const c1 = ast.children[0]
const c2 = ast.children[2] const c2 = ast.children[2]
@ -132,7 +130,7 @@ describe('compiler: transform', () => {
}) })
test('context.removeNode (prev sibling)', () => { test('context.removeNode (prev sibling)', () => {
const ast = parse(`<span/><div/><span/>`) const ast = baseParse(`<span/><div/><span/>`)
const c1 = ast.children[0] const c1 = ast.children[0]
const c2 = ast.children[2] const c2 = ast.children[2]
@ -159,7 +157,7 @@ describe('compiler: transform', () => {
}) })
test('context.removeNode (next sibling)', () => { test('context.removeNode (next sibling)', () => {
const ast = parse(`<span/><div/><span/>`) const ast = baseParse(`<span/><div/><span/>`)
const c1 = ast.children[0] const c1 = ast.children[0]
const d1 = ast.children[1] const d1 = ast.children[1]
@ -186,7 +184,7 @@ describe('compiler: transform', () => {
}) })
test('context.hoist', () => { test('context.hoist', () => {
const ast = parse(`<div :id="foo"/><div :id="bar"/>`) const ast = baseParse(`<div :id="foo"/><div :id="bar"/>`)
const hoisted: ExpressionNode[] = [] const hoisted: ExpressionNode[] = []
const mock: NodeTransform = (node, context) => { const mock: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
@ -204,7 +202,7 @@ describe('compiler: transform', () => {
}) })
test('onError option', () => { test('onError option', () => {
const ast = parse(`<div/>`) const ast = baseParse(`<div/>`)
const loc = ast.children[0].loc const loc = ast.children[0].loc
const plugin: NodeTransform = (node, context) => { const plugin: NodeTransform = (node, context) => {
context.onError( context.onError(
@ -225,20 +223,20 @@ describe('compiler: transform', () => {
}) })
test('should inject toString helper for interpolations', () => { test('should inject toString helper for interpolations', () => {
const ast = parse(`{{ foo }}`) const ast = baseParse(`{{ foo }}`)
transform(ast, {}) transform(ast, {})
expect(ast.helpers).toContain(TO_STRING) expect(ast.helpers).toContain(TO_DISPLAY_STRING)
}) })
test('should inject createVNode and Comment for comments', () => { test('should inject createVNode and Comment for comments', () => {
const ast = parse(`<!--foo-->`) const ast = baseParse(`<!--foo-->`)
transform(ast, {}) transform(ast, {})
expect(ast.helpers).toContain(CREATE_COMMENT) expect(ast.helpers).toContain(CREATE_COMMENT)
}) })
describe('root codegenNode', () => { describe('root codegenNode', () => {
function transformWithCodegen(template: string) { function transformWithCodegen(template: string) {
const ast = parse(template) const ast = baseParse(template)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
transformIf, transformIf,
@ -251,20 +249,19 @@ describe('compiler: transform', () => {
return ast return ast
} }
function createBlockMatcher(args: any[]) { function createBlockMatcher(
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag']
) {
return { return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ isBlock: true,
{ tag,
type: NodeTypes.JS_CALL_EXPRESSION, props,
callee: OPEN_BLOCK children,
}, patchFlag
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
arguments: args
}
]
} }
} }
@ -285,7 +282,7 @@ describe('compiler: transform', () => {
test('single element', () => { test('single element', () => {
const ast = transformWithCodegen(`<div/>`) const ast = transformWithCodegen(`<div/>`)
expect(ast.codegenNode).toMatchObject(createBlockMatcher([`"div"`])) expect(ast.codegenNode).toMatchObject(createBlockMatcher(`"div"`))
}) })
test('root v-if', () => { test('root v-if', () => {
@ -305,22 +302,8 @@ describe('compiler: transform', () => {
test('root element with custom directive', () => { test('root element with custom directive', () => {
const ast = transformWithCodegen(`<div v-foo/>`) const ast = transformWithCodegen(`<div v-foo/>`)
expect(ast.codegenNode).toMatchObject({ expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ directives: { type: NodeTypes.JS_ARRAY_EXPRESSION }
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
// should wrap withDirectives() around createBlock()
callee: WITH_DIRECTIVES,
arguments: [
{ callee: CREATE_BLOCK },
{ type: NodeTypes.JS_ARRAY_EXPRESSION }
]
}
]
}) })
}) })
@ -348,15 +331,15 @@ describe('compiler: transform', () => {
test('multiple children', () => { test('multiple children', () => {
const ast = transformWithCodegen(`<div/><div/>`) const ast = transformWithCodegen(`<div/><div/>`)
expect(ast.codegenNode).toMatchObject( expect(ast.codegenNode).toMatchObject(
createBlockMatcher([ createBlockMatcher(
FRAGMENT, FRAGMENT,
`null`, undefined,
[ [
{ type: NodeTypes.ELEMENT, tag: `div` }, { type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.ELEMENT, tag: `div` } { type: NodeTypes.ELEMENT, tag: `div` }
], ] as any,
genFlagText(PatchFlags.STABLE_FRAGMENT) genFlagText(PatchFlags.STABLE_FRAGMENT)
]) )
) )
}) })
}) })

View File

@ -2,13 +2,13 @@
exports[`compiler: hoistStatic transform hoist element with static key 1`] = ` exports[`compiler: hoistStatic transform hoist element with static key 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }) const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -19,16 +19,16 @@ return function render() {
exports[`compiler: hoistStatic transform hoist nested static tree 1`] = ` exports[`compiler: hoistStatic transform hoist nested static tree 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"p\\", null, [ const _hoisted_1 = _createVNode(\\"p\\", null, [
_createVNode(\\"span\\"), _createVNode(\\"span\\"),
_createVNode(\\"span\\") _createVNode(\\"span\\")
]) ], -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -39,15 +39,15 @@ return function render() {
exports[`compiler: hoistStatic transform hoist nested static tree with comments 1`] = ` exports[`compiler: hoistStatic transform hoist nested static tree with comments 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = Vue const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = _createVNode(\\"div\\", null, [ const _hoisted_1 = _createVNode(\\"div\\", null, [
_createCommentVNode(\\"comment\\") _createCommentVNode(\\"comment\\")
]) ], -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createCommentVNode: _createCommentVNode, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createCommentVNode: _createCommentVNode, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -58,14 +58,14 @@ return function render() {
exports[`compiler: hoistStatic transform hoist siblings with common non-hoistable parent 1`] = ` exports[`compiler: hoistStatic transform hoist siblings with common non-hoistable parent 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\") const _hoisted_1 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
const _hoisted_2 = _createVNode(\\"div\\") const _hoisted_2 = _createVNode(\\"div\\", null, null, -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1, _hoisted_1,
@ -77,13 +77,13 @@ return function render() {
exports[`compiler: hoistStatic transform hoist simple element 1`] = ` exports[`compiler: hoistStatic transform hoist simple element 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\") const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -94,18 +94,18 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with directives 1`] = ` exports[`compiler: hoistStatic transform hoist static props for elements with directives 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" } const _hoisted_1 = { id: \\"foo\\" }
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\") const _directive_foo = _resolveDirective(\\"foo\\")
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [ _withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 512 /* NEED_PATCH */), [
[_directive_foo] [_directive_foo]
]) ])
])) ]))
@ -115,16 +115,16 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with dynamic text children 1`] = ` exports[`compiler: hoistStatic transform hoist static props for elements with dynamic text children 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" } const _hoisted_1 = { id: \\"foo\\" }
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", _hoisted_1, _toString(hello), 1 /* TEXT */) _createVNode(\\"div\\", _hoisted_1, _toDisplayString(hello), 1 /* TEXT */)
])) ]))
} }
}" }"
@ -132,13 +132,13 @@ return function render() {
exports[`compiler: hoistStatic transform hoist static props for elements with unhoistable children 1`] = ` exports[`compiler: hoistStatic transform hoist static props for elements with unhoistable children 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" } const _hoisted_1 = { id: \\"foo\\" }
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
@ -153,16 +153,16 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist class with static object value 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers hoist class with static object value 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { class: { foo: true } } const _hoisted_1 = { class: { foo: true } }
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", _hoisted_1, _toString(_ctx.bar), 1 /* TEXT */) _createVNode(\\"span\\", _hoisted_1, _toDisplayString(_ctx.bar), 1 /* TEXT */)
])) ]))
} }
}" }"
@ -170,18 +170,13 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static interpolation 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static interpolation 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", null, [ const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -1 /* HOISTED */)
\\"foo \\",
_toString(1),
\\" \\",
_toString(true)
])
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -192,13 +187,13 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static prop value 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static prop value 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toString(1)) const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { toDisplayString: _toDisplayString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_hoisted_1 _hoisted_1
@ -208,14 +203,12 @@ return function render() {
`; `;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"div\\", null, [
const _cache = _ctx.$cache _createVNode(\\"div\\", null, [
return (openBlock(), createBlock(\\"div\\", null, [ _createVNode(\\"div\\", {
createVNode(\\"div\\", null, [
createVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.foo($event))) onClick: _cache[1] || (_cache[1] = $event => (_ctx.foo($event)))
}) })
]) ])
@ -226,14 +219,14 @@ export default function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => { (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
return (_openBlock(), _createBlock(\\"p\\", null, [ return (_openBlock(), _createBlock(\\"p\\", null, [
_createVNode(\\"span\\", null, _toString(o + 'foo'), 1 /* TEXT */) _createVNode(\\"span\\", null, _toDisplayString(o + 'foo'), 1 /* TEXT */)
])) ]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
])) ]))
@ -244,15 +237,17 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (v-slot) 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (v-slot) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, { return (_openBlock(), _createBlock(_component_Comp, null, {
default: ({ foo }) => [_toString(_ctx.foo)], default: _withCtx(({ foo }) => [
_compiled: true _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
]),
_: 1
})) }))
} }
}" }"
@ -261,14 +256,14 @@ return function render() {
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables 1`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => { (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
return (_openBlock(), _createBlock(\\"p\\", null, [ return (_openBlock(), _createBlock(\\"p\\", null, [
_createVNode(\\"span\\", null, _toString(o), 1 /* TEXT */) _createVNode(\\"span\\", null, _toDisplayString(o), 1 /* TEXT */)
])) ]))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
])) ]))
@ -279,9 +274,9 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist components 1`] = ` exports[`compiler: hoistStatic transform should NOT hoist components 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
@ -295,12 +290,12 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic key 1`] = ` exports[`compiler: hoistStatic transform should NOT hoist element with dynamic key 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { key: foo }) (_openBlock(), _createBlock(\\"div\\", { key: foo }))
])) ]))
} }
}" }"
@ -309,9 +304,9 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic props 1`] = ` exports[`compiler: hoistStatic transform should NOT hoist element with dynamic props 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]) _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"])
@ -323,12 +318,12 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist element with dynamic ref 1`] = ` exports[`compiler: hoistStatic transform should NOT hoist element with dynamic ref 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", { ref: foo }, null, 32 /* NEED_PATCH */) _createVNode(\\"div\\", { ref: foo }, null, 512 /* NEED_PATCH */)
])) ]))
} }
}" }"
@ -337,9 +332,9 @@ return function render() {
exports[`compiler: hoistStatic transform should NOT hoist root node 1`] = ` exports[`compiler: hoistStatic transform should NOT hoist root node 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\")) return (_openBlock(), _createBlock(\\"div\\"))
} }
@ -348,17 +343,17 @@ return function render() {
exports[`compiler: hoistStatic transform should hoist v-for children if static 1`] = ` exports[`compiler: hoistStatic transform should hoist v-for children if static 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue const { createVNode: _createVNode } = _Vue
const _hoisted_1 = { id: \\"foo\\" } const _hoisted_1 = { id: \\"foo\\" }
const _hoisted_2 = _createVNode(\\"span\\") const _hoisted_2 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => { (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [ return (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [
_hoisted_2 _hoisted_2
])) ]))
@ -370,24 +365,24 @@ return function render() {
exports[`compiler: hoistStatic transform should hoist v-if props/children if static 1`] = ` exports[`compiler: hoistStatic transform should hoist v-if props/children if static 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = Vue const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = { const _hoisted_1 = {
key: 0, key: 0,
id: \\"foo\\" id: \\"foo\\"
} }
const _hoisted_2 = _createVNode(\\"span\\") const _hoisted_2 = _createVNode(\\"span\\", null, null, -1 /* HOISTED */)
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
(_openBlock(), ok ok
? _createBlock(\\"div\\", _hoisted_1, [ ? (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [
_hoisted_2 _hoisted_2
]) ]))
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
])) ]))
} }
}" }"

View File

@ -3,11 +3,11 @@
exports[`compiler: transform text <template v-for> 1`] = ` exports[`compiler: transform text <template v-for> 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createTextVNode: _createTextVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createTextVNode: _createTextVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
_createTextVNode(\\"foo\\") _createTextVNode(\\"foo\\")
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
@ -19,11 +19,11 @@ return function render() {
exports[`compiler: transform text consecutive text 1`] = ` exports[`compiler: transform text consecutive text 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString } = _Vue const { toDisplayString: _toDisplayString } = _Vue
return _toString(foo) + \\" bar \\" + _toString(baz) return _toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz)
} }
}" }"
`; `;
@ -31,13 +31,13 @@ return function render() {
exports[`compiler: transform text consecutive text between elements 1`] = ` exports[`compiler: transform text consecutive text between elements 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, toString: _toString, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"), _createVNode(\\"div\\"),
_createTextVNode(_toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */), _createTextVNode(_toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz), 1 /* TEXT */),
_createVNode(\\"div\\") _createVNode(\\"div\\")
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }
@ -47,13 +47,13 @@ return function render() {
exports[`compiler: transform text consecutive text mixed with elements 1`] = ` exports[`compiler: transform text consecutive text mixed with elements 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, toString: _toString, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"), _createVNode(\\"div\\"),
_createTextVNode(_toString(foo) + \\" bar \\" + _toString(baz), 1 /* TEXT */), _createTextVNode(_toDisplayString(foo) + \\" bar \\" + _toDisplayString(baz), 1 /* TEXT */),
_createVNode(\\"div\\"), _createVNode(\\"div\\"),
_createTextVNode(\\"hello\\"), _createTextVNode(\\"hello\\"),
_createVNode(\\"div\\") _createVNode(\\"div\\")
@ -65,11 +65,11 @@ return function render() {
exports[`compiler: transform text no consecutive text 1`] = ` exports[`compiler: transform text no consecutive text 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { toString: _toString } = _Vue const { toDisplayString: _toDisplayString } = _Vue
return _toString(foo) return _toDisplayString(foo)
} }
}" }"
`; `;
@ -77,9 +77,9 @@ return function render() {
exports[`compiler: transform text text between elements (static) 1`] = ` exports[`compiler: transform text text between elements (static) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createTextVNode: _createTextVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\"), _createVNode(\\"div\\"),
@ -91,10 +91,9 @@ return function render() {
`; `;
exports[`compiler: transform text with prefixIdentifiers: true 1`] = ` exports[`compiler: transform text with prefixIdentifiers: true 1`] = `
"const { toString } = Vue "const { toDisplayString: _toDisplayString } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this return _toDisplayString(_ctx.foo) + \\" bar \\" + _toDisplayString(_ctx.baz + _ctx.qux)
return toString(_ctx.foo) + \\" bar \\" + toString(_ctx.baz + _ctx.qux)
}" }"
`; `;

View File

@ -3,11 +3,11 @@
exports[`compiler: v-for codegen basic v-for 1`] = ` exports[`compiler: v-for codegen basic v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(\\"span\\")) return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -17,11 +17,11 @@ return function render() {
exports[`compiler: v-for codegen keyed template v-for 1`] = ` exports[`compiler: v-for codegen keyed template v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(_Fragment, { key: item }, [ return (_openBlock(), _createBlock(_Fragment, { key: item }, [
\\"hello\\", \\"hello\\",
_createVNode(\\"span\\") _createVNode(\\"span\\")
@ -34,11 +34,11 @@ return function render() {
exports[`compiler: v-for codegen keyed v-for 1`] = ` exports[`compiler: v-for codegen keyed v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(\\"span\\", { key: item })) return (_openBlock(), _createBlock(\\"span\\", { key: item }))
}), 128 /* KEYED_FRAGMENT */)) }), 128 /* KEYED_FRAGMENT */))
} }
@ -48,11 +48,11 @@ return function render() {
exports[`compiler: v-for codegen skipped key 1`] = ` exports[`compiler: v-for codegen skipped key 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item, __, index) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item, __, index) => {
return (_openBlock(), _createBlock(\\"span\\")) return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -62,11 +62,11 @@ return function render() {
exports[`compiler: v-for codegen skipped value & key 1`] = ` exports[`compiler: v-for codegen skipped value & key 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (_, __, index) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (_, __, index) => {
return (_openBlock(), _createBlock(\\"span\\")) return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -76,11 +76,11 @@ return function render() {
exports[`compiler: v-for codegen skipped value 1`] = ` exports[`compiler: v-for codegen skipped value 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (_, key, index) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (_, key, index) => {
return (_openBlock(), _createBlock(\\"span\\")) return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -90,11 +90,11 @@ return function render() {
exports[`compiler: v-for codegen template v-for 1`] = ` exports[`compiler: v-for codegen template v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
\\"hello\\", \\"hello\\",
_createVNode(\\"span\\") _createVNode(\\"span\\")
@ -107,11 +107,11 @@ return function render() {
exports[`compiler: v-for codegen template v-for w/ <slot/> 1`] = ` exports[`compiler: v-for codegen template v-for w/ <slot/> 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, \\"default\\") return _renderSlot($slots, \\"default\\")
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -121,11 +121,11 @@ return function render() {
exports[`compiler: v-for codegen v-for on <slot/> 1`] = ` exports[`compiler: v-for codegen v-for on <slot/> 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, \\"default\\") return _renderSlot($slots, \\"default\\")
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
@ -135,16 +135,16 @@ return function render() {
exports[`compiler: v-for codegen v-for on element with custom directive 1`] = ` exports[`compiler: v-for codegen v-for on element with custom directive 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\") const _directive_foo = _resolveDirective(\\"foo\\")
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(list, (i) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _withDirectives(_createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */), [ return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 512 /* NEED_PATCH */)), [
[_directive_foo] [_directive_foo]
])) ])
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }
}" }"
@ -153,15 +153,15 @@ return function render() {
exports[`compiler: v-for codegen v-if + v-for 1`] = ` exports[`compiler: v-for codegen v-if + v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, renderList: _renderList, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _createBlock(_Fragment, { key: 0 }, _renderList(list, (i) => { ? (_openBlock(true), _createBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
return (_openBlock(), _createBlock(\\"div\\")) return (_openBlock(), _createBlock(\\"div\\"))
}), 256 /* UNKEYED_FRAGMENT */) }), 256 /* UNKEYED_FRAGMENT */))
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;
@ -169,11 +169,11 @@ return function render() {
exports[`compiler: v-for codegen value + key + index 1`] = ` exports[`compiler: v-for codegen value + key + index 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createVNode: _createVNode } = _Vue
return (_openBlock(false), _createBlock(_Fragment, null, _renderList(items, (item, key, index) => { return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item, key, index) => {
return (_openBlock(), _createBlock(\\"span\\")) return (_openBlock(), _createBlock(\\"span\\"))
}), 256 /* UNKEYED_FRAGMENT */)) }), 256 /* UNKEYED_FRAGMENT */))
} }

View File

@ -3,13 +3,13 @@
exports[`compiler: v-if codegen basic v-if 1`] = ` exports[`compiler: v-if codegen basic v-if 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _createBlock(\\"div\\", { key: 0 }) ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;
@ -17,17 +17,17 @@ return function render() {
exports[`compiler: v-if codegen template v-if 1`] = ` exports[`compiler: v-if codegen template v-if 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, Fragment: _Fragment, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue const { createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _createBlock(_Fragment, { key: 0 }, [ ? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [
_createVNode(\\"div\\"), _createVNode(\\"div\\"),
\\"hello\\", \\"hello\\",
_createVNode(\\"p\\") _createVNode(\\"p\\")
]) ], 64 /* STABLE_FRAGMENT */))
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;
@ -35,13 +35,13 @@ return function render() {
exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = ` exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue const { renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _renderSlot($slots, \\"default\\", { key: 0 }) ? _renderSlot($slots, \\"default\\", { key: 0 })
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;
@ -49,13 +49,13 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else 1`] = ` exports[`compiler: v-if codegen v-if + v-else 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _createBlock(\\"div\\", { key: 0 }) ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: _createBlock(\\"p\\", { key: 1 })) : (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
} }
}" }"
`; `;
@ -63,15 +63,15 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else-if + v-else 1`] = ` exports[`compiler: v-if codegen v-if + v-else-if + v-else 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
return (_openBlock(), ok return ok
? _createBlock(\\"div\\", { key: 0 }) ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: orNot : orNot
? _createBlock(\\"p\\", { key: 1 }) ? (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
: _createBlock(_Fragment, { key: 2 }, [\\"fine\\"])) : (_openBlock(), _createBlock(_Fragment, { key: 2 }, [\\"fine\\"], 64 /* STABLE_FRAGMENT */))
} }
}" }"
`; `;
@ -79,15 +79,15 @@ return function render() {
exports[`compiler: v-if codegen v-if + v-else-if 1`] = ` exports[`compiler: v-if codegen v-if + v-else-if 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _createBlock(\\"div\\", { key: 0 }) ? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: orNot : orNot
? _createBlock(\\"p\\", { key: 1 }) ? (_openBlock(), _createBlock(\\"p\\", { key: 1 }))
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;
@ -95,13 +95,27 @@ return function render() {
exports[`compiler: v-if codegen v-if on <slot/> 1`] = ` exports[`compiler: v-if codegen v-if on <slot/> 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { openBlock: _openBlock, renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue const { renderSlot: _renderSlot, createCommentVNode: _createCommentVNode } = _Vue
return (_openBlock(), ok return ok
? _renderSlot($slots, \\"default\\", { key: 0 }) ? _renderSlot($slots, \\"default\\", { key: 0 })
: _createCommentVNode(\\"v-if\\", true)) : _createCommentVNode(\\"v-if\\", true)
}
}"
`;
exports[`compiler: v-if codegen v-if with key 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(), _createBlock(\\"div\\", { key: \\"some-key\\" }))
: _createCommentVNode(\\"v-if\\", true)
} }
}" }"
`; `;

View File

@ -1,11 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform v-model compound expression (with prefixIdentifiers) 1`] = ` exports[`compiler: transform v-model compound expression (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"input\\", {
return (openBlock(), createBlock(\\"input\\", {
modelValue: _ctx.model[_ctx.index], modelValue: _ctx.model[_ctx.index],
\\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event) \\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"])) }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
@ -15,9 +14,9 @@ export default function render() {
exports[`compiler: transform v-model compound expression 1`] = ` exports[`compiler: transform v-model compound expression 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", { return (_openBlock(), _createBlock(\\"input\\", {
modelValue: model[index], modelValue: model[index],
@ -28,11 +27,10 @@ return function render() {
`; `;
exports[`compiler: transform v-model simple exprssion (with prefixIdentifiers) 1`] = ` exports[`compiler: transform v-model simple exprssion (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"input\\", {
return (openBlock(), createBlock(\\"input\\", {
modelValue: _ctx.model, modelValue: _ctx.model,
\\"onUpdate:modelValue\\": $event => (_ctx.model = $event) \\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"])) }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
@ -42,9 +40,9 @@ export default function render() {
exports[`compiler: transform v-model simple exprssion 1`] = ` exports[`compiler: transform v-model simple exprssion 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", { return (_openBlock(), _createBlock(\\"input\\", {
modelValue: model, modelValue: model,
@ -57,24 +55,23 @@ return function render() {
exports[`compiler: transform v-model with argument 1`] = ` exports[`compiler: transform v-model with argument 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", { return (_openBlock(), _createBlock(\\"input\\", {
value: model, value: model,
\\"onUpdate:value\\": $event => (model = $event) \\"onUpdate:value\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"value\\", \\"onUpdate:value\\"])) }, null, 40 /* PROPS, HYDRATE_EVENTS */, [\\"value\\", \\"onUpdate:value\\"]))
} }
}" }"
`; `;
exports[`compiler: transform v-model with dynamic argument (with prefixIdentifiers) 1`] = ` exports[`compiler: transform v-model with dynamic argument (with prefixIdentifiers) 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"input\\", {
return (openBlock(), createBlock(\\"input\\", {
[_ctx.value]: _ctx.model, [_ctx.value]: _ctx.model,
[\\"onUpdate:\\" + _ctx.value]: $event => (_ctx.model = $event) [\\"onUpdate:\\" + _ctx.value]: $event => (_ctx.model = $event)
}, null, 16 /* FULL_PROPS */)) }, null, 16 /* FULL_PROPS */))
@ -84,9 +81,9 @@ export default function render() {
exports[`compiler: transform v-model with dynamic argument 1`] = ` exports[`compiler: transform v-model with dynamic argument 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(\\"input\\", { return (_openBlock(), _createBlock(\\"input\\", {
[value]: model, [value]: model,

View File

@ -3,10 +3,9 @@
exports[`compiler: v-once transform as root node 1`] = ` exports[`compiler: v-once transform as root node 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode } = _Vue const { setBlockTracking: _setBlockTracking, createVNode: _createVNode } = _Vue
const _cache = $cache
return _cache[1] || ( return _cache[1] || (
_setBlockTracking(-1), _setBlockTracking(-1),
@ -21,10 +20,9 @@ return function render() {
exports[`compiler: v-once transform on component 1`] = ` exports[`compiler: v-once transform on component 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _cache = $cache
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
@ -43,10 +41,9 @@ return function render() {
exports[`compiler: v-once transform on nested plain element 1`] = ` exports[`compiler: v-once transform on nested plain element 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _cache = $cache
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || ( _cache[1] || (
@ -63,10 +60,9 @@ return function render() {
exports[`compiler: v-once transform on slot outlet 1`] = ` exports[`compiler: v-once transform on slot outlet 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _cache = $cache
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || ( _cache[1] || (
@ -83,10 +79,9 @@ return function render() {
exports[`compiler: v-once transform with hoistStatic: true 1`] = ` exports[`compiler: v-once transform with hoistStatic: true 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _cache = $cache
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_cache[1] || ( _cache[1] || (

View File

@ -1,111 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform component slots dynamically named slots 1`] = ` exports[`compiler: transform component slots dynamically named slots 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this const _component_Comp = _resolveComponent(\\"Comp\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, { return (_openBlock(), _createBlock(_component_Comp, null, {
[_ctx.one]: ({ foo }) => [toString(foo), toString(_ctx.bar)], [_ctx.one]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
[_ctx.two]: ({ bar }) => [toString(_ctx.foo), toString(bar)], [_ctx.two]: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_compiled: true _: 1
}, 512 /* DYNAMIC_SLOTS */)) }, 1024 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots explicit default slot 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
_compiled: true
}))
}" }"
`; `;
exports[`compiler: transform component slots implicit default slot 1`] = ` exports[`compiler: transform component slots implicit default slot 1`] = `
"const { createVNode, resolveComponent, createBlock, openBlock } = Vue "const { createVNode: _createVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this const _component_Comp = _resolveComponent(\\"Comp\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, { return (_openBlock(), _createBlock(_component_Comp, null, {
default: () => [ default: _withCtx(() => [
createVNode(\\"div\\") _createVNode(\\"div\\")
], ]),
_compiled: true _: 1
})) }))
}" }"
`; `;
exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = ` exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, renderList, createSlots, createVNode, createBlock, openBlock } = Vue "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, renderList: _renderList, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this const _component_Comp = _resolveComponent(\\"Comp\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
renderList(_ctx.list, (name) => { _renderList(_ctx.list, (name) => {
return { return {
name: name, name: name,
fn: () => [toString(name)] fn: _withCtx(() => [_toDisplayString(name)])
} }
}) })
]), 512 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
}" }"
`; `;
exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = ` exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, createSlots, createVNode, createBlock, openBlock } = Vue "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this const _component_Comp = _resolveComponent(\\"Comp\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
(_ctx.ok) (_ctx.ok)
? { ? {
name: \\"one\\", name: \\"one\\",
fn: (props) => [toString(props)] fn: _withCtx((props) => [_toDisplayString(props)])
} }
: undefined : undefined
]), 512 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
}" }"
`; `;
exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = ` exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _compiled: true }, [ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
ok ok
? { ? {
name: \\"one\\", name: \\"one\\",
fn: () => [\\"foo\\"] fn: _withCtx(() => [\\"foo\\"])
} }
: orNot : orNot
? { ? {
name: \\"two\\", name: \\"two\\",
fn: (props) => [\\"bar\\"] fn: _withCtx((props) => [\\"bar\\"])
} }
: { : {
name: \\"one\\", name: \\"one\\",
fn: () => [\\"baz\\"] fn: _withCtx(() => [\\"baz\\"])
} }
]), 512 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
} }
}" }"
`; `;
@ -113,59 +95,117 @@ return function render() {
exports[`compiler: transform component slots named slot with v-if 1`] = ` exports[`compiler: transform component slots named slot with v-if 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _compiled: true }, [ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 1 }, [
ok ok
? { ? {
name: \\"one\\", name: \\"one\\",
fn: () => [\\"hello\\"] fn: _withCtx(() => [\\"hello\\"])
} }
: undefined : undefined
]), 512 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
} }
}" }"
`; `;
exports[`compiler: transform component slots named slots 1`] = ` exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this with (_ctx) {
const _component_Comp = resolveComponent(\\"Comp\\") const { createVNode: _createVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (openBlock(), createBlock(_component_Comp, null, { const _component_Comp = _resolveComponent(\\"Comp\\")
one: ({ foo }) => [toString(foo), toString(_ctx.bar)],
two: ({ bar }) => [toString(_ctx.foo), toString(bar)], return (_openBlock(), _createBlock(_component_Comp, null, {
_compiled: true one: _withCtx(() => [\\"foo\\"]),
})) default: _withCtx(() => [
\\"bar\\",
_createVNode(\\"span\\")
]),
_: 1
}))
}
}" }"
`; `;
exports[`compiler: transform component slots nested slots scoping 1`] = ` exports[`compiler: transform component slots nested slots scoping 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render() { return function render(_ctx, _cache) {
const _ctx = this const _component_Inner = _resolveComponent(\\"Inner\\")
const _component_Inner = resolveComponent(\\"Inner\\") const _component_Comp = _resolveComponent(\\"Comp\\")
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, { return (_openBlock(), _createBlock(_component_Comp, null, {
default: ({ foo }) => [ default: _withCtx(({ foo }) => [
createVNode(_component_Inner, null, { _createVNode(_component_Inner, null, {
default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)], default: _withCtx(({ bar }) => [_toDisplayString(foo), _toDisplayString(bar), _toDisplayString(_ctx.baz)]),
_compiled: true _: 1
}, 512 /* DYNAMIC_SLOTS */), }, 1024 /* DYNAMIC_SLOTS */),
\\" \\", \\" \\",
toString(foo), _toDisplayString(foo),
toString(_ctx.bar), _toDisplayString(_ctx.bar),
toString(_ctx.baz) _toDisplayString(_ctx.baz)
], ]),
_compiled: true _: 1
}))
}"
`;
exports[`compiler: transform component slots on component dynamically named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
[_ctx.named]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on component named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
named: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on-component default slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
default: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots template named slots 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
two: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_: 1
})) }))
}" }"
`; `;

View File

@ -1,18 +1,15 @@
import { import {
parse, baseParse as parse,
transform, transform,
NodeTypes, NodeTypes,
generate, generate,
CompilerOptions CompilerOptions,
VNodeCall,
IfNode,
ElementNode,
ForNode
} from '../../src' } from '../../src'
import { import { FRAGMENT, RENDER_LIST, CREATE_TEXT } from '../../src/runtimeHelpers'
OPEN_BLOCK,
CREATE_BLOCK,
CREATE_VNODE,
WITH_DIRECTIVES,
FRAGMENT,
RENDER_LIST
} from '../../src/runtimeHelpers'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
@ -20,6 +17,7 @@ import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { createObjectMatcher, genFlagText } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
function transformWithHoist(template: string, options: CompilerOptions = {}) { function transformWithHoist(template: string, options: CompilerOptions = {}) {
@ -30,7 +28,8 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
transformIf, transformIf,
transformFor, transformFor,
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers ? [transformExpression] : []),
transformElement transformElement,
transformText
], ],
directiveTransforms: { directiveTransforms: {
on: transformOn, on: transformOn,
@ -39,56 +38,43 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
...options ...options
}) })
expect(ast.codegenNode).toMatchObject({ expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ isBlock: true
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
}
]
}) })
return { return ast
root: ast,
args: (ast.codegenNode as any).expressions[1].arguments
}
} }
describe('compiler: hoistStatic transform', () => { describe('compiler: hoistStatic transform', () => {
test('should NOT hoist root node', () => { test('should NOT hoist root node', () => {
// if the whole tree is static, the root still needs to be a block // if the whole tree is static, the root still needs to be a block
// so that it's patched in optimized mode to skip children // so that it's patched in optimized mode to skip children
const { root, args } = transformWithHoist(`<div/>`) const root = transformWithHoist(`<div/>`)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(args).toEqual([`"div"`]) expect(root.codegenNode).toMatchObject({
tag: `"div"`
})
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist simple element', () => { test('hoist simple element', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><span class="inline">hello</span></div>` `<div><span class="inline">hello</span></div>`
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"span"`,
arguments: [ props: createObjectMatcher({ class: 'inline' }),
`"span"`, children: {
createObjectMatcher({ class: 'inline' }), type: NodeTypes.TEXT,
{ content: `hello`
type: NodeTypes.TEXT, }
content: `hello`
}
]
} }
]) ])
expect(args).toMatchObject([ expect(root.codegenNode).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -97,29 +83,24 @@ describe('compiler: hoistStatic transform', () => {
} }
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist nested static tree', () => { test('hoist nested static tree', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(`<div><p><span/><span/></p></div>`)
`<div><p><span/><span/></p></div>`
)
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"p"`,
arguments: [ props: undefined,
`"p"`, children: [
`null`, { type: NodeTypes.ELEMENT, tag: `span` },
[ { type: NodeTypes.ELEMENT, tag: `span` }
{ type: NodeTypes.ELEMENT, tag: `span` },
{ type: NodeTypes.ELEMENT, tag: `span` }
]
] ]
} }
]) ])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -132,21 +113,16 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist nested static tree with comments', () => { test('hoist nested static tree with comments', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(`<div><div><!--comment--></div></div>`)
`<div><div><!--comment--></div></div>`
)
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"div"`,
arguments: [ props: undefined,
`"div"`, children: [{ type: NodeTypes.COMMENT, content: `comment` }]
`null`,
[{ type: NodeTypes.COMMENT, content: `comment` }]
]
} }
]) ])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -159,20 +135,18 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist siblings with common non-hoistable parent', () => { test('hoist siblings with common non-hoistable parent', () => {
const { root, args } = transformWithHoist(`<div><span/><div/></div>`) const root = transformWithHoist(`<div><span/><div/></div>`)
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"span"`
arguments: [`"span"`]
}, },
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"div"`
arguments: [`"div"`]
} }
]) ])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -192,14 +166,14 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist components', () => { test('should NOT hoist components', () => {
const { root, args } = transformWithHoist(`<div><Comp/></div>`) const root = transformWithHoist(`<div><Comp/></div>`)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`_component_Comp`] tag: `_component_Comp`
} }
} }
]) ])
@ -207,22 +181,20 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist element with dynamic props', () => { test('should NOT hoist element with dynamic props', () => {
const { root, args } = transformWithHoist(`<div><div :id="foo"/></div>`) const root = transformWithHoist(`<div><div :id="foo"/></div>`)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ id: `[foo]`
id: `[foo]` }),
}), children: undefined,
`null`, patchFlag: genFlagText(PatchFlags.PROPS),
genFlagText(PatchFlags.PROPS), dynamicProps: `["id"]`
`["id"]`
]
} }
} }
]) ])
@ -230,19 +202,19 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist element with static key', () => { test('hoist element with static key', () => {
const { root, args } = transformWithHoist(`<div><div key="foo"/></div>`) const root = transformWithHoist(`<div><div key="foo"/></div>`)
expect(root.hoists.length).toBe(1) expect(root.hoists.length).toBe(1)
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"div"`,
arguments: [`"div"`, createObjectMatcher({ key: 'foo' })] props: createObjectMatcher({ key: 'foo' })
} }
]) ])
expect(args).toMatchObject([ expect(root.codegenNode).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -251,24 +223,22 @@ describe('compiler: hoistStatic transform', () => {
} }
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist element with dynamic key', () => { test('should NOT hoist element with dynamic key', () => {
const { root, args } = transformWithHoist(`<div><div :key="foo"/></div>`) const root = transformWithHoist(`<div><div :key="foo"/></div>`)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ key: `[foo]`
key: `[foo]` })
})
]
} }
} }
]) ])
@ -276,21 +246,19 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist element with dynamic ref', () => { test('should NOT hoist element with dynamic ref', () => {
const { root, args } = transformWithHoist(`<div><div :ref="foo"/></div>`) const root = transformWithHoist(`<div><div :ref="foo"/></div>`)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ ref: `[foo]`
ref: `[foo]` }),
}), children: undefined,
`null`, patchFlag: genFlagText(PatchFlags.NEED_PATCH)
genFlagText(PatchFlags.NEED_PATCH)
]
} }
} }
]) ])
@ -298,32 +266,23 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist static props for elements with directives', () => { test('hoist static props for elements with directives', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(`<div><div id="foo" v-foo/></div>`)
`<div><div id="foo" v-foo/></div>`
)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: WITH_DIRECTIVES, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
{ props: {
callee: CREATE_VNODE, type: NodeTypes.SIMPLE_EXPRESSION,
arguments: [ content: `_hoisted_1`
`"div"`, },
{ children: undefined,
type: NodeTypes.SIMPLE_EXPRESSION, patchFlag: genFlagText(PatchFlags.NEED_PATCH),
content: `_hoisted_1` directives: {
}, type: NodeTypes.JS_ARRAY_EXPRESSION
`null`, }
genFlagText(PatchFlags.NEED_PATCH)
]
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION
}
]
} }
} }
]) ])
@ -331,21 +290,19 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist static props for elements with dynamic text children', () => { test('hoist static props for elements with dynamic text children', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><div id="foo">{{ hello }}</div></div>` `<div><div id="foo">{{ hello }}</div></div>`
) )
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
`"div"`, props: { content: `_hoisted_1` },
{ content: `_hoisted_1` }, children: { type: NodeTypes.INTERPOLATION },
{ type: NodeTypes.INTERPOLATION }, patchFlag: genFlagText(PatchFlags.TEXT)
genFlagText(PatchFlags.TEXT)
]
} }
} }
]) ])
@ -353,20 +310,16 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('hoist static props for elements with unhoistable children', () => { test('hoist static props for elements with unhoistable children', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(`<div><div id="foo"><Comp/></div></div>`)
`<div><div id="foo"><Comp/></div></div>`
)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect(args[2]).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"div"`,
`"div"`, props: { content: `_hoisted_1` },
{ content: `_hoisted_1` }, children: [{ type: NodeTypes.ELEMENT, tag: `Comp` }]
[{ type: NodeTypes.ELEMENT, tag: `Comp` }]
]
} }
} }
]) ])
@ -374,7 +327,7 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should hoist v-if props/children if static', () => { test('should hoist v-if props/children if static', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><div v-if="ok" id="foo"><span/></div></div>` `<div><div v-if="ok" id="foo"><span/></div></div>`
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
@ -383,37 +336,31 @@ describe('compiler: hoistStatic transform', () => {
id: 'foo' id: 'foo'
}), }),
{ {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`"span"`] tag: `"span"`
} }
]) ])
expect(args[2][0].codegenNode).toMatchObject({ expect(
type: NodeTypes.JS_SEQUENCE_EXPRESSION, ((root.children[0] as ElementNode).children[0] as IfNode).codegenNode
expressions: [ ).toMatchObject({
{ callee: OPEN_BLOCK }, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
{ consequent: {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, // blocks should NOT be hoisted
consequent: { type: NodeTypes.VNODE_CALL,
// blocks should NOT be hoisted tag: `"div"`,
callee: CREATE_BLOCK, props: { content: `_hoisted_1` },
arguments: [ children: [
`"div"`, {
{ content: `_hoisted_1` }, codegenNode: { content: `_hoisted_2` }
[
{
codegenNode: { content: `_hoisted_2` }
}
]
]
} }
} ]
] }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should hoist v-for children if static', () => { test('should hoist v-for children if static', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><div v-for="i in list" id="foo"><span/></div></div>` `<div><div v-for="i in list" id="foo"><span/></div></div>`
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
@ -421,55 +368,58 @@ describe('compiler: hoistStatic transform', () => {
id: 'foo' id: 'foo'
}), }),
{ {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`"span"`] tag: `"span"`
} }
]) ])
const forBlockCodegen = args[2][0].codegenNode const forBlockCodegen = ((root.children[0] as ElementNode)
.children[0] as ForNode).codegenNode
expect(forBlockCodegen).toMatchObject({ expect(forBlockCodegen).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ tag: FRAGMENT,
{ callee: OPEN_BLOCK }, props: undefined,
{ children: {
callee: CREATE_BLOCK, type: NodeTypes.JS_CALL_EXPRESSION,
arguments: [ callee: RENDER_LIST
FRAGMENT, },
`null`, patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT)
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST
},
genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
]
}) })
const innerBlockCodegen = const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
forBlockCodegen.expressions[1].arguments[2].arguments[1].returns expect(innerBlockCodegen.returns).toMatchObject({
expect(innerBlockCodegen).toMatchObject({ type: NodeTypes.VNODE_CALL,
type: NodeTypes.JS_SEQUENCE_EXPRESSION, tag: `"div"`,
expressions: [ props: { content: `_hoisted_1` },
{ callee: OPEN_BLOCK }, children: [
{ {
callee: CREATE_BLOCK, codegenNode: { content: `_hoisted_2` }
arguments: [
`"div"`,
{ content: `_hoisted_1` },
[
{
codegenNode: { content: `_hoisted_2` }
}
]
]
} }
] ]
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist static text node between elements', () => {
const root = transformWithHoist(`<div>static<div>static</div></div>`)
expect(root.hoists).toMatchObject([
{
callee: CREATE_TEXT,
arguments: [
{
type: NodeTypes.TEXT,
content: `static`
}
]
},
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`
}
])
})
describe('prefixIdentifiers', () => { describe('prefixIdentifiers', () => {
test('hoist nested static tree with static interpolation', () => { test('hoist nested static tree with static interpolation', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><span>foo {{ 1 }} {{ true }}</span></div>`, `<div><span>foo {{ 1 }} {{ true }}</span></div>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -477,44 +427,18 @@ describe('compiler: hoistStatic transform', () => {
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"span"`,
arguments: [ props: undefined,
`"span"`, children: {
`null`, type: NodeTypes.COMPOUND_EXPRESSION
[ }
{
type: NodeTypes.TEXT,
content: `foo `
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
isConstant: true
}
},
{
type: NodeTypes.TEXT,
content: ` `
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `true`,
isStatic: false,
isConstant: true
}
}
]
]
} }
]) ])
expect(args).toMatchObject([ expect(root.codegenNode).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -523,12 +447,12 @@ describe('compiler: hoistStatic transform', () => {
} }
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist nested static tree with static prop value', () => { test('hoist nested static tree with static prop value', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><span :foo="0">{{ 1 }}</span></div>`, `<div><span :foo="0">{{ 1 }}</span></div>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -537,26 +461,23 @@ describe('compiler: hoistStatic transform', () => {
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE, tag: `"span"`,
arguments: [ props: createObjectMatcher({ foo: `[0]` }),
`"span"`, children: {
createObjectMatcher({ foo: `[0]` }), type: NodeTypes.INTERPOLATION,
{ content: {
type: NodeTypes.INTERPOLATION, content: `1`,
content: { isStatic: false,
content: `1`, isConstant: true
isStatic: false,
isConstant: true
}
} }
] }
} }
]) ])
expect(args).toMatchObject([ expect(root.codegenNode).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
@ -565,12 +486,12 @@ describe('compiler: hoistStatic transform', () => {
} }
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist class with static object value', () => { test('hoist class with static object value', () => {
const { root, args } = transformWithHoist( const root = transformWithHoist(
`<div><span :class="{ foo: true }">{{ bar }}</span></div>`, `<div><span :class="{ foo: true }">{{ bar }}</span></div>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -596,39 +517,37 @@ describe('compiler: hoistStatic transform', () => {
] ]
} }
]) ])
expect(args).toMatchObject([ expect(root.codegenNode).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `"span"`,
`"span"`, props: {
{ type: NodeTypes.SIMPLE_EXPRESSION,
type: NodeTypes.SIMPLE_EXPRESSION, content: `_hoisted_1`
content: `_hoisted_1` },
}, children: {
{ type: NodeTypes.INTERPOLATION,
type: NodeTypes.INTERPOLATION, content: {
content: { content: `_ctx.bar`,
content: `_ctx.bar`, isConstant: false,
isConstant: false, isStatic: false
isStatic: false }
} },
}, patchFlag: `1 /* TEXT */`
`1 /* TEXT */`
]
} }
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist expressions that refer scope variables', () => { test('should NOT hoist expressions that refer scope variables', () => {
const { root } = transformWithHoist( const root = transformWithHoist(
`<div><p v-for="o in list"><span>{{ o }}</span></p></div>`, `<div><p v-for="o in list"><span>{{ o }}</span></p></div>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -640,7 +559,7 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist expressions that refer scope variables (2)', () => { test('should NOT hoist expressions that refer scope variables (2)', () => {
const { root } = transformWithHoist( const root = transformWithHoist(
`<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`, `<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -652,7 +571,7 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist expressions that refer scope variables (v-slot)', () => { test('should NOT hoist expressions that refer scope variables (v-slot)', () => {
const { root } = transformWithHoist( const root = transformWithHoist(
`<Comp v-slot="{ foo }">{{ foo }}</Comp>`, `<Comp v-slot="{ foo }">{{ foo }}</Comp>`,
{ {
prefixIdentifiers: true prefixIdentifiers: true
@ -664,7 +583,7 @@ describe('compiler: hoistStatic transform', () => {
}) })
test('should NOT hoist elements with cached handlers', () => { test('should NOT hoist elements with cached handlers', () => {
const { root } = transformWithHoist( const root = transformWithHoist(
`<div><div><div @click="foo"/></div></div>`, `<div><div><div @click="foo"/></div></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,

View File

@ -0,0 +1,24 @@
import {
baseParse as parse,
transform,
ElementNode,
noopDirectiveTransform,
VNodeCall
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
describe('compiler: noop directive transform', () => {
test('should add no props to DOM', () => {
const ast = parse(`<div v-noop/>`)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
noop: noopDirectiveTransform
}
})
const node = ast.children[0] as ElementNode
// As v-noop adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect((node.codegenNode as VNodeCall).props).toBeUndefined()
})
})

View File

@ -1,24 +1,28 @@
import { CompilerOptions, parse, transform, ErrorCodes } from '../../src' import {
CompilerOptions,
baseParse as parse,
transform,
ErrorCodes
} from '../../src'
import { import {
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
CREATE_VNODE, CREATE_VNODE,
MERGE_PROPS, MERGE_PROPS,
RESOLVE_DIRECTIVE, RESOLVE_DIRECTIVE,
WITH_DIRECTIVES,
TO_HANDLERS, TO_HANDLERS,
helperNameMap, helperNameMap,
PORTAL, TELEPORT,
RESOLVE_DYNAMIC_COMPONENT, RESOLVE_DYNAMIC_COMPONENT,
SUSPENSE, SUSPENSE,
KEEP_ALIVE, KEEP_ALIVE,
BASE_TRANSITION BASE_TRANSITION
} from '../../src/runtimeHelpers' } from '../../src/runtimeHelpers'
import { import {
CallExpression,
NodeTypes, NodeTypes,
createObjectProperty, createObjectProperty,
DirectiveNode, DirectiveNode,
RootNode RootNode,
VNodeCall
} from '../../src/ast' } from '../../src/ast'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformStyle } from '../../../compiler-dom/src/transforms/transformStyle' import { transformStyle } from '../../../compiler-dom/src/transforms/transformStyle'
@ -33,7 +37,7 @@ function parseWithElementTransform(
options: CompilerOptions = {} options: CompilerOptions = {}
): { ): {
root: RootNode root: RootNode
node: CallExpression node: VNodeCall
} { } {
// wrap raw template in an extra div so that it doesn't get turned into a // wrap raw template in an extra div so that it doesn't get turned into a
// block as root node // block as root node
@ -43,8 +47,8 @@ function parseWithElementTransform(
...options ...options
}) })
const codegenNode = (ast as any).children[0].children[0] const codegenNode = (ast as any).children[0].children[0]
.codegenNode as CallExpression .codegenNode as VNodeCall
expect(codegenNode.type).toBe(NodeTypes.JS_CALL_EXPRESSION) expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
return { return {
root: ast, root: ast,
node: codegenNode node: codegenNode
@ -68,63 +72,63 @@ describe('compiler: element transform', () => {
test('static props', () => { test('static props', () => {
const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`) const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`)
expect(node.callee).toBe(CREATE_VNODE) expect(node).toMatchObject({
expect(node.arguments).toMatchObject([ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({
id: 'foo', id: 'foo',
class: 'bar' class: 'bar'
}) }),
]) children: undefined
})
}) })
test('props + children', () => { test('props + children', () => {
const { node } = parseWithElementTransform(`<div id="foo"><span/></div>`) const { node } = parseWithElementTransform(`<div id="foo"><span/></div>`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
`"div"`, tag: `"div"`,
createObjectMatcher({ props: createObjectMatcher({
id: 'foo' id: 'foo'
}), }),
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
tag: 'span', tag: 'span',
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`"span"`] tag: `"span"`
} }
} }
] ]
]) })
}) })
test('0 placeholder for children with no props', () => { test('0 placeholder for children with no props', () => {
const { node } = parseWithElementTransform(`<div><span/></div>`) const { node } = parseWithElementTransform(`<div><span/></div>`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
`"div"`, tag: `"div"`,
`null`, props: undefined,
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
tag: 'span', tag: 'span',
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`"span"`] tag: `"span"`
} }
} }
] ]
]) })
}) })
test('v-bind="obj"', () => { test('v-bind="obj"', () => {
const { root, node } = parseWithElementTransform(`<div v-bind="obj" />`) const { root, node } = parseWithElementTransform(`<div v-bind="obj" />`)
// single v-bind doesn't need mergeProps // single v-bind doesn't need mergeProps
expect(root.helpers).not.toContain(MERGE_PROPS) expect(root.helpers).not.toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
// should directly use `obj` in props position // should directly use `obj` in props position
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `obj` content: `obj`
}) })
@ -135,8 +139,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-bind="obj" />` `<div id="foo" v-bind="obj" />`
) )
expect(root.helpers).toContain(MERGE_PROPS) expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -156,8 +160,8 @@ describe('compiler: element transform', () => {
`<div v-bind="obj" id="foo" />` `<div v-bind="obj" id="foo" />`
) )
expect(root.helpers).toContain(MERGE_PROPS) expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -177,8 +181,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-bind="obj" class="bar" />` `<div id="foo" v-bind="obj" class="bar" />`
) )
expect(root.helpers).toContain(MERGE_PROPS) expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -201,8 +205,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-on="obj" class="bar" />` `<div id="foo" v-on="obj" class="bar" />`
) )
expect(root.helpers).toContain(MERGE_PROPS) expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -231,8 +235,8 @@ describe('compiler: element transform', () => {
`<div id="foo" v-on="handlers" v-bind="obj" />` `<div id="foo" v-on="handlers" v-bind="obj" />`
) )
expect(root.helpers).toContain(MERGE_PROPS) expect(root.helpers).toContain(MERGE_PROPS)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -259,43 +263,43 @@ describe('compiler: element transform', () => {
test('should handle plain <template> as normal element', () => { test('should handle plain <template> as normal element', () => {
const { node } = parseWithElementTransform(`<template id="foo" />`) const { node } = parseWithElementTransform(`<template id="foo" />`)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
`"template"`, tag: `"template"`,
createObjectMatcher({ props: createObjectMatcher({
id: 'foo' id: 'foo'
}) })
]) })
}) })
test('should handle <Portal> with normal children', () => { test('should handle <Teleport> with normal children', () => {
function assert(tag: string) { function assert(tag: string) {
const { root, node } = parseWithElementTransform( const { root, node } = parseWithElementTransform(
`<${tag} target="#foo"><span /></${tag}>` `<${tag} target="#foo"><span /></${tag}>`
) )
expect(root.components.length).toBe(0) expect(root.components.length).toBe(0)
expect(root.helpers).toContain(PORTAL) expect(root.helpers).toContain(TELEPORT)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
PORTAL, tag: TELEPORT,
createObjectMatcher({ props: createObjectMatcher({
target: '#foo' target: '#foo'
}), }),
[ children: [
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
tag: 'span', tag: 'span',
codegenNode: { codegenNode: {
callee: CREATE_VNODE, type: NodeTypes.VNODE_CALL,
arguments: [`"span"`] tag: `"span"`
} }
} }
] ]
]) })
} }
assert(`portal`) assert(`teleport`)
assert(`Portal`) assert(`Teleport`)
}) })
test('should handle <Suspense>', () => { test('should handle <Suspense>', () => {
@ -305,11 +309,11 @@ describe('compiler: element transform', () => {
) )
expect(root.components.length).toBe(0) expect(root.components.length).toBe(0)
expect(root.helpers).toContain(SUSPENSE) expect(root.helpers).toContain(SUSPENSE)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
SUSPENSE, tag: SUSPENSE,
`null`, props: undefined,
hasFallback children: hasFallback
? createObjectMatcher({ ? createObjectMatcher({
default: { default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
@ -317,15 +321,15 @@ describe('compiler: element transform', () => {
fallback: { fallback: {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
}, },
_compiled: `[true]` _: `[1]`
}) })
: createObjectMatcher({ : createObjectMatcher({
default: { default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
}, },
_compiled: `[true]` _: `[1]`
}) })
]) })
} }
assert(`suspense`, `foo`) assert(`suspense`, `foo`)
@ -339,18 +343,23 @@ describe('compiler: element transform', () => {
test('should handle <KeepAlive>', () => { test('should handle <KeepAlive>', () => {
function assert(tag: string) { function assert(tag: string) {
const { root, node } = parseWithElementTransform( const root = parse(`<div><${tag}><span /></${tag}></div>`)
`<${tag}><span /></${tag}>` transform(root, {
) nodeTransforms: [transformElement, transformText]
})
expect(root.components.length).toBe(0) expect(root.components.length).toBe(0)
expect(root.helpers).toContain(KEEP_ALIVE) expect(root.helpers).toContain(KEEP_ALIVE)
expect(node.callee).toBe(CREATE_VNODE) const node = (root.children[0] as any).children[0].codegenNode
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
KEEP_ALIVE, type: NodeTypes.VNODE_CALL,
`null`, tag: KEEP_ALIVE,
isBlock: true, // should be forced into a block
props: undefined,
// keep-alive should not compile content to slots // keep-alive should not compile content to slots
[{ type: NodeTypes.ELEMENT, tag: 'span' }] children: [{ type: NodeTypes.ELEMENT, tag: 'span' }],
]) // should get a dynamic slots flag to force updates
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS)
})
} }
assert(`keep-alive`) assert(`keep-alive`)
@ -364,17 +373,17 @@ describe('compiler: element transform', () => {
) )
expect(root.components.length).toBe(0) expect(root.components.length).toBe(0)
expect(root.helpers).toContain(BASE_TRANSITION) expect(root.helpers).toContain(BASE_TRANSITION)
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
BASE_TRANSITION, tag: BASE_TRANSITION,
`null`, props: undefined,
createObjectMatcher({ children: createObjectMatcher({
default: { default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
}, },
_compiled: `[true]` _: `[1]`
}) })
]) })
} }
assert(`base-transition`) assert(`base-transition`)
@ -398,14 +407,13 @@ describe('compiler: element transform', () => {
foo(dir) { foo(dir) {
_dir = dir _dir = dir
return { return {
props: [createObjectProperty(dir.arg!, dir.exp!)], props: [createObjectProperty(dir.arg!, dir.exp!)]
needRuntime: false
} }
} }
} }
}) })
expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{ {
@ -417,8 +425,8 @@ describe('compiler: element transform', () => {
}) })
// should factor in props returned by custom directive transforms // should factor in props returned by custom directive transforms
// in patchFlag analysis // in patchFlag analysis
expect(node.arguments[3]).toMatch(PatchFlags.PROPS + '') expect(node.patchFlag).toMatch(PatchFlags.PROPS + '')
expect(node.arguments[4]).toMatch(`"bar"`) expect(node.dynamicProps).toMatch(`"bar"`)
}) })
test('directiveTransform with needRuntime: true', () => { test('directiveTransform with needRuntime: true', () => {
@ -437,20 +445,12 @@ describe('compiler: element transform', () => {
) )
expect(root.helpers).toContain(RESOLVE_DIRECTIVE) expect(root.helpers).toContain(RESOLVE_DIRECTIVE)
expect(root.directives).toContain(`foo`) expect(root.directives).toContain(`foo`)
expect(node).toMatchObject({
expect(node.callee).toBe(WITH_DIRECTIVES) tag: `"div"`,
expect(node.arguments).toMatchObject([ props: undefined,
{ children: undefined,
type: NodeTypes.JS_CALL_EXPRESSION, patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag
callee: CREATE_VNODE, directives: {
arguments: [
`"div"`,
`null`,
`null`,
genFlagText(PatchFlags.NEED_PATCH) // should generate appropriate flag
]
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [ elements: [
{ {
@ -473,7 +473,7 @@ describe('compiler: element transform', () => {
} }
] ]
} }
]) })
}) })
test('directiveTransform with needRuntime: Symbol', () => { test('directiveTransform with needRuntime: Symbol', () => {
@ -494,7 +494,7 @@ describe('compiler: element transform', () => {
expect(root.helpers).toContain(CREATE_VNODE) expect(root.helpers).toContain(CREATE_VNODE)
expect(root.helpers).not.toContain(RESOLVE_DIRECTIVE) expect(root.helpers).not.toContain(RESOLVE_DIRECTIVE)
expect(root.directives.length).toBe(0) expect(root.directives.length).toBe(0)
expect((node as any).arguments[1].elements[0].elements[0]).toBe( expect(node.directives!.elements[0].elements[0]).toBe(
`_${helperNameMap[CREATE_VNODE]}` `_${helperNameMap[CREATE_VNODE]}`
) )
}) })
@ -508,12 +508,8 @@ describe('compiler: element transform', () => {
expect(root.directives).toContain(`bar`) expect(root.directives).toContain(`bar`)
expect(root.directives).toContain(`baz`) expect(root.directives).toContain(`baz`)
expect(node.callee).toBe(WITH_DIRECTIVES) expect(node).toMatchObject({
expect(node.arguments).toMatchObject([ directives: {
{
type: NodeTypes.JS_CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [ elements: [
{ {
@ -583,7 +579,7 @@ describe('compiler: element transform', () => {
} }
] ]
} }
]) })
}) })
test(`props merging: event handlers`, () => { test(`props merging: event handlers`, () => {
@ -595,7 +591,7 @@ describe('compiler: element transform', () => {
} }
} }
) )
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{ {
@ -627,7 +623,7 @@ describe('compiler: element transform', () => {
test(`props merging: style`, () => { test(`props merging: style`, () => {
const { node } = parseWithElementTransform( const { node } = parseWithElementTransform(
`<div style="color: red" :style="{ color: 'red' }" />`, `<div style="color: green" :style="{ color: 'red' }" />`,
{ {
nodeTransforms: [transformStyle, transformElement], nodeTransforms: [transformStyle, transformElement],
directiveTransforms: { directiveTransforms: {
@ -635,7 +631,7 @@ describe('compiler: element transform', () => {
} }
} }
) )
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{ {
@ -650,7 +646,7 @@ describe('compiler: element transform', () => {
elements: [ elements: [
{ {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"green"}`,
isStatic: false isStatic: false
}, },
{ {
@ -674,7 +670,7 @@ describe('compiler: element transform', () => {
} }
} }
) )
expect(node.arguments[1]).toMatchObject({ expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{ {
@ -707,113 +703,175 @@ describe('compiler: element transform', () => {
describe('patchFlag analysis', () => { describe('patchFlag analysis', () => {
test('TEXT', () => { test('TEXT', () => {
const { node } = parseWithBind(`<div>foo</div>`) const { node } = parseWithBind(`<div>foo</div>`)
expect(node.arguments.length).toBe(3) expect(node.patchFlag).toBeUndefined()
const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`) const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`)
expect(node2.arguments.length).toBe(4) expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
expect(node2.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
// multiple nodes, merged with optimize text // multiple nodes, merged with optimize text
const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`) const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`)
expect(node3.arguments.length).toBe(4) expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
expect(node3.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
}) })
test('CLASS', () => { test('CLASS', () => {
const { node } = parseWithBind(`<div :class="foo" />`) const { node } = parseWithBind(`<div :class="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.CLASS))
}) })
test('STYLE', () => { test('STYLE', () => {
const { node } = parseWithBind(`<div :style="foo" />`) const { node } = parseWithBind(`<div :style="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.STYLE))
}) })
test('PROPS', () => { test('PROPS', () => {
const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`) const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
expect(node.arguments.length).toBe(5) expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.PROPS)) expect(node.dynamicProps).toBe(`["foo", "baz"]`)
expect(node.arguments[4]).toBe(`["foo", "baz"]`)
}) })
test('CLASS + STYLE + PROPS', () => { test('CLASS + STYLE + PROPS', () => {
const { node } = parseWithBind( const { node } = parseWithBind(
`<div id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>` `<div id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>`
) )
expect(node.arguments.length).toBe(5) expect(node.patchFlag).toBe(
expect(node.arguments[3]).toBe(
genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS]) genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS])
) )
expect(node.arguments[4]).toBe(`["foo", "baz"]`) expect(node.dynamicProps).toBe(`["foo", "baz"]`)
}) })
test('FULL_PROPS (v-bind)', () => { test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`) const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
}) })
test('FULL_PROPS (dynamic key)', () => { test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(`<div :[foo]="bar" />`) const { node } = parseWithBind(`<div :[foo]="bar" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
}) })
test('FULL_PROPS (w/ others)', () => { test('FULL_PROPS (w/ others)', () => {
const { node } = parseWithBind( const { node } = parseWithBind(
`<div id="foo" v-bind="bar" :class="cls" />` `<div id="foo" v-bind="bar" :class="cls" />`
) )
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
}) })
test('NEED_PATCH (static ref)', () => { test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(`<div ref="foo" />`) const { node } = parseWithBind(`<div ref="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
}) })
test('NEED_PATCH (dynamic ref)', () => { test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(`<div :ref="foo" />`) const { node } = parseWithBind(`<div :ref="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
}) })
test('NEED_PATCH (custom directives)', () => { test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(`<div v-foo />`) const { node } = parseWithBind(`<div v-foo />`)
const vnodeCall = node.arguments[0] as CallExpression expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(vnodeCall.arguments.length).toBe(4) })
expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
test('HYDRATE_EVENTS', () => {
// ignore click events (has dedicated fast path)
const { node } = parseWithElementTransform(`<div @click="foo" />`, {
directiveTransforms: {
on: transformOn
}
})
// should only have props flag
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
const { node: node2 } = parseWithElementTransform(
`<div @keyup="foo" />`,
{
directiveTransforms: {
on: transformOn
}
}
)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS])
)
}) })
}) })
describe('dynamic component', () => { describe('dynamic component', () => {
test('static binding', () => { test('static binding', () => {
const { node, root } = parseWithBind(`<component is="foo" />`) const { node, root } = parseWithBind(`<component is="foo" />`)
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT) expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({ expect(node).toMatchObject({
callee: CREATE_VNODE, tag: {
arguments: ['_component_foo'] callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: true
}
]
}
}) })
}) })
test('dynamic binding', () => { test('dynamic binding', () => {
const { node, root } = parseWithBind(`<component :is="foo" />`) const { node, root } = parseWithBind(`<component :is="foo" />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node.arguments).toMatchObject([ expect(node).toMatchObject({
{ tag: {
callee: RESOLVE_DYNAMIC_COMPONENT, callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [ arguments: [
{ {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo' content: 'foo',
}, isStatic: false
'$' }
] ]
} }
]) })
})
test('v-is', () => {
const { node, root } = parseWithBind(`<div v-is="'foo'" />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({
tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `'foo'`,
isStatic: false
}
]
},
// should skip v-is runtime check
directives: undefined
})
})
})
test('<svg> should be forced into blocks', () => {
const ast = parse(`<div><svg/></div>`)
transform(ast, {
nodeTransforms: [transformElement]
})
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"svg"`,
isBlock: true
})
})
// #938
test('element with dynamic keys should be forced into blocks', () => {
const ast = parse(`<div><div :key="foo" /></div>`)
transform(ast, {
nodeTransforms: [transformElement]
})
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
isBlock: true
}) })
}) })
}) })

View File

@ -1,5 +1,5 @@
import { import {
parse, baseParse as parse,
transform, transform,
ElementNode, ElementNode,
DirectiveNode, DirectiveNode,
@ -317,6 +317,16 @@ describe('compiler: expression transform', () => {
}) })
}) })
test('should not duplicate object key with same name as value', () => {
const node = parseWithExpressionTransform(
`{{ { foo: foo } }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ foo: `, { content: `_ctx.foo` }, ` }`]
})
})
test('should prefix a computed object property key', () => { test('should prefix a computed object property key', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(
`{{ { [foo]: bar } }}` `{{ { [foo]: bar } }}`
@ -380,7 +390,65 @@ describe('compiler: expression transform', () => {
const onError = jest.fn() const onError = jest.fn()
parseWithExpressionTransform(`{{ a( }}`, { onError }) parseWithExpressionTransform(`{{ a( }}`, { onError })
expect(onError.mock.calls[0][0].message).toMatch( expect(onError.mock.calls[0][0].message).toMatch(
`Invalid JavaScript expression. (1:4)` `Error parsing JavaScript expression: Unexpected token`
) )
}) })
describe('ES Proposals support', () => {
test('bigInt', () => {
const node = parseWithExpressionTransform(
`{{ 13000n }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `13000n`,
isStatic: false,
isConstant: true
})
})
test('nullish colescing', () => {
const node = parseWithExpressionTransform(
`{{ a ?? b }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` ?? `, { content: `_ctx.b` }]
})
})
test('optional chaining', () => {
const node = parseWithExpressionTransform(
`{{ a?.b?.c }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.a` },
`?.`,
{ content: `b` },
`?.`,
{ content: `c` }
]
})
})
test('Enabling additional plugins', () => {
// enabling pipeline operator to replace filters:
const node = parseWithExpressionTransform(`{{ a |> uppercase }}`, {
expressionPlugins: [
[
'pipelineOperator',
{
proposal: 'minimal'
}
]
]
}) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` |> `, { content: `_ctx.uppercase` }]
})
})
})
}) })

View File

@ -1,6 +1,6 @@
import { import {
CompilerOptions, CompilerOptions,
parse, baseParse as parse,
transform, transform,
ElementNode, ElementNode,
NodeTypes, NodeTypes,

View File

@ -1,6 +1,6 @@
import { import {
CompilerOptions, CompilerOptions,
parse, baseParse as parse,
transform, transform,
NodeTypes, NodeTypes,
generate, generate,

View File

@ -1,11 +1,11 @@
import { import {
parse, baseParse as parse,
transform, transform,
ElementNode, ElementNode,
ObjectExpression, ObjectExpression,
CompilerOptions, CompilerOptions,
ErrorCodes, ErrorCodes,
CallExpression VNodeCall
} from '../../src' } from '../../src'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
@ -33,8 +33,7 @@ function parseWithVBind(
describe('compiler: transform v-bind', () => { describe('compiler: transform v-bind', () => {
test('basic', () => { test('basic', () => {
const node = parseWithVBind(`<div v-bind:id="id"/>`) const node = parseWithVBind(`<div v-bind:id="id"/>`)
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as VNodeCall).props as ObjectExpression
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `id`, content: `id`,
@ -69,8 +68,7 @@ describe('compiler: transform v-bind', () => {
test('dynamic arg', () => { test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`) const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as VNodeCall).props as ObjectExpression
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `id`, content: `id`,
@ -103,8 +101,7 @@ describe('compiler: transform v-bind', () => {
test('.camel modifier', () => { test('.camel modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.camel="id"/>`) const node = parseWithVBind(`<div v-bind:foo-bar.camel="id"/>`)
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as VNodeCall).props as ObjectExpression
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `fooBar`, content: `fooBar`,
@ -119,8 +116,7 @@ describe('compiler: transform v-bind', () => {
test('.camel modifier w/ dynamic arg', () => { test('.camel modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`) const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as VNodeCall).props as ObjectExpression
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `_${helperNameMap[CAMELIZE]}(foo)`, content: `_${helperNameMap[CAMELIZE]}(foo)`,
@ -137,12 +133,11 @@ describe('compiler: transform v-bind', () => {
const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, { const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as VNodeCall).props as ObjectExpression
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
children: [ children: [
`${helperNameMap[CAMELIZE]}(`, `_${helperNameMap[CAMELIZE]}(`,
{ content: `_ctx.foo` }, { content: `_ctx.foo` },
`(`, `(`,
{ content: `_ctx.bar` }, { content: `_ctx.bar` },

View File

@ -1,4 +1,4 @@
import { parse } from '../../src/parse' import { baseParse as parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'
@ -12,19 +12,11 @@ import {
SimpleExpressionNode, SimpleExpressionNode,
ElementNode, ElementNode,
InterpolationNode, InterpolationNode,
CallExpression, ForCodegenNode
SequenceExpression
} from '../../src/ast' } from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { CompilerOptions, generate } from '../../src' import { CompilerOptions, generate } from '../../src'
import { import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT,
RENDER_LIST,
RENDER_SLOT,
WITH_DIRECTIVES
} from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
@ -48,7 +40,7 @@ function parseWithForTransform(
}) })
return { return {
root: ast, root: ast,
node: ast.children[0] as ForNode node: ast.children[0] as ForNode & { codegenNode: ForCodegenNode }
} }
} }
@ -566,64 +558,40 @@ describe('compiler: v-for', () => {
describe('codegen', () => { describe('codegen', () => {
function assertSharedCodegen( function assertSharedCodegen(
node: SequenceExpression, node: ForCodegenNode,
keyed: boolean = false, keyed: boolean = false,
customReturn: boolean = false customReturn: boolean = false
) { ) {
expect(node).toMatchObject({ expect(node).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ tag: FRAGMENT,
{ isForBlock: true,
type: NodeTypes.JS_CALL_EXPRESSION, patchFlag: keyed
callee: OPEN_BLOCK ? genFlagText(PatchFlags.KEYED_FRAGMENT)
}, : genFlagText(PatchFlags.UNKEYED_FRAGMENT),
{ children: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK, callee: RENDER_LIST,
arguments: [ arguments: [
FRAGMENT, {}, // to be asserted by each test
`null`, {
{ type: NodeTypes.JS_FUNCTION_EXPRESSION,
type: NodeTypes.JS_CALL_EXPRESSION, returns: customReturn
callee: RENDER_LIST, ? {}
arguments: [ : {
{}, // to be asserted by each test type: NodeTypes.VNODE_CALL,
{ isBlock: true
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: customReturn
? {}
: {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK
}
]
}
} }
] }
}, ]
keyed }
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
]
}) })
const renderListArgs = ((node.expressions[1] as CallExpression) const renderListArgs = node.children.arguments
.arguments[2] as CallExpression).arguments
return { return {
source: renderListArgs[0] as SimpleExpressionNode, source: renderListArgs[0] as SimpleExpressionNode,
params: (renderListArgs[1] as any).params, params: (renderListArgs[1] as any).params,
returns: (renderListArgs[1] as any).returns, returns: (renderListArgs[1] as any).returns,
blockArgs: customReturn innerVNodeCall: customReturn ? null : (renderListArgs[1] as any).returns
? null
: (renderListArgs[1] as any).returns.expressions[1].arguments
} }
} }
@ -635,7 +603,9 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode)).toMatchObject({ expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `items` }, source: { content: `items` },
params: [{ content: `item` }], params: [{ content: `item` }],
blockArgs: [`"span"`] innerVNodeCall: {
tag: `"span"`
}
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -698,15 +668,16 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode)).toMatchObject({ expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `items` }, source: { content: `items` },
params: [{ content: `item` }], params: [{ content: `item` }],
blockArgs: [ innerVNodeCall: {
FRAGMENT, tag: FRAGMENT,
`null`, props: undefined,
[ isBlock: true,
children: [
{ type: NodeTypes.TEXT, content: `hello` }, { type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` } { type: NodeTypes.ELEMENT, tag: `span` }
], ],
genFlagText(PatchFlags.STABLE_FRAGMENT) patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT)
] }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -757,12 +728,12 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({ expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `items` }, source: { content: `items` },
params: [{ content: `item` }], params: [{ content: `item` }],
blockArgs: [ innerVNodeCall: {
`"span"`, tag: `"span"`,
createObjectMatcher({ props: createObjectMatcher({
key: `[item]` key: `[item]`
}) })
] }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -777,17 +748,17 @@ describe('compiler: v-for', () => {
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({ expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `items` }, source: { content: `items` },
params: [{ content: `item` }], params: [{ content: `item` }],
blockArgs: [ innerVNodeCall: {
FRAGMENT, tag: FRAGMENT,
createObjectMatcher({ props: createObjectMatcher({
key: `[item]` key: `[item]`
}), }),
[ children: [
{ type: NodeTypes.TEXT, content: `hello` }, { type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` } { type: NodeTypes.ELEMENT, tag: `span` }
], ],
genFlagText(PatchFlags.STABLE_FRAGMENT) patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT)
] }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -798,53 +769,33 @@ describe('compiler: v-for', () => {
node: { codegenNode } node: { codegenNode }
} = parseWithForTransform(`<div v-if="ok" v-for="i in list"/>`) } = parseWithForTransform(`<div v-if="ok" v-for="i in list"/>`)
expect(codegenNode).toMatchObject({ expect(codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
expressions: [ test: { content: `ok` },
{ consequent: {
type: NodeTypes.VNODE_CALL,
props: createObjectMatcher({
key: `[0]`
}),
isBlock: true,
isForBlock: true,
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
children: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK, callee: RENDER_LIST,
arguments: [] arguments: [
}, { content: `list` },
{ {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
test: { content: `ok` }, params: [{ content: `i` }],
consequent: { returns: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_BLOCK, tag: `"div"`,
// should optimize v-if + v-for into a single Fragment block isBlock: true
arguments: [ }
FRAGMENT, }
createObjectMatcher({ key: `[0]` }), ]
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
arguments: [
{ content: `list` },
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: [{ content: `i` }],
returns: {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: OPEN_BLOCK
},
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_BLOCK,
arguments: [`"div"`]
}
]
}
}
]
},
genFlagText(PatchFlags.UNKEYED_FRAGMENT)
]
}
} }
] }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -856,18 +807,8 @@ describe('compiler: v-for', () => {
} = parseWithForTransform('<div v-for="i in list" v-foo/>') } = parseWithForTransform('<div v-for="i in list" v-foo/>')
const { returns } = assertSharedCodegen(codegenNode, false, true) const { returns } = assertSharedCodegen(codegenNode, false, true)
expect(returns).toMatchObject({ expect(returns).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.VNODE_CALL,
expressions: [ directives: { type: NodeTypes.JS_ARRAY_EXPRESSION }
{ callee: OPEN_BLOCK },
// should wrap withDirectives() around createBlock()
{
callee: WITH_DIRECTIVES,
arguments: [
{ callee: CREATE_BLOCK },
{ type: NodeTypes.JS_ARRAY_EXPRESSION }
]
}
]
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })

View File

@ -1,4 +1,4 @@
import { parse } from '../../src/parse' import { baseParse as parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
@ -10,18 +10,16 @@ import {
TextNode, TextNode,
CommentNode, CommentNode,
SimpleExpressionNode, SimpleExpressionNode,
SequenceExpression,
ConditionalExpression, ConditionalExpression,
CallExpression IfConditionalExpression,
VNodeCall,
ElementTypes
} from '../../src/ast' } from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { CompilerOptions, generate } from '../../src' import { CompilerOptions, generate } from '../../src'
import { import {
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT, FRAGMENT,
MERGE_PROPS, MERGE_PROPS,
WITH_DIRECTIVES,
RENDER_SLOT, RENDER_SLOT,
CREATE_COMMENT CREATE_COMMENT
} from '../../src/runtimeHelpers' } from '../../src/runtimeHelpers'
@ -43,7 +41,9 @@ function parseWithIfTransform(
} }
return { return {
root: ast, root: ast,
node: ast.children[returnIndex] as IfNode node: ast.children[returnIndex] as IfNode & {
codegenNode: IfConditionalExpression
}
} }
} }
@ -79,6 +79,22 @@ describe('compiler: v-if', () => {
expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`) expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`)
}) })
test('component v-if', () => {
const { node } = parseWithIfTransform(`<Component v-if="ok"></Component>`)
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(1)
expect((node.branches[0].children[0] as ElementNode).tag).toBe(
`Component`
)
expect((node.branches[0].children[0] as ElementNode).tagType).toBe(
ElementTypes.COMPONENT
)
expect(
((node.branches[0].children[0] as ElementNode)!
.codegenNode as VNodeCall)!.isBlock
).toBe(false)
})
test('v-if + v-else', () => { test('v-if + v-else', () => {
const { node } = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`) const { node } = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`)
expect(node.type).toBe(NodeTypes.IF) expect(node.type).toBe(NodeTypes.IF)
@ -266,49 +282,49 @@ describe('compiler: v-if', () => {
describe('codegen', () => { describe('codegen', () => {
function assertSharedCodegen( function assertSharedCodegen(
node: SequenceExpression, node: IfConditionalExpression,
depth: number = 0, depth: number = 0,
hasElse: boolean = false hasElse: boolean = false
) { ) {
expect(node).toMatchObject({ expect(node).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
expressions: [ test: {
{ content: `ok`
type: NodeTypes.JS_CALL_EXPRESSION, },
callee: OPEN_BLOCK, consequent: {
arguments: [] type: NodeTypes.VNODE_CALL,
}, isBlock: true
{ },
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, alternate:
test: { depth < 1
content: `ok` ? hasElse
}, ? {
consequent: { type: NodeTypes.VNODE_CALL,
type: NodeTypes.JS_CALL_EXPRESSION, isBlock: true
callee: CREATE_BLOCK }
}, : {
alternate: type: NodeTypes.JS_CALL_EXPRESSION,
depth < 1 callee: CREATE_COMMENT
? { }
type: NodeTypes.JS_CALL_EXPRESSION, : {
callee: hasElse ? CREATE_BLOCK : CREATE_COMMENT type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
} test: {
: { content: `orNot`
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, },
test: { consequent: {
content: `orNot` type: NodeTypes.VNODE_CALL,
}, isBlock: true
consequent: { },
type: NodeTypes.JS_CALL_EXPRESSION, alternate: hasElse
callee: CREATE_BLOCK ? {
}, type: NodeTypes.VNODE_CALL,
alternate: { isBlock: true
type: NodeTypes.JS_CALL_EXPRESSION,
callee: hasElse ? CREATE_BLOCK : CREATE_COMMENT
} }
} : {
} type: NodeTypes.JS_CALL_EXPRESSION,
] callee: CREATE_COMMENT
}
}
}) })
} }
@ -318,15 +334,11 @@ describe('compiler: v-if', () => {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/>`) } = parseWithIfTransform(`<div v-if="ok"/>`)
assertSharedCodegen(codegenNode) assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.consequent).toMatchObject({
.consequent as CallExpression tag: `"div"`,
expect(branch1.arguments).toMatchObject([ props: createObjectMatcher({ key: `[0]` })
`"div"`, })
createObjectMatcher({ key: `[0]` }) expect(codegenNode.alternate).toMatchObject({
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression
expect(branch2).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT callee: CREATE_COMMENT
}) })
@ -339,20 +351,16 @@ describe('compiler: v-if', () => {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><div/>hello<p/></template>`) } = parseWithIfTransform(`<template v-if="ok"><div/>hello<p/></template>`)
assertSharedCodegen(codegenNode) assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.consequent).toMatchObject({
.consequent as CallExpression tag: FRAGMENT,
expect(branch1.arguments).toMatchObject([ props: createObjectMatcher({ key: `[0]` }),
FRAGMENT, children: [
createObjectMatcher({ key: `[0]` }),
[
{ type: NodeTypes.ELEMENT, tag: 'div' }, { type: NodeTypes.ELEMENT, tag: 'div' },
{ type: NodeTypes.TEXT, content: `hello` }, { type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: 'p' } { type: NodeTypes.ELEMENT, tag: 'p' }
] ]
]) })
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.alternate).toMatchObject({
.alternate as CallExpression
expect(branch2).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_COMMENT callee: CREATE_COMMENT
}) })
@ -364,10 +372,7 @@ describe('compiler: v-if', () => {
root, root,
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><slot/></template>`) } = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
// assertSharedCodegen(codegenNode) expect(codegenNode.consequent).toMatchObject({
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT, callee: RENDER_SLOT,
arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })] arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })]
@ -380,10 +385,7 @@ describe('compiler: v-if', () => {
root, root,
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<slot v-if="ok"></slot>`) } = parseWithIfTransform(`<slot v-if="ok"></slot>`)
// assertSharedCodegen(codegenNode) expect(codegenNode.consequent).toMatchObject({
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT, callee: RENDER_SLOT,
arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })] arguments: ['$slots', '"default"', createObjectMatcher({ key: `[0]` })]
@ -397,18 +399,14 @@ describe('compiler: v-if', () => {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`) } = parseWithIfTransform(`<div v-if="ok"/><p v-else/>`)
assertSharedCodegen(codegenNode, 0, true) assertSharedCodegen(codegenNode, 0, true)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.consequent).toMatchObject({
.consequent as CallExpression tag: `"div"`,
expect(branch1.arguments).toMatchObject([ props: createObjectMatcher({ key: `[0]` })
`"div"`, })
createObjectMatcher({ key: `[0]` }) expect(codegenNode.alternate).toMatchObject({
]) tag: `"p"`,
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) props: createObjectMatcher({ key: `[1]` })
.alternate as CallExpression })
expect(branch2.arguments).toMatchObject([
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -418,18 +416,15 @@ describe('compiler: v-if', () => {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok"/><p v-else-if="orNot" />`) } = parseWithIfTransform(`<div v-if="ok"/><p v-else-if="orNot" />`)
assertSharedCodegen(codegenNode, 1) assertSharedCodegen(codegenNode, 1)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.consequent).toMatchObject({
.consequent as CallExpression tag: `"div"`,
expect(branch1.arguments).toMatchObject([ props: createObjectMatcher({ key: `[0]` })
`"div"`, })
createObjectMatcher({ key: `[0]` }) const branch2 = codegenNode.alternate as ConditionalExpression
]) expect(branch2.consequent).toMatchObject({
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) tag: `"p"`,
.alternate as ConditionalExpression props: createObjectMatcher({ key: `[1]` })
expect((branch2.consequent as CallExpression).arguments).toMatchObject([ })
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -441,28 +436,25 @@ describe('compiler: v-if', () => {
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>` `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
) )
assertSharedCodegen(codegenNode, 1, true) assertSharedCodegen(codegenNode, 1, true)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) expect(codegenNode.consequent).toMatchObject({
.consequent as CallExpression tag: `"div"`,
expect(branch1.arguments).toMatchObject([ props: createObjectMatcher({ key: `[0]` })
`"div"`, })
createObjectMatcher({ key: `[0]` }) const branch2 = codegenNode.alternate as ConditionalExpression
]) expect(branch2.consequent).toMatchObject({
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) tag: `"p"`,
.alternate as ConditionalExpression props: createObjectMatcher({ key: `[1]` })
expect((branch2.consequent as CallExpression).arguments).toMatchObject([ })
`"p"`, expect(branch2.alternate).toMatchObject({
createObjectMatcher({ key: `[1]` }) tag: FRAGMENT,
]) props: createObjectMatcher({ key: `[2]` }),
expect((branch2.alternate as CallExpression).arguments).toMatchObject([ children: [
FRAGMENT,
createObjectMatcher({ key: `[2]` }),
[
{ {
type: NodeTypes.TEXT, type: NodeTypes.TEXT,
content: `fine` content: `fine`
} }
] ]
]) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -470,9 +462,8 @@ describe('compiler: v-if', () => {
const { const {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-bind="obj"/>`) } = parseWithIfTransform(`<div v-if="ok" v-bind="obj"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = codegenNode.consequent as VNodeCall
.consequent as CallExpression expect(branch1.props).toMatchObject({
expect(branch1.arguments[1]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }] arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }]
@ -483,9 +474,8 @@ describe('compiler: v-if', () => {
const { const {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" id="foo" v-bind="obj"/>`) } = parseWithIfTransform(`<div v-if="ok" id="foo" v-bind="obj"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = codegenNode.consequent as VNodeCall
.consequent as CallExpression expect(branch1.props).toMatchObject({
expect(branch1.arguments[1]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -502,9 +492,8 @@ describe('compiler: v-if', () => {
const { const {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-bind="obj" id="foo"/>`) } = parseWithIfTransform(`<div v-if="ok" v-bind="obj" id="foo"/>`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = codegenNode.consequent as VNodeCall
.consequent as CallExpression expect(branch1.props).toMatchObject({
expect(branch1.arguments[1]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS, callee: MERGE_PROPS,
arguments: [ arguments: [
@ -521,13 +510,21 @@ describe('compiler: v-if', () => {
const { const {
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" v-foo />`) } = parseWithIfTransform(`<div v-if="ok" v-foo />`)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = codegenNode.consequent as VNodeCall
.consequent as CallExpression expect(branch1.directives).not.toBeUndefined()
expect(branch1.callee).toBe(WITH_DIRECTIVES) expect(branch1.props).toMatchObject(createObjectMatcher({ key: `[0]` }))
const realBranch = branch1.arguments[0] as CallExpression })
expect(realBranch.arguments[1]).toMatchObject(
createObjectMatcher({ key: `[0]` }) test('v-if with key', () => {
) const {
root,
node: { codegenNode }
} = parseWithIfTransform(`<div v-if="ok" key="some-key"/>`)
expect(codegenNode.consequent).toMatchObject({
tag: `"div"`,
props: createObjectMatcher({ key: 'some-key' })
})
expect(generate(root).code).toMatchSnapshot()
}) })
test.todo('with comments') test.todo('with comments')

View File

@ -1,16 +1,15 @@
import { import {
parse, baseParse as parse,
transform, transform,
generate, generate,
ElementNode, ElementNode,
ObjectExpression, ObjectExpression,
CompilerOptions, CompilerOptions,
CallExpression,
ForNode, ForNode,
PlainElementNode, PlainElementNode,
PlainElementCodegenNode,
ComponentNode, ComponentNode,
NodeTypes NodeTypes,
VNodeCall
} from '../../src' } from '../../src'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { transformModel } from '../../src/transforms/vModel' import { transformModel } from '../../src/transforms/vModel'
@ -43,8 +42,8 @@ describe('compiler: transform v-model', () => {
test('simple exprssion', () => { test('simple exprssion', () => {
const root = parseWithVModel('<input v-model="model" />') const root = parseWithVModel('<input v-model="model" />')
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -82,8 +81,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -119,8 +118,8 @@ describe('compiler: transform v-model', () => {
test('compound expression', () => { test('compound expression', () => {
const root = parseWithVModel('<input v-model="model[index]" />') const root = parseWithVModel('<input v-model="model[index]" />')
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -158,8 +157,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -191,15 +190,19 @@ describe('compiler: transform v-model', () => {
children: [ children: [
'$event => (', '$event => (',
{ {
content: '_ctx.model', children: [
isStatic: false {
content: '_ctx.model',
isStatic: false
},
'[',
{
content: '_ctx.index',
isStatic: false
},
']'
]
}, },
'[',
{
content: '_ctx.index',
isStatic: false
},
']',
' = $event)' ' = $event)'
] ]
} }
@ -211,9 +214,8 @@ describe('compiler: transform v-model', () => {
test('with argument', () => { test('with argument', () => {
const root = parseWithVModel('<input v-model:value="model" />') const root = parseWithVModel('<input v-model:value="model" />')
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
content: 'value', content: 'value',
@ -248,8 +250,8 @@ describe('compiler: transform v-model', () => {
test('with dynamic argument', () => { test('with dynamic argument', () => {
const root = parseWithVModel('<input v-model:[value]="model" />') const root = parseWithVModel('<input v-model:[value]="model" />')
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -292,8 +294,8 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const node = root.children[0] as ElementNode const node = root.children[0] as ElementNode
const props = ((node.codegenNode as CallExpression) const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
.arguments[1] as ObjectExpression).properties .properties
expect(props[0]).toMatchObject({ expect(props[0]).toMatchObject({
key: { key: {
@ -338,12 +340,12 @@ describe('compiler: transform v-model', () => {
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
const codegen = (root.children[0] as PlainElementNode) const codegen = (root.children[0] as PlainElementNode)
.codegenNode as PlainElementCodegenNode .codegenNode as VNodeCall
// should not list cached prop in dynamicProps // should not list cached prop in dynamicProps
expect(codegen.arguments[4]).toBe(`["modelValue"]`) expect(codegen.dynamicProps).toBe(`["modelValue"]`)
expect( expect((codegen.props as ObjectExpression).properties[1].value.type).toBe(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type NodeTypes.JS_CACHE_EXPRESSION
).toBe(NodeTypes.JS_CACHE_EXPRESSION) )
}) })
test('should not cache update handler if it refers v-for scope variables', () => { test('should not cache update handler if it refers v-for scope variables', () => {
@ -356,10 +358,10 @@ describe('compiler: transform v-model', () => {
) )
expect(root.cached).toBe(0) expect(root.cached).toBe(0)
const codegen = ((root.children[0] as ForNode) const codegen = ((root.children[0] as ForNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode .children[0] as PlainElementNode).codegenNode as VNodeCall
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
expect( expect(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type (codegen.props as ObjectExpression).properties[1].value.type
).not.toBe(NodeTypes.JS_CACHE_EXPRESSION) ).not.toBe(NodeTypes.JS_CACHE_EXPRESSION)
}) })
@ -371,18 +373,18 @@ describe('compiler: transform v-model', () => {
} }
) )
const codegen = ((root.children[0] as ComponentNode) const codegen = ((root.children[0] as ComponentNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode .children[0] as PlainElementNode).codegenNode as VNodeCall
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
}) })
test('should generate modelModifers for component v-model', () => { test('should generate modelModifers for component v-model', () => {
const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', { const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const args = ((root.children[0] as ComponentNode) const vnodeCall = (root.children[0] as ComponentNode)
.codegenNode as CallExpression).arguments .codegenNode as VNodeCall
// props // props
expect(args[1]).toMatchObject({ expect(vnodeCall.props).toMatchObject({
properties: [ properties: [
{ key: { content: `modelValue` } }, { key: { content: `modelValue` } },
{ key: { content: `onUpdate:modelValue` } }, { key: { content: `onUpdate:modelValue` } },
@ -394,7 +396,7 @@ describe('compiler: transform v-model', () => {
}) })
// should NOT include modelModifiers in dynamicPropNames because it's never // should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change // gonna change
expect(args[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) expect(vnodeCall.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
}) })
test('should generate modelModifers for component v-model with arguments', () => { test('should generate modelModifers for component v-model with arguments', () => {
@ -404,10 +406,10 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true prefixIdentifiers: true
} }
) )
const args = ((root.children[0] as ComponentNode) const vnodeCall = (root.children[0] as ComponentNode)
.codegenNode as CallExpression).arguments .codegenNode as VNodeCall
// props // props
expect(args[1]).toMatchObject({ expect(vnodeCall.props).toMatchObject({
properties: [ properties: [
{ key: { content: `foo` } }, { key: { content: `foo` } },
{ key: { content: `onUpdate:foo` } }, { key: { content: `onUpdate:foo` } },
@ -425,7 +427,9 @@ describe('compiler: transform v-model', () => {
}) })
// should NOT include modelModifiers in dynamicPropNames because it's never // should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change // gonna change
expect(args[4]).toBe(`["foo", "onUpdate:foo", "bar", "onUpdate:bar"]`) expect(vnodeCall.dynamicProps).toBe(
`["foo", "onUpdate:foo", "bar", "onUpdate:bar"]`
)
}) })
describe('errors', () => { describe('errors', () => {

View File

@ -1,13 +1,12 @@
import { import {
parse, baseParse as parse,
transform, transform,
ElementNode, ElementNode,
ObjectExpression, ObjectExpression,
CompilerOptions, CompilerOptions,
ErrorCodes, ErrorCodes,
NodeTypes, NodeTypes,
CallExpression, VNodeCall
PlainElementCodegenNode
} from '../../src' } from '../../src'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
@ -31,54 +30,58 @@ function parseWithVOn(template: string, options: CompilerOptions = {}) {
describe('compiler: transform v-on', () => { describe('compiler: transform v-on', () => {
test('basic', () => { test('basic', () => {
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`) const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { key: {
content: `onClick`, content: `onClick`,
isStatic: true, isStatic: true,
loc: { loc: {
start: { start: {
line: 1, line: 1,
column: 11 column: 11
},
end: {
line: 1,
column: 16
}
}
}, },
end: { value: {
line: 1, content: `onClick`,
column: 16 isStatic: false,
loc: {
start: {
line: 1,
column: 18
},
end: {
line: 1,
column: 25
}
}
} }
} }
}, ]
value: {
content: `onClick`,
isStatic: false,
loc: {
start: {
line: 1,
column: 18
},
end: {
line: 1,
column: 25
}
}
}
}) })
}) })
test('dynamic arg', () => { test('dynamic arg', () => {
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`) const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`)
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `event` }, `)`] children: [`"on" + (`, { content: `event` }, `)`]
}, },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `handler`, content: `handler`,
isStatic: false isStatic: false
} }
}
]
}) })
}) })
@ -86,18 +89,20 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, { const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [`"on" + (`, { content: `_ctx.event` }, `)`] children: [`"on" + (`, { content: `_ctx.event` }, `)`]
}, },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`, content: `_ctx.handler`,
isStatic: false isStatic: false
} }
}
]
}) })
}) })
@ -105,38 +110,60 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, { const { node } = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`"on" + (`, `"on" + (`,
{ content: `_ctx.event` }, { content: `_ctx.event` },
`(`, `(`,
{ content: `_ctx.foo` }, { content: `_ctx.foo` },
`)`, `)`,
`)` `)`
] ]
}, },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.handler`, content: `_ctx.handler`,
isStatic: false isStatic: false
} }
}
]
}) })
}) })
test('should wrap as function if expression is inline statement', () => { test('should wrap as function if expression is inline statement', () => {
const { node } = parseWithVOn(`<div @click="i++"/>`) const { node } = parseWithVOn(`<div @click="i++"/>`)
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `i++` }, `)`] children: [`$event => (`, { content: `i++` }, `)`]
} }
}
]
})
})
test('should handle multiple inline statement', () => {
const { node } = parseWithVOn(`<div @click="foo();bar()"/>`)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
// should wrap with `{` for multiple statements
// in this case the return value is discarded and the behavior is
// consistent with 2.x
children: [`$event => {`, { content: `foo();bar()` }, `}`]
}
}
]
}) })
}) })
@ -144,48 +171,90 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="foo($event)"/>`, { const { node } = parseWithVOn(`<div @click="foo($event)"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`$event => (`, `$event => (`,
{ content: `_ctx.foo` }, {
`(`, type: NodeTypes.COMPOUND_EXPRESSION,
// should NOT prefix $event children: [
{ content: `$event` }, { content: `_ctx.foo` },
`)`, `(`,
`)` // should NOT prefix $event
] { content: `$event` },
} `)`
]
},
`)`
]
}
}
]
})
})
test('multiple inline statements w/ prefixIdentifiers: true', () => {
const { node } = parseWithVOn(`<div @click="foo($event);bar()"/>`, {
prefixIdentifiers: true
})
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$event => {`,
{
children: [
{ content: `_ctx.foo` },
`(`,
// should NOT prefix $event
{ content: `$event` },
`);`,
{ content: `_ctx.bar` },
`()`
]
},
`}`
]
}
}
]
}) })
}) })
test('should NOT wrap as function if expression is already function expression', () => { test('should NOT wrap as function if expression is already function expression', () => {
const { node } = parseWithVOn(`<div @click="$event => foo($event)"/>`) const { node } = parseWithVOn(`<div @click="$event => foo($event)"/>`)
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `$event => foo($event)` content: `$event => foo($event)`
} }
}
]
}) })
}) })
test('should NOT wrap as function if expression is complex member expression', () => { test('should NOT wrap as function if expression is complex member expression', () => {
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`) const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`)
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `a['b' + c]` content: `a['b' + c]`
} }
}
]
}) })
}) })
@ -193,14 +262,21 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`, { const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, `['b' + `, { content: `_ctx.c` }, `]`] children: [
} { content: `_ctx.a` },
`['b' + `,
{ content: `_ctx.c` },
`]`
]
}
}
]
}) })
}) })
@ -208,21 +284,23 @@ describe('compiler: transform v-on', () => {
const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, { const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) expect((node.codegenNode as VNodeCall).props).toMatchObject({
.arguments[1] as ObjectExpression properties: [
expect(props.properties[0]).toMatchObject({ {
key: { content: `onClick` }, key: { content: `onClick` },
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
{ content: `e` }, { content: `e` },
` => `, ` => `,
{ content: `_ctx.foo` }, { content: `_ctx.foo` },
`(`, `(`,
{ content: `e` }, { content: `e` },
`)` `)`
] ]
} }
}
]
}) })
}) })
@ -250,6 +328,22 @@ describe('compiler: transform v-on', () => {
expect(onError).not.toHaveBeenCalled() expect(onError).not.toHaveBeenCalled()
}) })
test('case conversion for vnode hooks', () => {
const { node } = parseWithVOn(`<div v-on:vnode-mounted="onMount"/>`)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
content: `onVnodeMounted`
},
value: {
content: `onMount`
}
}
]
})
})
describe('cacheHandler', () => { describe('cacheHandler', () => {
test('empty handler', () => { test('empty handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click.prevent />`, { const { root, node } = parseWithVOn(`<div v-on:click.prevent />`, {
@ -257,10 +351,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true cacheHandlers: true
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2) expect(vnodeCall.patchFlag).toBeUndefined()
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({ expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
@ -276,10 +372,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true cacheHandlers: true
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2) expect(vnodeCall.patchFlag).toBeUndefined()
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({ expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
@ -295,10 +393,12 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true cacheHandlers: true
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2) expect(vnodeCall.patchFlag).toBeUndefined()
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({ expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
@ -314,15 +414,22 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true cacheHandlers: true
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments expect(root.cached).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2) expect(vnodeCall.patchFlag).toBeUndefined()
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({ expect(
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `_ctx.foo` }, `++`, `)`] children: [
`$event => (`,
{ children: [{ content: `_ctx.foo` }, `++`] },
`)`
]
} }
}) })
}) })

View File

@ -1,5 +1,5 @@
import { import {
parse, baseParse as parse,
transform, transform,
NodeTypes, NodeTypes,
generate, generate,
@ -7,11 +7,7 @@ import {
} from '../../src' } from '../../src'
import { transformOnce } from '../../src/transforms/vOnce' import { transformOnce } from '../../src/transforms/vOnce'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { import { RENDER_SLOT, SET_BLOCK_TRACKING } from '../../src/runtimeHelpers'
CREATE_VNODE,
RENDER_SLOT,
SET_BLOCK_TRACKING
} from '../../src/runtimeHelpers'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet' import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet'
@ -36,8 +32,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE tag: `"div"`
} }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -51,8 +47,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE tag: `"div"`
} }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -66,8 +62,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE tag: `_component_Comp`
} }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -100,8 +96,8 @@ describe('compiler: v-once transform', () => {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
callee: CREATE_VNODE tag: `"div"`
} }
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()

View File

@ -1,14 +1,15 @@
import { import {
CompilerOptions, CompilerOptions,
parse, baseParse as parse,
transform, transform,
generate, generate,
ElementNode, ElementNode,
NodeTypes, NodeTypes,
ErrorCodes, ErrorCodes,
ForNode, ForNode,
CallExpression, ComponentNode,
ComponentNode VNodeCall,
SlotsExpression
} from '../../src' } from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
@ -46,7 +47,8 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
root: ast, root: ast,
slots: slots:
ast.children[0].type === NodeTypes.ELEMENT ast.children[0].type === NodeTypes.ELEMENT
? (ast.children[0].codegenNode as CallExpression).arguments[2] ? ((ast.children[0].codegenNode as VNodeCall)
.children as SlotsExpression)
: null : null
} }
} }
@ -67,8 +69,8 @@ function createSlotMatcher(obj: Record<string, any>) {
} as any } as any
}) })
.concat({ .concat({
key: { content: `_compiled` }, key: { content: `_` },
value: { content: `true` } value: { content: `1`, isStatic: false }
}) })
} }
} }
@ -95,7 +97,7 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('explicit default slot', () => { test('on-component default slot', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`, `<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true } { prefixIdentifiers: true }
@ -128,7 +130,40 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('named slots', () => { test('on component named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:named="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
named: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('template named slots', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp> `<Comp>
<template v-slot:one="{ foo }"> <template v-slot:one="{ foo }">
@ -189,6 +224,76 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('on component dynamically named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:[named]="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
'[_ctx.named]': {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots w/ implicit default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one>foo</template>bar<span/>
</Comp>`
)
expect(slots).toMatchObject(
createSlotMatcher({
one: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `foo`
}
]
},
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `bar`
},
{
type: NodeTypes.ELEMENT,
tag: `span`
}
]
}
})
)
expect(generate(root).code).toMatchSnapshot()
})
test('dynamically named slots', () => { test('dynamically named slots', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp> `<Comp>
@ -274,43 +379,41 @@ describe('compiler: transform component slots', () => {
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.VNODE_CALL,
arguments: [ tag: `_component_Inner`,
`_component_Inner`, props: undefined,
`null`, children: createSlotMatcher({
createSlotMatcher({ default: {
default: { type: NodeTypes.JS_FUNCTION_EXPRESSION,
type: NodeTypes.JS_FUNCTION_EXPRESSION, params: {
params: { type: NodeTypes.COMPOUND_EXPRESSION,
type: NodeTypes.COMPOUND_EXPRESSION, children: [`{ `, { content: `bar` }, ` }`]
children: [`{ `, { content: `bar` }, ` }`] },
}, returns: [
returns: [ {
{ type: NodeTypes.INTERPOLATION,
type: NodeTypes.INTERPOLATION, content: {
content: { content: `foo`
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
} }
] },
} {
}), type: NodeTypes.INTERPOLATION,
// nested slot should be forced dynamic, since scope variables content: {
// are not tracked as dependencies of the slot. content: `bar`
genFlagText(PatchFlags.DYNAMIC_SLOTS) }
] },
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
}
]
}
}),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS)
} }
}, },
// test scope // test scope
@ -351,8 +454,8 @@ describe('compiler: transform component slots', () => {
) )
const div = ((root.children[0] as ForNode).children[0] as ElementNode) const div = ((root.children[0] as ForNode).children[0] as ElementNode)
.codegenNode as any .codegenNode as any
const comp = div.arguments[2][0] const comp = div.children[0]
expect(comp.codegenNode.arguments[3]).toBe( expect(comp.codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.DYNAMIC_SLOTS) genFlagText(PatchFlags.DYNAMIC_SLOTS)
) )
}) })
@ -364,12 +467,12 @@ describe('compiler: transform component slots', () => {
if (root.children[0].type === NodeTypes.FOR) { if (root.children[0].type === NodeTypes.FOR) {
const div = (root.children[0].children[0] as ElementNode) const div = (root.children[0].children[0] as ElementNode)
.codegenNode as any .codegenNode as any
const comp = div.arguments[2][0] const comp = div.children[0]
flag = comp.codegenNode.arguments[3] flag = comp.codegenNode.patchFlag
} else { } else {
const innerComp = (root.children[0] as ComponentNode) const innerComp = (root.children[0] as ComponentNode)
.children[0] as ComponentNode .children[0] as ComponentNode
flag = (innerComp.codegenNode as CallExpression).arguments[3] flag = (innerComp.codegenNode as VNodeCall).patchFlag
} }
if (shouldForce) { if (shouldForce) {
expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS)) expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))
@ -419,7 +522,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS, callee: CREATE_SLOTS,
arguments: [ arguments: [
createObjectMatcher({ createObjectMatcher({
_compiled: `[true]` _: `[1]`
}), }),
{ {
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
@ -443,7 +546,7 @@ describe('compiler: transform component slots', () => {
} }
] ]
}) })
expect((root as any).children[0].codegenNode.arguments[3]).toMatch( expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '' PatchFlags.DYNAMIC_SLOTS + ''
) )
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -461,7 +564,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS, callee: CREATE_SLOTS,
arguments: [ arguments: [
createObjectMatcher({ createObjectMatcher({
_compiled: `[true]` _: `[1]`
}), }),
{ {
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
@ -491,7 +594,7 @@ describe('compiler: transform component slots', () => {
} }
] ]
}) })
expect((root as any).children[0].codegenNode.arguments[3]).toMatch( expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '' PatchFlags.DYNAMIC_SLOTS + ''
) )
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
@ -510,7 +613,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS, callee: CREATE_SLOTS,
arguments: [ arguments: [
createObjectMatcher({ createObjectMatcher({
_compiled: `[true]` _: `[1]`
}), }),
{ {
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
@ -551,7 +654,7 @@ describe('compiler: transform component slots', () => {
} }
] ]
}) })
expect((root as any).children[0].codegenNode.arguments[3]).toMatch( expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '' PatchFlags.DYNAMIC_SLOTS + ''
) )
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
@ -569,7 +672,7 @@ describe('compiler: transform component slots', () => {
callee: CREATE_SLOTS, callee: CREATE_SLOTS,
arguments: [ arguments: [
createObjectMatcher({ createObjectMatcher({
_compiled: `[true]` _: `[1]`
}), }),
{ {
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
@ -601,20 +704,20 @@ describe('compiler: transform component slots', () => {
} }
] ]
}) })
expect((root as any).children[0].codegenNode.arguments[3]).toMatch( expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '' PatchFlags.DYNAMIC_SLOTS + ''
) )
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
describe('errors', () => { describe('errors', () => {
test('error on extraneous children w/ named slots', () => { test('error on extraneous children w/ named default slot', () => {
const onError = jest.fn() const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>` const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError }) parseWithSlots(source, { onError })
const index = source.indexOf('bar') const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({ expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN, code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
loc: { loc: {
source: `bar`, source: `bar`,
start: { start: {
@ -699,28 +802,5 @@ describe('compiler: transform component slots', () => {
} }
}) })
}) })
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
}
}
})
})
}) })
}) })

View File

@ -1,15 +1,16 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.0.0-alpha.0", "version": "3.0.0-alpha.11",
"description": "@vue/compiler-core", "description": "@vue/compiler-core",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-core.esm-bundler.js", "module": "dist/compiler-core.esm-bundler.js",
"types": "dist/compiler-core.d.ts",
"files": [ "files": [
"index.js", "index.js",
"dist" "dist"
], ],
"types": "dist/compiler-core.d.ts",
"buildOptions": { "buildOptions": {
"name": "VueCompilerCore",
"formats": [ "formats": [
"esm-bundler", "esm-bundler",
"cjs" "cjs"
@ -17,7 +18,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/vuejs/vue.git" "url": "git+https://github.com/vuejs/vue-next.git"
}, },
"keywords": [ "keywords": [
"vue" "vue"
@ -25,12 +26,14 @@
"author": "Evan You", "author": "Evan You",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vue/issues" "url": "https://github.com/vuejs/vue-next/issues"
}, },
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme", "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-core#readme",
"dependencies": { "dependencies": {
"acorn": "^7.1.0", "@vue/shared": "3.0.0-alpha.11",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"estree-walker": "^0.8.1", "estree-walker": "^0.8.1",
"source-map": "^0.7.3" "source-map": "^0.6.1"
} }
} }

View File

@ -1,17 +1,17 @@
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { ForParseResult } from './transforms/vFor' import { ForParseResult } from './transforms/vFor'
import { import {
CREATE_VNODE,
WITH_DIRECTIVES,
RENDER_SLOT, RENDER_SLOT,
CREATE_SLOTS, CREATE_SLOTS,
RENDER_LIST, RENDER_LIST,
OPEN_BLOCK, OPEN_BLOCK,
CREATE_BLOCK, CREATE_BLOCK,
FRAGMENT FRAGMENT,
CREATE_VNODE,
WITH_DIRECTIVES
} from './runtimeHelpers' } from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement' import { PropsExpression } from './transforms/transformElement'
import { ImportItem } from './transform' import { ImportItem, TransformContext } from './transform'
// Vue template is a platform-agnostic superset of HTML (syntax only). // Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces like SVG and MathML are declared by platform specific // More namespaces like SVG and MathML are declared by platform specific
@ -38,14 +38,22 @@ export const enum NodeTypes {
FOR, FOR,
TEXT_CALL, TEXT_CALL,
// codegen // codegen
VNODE_CALL,
JS_CALL_EXPRESSION, JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION, JS_OBJECT_EXPRESSION,
JS_PROPERTY, JS_PROPERTY,
JS_ARRAY_EXPRESSION, JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION, JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_CONDITIONAL_EXPRESSION, JS_CONDITIONAL_EXPRESSION,
JS_CACHE_EXPRESSION JS_CACHE_EXPRESSION,
// ssr codegen
JS_BLOCK_STATEMENT,
JS_TEMPLATE_LITERAL,
JS_IF_STATEMENT,
JS_ASSIGNMENT_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_RETURN_STATEMENT
} }
export const enum ElementTypes { export const enum ElementTypes {
@ -85,6 +93,7 @@ export type TemplateChildNode =
| TextNode | TextNode
| CommentNode | CommentNode
| IfNode | IfNode
| IfBranchNode
| ForNode | ForNode
| TextCallNode | TextCallNode
@ -97,7 +106,9 @@ export interface RootNode extends Node {
hoists: JSChildNode[] hoists: JSChildNode[]
imports: ImportItem[] imports: ImportItem[]
cached: number cached: number
codegenNode: TemplateChildNode | JSChildNode | undefined temps: number
ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
} }
export type ElementNode = export type ElementNode =
@ -114,35 +125,40 @@ export interface BaseElementNode extends Node {
isSelfClosing: boolean isSelfClosing: boolean
props: Array<AttributeNode | DirectiveNode> props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[] children: TemplateChildNode[]
codegenNode:
| CallExpression
| SimpleExpressionNode
| CacheExpression
| undefined
} }
export interface PlainElementNode extends BaseElementNode { export interface PlainElementNode extends BaseElementNode {
tagType: ElementTypes.ELEMENT tagType: ElementTypes.ELEMENT
codegenNode: codegenNode:
| ElementCodegenNode | VNodeCall
| undefined
| SimpleExpressionNode // when hoisted | SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once | CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: TemplateLiteral
} }
export interface ComponentNode extends BaseElementNode { export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT tagType: ElementTypes.COMPONENT
codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: CallExpression
} }
export interface SlotOutletNode extends BaseElementNode { export interface SlotOutletNode extends BaseElementNode {
tagType: ElementTypes.SLOT tagType: ElementTypes.SLOT
codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once codegenNode:
| RenderSlotCall
| CacheExpression // when cached by v-once
| undefined
ssrCodegenNode?: CallExpression
} }
export interface TemplateNode extends BaseElementNode { export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE tagType: ElementTypes.TEMPLATE
codegenNode: ElementCodegenNode | undefined | CacheExpression // TemplateNode is a container type that always gets compiled away
codegenNode: undefined
} }
export interface TextNode extends Node { export interface TextNode extends Node {
@ -190,6 +206,7 @@ export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION type: NodeTypes.COMPOUND_EXPRESSION
children: ( children: (
| SimpleExpressionNode | SimpleExpressionNode
| CompoundExpressionNode
| InterpolationNode | InterpolationNode
| TextNode | TextNode
| string | string
@ -202,7 +219,7 @@ export interface CompoundExpressionNode extends Node {
export interface IfNode extends Node { export interface IfNode extends Node {
type: NodeTypes.IF type: NodeTypes.IF
branches: IfBranchNode[] branches: IfBranchNode[]
codegenNode: IfCodegenNode codegenNode?: IfConditionalExpression
} }
export interface IfBranchNode extends Node { export interface IfBranchNode extends Node {
@ -217,28 +234,56 @@ export interface ForNode extends Node {
valueAlias: ExpressionNode | undefined valueAlias: ExpressionNode | undefined
keyAlias: ExpressionNode | undefined keyAlias: ExpressionNode | undefined
objectIndexAlias: ExpressionNode | undefined objectIndexAlias: ExpressionNode | undefined
parseResult: ForParseResult
children: TemplateChildNode[] children: TemplateChildNode[]
codegenNode: ForCodegenNode codegenNode?: ForCodegenNode
} }
export interface TextCallNode extends Node { export interface TextCallNode extends Node {
type: NodeTypes.TEXT_CALL type: NodeTypes.TEXT_CALL
content: TextNode | InterpolationNode | CompoundExpressionNode content: TextNode | InterpolationNode | CompoundExpressionNode
codegenNode: CallExpression codegenNode: CallExpression | SimpleExpressionNode // when hoisted
} }
export type TemplateTextChildNode =
| TextNode
| InterpolationNode
| CompoundExpressionNode
export interface VNodeCall extends Node {
type: NodeTypes.VNODE_CALL
tag: string | symbol | CallExpression
props: PropsExpression | undefined
children:
| TemplateChildNode[] // multiple children
| TemplateTextChildNode // single text child
| SlotsExpression // component slots
| ForRenderListExpression // v-for fragment call
| undefined
patchFlag: string | undefined
dynamicProps: string | undefined
directives: DirectiveArguments | undefined
isBlock: boolean
isForBlock: boolean
}
// JS Node Types ---------------------------------------------------------------
// We also include a number of JavaScript AST nodes for code generation. // We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentionally minimal subset just to meet the exact needs of // The AST is an intentionally minimal subset just to meet the exact needs of
// Vue render function generation. // Vue render function generation.
export type JSChildNode = export type JSChildNode =
| VNodeCall
| CallExpression | CallExpression
| ObjectExpression | ObjectExpression
| ArrayExpression | ArrayExpression
| ExpressionNode | ExpressionNode
| FunctionExpression | FunctionExpression
| ConditionalExpression | ConditionalExpression
| SequenceExpression
| CacheExpression | CacheExpression
| AssignmentExpression
| SequenceExpression
export interface CallExpression extends Node { export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION type: NodeTypes.JS_CALL_EXPRESSION
@ -247,6 +292,7 @@ export interface CallExpression extends Node {
| string | string
| symbol | symbol
| JSChildNode | JSChildNode
| SSRCodegenNode
| TemplateChildNode | TemplateChildNode
| TemplateChildNode[])[] | TemplateChildNode[])[]
} }
@ -269,21 +315,20 @@ export interface ArrayExpression extends Node {
export interface FunctionExpression extends Node { export interface FunctionExpression extends Node {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | ExpressionNode[] | undefined params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
body?: BlockStatement | IfStatement
newline: boolean newline: boolean
} // so that codegen knows it needs to generate ScopeId wrapper
isSlot: boolean
export interface SequenceExpression extends Node {
type: NodeTypes.JS_SEQUENCE_EXPRESSION
expressions: JSChildNode[]
} }
export interface ConditionalExpression extends Node { export interface ConditionalExpression extends Node {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION type: NodeTypes.JS_CONDITIONAL_EXPRESSION
test: ExpressionNode test: JSChildNode
consequent: JSChildNode consequent: JSChildNode
alternate: JSChildNode alternate: JSChildNode
newline: boolean
} }
export interface CacheExpression extends Node { export interface CacheExpression extends Node {
@ -293,59 +338,76 @@ export interface CacheExpression extends Node {
isVNode: boolean isVNode: boolean
} }
// SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode =
| BlockStatement
| TemplateLiteral
| IfStatement
| AssignmentExpression
| ReturnStatement
| SequenceExpression
export interface BlockStatement extends Node {
type: NodeTypes.JS_BLOCK_STATEMENT
body: (JSChildNode | IfStatement)[]
}
export interface TemplateLiteral extends Node {
type: NodeTypes.JS_TEMPLATE_LITERAL
elements: (string | JSChildNode)[]
}
export interface IfStatement extends Node {
type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode
consequent: BlockStatement
alternate: IfStatement | BlockStatement | ReturnStatement | undefined
}
export interface AssignmentExpression extends Node {
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION
left: SimpleExpressionNode
right: JSChildNode
}
export interface SequenceExpression extends Node {
type: NodeTypes.JS_SEQUENCE_EXPRESSION
expressions: JSChildNode[]
}
export interface ReturnStatement extends Node {
type: NodeTypes.JS_RETURN_STATEMENT
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
}
// Codegen Node Types ---------------------------------------------------------- // Codegen Node Types ----------------------------------------------------------
// createVNode(...) export interface DirectiveArguments extends ArrayExpression {
export interface PlainElementCodegenNode extends CallExpression { elements: DirectiveArgumentNode[]
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
arguments: // tag, props, children, patchFlag, dynamicProps
| [string | symbol]
| [string | symbol, PropsExpression]
| [string | symbol, 'null' | PropsExpression, TemplateChildNode[]]
| [
string | symbol,
'null' | PropsExpression,
'null' | TemplateChildNode[],
string
]
| [
string | symbol,
'null' | PropsExpression,
'null' | TemplateChildNode[],
string,
string
]
} }
export type ElementCodegenNode = export interface DirectiveArgumentNode extends ArrayExpression {
| PlainElementCodegenNode elements: // dir, exp, arg, modifiers
| CodegenNodeWithDirective<PlainElementCodegenNode> | [string]
| [string, ExpressionNode]
// createVNode(...) | [string, ExpressionNode, ExpressionNode]
export interface PlainComponentCodegenNode extends CallExpression { | [string, ExpressionNode, ExpressionNode, ObjectExpression]
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
arguments: // Comp, props, slots, patchFlag, dynamicProps
| [string | symbol]
| [string | symbol, PropsExpression]
| [string | symbol, 'null' | PropsExpression, SlotsExpression]
| [
string | symbol,
'null' | PropsExpression,
'null' | SlotsExpression,
string
]
| [
string | symbol,
'null' | PropsExpression,
'null' | SlotsExpression,
string,
string
]
} }
export type ComponentCodegenNode = // renderSlot(...)
| PlainComponentCodegenNode export interface RenderSlotCall extends CallExpression {
| CodegenNodeWithDirective<PlainComponentCodegenNode> callee: typeof RENDER_SLOT
arguments: // $slots, name, props, fallback
| [string, string | ExpressionNode]
| [string, string | ExpressionNode, PropsExpression]
| [
string,
string | ExpressionNode,
PropsExpression | '{}',
TemplateChildNode[]
]
}
export type SlotsExpression = SlotsObjectExpression | DynamicSlotsExpression export type SlotsExpression = SlotsObjectExpression | DynamicSlotsExpression
@ -397,63 +459,20 @@ export interface DynamicSlotFnProperty extends Property {
value: SlotFunctionExpression value: SlotFunctionExpression
} }
// withDirectives(createVNode(...), [ export type BlockCodegenNode = VNodeCall | RenderSlotCall
// [_directive_foo, someValue],
// [_directive_bar, someValue, "arg", { mod: true }]
// ])
export interface CodegenNodeWithDirective<T extends CallExpression>
extends CallExpression {
callee: typeof WITH_DIRECTIVES
arguments: [T, DirectiveArguments]
}
export interface DirectiveArguments extends ArrayExpression {
elements: DirectiveArgumentNode[]
}
export interface DirectiveArgumentNode extends ArrayExpression {
elements: // dir, exp, arg, modifiers
| [string]
| [string, ExpressionNode]
| [string, ExpressionNode, ExpressionNode]
| [string, ExpressionNode, ExpressionNode, ObjectExpression]
}
// renderSlot(...)
export interface SlotOutletCodegenNode extends CallExpression {
callee: typeof RENDER_SLOT
arguments: // $slots, name, props, fallback
| [string, string | ExpressionNode]
| [string, string | ExpressionNode, PropsExpression]
| [
string,
string | ExpressionNode,
PropsExpression | '{}',
TemplateChildNode[]
]
}
export type BlockCodegenNode =
| ElementCodegenNode
| ComponentCodegenNode
| SlotOutletCodegenNode
export interface IfCodegenNode extends SequenceExpression {
expressions: [OpenBlockExpression, IfConditionalExpression]
}
export interface IfConditionalExpression extends ConditionalExpression { export interface IfConditionalExpression extends ConditionalExpression {
consequent: BlockCodegenNode consequent: BlockCodegenNode
alternate: BlockCodegenNode | IfConditionalExpression alternate: BlockCodegenNode | IfConditionalExpression
} }
export interface ForCodegenNode extends SequenceExpression { export interface ForCodegenNode extends VNodeCall {
expressions: [OpenBlockExpression, ForBlockCodegenNode] isBlock: true
} tag: typeof FRAGMENT
props: undefined
export interface ForBlockCodegenNode extends CallExpression { children: ForRenderListExpression
callee: typeof CREATE_BLOCK patchFlag: string
arguments: [typeof FRAGMENT, 'null', ForRenderListExpression, string] isForBlock: true
} }
export interface ForRenderListExpression extends CallExpression { export interface ForRenderListExpression extends CallExpression {
@ -465,11 +484,6 @@ export interface ForIteratorExpression extends FunctionExpression {
returns: BlockCodegenNode returns: BlockCodegenNode
} }
export interface OpenBlockExpression extends CallExpression {
callee: typeof OPEN_BLOCK
arguments: []
}
// AST Utilities --------------------------------------------------------------- // AST Utilities ---------------------------------------------------------------
// Some expressions, e.g. sequence and conditional expressions, are never // Some expressions, e.g. sequence and conditional expressions, are never
@ -481,6 +495,63 @@ export const locStub: SourceLocation = {
end: { line: 1, column: 1, offset: 0 } end: { line: 1, column: 1, offset: 0 }
} }
export function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}
export function createVNodeCall(
context: TransformContext | null,
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag'],
dynamicProps?: VNodeCall['dynamicProps'],
directives?: VNodeCall['directives'],
isBlock: VNodeCall['isBlock'] = false,
isForBlock: VNodeCall['isForBlock'] = false,
loc = locStub
): VNodeCall {
if (context) {
if (isBlock) {
context.helper(OPEN_BLOCK)
context.helper(CREATE_BLOCK)
} else {
context.helper(CREATE_VNODE)
}
if (directives) {
context.helper(WITH_DIRECTIVES)
}
}
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
isForBlock,
loc
}
}
export function createArrayExpression( export function createArrayExpression(
elements: ArrayExpression['elements'], elements: ArrayExpression['elements'],
loc: SourceLocation = locStub loc: SourceLocation = locStub
@ -554,15 +625,9 @@ export function createCompoundExpression(
} }
} }
type InferCodegenNodeType<T> = T extends type InferCodegenNodeType<T> = T extends typeof RENDER_SLOT
| typeof CREATE_VNODE ? RenderSlotCall
| typeof CREATE_BLOCK : CallExpression
? PlainElementCodegenNode | PlainComponentCodegenNode
: T extends typeof WITH_DIRECTIVES
?
| CodegenNodeWithDirective<PlainElementCodegenNode>
| CodegenNodeWithDirective<PlainComponentCodegenNode>
: T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression
export function createCallExpression<T extends CallExpression['callee']>( export function createCallExpression<T extends CallExpression['callee']>(
callee: T, callee: T,
@ -579,8 +644,9 @@ export function createCallExpression<T extends CallExpression['callee']>(
export function createFunctionExpression( export function createFunctionExpression(
params: FunctionExpression['params'], params: FunctionExpression['params'],
returns: FunctionExpression['returns'], returns: FunctionExpression['returns'] = undefined,
newline: boolean = false, newline: boolean = false,
isSlot: boolean = false,
loc: SourceLocation = locStub loc: SourceLocation = locStub
): FunctionExpression { ): FunctionExpression {
return { return {
@ -588,30 +654,23 @@ export function createFunctionExpression(
params, params,
returns, returns,
newline, newline,
isSlot,
loc loc
} }
} }
export function createSequenceExpression(
expressions: SequenceExpression['expressions']
): SequenceExpression {
return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions,
loc: locStub
}
}
export function createConditionalExpression( export function createConditionalExpression(
test: ConditionalExpression['test'], test: ConditionalExpression['test'],
consequent: ConditionalExpression['consequent'], consequent: ConditionalExpression['consequent'],
alternate: ConditionalExpression['alternate'] alternate: ConditionalExpression['alternate'],
newline = true
): ConditionalExpression { ): ConditionalExpression {
return { return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test, test,
consequent, consequent,
alternate, alternate,
newline,
loc: locStub loc: locStub
} }
} }
@ -629,3 +688,69 @@ export function createCacheExpression(
loc: locStub loc: locStub
} }
} }
export function createBlockStatement(
body: BlockStatement['body']
): BlockStatement {
return {
type: NodeTypes.JS_BLOCK_STATEMENT,
body,
loc: locStub
}
}
export function createTemplateLiteral(
elements: TemplateLiteral['elements']
): TemplateLiteral {
return {
type: NodeTypes.JS_TEMPLATE_LITERAL,
elements,
loc: locStub
}
}
export function createIfStatement(
test: IfStatement['test'],
consequent: IfStatement['consequent'],
alternate?: IfStatement['alternate']
): IfStatement {
return {
type: NodeTypes.JS_IF_STATEMENT,
test,
consequent,
alternate,
loc: locStub
}
}
export function createAssignmentExpression(
left: AssignmentExpression['left'],
right: AssignmentExpression['right']
): AssignmentExpression {
return {
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION,
left,
right,
loc: locStub
}
}
export function createSequenceExpression(
expressions: SequenceExpression['expressions']
): SequenceExpression {
return {
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
expressions,
loc: locStub
}
}
export function createReturnStatement(
returns: ReturnStatement['returns']
): ReturnStatement {
return {
type: NodeTypes.JS_RETURN_STATEMENT,
returns,
loc: locStub
}
}

View File

@ -10,15 +10,21 @@ import {
CallExpression, CallExpression,
ArrayExpression, ArrayExpression,
ObjectExpression, ObjectExpression,
SourceLocation,
Position, Position,
InterpolationNode, InterpolationNode,
CompoundExpressionNode, CompoundExpressionNode,
SimpleExpressionNode, SimpleExpressionNode,
FunctionExpression, FunctionExpression,
SequenceExpression,
ConditionalExpression, ConditionalExpression,
CacheExpression CacheExpression,
locStub,
SSRCodegenNode,
TemplateLiteral,
IfStatement,
AssignmentExpression,
ReturnStatement,
VNodeCall,
SequenceExpression
} from './ast' } from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map' import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
@ -31,17 +37,25 @@ import {
import { isString, isArray, isSymbol } from '@vue/shared' import { isString, isArray, isSymbol } from '@vue/shared'
import { import {
helperNameMap, helperNameMap,
TO_STRING, TO_DISPLAY_STRING,
CREATE_VNODE, CREATE_VNODE,
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE, RESOLVE_DIRECTIVE,
SET_BLOCK_TRACKING, SET_BLOCK_TRACKING,
CREATE_COMMENT, CREATE_COMMENT,
CREATE_TEXT CREATE_TEXT,
PUSH_SCOPE_ID,
POP_SCOPE_ID,
WITH_SCOPE_ID,
WITH_DIRECTIVES,
CREATE_BLOCK,
OPEN_BLOCK,
CREATE_STATIC,
WITH_CTX
} from './runtimeHelpers' } from './runtimeHelpers'
import { ImportItem } from './transform' import { ImportItem } from './transform'
type CodegenNode = TemplateChildNode | JSChildNode type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult { export interface CodegenResult {
code: string code: string
@ -58,8 +72,7 @@ export interface CodegenContext extends Required<CodegenOptions> {
indentLevel: number indentLevel: number
map?: SourceMapGenerator map?: SourceMapGenerator
helper(key: symbol): string helper(key: symbol): string
push(code: string, node?: CodegenNode, openOnly?: boolean): void push(code: string, node?: CodegenNode): void
resetMapping(loc: SourceLocation): void
indent(): void indent(): void
deindent(withoutNewLine?: boolean): void deindent(withoutNewLine?: boolean): void
newline(): void newline(): void
@ -71,7 +84,12 @@ function createCodegenContext(
mode = 'function', mode = 'function',
prefixIdentifiers = mode === 'module', prefixIdentifiers = mode === 'module',
sourceMap = false, sourceMap = false,
filename = `template.vue.html` filename = `template.vue.html`,
scopeId = null,
optimizeBindings = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssr = false
}: CodegenOptions }: CodegenOptions
): CodegenContext { ): CodegenContext {
const context: CodegenContext = { const context: CodegenContext = {
@ -79,24 +97,22 @@ function createCodegenContext(
prefixIdentifiers, prefixIdentifiers,
sourceMap, sourceMap,
filename, filename,
scopeId,
optimizeBindings,
runtimeGlobalName,
runtimeModuleName,
ssr,
source: ast.loc.source, source: ast.loc.source,
code: ``, code: ``,
column: 1, column: 1,
line: 1, line: 1,
offset: 0, offset: 0,
indentLevel: 0, indentLevel: 0,
map: undefined,
// lazy require source-map implementation, only in non-browser builds!
map:
__BROWSER__ || !sourceMap
? undefined
: new (loadDep('source-map')).SourceMapGenerator(),
helper(key) { helper(key) {
const name = helperNameMap[key] return `_${helperNameMap[key]}`
return prefixIdentifiers ? name : `_${name}`
}, },
push(code, node, openOnly) { push(code, node) {
context.code += code context.code += code
if (!__BROWSER__ && context.map) { if (!__BROWSER__ && context.map) {
if (node) { if (node) {
@ -110,16 +126,11 @@ function createCodegenContext(
addMapping(node.loc.start, name) addMapping(node.loc.start, name)
} }
advancePositionWithMutation(context, code) advancePositionWithMutation(context, code)
if (node && !openOnly) { if (node && node.loc !== locStub) {
addMapping(node.loc.end) addMapping(node.loc.end)
} }
} }
}, },
resetMapping(loc: SourceLocation) {
if (!__BROWSER__ && context.map) {
addMapping(loc.start)
}
},
indent() { indent() {
newline(++context.indentLevel) newline(++context.indentLevel)
}, },
@ -154,9 +165,12 @@ function createCodegenContext(
}) })
} }
if (!__BROWSER__ && context.map) { if (!__BROWSER__ && sourceMap) {
context.map.setSourceContent(filename, context.source) // lazy require source-map implementation, only in non-browser builds
context.map = new (loadDep('source-map')).SourceMapGenerator()
context.map!.setSourceContent(filename, context.source)
} }
return context return context
} }
@ -168,63 +182,37 @@ export function generate(
const { const {
mode, mode,
push, push,
helper,
prefixIdentifiers, prefixIdentifiers,
indent, indent,
deindent, deindent,
newline newline,
scopeId,
ssr
} = context } = context
const hasHelpers = ast.helpers.length > 0 const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module' const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// preambles // preambles
if (mode === 'function') { if (!__BROWSER__ && mode === 'module') {
// Generate const declaration for helpers genModulePreamble(ast, context, genScopeId)
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasHelpers) {
if (prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = Vue\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = Vue\n`)
}
}
}
genHoists(ast.hoists, context)
newline()
push(`return `)
} else { } else {
// generate import statements for helpers genFunctionPreamble(ast, context)
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export default `)
} }
// enter render function // enter render function
push(`function render() {`) if (genScopeId && !ssr) {
push(`const render = _withId(`)
}
if (!ssr) {
push(`function render(_ctx, _cache) {`)
} else {
push(`function ssrRender(_ctx, _push, _parent) {`)
}
indent() indent()
if (useWithBlock) { if (useWithBlock) {
push(`with (this) {`) push(`with (_ctx) {`)
indent() indent()
// function mode const declarations should be inside with block // function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties // also they should be renamed to avoid collision with user properties
@ -234,35 +222,39 @@ export function generate(
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`) .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue` .join(', ')} } = _Vue`
) )
newline() push(`\n`)
if (ast.cached > 0) {
push(`const _cache = $cache`)
newline()
}
newline() newline()
} }
} else {
push(`const _ctx = this`)
if (ast.cached > 0) {
newline()
push(`const _cache = _ctx.$cache`)
}
newline()
} }
// generate asset resolution statements // generate asset resolution statements
if (ast.components.length) { if (ast.components.length) {
genAssets(ast.components, 'component', context) genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
} }
if (ast.directives.length) { if (ast.directives.length) {
genAssets(ast.directives, 'directive', context) genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
} }
if (ast.components.length || ast.directives.length) { if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline() newline()
} }
// generate the VNode tree expression // generate the VNode tree expression
push(`return `) if (!ssr) {
push(`return `)
}
if (ast.codegenNode) { if (ast.codegenNode) {
genNode(ast.codegenNode, context) genNode(ast.codegenNode, context)
} else { } else {
@ -276,27 +268,164 @@ export function generate(
deindent() deindent()
push(`}`) push(`}`)
if (genScopeId && !ssr) {
push(`)`)
}
return { return {
ast, ast,
code: context.code, code: context.code,
map: context.map ? context.map.toJSON() : undefined // SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
} }
} }
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
const VueBinding =
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
genHoists(ast.hoists, context)
newline()
push(`return `)
}
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean
) {
const {
push,
helper,
newline,
scopeId,
optimizeBindings,
runtimeModuleName
} = context
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
// generate import statements for helpers
if (ast.helpers.length) {
if (optimizeBindings) {
// when bundled with webpack with code-split, calling an import binding
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
// incurring both payload size increase and potential perf overhead.
// therefore we assign the imports to vairables (which is a constant ~50b
// cost per-component instead of scaling with template size)
push(
`import { ${ast.helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
push(
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
)
} else {
push(
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
}
if (ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "@vue/server-renderer"\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const _withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
}
function genAssets( function genAssets(
assets: string[], assets: string[],
type: 'component' | 'directive', type: 'component' | 'directive',
context: CodegenContext { helper, push, newline }: CodegenContext
) { ) {
const resolver = context.helper( const resolver = helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
) )
for (let i = 0; i < assets.length; i++) { for (let i = 0; i < assets.length; i++) {
const id = assets[i] const id = assets[i]
context.push( push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})` `const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
) )
context.newline() if (i < assets.length - 1) {
newline()
}
} }
} }
@ -304,12 +433,27 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
if (!hoists.length) { if (!hoists.length) {
return return
} }
context.newline() const { push, newline, helper, scopeId, mode } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline()
// push scope Id before initilaizing hoisted vnodes so that these vnodes
// get the proper scopeId as well.
if (genScopeId) {
push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`)
newline()
}
hoists.forEach((exp, i) => { hoists.forEach((exp, i) => {
context.push(`const _hoisted_${i + 1} = `) push(`const _hoisted_${i + 1} = `)
genNode(exp, context) genNode(exp, context)
context.newline() newline()
}) })
if (genScopeId) {
push(`${helper(POP_SCOPE_ID)}()`)
newline()
}
} }
function genImports(importsOptions: ImportItem[], context: CodegenContext) { function genImports(importsOptions: ImportItem[], context: CodegenContext) {
@ -351,7 +495,8 @@ function genNodeListAsArray(
function genNodeList( function genNodeList(
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[], nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext, context: CodegenContext,
multilines: boolean = false multilines: boolean = false,
comma: boolean = true
) { ) {
const { push, newline } = context const { push, newline } = context
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
@ -365,10 +510,10 @@ function genNodeList(
} }
if (i < nodes.length - 1) { if (i < nodes.length - 1) {
if (multilines) { if (multilines) {
push(',') comma && push(',')
newline() newline()
} else { } else {
push(', ') comma && push(', ')
} }
} }
} }
@ -413,6 +558,10 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
genComment(node, context) genComment(node, context)
break break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION: case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context) genCallExpression(node, context)
break break
@ -425,16 +574,37 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_FUNCTION_EXPRESSION: case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context) genFunctionExpression(node, context)
break break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
genSequenceExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION: case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context) genConditionalExpression(node, context)
break break
case NodeTypes.JS_CACHE_EXPRESSION: case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context) genCacheExpression(node, context)
break break
// SSR only types
case NodeTypes.JS_BLOCK_STATEMENT:
!__BROWSER__ && genNodeList(node.body, context, true, false)
break
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
case NodeTypes.JS_IF_STATEMENT:
!__BROWSER__ && genIfStatement(node, context)
break
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context)
break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
!__BROWSER__ && genSequenceExpression(node, context)
break
case NodeTypes.JS_RETURN_STATEMENT:
!__BROWSER__ && genReturnStatement(node, context)
break
/* istanbul ignore next */ /* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default: default:
if (__DEV__) { if (__DEV__) {
assert(false, `unhandled codegen node type: ${(node as any).type}`) assert(false, `unhandled codegen node type: ${(node as any).type}`)
@ -459,7 +629,7 @@ function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
function genInterpolation(node: InterpolationNode, context: CodegenContext) { function genInterpolation(node: InterpolationNode, context: CodegenContext) {
const { push, helper } = context const { push, helper } = context
push(`${helper(TO_STRING)}(`) push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context) genNode(node.content, context)
push(`)`) push(`)`)
} }
@ -505,18 +675,60 @@ function genComment(node: CommentNode, context: CodegenContext) {
} }
} }
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
isForBlock
} = node
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${isForBlock ? `true` : ``}), `)
}
push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
function genNullableArgs(args: any[]): CallExpression['arguments'] {
let i = args.length
while (i--) {
if (args[i] != null) break
}
return args.slice(0, i + 1).map(arg => arg || `null`)
}
// JavaScript // JavaScript
function genCallExpression(node: CallExpression, context: CodegenContext) { function genCallExpression(node: CallExpression, context: CodegenContext) {
const callee = isString(node.callee) const callee = isString(node.callee)
? node.callee ? node.callee
: context.helper(node.callee) : context.helper(node.callee)
context.push(callee + `(`, node, true) context.push(callee + `(`, node)
genNodeList(node.arguments, context) genNodeList(node.arguments, context)
context.push(`)`) context.push(`)`)
} }
function genObjectExpression(node: ObjectExpression, context: CodegenContext) { function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline, resetMapping } = context const { push, indent, deindent, newline } = context
const { properties } = node const { properties } = node
if (!properties.length) { if (!properties.length) {
push(`{}`, node) push(`{}`, node)
@ -529,8 +741,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
push(multilines ? `{` : `{ `) push(multilines ? `{` : `{ `)
multilines && indent() multilines && indent()
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const { key, value, loc } = properties[i] const { key, value } = properties[i]
resetMapping(loc) // reset source mapping for every property.
// key // key
genExpressionAsPropertyKey(key, context) genExpressionAsPropertyKey(key, context)
push(`: `) push(`: `)
@ -554,8 +765,17 @@ function genFunctionExpression(
node: FunctionExpression, node: FunctionExpression,
context: CodegenContext context: CodegenContext
) { ) {
const { push, indent, deindent } = context const { push, indent, deindent, scopeId, mode } = context
const { params, returns, newline } = node const { params, returns, body, newline, isSlot } = node
// slot functions also need to push scopeId before rendering its content
const genScopeId =
!__BROWSER__ && isSlot && scopeId != null && mode !== 'function'
if (genScopeId) {
push(`_withId(`)
} else if (isSlot) {
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node) push(`(`, node)
if (isArray(params)) { if (isArray(params)) {
genNodeList(params, context) genNodeList(params, context)
@ -563,27 +783,36 @@ function genFunctionExpression(
genNode(params, context) genNode(params, context)
} }
push(`) => `) push(`) => `)
if (newline) { if (newline || body) {
push(`{`) push(`{`)
indent() indent()
push(`return `)
} }
if (isArray(returns)) { if (returns) {
genNodeListAsArray(returns, context) if (newline) {
} else { push(`return `)
genNode(returns, context) }
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
} else if (body) {
genNode(body, context)
} }
if (newline) { if (newline || body) {
deindent() deindent()
push(`}`) push(`}`)
} }
if (genScopeId || isSlot) {
push(`)`)
}
} }
function genConditionalExpression( function genConditionalExpression(
node: ConditionalExpression, node: ConditionalExpression,
context: CodegenContext context: CodegenContext
) { ) {
const { test, consequent, alternate } = node const { test, consequent, alternate, newline: needNewline } = node
const { push, indent, deindent, newline } = context const { push, indent, deindent, newline } = context
if (test.type === NodeTypes.SIMPLE_EXPRESSION) { if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
const needsParens = !isSimpleIdentifier(test.content) const needsParens = !isSimpleIdentifier(test.content)
@ -592,15 +821,17 @@ function genConditionalExpression(
needsParens && push(`)`) needsParens && push(`)`)
} else { } else {
push(`(`) push(`(`)
genCompoundExpression(test, context) genNode(test, context)
push(`)`) push(`)`)
} }
indent() needNewline && indent()
context.indentLevel++ context.indentLevel++
needNewline || push(` `)
push(`? `) push(`? `)
genNode(consequent, context) genNode(consequent, context)
context.indentLevel-- context.indentLevel--
newline() needNewline && newline()
needNewline || push(` `)
push(`: `) push(`: `)
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
if (!isNested) { if (!isNested) {
@ -610,16 +841,7 @@ function genConditionalExpression(
if (!isNested) { if (!isNested) {
context.indentLevel-- context.indentLevel--
} }
deindent(true /* without newline */) needNewline && deindent(true /* without newline */)
}
function genSequenceExpression(
node: SequenceExpression,
context: CodegenContext
) {
context.push(`(`)
genNodeList(node.expressions, context)
context.push(`)`)
} }
function genCacheExpression(node: CacheExpression, context: CodegenContext) { function genCacheExpression(node: CacheExpression, context: CodegenContext) {
@ -642,3 +864,77 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
} }
push(`)`) push(`)`)
} }
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
const { push, indent, deindent } = context
push('`')
const l = node.elements.length
const multilines = l > 3
for (let i = 0; i < l; i++) {
const e = node.elements[i]
if (isString(e)) {
push(e.replace(/`/g, '\\`'))
} else {
push('${')
if (multilines) indent()
genNode(e, context)
if (multilines) deindent()
push('}')
}
}
push('`')
}
function genIfStatement(node: IfStatement, context: CodegenContext) {
const { push, indent, deindent } = context
const { test, consequent, alternate } = node
push(`if (`)
genNode(test, context)
push(`) {`)
indent()
genNode(consequent, context)
deindent()
push(`}`)
if (alternate) {
push(` else `)
if (alternate.type === NodeTypes.JS_IF_STATEMENT) {
genIfStatement(alternate, context)
} else {
push(`{`)
indent()
genNode(alternate, context)
deindent()
push(`}`)
}
}
}
function genAssignmentExpression(
node: AssignmentExpression,
context: CodegenContext
) {
genNode(node.left, context)
context.push(` = `)
genNode(node.right, context)
}
function genSequenceExpression(
node: SequenceExpression,
context: CodegenContext
) {
context.push(`(`)
genNodeList(node.expressions, context)
context.push(`)`)
}
function genReturnStatement(
{ returns }: ReturnStatement,
context: CodegenContext
) {
context.push(`return `)
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
}

View File

@ -0,0 +1,100 @@
import { CompilerOptions } from './options'
import { baseParse } from './parse'
import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type TransformPreset = [
NodeTransform[],
Record<string, DirectiveTransform>
]
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformFor,
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
if (__BROWSER__) {
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (isModuleMode) {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
if (!prefixIdentifiers && options.cacheHandlers) {
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
...directiveTransforms,
...(options.directiveTransforms || {}) // user transforms
}
})
return generate(ast, {
...options,
prefixIdentifiers
})
}

View File

@ -16,11 +16,14 @@ export function defaultOnError(error: CompilerError) {
export function createCompilerError<T extends number>( export function createCompilerError<T extends number>(
code: T, code: T,
loc?: SourceLocation, loc?: SourceLocation,
messages?: { [code: number]: string } messages?: { [code: number]: string },
additionalMessage?: string
): T extends ErrorCodes ? CoreCompilerError : CompilerError { ): T extends ErrorCodes ? CoreCompilerError : CompilerError {
const msg = __DEV__ || !__BROWSER__ ? (messages || errorMessages)[code] : code const msg =
const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : `` __DEV__ || !__BROWSER__
const error = new SyntaxError(msg + locInfo) as CompilerError ? (messages || errorMessages)[code] + (additionalMessage || ``)
: code
const error = new SyntaxError(String(msg)) as CompilerError
error.code = code error.code = code
error.loc = loc error.loc = loc
return error as any return error as any
@ -58,7 +61,6 @@ export const enum ErrorCodes {
UNEXPECTED_NULL_CHARACTER, UNEXPECTED_NULL_CHARACTER,
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME, UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG, UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE,
// Vue-specific parse errors // Vue-specific parse errors
X_INVALID_END_TAG, X_INVALID_END_TAG,
@ -74,19 +76,21 @@ export const enum ErrorCodes {
X_V_BIND_NO_EXPRESSION, X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION, X_V_ON_NO_EXPRESSION,
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
X_V_SLOT_MIXED_SLOT_USAGE, X_V_SLOT_MIXED_SLOT_USAGE,
X_V_SLOT_DUPLICATE_SLOT_NAMES, X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN, X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
X_V_SLOT_MISPLACED, X_V_SLOT_MISPLACED,
X_V_MODEL_NO_EXPRESSION, X_V_MODEL_NO_EXPRESSION,
X_V_MODEL_MALFORMED_EXPRESSION, X_V_MODEL_MALFORMED_EXPRESSION,
X_V_MODEL_ON_SCOPE_VARIABLE, X_V_MODEL_ON_SCOPE_VARIABLE,
X_INVALID_EXPRESSION, X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
// generic errors // generic errors
X_PREFIX_ID_NOT_SUPPORTED, X_PREFIX_ID_NOT_SUPPORTED,
X_MODULE_MODE_NOT_SUPPORTED, X_MODULE_MODE_NOT_SUPPORTED,
X_CACHE_HANDLER_NOT_SUPPORTED,
X_SCOPE_ID_NOT_SUPPORTED,
// Special value for higher-order compilers to pick up the last code // Special value for higher-order compilers to pick up the last code
// to avoid collision of error codes. This should always be kept as the last // to avoid collision of error codes. This should always be kept as the last
@ -140,11 +144,10 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]: [ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.", "'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.", [ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
// Vue-specific parse errors // Vue-specific parse errors
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.', [ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.', [ErrorCodes.X_MISSING_END_TAG]: 'Element is missing end tag.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]: [ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.', 'Interpolation end sign was not found.',
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]: [ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
@ -159,24 +162,24 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`, [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`, [ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`, [ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
[ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT]:
`Named v-slot on component. ` +
`Named slots should use <template v-slot> syntax nested inside the component.`,
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]: [ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
`Mixed v-slot usage on both the component and nested <template>.` + `Mixed v-slot usage on both the component and nested <template>.` +
`The default slot should also use <template> syntax when there are other ` + `When there are multiple named slots, all slots should use <template> ` +
`named slots to avoid scope ambiguity.`, `syntax to avoid scope ambiguity.`,
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `, [ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]: [ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` + `Extraneous children found when component already has explicitly named ` +
`These children will be ignored.`, `default slot. These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`, [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`, [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`, [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`, [ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Invalid JavaScript expression.`, [ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
// generic errors // generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.` [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
[ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`,
[ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.`
} }

View File

@ -1,90 +1,20 @@
import { CompilerOptions } from './options' export { baseCompile } from './compile'
import { parse } from './parse'
import { transform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
// we name it `baseCompile` so that higher order compilers like @vue/compiler-dom
// can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
/* istanbul ignore if */
if (__BROWSER__) {
const onError = options.onError || defaultOnError
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (options.mode === 'module') {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const ast = isString(template) ? parse(template, options) : template
const prefixIdentifiers =
!__BROWSER__ &&
(options.prefixIdentifiers === true || options.mode === 'module')
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
transformOnce,
transformIf,
transformFor,
...(prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
on: transformOn,
bind: transformBind,
model: transformModel,
...(options.directiveTransforms || {}) // user transforms
}
})
return generate(ast, {
...options,
prefixIdentifiers
})
}
// Also expose lower level APIs & types // Also expose lower level APIs & types
export { export {
CompilerOptions, CompilerOptions,
ParserOptions, ParserOptions,
TransformOptions, TransformOptions,
CodegenOptions CodegenOptions,
HoistTransform
} from './options' } from './options'
export { parse, TextModes } from './parse' export { baseParse, TextModes } from './parse'
export { export {
transform, transform,
createStructuralDirectiveTransform,
TransformContext, TransformContext,
createTransformContext,
traverseNode,
createStructuralDirectiveTransform,
NodeTransform, NodeTransform,
StructuralDirectiveTransform, StructuralDirectiveTransform,
DirectiveTransform DirectiveTransform
@ -96,19 +26,32 @@ export {
CompilerError, CompilerError,
createCompilerError createCompilerError
} from './errors' } from './errors'
export * from './ast' export * from './ast'
export * from './utils' export * from './utils'
export { registerRuntimeHelpers } from './runtimeHelpers' export * from './runtimeHelpers'
// expose transforms so higher-order compilers can import and extend them export { getBaseTransformPreset, TransformPreset } from './compile'
export { transformModel } from './transforms/vModel' export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn' export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind'
// utility, but need to rewrite typing to avoid dts relying on @vue/shared export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
import { generateCodeFrame as _genCodeFrame } from '@vue/shared' export { processIf } from './transforms/vIf'
const generateCodeFrame = _genCodeFrame as ( export { processFor, createForLoopParams } from './transforms/vFor'
source: string, export {
start?: number, transformExpression,
end?: number processExpression
) => string } from './transforms/transformExpression'
export { generateCodeFrame } export {
buildSlots,
SlotFnBuilder,
trackVForSlotScopes,
trackSlotScopes
} from './transforms/vSlot'
export {
transformElement,
resolveComponentType,
buildProps
} from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { generateCodeFrame } from '@vue/shared'

View File

@ -1,16 +1,30 @@
import { ElementNode, Namespace } from './ast' import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
import { TextModes } from './parse' import { TextModes } from './parse'
import { CompilerError } from './errors' import { CompilerError } from './errors'
import { NodeTransform, DirectiveTransform } from './transform' import {
NodeTransform,
DirectiveTransform,
TransformContext
} from './transform'
import { ParserPlugin } from '@babel/parser'
export interface ParserOptions { export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr // e.g. platform native elements, e.g. <div> for browsers
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex isNativeTag?: (tag: string) => boolean
isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact // e.g. native elements that can self-close, e.g. <img>, <br>, <hr>
isCustomElement?: (tag: string) => boolean isVoidTag?: (tag: string) => boolean
// e.g. elements that should preserve whitespace inside, e.g. <pre>
isPreTag?: (tag: string) => boolean
// platform-specific built-in components e.g. <Transition>
isBuiltInComponent?: (tag: string) => symbol | void isBuiltInComponent?: (tag: string) => symbol | void
// separate option for end users to extend the native elements list
isCustomElement?: (tag: string) => boolean
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
getTextMode?: (tag: string, ns: Namespace) => TextModes getTextMode?: (
tag: string,
ns: Namespace,
parent: ElementNode | undefined
) => TextModes
delimiters?: [string, string] // ['{{', '}}'] delimiters?: [string, string] // ['{{', '}}']
// Map to HTML entities. E.g., `{ "amp;": "&" }` // Map to HTML entities. E.g., `{ "amp;": "&" }`
@ -19,27 +33,46 @@ export interface ParserOptions {
// this number is based on the map above, but it should be pre-computed // this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call. // to avoid the cost on every parse() call.
maxCRNameLength?: number maxCRNameLength?: number
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
export type HoistTransform = (
node: PlainElementNode,
context: TransformContext
) => JSChildNode
export interface TransformOptions { export interface TransformOptions {
nodeTransforms?: NodeTransform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform } directiveTransforms?: Record<string, DirectiveTransform | undefined>
// an optional hook to transform a node being hoisted.
// used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
transformHoist?: HoistTransform | null
isBuiltInComponent?: (tag: string) => symbol | void isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`. // Transform expressions like {{ foo }} to `_ctx.foo`.
// Default: mode === 'module' // If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// - This is force-enabled in module mode, since modules are by default strict
// and cannot use `with`
// - Default: mode === 'module'
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
// Hoist static VNodes and props objects to `_hoisted_x` constants // Hoist static VNodes and props objects to `_hoisted_x` constants
// Default: false // - Default: false
hoistStatic?: boolean hoistStatic?: boolean
// Cache v-on handlers to avoid creating new inline functions on each render, // Cache v-on handlers to avoid creating new inline functions on each render,
// also avoids the need for dynamically patching the handlers by wrapping it. // also avoids the need for dynamically patching the handlers by wrapping it.
// e.g `@click="foo"` by default is compiled to `{ onClick: foo }`. With this // e.g `@click="foo"` by default is compiled to `{ onClick: foo }`. With this
// option it's compiled to: // option it's compiled to:
// `{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }` // `{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }`
// Default: false // - Requires "prefixIdentifiers" to be enabled because it relies on scope
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean cacheHandlers?: boolean
// a list of parser plugins to enable for @babel/parser
// https://babeljs.io/docs/en/next/babel-parser#plugins
expressionPlugins?: ParserPlugin[]
// SFC scoped styles ID
scopeId?: string | null
ssr?: boolean
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -49,19 +82,26 @@ export interface CodegenOptions {
// - Function mode will generate a single `const { helpers... } = Vue` // - Function mode will generate a single `const { helpers... } = Vue`
// statement and return the render function. It is meant to be used with // statement and return the render function. It is meant to be used with
// `new Function(code)()` to generate a render function at runtime. // `new Function(code)()` to generate a render function at runtime.
// Default: 'function' // - Default: 'function'
mode?: 'module' | 'function' mode?: 'module' | 'function'
// Prefix suitable identifiers with _ctx.
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// Default: false
prefixIdentifiers?: boolean
// Generate source map? // Generate source map?
// Default: false // - Default: false
sourceMap?: boolean sourceMap?: boolean
// Filename for source map generation. // Filename for source map generation.
// Default: `template.vue.html` // - Default: `template.vue.html`
filename?: string filename?: string
// SFC scoped styles ID
scopeId?: string | null
// we need to know about this to generate proper preambles
prefixIdentifiers?: boolean
// option to optimize helper import bindings via variable assignment
// (only used for webpack code-split)
optimizeBindings?: boolean
// for specifying where to import helpers
runtimeModuleName?: string
runtimeGlobalName?: string
// generate ssr-specific code?
ssr?: boolean
} }
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

View File

@ -1,5 +1,5 @@
import { ParserOptions } from './options' import { ParserOptions } from './options'
import { NO, isArray } from '@vue/shared' import { NO, isArray, makeMap } from '@vue/shared'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors' import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { import {
assert, assert,
@ -21,11 +21,11 @@ import {
SourceLocation, SourceLocation,
TextNode, TextNode,
TemplateChildNode, TemplateChildNode,
InterpolationNode InterpolationNode,
createRoot
} from './ast' } from './ast'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
// `isNativeTag` is optional, others are required
type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent' type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> & type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
Pick<ParserOptions, OptionalOptions> Pick<ParserOptions, OptionalOptions>
@ -64,25 +64,20 @@ interface ParserContext {
offset: number offset: number
line: number line: number
column: number column: number
inPre: boolean inPre: boolean // HTML <pre> tag, preserve whitespaces
inVPre: boolean // v-pre, do not process directives and interpolations
} }
export function parse(content: string, options: ParserOptions = {}): RootNode { export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options) const context = createParserContext(content, options)
const start = getCursor(context) const start = getCursor(context)
return createRoot(
return { parseChildren(context, TextModes.DATA, []),
type: NodeTypes.ROOT, getSelection(context, start)
children: parseChildren(context, TextModes.DATA, []), )
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
codegenNode: undefined,
loc: getSelection(context, start)
}
} }
function createParserContext( function createParserContext(
@ -99,7 +94,8 @@ function createParserContext(
offset: 0, offset: 0,
originalSource: content, originalSource: content,
source: content, source: content,
inPre: false inPre: false,
inVPre: false
} }
} }
@ -117,11 +113,11 @@ function parseChildren(
const s = context.source const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA) { if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inPre && startsWith(s, context.options.delimiters[0])) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{' // '{{'
node = parseInterpolation(context, mode) node = parseInterpolation(context, mode)
} else if (s[0] === '<') { } else if (mode === TextModes.DATA && s[0] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) { if (s.length === 1) {
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1) emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
@ -193,45 +189,51 @@ function parseChildren(
// Whitespace management for more efficient output // Whitespace management for more efficient output
// (same as v2 whitespace: 'condense') // (same as v2 whitespace: 'condense')
let removedWhitespace = false let removedWhitespace = false
if ( if (mode !== TextModes.RAWTEXT) {
mode !== TextModes.RAWTEXT && if (!context.inPre) {
(!parent || !context.options.isPreTag(parent.tag)) for (let i = 0; i < nodes.length; i++) {
) { const node = nodes[i]
for (let i = 0; i < nodes.length; i++) { if (node.type === NodeTypes.TEXT) {
const node = nodes[i] if (!node.content.trim()) {
if (node.type === NodeTypes.TEXT) { const prev = nodes[i - 1]
if (!node.content.trim()) { const next = nodes[i + 1]
const prev = nodes[i - 1] // If:
const next = nodes[i + 1] // - the whitespace is the first or last node, or:
// If: // - the whitespace is adjacent to a comment, or:
// - the whitespace is the first or last node, or: // - the whitespace is between two elements AND contains newline
// - the whitespace is adjacent to a comment, or: // Then the whitespace is ignored.
// - the whitespace is between two elements AND contains newline if (
// Then the whitespace is ignored. !prev ||
if ( !next ||
!prev || prev.type === NodeTypes.COMMENT ||
!next || next.type === NodeTypes.COMMENT ||
prev.type === NodeTypes.COMMENT || (prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.COMMENT || next.type === NodeTypes.ELEMENT &&
(prev.type === NodeTypes.ELEMENT && /[\r\n]/.test(node.content))
next.type === NodeTypes.ELEMENT && ) {
/[\r\n]/.test(node.content)) removedWhitespace = true
) { nodes[i] = null as any
removedWhitespace = true } else {
nodes[i] = null as any // Otherwise, condensed consecutive whitespace inside the text down to
// a single space
node.content = ' '
}
} else { } else {
// Otherwise, condensed consecutive whitespace inside the text down to node.content = node.content.replace(/\s+/g, ' ')
// a single space
node.content = ' '
} }
} else {
node.content = node.content.replace(/\s+/g, ' ')
} }
} }
} else {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
} }
} }
return removedWhitespace ? nodes.filter(node => node !== null) : nodes return removedWhitespace ? nodes.filter(Boolean) : nodes
} }
function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void { function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
@ -353,9 +355,11 @@ function parseElement(
// Start tag. // Start tag.
const wasInPre = context.inPre const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors) const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent) const element = parseTag(context, TagType.Start, parent)
const isPreBoundary = context.inPre && !wasInPre const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element return element
@ -363,7 +367,7 @@ function parseElement(
// Children. // Children.
ancestors.push(element) ancestors.push(element)
const mode = context.options.getTextMode(element.tag, element.ns) const mode = context.options.getTextMode(element.tag, element.ns, parent)
const children = parseChildren(context, mode, ancestors) const children = parseChildren(context, mode, ancestors)
ancestors.pop() ancestors.pop()
@ -373,7 +377,7 @@ function parseElement(
if (startsWithEndTagOpen(context.source, element.tag)) { if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent) parseTag(context, TagType.End, parent)
} else { } else {
emitError(context, ErrorCodes.X_MISSING_END_TAG) emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') { if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
const first = children[0] const first = children[0]
if (first && startsWith(first.loc.source, '<!--')) { if (first && startsWith(first.loc.source, '<!--')) {
@ -387,6 +391,9 @@ function parseElement(
if (isPreBoundary) { if (isPreBoundary) {
context.inPre = false context.inPre = false
} }
if (isVPreBoundary) {
context.inVPre = false
}
return element return element
} }
@ -395,6 +402,10 @@ const enum TagType {
End End
} }
const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
`if,else,else-if,for,slot`
)
/** /**
* Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag). * Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
*/ */
@ -425,12 +436,17 @@ function parseTag(
// Attributes. // Attributes.
let props = parseAttributes(context, type) let props = parseAttributes(context, type)
// check <pre> tag
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// check v-pre // check v-pre
if ( if (
!context.inPre && !context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre') props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) { ) {
context.inPre = true context.inVPre = true
// reset context // reset context
extend(context, cursor) extend(context, cursor)
context.source = currentSource context.source = currentSource
@ -452,20 +468,32 @@ function parseTag(
let tagType = ElementTypes.ELEMENT let tagType = ElementTypes.ELEMENT
const options = context.options const options = context.options
if (!context.inPre && !options.isCustomElement(tag)) { if (!context.inVPre && !options.isCustomElement(tag)) {
if (options.isNativeTag) { const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
)
if (options.isNativeTag && !hasVIs) {
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if ( } else if (
hasVIs ||
isCoreComponent(tag) || isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) /^[A-Z]/.test(tag) ||
tag === 'component'
) { ) {
tagType = ElementTypes.COMPONENT tagType = ElementTypes.COMPONENT
} }
if (tag === 'slot') { if (tag === 'slot') {
tagType = ElementTypes.SLOT tagType = ElementTypes.SLOT
} else if (tag === 'template') { } else if (
tag === 'template' &&
props.some(p => {
return (
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
})
) {
tagType = ElementTypes.TEMPLATE tagType = ElementTypes.TEMPLATE
} }
} }
@ -539,7 +567,7 @@ function parseAttribute(
{ {
const pattern = /["'<]/g const pattern = /["'<]/g
let m: RegExpExecArray | null let m: RegExpExecArray | null
while ((m = pattern.exec(name)) !== null) { while ((m = pattern.exec(name))) {
emitError( emitError(
context, context,
ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME, ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
@ -570,7 +598,7 @@ function parseAttribute(
} }
const loc = getSelection(context, start) const loc = getSelection(context, start)
if (!context.inPre && /^(v-|:|@|#)/.test(name)) { if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec( const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
name name
)! )!
@ -688,9 +716,9 @@ function parseAttributeValue(
if (!match) { if (!match) {
return undefined return undefined
} }
let unexpectedChars = /["'<=`]/g const unexpectedChars = /["'<=`]/g
let m: RegExpExecArray | null let m: RegExpExecArray | null
while ((m = unexpectedChars.exec(match[0])) !== null) { while ((m = unexpectedChars.exec(match[0]))) {
emitError( emitError(
context, context,
ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE, ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
@ -818,8 +846,8 @@ function parseTextData(
if (head[0] === '&') { if (head[0] === '&') {
// Named character reference. // Named character reference.
let name = '', let name = ''
value: string | undefined = undefined let value: string | undefined = undefined
if (/[0-9a-z]/i.test(rawText[1])) { if (/[0-9a-z]/i.test(rawText[1])) {
for ( for (
let length = context.options.maxCRNameLength; let length = context.options.maxCRNameLength;
@ -834,7 +862,7 @@ function parseTextData(
if ( if (
mode === TextModes.ATTRIBUTE_VALUE && mode === TextModes.ATTRIBUTE_VALUE &&
!semi && !semi &&
/[=a-z0-9]/i.test(rawText[1 + name.length] || '') /[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) { ) {
decodedText += '&' + name decodedText += '&' + name
advance(1 + name.length) advance(1 + name.length)
@ -849,7 +877,6 @@ function parseTextData(
} }
} }
} else { } else {
emitError(context, ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE)
decodedText += '&' + name decodedText += '&' + name
advance(1 + name.length) advance(1 + name.length)
} }
@ -964,9 +991,9 @@ function getNewPosition(
function emitError( function emitError(
context: ParserContext, context: ParserContext,
code: ErrorCodes, code: ErrorCodes,
offset?: number offset?: number,
loc: Position = getCursor(context)
): void { ): void {
const loc = getCursor(context)
if (offset) { if (offset) {
loc.offset += offset loc.offset += offset
loc.column += offset loc.column += offset

View File

@ -1,5 +1,5 @@
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``) export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
export const PORTAL = Symbol(__DEV__ ? `Portal` : ``) export const TELEPORT = Symbol(__DEV__ ? `Teleport` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``) export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``) export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``) export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``) export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``) export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``) export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``) export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
export const RESOLVE_DYNAMIC_COMPONENT = Symbol( export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
__DEV__ ? `resolveDynamicComponent` : `` __DEV__ ? `resolveDynamicComponent` : ``
@ -17,18 +18,22 @@ export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``)
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``) export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``) export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``)
export const CREATE_SLOTS = Symbol(__DEV__ ? `createSlots` : ``) export const CREATE_SLOTS = Symbol(__DEV__ ? `createSlots` : ``)
export const TO_STRING = Symbol(__DEV__ ? `toString` : ``) export const TO_DISPLAY_STRING = Symbol(__DEV__ ? `toDisplayString` : ``)
export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``) export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``) export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in // Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime! // generated code. Make sure these are correctly exported in the runtime!
// Using `any` here because TS doesn't allow symbols as index type. // Using `any` here because TS doesn't allow symbols as index type.
export const helperNameMap: any = { export const helperNameMap: any = {
[FRAGMENT]: `Fragment`, [FRAGMENT]: `Fragment`,
[PORTAL]: `Portal`, [TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`, [SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`, [KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`, [BASE_TRANSITION]: `BaseTransition`,
@ -37,6 +42,7 @@ export const helperNameMap: any = {
[CREATE_VNODE]: `createVNode`, [CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`, [CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`, [CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`, [RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`, [RESOLVE_DIRECTIVE]: `resolveDirective`,
@ -44,11 +50,15 @@ export const helperNameMap: any = {
[RENDER_LIST]: `renderList`, [RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`, [RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`, [CREATE_SLOTS]: `createSlots`,
[TO_STRING]: `toString`, [TO_DISPLAY_STRING]: `toDisplayString`,
[MERGE_PROPS]: `mergeProps`, [MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`, [TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`, [CAMELIZE]: `camelize`,
[SET_BLOCK_TRACKING]: `setBlockTracking` [SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`
} }
export function registerRuntimeHelpers(helpers: any) { export function registerRuntimeHelpers(helpers: any) {

View File

@ -12,11 +12,10 @@ import {
JSChildNode, JSChildNode,
SimpleExpressionNode, SimpleExpressionNode,
ElementTypes, ElementTypes,
ElementCodegenNode,
ComponentCodegenNode,
createCallExpression,
CacheExpression, CacheExpression,
createCacheExpression createCacheExpression,
TemplateLiteral,
createVNodeCall
} from './ast' } from './ast'
import { import {
isString, isString,
@ -27,14 +26,14 @@ import {
} from '@vue/shared' } from '@vue/shared'
import { defaultOnError } from './errors' import { defaultOnError } from './errors'
import { import {
TO_STRING, TO_DISPLAY_STRING,
FRAGMENT, FRAGMENT,
helperNameMap, helperNameMap,
WITH_DIRECTIVES,
CREATE_BLOCK, CREATE_BLOCK,
CREATE_COMMENT CREATE_COMMENT,
OPEN_BLOCK
} from './runtimeHelpers' } from './runtimeHelpers'
import { isVSlot, createBlockExpression } from './utils' import { isVSlot } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic' import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
// There are two types of transforms: // There are two types of transforms:
@ -61,7 +60,8 @@ export type DirectiveTransform = (
export interface DirectiveTransformResult { export interface DirectiveTransformResult {
props: Property[] props: Property[]
needRuntime: boolean | symbol needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
} }
// A structural directive transform is a technically a NodeTransform; // A structural directive transform is a technically a NodeTransform;
@ -84,6 +84,7 @@ export interface TransformContext extends Required<TransformOptions> {
directives: Set<string> directives: Set<string>
hoists: JSChildNode[] hoists: JSChildNode[]
imports: Set<ImportItem> imports: Set<ImportItem>
temps: number
cached: number cached: number
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
scopes: { scopes: {
@ -106,7 +107,7 @@ export interface TransformContext extends Required<TransformOptions> {
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
} }
function createTransformContext( export function createTransformContext(
root: RootNode, root: RootNode,
{ {
prefixIdentifiers = false, prefixIdentifiers = false,
@ -114,17 +115,36 @@ function createTransformContext(
cacheHandlers = false, cacheHandlers = false,
nodeTransforms = [], nodeTransforms = [],
directiveTransforms = {}, directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP, isBuiltInComponent = NOOP,
expressionPlugins = [],
scopeId = null,
ssr = false,
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
const context: TransformContext = { const context: TransformContext = {
// options
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
expressionPlugins,
scopeId,
ssr,
onError,
// state
root, root,
helpers: new Set(), helpers: new Set(),
components: new Set(), components: new Set(),
directives: new Set(), directives: new Set(),
hoists: [], hoists: [],
imports: new Set(), imports: new Set(),
temps: 0,
cached: 0, cached: 0,
identifiers: {}, identifiers: {},
scopes: { scopes: {
@ -133,25 +153,17 @@ function createTransformContext(
vPre: 0, vPre: 0,
vOnce: 0 vOnce: 0
}, },
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
onError,
parent: null, parent: null,
currentNode: root, currentNode: root,
childIndex: 0, childIndex: 0,
// methods
helper(name) { helper(name) {
context.helpers.add(name) context.helpers.add(name)
return name return name
}, },
helperString(name) { helperString(name) {
return ( return `_${helperNameMap[context.helper(name)]}`
(context.prefixIdentifiers ? `` : `_`) +
helperNameMap[context.helper(name)]
)
}, },
replaceNode(node) { replaceNode(node) {
/* istanbul ignore if */ /* istanbul ignore if */
@ -251,10 +263,20 @@ export function transform(root: RootNode, options: TransformOptions) {
if (options.hoistStatic) { if (options.hoistStatic) {
hoistStatic(root, context) hoistStatic(root, context)
} }
finalizeRoot(root, context) if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
} }
function finalizeRoot(root: RootNode, context: TransformContext) { function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context const { helper } = context
const { children } = root const { children } = root
const child = children[0] const child = children[0]
@ -263,20 +285,13 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
if (isSingleElementRoot(root, child) && child.codegenNode) { if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be // single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode // SimpleExpressionNode
const codegenNode = child.codegenNode as const codegenNode = child.codegenNode
| ElementCodegenNode if (codegenNode.type === NodeTypes.VNODE_CALL) {
| ComponentCodegenNode codegenNode.isBlock = true
| CacheExpression helper(OPEN_BLOCK)
if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) { helper(CREATE_BLOCK)
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
}
root.codegenNode = createBlockExpression(codegenNode, context)
} else {
root.codegenNode = codegenNode
} }
root.codegenNode = codegenNode
} else { } else {
// - single <slot/>, IfNode, ForNode: already blocks. // - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched. // - single text node: always patched.
@ -285,27 +300,21 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
} }
} else if (children.length > 1) { } else if (children.length > 1) {
// root has multiple nodes - return a fragment block. // root has multiple nodes - return a fragment block.
root.codegenNode = createBlockExpression( root.codegenNode = createVNodeCall(
createCallExpression(helper(CREATE_BLOCK), [ context,
helper(FRAGMENT), helper(FRAGMENT),
`null`, undefined,
root.children, root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${ `${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT] PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */` } */`,
]), undefined,
context undefined,
true
) )
} else { } else {
// no children = noop. codegen will return null. // no children = noop. codegen will return null.
} }
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
} }
export function traverseChildren( export function traverseChildren(
@ -319,7 +328,6 @@ export function traverseChildren(
for (; i < parent.children.length; i++) { for (; i < parent.children.length; i++) {
const child = parent.children[i] const child = parent.children[i]
if (isString(child)) continue if (isString(child)) continue
context.currentNode = child
context.parent = parent context.parent = parent
context.childIndex = i context.childIndex = i
context.onNodeRemoved = nodeRemoved context.onNodeRemoved = nodeRemoved
@ -331,6 +339,7 @@ export function traverseNode(
node: RootNode | TemplateChildNode, node: RootNode | TemplateChildNode,
context: TransformContext context: TransformContext
) { ) {
context.currentNode = node
// apply transform plugins // apply transform plugins
const { nodeTransforms } = context const { nodeTransforms } = context
const exitFns = [] const exitFns = []
@ -354,21 +363,26 @@ export function traverseNode(
switch (node.type) { switch (node.type) {
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating if (!context.ssr) {
// comment nodes with `createVNode` // inject import for the Comment symbol, which is needed for creating
context.helper(CREATE_COMMENT) // comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break break
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper // no need to traverse, but we need to inject toString helper
context.helper(TO_STRING) if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break break
// for container types, further traverse downwards // for container types, further traverse downwards
case NodeTypes.IF: case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) { for (let i = 0; i < node.branches.length; i++) {
traverseChildren(node.branches[i], context) traverseNode(node.branches[i], context)
} }
break break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR: case NodeTypes.FOR:
case NodeTypes.ELEMENT: case NodeTypes.ELEMENT:
case NodeTypes.ROOT: case NodeTypes.ROOT:

View File

@ -8,11 +8,9 @@ import {
ComponentNode, ComponentNode,
TemplateNode, TemplateNode,
ElementNode, ElementNode,
PlainElementCodegenNode, VNodeCall
CodegenNodeWithDirective
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags, isString, isSymbol } from '@vue/shared' import { PatchFlags, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet, findProp } from '../utils' import { isSlotOutlet, findProp } from '../utils'
@ -21,6 +19,8 @@ export function hoistStatic(root: RootNode, context: TransformContext) {
root.children, root.children,
context, context,
new Map(), new Map(),
// Root node is unfortuantely non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0]) isSingleElementRoot(root, root.children[0])
) )
} }
@ -52,13 +52,18 @@ function walk(
) { ) {
if (!doNotHoistNode && isStaticNode(child, resultCache)) { if (!doNotHoistNode && isStaticNode(child, resultCache)) {
// whole tree is static // whole tree is static
child.codegenNode = context.hoist(child.codegenNode!) ;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
const hoisted = context.transformHoist
? context.transformHoist(child, context)
: child.codegenNode!
child.codegenNode = context.hoist(hoisted)
continue continue
} else { } else {
// node may contain dynamic children, but its props may be eligible for // node may contain dynamic children, but its props may be eligible for
// hoisting. // hoisting.
const codegenNode = child.codegenNode! const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) { if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode) const flag = getPatchFlag(codegenNode)
if ( if (
(!flag || (!flag ||
@ -68,8 +73,8 @@ function walk(
!hasCachedProps(child) !hasCachedProps(child)
) { ) {
const props = getNodeProps(child) const props = getNodeProps(child)
if (props && props !== `null`) { if (props) {
getVNodeCall(codegenNode).arguments[1] = context.hoist(props) codegenNode.props = context.hoist(props)
} }
} }
} }
@ -86,6 +91,11 @@ function walk(
// Do not hoist v-if single child because it has to be a block // Do not hoist v-if single child because it has to be a block
walk(branchChildren, context, resultCache, branchChildren.length === 1) walk(branchChildren, context, resultCache, branchChildren.length === 1)
} }
} else if (
child.type === NodeTypes.TEXT_CALL &&
isStaticNode(child.content, resultCache)
) {
child.codegenNode = context.hoist(child.codegenNode)
} }
} }
} }
@ -104,7 +114,7 @@ export function isStaticNode(
return cached return cached
} }
const codegenNode = node.codegenNode! const codegenNode = node.codegenNode!
if (codegenNode.type !== NodeTypes.JS_CALL_EXPRESSION) { if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return false return false
} }
const flag = getPatchFlag(codegenNode) const flag = getPatchFlag(codegenNode)
@ -116,6 +126,12 @@ export function isStaticNode(
return false return false
} }
} }
// only svg/foreignObject could be block here, however if they are static
// then they don't need to be blocks since there will be no nested
// updates.
if (codegenNode.isBlock) {
codegenNode.isBlock = false
}
resultCache.set(node, true) resultCache.set(node, true)
return true return true
} else { } else {
@ -127,6 +143,7 @@ export function isStaticNode(
return true return true
case NodeTypes.IF: case NodeTypes.IF:
case NodeTypes.FOR: case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
return false return false
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL: case NodeTypes.TEXT_CALL:
@ -157,14 +174,20 @@ function hasCachedProps(node: PlainElementNode): boolean {
return false return false
} }
const props = getNodeProps(node) const props = getNodeProps(node)
if ( if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props &&
props !== 'null' &&
props.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
const { properties } = props const { properties } = props
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
if (properties[i].value.type === NodeTypes.JS_CACHE_EXPRESSION) { const val = properties[i].value
if (val.type === NodeTypes.JS_CACHE_EXPRESSION) {
return true
}
// merged event handlers
if (
val.type === NodeTypes.JS_ARRAY_EXPRESSION &&
val.elements.some(
e => !isString(e) && e.type === NodeTypes.JS_CACHE_EXPRESSION
)
) {
return true return true
} }
} }
@ -174,30 +197,12 @@ function hasCachedProps(node: PlainElementNode): boolean {
function getNodeProps(node: PlainElementNode) { function getNodeProps(node: PlainElementNode) {
const codegenNode = node.codegenNode! const codegenNode = node.codegenNode!
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) { if (codegenNode.type === NodeTypes.VNODE_CALL) {
return getVNodeArgAt( return codegenNode.props
codegenNode,
1
) as PlainElementCodegenNode['arguments'][1]
} }
} }
type NonCachedCodegenNode = function getPatchFlag(node: VNodeCall): number | undefined {
| PlainElementCodegenNode const flag = node.patchFlag
| CodegenNodeWithDirective<PlainElementCodegenNode>
function getVNodeArgAt(
node: NonCachedCodegenNode,
index: number
): PlainElementCodegenNode['arguments'][number] {
return getVNodeCall(node).arguments[index]
}
function getVNodeCall(node: NonCachedCodegenNode) {
return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node
}
function getPatchFlag(node: NonCachedCodegenNode): number | undefined {
const flag = getVNodeArgAt(node, 3) as string
return flag ? parseInt(flag, 10) : undefined return flag ? parseInt(flag, 10) : undefined
} }

View File

@ -0,0 +1,3 @@
import { DirectiveTransform } from '../transform'
export const noopDirectiveTransform: DirectiveTransform = () => ({ props: [] })

View File

@ -13,27 +13,31 @@ import {
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
createObjectExpression, createObjectExpression,
Property Property,
ComponentNode,
VNodeCall,
TemplateTextChildNode,
DirectiveArguments,
createVNodeCall
} from '../ast' } from '../ast'
import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared' import { PatchFlags, PatchFlagNames, isSymbol, isOn } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
CREATE_VNODE,
WITH_DIRECTIVES,
RESOLVE_DIRECTIVE, RESOLVE_DIRECTIVE,
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
RESOLVE_DYNAMIC_COMPONENT, RESOLVE_DYNAMIC_COMPONENT,
MERGE_PROPS, MERGE_PROPS,
TO_HANDLERS, TO_HANDLERS,
PORTAL, TELEPORT,
KEEP_ALIVE KEEP_ALIVE
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { import {
getInnerRange, getInnerRange,
isVSlot,
toValidAssetId, toValidAssetId,
findProp, findProp,
isCoreComponent isCoreComponent,
isBindKey,
findDir
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { isStaticNode } from './hoistStatic' import { isStaticNode } from './hoistStatic'
@ -45,107 +49,97 @@ const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
// generate a JavaScript AST for this element's codegen // generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => { export const transformElement: NodeTransform = (node, context) => {
if ( if (
node.type !== NodeTypes.ELEMENT || !(
// handled by transformSlotOutlet node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.SLOT || (node.tagType === ElementTypes.ELEMENT ||
// <template v-if/v-for> should have already been replaced node.tagType === ElementTypes.COMPONENT)
// <template v-slot> is handled by buildSlots )
(node.tagType === ElementTypes.TEMPLATE && node.props.some(isVSlot))
) { ) {
return return
} }
// perform the work on exit, after all child expressions have been // perform the work on exit, after all child expressions have been
// processed and merged. // processed and merged.
return function postTransformElement() { return function postTransformElement() {
const { tag, tagType, props } = node const { tag, props } = node
const builtInComponentSymbol = const isComponent = node.tagType === ElementTypes.COMPONENT
isCoreComponent(tag) || context.isBuiltInComponent(tag)
const isComponent = tagType === ElementTypes.COMPONENT
let hasProps = props.length > 0 // The goal of the transform is to create a codegenNode implementing the
// VNodeCall interface.
const vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0 let patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined let dynamicPropNames: string[] | undefined
let dynamicComponent: string | CallExpression | undefined let vnodeDirectives: VNodeCall['directives']
// handle dynamic component let shouldUseBlock =
const isProp = findProp(node, 'is') !isComponent &&
if (tag === 'component') { // <svg> and <foreignObject> must be forced into blocks so that block
if (isProp) { // updates inside get proper isSVG flag at runtime. (#639, #643)
// static <component is="foo" /> // This is technically web-specific, but splitting the logic out of core
if (isProp.type === NodeTypes.ATTRIBUTE) { // leads to too much unnecessary complexity.
const tag = isProp.value && isProp.value.content (tag === 'svg' ||
if (tag) { tag === 'foreignObject' ||
context.helper(RESOLVE_COMPONENT) // #938: elements with dynamic keys should be forced into blocks
context.components.add(tag) findProp(node, 'key', true))
dynamicComponent = toValidAssetId(tag, `component`)
} // props
} if (props.length > 0) {
// dynamic <component :is="asdf" /> const propsBuildResult = buildProps(node, context)
else if (isProp.exp) { vnodeProps = propsBuildResult.props
dynamicComponent = createCallExpression( patchFlag = propsBuildResult.patchFlag
context.helper(RESOLVE_DYNAMIC_COMPONENT), dynamicPropNames = propsBuildResult.dynamicPropNames
// _ctx.$ exposes the owner instance of current render function const directives = propsBuildResult.directives
[isProp.exp, context.prefixIdentifiers ? `_ctx.$` : `$`] vnodeDirectives =
directives && directives.length
? (createArrayExpression(
directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments)
: undefined
}
// children
if (node.children.length > 0) {
if (vnodeTag === KEEP_ALIVE) {
// Although a built-in component, we compile KeepAlive with raw children
// instead of slot functions so that it can be used inside Transition
// or other Transition-wrapping HOCs.
// To ensure correct updates with block optimizations, we need to:
// 1. Force keep-alive into a block. This avoids its children being
// collected by a parent block.
shouldUseBlock = true
// 2. Force keep-alive to always be updated, since it uses raw children.
patchFlag |= PatchFlags.DYNAMIC_SLOTS
if (__DEV__ && node.children.length > 1) {
context.onError(
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
})
) )
} }
} }
}
let nodeType const shouldBuildAsSlots =
if (dynamicComponent) {
nodeType = dynamicComponent
} else if (builtInComponentSymbol) {
nodeType = context.helper(builtInComponentSymbol)
} else if (isComponent) {
// user component w/ resolve
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
nodeType = toValidAssetId(tag, `component`)
} else {
// plain element
nodeType = `"${node.tag}"`
}
const args: CallExpression['arguments'] = [nodeType]
// props
if (hasProps) {
const propsBuildResult = buildProps(
node,
context,
// skip reserved "is" prop <component is>
isProp ? node.props.filter(p => p !== isProp) : node.props
)
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
runtimeDirectives = propsBuildResult.directives
if (!propsBuildResult.props) {
hasProps = false
} else {
args.push(propsBuildResult.props)
}
}
// children
const hasChildren = node.children.length > 0
if (hasChildren) {
if (!hasProps) {
args.push(`null`)
}
// Portal & KeepAlive should have normal children instead of slots
// Portal is not a real component has dedicated handling in the renderer
// KeepAlive should not track its own deps so that it can be used inside
// Transition
if (
isComponent && isComponent &&
builtInComponentSymbol !== PORTAL && // Teleport is not a real component and has dedicated runtime handling
builtInComponentSymbol !== KEEP_ALIVE vnodeTag !== TELEPORT &&
) { // explained above.
vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
const { slots, hasDynamicSlots } = buildSlots(node, context) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) vnodeChildren = slots
if (hasDynamicSlots) { if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS patchFlag |= PatchFlags.DYNAMIC_SLOTS
} }
} else if (node.children.length === 1) { } else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
const child = node.children[0] const child = node.children[0]
const type = child.type const type = child.type
// check for dynamic text children // check for dynamic text children
@ -158,65 +152,88 @@ export const transformElement: NodeTransform = (node, context) => {
// pass directly if the only child is a text node // pass directly if the only child is a text node
// (plain / interpolation / expression) // (plain / interpolation / expression)
if (hasDynamicTextChild || type === NodeTypes.TEXT) { if (hasDynamicTextChild || type === NodeTypes.TEXT) {
args.push(child) vnodeChildren = child as TemplateTextChildNode
} else { } else {
args.push(node.children) vnodeChildren = node.children
} }
} else { } else {
args.push(node.children) vnodeChildren = node.children
} }
} }
// patchFlag & dynamicPropNames // patchFlag & dynamicPropNames
if (patchFlag !== 0) { if (patchFlag !== 0) {
if (!hasChildren) {
if (!hasProps) {
args.push(`null`)
}
args.push(`null`)
}
if (__DEV__) { if (__DEV__) {
const flagNames = Object.keys(PatchFlagNames) if (patchFlag < 0) {
.map(Number) // special flags (negative and mutually exclusive)
.filter(n => n > 0 && patchFlag & n) vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
.map(n => PatchFlagNames[n]) } else {
.join(`, `) // bitwise flags
args.push(patchFlag + ` /* ${flagNames} */`) const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n])
.join(`, `)
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
}
} else { } else {
args.push(patchFlag + '') vnodePatchFlag = String(patchFlag)
} }
if (dynamicPropNames && dynamicPropNames.length) { if (dynamicPropNames && dynamicPropNames.length) {
args.push(stringifyDynamicPropNames(dynamicPropNames)) vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
} }
} }
const { loc } = node node.codegenNode = createVNodeCall(
const vnode = createCallExpression(context.helper(CREATE_VNODE), args, loc) context,
vnodeTag,
if (runtimeDirectives && runtimeDirectives.length) { vnodeProps,
node.codegenNode = createCallExpression( vnodeChildren,
context.helper(WITH_DIRECTIVES), vnodePatchFlag,
[ vnodeDynamicProps,
vnode, vnodeDirectives,
createArrayExpression( !!shouldUseBlock,
runtimeDirectives.map(dir => buildDirectiveArgs(dir, context)), false /* isForBlock */,
loc node.loc
) )
],
loc
)
} else {
node.codegenNode = vnode
}
} }
} }
function stringifyDynamicPropNames(props: string[]): string { export function resolveComponentType(
let propsNamesString = `[` node: ComponentNode,
for (let i = 0, l = props.length; i < l; i++) { context: TransformContext,
propsNamesString += JSON.stringify(props[i]) ssr = false
if (i < l - 1) propsNamesString += ', ' ) {
const { tag } = node
// 1. dynamic component
const isProp =
node.tag === 'component' ? findProp(node, 'is') : findDir(node, 'is')
if (isProp) {
const exp =
isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true)
: isProp.exp
if (exp) {
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
exp
])
}
} }
return propsNamesString + `]`
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) {
// built-ins are simply fallthroughs / have special handling during ssr
// no we don't need to import their runtime equivalents
if (!ssr) context.helper(builtIn)
return builtIn
}
// 3. user component (resolve)
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
return toValidAssetId(tag, `component`)
} }
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
@ -224,14 +241,15 @@ export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
export function buildProps( export function buildProps(
node: ElementNode, node: ElementNode,
context: TransformContext, context: TransformContext,
props: ElementNode['props'] = node.props props: ElementNode['props'] = node.props,
ssr = false
): { ): {
props: PropsExpression | undefined props: PropsExpression | undefined
directives: DirectiveNode[] directives: DirectiveNode[]
patchFlag: number patchFlag: number
dynamicPropNames: string[] dynamicPropNames: string[]
} { } {
const elementLoc = node.loc const { tag, loc: elementLoc } = node
const isComponent = node.tagType === ElementTypes.COMPONENT const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = [] let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = [] const mergeArgs: PropsExpression[] = []
@ -242,27 +260,40 @@ export function buildProps(
let hasRef = false let hasRef = false
let hasClassBinding = false let hasClassBinding = false
let hasStyleBinding = false let hasStyleBinding = false
let hasHydrationEventBinding = false
let hasDynamicKeys = false let hasDynamicKeys = false
const dynamicPropNames: string[] = [] const dynamicPropNames: string[] = []
const analyzePatchFlag = ({ key, value }: Property) => { const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
const name = key.content
if (
!isComponent &&
isOn(name) &&
// omit the flag for click handlers becaues hydration gives click
// dedicated fast path.
name.toLowerCase() !== 'onclick' &&
// omit v-model handlers
name !== 'onUpdate:modelValue'
) {
hasHydrationEventBinding = true
}
if ( if (
value.type === NodeTypes.JS_CACHE_EXPRESSION || value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION || ((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) && value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value)) isStaticNode(value))
) { ) {
// skip if the prop is a cached handler or has constant value
return return
} }
const name = key.content
if (name === 'ref') { if (name === 'ref') {
hasRef = true hasRef = true
} else if (name === 'class') { } else if (name === 'class') {
hasClassBinding = true hasClassBinding = true
} else if (name === 'style') { } else if (name === 'style') {
hasStyleBinding = true hasStyleBinding = true
} else if (name !== 'key') { } else if (name !== 'key' && !dynamicPropNames.includes(name)) {
dynamicPropNames.push(name) dynamicPropNames.push(name)
} }
} else { } else {
@ -278,6 +309,10 @@ export function buildProps(
if (name === 'ref') { if (name === 'ref') {
hasRef = true hasRef = true
} }
// skip :is on <component>
if (name === 'is' && tag === 'component') {
continue
}
properties.push( properties.push(
createObjectProperty( createObjectProperty(
createSimpleExpression( createSimpleExpression(
@ -295,6 +330,8 @@ export function buildProps(
} else { } else {
// directives // directives
const { name, arg, exp, loc } = prop const { name, arg, exp, loc } = prop
const isBind = name === 'bind'
const isOn = name === 'on'
// skip v-slot - it is handled by its dedicated transform. // skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') { if (name === 'slot') {
@ -305,15 +342,23 @@ export function buildProps(
} }
continue continue
} }
// skip v-once - it is handled by its dedicated transform. // skip v-once - it is handled by its dedicated transform.
if (name === 'once') { if (name === 'once') {
continue continue
} }
// skip v-is and :is on <component>
if (
name === 'is' ||
(isBind && tag === 'component' && isBindKey(arg, 'is'))
) {
continue
}
// skip v-on in SSR compilation
if (isOn && ssr) {
continue
}
// special case for v-bind and v-on with no argument // special case for v-bind and v-on with no argument
const isBind = name === 'bind'
const isOn = name === 'on'
if (!arg && (isBind || isOn)) { if (!arg && (isBind || isOn)) {
hasDynamicKeys = true hasDynamicKeys = true
if (exp) { if (exp) {
@ -351,7 +396,7 @@ export function buildProps(
if (directiveTransform) { if (directiveTransform) {
// has built-in directive transform. // has built-in directive transform.
const { props, needRuntime } = directiveTransform(prop, node, context) const { props, needRuntime } = directiveTransform(prop, node, context)
props.forEach(analyzePatchFlag) !ssr && props.forEach(analyzePatchFlag)
properties.push(...props) properties.push(...props)
if (needRuntime) { if (needRuntime) {
runtimeDirectives.push(prop) runtimeDirectives.push(prop)
@ -405,8 +450,14 @@ export function buildProps(
if (dynamicPropNames.length) { if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS patchFlag |= PatchFlags.PROPS
} }
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
} }
if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) { if (
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || runtimeDirectives.length > 0)
) {
patchFlag |= PatchFlags.NEED_PATCH patchFlag |= PatchFlags.NEED_PATCH
} }
@ -437,12 +488,7 @@ function dedupeProperties(properties: Property[]): Property[] {
const name = prop.key.content const name = prop.key.content
const existing = knownProps.get(name) const existing = knownProps.get(name)
if (existing) { if (existing) {
if ( if (name === 'style' || name === 'class' || name.startsWith('on')) {
name === 'style' ||
name === 'class' ||
name.startsWith('on') ||
name.startsWith('vnode')
) {
mergeAsArray(existing, prop) mergeAsArray(existing, prop)
} }
// unexpected duplicate, should have emitted error during parse // unexpected duplicate, should have emitted error during parse
@ -472,7 +518,6 @@ function buildDirectiveArgs(
const dirArgs: ArrayExpression['elements'] = [] const dirArgs: ArrayExpression['elements'] = []
const runtime = directiveImportMap.get(dir) const runtime = directiveImportMap.get(dir)
if (runtime) { if (runtime) {
context.helper(runtime)
dirArgs.push(context.helperString(runtime)) dirArgs.push(context.helperString(runtime))
} else { } else {
// inject statement for resolving directive // inject statement for resolving directive
@ -507,3 +552,12 @@ function buildDirectiveArgs(
} }
return createArrayExpression(dirArgs, dir.loc) return createArrayExpression(dirArgs, dir.loc)
} }
function stringifyDynamicPropNames(props: string[]): string {
let propsNamesString = `[`
for (let i = 0, l = props.length; i < l; i++) {
propsNamesString += JSON.stringify(props[i])
if (i < l - 1) propsNamesString += ', '
}
return propsNamesString + `]`
}

View File

@ -16,7 +16,6 @@ import {
CompoundExpressionNode, CompoundExpressionNode,
createCompoundExpression createCompoundExpression
} from '../ast' } from '../ast'
import { Node, Function, Identifier, Property } from 'estree'
import { import {
advancePositionWithClone, advancePositionWithClone,
isSimpleIdentifier, isSimpleIdentifier,
@ -25,6 +24,7 @@ import {
} from '../utils' } from '../utils'
import { isGloballyWhitelisted, makeMap } from '@vue/shared' import { isGloballyWhitelisted, makeMap } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -40,11 +40,15 @@ export const transformExpression: NodeTransform = (node, context) => {
const dir = node.props[i] const dir = node.props[i]
// do not process for v-on & v-for since they are special handled // do not process for v-on & v-for since they are special handled
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') { if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
const exp = dir.exp as SimpleExpressionNode | undefined const exp = dir.exp
const arg = dir.arg as SimpleExpressionNode | undefined const arg = dir.arg
// do not process exp if this is v-on:arg - we need special handling // do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements. // for wrapping inline statements.
if (exp && !(dir.name === 'on' && arg)) { if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
) {
dir.exp = processExpression( dir.exp = processExpression(
exp, exp,
context, context,
@ -52,7 +56,7 @@ export const transformExpression: NodeTransform = (node, context) => {
dir.name === 'slot' dir.name === 'slot'
) )
} }
if (arg && !arg.isStatic) { if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
dir.arg = processExpression(arg, context) dir.arg = processExpression(arg, context)
} }
} }
@ -76,7 +80,9 @@ export function processExpression(
context: TransformContext, context: TransformContext,
// some expressions like v-slot props & v-for aliases should be parsed as // some expressions like v-slot props & v-for aliases should be parsed as
// function params // function params
asParams: boolean = false asParams = false,
// v-on handler values may contain multiple statements
asRawStatements = false
): ExpressionNode { ): ExpressionNode {
if (!context.prefixIdentifiers || !node.content.trim()) { if (!context.prefixIdentifiers || !node.content.trim()) {
return node return node
@ -84,6 +90,8 @@ export function processExpression(
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
const rawExp = node.content const rawExp = node.content
// bail on parens to prevent any possible function invocations.
const bailConstant = rawExp.indexOf(`(`) > -1
if (isSimpleIdentifier(rawExp)) { if (isSimpleIdentifier(rawExp)) {
if ( if (
!asParams && !asParams &&
@ -92,7 +100,7 @@ export function processExpression(
!isLiteralWhitelisted(rawExp) !isLiteralWhitelisted(rawExp)
) { ) {
node.content = `_ctx.${rawExp}` node.content = `_ctx.${rawExp}`
} else if (!context.identifiers[rawExp]) { } else if (!context.identifiers[rawExp] && !bailConstant) {
// mark node constant for hoisting unless it's referring a scope variable // mark node constant for hoisting unless it's referring a scope variable
node.isConstant = true node.isConstant = true
} }
@ -100,26 +108,48 @@ export function processExpression(
} }
let ast: any let ast: any
// if the expression is supposed to be used in a function params position // exp needs to be parsed differently:
// we need to parse it differently. // 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
const source = `(${rawExp})${asParams ? `=>{}` : ``}` // exp, but make sure to pad with spaces for consistent ranges
// 2. Expressions: wrap with parens (for e.g. object expressions)
// 3. Function arguments (v-for, v-slot): place in a function argument position
const source = asRawStatements
? ` ${rawExp} `
: `(${rawExp})${asParams ? `=>{}` : ``}`
try { try {
ast = parseJS(source, { ranges: true }) ast = parseJS(source, {
plugins: [
...context.expressionPlugins,
// by default we enable proposals slated for ES2020.
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
// this will need to be updated as the spec moves forward.
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program
} catch (e) { } catch (e) {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_INVALID_EXPRESSION, node.loc) createCompilerError(
ErrorCodes.X_INVALID_EXPRESSION,
node.loc,
undefined,
e.message
)
) )
return node return node
} }
const ids: (Identifier & PrefixMeta)[] = [] const ids: (Identifier & PrefixMeta)[] = []
const knownIds = Object.create(context.identifiers) const knownIds = Object.create(context.identifiers)
const isDuplicate = (node: Node & PrefixMeta): boolean =>
ids.some(id => id.start === node.start)
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`. // walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
walkJS(ast, { walkJS(ast, {
enter(node: Node & PrefixMeta, parent) { enter(node: Node & PrefixMeta, parent) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
if (!ids.includes(node)) { if (!isDuplicate(node)) {
const needPrefix = shouldPrefix(node, parent) const needPrefix = shouldPrefix(node, parent)
if (!knownIds[node.name] && needPrefix) { if (!knownIds[node.name] && needPrefix) {
if (isPropertyShorthand(node, parent)) { if (isPropertyShorthand(node, parent)) {
@ -128,12 +158,13 @@ export function processExpression(
node.prefix = `${node.name}: ` node.prefix = `${node.name}: `
} }
node.name = `_ctx.${node.name}` node.name = `_ctx.${node.name}`
node.isConstant = false
ids.push(node) ids.push(node)
} else if (!isStaticPropertyKey(node, parent)) { } else if (!isStaticPropertyKey(node, parent)) {
// The identifier is considered constant unless it's pointing to a // The identifier is considered constant unless it's pointing to a
// scope variable (a v-for alias, or a v-slot prop) // scope variable (a v-for alias, or a v-slot prop)
node.isConstant = !(needPrefix && knownIds[node.name]) if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
node.isConstant = true
}
// also generate sub-expressions for other identifiers for better // also generate sub-expressions for other identifiers for better
// source map support. (except for property keys which are static) // source map support. (except for property keys which are static)
ids.push(node) ids.push(node)
@ -223,7 +254,7 @@ export function processExpression(
ret = createCompoundExpression(children, node.loc) ret = createCompoundExpression(children, node.loc)
} else { } else {
ret = node ret = node
ret.isConstant = true ret.isConstant = !bailConstant
} }
ret.identifiers = Object.keys(knownIds) ret.identifiers = Object.keys(knownIds)
return ret return ret
@ -232,17 +263,21 @@ export function processExpression(
const isFunction = (node: Node): node is Function => const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type) /Function(Expression|Declaration)$/.test(node.type)
const isPropertyKey = (node: Node, parent: Node) => const isStaticProperty = (node: Node): node is ObjectProperty =>
parent && node && node.type === 'ObjectProperty' && !node.computed
parent.type === 'Property' &&
parent.key === node &&
!parent.computed
const isPropertyShorthand = (node: Node, parent: Node) => const isPropertyShorthand = (node: Node, parent: Node) => {
isPropertyKey(node, parent) && (parent as Property).value === node return (
isStaticProperty(parent) &&
parent.value === node &&
parent.key.type === 'Identifier' &&
parent.key.name === (node as Identifier).name &&
parent.key.start === node.start
)
}
const isStaticPropertyKey = (node: Node, parent: Node) => const isStaticPropertyKey = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value !== node isStaticProperty(parent) && parent.key === node
function shouldPrefix(identifier: Identifier, parent: Node) { function shouldPrefix(identifier: Identifier, parent: Node) {
if ( if (
@ -257,7 +292,8 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
!isStaticPropertyKey(identifier, parent) && !isStaticPropertyKey(identifier, parent) &&
// not a property of a MemberExpression // not a property of a MemberExpression
!( !(
parent.type === 'MemberExpression' && (parent.type === 'MemberExpression' ||
parent.type === 'OptionalMemberExpression') &&
parent.property === identifier && parent.property === identifier &&
!parent.computed !parent.computed
) && ) &&

View File

@ -1,78 +1,32 @@
import { NodeTransform } from '../transform' import { NodeTransform, TransformContext } from '../transform'
import { import {
NodeTypes, NodeTypes,
CallExpression, CallExpression,
createCallExpression, createCallExpression,
ExpressionNode ExpressionNode,
SlotOutletNode
} from '../ast' } from '../ast'
import { isSlotOutlet } from '../utils' import { isSlotOutlet, findProp } from '../utils'
import { buildProps } from './transformElement' import { buildProps, PropsExpression } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeHelpers' import { RENDER_SLOT } from '../runtimeHelpers'
export const transformSlotOutlet: NodeTransform = (node, context) => { export const transformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) { if (isSlotOutlet(node)) {
const { props, children, loc } = node const { children, loc } = node
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots` const { slotName, slotProps } = processSlotOutlet(node, context)
let slotName: string | ExpressionNode = `"default"`
// check for <slot name="xxx" OR :name="xxx" /> const slotArgs: CallExpression['arguments'] = [
let nameIndex: number = -1 context.prefixIdentifiers ? `_ctx.$slots` : `$slots`,
for (let i = 0; i < props.length; i++) { slotName
const prop = props[i] ]
if (prop.type === NodeTypes.ATTRIBUTE) {
if (prop.name === `name` && prop.value) {
// static name="xxx"
slotName = JSON.stringify(prop.value.content)
nameIndex = i
break
}
} else if (prop.name === `bind`) {
const { arg, exp } = prop
if (
arg &&
exp &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === `name`
) {
// dynamic :name="xxx"
slotName = exp
nameIndex = i
break
}
}
}
const slotArgs: CallExpression['arguments'] = [$slots, slotName] if (slotProps) {
const propsWithoutName = slotArgs.push(slotProps)
nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props
let hasProps = propsWithoutName.length > 0
if (hasProps) {
const { props: propsExpression, directives } = buildProps(
node,
context,
propsWithoutName
)
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
if (propsExpression) {
slotArgs.push(propsExpression)
} else {
hasProps = false
}
} }
if (children.length) { if (children.length) {
if (!hasProps) { if (!slotProps) {
slotArgs.push(`{}`) slotArgs.push(`{}`)
} }
slotArgs.push(children) slotArgs.push(children)
@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
) )
} }
} }
interface SlotOutletProcessResult {
slotName: string | ExpressionNode
slotProps: PropsExpression | undefined
}
export function processSlotOutlet(
node: SlotOutletNode,
context: TransformContext
): SlotOutletProcessResult {
let slotName: string | ExpressionNode = `"default"`
let slotProps: PropsExpression | undefined = undefined
// check for <slot name="xxx" OR :name="xxx" />
const name = findProp(node, 'name')
if (name) {
if (name.type === NodeTypes.ATTRIBUTE && name.value) {
// static name
slotName = JSON.stringify(name.value.content)
} else if (name.type === NodeTypes.DIRECTIVE && name.exp) {
// dynamic name
slotName = name.exp
}
}
const propsWithoutName = name
? node.props.filter(p => p !== name)
: node.props
if (propsWithoutName.length > 0) {
const { props, directives } = buildProps(node, context, propsWithoutName)
slotProps = props
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
}
return {
slotName,
slotProps
}
}

View File

@ -1,29 +1,23 @@
import { NodeTransform } from '../transform' import { NodeTransform } from '../transform'
import { import {
NodeTypes, NodeTypes,
TemplateChildNode,
TextNode,
InterpolationNode,
CompoundExpressionNode, CompoundExpressionNode,
createCallExpression, createCallExpression,
CallExpression, CallExpression,
ElementTypes ElementTypes
} from '../ast' } from '../ast'
import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers' import { CREATE_TEXT } from '../runtimeHelpers'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
const isText = (
node: TemplateChildNode
): node is TextNode | InterpolationNode =>
node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
// Merge adjacent text nodes and expressions into a single expression // Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child. // e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
export const transformText: NodeTransform = (node, context) => { export const transformText: NodeTransform = (node, context) => {
if ( if (
node.type === NodeTypes.ROOT || node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) { ) {
// perform the transform on node exit so that all expressions have already // perform the transform on node exit so that all expressions have already
// been processed. // been processed.
@ -84,7 +78,7 @@ export const transformText: NodeTransform = (node, context) => {
callArgs.push(child) callArgs.push(child)
} }
// mark dynamic text with flag so it gets patched inside a block // mark dynamic text with flag so it gets patched inside a block
if (child.type !== NodeTypes.TEXT) { if (!context.ssr && child.type !== NodeTypes.TEXT) {
callArgs.push( callArgs.push(
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */` `${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
) )

View File

@ -30,7 +30,6 @@ export const transformBind: DirectiveTransform = (dir, node, context) => {
return { return {
props: [ props: [
createObjectProperty(arg!, exp || createSimpleExpression('', true, loc)) createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
], ]
needRuntime: false
} }
} }

View File

@ -8,22 +8,28 @@ import {
createSimpleExpression, createSimpleExpression,
SourceLocation, SourceLocation,
SimpleExpressionNode, SimpleExpressionNode,
createSequenceExpression,
createCallExpression, createCallExpression,
createFunctionExpression, createFunctionExpression,
ElementTypes, ElementTypes,
createObjectExpression, createObjectExpression,
createObjectProperty, createObjectProperty,
ForCodegenNode, ForCodegenNode,
ElementCodegenNode, RenderSlotCall,
SlotOutletCodegenNode, SlotOutletNode,
SlotOutletNode ElementNode,
DirectiveNode,
ForNode,
PlainElementNode,
createVNodeCall,
VNodeCall,
ForRenderListExpression,
BlockCodegenNode,
ForIteratorExpression
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
getInnerRange, getInnerRange,
findProp, findProp,
createBlockExpression,
isTemplateNode, isTemplateNode,
isSlotOutlet, isSlotOutlet,
injectProp injectProp
@ -32,8 +38,7 @@ import {
RENDER_LIST, RENDER_LIST,
OPEN_BLOCK, OPEN_BLOCK,
CREATE_BLOCK, CREATE_BLOCK,
FRAGMENT, FRAGMENT
WITH_DIRECTIVES
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
@ -41,141 +46,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformFor = createStructuralDirectiveTransform( export const transformFor = createStructuralDirectiveTransform(
'for', 'for',
(node, dir, context) => { (node, dir, context) => {
if (!dir.exp) { const { helper } = context
context.onError( return processFor(node, dir, context, forNode => {
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc) // create the loop render function expression now, and add the
) // iterator on exit after all children have been traversed
return const renderExp = createCallExpression(helper(RENDER_LIST), [
} forNode.source
]) as ForRenderListExpression
const parseResult = parseForExpression( const keyProp = findProp(node, `key`)
// can only be simple expression because vFor transform is applied const fragmentFlag = keyProp
// before expression transform. ? PatchFlags.KEYED_FRAGMENT
dir.exp as SimpleExpressionNode, : PatchFlags.UNKEYED_FRAGMENT
context forNode.codegenNode = createVNodeCall(
) context,
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { helper, addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
const codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT), helper(FRAGMENT),
`null`, undefined,
renderExp, renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */` `${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`,
]) undefined,
]) as ForCodegenNode undefined,
true /* isBlock */,
true /* isForBlock */,
node.loc
) as ForCodegenNode
context.replaceNode({ return () => {
type: NodeTypes.FOR, // finish the codegen now that all children have been traversed
loc: dir.loc, let childBlock: BlockCodegenNode
source, const isTemplate = isTemplateNode(node)
valueAlias: value, const { children } = forNode
keyAlias: key, const needFragmentWrapper =
objectIndexAlias: index, children.length > 1 || children[0].type !== NodeTypes.ELEMENT
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node], const slotOutlet = isSlotOutlet(node)
codegenNode ? node
}) : isTemplate &&
node.children.length === 1 &&
// bookkeeping isSlotOutlet(node.children[0])
scopes.vFor++ ? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
if (!__BROWSER__ && context.prefixIdentifiers) { : null
// scope management const keyProperty = keyProp
// inject identifiers to context ? createObjectProperty(
value && addIdentifiers(value) `key`,
key && addIdentifiers(key) keyProp.type === NodeTypes.ATTRIBUTE
index && addIdentifiers(index) ? createSimpleExpression(keyProp.value!.content, true)
} : keyProp.exp!
)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
// finish the codegen now that all children have been traversed
let childBlock
const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null : null
const keyProperty = keyProp if (slotOutlet) {
? createObjectProperty( // <slot v-for="..."> or <template v-for="..."><slot/></template>
`key`, childBlock = slotOutlet.codegenNode as RenderSlotCall
keyProp.type === NodeTypes.ATTRIBUTE if (isTemplate && keyProperty) {
? createSimpleExpression(keyProp.value!.content, true) // <template v-for="..." :key="..."><slot/></template>
: keyProp.exp! // we need to inject the key to the renderSlot() call.
) // the props for renderSlot is passed as the 3rd argument.
: null injectProp(childBlock, keyProperty, context)
if (slotOutlet) { }
// <slot v-for="..."> or <template v-for="..."><slot/></template> } else if (needFragmentWrapper) {
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode // <template v-for="..."> with text or multi-elements
if (isTemplate && keyProperty) { // should generate a fragment block for each loop
// <template v-for="..." :key="..."><slot/></template> childBlock = createVNodeCall(
// we need to inject the key to the renderSlot() call. context,
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT), helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`, keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children, node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${ `${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT] PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */` } */`,
]), undefined,
context undefined,
) true
} else { )
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else { } else {
codegenNode.callee = helper(CREATE_BLOCK) // Normal element v-for. Directly use the child's codegenNode
// but mark it as a block.
childBlock = (children[0] as PlainElementNode)
.codegenNode as VNodeCall
childBlock.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
} }
childBlock = createBlockExpression(codegenNode, context)
}
renderExp.arguments.push( renderExp.arguments.push(createFunctionExpression(
createFunctionExpression( createForLoopParams(forNode.parseResult),
createForLoopParams(parseResult),
childBlock, childBlock,
true /* force newline */ true /* force newline */
) ) as ForIteratorExpression)
) }
} })
} }
) )
// target-agnostic transform used for both Client and SSR
export function processFor(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (forNode: ForNode) => (() => void) | undefined
) {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
parseResult,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
}
context.replaceNode(forNode)
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
const onExit = processCodegen && processCodegen(forNode)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
}
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring, // This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice. // but those do not make sense in the first place, so this works in practice.

View File

@ -1,7 +1,7 @@
import { import {
createStructuralDirectiveTransform, createStructuralDirectiveTransform,
traverseChildren, TransformContext,
TransformContext traverseNode
} from '../transform' } from '../transform'
import { import {
NodeTypes, NodeTypes,
@ -10,130 +10,139 @@ import {
DirectiveNode, DirectiveNode,
IfBranchNode, IfBranchNode,
SimpleExpressionNode, SimpleExpressionNode,
createSequenceExpression,
createCallExpression, createCallExpression,
createConditionalExpression, createConditionalExpression,
ConditionalExpression,
CallExpression,
createSimpleExpression, createSimpleExpression,
createObjectProperty, createObjectProperty,
createObjectExpression, createObjectExpression,
IfCodegenNode,
IfConditionalExpression, IfConditionalExpression,
BlockCodegenNode, BlockCodegenNode,
SlotOutletCodegenNode, IfNode,
ElementCodegenNode, createVNodeCall
ComponentCodegenNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { import {
OPEN_BLOCK,
CREATE_BLOCK, CREATE_BLOCK,
FRAGMENT, FRAGMENT,
WITH_DIRECTIVES, CREATE_COMMENT,
CREATE_VNODE, OPEN_BLOCK,
CREATE_COMMENT TELEPORT
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { injectProp } from '../utils' import { injectProp } from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformIf = createStructuralDirectiveTransform( export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
(node, dir, context) => { (node, dir, context) => {
if ( return processIf(node, dir, context, (ifNode, branch, isRoot) => {
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) {
const loc = dir.exp ? dir.exp.loc : node.loc
context.onError(
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
)
dir.exp = createSimpleExpression(`true`, false, loc)
}
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
// dir.exp can only be simple expression because vIf transform is applied
// before expression transform.
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}
if (dir.name === 'if') {
const branch = createIfBranch(node, dir)
const codegenNode = createSequenceExpression([
createCallExpression(context.helper(OPEN_BLOCK))
]) as IfCodegenNode
context.replaceNode({
type: NodeTypes.IF,
loc: node.loc,
branches: [branch],
codegenNode
})
// Exit callback. Complete the codegenNode when all children have been // Exit callback. Complete the codegenNode when all children have been
// transformed. // transformed.
return () => { return () => {
codegenNode.expressions.push(createCodegenNodeForBranch( if (isRoot) {
branch, ifNode.codegenNode = createCodegenNodeForBranch(
0, branch,
context 0,
) as IfConditionalExpression) context
} ) as IfConditionalExpression
} else {
// locate the adjacent v-if
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling)
comments.unshift(sibling)
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
const branch = createIfBranch(node, dir)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
sibling.branches.push(branch)
// since the branch was removed, it will not be traversed.
// make sure to traverse here.
traverseChildren(branch, context)
// make sure to reset currentNode after traversal to indicate this
// node has been removed.
context.currentNode = null
// attach this branch's codegen node to the v-if root.
let parentCondition = sibling.codegenNode
.expressions[1] as ConditionalExpression
while (true) {
if (
parentCondition.alternate.type ===
NodeTypes.JS_CONDITIONAL_EXPRESSION
) {
parentCondition = parentCondition.alternate
} else {
parentCondition.alternate = createCodegenNodeForBranch(
branch,
sibling.branches.length - 1,
context
)
break
}
}
} else { } else {
context.onError( // attach this branch's codegen node to the v-if root.
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc) let parentCondition = ifNode.codegenNode!
while (
parentCondition.alternate.type ===
NodeTypes.JS_CONDITIONAL_EXPRESSION
) {
parentCondition = parentCondition.alternate
}
parentCondition.alternate = createCodegenNodeForBranch(
branch,
ifNode.branches.length - 1,
context
) )
} }
break
} }
} })
} }
) )
// target-agnostic transform used for both Client and SSR
export function processIf(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
) => (() => void) | undefined
) {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) {
const loc = dir.exp ? dir.exp.loc : node.loc
context.onError(
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
)
dir.exp = createSimpleExpression(`true`, false, loc)
}
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
// dir.exp can only be simple expression because vIf transform is applied
// before expression transform.
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}
if (dir.name === 'if') {
const branch = createIfBranch(node, dir)
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
branches: [branch]
}
context.replaceNode(ifNode)
if (processCodegen) {
return processCodegen(ifNode, branch, true)
}
} else {
// locate the adjacent v-if
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling)
comments.unshift(sibling)
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
const branch = createIfBranch(node, dir)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
sibling.branches.push(branch)
const onExit = processCodegen && processCodegen(sibling, branch, false)
// since the branch was removed, it will not be traversed.
// make sure to traverse here.
traverseNode(branch, context)
// call on exit
if (onExit) onExit()
// make sure to reset currentNode after traversal to indicate this
// node has been removed.
context.currentNode = null
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
)
}
break
}
}
}
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
return { return {
type: NodeTypes.IF_BRANCH, type: NodeTypes.IF_BRANCH,
@ -160,7 +169,7 @@ function createCodegenNodeForBranch(
]) ])
) as IfConditionalExpression ) as IfConditionalExpression
} else { } else {
return createChildrenCodegenNode(branch, index, context) as BlockCodegenNode return createChildrenCodegenNode(branch, index, context)
} }
} }
@ -168,46 +177,56 @@ function createChildrenCodegenNode(
branch: IfBranchNode, branch: IfBranchNode,
index: number, index: number,
context: TransformContext context: TransformContext
): CallExpression { ): BlockCodegenNode {
const { helper } = context const { helper } = context
const keyProperty = createObjectProperty( const keyProperty = createObjectProperty(
`key`, `key`,
createSimpleExpression(index + '', false) createSimpleExpression(index + '', false)
) )
const { children } = branch const { children } = branch
const child = children[0] const firstChild = children[0]
const needFragmentWrapper = const needFragmentWrapper =
children.length !== 1 || child.type !== NodeTypes.ELEMENT children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
if (needFragmentWrapper) { if (needFragmentWrapper) {
const blockArgs: CallExpression['arguments'] = [ if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children
]
if (children.length === 1 && child.type === NodeTypes.FOR) {
// optimize away nested fragments when child is a ForNode // optimize away nested fragments when child is a ForNode
const forBlockArgs = child.codegenNode.expressions[1].arguments const vnodeCall = firstChild.codegenNode!
// directly use the for block's children and patchFlag injectProp(vnodeCall, keyProperty, context)
blockArgs[2] = forBlockArgs[2] return vnodeCall
blockArgs[3] = forBlockArgs[3] } else {
return createVNodeCall(
context,
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true,
false,
branch.loc
)
} }
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
} else { } else {
const childCodegen = (child as ElementNode).codegenNode as const vnodeCall = (firstChild as ElementNode)
| ElementCodegenNode .codegenNode as BlockCodegenNode
| ComponentCodegenNode
| SlotOutletCodegenNode
let vnodeCall = childCodegen
// Element with custom directives. Locate the actual createVNode() call.
if (vnodeCall.callee === WITH_DIRECTIVES) {
vnodeCall = vnodeCall.arguments[0]
}
// Change createVNode to createBlock. // Change createVNode to createBlock.
if (vnodeCall.callee === CREATE_VNODE) { if (
vnodeCall.callee = helper(CREATE_BLOCK) vnodeCall.type === NodeTypes.VNODE_CALL &&
// component vnodes are always tracked and its children are
// compiled into slots so no need to make it a block
((firstChild as ElementNode).tagType !== ElementTypes.COMPONENT ||
// teleport has component type but isn't always tracked
vnodeCall.tag === TELEPORT)
) {
vnodeCall.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
} }
// inject branch key // inject branch key
injectProp(vnodeCall, keyProperty, context) injectProp(vnodeCall, keyProperty, context)
return childCodegen return vnodeCall
} }
} }

View File

@ -44,10 +44,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
const eventName = arg const eventName = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `onUpdate:${arg.content}` ? `onUpdate:${arg.content}`
: createCompoundExpression([ : createCompoundExpression(['"onUpdate:" + ', arg])
'"onUpdate:" + ',
...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
])
: `onUpdate:modelValue` : `onUpdate:modelValue`
const props = [ const props = [
@ -56,11 +53,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// "onUpdate:modelValue": $event => (foo = $event) // "onUpdate:modelValue": $event => (foo = $event)
createObjectProperty( createObjectProperty(
eventName, eventName,
createCompoundExpression([ createCompoundExpression([`$event => (`, exp, ` = $event)`])
`$event => (`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
` = $event)`
])
) )
] ]
@ -82,12 +75,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
const modifiersKey = arg const modifiersKey = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `${arg.content}Modifiers` ? `${arg.content}Modifiers`
: createCompoundExpression([ : createCompoundExpression([arg, ' + "Modifiers"'])
...(arg.type === NodeTypes.SIMPLE_EXPRESSION
? [arg]
: arg.children),
' + "Modifiers"'
])
: `modelModifiers` : `modelModifiers`
props.push( props.push(
createObjectProperty( createObjectProperty(
@ -101,5 +89,5 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
} }
function createTransformProps(props: Property[] = []) { function createTransformProps(props: Property[] = []) {
return { props, needRuntime: false } return { props }
} }

View File

@ -8,7 +8,7 @@ import {
createCompoundExpression, createCompoundExpression,
SimpleExpressionNode SimpleExpressionNode
} from '../ast' } from '../ast'
import { capitalize } from '@vue/shared' import { capitalize, camelize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { isMemberExpression, hasScopeRef } from '../utils' import { isMemberExpression, hasScopeRef } from '../utils'
@ -26,23 +26,24 @@ export interface VOnDirectiveNode extends DirectiveNode {
} }
export const transformOn: DirectiveTransform = ( export const transformOn: DirectiveTransform = (
dir: VOnDirectiveNode, dir,
node, node,
context, context,
augmentor augmentor
) => { ) => {
const { loc, modifiers, arg } = dir const { loc, modifiers, arg } = dir as VOnDirectiveNode
if (!dir.exp && !modifiers.length) { if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)) context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
} }
let eventName: ExpressionNode let eventName: ExpressionNode
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) { if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) { if (arg.isStatic) {
eventName = createSimpleExpression( const rawName = arg.content
`on${capitalize(arg.content)}`, // for @vnode-xxx event listeners, auto convert it to camelCase
true, const normalizedName = rawName.startsWith(`vnode`)
arg.loc ? capitalize(camelize(rawName))
) : capitalize(rawName)
eventName = createSimpleExpression(`on${normalizedName}`, true, arg.loc)
} else { } else {
eventName = createCompoundExpression([`"on" + (`, arg, `)`]) eventName = createCompoundExpression([`"on" + (`, arg, `)`])
} }
@ -54,16 +55,22 @@ export const transformOn: DirectiveTransform = (
} }
// handler processing // handler processing
let exp: ExpressionNode | undefined = dir.exp let exp: ExpressionNode | undefined = dir.exp as
| SimpleExpressionNode
| undefined
if (exp && !exp.content.trim()) {
exp = undefined
}
let isCacheable: boolean = !exp let isCacheable: boolean = !exp
if (exp) { if (exp) {
const isMemberExp = isMemberExpression(exp.content) const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content)) const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped // process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`) context.addIdentifiers(`$event`)
exp = processExpression(exp, context) exp = processExpression(exp, context, false, hasMultipleStatements)
context.removeIdentifiers(`$event`) context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference // with scope analysis, the function is hoistable if it has no reference
// to scope variables. // to scope variables.
@ -85,9 +92,9 @@ export const transformOn: DirectiveTransform = (
if (isInlineStatement || (isCacheable && isMemberExp)) { if (isInlineStatement || (isCacheable && isMemberExp)) {
// wrap inline statement in a function expression // wrap inline statement in a function expression
exp = createCompoundExpression([ exp = createCompoundExpression([
`$event => (`, `$event => ${hasMultipleStatements ? `{` : `(`}`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children), exp,
`)` hasMultipleStatements ? `}` : `)`
]) ])
} }
} }
@ -98,8 +105,7 @@ export const transformOn: DirectiveTransform = (
eventName, eventName,
exp || createSimpleExpression(`() => {}`, false, loc) exp || createSimpleExpression(`() => {}`, false, loc)
) )
], ]
needRuntime: false
} }
// apply extended compiler augmentor // apply extended compiler augmentor

View File

@ -19,12 +19,13 @@ import {
FunctionExpression, FunctionExpression,
CallExpression, CallExpression,
createCallExpression, createCallExpression,
createArrayExpression createArrayExpression,
SlotsExpression
} from '../ast' } from '../ast'
import { TransformContext, NodeTransform } from '../transform' import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils' import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers' import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor' import { parseForExpression, createForLoopParams } from './vFor'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode => const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
@ -93,19 +94,42 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
} }
} }
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
createFunctionExpression(
props,
children,
false /* newline */,
true /* isSlot */,
children.length ? children[0].loc : loc
)
// Instead of being a DirectiveTransform, v-slot processing is called during // Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component. // transformElement to build the slots object for a component.
export function buildSlots( export function buildSlots(
node: ElementNode, node: ElementNode,
context: TransformContext context: TransformContext,
buildSlotFn: SlotFnBuilder = buildClientSlotFn
): { ): {
slots: ObjectExpression | CallExpression slots: SlotsExpression
hasDynamicSlots: boolean hasDynamicSlots: boolean
} { } {
context.helper(WITH_CTX)
const { children, loc } = node const { children, loc } = node
const slotsProperties: Property[] = [] const slotsProperties: Property[] = []
const dynamicSlots: (ConditionalExpression | CallExpression)[] = [] const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
const buildDefaultSlotProperty = (
props: ExpressionNode | undefined,
children: TemplateChildNode[]
) => createObjectProperty(`default`, buildSlotFn(props, children, loc))
// If the slot is inside a v-for or another v-slot, force it to be dynamic // If the slot is inside a v-for or another v-slot, force it to be dynamic
// since it likely uses a scope variable. // since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0 let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
@ -115,24 +139,26 @@ export function buildSlots(
hasDynamicSlots = hasScopeRef(node, context.identifiers) hasDynamicSlots = hasScopeRef(node, context.identifiers)
} }
// 1. Check for default slot with slotProps on component itself. // 1. Check for slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/> // <Comp v-slot="{ prop }"/>
const explicitDefaultSlot = findDir(node, 'slot', true) const onComponentSlot = findDir(node, 'slot', true)
if (explicitDefaultSlot) { if (onComponentSlot) {
const { arg, exp, loc } = explicitDefaultSlot const { arg, exp } = onComponentSlot
if (arg) { slotsProperties.push(
context.onError( createObjectProperty(
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc) arg || createSimpleExpression('default', true),
buildSlotFn(exp, children, loc)
) )
} )
slotsProperties.push(buildDefaultSlot(exp, children, loc))
} }
// 2. Iterate through children and check for template slots // 2. Iterate through children and check for template slots
// <template v-slot:foo="{ prop }"> // <template v-slot:foo="{ prop }">
let hasTemplateSlots = false let hasTemplateSlots = false
let extraneousChild: TemplateChildNode | undefined = undefined let hasNamedDefaultSlot = false
const implicitDefaultChildren: TemplateChildNode[] = []
const seenSlotNames = new Set<string>() const seenSlotNames = new Set<string>()
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const slotElement = children[i] const slotElement = children[i]
let slotDir let slotDir
@ -142,14 +168,14 @@ export function buildSlots(
!(slotDir = findDir(slotElement, 'slot', true)) !(slotDir = findDir(slotElement, 'slot', true))
) { ) {
// not a <template v-slot>, skip. // not a <template v-slot>, skip.
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) { if (slotElement.type !== NodeTypes.COMMENT) {
extraneousChild = slotElement implicitDefaultChildren.push(slotElement)
} }
continue continue
} }
if (explicitDefaultSlot) { if (onComponentSlot) {
// already has on-component default slot - this is incorrect usage. // already has on-component slot - this is incorrect usage.
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc) createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
) )
@ -172,13 +198,7 @@ export function buildSlots(
hasDynamicSlots = true hasDynamicSlots = true
} }
const slotFunction = createFunctionExpression( const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
slotProps,
slotChildren,
false,
slotChildren.length ? slotChildren[0].loc : slotLoc
)
// check if this slot is conditional (v-if/v-for) // check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined let vIf: DirectiveNode | undefined
let vElse: DirectiveNode | undefined let vElse: DirectiveNode | undefined
@ -244,7 +264,7 @@ export function buildSlots(
createFunctionExpression( createFunctionExpression(
createForLoopParams(parseResult), createForLoopParams(parseResult),
buildDynamicSlot(slotName, slotFunction), buildDynamicSlot(slotName, slotFunction),
true true /* force newline */
) )
]) ])
) )
@ -266,36 +286,46 @@ export function buildSlots(
continue continue
} }
seenSlotNames.add(staticSlotName) seenSlotNames.add(staticSlotName)
if (staticSlotName === 'default') {
hasNamedDefaultSlot = true
}
} }
slotsProperties.push(createObjectProperty(slotName, slotFunction)) slotsProperties.push(createObjectProperty(slotName, slotFunction))
} }
} }
if (hasTemplateSlots && extraneousChild) { if (!onComponentSlot) {
context.onError( if (!hasTemplateSlots) {
createCompilerError( // implicit default slot (on component)
ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN, slotsProperties.push(buildDefaultSlotProperty(undefined, children))
extraneousChild.loc } else if (implicitDefaultChildren.length) {
) // implicit default slot (mixed with named slots)
) if (hasNamedDefaultSlot) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
implicitDefaultChildren[0].loc
)
)
} else {
slotsProperties.push(
buildDefaultSlotProperty(undefined, implicitDefaultChildren)
)
}
}
} }
if (!explicitDefaultSlot && !hasTemplateSlots) { let slots = createObjectExpression(
// implicit default slot.
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
}
let slots: ObjectExpression | CallExpression = createObjectExpression(
slotsProperties.concat( slotsProperties.concat(
createObjectProperty(`_compiled`, createSimpleExpression(`true`, false)) createObjectProperty(`_`, createSimpleExpression(`1`, false))
), ),
loc loc
) ) as SlotsExpression
if (dynamicSlots.length) { if (dynamicSlots.length) {
slots = createCallExpression(context.helper(CREATE_SLOTS), [ slots = createCallExpression(context.helper(CREATE_SLOTS), [
slots, slots,
createArrayExpression(dynamicSlots) createArrayExpression(dynamicSlots)
]) ]) as SlotsExpression
} }
return { return {
@ -304,22 +334,6 @@ export function buildSlots(
} }
} }
function buildDefaultSlot(
slotProps: ExpressionNode | undefined,
children: TemplateChildNode[],
loc: SourceLocation
): Property {
return createObjectProperty(
`default`,
createFunctionExpression(
slotProps,
children,
false,
children.length ? children[0].loc : loc
)
)
}
function buildDynamicSlot( function buildDynamicSlot(
name: ExpressionNode, name: ExpressionNode,
fn: FunctionExpression fn: FunctionExpression

View File

@ -4,8 +4,6 @@ import {
ElementNode, ElementNode,
NodeTypes, NodeTypes,
CallExpression, CallExpression,
SequenceExpression,
createSequenceExpression,
createCallExpression, createCallExpression,
DirectiveNode, DirectiveNode,
ElementTypes, ElementTypes,
@ -17,33 +15,31 @@ import {
createObjectExpression, createObjectExpression,
SlotOutletNode, SlotOutletNode,
TemplateNode, TemplateNode,
BlockCodegenNode, RenderSlotCall,
ElementCodegenNode,
SlotOutletCodegenNode,
ComponentCodegenNode,
ExpressionNode, ExpressionNode,
IfBranchNode IfBranchNode,
TextNode,
InterpolationNode,
VNodeCall
} from './ast' } from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { import {
OPEN_BLOCK,
MERGE_PROPS, MERGE_PROPS,
RENDER_SLOT, TELEPORT,
PORTAL,
SUSPENSE, SUSPENSE,
KEEP_ALIVE, KEEP_ALIVE,
BASE_TRANSITION BASE_TRANSITION
} from './runtimeHelpers' } from './runtimeHelpers'
import { isString, isFunction, isObject, hyphenate } from '@vue/shared' import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
import { parse } from '@babel/parser'
import { Node } from '@babel/types'
export const isBuiltInType = (tag: string, expected: string): boolean => export const isBuiltInType = (tag: string, expected: string): boolean =>
tag === expected || tag === hyphenate(expected) tag === expected || tag === hyphenate(expected)
export function isCoreComponent(tag: string): symbol | void { export function isCoreComponent(tag: string): symbol | void {
if (isBuiltInType(tag, 'Portal')) { if (isBuiltInType(tag, 'Teleport')) {
return PORTAL return TELEPORT
} else if (isBuiltInType(tag, 'Suspense')) { } else if (isBuiltInType(tag, 'Suspense')) {
return SUSPENSE return SUSPENSE
} else if (isBuiltInType(tag, 'KeepAlive')) { } else if (isBuiltInType(tag, 'KeepAlive')) {
@ -57,10 +53,10 @@ export function isCoreComponent(tag: string): symbol | void {
// lazy require dependencies so that they don't end up in rollup's dep graph // lazy require dependencies so that they don't end up in rollup's dep graph
// and thus can be tree-shaken in browser builds. // and thus can be tree-shaken in browser builds.
let _parse: typeof parse let _parse: typeof parse
let _walk: typeof walk let _walk: any
export function loadDep(name: string) { export function loadDep(name: string) {
if (typeof process !== 'undefined' && isFunction(require)) { if (!__BROWSER__ && typeof process !== 'undefined' && isFunction(require)) {
return require(name) return require(name)
} else { } else {
// This is only used when we are building a dev-only build of the compiler // This is only used when we are building a dev-only build of the compiler
@ -74,11 +70,18 @@ export const parseJS: typeof parse = (code, options) => {
!__BROWSER__, !__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.` `Expression AST analysis can only be performed in non-browser builds.`
) )
const parse = _parse || (_parse = loadDep('acorn').parse) if (!_parse) {
return parse(code, options) _parse = loadDep('@babel/parser').parse
}
return _parse(code, options)
} }
export const walkJS: typeof walk = (ast, walker) => { interface Walker {
enter?(node: Node, parent: Node): void
leave?(node: Node): void
}
export const walkJS = (ast: Node, walker: Walker) => {
assert( assert(
!__BROWSER__, !__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.` `Expression AST analysis can only be performed in non-browser builds.`
@ -149,7 +152,7 @@ export function advancePositionWithMutation(
pos.column = pos.column =
lastNewLinePos === -1 lastNewLinePos === -1
? pos.column + numberOfCharacters ? pos.column + numberOfCharacters
: Math.max(1, numberOfCharacters - lastNewLinePos) : numberOfCharacters - lastNewLinePos
return pos return pos
} }
@ -181,36 +184,46 @@ export function findDir(
export function findProp( export function findProp(
node: ElementNode, node: ElementNode,
name: string, name: string,
dynamicOnly: boolean = false dynamicOnly: boolean = false,
allowEmpty: boolean = false
): ElementNode['props'][0] | undefined { ): ElementNode['props'][0] | undefined {
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const p = node.props[i] const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
if (dynamicOnly) continue if (dynamicOnly) continue
if (p.name === name && p.value) { if (p.name === name && (p.value || allowEmpty)) {
return p return p
} }
} else if ( } else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {
p.name === 'bind' &&
p.arg &&
p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
p.arg.isStatic &&
p.arg.content === name &&
p.exp
) {
return p return p
} }
} }
} }
export function createBlockExpression( export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
blockExp: BlockCodegenNode, return !!(
context: TransformContext arg &&
): SequenceExpression { arg.type === NodeTypes.SIMPLE_EXPRESSION &&
return createSequenceExpression([ arg.isStatic &&
createCallExpression(context.helper(OPEN_BLOCK)), arg.content === name
blockExp )
]) }
export function hasDynamicKeyVBind(node: ElementNode): boolean {
return node.props.some(
p =>
p.type === NodeTypes.DIRECTIVE &&
p.name === 'bind' &&
(!p.arg || // v-bind="obj"
p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
!p.arg.isStatic) // v-bind:[foo]
)
}
export function isText(
node: TemplateChildNode
): node is TextNode | InterpolationNode {
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
} }
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode { export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
@ -232,13 +245,13 @@ export function isSlotOutlet(
} }
export function injectProp( export function injectProp(
node: ElementCodegenNode | ComponentCodegenNode | SlotOutletCodegenNode, node: VNodeCall | RenderSlotCall,
prop: Property, prop: Property,
context: TransformContext context: TransformContext
) { ) {
let propsWithInjection: ObjectExpression | CallExpression let propsWithInjection: ObjectExpression | CallExpression
const props = const props =
node.callee === RENDER_SLOT ? node.arguments[2] : node.arguments[1] node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
if (props == null || isString(props)) { if (props == null || isString(props)) {
propsWithInjection = createObjectExpression([prop]) propsWithInjection = createObjectExpression([prop])
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) { } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
@ -253,7 +266,19 @@ export function injectProp(
} }
propsWithInjection = props propsWithInjection = props
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) { } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props.properties.unshift(prop) let alreadyExists = false
// check existing key to avoid overriding user provided keys
if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
const propKeyName = prop.key.content
alreadyExists = props.properties.some(
p =>
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === propKeyName
)
}
if (!alreadyExists) {
props.properties.unshift(prop)
}
propsWithInjection = props propsWithInjection = props
} else { } else {
// single v-bind with expression, return a merged replacement // single v-bind with expression, return a merged replacement
@ -262,10 +287,10 @@ export function injectProp(
props props
]) ])
} }
if (node.callee === RENDER_SLOT) { if (node.type === NodeTypes.VNODE_CALL) {
node.arguments[2] = propsWithInjection node.props = propsWithInjection
} else { } else {
node.arguments[1] = propsWithInjection node.arguments[2] = propsWithInjection
} }
} }

View File

@ -2,19 +2,16 @@
exports[`compile should contain standard transforms 1`] = ` exports[`compile should contain standard transforms 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = Vue
const _hoisted_1 = {} return function render(_ctx, _cache) {
with (_ctx) {
return function render() { const { createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, Fragment: _Fragment, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [ return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]), _createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]),
_createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]), _createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]),
_createVNode(\\"div\\", null, \\"test\\"), _createVNode(\\"div\\", null, \\"test\\"),
_createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"), _createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"),
_createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */) _createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */)
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }

View File

@ -5,7 +5,7 @@ describe('compile', () => {
const { code } = compile(`<div v-text="text"></div> const { code } = compile(`<div v-text="text"></div>
<div v-html="html"></div> <div v-html="html"></div>
<div v-cloak>test</div> <div v-cloak>test</div>
<div style="color=red">red</div> <div style="color:red">red</div>
<div :style="{color: 'green'}"></div>`) <div :style="{color: 'green'}"></div>`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()

View File

@ -1,5 +1,5 @@
import { import {
parse, baseParse as parse,
NodeTypes, NodeTypes,
ElementNode, ElementNode,
TextNode, TextNode,
@ -49,6 +49,22 @@ describe('DOM parser', () => {
}) })
}) })
test('textarea support interpolation', () => {
const ast = parse('<textarea><div>{{ foo }}</textarea>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.children).toMatchObject([
{ type: NodeTypes.TEXT, content: `<div>` },
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `foo`,
isStatic: false
}
}
])
})
test('style handles comments/elements as just a text', () => { test('style handles comments/elements as just a text', () => {
const ast = parse( const ast = parse(
'<style>some<div>text</div>and<!--comment--></style>', '<style>some<div>text</div>and<!--comment--></style>',
@ -100,11 +116,36 @@ describe('DOM parser', () => {
}) })
test('<pre> tag should preserve raw whitespace', () => { test('<pre> tag should preserve raw whitespace', () => {
const rawText = ` \na b \n c` const rawText = ` \na <div>foo \n bar</div> \n c`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children).toMatchObject([
{
type: NodeTypes.TEXT,
content: ` \na `
},
{
type: NodeTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: `foo \n bar`
}
]
},
{
type: NodeTypes.TEXT,
content: ` \n c`
}
])
})
// #908
test('<pre> tag should remove leading newline', () => {
const rawText = `\nhello`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions) const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({ expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
type: NodeTypes.TEXT, type: NodeTypes.TEXT,
content: rawText content: rawText.slice(1)
}) })
}) })
}) })

View File

@ -1,23 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform v-model input w/ dynamic v-bind 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, mergeProps: _mergeProps, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock(\\"input\\", _mergeProps(obj, {
\\"onUpdate:modelValue\\": $event => (model = $event)
}), null, 16 /* FULL_PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_bind = _resolveDirective(\\"bind\\")
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_directive_bind, val, key],
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model modifiers .lazy 1`] = ` exports[`compiler: transform v-model modifiers .lazy 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[ [
_vModelText, _vModelText,
model, model,
void 0, void 0,
{ lazy: true } { lazy: true }
] ]
])) ])
} }
}" }"
`; `;
@ -25,21 +59,20 @@ return function render() {
exports[`compiler: transform v-model modifiers .number 1`] = ` exports[`compiler: transform v-model modifiers .number 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[ [
_vModelText, _vModelText,
model, model,
void 0, void 0,
{ number: true } { number: true }
] ]
])) ])
} }
}" }"
`; `;
@ -47,21 +80,20 @@ return function render() {
exports[`compiler: transform v-model modifiers .trim 1`] = ` exports[`compiler: transform v-model modifiers .trim 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[ [
_vModelText, _vModelText,
model, model,
void 0, void 0,
{ trim: true } { trim: true }
] ]
])) ])
} }
}" }"
`; `;
@ -69,16 +101,15 @@ return function render() {
exports[`compiler: transform v-model simple expression 1`] = ` exports[`compiler: transform v-model simple expression 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model] [_vModelText, model]
])) ])
} }
}" }"
`; `;
@ -86,17 +117,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (checkbox) 1`] = ` exports[`compiler: transform v-model simple expression for input (checkbox) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelCheckbox: _vModelCheckbox, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelCheckbox: _vModelCheckbox, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"checkbox\\", type: \\"checkbox\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelCheckbox, model] [_vModelCheckbox, model]
])) ])
} }
}" }"
`; `;
@ -104,19 +134,18 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (dynamic type) 1`] = ` exports[`compiler: transform v-model simple expression for input (dynamic type) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelDynamic: _vModelDynamic, createVNode: _createVNode, withDirectives: _withDirectives, resolveDirective: _resolveDirective, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_bind = _resolveDirective(\\"bind\\") const _directive_bind = _resolveDirective(\\"bind\\")
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_directive_bind, foo, \\"type\\"], [_directive_bind, foo, \\"type\\"],
[_vModelDynamic, model] [_vModelDynamic, model]
])) ])
} }
}" }"
`; `;
@ -124,17 +153,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (radio) 1`] = ` exports[`compiler: transform v-model simple expression for input (radio) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelRadio: _vModelRadio, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelRadio: _vModelRadio, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"radio\\", type: \\"radio\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelRadio, model] [_vModelRadio, model]
])) ])
} }
}" }"
`; `;
@ -142,17 +170,16 @@ return function render() {
exports[`compiler: transform v-model simple expression for input (text) 1`] = ` exports[`compiler: transform v-model simple expression for input (text) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"input\\", { return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
type: \\"text\\", type: \\"text\\",
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model] [_vModelText, model]
])) ])
} }
}" }"
`; `;
@ -160,16 +187,15 @@ return function render() {
exports[`compiler: transform v-model simple expression for select 1`] = ` exports[`compiler: transform v-model simple expression for select 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"select\\", { return _withDirectives((_openBlock(), _createBlock(\\"select\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelSelect, model] [_vModelSelect, model]
])) ])
} }
}" }"
`; `;
@ -177,16 +203,15 @@ return function render() {
exports[`compiler: transform v-model simple expression for textarea 1`] = ` exports[`compiler: transform v-model simple expression for textarea 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"textarea\\", { return _withDirectives((_openBlock(), _createBlock(\\"textarea\\", {
modelValue: model,
\\"onUpdate:modelValue\\": $event => (model = $event) \\"onUpdate:modelValue\\": $event => (model = $event)
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]), [ }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelText, model] [_vModelText, model]
])) ])
} }
}" }"
`; `;

View File

@ -3,13 +3,13 @@
exports[`compiler: v-show transform simple expression 1`] = ` exports[`compiler: v-show transform simple expression 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render() { return function render(_ctx, _cache) {
with (this) { with (_ctx) {
const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, createBlock: _createBlock, openBlock: _openBlock } = _Vue const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _withDirectives(_createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */), [ return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 512 /* NEED_PATCH */)), [
[_vShow, a] [_vShow, a]
])) ])
} }
}" }"
`; `;

View File

@ -0,0 +1,124 @@
import { compile, NodeTypes, CREATE_STATIC } from '../../src'
import {
stringifyStatic,
StringifyThresholds
} from '../../src/transforms/stringifyStatic'
describe('stringify static html', () => {
function compileWithStringify(template: string) {
return compile(template, {
hoistStatic: true,
prefixIdentifiers: true,
transformHoist: stringifyStatic
})
}
function repeat(code: string, n: number): string {
return new Array(n)
.fill(0)
.map(() => code)
.join('')
}
test('should bail on non-eligible static trees', () => {
const { ast } = compileWithStringify(
`<div><div><div>hello</div><div>hello</div></div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be a normal vnode call
expect(ast.hoists[0].type).toBe(NodeTypes.VNODE_CALL)
})
test('should work on eligible content (elements with binding > 5)', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
test('should work on eligible content (elements > 20)', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span/>`,
StringifyThresholds.NODE_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span></span>`,
StringifyThresholds.NODE_COUNT
)}</div>`
)
]
})
})
test('serliazing constant bindings', () => {
const { ast } = compileWithStringify(
`<div><div :style="{ color: 'red' }">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
test('escape', () => {
const { ast } = compileWithStringify(
`<div><div>${repeat(
`<span :class="'foo' + '&gt;ar'">{{ 1 }} + {{ '<' }}</span>` +
`<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`
)
expect(ast.hoists.length).toBe(1)
// should be optimized now
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>`
)
]
})
})
})

View File

@ -1,10 +1,10 @@
import { import {
parse, baseParse as parse,
transform, transform,
CompilerOptions, CompilerOptions,
ElementNode, ElementNode,
NodeTypes, NodeTypes,
CallExpression VNodeCall
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { transformBind } from '../../../compiler-core/src/transforms/vBind' import { transformBind } from '../../../compiler-core/src/transforms/vBind'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
@ -26,17 +26,8 @@ function transformWithStyleTransform(
} }
describe('compiler: style transform', () => { describe('compiler: style transform', () => {
test('should transform into directive node and hoist value', () => { test('should transform into directive node', () => {
const { root, node } = transformWithStyleTransform( const { node } = transformWithStyleTransform(`<div style="color: red"/>`)
`<div style="color: red"/>`
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{"color":"red"}`,
isStatic: false
}
])
expect(node.props[0]).toMatchObject({ expect(node.props[0]).toMatchObject({
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: `bind`, name: `bind`,
@ -47,7 +38,7 @@ describe('compiler: style transform', () => {
}, },
exp: { exp: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"red"}`,
isStatic: false isStatic: false
} }
}) })
@ -60,7 +51,7 @@ describe('compiler: style transform', () => {
bind: transformBind bind: transformBind
} }
}) })
expect((node.codegenNode as CallExpression).arguments[1]).toMatchObject({ expect((node.codegenNode as VNodeCall).props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{ {
@ -71,13 +62,13 @@ describe('compiler: style transform', () => {
}, },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"red"}`,
isStatic: false isStatic: false
} }
} }
] ]
}) })
// should not cause the STYLE patchFlag to be attached // should not cause the STYLE patchFlag to be attached
expect((node.codegenNode as CallExpression).arguments.length).toBe(2) expect((node.codegenNode as VNodeCall).patchFlag).toBeUndefined()
}) })
}) })

View File

@ -1,30 +0,0 @@
import {
parse,
transform,
ElementNode,
CallExpression
} from '@vue/compiler-core'
import { transformCloak } from '../../src/transforms/vCloak'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
function transformWithCloak(template: string) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
cloak: transformCloak
}
})
return ast.children[0] as ElementNode
}
describe('compiler: v-cloak transform', () => {
test('should add no props to DOM', () => {
const node = transformWithCloak(`<div v-cloak/>`)
const codegenArgs = (node.codegenNode as CallExpression).arguments
// As v-cloak adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect(codegenArgs.length).toBe(1)
})
})

View File

@ -1,5 +1,5 @@
import { import {
parse, baseParse as parse,
transform, transform,
PlainElementNode, PlainElementNode,
CompilerOptions CompilerOptions
@ -29,15 +29,13 @@ describe('compiler: v-html transform', () => {
it('should convert v-html to innerHTML', () => { it('should convert v-html to innerHTML', () => {
const ast = transformWithVHtml(`<div v-html="test"/>`) const ast = transformWithVHtml(`<div v-html="test"/>`)
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({ expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ innerHTML: `[test]`
innerHTML: `[test]` }),
}), children: undefined,
`null`, patchFlag: genFlagText(PatchFlags.PROPS),
genFlagText(PatchFlags.PROPS), dynamicProps: `["innerHTML"]`
`["innerHTML"]`
]
}) })
}) })
@ -50,15 +48,13 @@ describe('compiler: v-html transform', () => {
[{ code: DOMErrorCodes.X_V_HTML_WITH_CHILDREN }] [{ code: DOMErrorCodes.X_V_HTML_WITH_CHILDREN }]
]) ])
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({ expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ innerHTML: `[test]`
innerHTML: `[test]` }),
}), children: undefined, // <-- children should have been removed
`null`, // <-- children should have been removed patchFlag: genFlagText(PatchFlags.PROPS),
genFlagText(PatchFlags.PROPS), dynamicProps: `["innerHTML"]`
`["innerHTML"]`
]
}) })
}) })

View File

@ -1,4 +1,9 @@
import { parse, transform, CompilerOptions, generate } from '@vue/compiler-core' import {
baseParse as parse,
transform,
CompilerOptions,
generate
} from '@vue/compiler-core'
import { transformModel } from '../../src/transforms/vModel' import { transformModel } from '../../src/transforms/vModel'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { DOMErrorCodes } from '../../src/errors' import { DOMErrorCodes } from '../../src/errors'
@ -58,6 +63,19 @@ describe('compiler: transform v-model', () => {
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('input w/ dynamic v-bind', () => {
const root = transformWithModel('<input v-bind="obj" v-model="model" />')
expect(root.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root).code).toMatchSnapshot()
const root2 = transformWithModel(
'<input v-bind:[key]="val" v-model="model" />'
)
expect(root2.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root2).code).toMatchSnapshot()
})
test('simple expression for select', () => { test('simple expression for select', () => {
const root = transformWithModel('<select v-model="model" />') const root = transformWithModel('<select v-model="model" />')

View File

@ -1,17 +1,21 @@
import { import {
parse, baseParse as parse,
transform, transform,
CompilerOptions, CompilerOptions,
ElementNode, ElementNode,
ObjectExpression, ObjectExpression,
CallExpression, NodeTypes,
NodeTypes VNodeCall
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers' import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression' import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils' import {
createObjectMatcher,
genFlagText
} from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
function parseWithVOn(template: string, options: CompilerOptions = {}) { function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
@ -24,8 +28,8 @@ function parseWithVOn(template: string, options: CompilerOptions = {}) {
}) })
return { return {
root: ast, root: ast,
props: (((ast.children[0] as ElementNode).codegenNode as CallExpression) props: (((ast.children[0] as ElementNode).codegenNode as VNodeCall)
.arguments[1] as ObjectExpression).properties .props as ObjectExpression).properties
} }
} }
@ -148,6 +152,58 @@ describe('compiler-dom: transform v-on', () => {
}) })
}) })
test('should transform click.right', () => {
const {
props: [prop]
} = parseWithVOn(`<div @click.right="test"/>`)
expect(prop.key).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `onContextmenu`
})
// dynamic
const {
props: [prop2]
} = parseWithVOn(`<div @[event].right="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onContextmenu" : ("on" + (event))
expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`(`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`).toLowerCase() === "onclick" ? "onContextmenu" : (`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`)`
]
})
})
test('should transform click.middle', () => {
const {
props: [prop]
} = parseWithVOn(`<div @click.middle="test"/>`)
expect(prop.key).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `onMouseup`
})
// dynamic
const {
props: [prop2]
} = parseWithVOn(`<div @[event].middle="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onMouseup" : ("on" + (event))
expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`(`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`).toLowerCase() === "onclick" ? "onMouseup" : (`,
{ children: [`"on" + (`, { content: 'event' }, `)`] },
`)`
]
})
})
test('cache handler w/ modifiers', () => { test('cache handler w/ modifiers', () => {
const { const {
root, root,
@ -157,8 +213,11 @@ describe('compiler-dom: transform v-on', () => {
cacheHandlers: true cacheHandlers: true
}) })
expect(root.cached).toBe(1) expect(root.cached).toBe(1)
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so it should have no
expect((root as any).children[0].codegenNode.arguments.length).toBe(2) // dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.HYDRATE_EVENTS)
)
expect(prop.value).toMatchObject({ expect(prop.value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 1,

View File

@ -1,4 +1,9 @@
import { parse, transform, generate, CompilerOptions } from '@vue/compiler-core' import {
baseParse as parse,
transform,
generate,
CompilerOptions
} from '@vue/compiler-core'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformShow } from '../../src/transforms/vShow' import { transformShow } from '../../src/transforms/vShow'
import { DOMErrorCodes } from '../../src/errors' import { DOMErrorCodes } from '../../src/errors'

View File

@ -1,5 +1,5 @@
import { import {
parse, baseParse as parse,
transform, transform,
PlainElementNode, PlainElementNode,
CompilerOptions CompilerOptions
@ -29,15 +29,13 @@ describe('compiler: v-text transform', () => {
it('should convert v-text to textContent', () => { it('should convert v-text to textContent', () => {
const ast = transformWithVText(`<div v-text="test"/>`) const ast = transformWithVText(`<div v-text="test"/>`)
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({ expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ textContent: `[test]`
textContent: `[test]` }),
}), children: undefined,
`null`, patchFlag: genFlagText(PatchFlags.PROPS),
genFlagText(PatchFlags.PROPS), dynamicProps: `["textContent"]`
`["textContent"]`
]
}) })
}) })
@ -50,15 +48,13 @@ describe('compiler: v-text transform', () => {
[{ code: DOMErrorCodes.X_V_TEXT_WITH_CHILDREN }] [{ code: DOMErrorCodes.X_V_TEXT_WITH_CHILDREN }]
]) ])
expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({ expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
arguments: [ tag: `"div"`,
`"div"`, props: createObjectMatcher({
createObjectMatcher({ textContent: `[test]`
textContent: `[test]` }),
}), children: undefined, // <-- children should have been removed
`null`, // <-- children should have been removed patchFlag: genFlagText(PatchFlags.PROPS),
genFlagText(PatchFlags.PROPS), dynamicProps: `["textContent"]`
`["textContent"]`
]
}) })
}) })

View File

@ -0,0 +1,140 @@
import { compile } from '../../src'
describe('compiler warnings', () => {
describe('Transition', () => {
function checkWarning(
template: string,
shouldWarn: boolean,
message = `<Transition> expects exactly one child element or component.`
) {
const spy = jest.fn()
compile(template.trim(), {
hoistStatic: true,
transformHoist: null,
onError: err => {
spy(err.message)
}
})
if (shouldWarn) expect(spy).toHaveBeenCalledWith(message)
else expect(spy).not.toHaveBeenCalled()
}
test('warns if multiple children', () => {
checkWarning(
`
<transition>
<div>hey</div>
<div>hey</div>
</transition>
`,
true
)
})
test('warns with v-for', () => {
checkWarning(
`
<transition>
<div v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with multiple v-if + v-for', () => {
checkWarning(
`
<transition>
<div v-if="a" v-for="i in items">hey</div>
<div v-else v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with template v-if', () => {
checkWarning(
`
<transition>
<template v-if="ok"></template>
</transition>
`,
true
)
})
test('warns with multiple templates', () => {
checkWarning(
`
<transition>
<template v-if="a"></template>
<template v-else></template>
</transition>
`,
true
)
})
test('warns if multiple children with v-if', () => {
checkWarning(
`
<transition>
<div v-if="one">hey</div>
<div v-if="other">hey</div>
</transition>
`,
true
)
})
test('does not warn with regular element', () => {
checkWarning(
`
<transition>
<div>hey</div>
</transition>
`,
false
)
})
test('does not warn with one single v-if', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else-if="b">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
})
})

View File

@ -1,18 +1,18 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.0.0-alpha.0", "version": "3.0.0-alpha.11",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js", "module": "dist/compiler-dom.esm-bundler.js",
"types": "dist/compiler-dom.d.ts",
"unpkg": "dist/compiler-dom/global.js",
"files": [ "files": [
"index.js", "index.js",
"dist" "dist"
], ],
"types": "dist/compiler-dom.d.ts",
"unpkg": "dist/compiler-dom/global.js",
"sideEffects": false, "sideEffects": false,
"buildOptions": { "buildOptions": {
"name": "VueDOMCompiler", "name": "VueCompilerDOM",
"formats": [ "formats": [
"esm-bundler", "esm-bundler",
"cjs", "cjs",
@ -22,7 +22,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/vuejs/vue.git" "url": "git+https://github.com/vuejs/vue-next.git"
}, },
"keywords": [ "keywords": [
"vue" "vue"
@ -30,10 +30,11 @@
"author": "Evan You", "author": "Evan You",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vue/issues" "url": "https://github.com/vuejs/vue-next/issues"
}, },
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-dom#readme", "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-dom#readme",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.0.0-alpha.0" "@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-core": "3.0.0-alpha.11"
} }
} }

View File

@ -28,7 +28,10 @@ export const enum DOMErrorCodes {
X_V_MODEL_ON_INVALID_ELEMENT, X_V_MODEL_ON_INVALID_ELEMENT,
X_V_MODEL_ARG_ON_ELEMENT, X_V_MODEL_ARG_ON_ELEMENT,
X_V_MODEL_ON_FILE_INPUT_ELEMENT, X_V_MODEL_ON_FILE_INPUT_ELEMENT,
X_V_SHOW_NO_EXPRESSION X_V_MODEL_UNNECESSARY_VALUE,
X_V_SHOW_NO_EXPRESSION,
X_TRANSITION_INVALID_CHILDREN,
__EXTEND_POINT__
} }
export const DOMErrorMessages: { [code: number]: string } = { export const DOMErrorMessages: { [code: number]: string } = {
@ -39,5 +42,7 @@ export const DOMErrorMessages: { [code: number]: string } = {
[DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`, [DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
[DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`, [DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`,
[DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: `v-model cannot used on file inputs since they are read-only. Use a v-on:change listener instead.`, [DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: `v-model cannot used on file inputs since they are read-only. Use a v-on:change listener instead.`,
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.` [DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE]: `Unnecessary value binding used alongside v-model. It will interfere with v-model's behavior.`,
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`,
[DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN]: `<Transition> expects exactly one child element or component.`
} }

View File

@ -1,45 +1,67 @@
import { import {
baseCompile, baseCompile,
baseParse,
CompilerOptions, CompilerOptions,
CodegenResult, CodegenResult,
isBuiltInType ParserOptions,
RootNode,
noopDirectiveTransform,
NodeTransform,
DirectiveTransform
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard' import { parserOptionsStandard } from './parserOptionsStandard'
import { transformStyle } from './transforms/transformStyle' import { transformStyle } from './transforms/transformStyle'
import { transformCloak } from './transforms/vCloak'
import { transformVHtml } from './transforms/vHtml' import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText' import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow' import { transformShow } from './transforms/vShow'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers' import { warnTransitionChildren } from './transforms/warnTransitionChildren'
import { stringifyStatic } from './transforms/stringifyStatic'
export const parserOptions = __BROWSER__
? parserOptionsMinimal
: parserOptionsStandard
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
...(__DEV__ ? [warnTransitionChildren] : [])
]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn, // override compiler-core
show: transformShow
}
export function compile( export function compile(
template: string, template: string,
options: CompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
return baseCompile(template, { return baseCompile(template, {
...parserOptions,
...options, ...options,
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard), nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
directiveTransforms: { directiveTransforms: {
cloak: transformCloak, ...DOMDirectiveTransforms,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn,
show: transformShow,
...(options.directiveTransforms || {}) ...(options.directiveTransforms || {})
}, },
isBuiltInComponent: tag => { transformHoist: __BROWSER__ ? null : stringifyStatic
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
}
}) })
} }
export function parse(template: string, options: ParserOptions = {}): RootNode {
return baseParse(template, {
...parserOptions,
...options
})
}
export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle'
export { createDOMCompilerError, DOMErrorCodes } from './errors'
export * from '@vue/compiler-core' export * from '@vue/compiler-core'

View File

@ -3,9 +3,11 @@ import {
ParserOptions, ParserOptions,
ElementNode, ElementNode,
Namespaces, Namespaces,
NodeTypes NodeTypes,
isBuiltInType
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared' import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
const isRawTextContainer = /*#__PURE__*/ makeMap( const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript', 'style,iframe,script,noscript',
@ -23,6 +25,14 @@ export const parserOptionsMinimal: ParserOptions = {
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
isBuiltInComponent: (tag: string): symbol | undefined => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
},
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces { getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML let ns = parent ? parent.ns : DOMNamespaces.HTML

View File

@ -0,0 +1,171 @@
import {
NodeTypes,
ElementNode,
TransformContext,
TemplateChildNode,
SimpleExpressionNode,
createCallExpression,
HoistTransform,
CREATE_STATIC,
ExpressionNode
} from '@vue/compiler-core'
import {
isVoidTag,
isString,
isSymbol,
escapeHtml,
toDisplayString,
normalizeClass,
normalizeStyle,
stringifyStyle
} from '@vue/shared'
// Turn eligible hoisted static trees into stringied static nodes, e.g.
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
// This is only performed in non-in-browser compilations.
export const stringifyStatic: HoistTransform = (node, context) => {
if (shouldOptimize(node)) {
return createCallExpression(context.helper(CREATE_STATIC), [
JSON.stringify(stringifyElement(node, context))
])
} else {
return node.codegenNode!
}
}
export const enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20
}
// Opt-in heuristics based on:
// 1. number of elements with attributes > 5.
// 2. OR: number of total nodes > 20
// For some simple trees, the performance can actually be worse.
// it is only worth it when the tree is complex enough
// (e.g. big piece of static content)
function shouldOptimize(node: ElementNode): boolean {
let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
let nodeThreshold = StringifyThresholds.NODE_COUNT
// TODO: check for cases where using innerHTML will result in different
// output compared to imperative node insertions.
// probably only need to check for most common case
// i.e. non-phrasing-content tags inside `<p>`
function walk(node: ElementNode) {
for (let i = 0; i < node.children.length; i++) {
if (--nodeThreshold === 0) {
return true
}
const child = node.children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.props.length > 0 && --bindingThreshold === 0) {
return true
}
if (walk(child)) {
return true
}
}
}
return false
}
return walk(node)
}
function stringifyElement(
node: ElementNode,
context: TransformContext
): string {
let res = `<${node.tag}`
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
res += ` ${p.name}`
if (p.value) {
res += `="${escapeHtml(p.value.content)}"`
}
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
// constant v-bind, e.g. :foo="1"
let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
evaluated = stringifyStyle(normalizeStyle(evaluated))
}
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluated
)}"`
}
}
if (context.scopeId) {
res += ` ${context.scopeId}`
}
res += `>`
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
}
if (!isVoidTag(node.tag)) {
res += `</${node.tag}>`
}
return res
}
function stringifyNode(
node: string | TemplateChildNode,
context: TransformContext
): string {
if (isString(node)) {
return node
}
if (isSymbol(node)) {
return ``
}
switch (node.type) {
case NodeTypes.ELEMENT:
return stringifyElement(node, context)
case NodeTypes.TEXT:
return escapeHtml(node.content)
case NodeTypes.COMMENT:
return `<!--${escapeHtml(node.content)}-->`
case NodeTypes.INTERPOLATION:
return escapeHtml(toDisplayString(evaluateConstant(node.content)))
case NodeTypes.COMPOUND_EXPRESSION:
return escapeHtml(evaluateConstant(node))
case NodeTypes.TEXT_CALL:
return stringifyNode(node.content, context)
default:
// static trees will not contain if/for nodes
return ''
}
}
// __UNSAFE__
// Reason: eval.
// It's technically safe to eval because only constant expressions are possible
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transformExpressions)
function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return ${exp.content}`)()
} else {
// compound
let res = ``
exp.children.forEach(c => {
if (isString(c) || isSymbol(c)) {
return
}
if (c.type === NodeTypes.TEXT) {
res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
res += evaluateConstant(c)
}
})
return res
}
}

View File

@ -1,7 +1,9 @@
import { import {
NodeTransform, NodeTransform,
NodeTypes, NodeTypes,
createSimpleExpression createSimpleExpression,
SimpleExpressionNode,
SourceLocation
} from '@vue/compiler-core' } from '@vue/compiler-core'
// Parse inline CSS strings for static style attributes into an object. // Parse inline CSS strings for static style attributes into an object.
@ -15,13 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => {
node.props.forEach((p, i) => { node.props.forEach((p, i) => {
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) { if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
// replace p with an expression node // replace p with an expression node
const parsed = JSON.stringify(parseInlineCSS(p.value.content))
const exp = context.hoist(createSimpleExpression(parsed, false, p.loc))
node.props[i] = { node.props[i] = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: `bind`, name: `bind`,
arg: createSimpleExpression(`style`, true, p.loc), arg: createSimpleExpression(`style`, true, p.loc),
exp, exp: parseInlineCSS(p.value.content, p.loc),
modifiers: [], modifiers: [],
loc: p.loc loc: p.loc
} }
@ -33,7 +33,10 @@ export const transformStyle: NodeTransform = (node, context) => {
const listDelimiterRE = /;(?![^(]*\))/g const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:(.+)/ const propertyDelimiterRE = /:(.+)/
function parseInlineCSS(cssText: string): Record<string, string> { function parseInlineCSS(
cssText: string,
loc: SourceLocation
): SimpleExpressionNode {
const res: Record<string, string> = {} const res: Record<string, string> = {}
cssText.split(listDelimiterRE).forEach(item => { cssText.split(listDelimiterRE).forEach(item => {
if (item) { if (item) {
@ -41,5 +44,5 @@ function parseInlineCSS(cssText: string): Record<string, string> {
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()) tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
} }
}) })
return res return createSimpleExpression(JSON.stringify(res), false, loc, true)
} }

View File

@ -1,5 +0,0 @@
import { DirectiveTransform } from 'packages/compiler-core/src/transform'
export const transformCloak: DirectiveTransform = (node, context) => {
return { props: [], needRuntime: false }
}

View File

@ -24,7 +24,6 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`innerHTML`, true, loc), createSimpleExpression(`innerHTML`, true, loc),
exp || createSimpleExpression('', true) exp || createSimpleExpression('', true)
) )
], ]
needRuntime: false
} }
} }

View File

@ -3,7 +3,8 @@ import {
DirectiveTransform, DirectiveTransform,
ElementTypes, ElementTypes,
findProp, findProp,
NodeTypes NodeTypes,
hasDynamicKeyVBind
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors' import { createDOMCompilerError, DOMErrorCodes } from '../errors'
import { import {
@ -16,68 +17,104 @@ import {
export const transformModel: DirectiveTransform = (dir, node, context) => { export const transformModel: DirectiveTransform = (dir, node, context) => {
const baseResult = baseTransform(dir, node, context) const baseResult = baseTransform(dir, node, context)
// base transform has errors // base transform has errors OR component v-model (only need props)
if (!baseResult.props.length) { if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult return baseResult
} }
const { tag, tagType } = node if (dir.arg) {
if (tagType === ElementTypes.ELEMENT) { context.onError(
if (dir.arg) { createDOMCompilerError(
context.onError( DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
createDOMCompilerError( dir.arg.loc
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
dir.arg.loc
)
) )
} )
}
if (tag === 'input' || tag === 'textarea' || tag === 'select') { function checkDuplicatedValue() {
let directiveToUse = V_MODEL_TEXT const value = findProp(node, 'value')
let isInvalidType = false if (value) {
if (tag === 'input') {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc
)
)
break
}
}
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
} else {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT, DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
dir.loc value.loc
) )
) )
} }
} }
const { tag } = node
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
let directiveToUse = V_MODEL_TEXT
let isInvalidType = false
if (tag === 'input') {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc
)
)
break
default:
// text type
__DEV__ && checkDuplicatedValue()
break
}
}
} else if (hasDynamicKeyVBind(node)) {
// element has bindings with dynamic keys, which can possibly contain
// "type".
directiveToUse = V_MODEL_DYNAMIC
} else {
// text type
__DEV__ && checkDuplicatedValue()
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
} else if (tag === 'textarea') {
__DEV__ && checkDuplicatedValue()
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
} else {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
dir.loc
)
)
}
// native vmodel doesn't need the `modelValue` props since they are also
// passed to the runtime as `binding.value`. removing it reduces code size.
baseResult.props = baseResult.props.filter(p => {
if (
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === 'modelValue'
) {
return false
}
return true
})
return baseResult return baseResult
} }

View File

@ -5,7 +5,9 @@ import {
createCallExpression, createCallExpression,
createObjectExpression, createObjectExpression,
createSimpleExpression, createSimpleExpression,
NodeTypes NodeTypes,
createCompoundExpression,
ExpressionNode
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers' import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers'
import { makeMap } from '@vue/shared' import { makeMap } from '@vue/shared'
@ -52,6 +54,24 @@ const generateModifiers = (modifiers: string[]) => {
} }
} }
const transformClick = (key: ExpressionNode, event: string) => {
const isStaticClick =
key.type === NodeTypes.SIMPLE_EXPRESSION &&
key.isStatic &&
key.content.toLowerCase() === 'onclick'
return isStaticClick
? createSimpleExpression(event, true)
: key.type !== NodeTypes.SIMPLE_EXPRESSION
? createCompoundExpression([
`(`,
key,
`).toLowerCase() === "onclick" ? "${event}" : (`,
key,
`)`
])
: key
}
export const transformOn: DirectiveTransform = (dir, node, context) => { export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransform(dir, node, context, baseResult => { return baseTransform(dir, node, context, baseResult => {
const { modifiers } = dir const { modifiers } = dir
@ -64,6 +84,14 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
eventOptionModifiers eventOptionModifiers
} = generateModifiers(modifiers) } = generateModifiers(modifiers)
// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('right')) {
key = transformClick(key, `onContextmenu`)
}
if (nonKeyModifiers.includes('middle')) {
key = transformClick(key, `onMouseup`)
}
if (nonKeyModifiers.length) { if (nonKeyModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [ handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp, handlerExp,
@ -102,8 +130,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
} }
return { return {
props: [createObjectProperty(key, handlerExp)], props: [createObjectProperty(key, handlerExp)]
needRuntime: false
} }
}) })
} }

View File

@ -24,7 +24,6 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`textContent`, true, loc), createSimpleExpression(`textContent`, true, loc),
exp || createSimpleExpression('', true) exp || createSimpleExpression('', true)
) )
], ]
needRuntime: false
} }
} }

View File

@ -0,0 +1,43 @@
import {
NodeTransform,
NodeTypes,
ElementTypes,
ComponentNode,
IfBranchNode
} from '@vue/compiler-core'
import { TRANSITION } from '../runtimeHelpers'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
export const warnTransitionChildren: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT
) {
const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) {
return () => {
if (node.children.length && hasMultipleChildren(node)) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
{
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
}
)
)
}
}
}
}
}
function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
const child = node.children[0]
return (
node.children.length !== 1 ||
child.type === NodeTypes.FOR ||
(child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
)
}

View File

@ -2,7 +2,7 @@
exports[`source map 1`] = ` exports[`source map 1`] = `
Object { Object {
"mappings": ";;;;UAAA,aACE;IAAK,gCAAMA,WAAM", "mappings": ";;;wBACE,aAA8B;IAAzB,aAAmB,4BAAbA,WAAM",
"names": Array [ "names": Array [
"render", "render",
], ],
@ -20,8 +20,8 @@ Object {
exports[`template errors 1`] = ` exports[`template errors 1`] = `
Array [ Array [
[SyntaxError: Invalid JavaScript expression. (2:13)], [SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],
[SyntaxError: v-bind is missing expression. (1:6)], [SyntaxError: v-bind is missing expression.],
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements. (2:17)], [SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.],
] ]
`; `;

View File

@ -1,39 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler sfc: transform asset url support uri fragment 1`] = ` exports[`compiler sfc: transform asset url support uri fragment 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from '@svg/file.svg' import _imports_0 from '@svg/file.svg'
const _hoisted_1 = _imports_0 + '#fragment' const _hoisted_1 = _imports_0 + '#fragment'
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"use\\", { href: _hoisted_1 }))
return (openBlock(), createBlock(\\"use\\", { href: _hoisted_1 }))
}" }"
`; `;
exports[`compiler sfc: transform asset url support uri is empty 1`] = ` exports[`compiler sfc: transform asset url support uri is empty 1`] = `
"import { createVNode, createBlock, openBlock } from \\"vue\\" "import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(\\"use\\", { href: '' }))
return (openBlock(), createBlock(\\"use\\", { href: '' }))
}" }"
`; `;
exports[`compiler sfc: transform asset url transform assetUrls 1`] = ` exports[`compiler sfc: transform asset url transform assetUrls 1`] = `
"import { createVNode, createBlock, Fragment, openBlock } from \\"vue\\" "import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from './logo.png' import _imports_0 from './logo.png'
import _imports_1 from 'fixtures/logo.png' import _imports_1 from 'fixtures/logo.png'
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(_Fragment, null, [
return (openBlock(), createBlock(Fragment, null, [ _createVNode(\\"img\\", { src: _imports_0 }),
createVNode(\\"img\\", { src: _imports_0 }), _createVNode(\\"img\\", { src: _imports_1 }),
createVNode(\\"img\\", { src: _imports_1 }), _createVNode(\\"img\\", { src: _imports_1 }),
createVNode(\\"img\\", { src: _imports_1 }) _createVNode(\\"img\\", { src: \\"http://example.com/fixtures/logo.png\\" }),
_createVNode(\\"img\\", { src: \\"/fixtures/logo.png\\" })
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler sfc: transform srcset transform srcset 1`] = ` exports[`compiler sfc: transform srcset transform srcset 1`] = `
"import { createVNode, createBlock, Fragment, openBlock } from \\"vue\\" "import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import _imports_0 from './logo.png' import _imports_0 from './logo.png'
@ -12,37 +12,49 @@ const _hoisted_4 = _imports_0 + ', ' + _imports_0 + '2x'
const _hoisted_5 = _imports_0 + '2x, ' + _imports_0 const _hoisted_5 = _imports_0 + '2x, ' + _imports_0
const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x' const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_8 = \\"/logo.png\\" + ', ' + _imports_0 + '2x'
export default function render() { export function render(_ctx, _cache) {
const _ctx = this return (_openBlock(), _createBlock(_Fragment, null, [
return (openBlock(), createBlock(Fragment, null, [ _createVNode(\\"img\\", {
createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_1 srcset: _hoisted_1
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_2 srcset: _hoisted_2
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_3 srcset: _hoisted_3
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_4 srcset: _hoisted_4
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_5 srcset: _hoisted_5
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_6 srcset: _hoisted_6
}), }),
createVNode(\\"img\\", { _createVNode(\\"img\\", {
src: \\"./logo.png\\", src: \\"./logo.png\\",
srcset: _hoisted_7 srcset: _hoisted_7
}),
_createVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}),
_createVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}),
_createVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
}) })
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"

View File

@ -0,0 +1,237 @@
import { compileStyle } from '../src/compileStyle'
import { mockWarn } from '@vue/shared'
function compile(source: string): string {
const res = compileStyle({
source,
filename: 'test.css',
id: 'test'
})
if (res.errors.length) {
res.errors.forEach(err => {
console.error(err)
})
expect(res.errors.length).toBe(0)
}
return res.code
}
describe('SFC scoped CSS', () => {
mockWarn()
test('simple selectors', () => {
expect(compile(`h1 { color: red; }`)).toMatch(`h1[test] { color: red;`)
expect(compile(`.foo { color: red; }`)).toMatch(`.foo[test] { color: red;`)
})
test('descendent selector', () => {
expect(compile(`h1 .foo { color: red; }`)).toMatch(
`h1 .foo[test] { color: red;`
)
})
test('multiple selectors', () => {
expect(compile(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`h1 .foo[test], .bar[test], .baz[test] { color: red;`
)
})
test('pseudo class', () => {
expect(compile(`.foo:after { color: red; }`)).toMatch(
`.foo[test]:after { color: red;`
)
})
test('pseudo element', () => {
expect(compile(`::selection { display: none; }`)).toMatch(
'[test]::selection {'
)
})
test('spaces before pseudo element', () => {
const code = compile(`.abc, ::selection { color: red; }`)
expect(code).toMatch('.abc[test],')
expect(code).toMatch('[test]::selection {')
})
test('::v-deep', () => {
expect(compile(`::v-deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(compile(`::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
"[test] .foo .bar { color: red;
}"
`)
expect(compile(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux[test] .foo .bar { color: red;
}"
`)
})
test('::v-slotted', () => {
expect(compile(`::v-slotted(.foo) { color: red; }`)).toMatchInlineSnapshot(`
".foo[test-s] { color: red;
}"
`)
expect(compile(`::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar[test-s] { color: red;
}"
`)
expect(compile(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux .foo .bar[test-s] { color: red;
}"
`)
})
test('::v-global', () => {
expect(compile(`::v-global(.foo) { color: red; }`)).toMatchInlineSnapshot(`
".foo { color: red;
}"
`)
expect(compile(`::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
// global ignores anything before it
expect(compile(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
})
test('media query', () => {
expect(compile(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(`
"@media print {
.foo[test] { color: red
}}"
`)
})
test('supports query', () => {
expect(compile(`@supports(display: grid) { .foo { display: grid }}`))
.toMatchInlineSnapshot(`
"@supports(display: grid) {
.foo[test] { display: grid
}}"
`)
})
test('scoped keyframes', () => {
const style = compile(`
.anim {
animation: color 5s infinite, other 5s;
}
.anim-2 {
animation-name: color;
animation-duration: 5s;
}
.anim-3 {
animation: 5s color infinite, 5s other;
}
.anim-multiple {
animation: color 5s infinite, opacity 2s;
}
.anim-multiple-2 {
animation-name: color, opacity;
animation-duration: 5s, 2s;
}
@keyframes color {
from { color: red; }
to { color: green; }
}
@-webkit-keyframes color {
from { color: red; }
to { color: green; }
}
@keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
`)
expect(style).toContain(
`.anim[test] {\n animation: color-test 5s infinite, other 5s;`
)
expect(style).toContain(`.anim-2[test] {\n animation-name: color-test`)
expect(style).toContain(
`.anim-3[test] {\n animation: 5s color-test infinite, 5s other;`
)
expect(style).toContain(`@keyframes color-test {`)
expect(style).toContain(`@-webkit-keyframes color-test {`)
expect(style).toContain(
`.anim-multiple[test] {\n animation: color-test 5s infinite,opacity-test 2s;`
)
expect(style).toContain(
`.anim-multiple-2[test] {\n animation-name: color-test,opacity-test;`
)
expect(style).toContain(`@keyframes opacity-test {`)
expect(style).toContain(`@-webkit-keyframes opacity-test {`)
})
// vue-loader/#1370
test('spaces after selector', () => {
const { code } = compileStyle({
source: `.foo , .bar { color: red; }`,
filename: 'test.css',
id: 'test'
})
expect(code).toMatchInlineSnapshot(`
".foo[test], .bar[test] { color: red;
}"
`)
})
describe('deprecated syntax', () => {
test('::v-deep as combinator', () => {
expect(compile(`::v-deep .foo { color: red; }`)).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(compile(`.bar ::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
".bar[test] .foo { color: red;
}"
`)
expect(
`::v-deep usage as a combinator has been deprecated.`
).toHaveBeenWarned()
})
test('>>> (deprecated syntax)', () => {
const code = compile(`>>> .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`
).toHaveBeenWarned()
})
test('/deep/ (deprecated syntax)', () => {
const code = compile(`/deep/ .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`
).toHaveBeenWarned()
})
})
})

View File

@ -9,7 +9,7 @@ test('should work', () => {
expect(result.errors.length).toBe(0) expect(result.errors.length).toBe(0)
expect(result.source).toBe(source) expect(result.source).toBe(source)
// should expose render fn // should expose render fn
expect(result.code).toMatch(`export default function render()`) expect(result.code).toMatch(`export function render(`)
}) })
test('preprocess pug', () => { test('preprocess pug', () => {
@ -23,7 +23,7 @@ body
</template> </template>
`, `,
{ filename: 'example.vue', sourceMap: true } { filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock ).descriptor.template as SFCTemplateBlock
const result = compileTemplate({ const result = compileTemplate({
filename: 'example.vue', filename: 'example.vue',
@ -35,10 +35,10 @@ body
}) })
test('warn missing preprocessor', () => { test('warn missing preprocessor', () => {
const template = parse(`<template lang="unknownLang">\n</template>\n`, { const template = parse(`<template lang="unknownLang">hi</template>\n`, {
filename: 'example.vue', filename: 'example.vue',
sourceMap: true sourceMap: true
}).template as SFCTemplateBlock }).descriptor.template as SFCTemplateBlock
const result = compileTemplate({ const result = compileTemplate({
filename: 'example.vue', filename: 'example.vue',
@ -50,7 +50,7 @@ test('warn missing preprocessor', () => {
}) })
test('transform asset url options', () => { test('transform asset url options', () => {
const input = { source: `<foo bar="baz"/>`, filename: 'example.vue' } const input = { source: `<foo bar="~baz"/>`, filename: 'example.vue' }
// Object option // Object option
const { code: code1 } = compileTemplate({ const { code: code1 } = compileTemplate({
...input, ...input,
@ -73,7 +73,7 @@ test('source map', () => {
</template> </template>
`, `,
{ filename: 'example.vue', sourceMap: true } { filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock ).descriptor.template as SFCTemplateBlock
const result = compileTemplate({ const result = compileTemplate({
filename: 'example.vue', filename: 'example.vue',
@ -100,7 +100,7 @@ test('preprocessor errors', () => {
</template> </template>
`, `,
{ filename: 'example.vue', sourceMap: true } { filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock ).descriptor.template as SFCTemplateBlock
const result = compileTemplate({ const result = compileTemplate({
filename: 'example.vue', filename: 'example.vue',

View File

@ -1,21 +1,40 @@
import { parse } from '../src' import { parse } from '../src'
import { mockWarn } from '@vue/runtime-test' import { mockWarn } from '@vue/shared'
import { baseParse, baseCompile } from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map'
describe('compiler:sfc', () => { describe('compiler:sfc', () => {
mockWarn() mockWarn()
describe('source map', () => { describe('source map', () => {
test('style block', () => { test('style block', () => {
const style = parse(`<style>\n.color {\n color: red;\n }\n</style>\n`) // Padding determines how many blank lines will there be before the style block
.styles[0] const padding = Math.round(Math.random() * 10)
// TODO need to actually test this with SourceMapConsumer const style = parse(
`${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n`
).descriptor.styles[0]
expect(style.map).not.toBeUndefined() expect(style.map).not.toBeUndefined()
const consumer = new SourceMapConsumer(style.map!)
consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
})
}) })
test('script block', () => { test('script block', () => {
const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`).script // Padding determines how many blank lines will there be before the style block
// TODO need to actually test this with SourceMapConsumer const padding = Math.round(Math.random() * 10)
const script = parse(
`${'\n'.repeat(padding)}<script>\nconsole.log(1)\n }\n</script>\n`
).descriptor.script
expect(script!.map).not.toBeUndefined() expect(script!.map).not.toBeUndefined()
const consumer = new SourceMapConsumer(script!.map!)
consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
})
}) })
}) })
@ -30,12 +49,12 @@ export default {}
<style> <style>
h1 { color: red } h1 { color: red }
</style>` </style>`
const padFalse = parse(content.trim(), { pad: false }) const padFalse = parse(content.trim(), { pad: false }).descriptor
expect(padFalse.template!.content).toBe('\n<div></div>\n') expect(padFalse.template!.content).toBe('\n<div></div>\n')
expect(padFalse.script!.content).toBe('\nexport default {}\n') expect(padFalse.script!.content).toBe('\nexport default {}\n')
expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n') expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
const padTrue = parse(content.trim(), { pad: true }) const padTrue = parse(content.trim(), { pad: true }).descriptor
expect(padTrue.script!.content).toBe( expect(padTrue.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n' Array(3 + 1).join('//\n') + '\nexport default {}\n'
) )
@ -43,7 +62,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n' Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
) )
const padLine = parse(content.trim(), { pad: 'line' }) const padLine = parse(content.trim(), { pad: 'line' }).descriptor
expect(padLine.script!.content).toBe( expect(padLine.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n' Array(3 + 1).join('//\n') + '\nexport default {}\n'
) )
@ -51,7 +70,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n' Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
) )
const padSpace = parse(content.trim(), { pad: 'space' }) const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
expect(padSpace.script!.content).toBe( expect(padSpace.script!.content).toBe(
`<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') + `<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
'\nexport default {}\n' '\nexport default {}\n'
@ -65,13 +84,55 @@ h1 { color: red }
}) })
test('should ignore nodes with no content', () => { test('should ignore nodes with no content', () => {
expect(parse(`<template/>`).template).toBe(null) expect(parse(`<template/>`).descriptor.template).toBe(null)
expect(parse(`<script/>`).script).toBe(null) expect(parse(`<script/>`).descriptor.script).toBe(null)
expect(parse(`<style/>`).styles.length).toBe(0) expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
expect(parse(`<custom/>`).customBlocks.length).toBe(0) expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
}) })
describe('error', () => { test('handle empty nodes with src attribute', () => {
const { descriptor } = parse(`<script src="com"/>`)
expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.content).toBeFalsy()
expect(descriptor.script!.attrs['src']).toBe('com')
})
test('nested templates', () => {
const content = `
<template v-if="ok">ok</template>
<div><div></div></div>
`
const { descriptor } = parse(`<template>${content}</template>`)
expect(descriptor.template!.content).toBe(content)
})
test('error tolerance', () => {
const { errors } = parse(`<template>`)
expect(errors.length).toBe(1)
})
test('should parse as DOM by default', () => {
const { errors } = parse(`<template><input></template>`)
expect(errors.length).toBe(0)
})
test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, {
compiler: {
parse: baseParse,
compile: baseCompile
}
})
expect(errors.length).toBe(1)
})
test('treat custom blocks as raw text', () => {
const { errors, descriptor } = parse(`<foo> <-& </foo>`)
expect(errors.length).toBe(0)
expect(descriptor.customBlocks[0].content).toBe(` <-& `)
})
describe('warnings', () => {
test('should only allow single template element', () => { test('should only allow single template element', () => {
parse(`<template><div/></template><template><div/></template>`) parse(`<template><div/></template><template><div/></template>`)
expect( expect(

View File

@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core' import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformAssetUrl } from '../src/templateTransformAssetUrl' import { transformAssetUrl } from '../src/templateTransformAssetUrl'
import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind' import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithAssetUrls(template: string) { function compileWithAssetUrls(template: string) {
const ast = parse(template) const ast = baseParse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformAssetUrl, transformElement], nodeTransforms: [transformAssetUrl, transformElement],
directiveTransforms: { directiveTransforms: {
@ -20,6 +20,8 @@ describe('compiler sfc: transform asset url', () => {
<img src="./logo.png"/> <img src="./logo.png"/>
<img src="~fixtures/logo.png"/> <img src="~fixtures/logo.png"/>
<img src="~/fixtures/logo.png"/> <img src="~/fixtures/logo.png"/>
<img src="http://example.com/fixtures/logo.png"/>
<img src="/fixtures/logo.png"/>
`) `)
expect(result.code).toMatchSnapshot() expect(result.code).toMatchSnapshot()

View File

@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core' import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformSrcset } from '../src/templateTransformSrcset' import { transformSrcset } from '../src/templateTransformSrcset'
import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind' import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithSrcset(template: string) { function compileWithSrcset(template: string) {
const ast = parse(template) const ast = baseParse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformSrcset, transformElement], nodeTransforms: [transformSrcset, transformElement],
directiveTransforms: { directiveTransforms: {
@ -24,6 +24,9 @@ describe('compiler sfc: transform srcset', () => {
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png"/> <img src="./logo.png" srcset="./logo.png 2x, ./logo.png"/>
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x"/> <img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x"/>
<img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x"/> <img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x"/>
<img src="/logo.png" srcset="/logo.png, /logo.png 2x"/>
<img src="https://example.com/logo.png" srcset="https://example.com/logo.png, https://example.com/logo.png 2x"/>
<img src="/logo.png" srcset="/logo.png, ./logo.png 2x"/>
`) `)
expect(result.code).toMatchSnapshot() expect(result.code).toMatchSnapshot()

View File

@ -1,12 +1,12 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.0.0-alpha.0", "version": "3.0.0-alpha.11",
"description": "@vue/compiler-sfc", "description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js", "main": "dist/compiler-sfc.cjs.js",
"types": "dist/compiler-sfc.d.ts",
"files": [ "files": [
"dist" "dist"
], ],
"types": "dist/compiler-sfc.d.ts",
"buildOptions": { "buildOptions": {
"prod": false, "prod": false,
"formats": [ "formats": [
@ -15,7 +15,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/vuejs/vue.git" "url": "git+https://github.com/vuejs/vue-next.git"
}, },
"keywords": [ "keywords": [
"vue" "vue"
@ -23,19 +23,24 @@
"author": "Evan You", "author": "Evan You",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vue/issues" "url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc#readme",
"peerDependencies": {
"vue": "3.0.0-alpha.11"
}, },
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.0.0-alpha.0", "@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.0", "@vue/compiler-core": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.11",
"@vue/compiler-ssr": "3.0.0-alpha.11",
"consolidate": "^0.15.1", "consolidate": "^0.15.1",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"postcss": "^7.0.21", "postcss": "^7.0.21",
"postcss-selector-parser": "^6.0.2", "postcss-selector-parser": "^6.0.2",
"source-map": "^0.7.3" "source-map": "^0.6.1"
}, },
"devDependencies": { "devDependencies": {
"@types/consolidate": "^0.14.0", "@types/consolidate": "^0.14.0",

View File

@ -10,7 +10,7 @@ import {
} from './stylePreprocessors' } from './stylePreprocessors'
import { RawSourceMap } from 'source-map' import { RawSourceMap } from 'source-map'
export interface StyleCompileOptions { export interface SFCStyleCompileOptions {
source: string source: string
filename: string filename: string
id: string id: string
@ -23,11 +23,11 @@ export interface StyleCompileOptions {
postcssPlugins?: any[] postcssPlugins?: any[]
} }
export interface AsyncStyleCompileOptions extends StyleCompileOptions { export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
isAsync?: boolean isAsync?: boolean
} }
export interface StyleCompileResults { export interface SFCStyleCompileResults {
code: string code: string
map: RawSourceMap | undefined map: RawSourceMap | undefined
rawResult: LazyResult | Result | undefined rawResult: LazyResult | Result | undefined
@ -35,22 +35,25 @@ export interface StyleCompileResults {
} }
export function compileStyle( export function compileStyle(
options: StyleCompileOptions options: SFCStyleCompileOptions
): StyleCompileResults { ): SFCStyleCompileResults {
return doCompileStyle({ ...options, isAsync: false }) as StyleCompileResults return doCompileStyle({
...options,
isAsync: false
}) as SFCStyleCompileResults
} }
export function compileStyleAsync( export function compileStyleAsync(
options: StyleCompileOptions options: SFCStyleCompileOptions
): Promise<StyleCompileResults> { ): Promise<SFCStyleCompileResults> {
return doCompileStyle({ ...options, isAsync: true }) as Promise< return doCompileStyle({ ...options, isAsync: true }) as Promise<
StyleCompileResults SFCStyleCompileResults
> >
} }
export function doCompileStyle( export function doCompileStyle(
options: AsyncStyleCompileOptions options: SFCAsyncStyleCompileOptions
): StyleCompileResults | Promise<StyleCompileResults> { ): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
const { const {
filename, filename,
id, id,
@ -131,7 +134,7 @@ export function doCompileStyle(
} }
function preprocess( function preprocess(
options: StyleCompileOptions, options: SFCStyleCompileOptions,
preprocessor: StylePreprocessor preprocessor: StylePreprocessor
): StylePreprocessorResults { ): StylePreprocessorResults {
return preprocessor.render(options.source, options.map, { return preprocessor.render(options.source, options.map, {

View File

@ -2,9 +2,11 @@ import {
CompilerOptions, CompilerOptions,
CodegenResult, CodegenResult,
CompilerError, CompilerError,
NodeTransform NodeTransform,
ParserOptions,
RootNode
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { RawSourceMap } from 'source-map' import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
transformAssetUrl, transformAssetUrl,
AssetURLOptions, AssetURLOptions,
@ -14,7 +16,12 @@ import { transformSrcset } from './templateTransformSrcset'
import { isObject } from '@vue/shared' import { isObject } from '@vue/shared'
import consolidate from 'consolidate' import consolidate from 'consolidate'
export interface TemplateCompileResults { export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode
}
export interface SFCTemplateCompileResults {
code: string code: string
source: string source: string
tips: string[] tips: string[]
@ -22,13 +29,11 @@ export interface TemplateCompileResults {
map?: RawSourceMap map?: RawSourceMap
} }
export interface TemplateCompiler { export interface SFCTemplateCompileOptions {
compile(template: string, options: CompilerOptions): CodegenResult
}
export interface TemplateCompileOptions {
source: string source: string
filename: string filename: string
ssr?: boolean
inMap?: RawSourceMap
compiler?: TemplateCompiler compiler?: TemplateCompiler
compilerOptions?: CompilerOptions compilerOptions?: CompilerOptions
preprocessLang?: string preprocessLang?: string
@ -37,7 +42,7 @@ export interface TemplateCompileOptions {
} }
function preprocess( function preprocess(
{ source, filename, preprocessOptions }: TemplateCompileOptions, { source, filename, preprocessOptions }: SFCTemplateCompileOptions,
preprocessor: any preprocessor: any
): string { ): string {
// Consolidate exposes a callback based API, but the callback is in fact // Consolidate exposes a callback based API, but the callback is in fact
@ -59,8 +64,8 @@ function preprocess(
} }
export function compileTemplate( export function compileTemplate(
options: TemplateCompileOptions options: SFCTemplateCompileOptions
): TemplateCompileResults { ): SFCTemplateCompileResults {
const { preprocessLang } = options const { preprocessLang } = options
const preprocessor = const preprocessor =
preprocessLang && consolidate[preprocessLang as keyof typeof consolidate] preprocessLang && consolidate[preprocessLang as keyof typeof consolidate]
@ -100,11 +105,13 @@ export function compileTemplate(
function doCompileTemplate({ function doCompileTemplate({
filename, filename,
inMap,
source, source,
compiler = require('@vue/compiler-dom'), ssr = false,
compiler = ssr ? require('@vue/compiler-ssr') : require('@vue/compiler-dom'),
compilerOptions = {}, compilerOptions = {},
transformAssetUrls transformAssetUrls
}: TemplateCompileOptions): TemplateCompileResults { }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
const errors: CompilerError[] = [] const errors: CompilerError[] = []
let nodeTransforms: NodeTransform[] = [] let nodeTransforms: NodeTransform[] = []
@ -117,7 +124,7 @@ function doCompileTemplate({
nodeTransforms = [transformAssetUrl, transformSrcset] nodeTransforms = [transformAssetUrl, transformSrcset]
} }
const { code, map } = compiler.compile(source, { let { code, map } = compiler.compile(source, {
mode: 'module', mode: 'module',
prefixIdentifiers: true, prefixIdentifiers: true,
hoistStatic: true, hoistStatic: true,
@ -128,5 +135,91 @@ function doCompileTemplate({
sourceMap: true, sourceMap: true,
onError: e => errors.push(e) onError: e => errors.push(e)
}) })
// 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
// reflect the original line numbers.
if (inMap) {
if (map) {
map = mapLines(inMap, map)
}
if (errors.length) {
patchErrors(errors, source, inMap)
}
}
return { code, source, errors, tips: [], map } return { code, source, errors, tips: [], map }
} }
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
if (!oldMap) return newMap
if (!newMap) return oldMap
const oldMapConsumer = new SourceMapConsumer(oldMap)
const newMapConsumer = new SourceMapConsumer(newMap)
const mergedMapGenerator = new SourceMapGenerator()
newMapConsumer.eachMapping(m => {
if (m.originalLine == null) {
return
}
const origPosInOldMap = oldMapConsumer.originalPositionFor({
line: m.originalLine,
column: m.originalColumn
})
if (origPosInOldMap.source == null) {
return
}
mergedMapGenerator.addMapping({
generated: {
line: m.generatedLine,
column: m.generatedColumn
},
original: {
line: origPosInOldMap.line, // map line
// use current column, since the oldMap produced by @vue/compiler-sfc
// does not
column: m.originalColumn
},
source: origPosInOldMap.source,
name: origPosInOldMap.name
})
})
// source-map's type definition is incomplete
const generator = mergedMapGenerator as any
;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
generator._sources.add(sourceFile)
const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
if (sourceContent != null) {
mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
}
})
generator._sourceRoot = oldMap.sourceRoot
generator._file = oldMap.file
return generator.toJSON()
}
function patchErrors(
errors: CompilerError[],
source: string,
inMap: RawSourceMap
) {
const originalSource = inMap.sourcesContent![0]
const offset = originalSource.indexOf(source)
const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
errors.forEach(err => {
if (err.loc) {
err.loc.start.line += lineOffset
err.loc.start.offset += offset
if (err.loc.end !== err.loc.start) {
err.loc.end.line += lineOffset
err.loc.end.offset += offset
}
}
})
}

View File

@ -14,8 +14,12 @@ export {
} from './parse' } from './parse'
export { export {
TemplateCompiler, TemplateCompiler,
TemplateCompileOptions, SFCTemplateCompileOptions,
TemplateCompileResults SFCTemplateCompileResults
} from './compileTemplate' } from './compileTemplate'
export { StyleCompileOptions, StyleCompileResults } from './compileStyle' export { SFCStyleCompileOptions, SFCStyleCompileResults } from './compileStyle'
export { CompilerOptions, generateCodeFrame } from '@vue/compiler-core' export {
CompilerOptions,
CompilerError,
generateCodeFrame
} from '@vue/compiler-core'

Some files were not shown because too many files have changed in this diff Show More