Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2023-11-29 21:10:26 +08:00
commit 377723d8b2
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
150 changed files with 5949 additions and 11163 deletions

View File

@ -3,6 +3,15 @@
const DOMGlobals = ['window', 'document']
const NodeGlobals = ['module', 'require']
const banConstEnum = {
selector: 'TSEnumDeclaration[const=true]',
message:
'Please use non-const enums. This project automatically inlines enums.'
}
/**
* @type {import('eslint-define-config').ESLintConfig}
*/
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
@ -16,6 +25,7 @@ module.exports = {
'no-restricted-syntax': [
'error',
banConstEnum,
// since we target ES2015 for baseline support, we need to forbid object
// rest spread usage in destructure as it compiles into a verbose helper.
'ObjectPattern > RestElement',
@ -52,12 +62,10 @@ module.exports = {
},
// Packages targeting Node
{
files: [
'packages/{compiler-sfc,compiler-ssr,server-renderer,reactivity-transform}/**'
],
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
rules: {
'no-restricted-globals': ['error', ...DOMGlobals],
'no-restricted-syntax': 'off'
'no-restricted-syntax': ['error', banConstEnum]
}
},
// Private package, browser only + no syntax restrictions
@ -65,7 +73,7 @@ module.exports = {
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
'no-restricted-syntax': 'off'
'no-restricted-syntax': ['error', banConstEnum]
}
},
// JavaScript files
@ -81,7 +89,7 @@ module.exports = {
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
rules: {
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off'
'no-restricted-syntax': ['error', banConstEnum]
}
}
]

View File

@ -1,3 +1,59 @@
# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
### Bug Fixes
* **deps:** update compiler to ^7.23.4 ([#9681](https://github.com/vuejs/core/issues/9681)) ([31f6ebc](https://github.com/vuejs/core/commit/31f6ebc4df84490ed29fb75e7bf4259200eb51f0))
* **parser:** directive arg should be undefined on shorthands with no arg ([e49dffc](https://github.com/vuejs/core/commit/e49dffc9ece86bddf094b9ad4ad15eb4856d6277))
### Features
* **dx:** link errors to docs in prod build ([#9165](https://github.com/vuejs/core/issues/9165)) ([9f8ba98](https://github.com/vuejs/core/commit/9f8ba9821fe166f77e63fa940e9e7e13ec3344fa))
# [3.4.0-alpha.2](https://github.com/vuejs/core/compare/v3.3.9...v3.4.0-alpha.2) (2023-11-27)
### Bug Fixes
* avoid confusing breakage in @vitejs/plugin-vue ([ceec69c](https://github.com/vuejs/core/commit/ceec69c8ccb96c433a4a506ad2e85e276998bade))
* **compiler-core:** fix line/column tracking when fast forwarding ([2e65ea4](https://github.com/vuejs/core/commit/2e65ea481f74db8649df8110a031cbdc98f98c84))
* **compiler-sfc:** fix ast reuse for ssr ([fb619cf](https://github.com/vuejs/core/commit/fb619cf9a440239f0ba88e327d10001a6a3c8171))
* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([c6083dc](https://github.com/vuejs/core/commit/c6083dcad31f3e9292c687fada9e32f287e2317f))
* **compiler-sfc:** use correct compiler when re-parsing in ssr mode ([678378a](https://github.com/vuejs/core/commit/678378afd559481badb486b243722b6287862e09))
* feat!: remove reactivity transform (#9321) ([79b8a09](https://github.com/vuejs/core/commit/79b8a0905bf363bf82edd2096fef10c3db6d9c3c)), closes [#9321](https://github.com/vuejs/core/issues/9321)
### Features
* **compiler-core:** support specifying root namespace when parsing ([40f72d5](https://github.com/vuejs/core/commit/40f72d5e50b389cb11b7ca13461aa2a75ddacdb4))
* **compiler-core:** support v-bind shorthand for key and value with the same name ([#9451](https://github.com/vuejs/core/issues/9451)) ([26399aa](https://github.com/vuejs/core/commit/26399aa6fac1596b294ffeba06bb498d86f5508c))
* **compiler:** improve parsing tolerance for language-tools ([41ff68e](https://github.com/vuejs/core/commit/41ff68ea579d933333392146625560359acb728a))
* **reactivity:** expose last result for computed getter ([#9497](https://github.com/vuejs/core/issues/9497)) ([48b47a1](https://github.com/vuejs/core/commit/48b47a1ab63577e2dbd91947eea544e3ef185b85))
### Performance Improvements
* avoid sfc source map unnecessary serialization and parsing ([f15d2f6](https://github.com/vuejs/core/commit/f15d2f6cf69c0c39f8dfb5c33122790c68bf92e2))
* **codegen:** optimize line / column calculation during codegen ([3be53d9](https://github.com/vuejs/core/commit/3be53d9b974dae1a10eb795cade71ae765e17574))
* **codegen:** optimize source map generation ([c11002f](https://github.com/vuejs/core/commit/c11002f16afd243a2b15b546816e73882eea9e4d))
* **compiler-sfc:** remove magic-string trim on script ([e8e3ec6](https://github.com/vuejs/core/commit/e8e3ec6ca7392e43975c75b56eaaa711d5ea9410))
* **compiler-sfc:** use faster source map addMapping ([50cde7c](https://github.com/vuejs/core/commit/50cde7cfbcc49022ba88f5f69fa9b930b483c282))
* optimize away isBuiltInType ([66c0ed0](https://github.com/vuejs/core/commit/66c0ed0a3c1c6f37dafc6b1c52b75c6bf60e3136))
* optimize makeMap ([ae6fba9](https://github.com/vuejs/core/commit/ae6fba94954bac6430902f77b0d1113a98a75b18))
* optimize position cloning ([2073236](https://github.com/vuejs/core/commit/20732366b9b3530d33b842cf1fc985919afb9317))
### BREAKING CHANGES
* Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html
## [3.3.9](https://github.com/vuejs/core/compare/v3.3.8...v3.3.9) (2023-11-25)
@ -47,6 +103,19 @@
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)
### Features
* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd))
* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433))
* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b))
* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464)
* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d))
## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "0.0.0-vapor",
"packageManager": "pnpm@8.10.5",
"packageManager": "pnpm@8.11.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js vue vue-vapor",
@ -35,7 +35,7 @@
"serve": "serve",
"open": "open http://localhost:3000/packages/template-explorer/local.html",
"build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity reactivity-transform shared -af cjs",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
"build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
"build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
@ -59,8 +59,8 @@
"node": ">=18.12.0"
},
"devDependencies": {
"@babel/parser": "^7.23.3",
"@babel/types": "^7.23.3",
"@babel/parser": "^7.23.4",
"@babel/types": "^7.23.4",
"@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.1",
@ -68,8 +68,10 @@
"@rollup/plugin-replace": "^5.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.9.2",
"@typescript-eslint/parser": "^6.11.0",
"@types/minimist": "^1.2.5",
"@types/node": "^20.10.0",
"@types/semver": "^7.5.5",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-istanbul": "^0.34.6",
"@vue/consolidate": "0.17.3",
"conventional-changelog-cli": "^4.1.0",
@ -77,6 +79,7 @@
"esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.54.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
@ -104,7 +107,7 @@
"terser": "^5.22.0",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2",
"tsx": "^4.1.4",
"tsx": "^4.5.0",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^0.34.6"

View File

@ -40,6 +40,7 @@ import { PatchFlags } from '@vue/shared'
function createRoot(options: Partial<RootNode> = {}): RootNode {
return {
type: NodeTypes.ROOT,
source: '',
children: [],
helpers: new Set(),
components: [],

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,6 @@ export function createElementWithCodegen(
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
isSelfClosing: false,
props: [],
children: [],
codegenNode: {

View File

@ -1,4 +1,4 @@
import { baseParse } from '../src/parse'
import { baseParse } from '../src/parser'
import { transform, NodeTransform } from '../src/transform'
import {
ElementNode,

View File

@ -22,7 +22,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
onClick: () => {
for (let i = 0; i < _ctx.list.length; i++) {
_ctx.log(i)
}
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}"
@ -36,7 +36,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
onClick: () => {
for (const x in _ctx.list) {
_ctx.log(x)
}
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}"
@ -50,7 +50,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
onClick: () => {
for (const x of _ctx.list) {
_ctx.log(x)
}
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}"

View File

@ -1195,25 +1195,13 @@ describe('compiler: element transform', () => {
})
})
// TODO remove in 3.4
test('v-is', () => {
const { node, root } = parseWithBind(`<div v-is="'foo'" />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
test('is casting', () => {
const { node, root } = parseWithBind(`<div is="vue:foo" />`)
expect(root.helpers).toContain(RESOLVE_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
type: NodeTypes.VNODE_CALL,
tag: '_component_foo'
})
expect('v-is="component-name" has been deprecated').toHaveBeenWarned()
})
// #3934

View File

@ -128,51 +128,24 @@ describe('compiler: expression transform', () => {
{
content: `_ctx.foo`,
loc: {
source: `foo`,
start: {
offset: 3,
line: 1,
column: 4
},
end: {
offset: 6,
line: 1,
column: 7
}
start: { offset: 3, line: 1, column: 4 },
end: { offset: 6, line: 1, column: 7 }
}
},
`(`,
{
content: `_ctx.baz`,
loc: {
source: `baz`,
start: {
offset: 7,
line: 1,
column: 8
},
end: {
offset: 10,
line: 1,
column: 11
}
start: { offset: 7, line: 1, column: 8 },
end: { offset: 10, line: 1, column: 11 }
}
},
` + 1, { key: `,
{
content: `_ctx.kuz`,
loc: {
source: `kuz`,
start: {
offset: 23,
line: 1,
column: 24
},
end: {
offset: 26,
line: 1,
column: 27
}
start: { offset: 23, line: 1, column: 24 },
end: { offset: 26, line: 1, column: 27 }
}
},
` })`
@ -539,7 +512,7 @@ describe('compiler: expression transform', () => {
`<div @click="() => {
for (const x in list) {
log(x)
}
}
}"/>`
)
expect(code).not.toMatch(`_ctx.x`)
@ -551,7 +524,7 @@ describe('compiler: expression transform', () => {
`<div @click="() => {
for (const x of list) {
log(x)
}
}
}"/>`
)
expect(code).not.toMatch(`_ctx.x`)
@ -563,7 +536,7 @@ describe('compiler: expression transform', () => {
`<div @click="() => {
for (let i = 0; i < list.length; i++) {
log(i)
}
}
}"/>`
)
expect(code).not.toMatch(`_ctx.i`)

View File

@ -376,7 +376,6 @@ describe('compiler: transform <slot> outlets', () => {
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
loc: {
source: `v-foo`,
start: {
offset: index,
line: 1,

View File

@ -72,6 +72,44 @@ describe('compiler: transform v-bind', () => {
})
})
test('no expression', () => {
const node = parseWithVBind(`<div v-bind:id />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
isStatic: true,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 }
}
},
value: {
content: `id`,
isStatic: false,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 }
}
}
})
})
test('no expression (shorthand)', () => {
const node = parseWithVBind(`<div :id />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
isStatic: true
},
value: {
content: `id`,
isStatic: false
}
})
})
test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -98,9 +136,9 @@ describe('compiler: transform v-bind', () => {
})
})
test('should error if no expression', () => {
test('should error if empty expression', () => {
const onError = vi.fn()
const node = parseWithVBind(`<div v-bind:arg />`, { onError })
const node = parseWithVBind(`<div v-bind:arg="" />`, { onError })
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
@ -111,7 +149,7 @@ describe('compiler: transform v-bind', () => {
},
end: {
line: 1,
column: 16
column: 19
}
}
})
@ -142,6 +180,21 @@ describe('compiler: transform v-bind', () => {
})
})
test('.camel modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.camel />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.camel modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -219,6 +272,21 @@ describe('compiler: transform v-bind', () => {
})
})
test('.prop modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:fooBar.prop />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `.fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.prop modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -296,6 +364,21 @@ describe('compiler: transform v-bind', () => {
})
})
test('.prop modifier (shortband) w/ no expression', () => {
const node = parseWithVBind(`<div .fooBar />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `.fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.attr modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
@ -310,4 +393,19 @@ describe('compiler: transform v-bind', () => {
}
})
})
test('.attr modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `^foo-bar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
})

View File

@ -1,4 +1,4 @@
import { baseParse as parse } from '../../src/parse'
import { baseParse as parse } from '../../src/parser'
import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'

View File

@ -1,4 +1,4 @@
import { baseParse as parse } from '../../src/parse'
import { baseParse as parse } from '../../src/parser'
import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement'

View File

@ -850,7 +850,6 @@ describe('compiler: transform component slots', () => {
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {
offset: index,
line: 1,
@ -873,7 +872,6 @@ describe('compiler: transform component slots', () => {
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
@ -896,7 +894,6 @@ describe('compiler: transform component slots', () => {
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
@ -919,7 +916,6 @@ describe('compiler: transform component slots', () => {
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_MISPLACED,
loc: {
source: `v-slot`,
start: {
offset: index,
line: 1,

View File

@ -1,7 +1,6 @@
import { TransformContext } from '../src'
import { Position } from '../src/ast'
import {
getInnerRange,
advancePositionWithClone,
isMemberExpressionNode,
isMemberExpressionBrowser,
@ -41,32 +40,6 @@ describe('advancePositionWithClone', () => {
})
})
describe('getInnerRange', () => {
const loc1 = {
source: 'foo\nbar\nbaz',
start: p(1, 1, 0),
end: p(3, 3, 11)
}
test('at start', () => {
const loc2 = getInnerRange(loc1, 0, 4)
expect(loc2.start).toEqual(loc1.start)
expect(loc2.end.column).toBe(1)
expect(loc2.end.line).toBe(2)
expect(loc2.end.offset).toBe(4)
})
test('in between', () => {
const loc2 = getInnerRange(loc1, 4, 3)
expect(loc2.start.column).toBe(1)
expect(loc2.start.line).toBe(2)
expect(loc2.start.offset).toBe(4)
expect(loc2.end.column).toBe(4)
expect(loc2.end.line).toBe(2)
expect(loc2.end.offset).toBe(7)
})
})
describe('isMemberExpression', () => {
function commonAssertions(fn: (str: string) => boolean) {
// should work

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.3.9",
"version": "3.4.0-alpha.3",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@ -32,12 +32,13 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-core#readme",
"dependencies": {
"@babel/parser": "^7.23.3",
"@babel/parser": "^7.23.4",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
},
"devDependencies": {
"@babel/types": "^7.23.3"
"@babel/types": "^7.23.4"
}
}

View File

@ -1,5 +1,4 @@
import { isString } from '@vue/shared'
import { ForParseResult } from './transforms/vFor'
import {
RENDER_SLOT,
CREATE_SLOTS,
@ -17,15 +16,16 @@ import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
// Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces like SVG and MathML are declared by platform specific
// compilers.
// More namespaces can be declared by platform specific compilers.
export type Namespace = number
export const enum Namespaces {
HTML
export enum Namespaces {
HTML,
SVG,
MATH_ML
}
export const enum NodeTypes {
export enum NodeTypes {
ROOT,
ELEMENT,
TEXT,
@ -59,7 +59,7 @@ export const enum NodeTypes {
JS_RETURN_STATEMENT
}
export const enum ElementTypes {
export enum ElementTypes {
ELEMENT,
COMPONENT,
SLOT,
@ -102,6 +102,7 @@ export type TemplateChildNode =
export interface RootNode extends Node {
type: NodeTypes.ROOT
source: string
children: TemplateChildNode[]
helpers: Set<symbol>
components: string[]
@ -112,6 +113,7 @@ export interface RootNode extends Node {
temps: number
ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
transformed?: boolean
// v2 compat only
filters?: string[]
@ -128,9 +130,10 @@ export interface BaseElementNode extends Node {
ns: Namespace
tag: string
tagType: ElementTypes
isSelfClosing: boolean
props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[]
isSelfClosing?: boolean
innerLoc?: SourceLocation // only for SFC root level elements
}
export interface PlainElementNode extends BaseElementNode {
@ -182,19 +185,28 @@ export interface CommentNode extends Node {
export interface AttributeNode extends Node {
type: NodeTypes.ATTRIBUTE
name: string
nameLoc: SourceLocation
value: TextNode | undefined
}
export interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE
/**
* the normalized name without prefix or shorthands, e.g. "bind", "on"
*/
name: string
/**
* the raw attribute name, preserving shorthand, and including arg & modifiers
* this is only used during parse.
*/
rawName?: string
exp: ExpressionNode | undefined
arg: ExpressionNode | undefined
modifiers: string[]
/**
* optional property to cache the expression parse result for v-for
*/
parseResult?: ForParseResult
forParseResult?: ForParseResult
}
/**
@ -202,7 +214,7 @@ export interface DirectiveNode extends Node {
* Higher levels implies lower levels. e.g. a node that can be stringified
* can always be hoisted and skipped for patch.
*/
export const enum ConstantTypes {
export enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
@ -276,6 +288,14 @@ export interface ForNode extends Node {
codegenNode?: ForCodegenNode
}
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
finalized: boolean
}
export interface TextCallNode extends Node {
type: NodeTypes.TEXT_CALL
content: TextNode | InterpolationNode | CompoundExpressionNode
@ -547,17 +567,18 @@ export interface ForIteratorExpression extends FunctionExpression {
// associated with template nodes, so their source locations are just a stub.
// Container types like CompoundExpression also don't need a real location.
export const locStub: SourceLocation = {
source: '',
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 }
end: { line: 1, column: 1, offset: 0 },
source: ''
}
export function createRoot(
children: TemplateChildNode[],
loc = locStub
source = ''
): RootNode {
return {
type: NodeTypes.ROOT,
source,
children,
helpers: new Set(),
components: [],
@ -567,7 +588,7 @@ export function createRoot(
cached: 0,
temps: 0,
codegenNode: undefined,
loc
loc: locStub
}
}

View File

@ -69,6 +69,13 @@ export interface CodegenResult {
map?: RawSourceMap
}
enum NewlineType {
Start = 0,
End = -1,
None = -2,
Unknown = -3
}
export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
source: string
@ -80,7 +87,7 @@ export interface CodegenContext
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
push(code: string, newlineIndex?: number, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
@ -116,7 +123,7 @@ function createCodegenContext(
ssr,
isTS,
inSSR,
source: ast.loc.source,
source: ast.source,
code: ``,
column: 1,
line: 1,
@ -127,7 +134,7 @@ function createCodegenContext(
helper(key) {
return `_${helperNameMap[key]}`
},
push(code, node) {
push(code, newlineIndex = NewlineType.None, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
@ -140,7 +147,41 @@ function createCodegenContext(
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation(context, code)
} else {
// fast paths
context.offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`
)
}
context.column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`
)
}
context.line++
context.column = code.length - newlineIndex
}
}
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
@ -162,28 +203,31 @@ function createCodegenContext(
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
context.push('\n' + ` `.repeat(n), NewlineType.Start)
}
function addMapping(loc: Position, name?: string) {
context.map!.addMapping({
name,
source: context.filename,
original: {
line: loc.line,
column: loc.column - 1 // source-map column is 0 based
},
generated: {
line: context.line,
column: context.column - 1
}
function addMapping(loc: Position, name: string | null = null) {
// we use the private property to directly add the mapping
// because the addMapping() implementation in source-map-js has a bunch of
// unnecessary arg and validation checks that are pure overhead in our case.
const { _names, _mappings } = context.map!
if (name !== null && !_names.has(name)) _names.add(name)
_mappings.add({
originalLine: loc.line,
originalColumn: loc.column - 1, // source-map column is 0 based
generatedLine: context.line,
generatedColumn: context.column - 1,
source: filename,
// @ts-ignore it is possible to be null
name
})
}
if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator()
context.map!.setSourceContent(filename, context.source)
context.map.setSourceContent(filename, context.source)
context.map._sources.add(filename)
}
return context
@ -250,8 +294,10 @@ export function generate(
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasHelpers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue`)
push(`\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue\n`,
NewlineType.End
)
newline()
}
}
@ -282,7 +328,7 @@ export function generate(
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
push(`\n`, NewlineType.Start)
newline()
}
@ -308,8 +354,7 @@ export function generate(
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
map: context.map ? context.map.toJSON() : undefined
}
}
@ -334,11 +379,14 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const helpers = Array.from(ast.helpers)
if (helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`,
NewlineType.End
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
push(`const _Vue = ${VueBinding}\n`, NewlineType.End)
// 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.
@ -353,7 +401,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
.filter(helper => helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
push(`const { ${staticHelpers} } = _Vue\n`, NewlineType.End)
}
}
}
@ -363,7 +411,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`,
NewlineType.End
)
}
genHoists(ast.hoists, context)
@ -402,18 +451,21 @@ function genModulePreamble(
push(
`import { ${helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
push(
`\n// Binding optimization for webpack code-split\nconst ${helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
.join(', ')}\n`,
NewlineType.End
)
} else {
push(
`import { ${helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
}
}
@ -422,7 +474,8 @@ function genModulePreamble(
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
.join(', ')} } from "${ssrRuntimeModuleName}"\n`,
NewlineType.End
)
}
@ -554,7 +607,7 @@ function genNodeList(
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
push(node, NewlineType.Unknown)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
@ -573,7 +626,7 @@ function genNodeList(
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
context.push(node, NewlineType.Unknown)
return
}
if (isSymbol(node)) {
@ -671,12 +724,16 @@ function genText(
node: TextNode | SimpleExpressionNode,
context: CodegenContext
) {
context.push(JSON.stringify(node.content), node)
context.push(JSON.stringify(node.content), NewlineType.Unknown, node)
}
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
context.push(
isStatic ? JSON.stringify(content) : content,
NewlineType.Unknown,
node
)
}
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
@ -694,7 +751,7 @@ function genCompoundExpression(
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
context.push(child, NewlineType.Unknown)
} else {
genNode(child, context)
}
@ -715,9 +772,9 @@ function genExpressionAsPropertyKey(
const text = isSimpleIdentifier(node.content)
? node.content
: JSON.stringify(node.content)
push(text, node)
push(text, NewlineType.None, node)
} else {
push(`[${node.content}]`, node)
push(`[${node.content}]`, NewlineType.Unknown, node)
}
}
@ -726,7 +783,11 @@ function genComment(node: CommentNode, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
push(
`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`,
NewlineType.Unknown,
node
)
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
@ -754,7 +815,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const callHelper: symbol = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, node)
push(helper(callHelper) + `(`, NewlineType.None, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
@ -785,7 +846,7 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(callee + `(`, node)
push(callee + `(`, NewlineType.None, node)
genNodeList(node.arguments, context)
push(`)`)
}
@ -794,7 +855,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline } = context
const { properties } = node
if (!properties.length) {
push(`{}`, node)
push(`{}`, NewlineType.None, node)
return
}
const multilines =
@ -834,7 +895,7 @@ function genFunctionExpression(
// wrap slot functions with owner context
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node)
push(`(`, NewlineType.None, node)
if (isArray(params)) {
genNodeList(params, context)
} else if (params) {
@ -934,7 +995,7 @@ function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
for (let i = 0; i < l; i++) {
const e = node.elements[i]
if (isString(e)) {
push(e.replace(/(`|\$|\\)/g, '\\$1'))
push(e.replace(/(`|\$|\\)/g, '\\$1'), NewlineType.Unknown)
} else {
push('${')
if (multilines) indent()

View File

@ -1,6 +1,6 @@
import { SourceLocation } from '../ast'
import { CompilerError } from '../errors'
import { ParserContext } from '../parse'
import { MergedParserOptions } from '../parser'
import { TransformContext } from '../transform'
export type CompilerCompatConfig = Partial<
@ -13,10 +13,9 @@ export interface CompilerCompatOptions {
compatConfig?: CompilerCompatConfig
}
export const enum CompilerDeprecationTypes {
export enum CompilerDeprecationTypes {
COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC',
COMPILER_V_BIND_PROP = 'COMPILER_V_BIND_PROP',
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
@ -47,12 +46,6 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
link: `https://v3-migration.vuejs.org/breaking-changes/v-model.html`
},
[CompilerDeprecationTypes.COMPILER_V_BIND_PROP]: {
message:
`.prop modifier for v-bind has been removed and no longer necessary. ` +
`Vue 3 will automatically set a binding as DOM property when appropriate.`
},
[CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: {
message:
`v-bind="obj" usage is now order sensitive and behaves like JavaScript ` +
@ -100,12 +93,9 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
function getCompatValue(
key: CompilerDeprecationTypes | 'MODE',
context: ParserContext | TransformContext
{ compatConfig }: MergedParserOptions | TransformContext
) {
const config = (context as ParserContext).options
? (context as ParserContext).options.compatConfig
: (context as TransformContext).compatConfig
const value = config && config[key]
const value = compatConfig && compatConfig[key]
if (key === 'MODE') {
return value || 3 // compiler defaults to v3 behavior
} else {
@ -115,7 +105,7 @@ function getCompatValue(
export function isCompatEnabled(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext
context: MergedParserOptions | TransformContext
) {
const mode = getCompatValue('MODE', context)
const value = getCompatValue(key, context)
@ -126,7 +116,7 @@ export function isCompatEnabled(
export function checkCompatEnabled(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext,
context: MergedParserOptions | TransformContext,
loc: SourceLocation | null,
...args: any[]
): boolean {
@ -139,7 +129,7 @@ export function checkCompatEnabled(
export function warnDeprecation(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext,
context: MergedParserOptions | TransformContext,
loc: SourceLocation | null,
...args: any[]
) {

View File

@ -1,5 +1,5 @@
import { CompilerOptions } from './options'
import { baseParse } from './parse'
import { baseParse } from './parser'
import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
@ -59,7 +59,7 @@ export function getBaseTransformPreset(
// 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,
source: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
@ -82,7 +82,7 @@ export function baseCompile(
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
const ast = isString(template) ? baseParse(template, options) : template
const ast = isString(source) ? baseParse(source, options) : source
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)

View File

@ -30,14 +30,14 @@ export function createCompilerError<T extends number>(
const msg =
__DEV__ || !__BROWSER__
? (messages || errorMessages)[code] + (additionalMessage || ``)
: code
: `https://vuejs.org/errors/#compiler-${code}`
const error = new SyntaxError(String(msg)) as InferCompilerError<T>
error.code = code
error.loc = loc
return error
}
export const enum ErrorCodes {
export enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
CDATA_IN_HTML_CONTENT,

View File

@ -10,7 +10,7 @@ export {
type BindingMetadata,
BindingTypes
} from './options'
export { baseParse, TextModes } from './parse'
export { baseParse } from './parser'
export {
transform,
type TransformContext,
@ -24,6 +24,7 @@ export {
export { generate, type CodegenContext, type CodegenResult } from './codegen'
export {
ErrorCodes,
errorMessages,
createCompilerError,
type CoreCompilerError,
type CompilerError

View File

@ -1,5 +1,10 @@
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
import { TextModes } from './parse'
import {
ElementNode,
Namespace,
TemplateChildNode,
ParentNode,
Namespaces
} from './ast'
import { CompilerError } from './errors'
import {
NodeTransform,
@ -17,6 +22,24 @@ export interface ErrorHandlingOptions {
export interface ParserOptions
extends ErrorHandlingOptions,
CompilerCompatOptions {
/**
* Base mode is platform agnostic and only parses HTML-like template syntax,
* treating all tags the same way. Specific tag parsing behavior can be
* configured by higher-level compilers.
*
* HTML mode adds additional logic for handling special parsing behavior in
* `<script>`, `<style>`,`<title>` and `<textarea>`.
* The logic is handled inside compiler-core for efficiency.
*
* SFC mode treats content of all root-level tags except `<template>` as plain
* text.
*/
parseMode?: 'base' | 'html' | 'sfc'
/**
* Specify the root namespace to use when parsing a template.
* Defaults to `Namespaces.HTML` (0).
*/
ns?: Namespaces
/**
* e.g. platform native elements, e.g. `<div>` for browsers
*/
@ -40,14 +63,11 @@ export interface ParserOptions
/**
* Get tag namespace
*/
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
/**
* Get text parsing mode for this element
*/
getTextMode?: (
node: ElementNode,
parent: ElementNode | undefined
) => TextModes
getNamespace?: (
tag: string,
parent: ElementNode | undefined,
rootNamespace: Namespace
) => Namespace
/**
* @default ['{{', '}}']
*/
@ -57,7 +77,8 @@ export interface ParserOptions
*/
whitespace?: 'preserve' | 'condense'
/**
* Only needed for DOM compilers
* Only used for DOM compilers that runs in the browser.
* In non-browser builds, this option is ignored.
*/
decodeEntities?: (rawText: string, asAttr: boolean) => string
/**
@ -73,7 +94,7 @@ export type HoistTransform = (
parent: ParentNode
) => void
export const enum BindingTypes {
export enum BindingTypes {
/**
* returned from data()
*/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,997 @@
import {
AttributeNode,
ConstantTypes,
DirectiveNode,
ElementNode,
ElementTypes,
ForParseResult,
Namespaces,
NodeTypes,
RootNode,
SimpleExpressionNode,
SourceLocation,
TemplateChildNode,
createRoot,
createSimpleExpression
} from './ast'
import { ParserOptions } from './options'
import Tokenizer, {
CharCodes,
ParseMode,
QuoteType,
Sequences,
State,
isWhitespace,
toCharCodes
} from './tokenizer'
import {
CompilerCompatOptions,
CompilerDeprecationTypes,
checkCompatEnabled,
isCompatEnabled,
warnDeprecation
} from './compat/compatConfig'
import { NO, extend } from '@vue/shared'
import {
ErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn
} from './errors'
import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
import { decodeHTML } from 'entities/lib/decode.js'
type OptionalOptions =
| 'decodeEntities'
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
| keyof CompilerCompatOptions
export type MergedParserOptions = Omit<
Required<ParserOptions>,
OptionalOptions
> &
Pick<ParserOptions, OptionalOptions>
export const defaultParserOptions: MergedParserOptions = {
parseMode: 'base',
ns: Namespaces.HTML,
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
}
let currentOptions: MergedParserOptions = defaultParserOptions
let currentRoot: RootNode | null = null
// parser state
let currentInput = ''
let currentOpenTag: ElementNode | null = null
let currentProp: AttributeNode | DirectiveNode | null = null
let currentAttrValue = ''
let currentAttrStartIndex = -1
let currentAttrEndIndex = -1
let inPre = 0
let inVPre = false
let currentVPreBoundary: ElementNode | null = null
const stack: ElementNode[] = []
const tokenizer = new Tokenizer(stack, {
onerr: emitError,
ontext(start, end) {
onText(getSlice(start, end), start, end)
},
ontextentity(char, start, end) {
onText(char, start, end)
},
oninterpolation(start, end) {
if (inVPre) {
return onText(getSlice(start, end), start, end)
}
let innerStart = start + tokenizer.delimiterOpen.length
let innerEnd = end - tokenizer.delimiterClose.length
while (isWhitespace(currentInput.charCodeAt(innerStart))) {
innerStart++
}
while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) {
innerEnd--
}
let exp = getSlice(innerStart, innerEnd)
// decode entities for backwards compat
if (exp.includes('&')) {
if (__BROWSER__) {
exp = currentOptions.decodeEntities!(exp, false)
} else {
exp = decodeHTML(exp)
}
}
addNode({
type: NodeTypes.INTERPOLATION,
content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
loc: getLoc(start, end)
})
},
onopentagname(start, end) {
const name = getSlice(start, end)
currentOpenTag = {
type: NodeTypes.ELEMENT,
tag: name,
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [],
children: [],
loc: getLoc(start - 1, end),
codegenNode: undefined
}
if (tokenizer.inSFCRoot) {
// in SFC mode, generate locations for root-level tags' inner content.
currentOpenTag.innerLoc = getLoc(
end + fastForward(end, CharCodes.Gt) + 1,
end
)
}
},
onopentagend(end) {
endOpenTag(end)
},
onclosetag(start, end) {
const name = getSlice(start, end)
if (!currentOptions.isVoidTag(name)) {
let found = false
for (let i = 0; i < stack.length; i++) {
const e = stack[i]
if (e.tag.toLowerCase() === name.toLowerCase()) {
found = true
if (i > 0) {
emitError(ErrorCodes.X_MISSING_END_TAG, stack[0].loc.start.offset)
}
for (let j = 0; j <= i; j++) {
const el = stack.shift()!
onCloseTag(el, end, j < i)
}
break
}
}
if (!found) {
emitError(ErrorCodes.X_INVALID_END_TAG, backTrack(start, CharCodes.Lt))
}
}
},
onselfclosingtag(end) {
const name = currentOpenTag!.tag
currentOpenTag!.isSelfClosing = true
endOpenTag(end)
if (stack[0]?.tag === name) {
onCloseTag(stack.shift()!, end)
}
},
onattribname(start, end) {
// plain attribute
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start)
}
},
ondirname(start, end) {
const raw = getSlice(start, end)
const name =
raw === '.' || raw === ':'
? 'bind'
: raw === '@'
? 'on'
: raw === '#'
? 'slot'
: raw.slice(2)
if (!inVPre && name === '') {
emitError(ErrorCodes.X_MISSING_DIRECTIVE_NAME, start)
}
if (inVPre || name === '') {
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: raw,
nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start)
}
} else {
currentProp = {
type: NodeTypes.DIRECTIVE,
name,
rawName: raw,
exp: undefined,
arg: undefined,
modifiers: raw === '.' ? ['prop'] : [],
loc: getLoc(start)
}
if (name === 'pre') {
inVPre = true
currentVPreBoundary = currentOpenTag
// convert dirs before this one to attributes
const props = currentOpenTag!.props
for (let i = 0; i < props.length; i++) {
if (props[i].type === NodeTypes.DIRECTIVE) {
props[i] = dirToAttr(props[i] as DirectiveNode)
}
}
}
}
},
ondirarg(start, end) {
if (start === end) return
const arg = getSlice(start, end)
if (inVPre) {
;(currentProp as AttributeNode).name += arg
setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else {
const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = createSimpleExpression(
isStatic ? arg : arg.slice(1, -1),
isStatic,
getLoc(start, end),
isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
)
}
},
ondirmodifier(start, end) {
const mod = getSlice(start, end)
if (inVPre) {
;(currentProp as AttributeNode).name += '.' + mod
setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else if ((currentProp as DirectiveNode).name === 'slot') {
// slot has no modifiers, special case for edge cases like
// https://github.com/vuejs/language-tools/issues/2710
const arg = (currentProp as DirectiveNode).arg
if (arg) {
;(arg as SimpleExpressionNode).content += '.' + mod
setLocEnd(arg.loc, end)
}
} else {
;(currentProp as DirectiveNode).modifiers.push(mod)
}
},
onattribdata(start, end) {
currentAttrValue += getSlice(start, end)
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
onattribentity(char, start, end) {
currentAttrValue += char
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
onattribnameend(end) {
const start = currentProp!.loc.start.offset
const name = getSlice(start, end)
if (currentProp!.type === NodeTypes.DIRECTIVE) {
currentProp!.rawName = name
}
// check duplicate attrs
if (
currentOpenTag!.props.some(
p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name
)
) {
emitError(ErrorCodes.DUPLICATE_ATTRIBUTE, start)
}
},
onattribend(quote, end) {
if (currentOpenTag && currentProp) {
// finalize end pos
setLocEnd(currentProp.loc, end)
if (quote !== QuoteType.NoValue) {
if (__BROWSER__ && currentAttrValue.includes('&')) {
currentAttrValue = currentOptions.decodeEntities!(
currentAttrValue,
true
)
}
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// assign value
// condense whitespaces in class
if (currentProp!.name === 'class') {
currentAttrValue = condense(currentAttrValue).trim()
}
if (quote === QuoteType.Unquoted && !currentAttrValue) {
emitError(ErrorCodes.MISSING_ATTRIBUTE_VALUE, end)
}
currentProp!.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc:
quote === QuoteType.Unquoted
? getLoc(currentAttrStartIndex, currentAttrEndIndex)
: getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
}
if (
tokenizer.inSFCRoot &&
currentOpenTag.tag === 'template' &&
currentProp.name === 'lang' &&
currentAttrValue &&
currentAttrValue !== 'html'
) {
// SFC root template with preprocessor lang, force tokenizer to
// RCDATA mode
tokenizer.enterRCDATA(toCharCodes(`</template`), 0)
}
} else {
// directive
currentProp.exp = createSimpleExpression(
currentAttrValue,
false,
getLoc(currentAttrStartIndex, currentAttrEndIndex)
)
if (currentProp.name === 'for') {
currentProp.forParseResult = parseForExpression(currentProp.exp)
}
// 2.x compat v-bind:foo.sync -> v-model:foo
let syncIndex = -1
if (
__COMPAT__ &&
currentProp.name === 'bind' &&
(syncIndex = currentProp.modifiers.indexOf('sync')) > -1 &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
currentOptions,
currentProp.loc,
currentProp.rawName
)
) {
currentProp.name = 'model'
currentProp.modifiers.splice(syncIndex, 1)
}
}
}
if (
currentProp.type !== NodeTypes.DIRECTIVE ||
currentProp.name !== 'pre'
) {
currentOpenTag.props.push(currentProp)
}
}
currentAttrValue = ''
currentAttrStartIndex = currentAttrEndIndex = -1
},
oncomment(start, end) {
if (currentOptions.comments) {
addNode({
type: NodeTypes.COMMENT,
content: getSlice(start, end),
loc: getLoc(start - 4, end + 3)
})
}
},
onend() {
const end = currentInput.length
// EOF ERRORS
if ((__DEV__ || !__BROWSER__) && tokenizer.state !== State.Text) {
switch (tokenizer.state) {
case State.BeforeTagName:
case State.BeforeClosingTagName:
emitError(ErrorCodes.EOF_BEFORE_TAG_NAME, end)
break
case State.Interpolation:
case State.InterpolationClose:
emitError(
ErrorCodes.X_MISSING_INTERPOLATION_END,
tokenizer.sectionStart
)
break
case State.InCommentLike:
if (tokenizer.currentSequence === Sequences.CdataEnd) {
emitError(ErrorCodes.EOF_IN_CDATA, end)
} else {
emitError(ErrorCodes.EOF_IN_COMMENT, end)
}
break
case State.InTagName:
case State.InSelfClosingTag:
case State.InClosingTagName:
case State.BeforeAttrName:
case State.InAttrName:
case State.InDirName:
case State.InDirArg:
case State.InDirDynamicArg:
case State.InDirModifier:
case State.AfterAttrName:
case State.BeforeAttrValue:
case State.InAttrValueDq: // "
case State.InAttrValueSq: // '
case State.InAttrValueNq:
emitError(ErrorCodes.EOF_IN_TAG, end)
break
default:
// console.log(tokenizer.state)
break
}
}
for (let index = 0; index < stack.length; index++) {
onCloseTag(stack[index], end - 1)
emitError(ErrorCodes.X_MISSING_END_TAG, stack[index].loc.start.offset)
}
},
oncdata(start, end) {
if (stack[0].ns !== Namespaces.HTML) {
onText(getSlice(start, end), start, end)
} else {
emitError(ErrorCodes.CDATA_IN_HTML_CONTENT, start - 9)
}
},
onprocessinginstruction(start) {
// ignore as we do not have runtime handling for this, only check error
if ((stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML) {
emitError(
ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
start - 1
)
}
}
})
// 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.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
function parseForExpression(
input: SimpleExpressionNode
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const createAliasExpression = (content: string, offset: number) => {
const start = loc.start.offset + offset
const end = start + content.length
return createSimpleExpression(content, false, getLoc(start, end))
}
const result: ForParseResult = {
source: createAliasExpression(RHS.trim(), exp.indexOf(RHS, LHS.length)),
value: undefined,
key: undefined,
index: undefined,
finalized: false
}
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
const trimmedOffset = LHS.indexOf(valueContent)
const iteratorMatch = valueContent.match(forIteratorRE)
if (iteratorMatch) {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(keyContent, keyOffset)
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
}
}
}
if (valueContent) {
result.value = createAliasExpression(valueContent, trimmedOffset)
}
return result
}
function getSlice(start: number, end: number) {
return currentInput.slice(start, end)
}
function endOpenTag(end: number) {
addNode(currentOpenTag!)
const { tag, ns } = currentOpenTag!
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre++
}
if (currentOptions.isVoidTag(tag)) {
onCloseTag(currentOpenTag!, end)
} else {
stack.unshift(currentOpenTag!)
if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
tokenizer.inXML = true
}
}
currentOpenTag = null
}
function onText(content: string, start: number, end: number) {
if (__BROWSER__) {
const tag = stack[0]?.tag
if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
content = currentOptions.decodeEntities!(content, false)
}
}
const parent = stack[0] || currentRoot
const lastNode = parent.children[parent.children.length - 1]
if (lastNode?.type === NodeTypes.TEXT) {
// merge
lastNode.content += content
setLocEnd(lastNode.loc, end)
} else {
parent.children.push({
type: NodeTypes.TEXT,
content,
loc: getLoc(start, end)
})
}
}
function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// attach end position
if (isImplied) {
// implied close, end should be backtracked to close
setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
} else {
setLocEnd(el.loc, end + fastForward(end, CharCodes.Gt) + 1)
}
if (tokenizer.inSFCRoot) {
// SFC root tag, resolve inner end
if (el.children.length) {
el.innerLoc!.end = extend({}, el.children[el.children.length - 1].loc.end)
} else {
el.innerLoc!.end = extend({}, el.innerLoc!.start)
}
el.innerLoc!.source = getSlice(
el.innerLoc!.start.offset,
el.innerLoc!.end.offset
)
}
// refine element type
const { tag, ns } = el
if (!inVPre) {
if (tag === 'slot') {
el.tagType = ElementTypes.SLOT
} else if (isFragmentTemplate(el)) {
el.tagType = ElementTypes.TEMPLATE
} else if (isComponent(el)) {
el.tagType = ElementTypes.COMPONENT
}
}
// whitespace management
if (!tokenizer.inRCDATA) {
el.children = condenseWhitespace(el.children, el.tag)
}
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre--
}
if (currentVPreBoundary === el) {
inVPre = false
currentVPreBoundary = null
}
if (
tokenizer.inXML &&
(stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML
) {
tokenizer.inXML = false
}
// 2.x compat / deprecation checks
if (__COMPAT__) {
const props = el.props
if (
__DEV__ &&
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
currentOptions
)
) {
let hasIf = false
let hasFor = false
for (let i = 0; i < props.length; i++) {
const p = props[i]
if (p.type === NodeTypes.DIRECTIVE) {
if (p.name === 'if') {
hasIf = true
} else if (p.name === 'for') {
hasFor = true
}
}
if (hasIf && hasFor) {
warnDeprecation(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
currentOptions,
el.loc
)
break
}
}
}
if (
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
currentOptions
) &&
el.tag === 'template' &&
!isFragmentTemplate(el)
) {
__DEV__ &&
warnDeprecation(
CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
currentOptions,
el.loc
)
// unwrap
const parent = stack[0] || currentRoot
const index = parent.children.indexOf(el)
parent.children.splice(index, 1, ...el.children)
}
const inlineTemplateProp = props.find(
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
) as AttributeNode
if (
inlineTemplateProp &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
currentOptions,
inlineTemplateProp.loc
) &&
el.children.length
) {
inlineTemplateProp.value = {
type: NodeTypes.TEXT,
content: getSlice(
el.children[0].loc.start.offset,
el.children[el.children.length - 1].loc.end.offset
),
loc: inlineTemplateProp.loc
}
}
}
}
function fastForward(start: number, c: number) {
let offset = 0
while (
currentInput.charCodeAt(start + offset) !== CharCodes.Gt &&
start + offset < currentInput.length
) {
offset++
}
return offset
}
function backTrack(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i >= 0) i--
return i
}
const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
function isFragmentTemplate({ tag, props }: ElementNode): boolean {
if (tag === 'template') {
for (let i = 0; i < props.length; i++) {
if (
props[i].type === NodeTypes.DIRECTIVE &&
specialTemplateDir.has((props[i] as DirectiveNode).name)
) {
return true
}
}
}
return false
}
function isComponent({ tag, props }: ElementNode): boolean {
if (currentOptions.isCustomElement(tag)) {
return false
}
if (
tag === 'component' ||
isUpperCase(tag.charCodeAt(0)) ||
isCoreComponent(tag) ||
currentOptions.isBuiltInComponent?.(tag) ||
(currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
) {
return true
}
// at this point the tag should be a native tag, but check for potential "is"
// casting
for (let i = 0; i < props.length; i++) {
const p = props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (p.name === 'is' && p.value) {
if (p.value.content.startsWith('vue:')) {
return true
} else if (
__COMPAT__ &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
currentOptions,
p.loc
)
) {
return true
}
}
} else if (
__COMPAT__ &&
// :is on plain element - only treat as component in compat mode
p.name === 'bind' &&
isStaticArgOf(p.arg, 'is') &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
currentOptions,
p.loc
)
) {
return true
}
}
return false
}
function isUpperCase(c: number) {
return c > 64 && c < 91
}
const windowsNewlineRE = /\r\n/g
function condenseWhitespace(
nodes: TemplateChildNode[],
tag?: string
): TemplateChildNode[] {
const shouldCondense = currentOptions.whitespace !== 'preserve'
let removedWhitespace = false
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!inPre) {
if (isAllWhitespace(node.content)) {
const prev = nodes[i - 1]?.type
const next = nodes[i + 1]?.type
// Remove if:
// - the whitespace is the first or last node, or:
// - (condense mode) the whitespace is between two comments, or:
// - (condense mode) the whitespace is between comment and element, or:
// - (condense mode) the whitespace is between two elements AND contains newline
if (
!prev ||
!next ||
(shouldCondense &&
((prev === NodeTypes.COMMENT &&
(next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
(prev === NodeTypes.ELEMENT &&
(next === NodeTypes.COMMENT ||
(next === NodeTypes.ELEMENT &&
hasNewlineChar(node.content))))))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
// Otherwise, the whitespace is condensed into a single space
node.content = ' '
}
} else if (shouldCondense) {
// in condense mode, consecutive whitespaces in text are condensed
// down to a single space.
node.content = condense(node.content)
}
} else {
// #6410 normalize windows newlines in <pre>:
// in SSR, browsers normalize server-rendered \r\n into a single \n
// in the DOM
node.content = node.content.replace(windowsNewlineRE, '\n')
}
}
}
if (inPre && tag && currentOptions.isPreTag(tag)) {
// 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(Boolean) : nodes
}
function isAllWhitespace(str: string) {
for (let i = 0; i < str.length; i++) {
if (!isWhitespace(str.charCodeAt(i))) {
return false
}
}
return true
}
function hasNewlineChar(str: string) {
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i)
if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
return true
}
}
return false
}
function condense(str: string) {
let ret = ''
let prevCharIsWhitespace = false
for (let i = 0; i < str.length; i++) {
if (isWhitespace(str.charCodeAt(i))) {
if (!prevCharIsWhitespace) {
ret += ' '
prevCharIsWhitespace = true
}
} else {
ret += str[i]
prevCharIsWhitespace = false
}
}
return ret
}
function addNode(node: TemplateChildNode) {
;(stack[0] || currentRoot).children.push(node)
}
function getLoc(start: number, end?: number): SourceLocation {
return {
start: tokenizer.getPos(start),
// @ts-expect-error allow late attachment
end: end == null ? end : tokenizer.getPos(end),
// @ts-expect-error allow late attachment
source: end == null ? end : getSlice(start, end)
}
}
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)
}
function dirToAttr(dir: DirectiveNode): AttributeNode {
const attr: AttributeNode = {
type: NodeTypes.ATTRIBUTE,
name: dir.rawName!,
nameLoc: getLoc(
dir.loc.start.offset,
dir.loc.start.offset + dir.rawName!.length
),
value: undefined,
loc: dir.loc
}
if (dir.exp) {
// account for quotes
const loc = dir.exp.loc
if (loc.end.offset < dir.loc.end.offset) {
loc.start.offset--
loc.start.column--
loc.end.offset++
loc.end.column++
}
attr.value = {
type: NodeTypes.TEXT,
content: (dir.exp as SimpleExpressionNode).content,
loc
}
}
return attr
}
function emitError(code: ErrorCodes, index: number) {
currentOptions.onError(createCompilerError(code, getLoc(index, index)))
}
function reset() {
tokenizer.reset()
currentOpenTag = null
currentProp = null
currentAttrValue = ''
currentAttrStartIndex = -1
currentAttrEndIndex = -1
stack.length = 0
}
export function baseParse(input: string, options?: ParserOptions): RootNode {
reset()
currentInput = input
currentOptions = extend({}, defaultParserOptions)
if (options) {
let key: keyof ParserOptions
for (key in options) {
if (options[key] != null) {
// @ts-ignore
currentOptions[key] = options[key]
}
}
}
if (__DEV__) {
if (!__BROWSER__ && currentOptions.decodeEntities) {
console.warn(
`[@vue/compiler-core] decodeEntities option is passed but will be ` +
`ignored in non-browser builds.`
)
} else if (__BROWSER__ && !currentOptions.decodeEntities) {
throw new Error(
`[@vue/compiler-core] decodeEntities option is required in browser builds.`
)
}
}
tokenizer.mode =
currentOptions.parseMode === 'html'
? ParseMode.HTML
: currentOptions.parseMode === 'sfc'
? ParseMode.SFC
: ParseMode.BASE
tokenizer.inXML =
currentOptions.ns === Namespaces.SVG ||
currentOptions.ns === Namespaces.MATH_ML
const delimiters = options?.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
const root = (currentRoot = createRoot([], input))
tokenizer.parse(currentInput)
root.loc = getLoc(0, input.length)
root.children = condenseWhitespace(root.children)
currentRoot = null
return root
}

File diff suppressed because it is too large Load Diff

View File

@ -334,6 +334,7 @@ export function transform(root: RootNode, options: TransformOptions) {
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
root.transformed = true
if (__COMPAT__) {
root.filters = [...context.filters!]

View File

@ -49,12 +49,10 @@ import {
GUARD_REACTIVE_PROPS
} from '../runtimeHelpers'
import {
getInnerRange,
toValidAssetId,
findProp,
isCoreComponent,
isStaticArgOf,
findDir,
isStaticExp
} from '../utils'
import { buildSlots } from './vSlot'
@ -285,19 +283,6 @@ export function resolveComponentType(
}
}
// 1.5 v-is (TODO: remove in 3.4)
const isDir = !isExplicitDynamic && findDir(node, 'is')
if (isDir && isDir.exp) {
if (__DEV__) {
context.onWarn(
createCompilerError(ErrorCodes.DEPRECATION_V_IS, isDir.loc)
)
}
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
isDir.exp
])
}
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) {
@ -489,7 +474,7 @@ export function buildProps(
// static attribute
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop
const { loc, name, nameLoc, value } = prop
let isStatic = true
if (name === 'ref') {
hasRef = true
@ -536,11 +521,7 @@ export function buildProps(
}
properties.push(
createObjectProperty(
createSimpleExpression(
name,
true,
getInnerRange(loc, 0, name.length)
),
createSimpleExpression(name, true, nameLoc),
createSimpleExpression(
value ? value.content : '',
isStatic,

View File

@ -336,9 +336,9 @@ export function processExpression(
id.name,
false,
{
source,
start: advancePositionWithClone(node.loc.start, source, start),
end: advancePositionWithClone(node.loc.start, source, end)
end: advancePositionWithClone(node.loc.start, source, end),
source
},
id.isConstant ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
)

View File

@ -8,14 +8,25 @@ import {
import { createCompilerError, ErrorCodes } from '../errors'
import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-bind
// *with* args.
export const transformBind: DirectiveTransform = (dir, _node, context) => {
const { exp, modifiers, loc } = dir
const { modifiers, loc } = dir
const arg = dir.arg!
// :arg is replaced by :arg="arg"
let { exp } = dir
if (!exp && arg.type === NodeTypes.SIMPLE_EXPRESSION) {
const propName = camelize(arg.content)
exp = dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
exp = dir.exp = processExpression(exp, context)
}
}
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
arg.children.unshift(`(`)
arg.children.push(`) || ""`)

View File

@ -6,7 +6,6 @@ import {
NodeTypes,
ExpressionNode,
createSimpleExpression,
SourceLocation,
SimpleExpressionNode,
createCallExpression,
createFunctionExpression,
@ -28,17 +27,16 @@ import {
createBlockStatement,
createCompoundExpression,
getVNodeBlockHelper,
getVNodeHelper
getVNodeHelper,
ForParseResult
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
getInnerRange,
findProp,
isTemplateNode,
isSlotOutlet,
injectProp,
findDir,
forAliasRE
findDir
} from '../utils'
import {
RENDER_LIST,
@ -256,12 +254,7 @@ export function processFor(
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
const parseResult = dir.forParseResult
if (!parseResult) {
context.onError(
@ -270,6 +263,8 @@ export function processFor(
return
}
finalizeForParseResult(parseResult, context)
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
@ -309,107 +304,56 @@ export function processFor(
}
}
// 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.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
}
export function parseForExpression(
input: SimpleExpressionNode,
export function finalizeForParseResult(
result: ForParseResult,
context: TransformContext
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
) {
if (result.finalized) return
const [, LHS, RHS] = inMatch
const result: ForParseResult = {
source: createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
),
value: undefined,
key: undefined,
index: undefined
}
if (!__BROWSER__ && context.prefixIdentifiers) {
result.source = processExpression(
result.source as SimpleExpressionNode,
context
)
if (result.key) {
result.key = processExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
if (result.index) {
result.index = processExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
if (result.value) {
result.value = processExpression(
result.value as SimpleExpressionNode,
context,
true
)
}
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(result.source as SimpleExpressionNode, context)
}
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
const trimmedOffset = LHS.indexOf(valueContent)
const iteratorMatch = valueContent.match(forIteratorRE)
if (iteratorMatch) {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(loc, keyContent, keyOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.key = processExpression(result.key, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
if (result.key) {
validateBrowserExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
loc,
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.index = processExpression(result.index, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
if (result.index) {
validateBrowserExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
if (valueContent) {
result.value = createAliasExpression(loc, valueContent, trimmedOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.value = processExpression(result.value, context, true)
}
if (__DEV__ && __BROWSER__) {
if (result.value) {
validateBrowserExpression(
result.value as SimpleExpressionNode,
context,
@ -417,20 +361,7 @@ export function parseForExpression(
)
}
}
return result
}
function createAliasExpression(
range: SourceLocation,
content: string,
offset: number
): SimpleExpressionNode {
return createSimpleExpression(
content,
false,
getInnerRange(range, offset, content.length)
)
result.finalized = true
}
export function createForLoopParams(

View File

@ -30,13 +30,7 @@ import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { FRAGMENT, CREATE_COMMENT } from '../runtimeHelpers'
import {
injectProp,
findDir,
findProp,
isBuiltInType,
getMemoedVNodeCall
} from '../utils'
import { injectProp, findDir, findProp, getMemoedVNodeCall } from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformIf = createStructuralDirectiveTransform(
@ -165,7 +159,8 @@ export function processIf(
!(
context.parent &&
context.parent.type === NodeTypes.ELEMENT &&
isBuiltInType(context.parent.tag, 'transition')
(context.parent.tag === 'transition' ||
context.parent.tag === 'Transition')
)
) {
branch.children = [...comments, ...branch.children]

View File

@ -29,6 +29,8 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
return createTransformProps()
}
// we assume v-model directives are always parsed
// (not artificially created by a transform)
const rawExp = exp.loc.source
const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp

View File

@ -14,7 +14,6 @@ import {
SourceLocation,
createConditionalExpression,
ConditionalExpression,
SimpleExpressionNode,
FunctionExpression,
CallExpression,
createCallExpression,
@ -32,7 +31,7 @@ import {
isStaticExp
} from '../utils'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor'
import { createForLoopParams, finalizeForParseResult } from './vFor'
import { SlotFlags, slotFlagsText } from '@vue/shared'
const defaultFallback = createSimpleExpression(`undefined`, false)
@ -78,11 +77,9 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
node.props.some(isVSlot) &&
(vFor = findDir(node, 'for'))
) {
const result = (vFor.parseResult = parseForExpression(
vFor.exp as SimpleExpressionNode,
context
))
const result = vFor.forParseResult
if (result) {
finalizeForParseResult(result, context)
const { value, key, index } = result
const { addIdentifiers, removeIdentifiers } = context
value && addIdentifiers(value)
@ -100,7 +97,7 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
vForExp: ExpressionNode | undefined,
vFor: DirectiveNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
@ -203,12 +200,7 @@ export function buildSlots(
}
const vFor = findDir(slotElement, 'for')
const slotFunction = buildSlotFn(
slotProps,
vFor?.exp,
slotChildren,
slotLoc
)
const slotFunction = buildSlotFn(slotProps, vFor, slotChildren, slotLoc)
// check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined
@ -266,10 +258,9 @@ export function buildSlots(
}
} else if (vFor) {
hasDynamicSlots = true
const parseResult =
vFor.parseResult ||
parseForExpression(vFor.exp as SimpleExpressionNode, context)
const parseResult = vFor.forParseResult
if (parseResult) {
finalizeForParseResult(parseResult, context)
// Render the dynamic slots as an array and add it to the createSlot()
// args. The runtime knows how to handle it appropriately.
dynamicSlots.push(

View File

@ -1,5 +1,4 @@
import {
SourceLocation,
Position,
ElementNode,
NodeTypes,
@ -37,7 +36,7 @@ import {
GUARD_REACTIVE_PROPS,
WITH_MEMO
} from './runtimeHelpers'
import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
import { isString, isObject, NOOP } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
import { Expression } from '@babel/types'
@ -45,18 +44,20 @@ import { Expression } from '@babel/types'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
export const isBuiltInType = (tag: string, expected: string): boolean =>
tag === expected || tag === hyphenate(expected)
export function isCoreComponent(tag: string): symbol | void {
if (isBuiltInType(tag, 'Teleport')) {
return TELEPORT
} else if (isBuiltInType(tag, 'Suspense')) {
return SUSPENSE
} else if (isBuiltInType(tag, 'KeepAlive')) {
return KEEP_ALIVE
} else if (isBuiltInType(tag, 'BaseTransition')) {
return BASE_TRANSITION
switch (tag) {
case 'Teleport':
case 'teleport':
return TELEPORT
case 'Suspense':
case 'suspense':
return SUSPENSE
case 'KeepAlive':
case 'keep-alive':
return KEEP_ALIVE
case 'BaseTransition':
case 'base-transition':
return BASE_TRANSITION
}
}
@ -64,7 +65,7 @@ const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
const enum MemberExpLexState {
enum MemberExpLexState {
inMemberExp,
inBrackets,
inParens,
@ -174,38 +175,17 @@ export const isMemberExpression = __BROWSER__
? isMemberExpressionBrowser
: isMemberExpressionNode
export function getInnerRange(
loc: SourceLocation,
offset: number,
length: number
): SourceLocation {
__TEST__ && assert(offset <= loc.source.length)
const source = loc.source.slice(offset, offset + length)
const newLoc: SourceLocation = {
source,
start: advancePositionWithClone(loc.start, loc.source, offset),
end: loc.end
}
if (length != null) {
__TEST__ && assert(offset + length <= loc.source.length)
newLoc.end = advancePositionWithClone(
loc.start,
loc.source,
offset + length
)
}
return newLoc
}
export function advancePositionWithClone(
pos: Position,
source: string,
numberOfCharacters: number = source.length
): Position {
return advancePositionWithMutation(
extend({}, pos),
{
offset: pos.offset,
line: pos.line,
column: pos.column
},
source,
numberOfCharacters
)

View File

@ -3,13 +3,13 @@ import {
NodeTypes,
ElementNode,
TextNode,
ErrorCodes,
ElementTypes,
InterpolationNode,
AttributeNode,
ConstantTypes
ConstantTypes,
Namespaces
} from '@vue/compiler-core'
import { parserOptions, DOMNamespaces } from '../src/parserOptions'
import { parserOptions } from '../src/parserOptions'
describe('DOM parser', () => {
describe('Text', () => {
@ -32,7 +32,7 @@ describe('DOM parser', () => {
})
})
test('textarea handles character references', () => {
test('textarea handles entities', () => {
const ast = parse('<textarea>&amp;</textarea>', parserOptions)
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
@ -277,11 +277,10 @@ describe('DOM parser', () => {
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: DOMNamespaces.HTML,
ns: Namespaces.HTML,
tag: 'img',
tagType: ElementTypes.ELEMENT,
props: [],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
@ -316,15 +315,8 @@ describe('DOM parser', () => {
test('Strict end tag detection for textarea.', () => {
const ast = parse(
'<textarea>hello</textarea</textarea0></texTArea a="<>">',
{
...parserOptions,
onError: err => {
if (err.code !== ErrorCodes.END_TAG_WITH_ATTRIBUTES) {
throw err
}
}
}
'<textarea>hello</textarea</textarea0></texTArea>',
parserOptions
)
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
@ -347,21 +339,21 @@ describe('DOM parser', () => {
const ast = parse('<html>test</html>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('SVG namespace', () => {
const ast = parse('<svg>test</svg>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(Namespaces.SVG)
})
test('MATH_ML namespace', () => {
const ast = parse('<math>test</math>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('SVG in MATH_ML namespace', () => {
@ -373,8 +365,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const elementSvg = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
})
test('html text/html in MATH_ML namespace', () => {
@ -387,8 +379,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('html application/xhtml+xml in MATH_ML namespace', () => {
@ -400,8 +392,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('mtext malignmark in MATH_ML namespace', () => {
@ -413,8 +405,8 @@ describe('DOM parser', () => {
const elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('mtext and not malignmark tag in MATH_ML namespace', () => {
@ -423,8 +415,8 @@ describe('DOM parser', () => {
const elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('foreignObject tag in SVG namespace', () => {
@ -436,8 +428,8 @@ describe('DOM parser', () => {
const elementForeignObject = elementSvg.children[0] as ElementNode
const element = elementForeignObject.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('desc tag in SVG namespace', () => {
@ -446,8 +438,8 @@ describe('DOM parser', () => {
const elementDesc = elementSvg.children[0] as ElementNode
const element = elementDesc.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('title tag in SVG namespace', () => {
@ -456,8 +448,8 @@ describe('DOM parser', () => {
const elementTitle = elementSvg.children[0] as ElementNode
const element = elementTitle.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('SVG in HTML namespace', () => {
@ -465,8 +457,8 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.SVG)
expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(Namespaces.SVG)
})
test('MATH in HTML namespace', () => {
@ -474,8 +466,35 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('root ns', () => {
const ast = parse('<foreignObject><test/></foreignObject>', {
...parserOptions,
ns: Namespaces.SVG
})
const elementForieng = ast.children[0] as ElementNode
const element = elementForieng.children[0] as ElementNode
expect(elementForieng.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('correct XML handling with root ns', () => {
// when root ns is an XML namespace, there should be no special content
// treatment for <script>, <style>, <textarea> etc.
const ast = parse('<script><g/><g/></script>', {
...parserOptions,
ns: Namespaces.SVG
})
const elementSvg = ast.children[0] as ElementNode
// should parse as nodes instead of text
expect(elementSvg.children).toMatchObject([
{ type: NodeTypes.ELEMENT, tag: 'g' },
{ type: NodeTypes.ELEMENT, tag: 'g' }
])
})
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.3.9",
"version": "3.4.0-alpha.3",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -1,133 +0,0 @@
import { ParserOptions } from '@vue/compiler-core'
import namedCharacterReferences from './namedChars.json'
// lazy compute this to make this file tree-shakable for browser
let maxCRNameLength: number
export const decodeHtml: ParserOptions['decodeEntities'] = (
rawText,
asAttr
) => {
let offset = 0
const end = rawText.length
let decodedText = ''
function advance(length: number) {
offset += length
rawText = rawText.slice(length)
}
while (offset < end) {
const head = /&(?:#x?)?/i.exec(rawText)
if (!head || offset + head.index >= end) {
const remaining = end - offset
decodedText += rawText.slice(0, remaining)
advance(remaining)
break
}
// Advance to the "&".
decodedText += rawText.slice(0, head.index)
advance(head.index)
if (head[0] === '&') {
// Named character reference.
let name = ''
let value: string | undefined = undefined
if (/[0-9a-z]/i.test(rawText[1])) {
if (!maxCRNameLength) {
maxCRNameLength = Object.keys(namedCharacterReferences).reduce(
(max, name) => Math.max(max, name.length),
0
)
}
for (let length = maxCRNameLength; !value && length > 0; --length) {
name = rawText.slice(1, 1 + length)
value = (namedCharacterReferences as Record<string, string>)[name]
}
if (value) {
const semi = name.endsWith(';')
if (
asAttr &&
!semi &&
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) {
decodedText += '&' + name
advance(1 + name.length)
} else {
decodedText += value
advance(1 + name.length)
}
} else {
decodedText += '&' + name
advance(1 + name.length)
}
} else {
decodedText += '&'
advance(1)
}
} else {
// Numeric character reference.
const hex = head[0] === '&#x'
const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
const body = pattern.exec(rawText)
if (!body) {
decodedText += head[0]
advance(head[0].length)
} else {
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
let cp = Number.parseInt(body[1], hex ? 16 : 10)
if (cp === 0) {
cp = 0xfffd
} else if (cp > 0x10ffff) {
cp = 0xfffd
} else if (cp >= 0xd800 && cp <= 0xdfff) {
cp = 0xfffd
} else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
// noop
} else if (
(cp >= 0x01 && cp <= 0x08) ||
cp === 0x0b ||
(cp >= 0x0d && cp <= 0x1f) ||
(cp >= 0x7f && cp <= 0x9f)
) {
cp = CCR_REPLACEMENTS[cp] || cp
}
decodedText += String.fromCodePoint(cp)
advance(body[0].length)
}
}
}
return decodedText
}
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
const CCR_REPLACEMENTS: Record<number, number | undefined> = {
0x80: 0x20ac,
0x82: 0x201a,
0x83: 0x0192,
0x84: 0x201e,
0x85: 0x2026,
0x86: 0x2020,
0x87: 0x2021,
0x88: 0x02c6,
0x89: 0x2030,
0x8a: 0x0160,
0x8b: 0x2039,
0x8c: 0x0152,
0x8e: 0x017d,
0x91: 0x2018,
0x92: 0x2019,
0x93: 0x201c,
0x94: 0x201d,
0x95: 0x2022,
0x96: 0x2013,
0x97: 0x2014,
0x98: 0x02dc,
0x99: 0x2122,
0x9a: 0x0161,
0x9b: 0x203a,
0x9c: 0x0153,
0x9e: 0x017e,
0x9f: 0x0178
}

View File

@ -20,7 +20,7 @@ export function createDOMCompilerError(
) as DOMCompilerError
}
export const enum DOMErrorCodes {
export enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION,
@ -36,7 +36,7 @@ export const enum DOMErrorCodes {
}
if (__TEST__) {
// esbuild cannot infer const enum increments if first value is from another
// esbuild cannot infer enum increments if first value is from another
// file, so we have to manually keep them in sync. this check ensures it
// errors out if there are collisions.
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {

View File

@ -38,11 +38,11 @@ export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
}
export function compile(
template: string,
src: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
src,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle'
export { createDOMCompilerError, DOMErrorCodes } from './errors'
export {
createDOMCompilerError,
DOMErrorCodes,
DOMErrorMessages
} from './errors'
export * from '@vue/compiler-core'

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,30 @@
import {
TextModes,
ParserOptions,
ElementNode,
NodeTypes,
isBuiltInType
} from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtml } from './decodeHtml'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript',
true
)
export const enum DOMNamespaces {
HTML = 0 /* Namespaces.HTML */,
SVG,
MATH_ML
}
export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : decodeHtml,
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
isBuiltInComponent: (tag: string): symbol | undefined => {
if (isBuiltInType(tag, `Transition`)) {
isBuiltInComponent: tag => {
if (tag === 'Transition' || tag === 'transition') {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
} else if (tag === 'TransitionGroup' || tag === 'transition-group') {
return TRANSITION_GROUP
}
},
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML
if (parent && ns === DOMNamespaces.MATH_ML) {
getNamespace(tag, parent, rootNamespace) {
let ns = parent ? parent.ns : rootNamespace
if (parent && ns === Namespaces.MATH_ML) {
if (parent.tag === 'annotation-xml') {
if (tag === 'svg') {
return DOMNamespaces.SVG
return Namespaces.SVG
}
if (
parent.props.some(
@ -54,46 +36,33 @@ export const parserOptions: ParserOptions = {
a.value.content === 'application/xhtml+xml')
)
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
} else if (
/^m(?:[ions]|text)$/.test(parent.tag) &&
tag !== 'mglyph' &&
tag !== 'malignmark'
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
} else if (parent && ns === DOMNamespaces.SVG) {
} else if (parent && ns === Namespaces.SVG) {
if (
parent.tag === 'foreignObject' ||
parent.tag === 'desc' ||
parent.tag === 'title'
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
}
if (ns === DOMNamespaces.HTML) {
if (ns === Namespaces.HTML) {
if (tag === 'svg') {
return DOMNamespaces.SVG
return Namespaces.SVG
}
if (tag === 'math') {
return DOMNamespaces.MATH_ML
return Namespaces.MATH_ML
}
}
return ns
},
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
getTextMode({ tag, ns }: ElementNode): TextModes {
if (ns === DOMNamespaces.HTML) {
if (tag === 'textarea' || tag === 'title') {
return TextModes.RCDATA
}
if (isRawTextContainer(tag)) {
return TextModes.RAWTEXT
}
}
return TextModes.DATA
}
}

View File

@ -43,6 +43,7 @@ export const transformTransition: NodeTransform = (node, context) => {
node.props.push({
type: NodeTypes.ATTRIBUTE,
name: 'persisted',
nameLoc: node.loc,
value: undefined,
loc: node.loc
})

View File

@ -15,7 +15,8 @@ import {
PlainElementNode,
JSChildNode,
TextCallNode,
ConstantTypes
ConstantTypes,
Namespaces
} from '@vue/compiler-core'
import {
isVoidTag,
@ -31,9 +32,8 @@ import {
isKnownSvgAttr,
isBooleanAttr
} from '@vue/shared'
import { DOMNamespaces } from '../parserOptions'
export const enum StringifyThresholds {
export enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20
}
@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
node.codegenNode.hoisted
const dataAriaRE = /^(data|aria)-/
const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
const isStringifiableAttr = (name: string, ns: Namespaces) => {
return (
(ns === DOMNamespaces.HTML
(ns === Namespaces.HTML
? isKnownHtmlAttr(name)
: ns === DOMNamespaces.SVG
: ns === Namespaces.SVG
? isKnownSvgAttr(name)
: false) || dataAriaRE.test(name)
)

View File

@ -14,7 +14,8 @@ return { a }
`;
exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (call) 1`] = `
"import { defineComponent } from 'vue'
"
import { defineComponent } from 'vue'
const __default__ = defineComponent({
name: 'Baz'
})
@ -30,7 +31,8 @@ return { a, defineComponent }
`;
exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (object) 1`] = `
"const __default__ = {
"
const __default__ = {
name: 'Baz'
}
@ -45,7 +47,8 @@ return { a }
`;
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > export call expression as default 1`] = `
"function fn() {
"
function fn() {
return \\"hello, world\\";
}
const __default__ = fn();
@ -63,7 +66,8 @@ return { fn }
`;
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > keep original semi style 1`] = `
"export default {
"
export default {
props: ['item'],
emits: ['change'],
setup(__props, { expose: __expose, emit: __emit }) {
@ -515,27 +519,28 @@ return { }
`;
exports[`SFC compile <script setup> > async/await detection > ref 1`] = `
"import { withAsyncContext as _withAsyncContext, ref as _ref } from 'vue'
"import { withAsyncContext as _withAsyncContext } from 'vue'
export default {
async setup(__props, { expose: __expose }) {
__expose();
let __temp, __restore
let a = _ref(1 + ((
let a = ref(1 + ((
([__temp,__restore] = _withAsyncContext(() => foo)),
__temp = await __temp,
__restore(),
__temp
)))
return { a }
return { get a() { return a }, set a(v) { a = v } }
}
}"
`;
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 1`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();
async function foo() { await bar }
@ -546,7 +551,8 @@ return { foo }
`;
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 2`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();
const foo = async () => { await bar }
@ -557,7 +563,8 @@ return { foo }
`;
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 3`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();
const obj = { async method() { await bar }}
@ -568,7 +575,8 @@ return { obj }
`;
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 4`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();
const cls = class Foo { async method() { await bar }}
@ -618,7 +626,8 @@ return { a }
`;
exports[`SFC compile <script setup> > binding analysis for destructure 1`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();
@ -818,23 +827,29 @@ return { bar }
`;
exports[`SFC compile <script setup> > imports > dedupe between user & helper 1`] = `
"import { ref as _ref } from 'vue'
import { ref } from 'vue'
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { useCssVars, ref } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = _ref(1)
_useCssVars(_ctx => ({
\\"xxxxxxxx-msg\\": (msg.value)
}))
const msg = ref()
return { foo, ref }
return { msg, useCssVars, ref }
}
}"
`;
exports[`SFC compile <script setup> > imports > import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
"
import { x } from './x'
export default {
setup(__props, { expose: __expose }) {
@ -898,7 +913,9 @@ return { ref }
`;
exports[`SFC compile <script setup> > imports > should support module string names syntax 1`] = `
"import { \\"😏\\" as foo } from './foo'
"
import { \\"😏\\" as foo } from './foo'
export default {
setup(__props, { expose: __expose }) {
@ -1185,7 +1202,8 @@ return (_ctx, _cache) => {
`;
exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
const count = ref(0)
@ -1348,7 +1366,8 @@ return { a }
`;
exports[`SFC genDefaultAs > <script> + <script setup> 1`] = `
"const __default__ = {}
"
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {
@ -1363,7 +1382,8 @@ return { a }
`;
exports[`SFC genDefaultAs > <script> + <script setup> 2`] = `
"const __default__ = {}
"
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) {

View File

@ -58,28 +58,9 @@ export function ssrRender(_ctx, _push, _parent, _attrs) {
}"
`;
exports[`source map 1`] = `
{
"mappings": ";;;wBACE,oBAA8B;IAAzB,oBAAmB,4BAAbA,WAAM",
"names": [
"render",
],
"sources": [
"example.vue",
],
"sourcesContent": [
"
<div><p>{{ render }}</p></div>
",
],
"version": 3,
}
`;
exports[`template errors 1`] = `
[
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],
[SyntaxError: v-bind is missing expression.],
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.],
]
`;

View File

@ -240,14 +240,22 @@ describe('SFC compile <script setup>', () => {
const { content } = compile(
`
<script setup>
import { ref } from 'vue'
let foo = $ref(1)
import { useCssVars, ref } from 'vue'
const msg = ref()
</script>
`,
{ reactivityTransform: true }
<style>
.foo {
color: v-bind(msg)
}
</style>
`
)
assertCode(content)
expect(content).toMatch(`import { ref } from 'vue'`)
expect(content).toMatch(
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`
)
expect(content).toMatch(`import { useCssVars, ref } from 'vue'`)
})
test('import dedupe between <script> and <script setup>', () => {
@ -891,9 +899,7 @@ describe('SFC compile <script setup>', () => {
describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`, {
reactivityTransform: true
})
const { content } = compile(`<script setup>${code}</script>`)
if (shouldAsync) {
expect(content).toMatch(`let __temp, __restore`)
}
@ -911,7 +917,7 @@ describe('SFC compile <script setup>', () => {
})
test('ref', () => {
assertAwaitDetection(`let a = $ref(1 + (await foo))`)
assertAwaitDetection(`let a = ref(1 + (await foo))`)
})
// #4448

View File

@ -1,7 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`defineEmits > basic usage 1`] = `
"export default {
"
export default {
emits: ['foo', 'bar'],
setup(__props, { expose: __expose, emit: __emit }) {
__expose();

View File

@ -16,7 +16,8 @@ return { n, get x() { return x } }
`;
exports[`defineExpose() 1`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose({ foo: 123 })

View File

@ -1,7 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`defineOptions() > basic usage 1`] = `
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
"
export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
setup(__props, { expose: __expose }) {
__expose();
@ -14,7 +15,8 @@ return { }
`;
exports[`defineOptions() > empty argument 1`] = `
"export default {
"
export default {
setup(__props, { expose: __expose }) {
__expose();

View File

@ -177,7 +177,8 @@ return () => {}
`;
exports[`sfc reactive props destructure > defineProps/defineEmits in multi-variable declaration (full removal) 1`] = `
"export default {
"
export default {
props: ['item'],
emits: ['a'],
setup(__props, { emit: __emit }) {
@ -192,7 +193,8 @@ return () => {}
`;
exports[`sfc reactive props destructure > multi-variable declaration 1`] = `
"export default {
"
export default {
props: ['item'],
setup(__props) {
@ -205,7 +207,8 @@ return () => {}
`;
exports[`sfc reactive props destructure > multi-variable declaration fix #6757 1`] = `
"export default {
"
export default {
props: ['item'],
setup(__props) {
@ -218,7 +221,8 @@ return () => {}
`;
exports[`sfc reactive props destructure > multi-variable declaration fix #7422 1`] = `
"export default {
"
export default {
props: ['item'],
setup(__props) {
@ -250,7 +254,8 @@ return (_ctx, _cache) => {
`;
exports[`sfc reactive props destructure > nested scope 1`] = `
"export default {
"
export default {
props: ['foo', 'bar'],
setup(__props) {

View File

@ -1,7 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sfc hoist static > should enable when only script setup 1`] = `
"const foo = 'bar'
"
const foo = 'bar'
export default {
setup(__props) {
@ -91,7 +92,8 @@ return () => {}
`;
exports[`sfc hoist static > should not hoist a function or class 1`] = `
"export default {
"
export default {
setup(__props) {
const fn = () => {}
@ -105,7 +107,8 @@ return () => {}
`;
exports[`sfc hoist static > should not hoist a object or array 1`] = `
"export default {
"
export default {
setup(__props) {
const obj = { foo: 'bar' }
@ -118,7 +121,8 @@ return () => {}
`;
exports[`sfc hoist static > should not hoist a variable 1`] = `
"export default {
"
export default {
setup(__props) {
let KEY1 = 'default value'
@ -133,7 +137,8 @@ return () => {}
`;
exports[`sfc hoist static > should not hoist when disabled 1`] = `
"export default {
"
export default {
setup(__props) {
const foo = 'bar'

View File

@ -1,113 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sfc ref transform > $ unwrapping 1`] = `
"import { ref, shallowRef } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = (ref())
let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
return { foo, a, b, get c() { return c }, set c(v) { c = v }, get d() { return d }, set d(v) { d = v }, ref, shallowRef }
}
}"
`;
exports[`sfc ref transform > $ref & $shallowRef declarations 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = _ref()
let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
return { foo, a, b, get c() { return c }, set c(v) { c = v }, get d() { return d }, set d(v) { d = v } }
}
}"
`;
exports[`sfc ref transform > usage /w typescript 1`] = `
"import { ref as _ref, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
let msg = _ref<string | number>('foo');
let bar = _ref <string | number>('bar');
return { msg, bar }
}
})"
`;
exports[`sfc ref transform > usage in normal <script> 1`] = `
"import { ref as _ref } from 'vue'
export default {
setup() {
let count = _ref(0)
const inc = () => count.value++
return ({ count })
}
}
"
`;
exports[`sfc ref transform > usage with normal <script> (has macro usage) + <script setup> (no macro usage) 1`] = `
"import { ref as _ref } from 'vue'
let data = _ref()
export default {
setup(__props, { expose: __expose }) {
__expose();
console.log(data.value)
return { data }
}
}"
`;
exports[`sfc ref transform > usage with normal <script> + <script setup> 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(0)
let c = _ref(0)
export default {
setup(__props, { expose: __expose }) {
__expose();
let b = _ref(0)
let c = 0
function change() {
a.value++
b.value++
c++
}
return { a, c, b, change }
}
}"
`;

View File

@ -1,193 +0,0 @@
// TODO remove in 3.4
import { BindingTypes } from '@vue/compiler-core'
import { compileSFCScript as compile, assertCode } from '../utils'
// this file only tests integration with SFC - main test case for the ref
// transform can be found in <root>/packages/reactivity-transform/__tests__
describe('sfc ref transform', () => {
function compileWithReactivityTransform(src: string) {
return compile(src, { reactivityTransform: true })
}
test('$ unwrapping', () => {
const { content, bindings } = compileWithReactivityTransform(`<script setup>
import { ref, shallowRef } from 'vue'
let foo = $(ref())
let a = $(ref(1))
let b = $(shallowRef({
count: 0
}))
let c = () => {}
let d
</script>`)
expect(content).not.toMatch(`$(ref())`)
expect(content).not.toMatch(`$(ref(1))`)
expect(content).not.toMatch(`$(shallowRef({`)
expect(content).toMatch(`let foo = (ref())`)
expect(content).toMatch(`let a = (ref(1))`)
expect(content).toMatch(`
let b = (shallowRef({
count: 0
}))
`)
// normal declarations left untouched
expect(content).toMatch(`let c = () => {}`)
expect(content).toMatch(`let d`)
expect(content).toMatch(
`return { foo, a, b, get c() { return c }, set c(v) { c = v }, ` +
`get d() { return d }, set d(v) { d = v }, ref, shallowRef }`
)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET,
ref: BindingTypes.SETUP_CONST,
shallowRef: BindingTypes.SETUP_CONST
})
})
test('$ref & $shallowRef declarations', () => {
const { content, bindings } = compileWithReactivityTransform(`<script setup>
let foo = $ref()
let a = $ref(1)
let b = $shallowRef({
count: 0
})
let c = () => {}
let d
</script>`)
expect(content).toMatch(
`import { ref as _ref, shallowRef as _shallowRef } from 'vue'`
)
expect(content).not.toMatch(`$ref()`)
expect(content).not.toMatch(`$ref(1)`)
expect(content).not.toMatch(`$shallowRef({`)
expect(content).toMatch(`let foo = _ref()`)
expect(content).toMatch(`let a = _ref(1)`)
expect(content).toMatch(`
let b = _shallowRef({
count: 0
})
`)
// normal declarations left untouched
expect(content).toMatch(`let c = () => {}`)
expect(content).toMatch(`let d`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET
})
})
test('usage in normal <script>', () => {
const { content } = compileWithReactivityTransform(`<script>
export default {
setup() {
let count = $ref(0)
const inc = () => count++
return $$({ count })
}
}
</script>`)
expect(content).not.toMatch(`$ref(0)`)
expect(content).toMatch(`import { ref as _ref } from 'vue'`)
expect(content).toMatch(`let count = _ref(0)`)
expect(content).toMatch(`count.value++`)
expect(content).toMatch(`return ({ count })`)
assertCode(content)
})
test('usage /w typescript', () => {
const { content } = compileWithReactivityTransform(`
<script setup lang="ts">
let msg = $ref<string | number>('foo');
let bar = $ref <string | number>('bar');
</script>
`)
expect(content).toMatch(`import { ref as _ref`)
expect(content).toMatch(`let msg = _ref<string | number>('foo')`)
expect(content).toMatch(`let bar = _ref <string | number>('bar')`)
assertCode(content)
})
test('usage with normal <script> + <script setup>', () => {
const { content, bindings } = compileWithReactivityTransform(`<script>
let a = $ref(0)
let c = $ref(0)
</script>
<script setup>
let b = $ref(0)
let c = 0
function change() {
a++
b++
c++
}
</script>`)
// should dedupe helper imports
expect(content).toMatch(`import { ref as _ref } from 'vue'`)
expect(content).toMatch(`let a = _ref(0)`)
expect(content).toMatch(`let b = _ref(0)`)
// root level ref binding declared in <script> should be inherited in <script setup>
expect(content).toMatch(`a.value++`)
expect(content).toMatch(`b.value++`)
// c shadowed
expect(content).toMatch(`c++`)
assertCode(content)
expect(bindings).toStrictEqual({
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_REF,
change: BindingTypes.SETUP_CONST
})
})
test('usage with normal <script> (has macro usage) + <script setup> (no macro usage)', () => {
const { content } = compileWithReactivityTransform(`
<script>
let data = $ref()
</script>
<script setup>
console.log(data)
</script>
`)
expect(content).toMatch(`console.log(data.value)`)
assertCode(content)
})
describe('errors', () => {
test('defineProps/Emit() referencing ref declarations', () => {
expect(() =>
compile(
`<script setup>
let bar = $ref(1)
defineProps({
bar
})
</script>`,
{ reactivityTransform: true }
)
).toThrow(`cannot reference locally declared variables`)
expect(() =>
compile(
`<script setup>
let bar = $ref(1)
defineEmits({
bar
})
</script>`,
{ reactivityTransform: true }
)
).toThrow(`cannot reference locally declared variables`)
})
})
})

View File

@ -1,3 +1,4 @@
import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
import {
compileTemplate,
SFCTemplateCompileOptions
@ -107,24 +108,193 @@ test('source map', () => {
const template = parse(
`
<template>
<div><p>{{ render }}</p></div>
<div><p>{{ foobar }}</p></div>
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock
).descriptor.template!
const result = compile({
const { code, map } = compile({
filename: 'example.vue',
source: template.content
})
expect(result.map).toMatchSnapshot()
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
).toMatchObject(getPositionInCode(template.content, `foobar`))
})
test('should work w/ AST from descriptor', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content
}).code
)
})
test('should work w/ AST from descriptor in SSR mode', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: '', // make sure it's actually using the AST instead of source
ast: template.ast,
ssr: true
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true
}).code
)
})
test('should not reuse AST if using custom compiler', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
const { code } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast,
compiler: {
parse: () => null as any,
// @ts-ignore
compile: input => ({ code: input })
}
})
// what we really want to assert is that the `input` received by the custom
// compiler is the source string, not the AST.
expect(code).toBe(template.content)
})
test('should force re-parse on already transformed AST', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content
}).code
)
})
test('should force re-parse with correct compiler in SSR mode', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast,
ssr: true
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true
}).code
)
})
test('template errors', () => {
const result = compile({
filename: 'example.vue',
source: `<div :foo
source: `<div
:bar="a[" v-model="baz"/>`
})
expect(result.errors).toMatchSnapshot()
@ -199,3 +369,36 @@ test('dynamic v-on + static v-on should merged', () => {
expect(result.code).toMatchSnapshot()
})
interface Pos {
line: number
column: number
name?: string
}
function getPositionInCode(
code: string,
token: string,
expectName: string | boolean = false
): Pos {
const generatedOffset = code.indexOf(token)
let line = 1
let lastNewLinePos = -1
for (let i = 0; i < generatedOffset; i++) {
if (code.charCodeAt(i) === 10 /* newline char code */) {
line++
lastNewLinePos = i
}
}
const res: Pos = {
line,
column:
lastNewLinePos === -1
? generatedOffset
: generatedOffset - lastNewLinePos - 1
}
if (expectName) {
res.name = typeof expectName === 'string' ? expectName : token
}
return res
}

View File

@ -1,5 +1,5 @@
import { parse } from '../src'
import { baseParse, baseCompile } from '@vue/compiler-core'
import { baseCompile, createRoot } from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map-js'
describe('compiler:sfc', () => {
@ -7,15 +7,61 @@ describe('compiler:sfc', () => {
test('style block', () => {
// Padding determines how many blank lines will there be before the style block
const padding = Math.round(Math.random() * 10)
const style = parse(
`${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n`
).descriptor.styles[0]
const src =
`${'\n'.repeat(padding)}` +
`<style>
.css {
color: red;
}
</style>
expect(style.map).not.toBeUndefined()
<style module>
.css-module {
color: red;
}
</style>
const consumer = new SourceMapConsumer(style.map!)
<style scoped>
.css-scoped {
color: red;
}
</style>
<style scoped>
.css-scoped-nested {
color: red;
.dummy {
color: green;
}
font-weight: bold;
}
</style>`
const {
descriptor: { styles }
} = parse(src)
expect(styles[0].map).not.toBeUndefined()
const consumer = new SourceMapConsumer(styles[0].map!)
const lineOffset =
src.slice(0, src.indexOf(`<style>`)).split('\n').length - 1
consumer.eachMapping(mapping => {
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
expect(mapping.generatedLine + lineOffset).toBe(mapping.originalLine)
})
expect(styles[1].map).not.toBeUndefined()
const consumer1 = new SourceMapConsumer(styles[1].map!)
const lineOffset1 =
src.slice(0, src.indexOf(`<style module>`)).split('\n').length - 1
consumer1.eachMapping(mapping => {
expect(mapping.generatedLine + lineOffset1).toBe(mapping.originalLine)
})
expect(styles[2].map).not.toBeUndefined()
const consumer2 = new SourceMapConsumer(styles[2].map!)
const lineOffset2 =
src.slice(0, src.indexOf(`<style scoped>`)).split('\n').length - 1
consumer2.eachMapping(mapping => {
expect(mapping.generatedLine + lineOffset2).toBe(mapping.originalLine)
})
})
@ -122,8 +168,7 @@ h1 { color: red }
line: 3,
column: 1,
offset: 10 + content.length
},
source: content
}
})
})
@ -132,9 +177,8 @@ h1 { color: red }
expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
source: ''
start: { line: 1, column: 12, offset: 11 },
end: { line: 1, column: 12, offset: 11 }
})
})
@ -144,8 +188,7 @@ h1 { color: red }
expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 11, offset: 10 },
end: { line: 1, column: 11, offset: 10 },
source: ''
end: { line: 1, column: 11, offset: 10 }
})
})
@ -167,6 +210,11 @@ h1 { color: red }
expect(descriptor.script!.attrs['src']).toBe('com')
})
test('should not expose ast on template node if has src import', () => {
const { descriptor } = parse(`<template src="./foo.html"/>`)
expect(descriptor.template!.ast).toBeUndefined()
})
test('ignoreEmpty: false', () => {
const { descriptor } = parse(
`<script></script>\n<script setup>\n</script>`,
@ -176,14 +224,12 @@ h1 { color: red }
)
expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.loc).toMatchObject({
source: '',
start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 9, offset: 8 }
})
expect(descriptor.scriptSetup).toBeTruthy()
expect(descriptor.scriptSetup!.loc).toMatchObject({
source: '\n',
start: { line: 2, column: 15, offset: 32 },
end: { line: 3, column: 1, offset: 33 }
})
@ -208,13 +254,15 @@ h1 { color: red }
})
// #1120
test('alternative template lang should be treated as plain text', () => {
const content = `p(v-if="1 < 2") test`
test('template with preprocessor lang should be treated as plain text', () => {
const content = `p(v-if="1 < 2") test <div/>`
const { descriptor, errors } = parse(
`<template lang="pug">` + content + `</template>`
)
expect(errors.length).toBe(0)
expect(descriptor.template!.content).toBe(content)
// should not attempt to parse the content
expect(descriptor.template!.ast!.children.length).toBe(1)
})
//#2566
@ -260,11 +308,18 @@ h1 { color: red }
test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, {
compiler: {
parse: baseParse,
parse: (_, options) => {
options.onError!(new Error('foo') as any)
return createRoot([])
},
compile: baseCompile
}
})
expect(errors.length).toBe(1)
expect(errors.length).toBe(2)
// error thrown by the custom parse
expect(errors[0].message).toBe('foo')
// error thrown based on the returned root
expect(errors[1].message).toMatch('At least one')
})
test('treat custom blocks as raw text', () => {

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.3.9",
"version": "3.4.0-alpha.3",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -32,11 +32,10 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.23.3",
"@babel/parser": "^7.23.4",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/reactivity-transform": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
@ -44,10 +43,10 @@
"source-map-js": "^1.0.2"
},
"devDependencies": {
"@babel/types": "^7.23.3",
"@babel/types": "^7.23.4",
"@vue/consolidate": "^0.17.3",
"hash-sum": "^2.0.0",
"lru-cache": "^10.0.3",
"lru-cache": "^10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "^9.0.3",
"postcss-modules": "^4.3.1",

View File

@ -26,7 +26,6 @@ import {
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnOnce } from './warn'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { transformDestructuredProps } from './script/definePropsDestructure'
import { ScriptCompileContext } from './script/context'
import {
@ -122,14 +121,6 @@ export interface SFCScriptCompileOptions {
fileExists(file: string): boolean
readFile(file: string): string | undefined
}
/**
* (Experimental) Enable syntax transform for using refs without `.value` and
* using destructured props with reactivity
* @deprecated the Reactivity Transform proposal has been dropped. This
* feature will be removed from Vue core in 3.4. If you intend to continue
* using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
*/
reactivityTransform?: boolean
}
export interface ImportBinding {
@ -165,8 +156,6 @@ export function compileScript(
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
// TODO remove in 3.4
const enableReactivityTransform = !!options.reactivityTransform
let refBindings: string[] | undefined
if (!scriptSetup) {
@ -478,20 +467,6 @@ export function compileScript(
}
}
// apply reactivity transform
// TODO remove in 3.4
if (enableReactivityTransform && shouldTransform(script.content)) {
const { rootRefs, importedHelpers } = transformAST(
scriptAst,
ctx.s,
scriptStartOffset!
)
refBindings = rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// <script> after <script setup>
// we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition
@ -687,26 +662,7 @@ export function compileScript(
transformDestructuredProps(ctx, vueImportAliases)
}
// 4. Apply reactivity transform
// TODO remove in 3.4
if (
enableReactivityTransform &&
// normal <script> had ref bindings that maybe used in <script setup>
(refBindings || shouldTransform(scriptSetup.content))
) {
const { rootRefs, importedHelpers } = transformAST(
scriptSetupAst,
ctx.s,
startOffset,
refBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// 5. check macro args to make sure it doesn't reference setup scope
// 4. check macro args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
@ -714,7 +670,7 @@ export function compileScript(
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS)
// 6. remove non-script content
// 5. remove non-script content
if (script) {
if (startOffset < scriptStartOffset!) {
// <script setup> before <script>
@ -733,7 +689,7 @@ export function compileScript(
ctx.s.remove(endOffset, source.length)
}
// 7. analyze binding metadata
// 6. analyze binding metadata
// `defineProps` & `defineModel` also register props bindings
if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
@ -762,7 +718,7 @@ export function compileScript(
}
}
// 8. inject `useCssVars` calls
// 7. inject `useCssVars` calls
if (
sfc.cssVars.length &&
// no need to do this when targeting SSR
@ -781,7 +737,7 @@ export function compileScript(
)
}
// 9. finalize setup() argument signature
// 8. finalize setup() argument signature
let args = `__props`
if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment
@ -832,7 +788,7 @@ export function compileScript(
args += `, { ${destructureElements.join(', ')} }`
}
// 10. generate return statement
// 9. generate return statement
let returned
if (
!options.inlineTemplate ||
@ -948,7 +904,7 @@ export function compileScript(
ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
}
// 11. finalize default export
// 10. finalize default export
const genDefaultAs = options.genDefaultAs
? `const ${options.genDefaultAs} =`
: `export default`
@ -1022,7 +978,7 @@ export function compileScript(
}
}
// 12. finalize Vue helper imports
// 11. finalize Vue helper imports
if (ctx.helperImports.size > 0) {
ctx.s.prepend(
`import { ${[...ctx.helperImports]
@ -1031,8 +987,6 @@ export function compileScript(
)
}
ctx.s.trim()
return {
...scriptSetup,
bindings: ctx.bindingMetadata,

View File

@ -4,7 +4,10 @@ import {
CompilerError,
NodeTransform,
ParserOptions,
RootNode
RootNode,
NodeTypes,
ElementNode,
createRoot
} from '@vue/compiler-core'
import {
SourceMapConsumer,
@ -30,7 +33,7 @@ import { warnOnce } from './warn'
import { genCssVarsFromList } from './style/cssVars'
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
compile(source: string | RootNode, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode
}
@ -46,6 +49,7 @@ export interface SFCTemplateCompileResults {
export interface SFCTemplateCompileOptions {
source: string
ast?: RootNode
filename: string
id: string
scoped?: boolean
@ -131,7 +135,8 @@ export function compileTemplate(
try {
return doCompileTemplate({
...options,
source: preprocess(options, preprocessor)
source: preprocess(options, preprocessor),
ast: undefined // invalidate AST if template goes through preprocessor
})
} catch (e: any) {
return {
@ -164,10 +169,11 @@ function doCompileTemplate({
slotted,
inMap,
source,
ast: inAST,
ssr = false,
ssrCssVars,
isProd = false,
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
compiler,
compilerOptions = {},
transformAssetUrls
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
@ -199,7 +205,30 @@ function doCompileTemplate({
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
let { code, ast, preamble, map } = compiler.compile(source, {
const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
compiler = compiler || defaultCompiler
if (compiler !== defaultCompiler) {
// user using custom compiler, this means we cannot reuse the AST from
// the descriptor as they might be different.
inAST = undefined
}
if (inAST?.transformed) {
// If input AST has already been transformed, then it cannot be reused.
// We need to parse a fresh one. Can't just use `source` here since we need
// the AST location info to be relative to the entire SFC.
const newAST = (ssr ? CompilerDOM : compiler).parse(inAST.source, {
parseMode: 'sfc',
onError: e => errors.push(e)
})
const template = newAST.children.find(
node => node.type === NodeTypes.ELEMENT && node.tag === 'template'
) as ElementNode
inAST = createRoot(template.children, inAST.source)
}
let { code, ast, preamble, map } = compiler.compile(inAST || source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
@ -222,7 +251,7 @@ function doCompileTemplate({
// 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 (inMap && !inAST) {
if (map) {
map = mapLines(inMap, map)
}
@ -235,7 +264,7 @@ function doCompileTemplate({
let msg = w.message
if (w.loc) {
msg += `\n${generateCodeFrame(
source,
inAST?.source || source,
w.loc.start.offset,
w.loc.end.offset
)}`

View File

@ -12,13 +12,6 @@ import { SFCParseResult, parseCache as _parseCache } from './parse'
// #9521 export parseCache as a simple map to avoid exposing LRU types
export const parseCache = _parseCache as Map<string, SFCParseResult>
// TODO remove in 3.4
export {
shouldTransform as shouldTransformRef,
transform as transformRef,
transformAST as transformRefAST
} from '@vue/reactivity-transform'
// Utilities
export { parse as babelParse } from '@babel/parser'
import MagicString from 'magic-string'
@ -37,6 +30,8 @@ export {
// Internals for type resolution
export { invalidateTypeCache, registerTS } from './script/resolveType'
export { extractRuntimeProps } from './script/defineProps'
export { extractRuntimeEmits } from './script/defineEmits'
// Types
export type {
@ -62,6 +57,7 @@ export type { SFCScriptCompileOptions } from './compileScript'
export type { ScriptCompileContext } from './script/context'
export type {
TypeResolveContext,
SimpleTypeResolveOptions,
SimpleTypeResolveContext
} from './script/resolveType'
export type {
@ -73,3 +69,10 @@ export type {
CompilerError,
BindingMetadata
} from '@vue/compiler-core'
/**
* @deprecated this is preserved to avoid breaking vite-plugin-vue < 5.0
* with reactivityTransform: true. The desired behavior should be silently
* ignoring the option instead of breaking.
*/
export const shouldTransformRef = () => false

View File

@ -3,8 +3,9 @@ import {
ElementNode,
SourceLocation,
CompilerError,
TextModes,
BindingMetadata
BindingMetadata,
RootNode,
createRoot
} from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
@ -37,7 +38,7 @@ export interface SFCBlock {
export interface SFCTemplateBlock extends SFCBlock {
type: 'template'
ast: ElementNode
ast?: RootNode
}
export interface SFCScriptBlock extends SFCBlock {
@ -128,31 +129,7 @@ export function parse(
const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
isNativeTag: () => true,
// preserve all whitespaces
isPreTag: () => true,
getTextMode: ({ tag, props }, parent) => {
// all top level elements except <template> are parsed as raw text
// containers
if (
(!parent && tag !== 'template') ||
// <template lang="xxx"> should also be treated as raw text
(tag === 'template' &&
props.some(
p =>
p.type === NodeTypes.ATTRIBUTE &&
p.name === 'lang' &&
p.value &&
p.value.content &&
p.value.content !== 'html'
))
) {
return TextModes.RAWTEXT
} else {
return TextModes.DATA
}
},
parseMode: 'sfc',
onError: e => {
errors.push(e)
}
@ -161,7 +138,8 @@ export function parse(
if (node.type !== NodeTypes.ELEMENT) {
return
}
// we only want to keep the nodes that are not empty (when the tag is not a template)
// we only want to keep the nodes that are not empty
// (when the tag is not a template)
if (
ignoreEmpty &&
node.tag !== 'template' &&
@ -178,7 +156,10 @@ export function parse(
source,
false
) as SFCTemplateBlock)
templateBlock.ast = node
if (!templateBlock.attrs.src) {
templateBlock.ast = createRoot(node.children, source)
}
// warn against 2.x <template functional>
if (templateBlock.attrs.functional) {
@ -188,7 +169,9 @@ export function parse(
`difference from stateful ones. Just use a normal <template> ` +
`instead.`
) as CompilerError
err.loc = node.props.find(p => p.name === 'functional')!.loc
err.loc = node.props.find(
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'functional'
)!.loc
errors.push(err)
}
} else {
@ -307,32 +290,11 @@ function createBlock(
pad: SFCParseOptions['pad']
): SFCBlock {
const type = node.tag
let { start, end } = node.loc
let content = ''
if (node.children.length) {
start = node.children[0].loc.start
end = node.children[node.children.length - 1].loc.end
content = source.slice(start.offset, end.offset)
} else {
const offset = node.loc.source.indexOf(`</`)
if (offset > -1) {
start = {
line: start.line,
column: start.column + offset,
offset: start.offset + offset
}
}
end = { ...start }
}
const loc = {
source: content,
start,
end
}
const loc = node.innerLoc!
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
content,
content: source.slice(loc.start.offset, loc.end.offset),
loc,
attrs
}
@ -341,18 +303,19 @@ function createBlock(
}
node.props.forEach(p => {
if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true
if (p.name === 'lang') {
const name = p.name
attrs[name] = p.value ? p.value.content || true : true
if (name === 'lang') {
block.lang = p.value && p.value.content
} else if (p.name === 'src') {
} else if (name === 'src') {
block.src = p.value && p.value.content
} else if (type === 'style') {
if (p.name === 'scoped') {
if (name === 'scoped') {
;(block as SFCStyleBlock).scoped = true
} else if (p.name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name]
} else if (name === 'module') {
;(block as SFCStyleBlock).module = attrs[name]
}
} else if (type === 'script' && p.name === 'setup') {
} else if (type === 'script' && name === 'setup') {
;(block as SFCScriptBlock).setup = attrs.setup
}
}
@ -376,28 +339,27 @@ function generateSourceMap(
sourceRoot: sourceRoot.replace(/\\/g, '/')
})
map.setSourceContent(filename, source)
map._sources.add(filename)
generated.split(splitRE).forEach((line, index) => {
if (!emptyRE.test(line)) {
const originalLine = index + 1 + lineOffset
const generatedLine = index + 1
for (let i = 0; i < line.length; i++) {
if (!/\s/.test(line[i])) {
map.addMapping({
map._mappings.add({
originalLine,
originalColumn: i,
generatedLine,
generatedColumn: i,
source: filename,
original: {
line: originalLine,
column: i
},
generated: {
line: generatedLine,
column: i
}
// @ts-ignore
name: null
})
}
}
}
})
return JSON.parse(map.toString())
return map.toJSON()
}
function padContent(

View File

@ -8,7 +8,11 @@ import {
} from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
import { resolveTypeElements, resolveUnionType } from './resolveType'
import {
TypeResolveContext,
resolveTypeElements,
resolveUnionType
} from './resolveType'
export const DEFINE_EMITS = 'defineEmits'
@ -64,7 +68,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
return emitsDecl
}
function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
export function extractRuntimeEmits(ctx: TypeResolveContext): Set<string> {
const emits = new Set<string>()
const node = ctx.emitsTypeDecl!
@ -97,7 +101,7 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
}
function extractEventNames(
ctx: ScriptCompileContext,
ctx: TypeResolveContext,
eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
emits: Set<string>
) {

View File

@ -8,7 +8,11 @@ import {
} from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import { inferRuntimeType, resolveTypeElements } from './resolveType'
import {
TypeResolveContext,
inferRuntimeType,
resolveTypeElements
} from './resolveType'
import {
resolveObjectKey,
UNKNOWN_TYPE,
@ -150,7 +154,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
}
}
} else if (ctx.propsTypeDecl) {
propsDecls = genRuntimePropsFromTypes(ctx)
propsDecls = extractRuntimeProps(ctx)
}
const modelsDecls = genModelProps(ctx)
@ -162,7 +166,9 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
}
}
function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
export function extractRuntimeProps(
ctx: TypeResolveContext
): string | undefined {
// this is only called if propsTypeDecl exists
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
if (!props.length) {
@ -175,7 +181,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
// register bindings
if (!(prop.key in ctx.bindingMetadata)) {
if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
}
@ -193,7 +199,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
}
function resolveRuntimePropsFromType(
ctx: ScriptCompileContext,
ctx: TypeResolveContext,
node: Node
): PropTypeData[] {
const props: PropTypeData[] = []
@ -222,7 +228,7 @@ function resolveRuntimePropsFromType(
}
function genRuntimePropFromType(
ctx: ScriptCompileContext,
ctx: TypeResolveContext,
{ key, required, type, skipCheck }: PropTypeData,
hasStaticDefaults: boolean
): string {
@ -284,7 +290,7 @@ function genRuntimePropFromType(
* static properties, we can directly generate more optimized default
* declarations. Otherwise we will have to fallback to runtime merging.
*/
function hasStaticWithDefaults(ctx: ScriptCompileContext) {
function hasStaticWithDefaults(ctx: TypeResolveContext) {
return !!(
ctx.propsRuntimeDefaults &&
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
@ -297,7 +303,7 @@ function hasStaticWithDefaults(ctx: ScriptCompileContext) {
}
function genDestructuredDefaultValue(
ctx: ScriptCompileContext,
ctx: TypeResolveContext,
key: string,
inferredType?: string[]
):

View File

@ -27,7 +27,7 @@ export function processPropsDestructure(
ctx: ScriptCompileContext,
declId: ObjectPattern
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
if (!ctx.options.propsDestructure) {
return
}
@ -103,7 +103,7 @@ export function transformDestructuredProps(
ctx: ScriptCompileContext,
vueImportAliases: Record<string, string>
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
if (!ctx.options.propsDestructure) {
return
}

View File

@ -3,11 +3,10 @@ import { SFCDescriptor } from '../parse'
import {
NodeTypes,
SimpleExpressionNode,
createRoot,
forAliasRE,
parserOptions,
transform,
walkIdentifiers
walkIdentifiers,
TemplateChildNode
} from '@vue/compiler-dom'
import { createCache } from '../cache'
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
@ -35,53 +34,54 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
}
let code = ''
transform(createRoot([ast]), {
nodeTransforms: [
node => {
if (node.type === NodeTypes.ELEMENT) {
if (
!parserOptions.isNativeTag!(node.tag) &&
!parserOptions.isBuiltInComponent!(node.tag)
) {
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
}
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
if (!isBuiltInDirective(prop.name)) {
code += `,v${capitalize(camelize(prop.name))}`
}
// process dynamic directive arguments
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
code += `,${stripStrings(
(prop.arg as SimpleExpressionNode).content
)}`
}
ast!.children.forEach(walk)
if (prop.exp) {
code += `,${processExp(
(prop.exp as SimpleExpressionNode).content,
prop.name
)}`
}
}
if (
prop.type === NodeTypes.ATTRIBUTE &&
prop.name === 'ref' &&
prop.value?.content
) {
code += `,${prop.value.content}`
}
}
} else if (node.type === NodeTypes.INTERPOLATION) {
code += `,${processExp(
(node.content as SimpleExpressionNode).content
)}`
function walk(node: TemplateChildNode) {
switch (node.type) {
case NodeTypes.ELEMENT:
if (
!parserOptions.isNativeTag!(node.tag) &&
!parserOptions.isBuiltInComponent!(node.tag)
) {
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
}
}
]
})
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
if (!isBuiltInDirective(prop.name)) {
code += `,v${capitalize(camelize(prop.name))}`
}
// process dynamic directive arguments
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
code += `,${stripStrings(
(prop.arg as SimpleExpressionNode).content
)}`
}
if (prop.exp) {
code += `,${processExp(
(prop.exp as SimpleExpressionNode).content,
prop.name
)}`
}
}
if (
prop.type === NodeTypes.ATTRIBUTE &&
prop.name === 'ref' &&
prop.value?.content
) {
code += `,${prop.value.content}`
}
}
node.children.forEach(walk)
break
case NodeTypes.INTERPOLATION:
code += `,${processExp((node.content as SimpleExpressionNode).content)}`
break
}
}
code += ';'
templateUsageCheckCache.set(content, code)

View File

@ -1,8 +1,6 @@
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { analyzeScriptBindings } from './analyzeScriptBindings'
import { ScriptCompileContext } from './context'
import MagicString from 'magic-string'
import { RawSourceMap } from 'source-map-js'
import { rewriteDefaultAST } from '../rewriteDefault'
import { genNormalScriptCssVarsCode } from '../style/cssVars'
@ -22,33 +20,8 @@ export function processNormalScript(
let map = script.map
const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body)
const { source, filename, cssVars } = ctx.descriptor
const { sourceMap, genDefaultAs, isProd } = ctx.options
// TODO remove in 3.4
if (ctx.options.reactivityTransform && shouldTransform(content)) {
const s = new MagicString(source)
const startOffset = script.loc.start.offset
const endOffset = script.loc.end.offset
const { importedHelpers } = transformAST(scriptAst, s, startOffset)
if (importedHelpers.length) {
s.prepend(
`import { ${importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}
s.remove(0, startOffset)
s.remove(endOffset, source.length)
content = s.toString()
if (sourceMap !== false) {
map = s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap
}
}
const { cssVars } = ctx.descriptor
const { genDefaultAs, isProd } = ctx.options
if (cssVars.length || genDefaultAs) {
const defaultVar = genDefaultAs || normalScriptDefaultVar

View File

@ -43,6 +43,13 @@ import { extname, dirname, join } from 'path'
import { minimatch as isMatch } from 'minimatch'
import * as process from 'process'
export type SimpleTypeResolveOptions = Partial<
Pick<
SFCScriptCompileOptions,
'globalTypeFiles' | 'fs' | 'babelParserPlugins' | 'isProd'
>
>
/**
* TypeResolveContext is compatible with ScriptCompileContext
* but also allows a simpler version of it with minimal required properties
@ -60,13 +67,28 @@ import * as process from 'process'
*/
export type SimpleTypeResolveContext = Pick<
ScriptCompileContext,
// required
'source' | 'filename' | 'error' | 'options'
// file
| 'source'
| 'filename'
// utils
| 'error'
| 'helper'
| 'getString'
// props
| 'propsTypeDecl'
| 'propsRuntimeDefaults'
| 'propsDestructuredBindings'
// emits
| 'emitsTypeDecl'
> &
Partial<
Pick<ScriptCompileContext, 'scope' | 'globalScopes' | 'deps' | 'fs'>
> & {
ast: Statement[]
options: SimpleTypeResolveOptions
}
export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext

View File

@ -70,7 +70,7 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
return vars
}
const enum LexerState {
enum LexerState {
inParens,
inSingleQuoteString,
inDoubleQuoteString

View File

@ -38,7 +38,12 @@ const scss: StylePreprocessor = (source, map, options, load = require) => {
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
map: merge(
map,
result.map.toJSON
? result.map.toJSON()
: JSON.parse(result.map.toString())
),
errors: [],
dependencies
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.3.9",
"version": "3.4.0-alpha.3",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

@ -16,14 +16,14 @@ export function createSSRCompilerError(
return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError
}
export const enum SSRErrorCodes {
export enum SSRErrorCodes {
X_SSR_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_NO_TELEPORT_TARGET,
X_SSR_INVALID_AST_NODE
}
if (__TEST__) {
// esbuild cannot infer const enum increments if first value is from another
// esbuild cannot infer enum increments if first value is from another
// file, so we have to manually keep them in sync. this check ensures it
// errors out if there are collisions.
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {

View File

@ -11,7 +11,8 @@ import {
noopDirectiveTransform,
transformBind,
transformStyle,
transformOn
transformOn,
RootNode
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -28,12 +29,11 @@ import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttr
import { ssrInjectCssVars } from './transforms/ssrInjectCssVars'
export function compile(
template: string,
source: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
options = {
...options,
// apply DOM-specific parsing options
...parserOptions,
ssr: true,
inSSR: true,
@ -45,7 +45,7 @@ export function compile(
hoistStatic: false
}
const ast = baseParse(template, options)
const ast = typeof source === 'string' ? baseParse(source, options) : source
// Save raw options for AST. This is needed when performing sub-transforms
// on slot vnode branches.

View File

@ -6,8 +6,7 @@ import {
createSimpleExpression,
RootNode,
TemplateChildNode,
findDir,
isBuiltInType
findDir
} from '@vue/compiler-dom'
export const ssrInjectCssVars: NodeTransform = (node, context) => {
@ -43,7 +42,7 @@ function injectCssVars(node: RootNode | TemplateChildNode) {
node.tagType === ElementTypes.COMPONENT) &&
!findDir(node, 'for')
) {
if (isBuiltInType(node.tag, 'Suspense')) {
if (node.tag === 'suspense' || node.tag === 'Suspense') {
for (const child of node.children) {
if (
child.type === NodeTypes.ELEMENT &&

View File

@ -7,8 +7,7 @@ import {
RootNode,
TemplateChildNode,
ParentNode,
findDir,
isBuiltInType
findDir
} from '@vue/compiler-dom'
const filterChild = (node: ParentNode) =>
@ -28,8 +27,10 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT &&
(isBuiltInType(node.tag, 'Transition') ||
isBuiltInType(node.tag, 'KeepAlive'))
(node.tag === 'transition' ||
node.tag === 'Transition' ||
node.tag === 'KeepAlive' ||
node.tag === 'keep-alive')
) {
const rootChildren = filterChild(context.root)
if (rootChildren.length === 1 && rootChildren[0] === node) {

View File

@ -37,7 +37,8 @@ import {
JSChildNode,
RESOLVE_DYNAMIC_COMPONENT,
TRANSITION,
stringifyExpression
stringifyExpression,
DirectiveNode
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import {
@ -54,7 +55,7 @@ import {
ssrProcessTransitionGroup,
ssrTransformTransitionGroup
} from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
import { isSymbol, isObject, isArray, extend } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
import {
ssrProcessTransition,
@ -278,8 +279,8 @@ const vnodeDirectiveTransforms = {
}
function createVNodeSlotBranch(
props: ExpressionNode | undefined,
vForExp: ExpressionNode | undefined,
slotProps: ExpressionNode | undefined,
vFor: DirectiveNode | undefined,
children: TemplateChildNode[],
parentContext: TransformContext
): ReturnStatement {
@ -300,32 +301,28 @@ function createVNodeSlotBranch(
}
// wrap the children with a wrapper template for proper children treatment.
// important: provide v-slot="props" and v-for="exp" on the wrapper for
// proper scope analysis
const wrapperProps: TemplateNode['props'] = []
if (slotProps) {
wrapperProps.push({
type: NodeTypes.DIRECTIVE,
name: 'slot',
exp: slotProps,
arg: undefined,
modifiers: [],
loc: locStub
})
}
if (vFor) {
wrapperProps.push(extend({}, vFor))
}
const wrapperNode: TemplateNode = {
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'template',
tagType: ElementTypes.TEMPLATE,
isSelfClosing: false,
// important: provide v-slot="props" and v-for="exp" on the wrapper for
// proper scope analysis
props: [
{
type: NodeTypes.DIRECTIVE,
name: 'slot',
exp: props,
arg: undefined,
modifiers: [],
loc: locStub
},
{
type: NodeTypes.DIRECTIVE,
name: 'for',
exp: vForExp,
arg: undefined,
modifiers: [],
loc: locStub
}
],
props: wrapperProps,
children,
loc: locStub,
codegenNode: undefined

View File

@ -292,14 +292,15 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
}
} else {
// special case: value on <textarea>
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
const name = prop.name
if (node.tag === 'textarea' && name === 'value' && prop.value) {
rawChildrenMap.set(node, escapeHtml(prop.value.content))
} else if (!needMergeProps) {
if (prop.name === 'key' || prop.name === 'ref') {
if (name === 'key' || name === 'ref') {
continue
}
// static prop
if (prop.name === 'class' && prop.value) {
if (name === 'class' && prop.value) {
staticClassBinding = JSON.stringify(prop.value.content)
}
openTag.push(

View File

@ -1,110 +0,0 @@
import { ref, computed, Ref, ComputedRef, WritableComputedRef } from 'vue'
import 'vue/macros-global'
import { RefType, RefTypes } from 'vue/macros'
import { expectType } from './utils'
// wrapping refs
// normal
let n = $(ref(1))
n = 2
// @ts-expect-error
n = 'foo'
// #4499 nullable
let msg = $(ref<string | null>(null))
msg = 'hello world'
msg = null
expectType<RefTypes.Ref | undefined>(msg![RefType])
// computed
let m = $(computed(() => n + 1))
m * 1
// @ts-expect-error
m.slice()
expectType<RefTypes.ComputedRef | undefined>(m[RefType])
// writable computed
let wc = $(
computed({
get: () => n + 1,
set: v => (n = v - 1)
})
)
wc = 2
// @ts-expect-error
wc = 'foo'
expectType<RefTypes.WritableComputedRef | undefined>(wc[RefType])
// destructure
function useFoo() {
let x = $ref(1)
let y = $computed(() => 'hi')
return $$({
x,
y,
z: 123
})
}
const fooRes = useFoo()
const { x, y, z } = $(fooRes)
expectType<number>(x)
expectType<string>(y)
expectType<number>(z)
// $ref
expectType<number>($ref(1))
expectType<number>($ref(ref(1)))
expectType<{ foo: number }>($ref({ foo: ref(1) }))
// $shallowRef
expectType<number>($shallowRef(1))
expectType<{ foo: Ref<number> }>($shallowRef({ foo: ref(1) }))
// $computed
expectType<number>($computed(() => 1))
let b = $ref(1)
expectType<number>(
$computed(() => b, {
onTrack() {}
})
)
// writable computed
expectType<number>(
$computed({
get: () => 1,
set: () => {}
})
)
// $$
const xRef = $$(x)
expectType<Ref<number>>(xRef)
const yRef = $$(y)
expectType<ComputedRef<string>>(yRef)
const c = $computed(() => 1)
const cRef = $$(c)
expectType<ComputedRef<number>>(cRef)
const c2 = $computed({
get: () => 1,
set: () => {}
})
const c2Ref = $$(c2)
expectType<WritableComputedRef<number>>(c2Ref)
// $$ on object
const obj = $$({
n,
m,
wc
})
expectType<Ref<number>>(obj.n)
expectType<ComputedRef<number>>(obj.m)
expectType<WritableComputedRef<number>>(obj.wc)

12
packages/global.d.ts vendored
View File

@ -44,6 +44,18 @@ declare module 'estree-walker' {
)
}
declare module 'source-map-js' {
export interface SourceMapGenerator {
// SourceMapGenerator has this method but the types do not include it
toJSON(): RawSourceMap
_sources: Set<string>
_names: Set<string>
_mappings: {
add(mapping: MappingItem): void
}
}
}
declare interface String {
/**
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository.

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,123 +0,0 @@
# @vue/reactivity-transform
> ⚠️ This is experimental and the proposal has been dropped.
> The feature is now marked as deprecated and will be removed from Vue core
> in 3.4.
>
> See reason for deprecation [here](https://github.com/vuejs/rfcs/discussions/369#discussioncomment-5059028).
## Basic Rules
- Ref-creating APIs have `$`-prefixed versions that create reactive variables instead. They also do not need to be explicitly imported. These include:
- `ref`
- `computed`
- `shallowRef`
- `customRef`
- `toRef`
- `$()` can be used to destructure an object into reactive variables, or turn existing refs into reactive variables
- `$$()` to "escape" the transform, which allows access to underlying refs
```js
import { watchEffect } from 'vue'
// bind ref as a variable
let count = $ref(0)
watchEffect(() => {
// no need for .value
console.log(count)
})
// assignments are reactive
count++
// get the actual ref
console.log($$(count)) // { value: 1 }
```
Macros can be optionally imported to make it more explicit:
```js
// not necessary, but also works
import { $, $ref } from 'vue/macros'
let count = $ref(0)
const { x, y } = $(useMouse())
```
### Global Types
To enable types for the macros globally, include the following in a `.d.ts` file:
```ts
/// <reference types="vue/macros-global" />
```
## API
This package is the lower-level transform that can be used standalone. Higher-level tooling (e.g. `@vitejs/plugin-vue` and `vue-loader`) will provide integration via options.
### `shouldTransform`
Can be used to do a cheap check to determine whether full transform should be performed.
```js
import { shouldTransform } from '@vue/reactivity-transform'
shouldTransform(`let a = ref(0)`) // false
shouldTransform(`let a = $ref(0)`) // true
```
### `transform`
```js
import { transform } from '@vue/reactivity-transform'
const src = `let a = $ref(0); a++`
const {
code, // import { ref as _ref } from 'vue'; let a = (ref(0)); a.value++"
map
} = transform(src, {
filename: 'foo.ts',
sourceMap: true,
// @babel/parser plugins to enable.
// 'typescript' and 'jsx' will be auto-inferred from filename if provided,
// so in most cases explicit parserPlugins are not necessary
parserPlugins: [
/* ... */
]
})
```
**Options**
```ts
interface RefTransformOptions {
filename?: string
sourceMap?: boolean // default: false
parserPlugins?: ParserPlugin[]
importHelpersFrom?: string // default: "vue"
}
```
### `transformAST`
Transform with an existing Babel AST + MagicString instance. This is used internally by `@vue/compiler-sfc` to avoid double parse/transform cost.
```js
import { transformAST } from '@vue/reactivity-transform'
import { parse } from '@babel/parser'
import MagicString from 'magic-string'
const src = `let a = $ref(0); a++`
const ast = parse(src, { sourceType: 'module' })
const s = new MagicString(src)
const {
rootRefs, // ['a']
importedHelpers // ['ref']
} = transformAST(ast, s)
console.log(s.toString()) // let a = _ref(0); a.value++
```

View File

@ -1,292 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`$ unwrapping 1`] = `
"
import { ref, shallowRef } from 'vue'
let foo = (ref())
export let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
label: var e = (ref())
"
`;
exports[`$$ 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
const b = (a)
const c = ({ a })
callExternal((a))
"
`;
exports[`$$ with some edge cases 1`] = `
"import { ref as _ref } from 'vue'
;( /* 2 */ count /* 2 */ )
;( count /* 2 */, /**/ a )
;( (count /* 2 */, /**/ a) /**/ )
{
a:(count,a)
}
;((count) + 1)
;([count])
; (count )
console.log(((a)))
;(a,b)
;(((a++,b)))
count = ( a++ ,b)
count = ()=>(a++,b)
let r1 = _ref(a, (a++,b))
let r2 = { a:(a++,b),b: (a) }
switch((c)){
case d:
;(a)
;((h,f))
break
}
((count++,(count),(count,a)))
"
`;
exports[`$computed declaration 1`] = `
"import { computed as _computed } from 'vue'
let a = _computed(() => 1)
"
`;
exports[`$ref & $shallowRef declarations 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
let foo = _ref()
export let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
label: var e = _ref()
"
`;
exports[`accessing ref binding 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
console.log(a.value)
function get() {
return a.value + 1
}
"
`;
exports[`array destructure 1`] = `
"import { ref as _ref, toRef as _toRef } from 'vue'
let n = _ref(1), __$temp_1 = (useFoo()),
a = _toRef(__$temp_1, 0),
b = _toRef(__$temp_1, 1, 1);
console.log(n.value, a.value, b.value)
"
`;
exports[`handle TS casting syntax 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
console.log(a.value!)
console.log(a.value! + 1)
console.log(a.value as number)
console.log((a.value as number) + 1)
console.log(<number>a.value)
console.log(<number>a.value + 1)
console.log(a.value! + (a.value as number))
console.log(a.value! + <number>a.value)
console.log((a.value as number) + <number>a.value)
"
`;
exports[`macro import alias and removal 1`] = `
"import { ref as _ref, toRef as _toRef } from 'vue'
let a = _ref(1)
const __$temp_1 = (useMouse()),
x = _toRef(__$temp_1, 'x'),
y = _toRef(__$temp_1, 'y');
"
`;
exports[`mixing $ref & $computed declarations 1`] = `
"import { ref as _ref, computed as _computed } from 'vue'
let a = _ref(1), b = _computed(() => a.value + 1)
"
`;
exports[`multi $ref declarations 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
"
`;
exports[`mutating ref binding 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
let b = _ref({ count: 0 })
function inc() {
a.value++
a.value = a.value + 1
b.value.count++
b.value.count = b.value.count + 1
;({ a: a.value } = { a: 2 })
;[a.value] = [1]
}
"
`;
exports[`nested destructure 1`] = `
"import { toRef as _toRef } from 'vue'
let __$temp_1 = (useFoo()),
b = _toRef(__$temp_1[0].a, 'b');
let __$temp_2 = (useBar()),
d = _toRef(__$temp_2.c, 0),
e = _toRef(__$temp_2.c, 1);
console.log(b.value, d.value, e.value)
"
`;
exports[`nested scopes 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(0)
let b = _ref(0)
let c = 0
a.value++ // outer a
b.value++ // outer b
c++ // outer c
let bar = _ref(0)
bar.value++ // outer bar
function foo({ a }) {
a++ // inner a
b.value++ // inner b
let c = _ref(0)
c.value++ // inner c
let d = _ref(0)
function bar(c) {
c++ // nested c
d.value++ // nested d
}
bar() // inner bar
if (true) {
let a = _ref(0)
a.value++ // if block a
}
return ({ a, b, c, d })
}
"
`;
exports[`object destructure 1`] = `
"import { ref as _ref, toRef as _toRef } from 'vue'
let n = _ref(1), __$temp_1 = (useFoo()),
a = _toRef(__$temp_1, 'a'),
c = _toRef(__$temp_1, 'b'),
d = _toRef(__$temp_1, 'd', 1),
f = _toRef(__$temp_1, 'e', 2),
h = _toRef(__$temp_1, g);
let __$temp_2 = (useSomething(() => 1)),
foo = _toRef(__$temp_2, 'foo');;
console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)
"
`;
exports[`object destructure w/ mid-path default values 1`] = `
"import { toRef as _toRef } from 'vue'
const __$temp_1 = (useFoo()),
b = _toRef((__$temp_1.a || { b: 123 }), 'b');
console.log(b.value)
"
`;
exports[`should not overwrite current scope 1`] = `
"
const fn = () => {
const $ = () => 'foo'
const $ref = () => 'bar'
const $$ = () => 'baz'
console.log($())
console.log($ref())
console.log($$())
}
"
`;
exports[`should not overwrite importing 1`] = `
"
import { $, $$ } from './foo'
$('foo')
$$('bar')
"
`;
exports[`should not rewrite scope variable 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
let b = _ref(1)
let d = _ref(1)
const e = 1
function test() {
const a = 2
console.log(a)
console.log(b.value)
let c = { c: 3 }
console.log(c)
console.log(d.value)
console.log(e)
}
let err = _ref(null)
try {
} catch (err) {
console.log(err)
}
"
`;
exports[`should not rewrite type identifiers 1`] = `
"import { ref as _ref } from 'vue'
const props = defineProps<{msg: string; ids?: string[]}>()
let ids = _ref([])"
`;
exports[`using ref binding in property shorthand 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
const b = { a: a.value }
function test() {
const { a } = b
}
"
`;

View File

@ -1,564 +0,0 @@
import { parse } from '@babel/parser'
import { transform } from '../src'
function assertCode(code: string) {
// parse the generated code to make sure it is valid
try {
parse(code, {
sourceType: 'module',
plugins: ['typescript']
})
} catch (e: any) {
console.log(code)
throw e
}
expect(code).toMatchSnapshot()
}
test('$ unwrapping', () => {
const { code, rootRefs } = transform(`
import { ref, shallowRef } from 'vue'
let foo = $(ref())
export let a = $(ref(1))
let b = $(shallowRef({
count: 0
}))
let c = () => {}
let d
label: var e = $(ref())
`)
expect(code).not.toMatch(`$(ref())`)
expect(code).not.toMatch(`$(ref(1))`)
expect(code).not.toMatch(`$(shallowRef({`)
expect(code).toMatch(`let foo = (ref())`)
expect(code).toMatch(`export let a = (ref(1))`)
expect(code).toMatch(`
let b = (shallowRef({
count: 0
}))
`)
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
expect(code).toMatch(`label: var e = (ref())`)
expect(rootRefs).toStrictEqual(['foo', 'a', 'b', 'e'])
assertCode(code)
})
test('$ref & $shallowRef declarations', () => {
const { code, rootRefs, importedHelpers } = transform(`
let foo = $ref()
export let a = $ref(1)
let b = $shallowRef({
count: 0
})
let c = () => {}
let d
label: var e = $ref()
`)
expect(code).toMatch(
`import { ref as _ref, shallowRef as _shallowRef } from 'vue'`
)
expect(code).not.toMatch(`$ref()`)
expect(code).not.toMatch(`$ref(1)`)
expect(code).not.toMatch(`$shallowRef({`)
expect(code).toMatch(`let foo = _ref()`)
expect(code).toMatch(`let a = _ref(1)`)
expect(code).toMatch(`
let b = _shallowRef({
count: 0
})
`)
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
expect(code).toMatch(`label: var e = _ref()`)
expect(rootRefs).toStrictEqual(['foo', 'a', 'b', 'e'])
expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
assertCode(code)
})
test('multi $ref declarations', () => {
const { code, rootRefs, importedHelpers } = transform(`
let a = $ref(1), b = $ref(2), c = $ref({
count: 0
})
`)
expect(code).toMatch(`
let a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
`)
expect(rootRefs).toStrictEqual(['a', 'b', 'c'])
expect(importedHelpers).toStrictEqual(['ref'])
assertCode(code)
})
test('$computed declaration', () => {
const { code, rootRefs, importedHelpers } = transform(`
let a = $computed(() => 1)
`)
expect(code).toMatch(`
let a = _computed(() => 1)
`)
expect(rootRefs).toStrictEqual(['a'])
expect(importedHelpers).toStrictEqual(['computed'])
assertCode(code)
})
test('mixing $ref & $computed declarations', () => {
const { code, rootRefs, importedHelpers } = transform(`
let a = $ref(1), b = $computed(() => a + 1)
`)
expect(code).toMatch(`
let a = _ref(1), b = _computed(() => a.value + 1)
`)
expect(rootRefs).toStrictEqual(['a', 'b'])
expect(importedHelpers).toStrictEqual(['ref', 'computed'])
assertCode(code)
})
test('accessing ref binding', () => {
const { code } = transform(`
let a = $ref(1)
console.log(a)
function get() {
return a + 1
}
`)
expect(code).toMatch(`console.log(a.value)`)
expect(code).toMatch(`return a.value + 1`)
assertCode(code)
})
describe('cases that should not append .value', () => {
test('member expression', () => {
const { code } = transform(`
let a = $ref(1)
console.log(b.a)
`)
expect(code).not.toMatch(`a.value`)
})
test('function argument', () => {
const { code } = transform(`
let a = $ref(1)
function get(a) {
return a + 1
}
function get2({ a }) {
return a + 1
}
function get3([a]) {
return a + 1
}
`)
expect(code).not.toMatch(`a.value`)
})
test('for in/of loops', () => {
const { code } = transform(`
let a = $ref(1)
for (const [a, b] of arr) {
console.log(a)
}
for (let a in arr) {
console.log(a)
}
`)
expect(code).not.toMatch(`a.value`)
})
})
test('mutating ref binding', () => {
const { code } = transform(`
let a = $ref(1)
let b = $ref({ count: 0 })
function inc() {
a++
a = a + 1
b.count++
b.count = b.count + 1
;({ a } = { a: 2 })
;[a] = [1]
}
`)
expect(code).toMatch(`a.value++`)
expect(code).toMatch(`a.value = a.value + 1`)
expect(code).toMatch(`b.value.count++`)
expect(code).toMatch(`b.value.count = b.value.count + 1`)
expect(code).toMatch(`;({ a: a.value } = { a: 2 })`)
expect(code).toMatch(`;[a.value] = [1]`)
assertCode(code)
})
test('using ref binding in property shorthand', () => {
const { code } = transform(`
let a = $ref(1)
const b = { a }
function test() {
const { a } = b
}
`)
expect(code).toMatch(`const b = { a: a.value }`)
// should not convert destructure
expect(code).toMatch(`const { a } = b`)
assertCode(code)
})
test('should not rewrite scope variable', () => {
const { code } = transform(`
let a = $ref(1)
let b = $ref(1)
let d = $ref(1)
const e = 1
function test() {
const a = 2
console.log(a)
console.log(b)
let c = { c: 3 }
console.log(c)
console.log(d)
console.log(e)
}
let err = $ref(null)
try {
} catch (err) {
console.log(err)
}
`)
expect(code).toMatch('console.log(a)')
expect(code).toMatch('console.log(b.value)')
expect(code).toMatch('console.log(c)')
expect(code).toMatch('console.log(d.value)')
expect(code).toMatch('console.log(e)')
expect(code).toMatch('console.log(err)')
assertCode(code)
})
test('object destructure', () => {
const { code, rootRefs } = transform(`
let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
let { foo } = $(useSomething(() => 1));
console.log(n, a, c, d, f, h, foo)
`)
expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)
expect(code).toMatch(`c = _toRef(__$temp_1, 'b')`)
expect(code).toMatch(`d = _toRef(__$temp_1, 'd', 1)`)
expect(code).toMatch(`f = _toRef(__$temp_1, 'e', 2)`)
expect(code).toMatch(`h = _toRef(__$temp_1, g)`)
expect(code).toMatch(`foo = _toRef(__$temp_2, 'foo')`)
expect(code).toMatch(
`console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)`
)
expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'h', 'foo'])
assertCode(code)
})
test('object destructure w/ mid-path default values', () => {
const { code, rootRefs } = transform(`
const { a: { b } = { b: 123 }} = $(useFoo())
console.log(b)
`)
expect(code).toMatch(`b = _toRef((__$temp_1.a || { b: 123 }), 'b')`)
expect(code).toMatch(`console.log(b.value)`)
expect(rootRefs).toStrictEqual(['b'])
assertCode(code)
})
test('array destructure', () => {
const { code, rootRefs } = transform(`
let n = $ref(1), [a, b = 1] = $(useFoo())
console.log(n, a, b)
`)
expect(code).toMatch(`a = _toRef(__$temp_1, 0)`)
expect(code).toMatch(`b = _toRef(__$temp_1, 1, 1)`)
expect(code).toMatch(`console.log(n.value, a.value, b.value)`)
expect(rootRefs).toStrictEqual(['n', 'a', 'b'])
assertCode(code)
})
test('nested destructure', () => {
const { code, rootRefs } = transform(`
let [{ a: { b }}] = $(useFoo())
let { c: [d, e] } = $(useBar())
console.log(b, d, e)
`)
expect(code).toMatch(`b = _toRef(__$temp_1[0].a, 'b')`)
expect(code).toMatch(`d = _toRef(__$temp_2.c, 0)`)
expect(code).toMatch(`e = _toRef(__$temp_2.c, 1)`)
expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
assertCode(code)
})
test('$$', () => {
const { code } = transform(`
let a = $ref(1)
const b = $$(a)
const c = $$({ a })
callExternal($$(a))
`)
expect(code).toMatch(`const b = (a)`)
expect(code).toMatch(`const c = ({ a })`)
expect(code).toMatch(`callExternal((a))`)
assertCode(code)
})
test('$$ with some edge cases', () => {
const { code } = transform(`
$$( /* 2 */ count /* 2 */ )
$$( count /* 2 */, /**/ a )
$$( (count /* 2 */, /**/ a) /**/ )
{
a:$$(count,a)
}
$$((count) + 1)
$$([count])
$$ (count )
console.log($$($$(a)))
$$(a,b)
$$($$((a++,b)))
count = $$( a++ ,b)
count = ()=>$$(a++,b)
let r1 = $ref(a, $$(a++,b))
let r2 = { a:$$(a++,b),b:$$ (a) }
switch($$(c)){
case d:
$$(a)
$$($$(h,f))
break
}
($$(count++,$$(count),$$(count,a)))
`)
expect(code).toMatch(`/* 2 */ count /* 2 */`)
expect(code).toMatch(`;( count /* 2 */, /**/ a )`)
expect(code).toMatch(`;( (count /* 2 */, /**/ a) /**/ )`)
expect(code).toMatch(`a:(count,a)`)
expect(code).toMatch(`;((count) + 1)`)
expect(code).toMatch(`;([count])`)
expect(code).toMatch(`;(a,b)`)
expect(code).toMatch(`log(((a)))`)
expect(code).toMatch(`count = ( a++ ,b)`)
expect(code).toMatch(`()=>(a++,b)`)
expect(code).toMatch(`_ref(a, (a++,b))`)
expect(code).toMatch(`{ a:(a++,b),b: (a) }`)
expect(code).toMatch(`switch((c))`)
expect(code).toMatch(`;((h,f))`)
expect(code).toMatch(`((count++,(count),(count,a)))`)
assertCode(code)
})
test('nested scopes', () => {
const { code, rootRefs } = transform(`
let a = $ref(0)
let b = $ref(0)
let c = 0
a++ // outer a
b++ // outer b
c++ // outer c
let bar = $ref(0)
bar++ // outer bar
function foo({ a }) {
a++ // inner a
b++ // inner b
let c = $ref(0)
c++ // inner c
let d = $ref(0)
function bar(c) {
c++ // nested c
d++ // nested d
}
bar() // inner bar
if (true) {
let a = $ref(0)
a++ // if block a
}
return $$({ a, b, c, d })
}
`)
expect(rootRefs).toStrictEqual(['a', 'b', 'bar'])
expect(code).toMatch('a.value++ // outer a')
expect(code).toMatch('b.value++ // outer b')
expect(code).toMatch('c++ // outer c')
expect(code).toMatch('a++ // inner a') // shadowed by function arg
expect(code).toMatch('b.value++ // inner b')
expect(code).toMatch('c.value++ // inner c') // shadowed by local ref binding
expect(code).toMatch('c++ // nested c') // shadowed by inline fn arg
expect(code).toMatch(`d.value++ // nested d`)
expect(code).toMatch(`a.value++ // if block a`) // if block
expect(code).toMatch(`bar.value++ // outer bar`)
// inner bar shadowed by function declaration
expect(code).toMatch(`bar() // inner bar`)
expect(code).toMatch(`return ({ a, b, c, d })`)
assertCode(code)
})
//#4062
test('should not rewrite type identifiers', () => {
const { code } = transform(
`const props = defineProps<{msg: string; ids?: string[]}>()
let ids = $ref([])`,
{
parserPlugins: ['typescript']
}
)
expect(code).not.toMatch('.value')
assertCode(code)
})
// #4254
test('handle TS casting syntax', () => {
const { code } = transform(
`
let a = $ref(1)
console.log(a!)
console.log(a! + 1)
console.log(a as number)
console.log((a as number) + 1)
console.log(<number>a)
console.log(<number>a + 1)
console.log(a! + (a as number))
console.log(a! + <number>a)
console.log((a as number) + <number>a)
`,
{
parserPlugins: ['typescript']
}
)
expect(code).toMatch('console.log(a.value!)')
expect(code).toMatch('console.log(a.value as number)')
expect(code).toMatch('console.log(<number>a.value)')
assertCode(code)
})
test('macro import alias and removal', () => {
const { code } = transform(
`
import { $ as fromRefs, $ref } from 'vue/macros'
let a = $ref(1)
const { x, y } = fromRefs(useMouse())
`
)
// should remove imports
expect(code).not.toMatch(`from 'vue/macros'`)
expect(code).toMatch(`let a = _ref(1)`)
expect(code).toMatch(`const __$temp_1 = (useMouse())`)
assertCode(code)
})
// #6838
test('should not overwrite importing', () => {
const { code } = transform(
`
import { $, $$ } from './foo'
$('foo')
$$('bar')
`
)
assertCode(code)
})
// #6838
test('should not overwrite current scope', () => {
const { code } = transform(
`
const fn = () => {
const $ = () => 'foo'
const $ref = () => 'bar'
const $$ = () => 'baz'
console.log($())
console.log($ref())
console.log($$())
}
`
)
assertCode(code)
})
describe('errors', () => {
test('$ref w/ destructure', () => {
expect(() => transform(`let { a } = $ref(1)`)).toThrow(
`cannot be used with destructure`
)
})
test('$computed w/ destructure', () => {
expect(() => transform(`let { a } = $computed(() => 1)`)).toThrow(
`cannot be used with destructure`
)
})
test('warn usage in non-init positions', () => {
expect(() =>
transform(
`let bar = $ref(1)
bar = $ref(2)`
)
).toThrow(`$ref can only be used as the initializer`)
expect(() => transform(`let bar = { foo: $computed(1) }`)).toThrow(
`$computed can only be used as the initializer`
)
})
test('not transform the prototype attributes', () => {
const { code } = transform(`
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (val, key) => hasOwnProperty.call(val, key)
`)
expect(code).not.toMatch('.value')
})
test('rest element in $() destructure', () => {
expect(() => transform(`let { a, ...b } = $(foo())`)).toThrow(
`does not support rest element`
)
expect(() => transform(`let [a, ...b] = $(foo())`)).toThrow(
`does not support rest element`
)
})
test('assignment to constant variable', () => {
expect(() =>
transform(`
const foo = $ref(0)
foo = 1
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const [a, b] = $([1, 2])
a = 1
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const foo = $ref(0)
foo++
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const foo = $ref(0)
bar = foo
`)
).not.toThrow()
})
})

View File

@ -1,41 +0,0 @@
{
"name": "@vue/reactivity-transform",
"version": "3.3.9",
"description": "@vue/reactivity-transform",
"main": "dist/reactivity-transform.cjs.js",
"files": [
"dist"
],
"buildOptions": {
"formats": [
"cjs"
],
"prod": false
},
"types": "dist/reactivity-transform.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core-vapor.git",
"directory": "packages/reactivity-transform"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core-vapor/issues"
},
"homepage": "https://github.com/vuejs/core-vapor/tree/dev/packages/reactivity-transform#readme",
"dependencies": {
"@babel/parser": "^7.23.3",
"@vue/compiler-core": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
},
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/types": "^7.23.3"
}
}

View File

@ -1,3 +0,0 @@
export function plugin() {
// TODO
}

View File

@ -1 +0,0 @@
export * from './reactivityTransform'

View File

@ -1,794 +0,0 @@
import {
Node,
Identifier,
BlockStatement,
CallExpression,
ObjectPattern,
ArrayPattern,
Program,
VariableDeclarator,
Expression,
VariableDeclaration,
ImportDeclaration,
ImportSpecifier,
ImportDefaultSpecifier,
ImportNamespaceSpecifier
} from '@babel/types'
import MagicString, { SourceMap } from 'magic-string'
import { walk } from 'estree-walker'
import {
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
isReferencedIdentifier,
isStaticProperty,
walkFunctionParams
} from '@vue/compiler-core'
import { parse, ParserPlugin } from '@babel/parser'
import { hasOwn, isArray, isString, genPropsAccessExp } from '@vue/shared'
const CONVERT_SYMBOL = '$'
const ESCAPE_SYMBOL = '$$'
const IMPORT_SOURCE = 'vue/macros'
const shorthands = ['ref', 'computed', 'shallowRef', 'toRef', 'customRef']
const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\s*(\(|\<)/
/**
* @deprecated will be removed in 3.4
*/
export function shouldTransform(src: string): boolean {
return transformCheckRE.test(src)
}
interface Binding {
isConst?: boolean
isProp?: boolean
}
type Scope = Record<string, Binding | false>
export interface RefTransformOptions {
filename?: string
sourceMap?: boolean
parserPlugins?: ParserPlugin[]
importHelpersFrom?: string
}
export interface RefTransformResults {
code: string
map: SourceMap | null
rootRefs: string[]
importedHelpers: string[]
}
export interface ImportBinding {
local: string
imported: string
source: string
specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
}
/**
* @deprecated will be removed in 3.4
*/
export function transform(
src: string,
{
filename,
sourceMap,
parserPlugins,
importHelpersFrom = 'vue'
}: RefTransformOptions = {}
): RefTransformResults {
const plugins: ParserPlugin[] = parserPlugins || []
if (filename) {
if (/\.tsx?$/.test(filename)) {
plugins.push('typescript')
}
if (filename.endsWith('x')) {
plugins.push('jsx')
}
}
const ast = parse(src, {
sourceType: 'module',
plugins
})
const s = new MagicString(src)
const res = transformAST(ast.program, s, 0)
// inject helper imports
if (res.importedHelpers.length) {
s.prepend(
`import { ${res.importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from '${importHelpersFrom}'\n`
)
}
return {
...res,
code: s.toString(),
map: sourceMap
? s.generateMap({
source: filename,
hires: true,
includeContent: true
})
: null
}
}
/**
* @deprecated will be removed in 3.4
*/
export function transformAST(
ast: Program,
s: MagicString,
offset = 0,
knownRefs?: string[],
knownProps?: Record<
string, // public prop key
{
local: string // local identifier, may be different
default?: any
isConst?: boolean
}
>
): {
rootRefs: string[]
importedHelpers: string[]
} {
warnExperimental()
const userImports: Record<string, ImportBinding> = Object.create(null)
for (const node of ast.body) {
if (node.type !== 'ImportDeclaration') continue
walkImportDeclaration(node)
}
// macro import handling
let convertSymbol: string | undefined
let escapeSymbol: string | undefined
for (const { local, imported, source, specifier } of Object.values(
userImports
)) {
if (source === IMPORT_SOURCE) {
if (imported === ESCAPE_SYMBOL) {
escapeSymbol = local
} else if (imported === CONVERT_SYMBOL) {
convertSymbol = local
} else if (imported !== local) {
error(
`macro imports for ref-creating methods do not support aliasing.`,
specifier
)
}
}
}
// default symbol
if (!convertSymbol && !userImports[CONVERT_SYMBOL]) {
convertSymbol = CONVERT_SYMBOL
}
if (!escapeSymbol && !userImports[ESCAPE_SYMBOL]) {
escapeSymbol = ESCAPE_SYMBOL
}
const importedHelpers = new Set<string>()
const rootScope: Scope = {}
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
let escapeScope: CallExpression | undefined // inside $$()
const excludedIds = new WeakSet<Identifier>()
const parentStack: Node[] = []
const propsLocalToPublicMap: Record<string, string> = Object.create(null)
if (knownRefs) {
for (const key of knownRefs) {
rootScope[key] = {}
}
}
if (knownProps) {
for (const key in knownProps) {
const { local, isConst } = knownProps[key]
rootScope[local] = {
isProp: true,
isConst: !!isConst
}
propsLocalToPublicMap[local] = key
}
}
function walkImportDeclaration(node: ImportDeclaration) {
const source = node.source.value
if (source === IMPORT_SOURCE) {
s.remove(node.start! + offset, node.end! + offset)
}
for (const specifier of node.specifiers) {
const local = specifier.local.name
const imported =
(specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name) ||
'default'
userImports[local] = {
source,
local,
imported,
specifier
}
}
}
function isRefCreationCall(callee: string): string | false {
if (!convertSymbol || currentScope[convertSymbol] !== undefined) {
return false
}
if (callee === convertSymbol) {
return convertSymbol
}
if (callee[0] === '$' && shorthands.includes(callee.slice(1))) {
return callee
}
return false
}
function error(msg: string, node: Node): never {
const e = new Error(msg)
;(e as any).node = node
throw e
}
function helper(msg: string) {
importedHelpers.add(msg)
return `_${msg}`
}
function registerBinding(id: Identifier, binding?: Binding) {
excludedIds.add(id)
if (currentScope) {
currentScope[id.name] = binding ? binding : false
} else {
error(
'registerBinding called without active scope, something is wrong.',
id
)
}
}
const registerRefBinding = (id: Identifier, isConst = false) =>
registerBinding(id, { isConst })
let tempVarCount = 0
function genTempVar() {
return `__$temp_${++tempVarCount}`
}
function snip(node: Node) {
return s.original.slice(node.start! + offset, node.end! + offset)
}
function walkScope(node: Program | BlockStatement, isRoot = false) {
for (const stmt of node.body) {
if (stmt.type === 'VariableDeclaration') {
walkVariableDeclaration(stmt, isRoot)
} else if (
stmt.type === 'FunctionDeclaration' ||
stmt.type === 'ClassDeclaration'
) {
if (stmt.declare || !stmt.id) continue
registerBinding(stmt.id)
} else if (
(stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
stmt.left.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.left)
} else if (
stmt.type === 'ExportNamedDeclaration' &&
stmt.declaration &&
stmt.declaration.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.declaration, isRoot)
} else if (
stmt.type === 'LabeledStatement' &&
stmt.body.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.body, isRoot)
}
}
}
function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
if (stmt.declare) {
return
}
for (const decl of stmt.declarations) {
let refCall
const isCall =
decl.init &&
decl.init.type === 'CallExpression' &&
decl.init.callee.type === 'Identifier'
if (
isCall &&
(refCall = isRefCreationCall((decl as any).init.callee.name))
) {
processRefDeclaration(
refCall,
decl.id,
decl.init as CallExpression,
stmt.kind === 'const'
)
} else {
const isProps =
isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
for (const id of extractIdentifiers(decl.id)) {
if (isProps) {
// for defineProps destructure, only exclude them since they
// are already passed in as knownProps
excludedIds.add(id)
} else {
registerBinding(id)
}
}
}
}
}
function processRefDeclaration(
method: string,
id: VariableDeclarator['id'],
call: CallExpression,
isConst: boolean
) {
excludedIds.add(call.callee as Identifier)
if (method === convertSymbol) {
// $
// remove macro
s.remove(call.callee.start! + offset, call.callee.end! + offset)
if (id.type === 'Identifier') {
// single variable
registerRefBinding(id, isConst)
} else if (id.type === 'ObjectPattern') {
processRefObjectPattern(id, call, isConst)
} else if (id.type === 'ArrayPattern') {
processRefArrayPattern(id, call, isConst)
}
} else {
// shorthands
if (id.type === 'Identifier') {
registerRefBinding(id, isConst)
// replace call
s.overwrite(
call.start! + offset,
call.start! + method.length + offset,
helper(method.slice(1))
)
} else {
error(`${method}() cannot be used with destructure patterns.`, call)
}
}
}
function processRefObjectPattern(
pattern: ObjectPattern,
call: CallExpression,
isConst: boolean,
tempVar?: string,
path: PathSegment[] = []
) {
if (!tempVar) {
tempVar = genTempVar()
// const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
let nameId: Identifier | undefined
for (const p of pattern.properties) {
let key: Expression | string | undefined
let defaultValue: Expression | undefined
if (p.type === 'ObjectProperty') {
if (p.key.start! === p.value.start!) {
// shorthand { foo }
nameId = p.key as Identifier
if (p.value.type === 'Identifier') {
// avoid shorthand value identifier from being processed
excludedIds.add(p.value)
} else if (
p.value.type === 'AssignmentPattern' &&
p.value.left.type === 'Identifier'
) {
// { foo = 1 }
excludedIds.add(p.value.left)
defaultValue = p.value.right
}
} else {
key = p.computed ? (p.key as Expression) : (p.key as Identifier).name
if (p.value.type === 'Identifier') {
// { foo: bar }
nameId = p.value
} else if (p.value.type === 'ObjectPattern') {
processRefObjectPattern(p.value, call, isConst, tempVar, [
...path,
key
])
} else if (p.value.type === 'ArrayPattern') {
processRefArrayPattern(p.value, call, isConst, tempVar, [
...path,
key
])
} else if (p.value.type === 'AssignmentPattern') {
if (p.value.left.type === 'Identifier') {
// { foo: bar = 1 }
nameId = p.value.left
defaultValue = p.value.right
} else if (p.value.left.type === 'ObjectPattern') {
processRefObjectPattern(p.value.left, call, isConst, tempVar, [
...path,
[key, p.value.right]
])
} else if (p.value.left.type === 'ArrayPattern') {
processRefArrayPattern(p.value.left, call, isConst, tempVar, [
...path,
[key, p.value.right]
])
} else {
// MemberExpression case is not possible here, ignore
}
}
}
} else {
// rest element { ...foo }
error(`reactivity destructure does not support rest elements.`, p)
}
if (nameId) {
registerRefBinding(nameId, isConst)
// inject toRef() after original replaced pattern
const source = pathToString(tempVar, path)
const keyStr = isString(key)
? `'${key}'`
: key
? snip(key)
: `'${nameId.name}'`
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft(
call.end! + offset,
`,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${keyStr}${defaultStr})`
)
}
}
if (nameId) {
s.appendLeft(call.end! + offset, ';')
}
}
function processRefArrayPattern(
pattern: ArrayPattern,
call: CallExpression,
isConst: boolean,
tempVar?: string,
path: PathSegment[] = []
) {
if (!tempVar) {
// const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
tempVar = genTempVar()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
let nameId: Identifier | undefined
for (let i = 0; i < pattern.elements.length; i++) {
const e = pattern.elements[i]
if (!e) continue
let defaultValue: Expression | undefined
if (e.type === 'Identifier') {
// [a] --> [__a]
nameId = e
} else if (e.type === 'AssignmentPattern') {
// [a = 1]
nameId = e.left as Identifier
defaultValue = e.right
} else if (e.type === 'RestElement') {
// [...a]
error(`reactivity destructure does not support rest elements.`, e)
} else if (e.type === 'ObjectPattern') {
processRefObjectPattern(e, call, isConst, tempVar, [...path, i])
} else if (e.type === 'ArrayPattern') {
processRefArrayPattern(e, call, isConst, tempVar, [...path, i])
}
if (nameId) {
registerRefBinding(nameId, isConst)
// inject toRef() after original replaced pattern
const source = pathToString(tempVar, path)
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft(
call.end! + offset,
`,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${i}${defaultStr})`
)
}
}
if (nameId) {
s.appendLeft(call.end! + offset, ';')
}
}
type PathSegmentAtom = Expression | string | number
type PathSegment =
| PathSegmentAtom
| [PathSegmentAtom, Expression /* default value */]
function pathToString(source: string, path: PathSegment[]): string {
if (path.length) {
for (const seg of path) {
if (isArray(seg)) {
source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
} else {
source += segToString(seg)
}
}
}
return source
}
function segToString(seg: PathSegmentAtom): string {
if (typeof seg === 'number') {
return `[${seg}]`
} else if (typeof seg === 'string') {
return `.${seg}`
} else {
return snip(seg)
}
}
function rewriteId(
scope: Scope,
id: Identifier,
parent: Node,
parentStack: Node[]
): boolean {
if (hasOwn(scope, id.name)) {
const binding = scope[id.name]
if (binding) {
if (
binding.isConst &&
((parent.type === 'AssignmentExpression' && id === parent.left) ||
parent.type === 'UpdateExpression')
) {
error(`Assignment to constant variable.`, id)
}
const { isProp } = binding
if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
if (isProp) {
if (escapeScope) {
// prop binding in $$()
// { prop } -> { prop: __props_prop }
registerEscapedPropBinding(id)
s.appendLeft(
id.end! + offset,
`: __props_${propsLocalToPublicMap[id.name]}`
)
} else {
// { prop } -> { prop: __props.prop }
s.appendLeft(
id.end! + offset,
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
)
}
} else {
// { foo } -> { foo: foo.value }
s.appendLeft(id.end! + offset, `: ${id.name}.value`)
}
}
} else {
if (isProp) {
if (escapeScope) {
// x --> __props_x
registerEscapedPropBinding(id)
s.overwrite(
id.start! + offset,
id.end! + offset,
`__props_${propsLocalToPublicMap[id.name]}`
)
} else {
// x --> __props.x
s.overwrite(
id.start! + offset,
id.end! + offset,
genPropsAccessExp(propsLocalToPublicMap[id.name])
)
}
} else {
// x --> x.value
s.appendLeft(id.end! + offset, '.value')
}
}
}
return true
}
return false
}
const propBindingRefs: Record<string, true> = {}
function registerEscapedPropBinding(id: Identifier) {
if (!propBindingRefs.hasOwnProperty(id.name)) {
propBindingRefs[id.name] = true
const publicKey = propsLocalToPublicMap[id.name]
s.prependRight(
offset,
`const __props_${publicKey} = ${helper(
`toRef`
)}(__props, '${publicKey}');\n`
)
}
}
// check root scope first
walkScope(ast, true)
walk(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)
// function scopes
if (isFunctionType(node)) {
scopeStack.push((currentScope = {}))
walkFunctionParams(node, registerBinding)
if (node.body.type === 'BlockStatement') {
walkScope(node.body)
}
return
}
// catch param
if (node.type === 'CatchClause') {
scopeStack.push((currentScope = {}))
if (node.param && node.param.type === 'Identifier') {
registerBinding(node.param)
}
walkScope(node.body)
return
}
// non-function block scopes
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
scopeStack.push((currentScope = {}))
walkScope(node)
return
}
// skip type nodes
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
) {
return this.skip()
}
if (node.type === 'Identifier') {
const binding = rootScope[node.name]
if (
// if inside $$(), skip unless this is a destructured prop binding
!(escapeScope && (!binding || !binding.isProp)) &&
isReferencedIdentifier(node, parent!, parentStack) &&
!excludedIds.has(node)
) {
// walk up the scope chain to check if id should be appended .value
let i = scopeStack.length
while (i--) {
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
return
}
}
}
}
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
const callee = node.callee.name
const refCall = isRefCreationCall(callee)
if (refCall && (!parent || parent.type !== 'VariableDeclarator')) {
return error(
`${refCall} can only be used as the initializer of ` +
`a variable declaration.`,
node
)
}
if (
escapeSymbol &&
currentScope[escapeSymbol] === undefined &&
callee === escapeSymbol
) {
escapeScope = node
s.remove(node.callee.start! + offset, node.callee.end! + offset)
if (parent?.type === 'ExpressionStatement') {
// edge case where the call expression is an expression statement
// if its own - prepend semicolon to avoid it being parsed as
// function invocation of previous line
let i =
(node.leadingComments
? node.leadingComments[0].start
: node.start)! + offset
while (i--) {
const char = s.original.charAt(i)
if (char === '\n') {
// only insert semi if it's actually the first thing after
// newline
s.prependRight(node.start! + offset, ';')
break
} else if (!/\s/.test(char)) {
break
}
}
}
}
}
},
leave(node: Node, parent?: Node) {
parent && parentStack.pop()
if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
isFunctionType(node)
) {
scopeStack.pop()
currentScope = scopeStack[scopeStack.length - 1] || null
}
if (node === escapeScope) {
escapeScope = undefined
}
}
})
return {
rootRefs: Object.keys(rootScope).filter(key => {
const binding = rootScope[key]
return binding && !binding.isProp
}),
importedHelpers: [...importedHelpers]
}
}
const hasWarned: Record<string, boolean> = {}
function warnExperimental() {
// eslint-disable-next-line
if (typeof window !== 'undefined') {
return
}
warnOnce(
`Reactivity Transform was an experimental feature and has now been deprecated. ` +
`It will be removed from Vue core in 3.4. If you intend to continue using it, ` +
`switch to https://vue-macros.sxzz.moe/features/reactivity-transform.html.\n` +
`See reason for deprecation here: https://github.com/vuejs/rfcs/discussions/369#discussioncomment-5059028`
)
}
function warnOnce(msg: string) {
const isNodeProd =
typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
if (!isNodeProd && !__TEST__ && !hasWarned[msg]) {
hasWarned[msg] = true
warn(msg)
}
}
function warn(msg: string) {
console.warn(
`\x1b[1m\x1b[33m[@vue/reactivity-transform]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`
)
}

View File

@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
// mutate n
n.value++
// on the 2nd run, plusOne.value should have already updated.
expect(plusOneValues).toMatchObject([1, 2, 2])
expect(plusOneValues).toMatchObject([1, 2])
})
it('should warn if trying to set a readonly computed', () => {
@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
oldValue: 2
})
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
it('should query deps dirty sequentially', () => {
const cSpy = vi.fn()
const a = ref<null | { v: number }>({
v: 1
})
const b = computed(() => {
return a.value
})
const c = computed(() => {
cSpy()
return b.value?.v
})
const d = computed(() => {
if (b.value) {
return c.value
}
return 0
})
d.value
a.value!.v = 2
a.value = null
d.value
expect(cSpy).toHaveBeenCalledTimes(1)
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
it('chained computed dirty reallocation after querying dirty', () => {
let _msg: string | undefined
const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})
effect(() => {
_msg = msg.value
})
items.value = [1, 2, 3]
items.value = [1, 2, 3]
items.value = undefined
expect(_msg).toBe('The items are not loaded')
})
it('chained computed dirty reallocation after trigger computed getter', () => {
let _msg: string | undefined
const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})
_msg = msg.value
items.value = [1, 2, 3]
isLoaded.value // <- trigger computed getter
_msg = msg.value
items.value = undefined
_msg = msg.value
expect(_msg).toBe('The items are not loaded')
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
it('deps order should be consistent with the last time get value', () => {
const cSpy = vi.fn()
const a = ref(0)
const b = computed(() => {
return a.value % 3 !== 0
})
const c = computed(() => {
cSpy()
if (a.value % 3 === 2) {
return 'expensive'
}
return 'cheap'
})
const d = computed(() => {
return a.value % 3 === 2
})
const e = computed(() => {
if (b.value) {
if (d.value) {
return 'Avoiding expensive calculation'
}
}
return c.value
})
e.value
a.value++
e.value
expect(e.effect.deps.length).toBe(3)
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
e.value
expect(cSpy).toHaveBeenCalledTimes(2)
})
it('should trigger by the second computed that maybe dirty', () => {
const cSpy = vi.fn()
const src1 = ref(0)
const src2 = ref(0)
const c1 = computed(() => src1.value)
const c2 = computed(() => (src1.value % 2) + src2.value)
const c3 = computed(() => {
cSpy()
c1.value
c2.value
})
c3.value
src1.value = 2
c3.value
expect(cSpy).toHaveBeenCalledTimes(2)
src2.value = 1
c3.value
expect(cSpy).toHaveBeenCalledTimes(3)
})
it('should trigger the second effect', () => {
const fnSpy = vi.fn()
const v = ref(1)
const c = computed(() => v.value)
effect(() => {
c.value
})
effect(() => {
c.value
fnSpy()
})
expect(fnSpy).toBeCalledTimes(1)
v.value = 2
expect(fnSpy).toBeCalledTimes(2)
})
})

View File

@ -1,57 +1,32 @@
import { computed, deferredComputed, effect, ref } from '../src'
import { computed, effect, ref } from '../src'
describe('deferred computed', () => {
const tick = Promise.resolve()
test('should only trigger once on multiple mutations', async () => {
test('should not trigger if value did not change', () => {
const src = ref(0)
const c = deferredComputed(() => src.value)
const c = computed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
src.value = 1
src.value = 2
src.value = 3
// not called yet
expect(spy).toHaveBeenCalledTimes(1)
await tick
// should only trigger once
expect(spy).toHaveBeenCalledTimes(2)
expect(spy).toHaveBeenCalledWith(c.value)
})
test('should not trigger if value did not change', async () => {
const src = ref(0)
const c = deferredComputed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
src.value = 1
src.value = 2
await tick
// should not trigger
expect(spy).toHaveBeenCalledTimes(1)
src.value = 3
src.value = 4
src.value = 5
await tick
// should trigger because latest value changes
expect(spy).toHaveBeenCalledTimes(2)
})
test('chained computed trigger', async () => {
test('chained computed trigger', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@ -69,19 +44,18 @@ describe('deferred computed', () => {
expect(effectSpy).toHaveBeenCalledTimes(1)
src.value = 1
await tick
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c2Spy).toHaveBeenCalledTimes(2)
expect(effectSpy).toHaveBeenCalledTimes(2)
})
test('chained computed avoid re-compute', async () => {
test('chained computed avoid re-compute', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@ -98,26 +72,24 @@ describe('deferred computed', () => {
src.value = 2
src.value = 4
src.value = 6
await tick
// c1 should re-compute once.
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c1Spy).toHaveBeenCalledTimes(4)
// c2 should not have to re-compute because c1 did not change.
expect(c2Spy).toHaveBeenCalledTimes(1)
// effect should not trigger because c2 did not change.
expect(effectSpy).toHaveBeenCalledTimes(1)
})
test('chained computed value invalidation', async () => {
test('chained computed value invalidation', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
const c2 = deferredComputed(() => {
const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
@ -139,17 +111,17 @@ describe('deferred computed', () => {
expect(c2Spy).toHaveBeenCalledTimes(2)
})
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
const c2 = deferredComputed(() => {
const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
@ -162,14 +134,13 @@ describe('deferred computed', () => {
src.value = 1
// sync access c2
c2.value
await tick
expect(effectSpy).toHaveBeenCalledTimes(2)
})
test('should not compute if deactivated before scheduler is called', async () => {
test('should not compute if deactivated before scheduler is called', () => {
const c1Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@ -179,7 +150,6 @@ describe('deferred computed', () => {
c1.effect.stop()
// trigger
src.value++
await tick
expect(c1Spy).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,5 +1,4 @@
import {
ref,
reactive,
effect,
stop,
@ -12,7 +11,8 @@ import {
readonly,
ReactiveEffectRunner
} from '../src/index'
import { ITERATE_KEY } from '../src/effect'
import { pauseScheduling, resetScheduling } from '../src/effect'
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
@ -574,8 +574,8 @@ describe('reactivity/effect', () => {
expect(output.fx2).toBe(1 + 3 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1)
// Invoked twice due to change of fx1.
expect(fx2Spy).toHaveBeenCalledTimes(2)
// Invoked due to change of fx1.
expect(fx2Spy).toHaveBeenCalledTimes(1)
fx1Spy.mockClear()
fx2Spy.mockClear()
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(3)
})
// #5707
// when an effect completes its run, it should clear the tracking bits of
// its tracked deps. However, if the effect stops itself, the deps list is
// emptied so their bits are never cleared.
it('edge case: self-stopping effect tracking ref', () => {
const c = ref(true)
const runner = effect(() => {
// reference ref
if (!c.value) {
// stop itself while running
stop(runner)
}
})
// trigger run
c.value = !c.value
// should clear bits
expect((c as any).dep.w).toBe(0)
expect((c as any).dep.n).toBe(0)
})
it('events: onStop', () => {
const onStop = vi.fn()
const runner = effect(() => {}, {
@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
expect(has).toBe(false)
})
})
it('should be triggered once with pauseScheduling', () => {
const counter = reactive({ num: 0 })
const counterSpy = vi.fn(() => counter.num)
effect(counterSpy)
counterSpy.mockClear()
pauseScheduling()
counter.num++
counter.num++
resetScheduling()
expect(counterSpy).toHaveBeenCalledTimes(1)
})
describe('empty dep cleanup', () => {
it('should remove the dep when the effect is stopped', () => {
const obj = reactive({ prop: 1 })
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop')
expect(dep).toHaveLength(1)
obj.prop = 2
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
expect(dep).toHaveLength(1)
stop(runner)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
obj.prop = 3
runner()
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
})
it('should only remove the dep when the last effect is stopped', () => {
const obj = reactive({ prop: 1 })
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner1 = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop')
expect(dep).toHaveLength(1)
const runner2 = effect(() => obj.prop)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
expect(dep).toHaveLength(2)
obj.prop = 2
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
expect(dep).toHaveLength(2)
stop(runner1)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
expect(dep).toHaveLength(1)
obj.prop = 3
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
expect(dep).toHaveLength(1)
stop(runner2)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
obj.prop = 4
runner1()
runner2()
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
})
it('should remove the dep when it is no longer used by the effect', () => {
const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
a: 1,
b: 2,
c: 'a'
})
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
effect(() => obj[obj.c])
const depC = getDepFromReactive(toRaw(obj), 'c')
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
expect(depC).toHaveLength(1)
obj.c = 'b'
obj.a = 4
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
expect(depC).toHaveLength(1)
})
})
})

View File

@ -0,0 +1,81 @@
import {
ComputedRef,
computed,
effect,
reactive,
shallowRef as ref,
toRaw
} from '../src/index'
import { getDepFromReactive } from '../src/reactiveEffect'
describe.skipIf(!global.gc)('reactivity/gc', () => {
const gc = () => {
return new Promise<void>(resolve => {
setTimeout(() => {
global.gc!()
resolve()
})
})
}
// #9233
it('should release computed cache', async () => {
const src = ref<{} | undefined>({})
const srcRef = new WeakRef(src.value!)
let c: ComputedRef | undefined = computed(() => src.value)
c.value // cache src value
src.value = undefined // release value
c = undefined // release computed
await gc()
expect(srcRef.deref()).toBeUndefined()
})
it('should release reactive property dep', async () => {
const src = reactive({ foo: 1 })
let c: ComputedRef | undefined = computed(() => src.foo)
c.value
expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined()
c = undefined
await gc()
await gc()
expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined()
})
it('should not release effect for ref', async () => {
const spy = vi.fn()
const src = ref(0)
effect(() => {
spy()
src.value
})
expect(spy).toHaveBeenCalledTimes(1)
await gc()
src.value++
expect(spy).toHaveBeenCalledTimes(2)
})
it('should not release effect for reactive', async () => {
const spy = vi.fn()
const src = reactive({ foo: 1 })
effect(() => {
spy()
src.foo
})
expect(spy).toHaveBeenCalledTimes(1)
await gc()
src.foo++
expect(spy).toHaveBeenCalledTimes(2)
})
})

View File

@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
expect(fn).toHaveBeenCalledTimes(1)
})
test('shift on Array should trigger dependency once', () => {
const arr = reactive([1, 2, 3])
const fn = vi.fn()
effect(() => {
for (let i = 0; i < arr.length; i++) {
arr[i]
}
fn()
})
expect(fn).toHaveBeenCalledTimes(1)
arr.shift()
expect(fn).toHaveBeenCalledTimes(2)
})
//#6018
test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
const arr = ref([1])
const fn1 = vi.fn()
const fn2 = vi.fn()
effect(() => {
fn1()
if (arr.value.length > 0) {
arr.value.slice()
fn2()
}
})
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
arr.value.splice(0)
expect(fn1).toHaveBeenCalledTimes(2)
expect(fn2).toHaveBeenCalledTimes(1)
})
test('add existing index on Array should not trigger length dependency', () => {
const array = new Array(3)
const observed = reactive(array)

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.3.9",
"version": "3.4.0-alpha.3",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -2,7 +2,6 @@ import {
reactive,
readonly,
toRaw,
ReactiveFlags,
Target,
readonlyMap,
reactiveMap,
@ -11,14 +10,14 @@ import {
isReadonly,
isShallow
} from './reactive'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import {
track,
trigger,
ITERATE_KEY,
pauseTracking,
resetTracking
resetTracking,
pauseScheduling,
resetScheduling
} from './effect'
import { track, trigger, ITERATE_KEY } from './reactiveEffect'
import {
isObject,
hasOwn,
@ -71,7 +70,9 @@ function createArrayInstrumentations() {
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}

View File

@ -1,6 +1,11 @@
import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { toRaw, toReactive, toReadonly } from './reactive'
import {
track,
trigger,
ITERATE_KEY,
MAP_KEY_ITERATE_KEY
} from './reactiveEffect'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
export type CollectionTypes = IterableCollections | WeakCollections

View File

@ -1,8 +1,9 @@
import { DebuggerOptions, ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive'
import { hasChanged, isFunction, NOOP } from '@vue/shared'
import { toRaw } from './reactive'
import { Dep } from './dep'
import { DirtyLevels, ReactiveFlags } from './constants'
declare const ComputedRefSymbol: unique symbol
@ -15,8 +16,8 @@ export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T>
}
export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
export type ComputedGetter<T> = (oldValue?: T) => T
export type ComputedSetter<T> = (newValue: T) => void
export interface WritableComputedOptions<T> {
get: ComputedGetter<T>
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true
public _cacheable: boolean
constructor(
@ -41,12 +41,10 @@ export class ComputedRefImpl<T> {
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
@ -56,9 +54,10 @@ export class ComputedRefImpl<T> {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
if (!self._cacheable || self.effect.dirty) {
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
}
}
return self._value
}
@ -66,6 +65,16 @@ export class ComputedRefImpl<T> {
set value(newValue: T) {
this._setter(newValue)
}
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
get _dirty() {
return this.effect.dirty
}
set _dirty(v) {
this.effect.dirty = v
}
// #endregion
}
/**

View File

@ -0,0 +1,30 @@
// using literal strings instead of numbers so that it's easier to inspect
// debugger events
export enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
export enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
export enum DirtyLevels {
NotDirty = 0,
ComputedValueMaybeDirty = 1,
ComputedValueDirty = 2,
Dirty = 3
}

View File

@ -1,88 +1,6 @@
import { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { ComputedGetter, ComputedRef } from './computed'
import { ReactiveFlags, toRaw } from './reactive'
import { trackRefValue, triggerRefValue } from './ref'
import { computed } from './computed'
const tick = /*#__PURE__*/ Promise.resolve()
const queue: any[] = []
let queued = false
const scheduler = (fn: any) => {
queue.push(fn)
if (!queued) {
queued = true
tick.then(flush)
}
}
const flush = () => {
for (let i = 0; i < queue.length; i++) {
queue[i]()
}
queue.length = 0
queued = false
}
class DeferredComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY] = true
constructor(getter: ComputedGetter<T>) {
let compareTarget: any
let hasCompareTarget = false
let scheduled = false
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
if (this.dep) {
if (computedTrigger) {
compareTarget = this._value
hasCompareTarget = true
} else if (!scheduled) {
const valueToCompare = hasCompareTarget ? compareTarget : this._value
scheduled = true
hasCompareTarget = false
scheduler(() => {
if (this.effect.active && this._get() !== valueToCompare) {
triggerRefValue(this)
}
scheduled = false
})
}
// chained upstream computeds are notified synchronously to ensure
// value invalidation in case of sync access; normal effects are
// deferred to be triggered in scheduler.
for (const e of this.dep) {
if (e.computed instanceof DeferredComputedRefImpl) {
e.scheduler!(true /* computedTrigger */)
}
}
}
this._dirty = true
})
this.effect.computed = this as any
}
private _get() {
if (this._dirty) {
this._dirty = false
return (this._value = this.effect.run()!)
}
return this._value
}
get value() {
trackRefValue(this)
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
return toRaw(this)._get()
}
}
export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
return new DeferredComputedRefImpl(getter) as any
}
/**
* @deprecated use `computed` instead. See #5912
*/
export const deferredComputed = computed

View File

@ -1,57 +1,17 @@
import { ReactiveEffect, trackOpBit } from './effect'
import type { ReactiveEffect } from './effect'
import type { ComputedRefImpl } from './computed'
export type Dep = Set<ReactiveEffect> & TrackedMarkers
/**
* wasTracked and newTracked maintain the status for several levels of effect
* tracking recursion. One bit per level is used to define whether the dependency
* was/is tracked.
*/
type TrackedMarkers = {
/**
* wasTracked
*/
w: number
/**
* newTracked
*/
n: number
export type Dep = Map<ReactiveEffect, number> & {
cleanup: () => void
computed?: ComputedRefImpl<any>
}
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
export const createDep = (
cleanup: () => void,
computed?: ComputedRefImpl<any>
): Dep => {
const dep = new Map() as Dep
dep.cleanup = cleanup
dep.computed = computed
return dep
}
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // set was tracked
}
}
}
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}

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