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 DOMGlobals = ['window', 'document']
const NodeGlobals = ['module', 'require'] 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 = { module.exports = {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
@ -16,6 +25,7 @@ module.exports = {
'no-restricted-syntax': [ 'no-restricted-syntax': [
'error', 'error',
banConstEnum,
// since we target ES2015 for baseline support, we need to forbid object // since we target ES2015 for baseline support, we need to forbid object
// rest spread usage in destructure as it compiles into a verbose helper. // rest spread usage in destructure as it compiles into a verbose helper.
'ObjectPattern > RestElement', 'ObjectPattern > RestElement',
@ -52,12 +62,10 @@ module.exports = {
}, },
// Packages targeting Node // Packages targeting Node
{ {
files: [ files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
'packages/{compiler-sfc,compiler-ssr,server-renderer,reactivity-transform}/**'
],
rules: { rules: {
'no-restricted-globals': ['error', ...DOMGlobals], 'no-restricted-globals': ['error', ...DOMGlobals],
'no-restricted-syntax': 'off' 'no-restricted-syntax': ['error', banConstEnum]
} }
}, },
// Private package, browser only + no syntax restrictions // Private package, browser only + no syntax restrictions
@ -65,7 +73,7 @@ module.exports = {
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'], files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
rules: { rules: {
'no-restricted-globals': ['error', ...NodeGlobals], 'no-restricted-globals': ['error', ...NodeGlobals],
'no-restricted-syntax': 'off' 'no-restricted-syntax': ['error', banConstEnum]
} }
}, },
// JavaScript files // JavaScript files
@ -81,7 +89,7 @@ module.exports = {
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'], files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
rules: { rules: {
'no-restricted-globals': 'off', '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) ## [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) ## [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, "private": true,
"version": "0.0.0-vapor", "version": "0.0.0-vapor",
"packageManager": "pnpm@8.10.5", "packageManager": "pnpm@8.11.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js vue vue-vapor", "dev": "node scripts/dev.js vue vue-vapor",
@ -35,7 +35,7 @@
"serve": "serve", "serve": "serve",
"open": "open http://localhost:3000/packages/template-explorer/local.html", "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-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-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-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build", "build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
@ -59,8 +59,8 @@
"node": ">=18.12.0" "node": ">=18.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.23.3", "@babel/parser": "^7.23.4",
"@babel/types": "^7.23.3", "@babel/types": "^7.23.4",
"@rollup/plugin-alias": "^5.0.1", "@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.1", "@rollup/plugin-json": "^6.0.1",
@ -68,8 +68,10 @@
"@rollup/plugin-replace": "^5.0.4", "@rollup/plugin-replace": "^5.0.4",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/node": "^20.9.2", "@types/minimist": "^1.2.5",
"@typescript-eslint/parser": "^6.11.0", "@types/node": "^20.10.0",
"@types/semver": "^7.5.5",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-istanbul": "^0.34.6", "@vitest/coverage-istanbul": "^0.34.6",
"@vue/consolidate": "0.17.3", "@vue/consolidate": "0.17.3",
"conventional-changelog-cli": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
@ -77,6 +79,7 @@
"esbuild": "^0.19.5", "esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.54.0", "eslint": "^8.54.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-jest": "^27.6.0", "eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"execa": "^8.0.1", "execa": "^8.0.1",
@ -104,7 +107,7 @@
"terser": "^5.22.0", "terser": "^5.22.0",
"todomvc-app-css": "^2.4.3", "todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"tsx": "^4.1.4", "tsx": "^4.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.0", "vite": "^5.0.0",
"vitest": "^0.34.6" "vitest": "^0.34.6"

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1195,25 +1195,13 @@ describe('compiler: element transform', () => {
}) })
}) })
// TODO remove in 3.4 test('is casting', () => {
test('v-is', () => { const { node, root } = parseWithBind(`<div is="vue:foo" />`)
const { node, root } = parseWithBind(`<div v-is="'foo'" />`) expect(root.helpers).toContain(RESOLVE_COMPONENT)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({ expect(node).toMatchObject({
tag: { type: NodeTypes.VNODE_CALL,
callee: RESOLVE_DYNAMIC_COMPONENT, tag: '_component_foo'
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `'foo'`,
isStatic: false
}
]
},
// should skip v-is runtime check
directives: undefined
}) })
expect('v-is="component-name" has been deprecated').toHaveBeenWarned()
}) })
// #3934 // #3934

View File

@ -128,51 +128,24 @@ describe('compiler: expression transform', () => {
{ {
content: `_ctx.foo`, content: `_ctx.foo`,
loc: { loc: {
source: `foo`, start: { offset: 3, line: 1, column: 4 },
start: { end: { offset: 6, line: 1, column: 7 }
offset: 3,
line: 1,
column: 4
},
end: {
offset: 6,
line: 1,
column: 7
}
} }
}, },
`(`, `(`,
{ {
content: `_ctx.baz`, content: `_ctx.baz`,
loc: { loc: {
source: `baz`, start: { offset: 7, line: 1, column: 8 },
start: { end: { offset: 10, line: 1, column: 11 }
offset: 7,
line: 1,
column: 8
},
end: {
offset: 10,
line: 1,
column: 11
}
} }
}, },
` + 1, { key: `, ` + 1, { key: `,
{ {
content: `_ctx.kuz`, content: `_ctx.kuz`,
loc: { loc: {
source: `kuz`, start: { offset: 23, line: 1, column: 24 },
start: { end: { offset: 26, line: 1, column: 27 }
offset: 23,
line: 1,
column: 24
},
end: {
offset: 26,
line: 1,
column: 27
}
} }
}, },
` })` ` })`

View File

@ -376,7 +376,6 @@ describe('compiler: transform <slot> outlets', () => {
expect(onError.mock.calls[0][0]).toMatchObject({ expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
loc: { loc: {
source: `v-foo`,
start: { start: {
offset: index, offset: index,
line: 1, 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', () => { test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`) const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression 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 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 const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(onError.mock.calls[0][0]).toMatchObject({ expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_BIND_NO_EXPRESSION, code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
@ -111,7 +149,7 @@ describe('compiler: transform v-bind', () => {
}, },
end: { end: {
line: 1, 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', () => { test('.camel modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`) const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression 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', () => { test('.prop modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`) const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression 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', () => { test('.attr modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`) const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression 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 { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'

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 { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'

View File

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

View File

@ -1,7 +1,6 @@
import { TransformContext } from '../src' import { TransformContext } from '../src'
import { Position } from '../src/ast' import { Position } from '../src/ast'
import { import {
getInnerRange,
advancePositionWithClone, advancePositionWithClone,
isMemberExpressionNode, isMemberExpressionNode,
isMemberExpressionBrowser, 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', () => { describe('isMemberExpression', () => {
function commonAssertions(fn: (str: string) => boolean) { function commonAssertions(fn: (str: string) => boolean) {
// should work // should work

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { SourceLocation } from '../ast' import { SourceLocation } from '../ast'
import { CompilerError } from '../errors' import { CompilerError } from '../errors'
import { ParserContext } from '../parse' import { MergedParserOptions } from '../parser'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
export type CompilerCompatConfig = Partial< export type CompilerCompatConfig = Partial<
@ -13,10 +13,9 @@ export interface CompilerCompatOptions {
compatConfig?: CompilerCompatConfig compatConfig?: CompilerCompatConfig
} }
export const enum CompilerDeprecationTypes { export enum CompilerDeprecationTypes {
COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT', COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC', 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_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE', COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE', 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` 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]: { [CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: {
message: message:
`v-bind="obj" usage is now order sensitive and behaves like JavaScript ` + `v-bind="obj" usage is now order sensitive and behaves like JavaScript ` +
@ -100,12 +93,9 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
function getCompatValue( function getCompatValue(
key: CompilerDeprecationTypes | 'MODE', key: CompilerDeprecationTypes | 'MODE',
context: ParserContext | TransformContext { compatConfig }: MergedParserOptions | TransformContext
) { ) {
const config = (context as ParserContext).options const value = compatConfig && compatConfig[key]
? (context as ParserContext).options.compatConfig
: (context as TransformContext).compatConfig
const value = config && config[key]
if (key === 'MODE') { if (key === 'MODE') {
return value || 3 // compiler defaults to v3 behavior return value || 3 // compiler defaults to v3 behavior
} else { } else {
@ -115,7 +105,7 @@ function getCompatValue(
export function isCompatEnabled( export function isCompatEnabled(
key: CompilerDeprecationTypes, key: CompilerDeprecationTypes,
context: ParserContext | TransformContext context: MergedParserOptions | TransformContext
) { ) {
const mode = getCompatValue('MODE', context) const mode = getCompatValue('MODE', context)
const value = getCompatValue(key, context) const value = getCompatValue(key, context)
@ -126,7 +116,7 @@ export function isCompatEnabled(
export function checkCompatEnabled( export function checkCompatEnabled(
key: CompilerDeprecationTypes, key: CompilerDeprecationTypes,
context: ParserContext | TransformContext, context: MergedParserOptions | TransformContext,
loc: SourceLocation | null, loc: SourceLocation | null,
...args: any[] ...args: any[]
): boolean { ): boolean {
@ -139,7 +129,7 @@ export function checkCompatEnabled(
export function warnDeprecation( export function warnDeprecation(
key: CompilerDeprecationTypes, key: CompilerDeprecationTypes,
context: ParserContext | TransformContext, context: MergedParserOptions | TransformContext,
loc: SourceLocation | null, loc: SourceLocation | null,
...args: any[] ...args: any[]
) { ) {

View File

@ -1,5 +1,5 @@
import { CompilerOptions } from './options' import { CompilerOptions } from './options'
import { baseParse } from './parse' import { baseParse } from './parser'
import { transform, NodeTransform, DirectiveTransform } from './transform' import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen' import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast' import { RootNode } from './ast'
@ -59,7 +59,7 @@ export function getBaseTransformPreset(
// we name it `baseCompile` so that higher order compilers like // we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else. // @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile( export function baseCompile(
template: string | RootNode, source: string | RootNode,
options: CompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
const onError = options.onError || defaultOnError const onError = options.onError || defaultOnError
@ -82,7 +82,7 @@ export function baseCompile(
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED)) 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] = const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers) getBaseTransformPreset(prefixIdentifiers)

View File

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

View File

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

View File

@ -1,5 +1,10 @@
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast' import {
import { TextModes } from './parse' ElementNode,
Namespace,
TemplateChildNode,
ParentNode,
Namespaces
} from './ast'
import { CompilerError } from './errors' import { CompilerError } from './errors'
import { import {
NodeTransform, NodeTransform,
@ -17,6 +22,24 @@ export interface ErrorHandlingOptions {
export interface ParserOptions export interface ParserOptions
extends ErrorHandlingOptions, extends ErrorHandlingOptions,
CompilerCompatOptions { 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 * e.g. platform native elements, e.g. `<div>` for browsers
*/ */
@ -40,14 +63,11 @@ export interface ParserOptions
/** /**
* Get tag namespace * Get tag namespace
*/ */
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace getNamespace?: (
/** tag: string,
* Get text parsing mode for this element parent: ElementNode | undefined,
*/ rootNamespace: Namespace
getTextMode?: ( ) => Namespace
node: ElementNode,
parent: ElementNode | undefined
) => TextModes
/** /**
* @default ['{{', '}}'] * @default ['{{', '}}']
*/ */
@ -57,7 +77,8 @@ export interface ParserOptions
*/ */
whitespace?: 'preserve' | 'condense' 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 decodeEntities?: (rawText: string, asAttr: boolean) => string
/** /**
@ -73,7 +94,7 @@ export type HoistTransform = (
parent: ParentNode parent: ParentNode
) => void ) => void
export const enum BindingTypes { export enum BindingTypes {
/** /**
* returned from data() * 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.hoists = context.hoists
root.temps = context.temps root.temps = context.temps
root.cached = context.cached root.cached = context.cached
root.transformed = true
if (__COMPAT__) { if (__COMPAT__) {
root.filters = [...context.filters!] root.filters = [...context.filters!]

View File

@ -49,12 +49,10 @@ import {
GUARD_REACTIVE_PROPS GUARD_REACTIVE_PROPS
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { import {
getInnerRange,
toValidAssetId, toValidAssetId,
findProp, findProp,
isCoreComponent, isCoreComponent,
isStaticArgOf, isStaticArgOf,
findDir,
isStaticExp isStaticExp
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' 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...) // 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) { if (builtIn) {
@ -489,7 +474,7 @@ export function buildProps(
// static attribute // static attribute
const prop = props[i] const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) { if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop const { loc, name, nameLoc, value } = prop
let isStatic = true let isStatic = true
if (name === 'ref') { if (name === 'ref') {
hasRef = true hasRef = true
@ -536,11 +521,7 @@ export function buildProps(
} }
properties.push( properties.push(
createObjectProperty( createObjectProperty(
createSimpleExpression( createSimpleExpression(name, true, nameLoc),
name,
true,
getInnerRange(loc, 0, name.length)
),
createSimpleExpression( createSimpleExpression(
value ? value.content : '', value ? value.content : '',
isStatic, isStatic,

View File

@ -336,9 +336,9 @@ export function processExpression(
id.name, id.name,
false, false,
{ {
source,
start: advancePositionWithClone(node.loc.start, source, start), 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 id.isConstant ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,13 @@ import {
NodeTypes, NodeTypes,
ElementNode, ElementNode,
TextNode, TextNode,
ErrorCodes,
ElementTypes, ElementTypes,
InterpolationNode, InterpolationNode,
AttributeNode, AttributeNode,
ConstantTypes ConstantTypes,
Namespaces
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parserOptions, DOMNamespaces } from '../src/parserOptions' import { parserOptions } from '../src/parserOptions'
describe('DOM parser', () => { describe('DOM parser', () => {
describe('Text', () => { 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 ast = parse('<textarea>&amp;</textarea>', parserOptions)
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode const text = element.children[0] as TextNode
@ -277,11 +277,10 @@ describe('DOM parser', () => {
expect(element).toStrictEqual({ expect(element).toStrictEqual({
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
ns: DOMNamespaces.HTML, ns: Namespaces.HTML,
tag: 'img', tag: 'img',
tagType: ElementTypes.ELEMENT, tagType: ElementTypes.ELEMENT,
props: [], props: [],
isSelfClosing: false,
children: [], children: [],
loc: { loc: {
start: { offset: 0, line: 1, column: 1 }, start: { offset: 0, line: 1, column: 1 },
@ -316,15 +315,8 @@ describe('DOM parser', () => {
test('Strict end tag detection for textarea.', () => { test('Strict end tag detection for textarea.', () => {
const ast = parse( const ast = parse(
'<textarea>hello</textarea</textarea0></texTArea a="<>">', '<textarea>hello</textarea</textarea0></texTArea>',
{ parserOptions
...parserOptions,
onError: err => {
if (err.code !== ErrorCodes.END_TAG_WITH_ATTRIBUTES) {
throw err
}
}
}
) )
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode const text = element.children[0] as TextNode
@ -347,21 +339,21 @@ describe('DOM parser', () => {
const ast = parse('<html>test</html>', parserOptions) const ast = parse('<html>test</html>', parserOptions)
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('SVG namespace', () => { test('SVG namespace', () => {
const ast = parse('<svg>test</svg>', parserOptions) const ast = parse('<svg>test</svg>', parserOptions)
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.SVG) expect(element.ns).toBe(Namespaces.SVG)
}) })
test('MATH_ML namespace', () => { test('MATH_ML namespace', () => {
const ast = parse('<math>test</math>', parserOptions) const ast = parse('<math>test</math>', parserOptions)
const element = ast.children[0] as ElementNode 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', () => { test('SVG in MATH_ML namespace', () => {
@ -373,8 +365,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode const elementAnnotation = elementMath.children[0] as ElementNode
const elementSvg = elementAnnotation.children[0] as ElementNode const elementSvg = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML) expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(elementSvg.ns).toBe(DOMNamespaces.SVG) expect(elementSvg.ns).toBe(Namespaces.SVG)
}) })
test('html text/html in MATH_ML namespace', () => { test('html text/html in MATH_ML namespace', () => {
@ -387,8 +379,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML) expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('html application/xhtml+xml in MATH_ML namespace', () => { test('html application/xhtml+xml in MATH_ML namespace', () => {
@ -400,8 +392,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML) expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('mtext malignmark in MATH_ML namespace', () => { test('mtext malignmark in MATH_ML namespace', () => {
@ -413,8 +405,8 @@ describe('DOM parser', () => {
const elementText = elementMath.children[0] as ElementNode const elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML) expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML) expect(element.ns).toBe(Namespaces.MATH_ML)
}) })
test('mtext and not malignmark tag in MATH_ML namespace', () => { 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 elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML) expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('foreignObject tag in SVG namespace', () => { test('foreignObject tag in SVG namespace', () => {
@ -436,8 +428,8 @@ describe('DOM parser', () => {
const elementForeignObject = elementSvg.children[0] as ElementNode const elementForeignObject = elementSvg.children[0] as ElementNode
const element = elementForeignObject.children[0] as ElementNode const element = elementForeignObject.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG) expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('desc tag in SVG namespace', () => { test('desc tag in SVG namespace', () => {
@ -446,8 +438,8 @@ describe('DOM parser', () => {
const elementDesc = elementSvg.children[0] as ElementNode const elementDesc = elementSvg.children[0] as ElementNode
const element = elementDesc.children[0] as ElementNode const element = elementDesc.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG) expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('title tag in SVG namespace', () => { test('title tag in SVG namespace', () => {
@ -456,8 +448,8 @@ describe('DOM parser', () => {
const elementTitle = elementSvg.children[0] as ElementNode const elementTitle = elementSvg.children[0] as ElementNode
const element = elementTitle.children[0] as ElementNode const element = elementTitle.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG) expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML) expect(element.ns).toBe(Namespaces.HTML)
}) })
test('SVG in HTML namespace', () => { test('SVG in HTML namespace', () => {
@ -465,8 +457,8 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML) expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.SVG) expect(element.ns).toBe(Namespaces.SVG)
}) })
test('MATH in HTML namespace', () => { test('MATH in HTML namespace', () => {
@ -474,8 +466,35 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML) expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML) 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", "name": "@vue/compiler-dom",
"version": "3.3.9", "version": "3.4.0-alpha.3",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js", "module": "dist/compiler-dom.esm-bundler.js",

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 ) as DOMCompilerError
} }
export const enum DOMErrorCodes { export enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */, X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN, X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION, X_V_TEXT_NO_EXPRESSION,
@ -36,7 +36,7 @@ export const enum DOMErrorCodes {
} }
if (__TEST__) { 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 // file, so we have to manually keep them in sync. this check ensures it
// errors out if there are collisions. // errors out if there are collisions.
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) { 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( export function compile(
template: string, src: string | RootNode,
options: CompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
return baseCompile( return baseCompile(
template, src,
extend({}, parserOptions, options, { extend({}, parserOptions, options, {
nodeTransforms: [ nodeTransforms: [
// ignore <script> and <tag> // ignore <script> and <tag>
@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
export * from './runtimeHelpers' export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle' export { transformStyle } from './transforms/transformStyle'
export { createDOMCompilerError, DOMErrorCodes } from './errors' export {
createDOMCompilerError,
DOMErrorCodes,
DOMErrorMessages
} from './errors'
export * from '@vue/compiler-core' export * from '@vue/compiler-core'

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,30 @@
import { import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
TextModes, import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
ParserOptions,
ElementNode,
NodeTypes,
isBuiltInType
} from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers' import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtml } from './decodeHtml'
import { decodeHtmlBrowser } from './decodeHtmlBrowser' 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 = { export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag, isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : decodeHtml, decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
isBuiltInComponent: (tag: string): symbol | undefined => { isBuiltInComponent: tag => {
if (isBuiltInType(tag, `Transition`)) { if (tag === 'Transition' || tag === 'transition') {
return TRANSITION return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) { } else if (tag === 'TransitionGroup' || tag === 'transition-group') {
return TRANSITION_GROUP return TRANSITION_GROUP
} }
}, },
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces { getNamespace(tag, parent, rootNamespace) {
let ns = parent ? parent.ns : DOMNamespaces.HTML let ns = parent ? parent.ns : rootNamespace
if (parent && ns === Namespaces.MATH_ML) {
if (parent && ns === DOMNamespaces.MATH_ML) {
if (parent.tag === 'annotation-xml') { if (parent.tag === 'annotation-xml') {
if (tag === 'svg') { if (tag === 'svg') {
return DOMNamespaces.SVG return Namespaces.SVG
} }
if ( if (
parent.props.some( parent.props.some(
@ -54,46 +36,33 @@ export const parserOptions: ParserOptions = {
a.value.content === 'application/xhtml+xml') a.value.content === 'application/xhtml+xml')
) )
) { ) {
ns = DOMNamespaces.HTML ns = Namespaces.HTML
} }
} else if ( } else if (
/^m(?:[ions]|text)$/.test(parent.tag) && /^m(?:[ions]|text)$/.test(parent.tag) &&
tag !== 'mglyph' && tag !== 'mglyph' &&
tag !== 'malignmark' tag !== 'malignmark'
) { ) {
ns = DOMNamespaces.HTML ns = Namespaces.HTML
} }
} else if (parent && ns === DOMNamespaces.SVG) { } else if (parent && ns === Namespaces.SVG) {
if ( if (
parent.tag === 'foreignObject' || parent.tag === 'foreignObject' ||
parent.tag === 'desc' || parent.tag === 'desc' ||
parent.tag === 'title' parent.tag === 'title'
) { ) {
ns = DOMNamespaces.HTML ns = Namespaces.HTML
} }
} }
if (ns === DOMNamespaces.HTML) { if (ns === Namespaces.HTML) {
if (tag === 'svg') { if (tag === 'svg') {
return DOMNamespaces.SVG return Namespaces.SVG
} }
if (tag === 'math') { if (tag === 'math') {
return DOMNamespaces.MATH_ML return Namespaces.MATH_ML
} }
} }
return ns 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({ node.props.push({
type: NodeTypes.ATTRIBUTE, type: NodeTypes.ATTRIBUTE,
name: 'persisted', name: 'persisted',
nameLoc: node.loc,
value: undefined, value: undefined,
loc: node.loc loc: node.loc
}) })

View File

@ -15,7 +15,8 @@ import {
PlainElementNode, PlainElementNode,
JSChildNode, JSChildNode,
TextCallNode, TextCallNode,
ConstantTypes ConstantTypes,
Namespaces
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { import {
isVoidTag, isVoidTag,
@ -31,9 +32,8 @@ import {
isKnownSvgAttr, isKnownSvgAttr,
isBooleanAttr isBooleanAttr
} from '@vue/shared' } from '@vue/shared'
import { DOMNamespaces } from '../parserOptions'
export const enum StringifyThresholds { export enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5, ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20 NODE_COUNT = 20
} }
@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
node.codegenNode.hoisted node.codegenNode.hoisted
const dataAriaRE = /^(data|aria)-/ const dataAriaRE = /^(data|aria)-/
const isStringifiableAttr = (name: string, ns: DOMNamespaces) => { const isStringifiableAttr = (name: string, ns: Namespaces) => {
return ( return (
(ns === DOMNamespaces.HTML (ns === Namespaces.HTML
? isKnownHtmlAttr(name) ? isKnownHtmlAttr(name)
: ns === DOMNamespaces.SVG : ns === Namespaces.SVG
? isKnownSvgAttr(name) ? isKnownSvgAttr(name)
: false) || dataAriaRE.test(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`] = ` 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({ const __default__ = defineComponent({
name: 'Baz' name: 'Baz'
}) })
@ -30,7 +31,8 @@ return { a, defineComponent }
`; `;
exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (object) 1`] = ` exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (object) 1`] = `
"const __default__ = { "
const __default__ = {
name: 'Baz' 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`] = ` exports[`SFC compile <script setup> > <script> and <script setup> co-usage > export call expression as default 1`] = `
"function fn() { "
function fn() {
return \\"hello, world\\"; return \\"hello, world\\";
} }
const __default__ = fn(); 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`] = ` exports[`SFC compile <script setup> > <script> and <script setup> co-usage > keep original semi style 1`] = `
"export default { "
export default {
props: ['item'], props: ['item'],
emits: ['change'], emits: ['change'],
setup(__props, { expose: __expose, emit: __emit }) { setup(__props, { expose: __expose, emit: __emit }) {
@ -515,27 +519,28 @@ return { }
`; `;
exports[`SFC compile <script setup> > async/await detection > ref 1`] = ` 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 { export default {
async setup(__props, { expose: __expose }) { async setup(__props, { expose: __expose }) {
__expose(); __expose();
let __temp, __restore let __temp, __restore
let a = _ref(1 + (( let a = ref(1 + ((
([__temp,__restore] = _withAsyncContext(() => foo)), ([__temp,__restore] = _withAsyncContext(() => foo)),
__temp = await __temp, __temp = await __temp,
__restore(), __restore(),
__temp __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`] = ` exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 1`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
async function foo() { await bar } 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`] = ` exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 2`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const foo = async () => { await bar } 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`] = ` exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 3`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const obj = { async method() { await bar }} 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`] = ` exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 4`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
const cls = class Foo { async method() { await bar }} const cls = class Foo { async method() { await bar }}
@ -618,7 +626,8 @@ return { a }
`; `;
exports[`SFC compile <script setup> > binding analysis for destructure 1`] = ` exports[`SFC compile <script setup> > binding analysis for destructure 1`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
@ -818,23 +827,29 @@ return { bar }
`; `;
exports[`SFC compile <script setup> > imports > dedupe between user & helper 1`] = ` exports[`SFC compile <script setup> > imports > dedupe between user & helper 1`] = `
"import { ref as _ref } from 'vue' "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { ref } from 'vue' import { useCssVars, ref } from 'vue'
export default { export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
let foo = _ref(1) _useCssVars(_ctx => ({
\\"xxxxxxxx-msg\\": (msg.value)
}))
return { foo, ref } const msg = ref()
return { msg, useCssVars, ref }
} }
}" }"
`; `;
exports[`SFC compile <script setup> > imports > import dedupe between <script> and <script setup> 1`] = ` exports[`SFC compile <script setup> > imports > import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x' "
import { x } from './x'
export default { export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -898,7 +913,9 @@ return { ref }
`; `;
exports[`SFC compile <script setup> > imports > should support module string names syntax 1`] = ` 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 { export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -1185,7 +1202,8 @@ return (_ctx, _cache) => {
`; `;
exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = ` exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
"export default { "
export default {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
const count = ref(0) const count = ref(0)
@ -1348,7 +1366,8 @@ return { a }
`; `;
exports[`SFC genDefaultAs > <script> + <script setup> 1`] = ` exports[`SFC genDefaultAs > <script> + <script setup> 1`] = `
"const __default__ = {} "
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, { const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -1363,7 +1382,8 @@ return { a }
`; `;
exports[`SFC genDefaultAs > <script> + <script setup> 2`] = ` exports[`SFC genDefaultAs > <script> + <script setup> 2`] = `
"const __default__ = {} "
const __default__ = {}
const _sfc_ = /*#__PURE__*/Object.assign(__default__, { const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose: __expose }) { 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`] = ` exports[`template errors 1`] = `
[ [
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)], [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.], [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( const { content } = compile(
` `
<script setup> <script setup>
import { ref } from 'vue' import { useCssVars, ref } from 'vue'
let foo = $ref(1) const msg = ref()
</script> </script>
`,
{ reactivityTransform: true } <style>
.foo {
color: v-bind(msg)
}
</style>
`
) )
assertCode(content) 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>', () => { test('import dedupe between <script> and <script setup>', () => {
@ -891,9 +899,7 @@ describe('SFC compile <script setup>', () => {
describe('async/await detection', () => { describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) { function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`, { const { content } = compile(`<script setup>${code}</script>`)
reactivityTransform: true
})
if (shouldAsync) { if (shouldAsync) {
expect(content).toMatch(`let __temp, __restore`) expect(content).toMatch(`let __temp, __restore`)
} }
@ -911,7 +917,7 @@ describe('SFC compile <script setup>', () => {
}) })
test('ref', () => { test('ref', () => {
assertAwaitDetection(`let a = $ref(1 + (await foo))`) assertAwaitDetection(`let a = ref(1 + (await foo))`)
}) })
// #4448 // #4448

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sfc hoist static > should enable when only script setup 1`] = ` exports[`sfc hoist static > should enable when only script setup 1`] = `
"const foo = 'bar' "
const foo = 'bar'
export default { export default {
setup(__props) { setup(__props) {
@ -91,7 +92,8 @@ return () => {}
`; `;
exports[`sfc hoist static > should not hoist a function or class 1`] = ` exports[`sfc hoist static > should not hoist a function or class 1`] = `
"export default { "
export default {
setup(__props) { setup(__props) {
const fn = () => {} const fn = () => {}
@ -105,7 +107,8 @@ return () => {}
`; `;
exports[`sfc hoist static > should not hoist a object or array 1`] = ` exports[`sfc hoist static > should not hoist a object or array 1`] = `
"export default { "
export default {
setup(__props) { setup(__props) {
const obj = { foo: 'bar' } const obj = { foo: 'bar' }
@ -118,7 +121,8 @@ return () => {}
`; `;
exports[`sfc hoist static > should not hoist a variable 1`] = ` exports[`sfc hoist static > should not hoist a variable 1`] = `
"export default { "
export default {
setup(__props) { setup(__props) {
let KEY1 = 'default value' let KEY1 = 'default value'
@ -133,7 +137,8 @@ return () => {}
`; `;
exports[`sfc hoist static > should not hoist when disabled 1`] = ` exports[`sfc hoist static > should not hoist when disabled 1`] = `
"export default { "
export default {
setup(__props) { setup(__props) {
const foo = 'bar' 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 { import {
compileTemplate, compileTemplate,
SFCTemplateCompileOptions SFCTemplateCompileOptions
@ -107,24 +108,193 @@ test('source map', () => {
const template = parse( const template = parse(
` `
<template> <template>
<div><p>{{ render }}</p></div> <div><p>{{ foobar }}</p></div>
</template> </template>
`, `,
{ filename: 'example.vue', sourceMap: true } { filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock ).descriptor.template!
const result = compile({ const { code, map } = compile({
filename: 'example.vue', filename: 'example.vue',
source: template.content 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', () => { test('template errors', () => {
const result = compile({ const result = compile({
filename: 'example.vue', filename: 'example.vue',
source: `<div :foo source: `<div
:bar="a[" v-model="baz"/>` :bar="a[" v-model="baz"/>`
}) })
expect(result.errors).toMatchSnapshot() expect(result.errors).toMatchSnapshot()
@ -199,3 +369,36 @@ test('dynamic v-on + static v-on should merged', () => {
expect(result.code).toMatchSnapshot() 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 { parse } from '../src'
import { baseParse, baseCompile } from '@vue/compiler-core' import { baseCompile, createRoot } from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map-js' import { SourceMapConsumer } from 'source-map-js'
describe('compiler:sfc', () => { describe('compiler:sfc', () => {
@ -7,15 +7,61 @@ describe('compiler:sfc', () => {
test('style block', () => { test('style block', () => {
// Padding determines how many blank lines will there be before the style block // Padding determines how many blank lines will there be before the style block
const padding = Math.round(Math.random() * 10) const padding = Math.round(Math.random() * 10)
const style = parse( const src =
`${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n` `${'\n'.repeat(padding)}` +
).descriptor.styles[0] `<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 => { 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, line: 3,
column: 1, column: 1,
offset: 10 + content.length offset: 10 + content.length
}, }
source: content
}) })
}) })
@ -132,9 +177,8 @@ h1 { color: red }
expect(descriptor.template).toBeTruthy() expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBeFalsy() expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({ expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 1, offset: 0 }, start: { line: 1, column: 12, offset: 11 },
end: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 12, offset: 11 }
source: ''
}) })
}) })
@ -144,8 +188,7 @@ h1 { color: red }
expect(descriptor.template!.content).toBeFalsy() expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({ expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 11, offset: 10 }, start: { line: 1, column: 11, offset: 10 },
end: { line: 1, column: 11, offset: 10 }, end: { line: 1, column: 11, offset: 10 }
source: ''
}) })
}) })
@ -167,6 +210,11 @@ h1 { color: red }
expect(descriptor.script!.attrs['src']).toBe('com') 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', () => { test('ignoreEmpty: false', () => {
const { descriptor } = parse( const { descriptor } = parse(
`<script></script>\n<script setup>\n</script>`, `<script></script>\n<script setup>\n</script>`,
@ -176,14 +224,12 @@ h1 { color: red }
) )
expect(descriptor.script).toBeTruthy() expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.loc).toMatchObject({ expect(descriptor.script!.loc).toMatchObject({
source: '',
start: { line: 1, column: 9, offset: 8 }, start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 9, offset: 8 } end: { line: 1, column: 9, offset: 8 }
}) })
expect(descriptor.scriptSetup).toBeTruthy() expect(descriptor.scriptSetup).toBeTruthy()
expect(descriptor.scriptSetup!.loc).toMatchObject({ expect(descriptor.scriptSetup!.loc).toMatchObject({
source: '\n',
start: { line: 2, column: 15, offset: 32 }, start: { line: 2, column: 15, offset: 32 },
end: { line: 3, column: 1, offset: 33 } end: { line: 3, column: 1, offset: 33 }
}) })
@ -208,13 +254,15 @@ h1 { color: red }
}) })
// #1120 // #1120
test('alternative template lang should be treated as plain text', () => { test('template with preprocessor lang should be treated as plain text', () => {
const content = `p(v-if="1 < 2") test` const content = `p(v-if="1 < 2") test <div/>`
const { descriptor, errors } = parse( const { descriptor, errors } = parse(
`<template lang="pug">` + content + `</template>` `<template lang="pug">` + content + `</template>`
) )
expect(errors.length).toBe(0) expect(errors.length).toBe(0)
expect(descriptor.template!.content).toBe(content) expect(descriptor.template!.content).toBe(content)
// should not attempt to parse the content
expect(descriptor.template!.ast!.children.length).toBe(1)
}) })
//#2566 //#2566
@ -260,11 +308,18 @@ h1 { color: red }
test('custom compiler', () => { test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, { const { errors } = parse(`<template><input></template>`, {
compiler: { compiler: {
parse: baseParse, parse: (_, options) => {
options.onError!(new Error('foo') as any)
return createRoot([])
},
compile: baseCompile 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', () => { test('treat custom blocks as raw text', () => {

View File

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

View File

@ -26,7 +26,6 @@ import {
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars' import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnOnce } from './warn' import { warnOnce } from './warn'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { transformDestructuredProps } from './script/definePropsDestructure' import { transformDestructuredProps } from './script/definePropsDestructure'
import { ScriptCompileContext } from './script/context' import { ScriptCompileContext } from './script/context'
import { import {
@ -122,14 +121,6 @@ export interface SFCScriptCompileOptions {
fileExists(file: string): boolean fileExists(file: string): boolean
readFile(file: string): string | undefined 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 { export interface ImportBinding {
@ -165,8 +156,6 @@ export function compileScript(
const scriptLang = script && script.lang const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang const scriptSetupLang = scriptSetup && scriptSetup.lang
// TODO remove in 3.4
const enableReactivityTransform = !!options.reactivityTransform
let refBindings: string[] | undefined let refBindings: string[] | undefined
if (!scriptSetup) { 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> // <script> after <script setup>
// we need to move the block up so that `const __default__` is // we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition // declared before being used in the actual component definition
@ -687,26 +662,7 @@ export function compileScript(
transformDestructuredProps(ctx, vueImportAliases) transformDestructuredProps(ctx, vueImportAliases)
} }
// 4. Apply reactivity transform // 4. check macro args to make sure it doesn't reference setup scope
// 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
// variables // variables
checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
@ -714,7 +670,7 @@ export function compileScript(
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS) checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS) checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS)
// 6. remove non-script content // 5. remove non-script content
if (script) { if (script) {
if (startOffset < scriptStartOffset!) { if (startOffset < scriptStartOffset!) {
// <script setup> before <script> // <script setup> before <script>
@ -733,7 +689,7 @@ export function compileScript(
ctx.s.remove(endOffset, source.length) ctx.s.remove(endOffset, source.length)
} }
// 7. analyze binding metadata // 6. analyze binding metadata
// `defineProps` & `defineModel` also register props bindings // `defineProps` & `defineModel` also register props bindings
if (scriptAst) { if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body)) Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
@ -762,7 +718,7 @@ export function compileScript(
} }
} }
// 8. inject `useCssVars` calls // 7. inject `useCssVars` calls
if ( if (
sfc.cssVars.length && sfc.cssVars.length &&
// no need to do this when targeting SSR // 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` let args = `__props`
if (ctx.propsTypeDecl) { if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment // mark as any and only cast on assignment
@ -832,7 +788,7 @@ export function compileScript(
args += `, { ${destructureElements.join(', ')} }` args += `, { ${destructureElements.join(', ')} }`
} }
// 10. generate return statement // 9. generate return statement
let returned let returned
if ( if (
!options.inlineTemplate || !options.inlineTemplate ||
@ -948,7 +904,7 @@ export function compileScript(
ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`) ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
} }
// 11. finalize default export // 10. finalize default export
const genDefaultAs = options.genDefaultAs const genDefaultAs = options.genDefaultAs
? `const ${options.genDefaultAs} =` ? `const ${options.genDefaultAs} =`
: `export default` : `export default`
@ -1022,7 +978,7 @@ export function compileScript(
} }
} }
// 12. finalize Vue helper imports // 11. finalize Vue helper imports
if (ctx.helperImports.size > 0) { if (ctx.helperImports.size > 0) {
ctx.s.prepend( ctx.s.prepend(
`import { ${[...ctx.helperImports] `import { ${[...ctx.helperImports]
@ -1031,8 +987,6 @@ export function compileScript(
) )
} }
ctx.s.trim()
return { return {
...scriptSetup, ...scriptSetup,
bindings: ctx.bindingMetadata, bindings: ctx.bindingMetadata,

View File

@ -4,7 +4,10 @@ import {
CompilerError, CompilerError,
NodeTransform, NodeTransform,
ParserOptions, ParserOptions,
RootNode RootNode,
NodeTypes,
ElementNode,
createRoot
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { import {
SourceMapConsumer, SourceMapConsumer,
@ -30,7 +33,7 @@ import { warnOnce } from './warn'
import { genCssVarsFromList } from './style/cssVars' import { genCssVarsFromList } from './style/cssVars'
export interface TemplateCompiler { export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult compile(source: string | RootNode, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode parse(template: string, options: ParserOptions): RootNode
} }
@ -46,6 +49,7 @@ export interface SFCTemplateCompileResults {
export interface SFCTemplateCompileOptions { export interface SFCTemplateCompileOptions {
source: string source: string
ast?: RootNode
filename: string filename: string
id: string id: string
scoped?: boolean scoped?: boolean
@ -131,7 +135,8 @@ export function compileTemplate(
try { try {
return doCompileTemplate({ return doCompileTemplate({
...options, ...options,
source: preprocess(options, preprocessor) source: preprocess(options, preprocessor),
ast: undefined // invalidate AST if template goes through preprocessor
}) })
} catch (e: any) { } catch (e: any) {
return { return {
@ -164,10 +169,11 @@ function doCompileTemplate({
slotted, slotted,
inMap, inMap,
source, source,
ast: inAST,
ssr = false, ssr = false,
ssrCssVars, ssrCssVars,
isProd = false, isProd = false,
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM, compiler,
compilerOptions = {}, compilerOptions = {},
transformAssetUrls transformAssetUrls
}: SFCTemplateCompileOptions): SFCTemplateCompileResults { }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
@ -199,7 +205,30 @@ function doCompileTemplate({
const shortId = id.replace(/^data-v-/, '') const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}` 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', mode: 'module',
prefixIdentifiers: true, prefixIdentifiers: true,
hoistStatic: true, hoistStatic: true,
@ -222,7 +251,7 @@ function doCompileTemplate({
// inMap should be the map produced by ./parse.ts which is a simple line-only // 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 // mapping. If it is present, we need to adjust the final map and errors to
// reflect the original line numbers. // reflect the original line numbers.
if (inMap) { if (inMap && !inAST) {
if (map) { if (map) {
map = mapLines(inMap, map) map = mapLines(inMap, map)
} }
@ -235,7 +264,7 @@ function doCompileTemplate({
let msg = w.message let msg = w.message
if (w.loc) { if (w.loc) {
msg += `\n${generateCodeFrame( msg += `\n${generateCodeFrame(
source, inAST?.source || source,
w.loc.start.offset, w.loc.start.offset,
w.loc.end.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 // #9521 export parseCache as a simple map to avoid exposing LRU types
export const parseCache = _parseCache as Map<string, SFCParseResult> 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 // Utilities
export { parse as babelParse } from '@babel/parser' export { parse as babelParse } from '@babel/parser'
import MagicString from 'magic-string' import MagicString from 'magic-string'
@ -37,6 +30,8 @@ export {
// Internals for type resolution // Internals for type resolution
export { invalidateTypeCache, registerTS } from './script/resolveType' export { invalidateTypeCache, registerTS } from './script/resolveType'
export { extractRuntimeProps } from './script/defineProps'
export { extractRuntimeEmits } from './script/defineEmits'
// Types // Types
export type { export type {
@ -62,6 +57,7 @@ export type { SFCScriptCompileOptions } from './compileScript'
export type { ScriptCompileContext } from './script/context' export type { ScriptCompileContext } from './script/context'
export type { export type {
TypeResolveContext, TypeResolveContext,
SimpleTypeResolveOptions,
SimpleTypeResolveContext SimpleTypeResolveContext
} from './script/resolveType' } from './script/resolveType'
export type { export type {
@ -73,3 +69,10 @@ export type {
CompilerError, CompilerError,
BindingMetadata BindingMetadata
} from '@vue/compiler-core' } 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, ElementNode,
SourceLocation, SourceLocation,
CompilerError, CompilerError,
TextModes, BindingMetadata,
BindingMetadata RootNode,
createRoot
} from '@vue/compiler-core' } from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map-js' import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
@ -37,7 +38,7 @@ export interface SFCBlock {
export interface SFCTemplateBlock extends SFCBlock { export interface SFCTemplateBlock extends SFCBlock {
type: 'template' type: 'template'
ast: ElementNode ast?: RootNode
} }
export interface SFCScriptBlock extends SFCBlock { export interface SFCScriptBlock extends SFCBlock {
@ -128,31 +129,7 @@ export function parse(
const errors: (CompilerError | SyntaxError)[] = [] const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, { const ast = compiler.parse(source, {
// there are no components at SFC parsing level parseMode: 'sfc',
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
}
},
onError: e => { onError: e => {
errors.push(e) errors.push(e)
} }
@ -161,7 +138,8 @@ export function parse(
if (node.type !== NodeTypes.ELEMENT) { if (node.type !== NodeTypes.ELEMENT) {
return 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 ( if (
ignoreEmpty && ignoreEmpty &&
node.tag !== 'template' && node.tag !== 'template' &&
@ -178,7 +156,10 @@ export function parse(
source, source,
false false
) as SFCTemplateBlock) ) as SFCTemplateBlock)
templateBlock.ast = node
if (!templateBlock.attrs.src) {
templateBlock.ast = createRoot(node.children, source)
}
// warn against 2.x <template functional> // warn against 2.x <template functional>
if (templateBlock.attrs.functional) { if (templateBlock.attrs.functional) {
@ -188,7 +169,9 @@ export function parse(
`difference from stateful ones. Just use a normal <template> ` + `difference from stateful ones. Just use a normal <template> ` +
`instead.` `instead.`
) as CompilerError ) 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) errors.push(err)
} }
} else { } else {
@ -307,32 +290,11 @@ function createBlock(
pad: SFCParseOptions['pad'] pad: SFCParseOptions['pad']
): SFCBlock { ): SFCBlock {
const type = node.tag const type = node.tag
let { start, end } = node.loc const loc = node.innerLoc!
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 attrs: Record<string, string | true> = {} const attrs: Record<string, string | true> = {}
const block: SFCBlock = { const block: SFCBlock = {
type, type,
content, content: source.slice(loc.start.offset, loc.end.offset),
loc, loc,
attrs attrs
} }
@ -341,18 +303,19 @@ function createBlock(
} }
node.props.forEach(p => { node.props.forEach(p => {
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true const name = p.name
if (p.name === 'lang') { attrs[name] = p.value ? p.value.content || true : true
if (name === 'lang') {
block.lang = p.value && p.value.content block.lang = p.value && p.value.content
} else if (p.name === 'src') { } else if (name === 'src') {
block.src = p.value && p.value.content block.src = p.value && p.value.content
} else if (type === 'style') { } else if (type === 'style') {
if (p.name === 'scoped') { if (name === 'scoped') {
;(block as SFCStyleBlock).scoped = true ;(block as SFCStyleBlock).scoped = true
} else if (p.name === 'module') { } else if (name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name] ;(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 ;(block as SFCScriptBlock).setup = attrs.setup
} }
} }
@ -376,28 +339,27 @@ function generateSourceMap(
sourceRoot: sourceRoot.replace(/\\/g, '/') sourceRoot: sourceRoot.replace(/\\/g, '/')
}) })
map.setSourceContent(filename, source) map.setSourceContent(filename, source)
map._sources.add(filename)
generated.split(splitRE).forEach((line, index) => { generated.split(splitRE).forEach((line, index) => {
if (!emptyRE.test(line)) { if (!emptyRE.test(line)) {
const originalLine = index + 1 + lineOffset const originalLine = index + 1 + lineOffset
const generatedLine = index + 1 const generatedLine = index + 1
for (let i = 0; i < line.length; i++) { for (let i = 0; i < line.length; i++) {
if (!/\s/.test(line[i])) { if (!/\s/.test(line[i])) {
map.addMapping({ map._mappings.add({
originalLine,
originalColumn: i,
generatedLine,
generatedColumn: i,
source: filename, source: filename,
original: { // @ts-ignore
line: originalLine, name: null
column: i
},
generated: {
line: generatedLine,
column: i
}
}) })
} }
} }
} }
}) })
return JSON.parse(map.toString()) return map.toJSON()
} }
function padContent( function padContent(

View File

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

View File

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

View File

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

View File

@ -3,11 +3,10 @@ import { SFCDescriptor } from '../parse'
import { import {
NodeTypes, NodeTypes,
SimpleExpressionNode, SimpleExpressionNode,
createRoot,
forAliasRE, forAliasRE,
parserOptions, parserOptions,
transform, walkIdentifiers,
walkIdentifiers TemplateChildNode
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { createCache } from '../cache' import { createCache } from '../cache'
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared' import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
@ -35,53 +34,54 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
} }
let code = '' 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 ast!.children.forEach(walk)
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
code += `,${stripStrings(
(prop.arg as SimpleExpressionNode).content
)}`
}
if (prop.exp) { function walk(node: TemplateChildNode) {
code += `,${processExp( switch (node.type) {
(prop.exp as SimpleExpressionNode).content, case NodeTypes.ELEMENT:
prop.name if (
)}` !parserOptions.isNativeTag!(node.tag) &&
} !parserOptions.isBuiltInComponent!(node.tag)
} ) {
if ( code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
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
)}`
} }
} 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 += ';' code += ';'
templateUsageCheckCache.set(content, code) templateUsageCheckCache.set(content, code)

View File

@ -1,8 +1,6 @@
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { analyzeScriptBindings } from './analyzeScriptBindings' import { analyzeScriptBindings } from './analyzeScriptBindings'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { RawSourceMap } from 'source-map-js'
import { rewriteDefaultAST } from '../rewriteDefault' import { rewriteDefaultAST } from '../rewriteDefault'
import { genNormalScriptCssVarsCode } from '../style/cssVars' import { genNormalScriptCssVarsCode } from '../style/cssVars'
@ -22,33 +20,8 @@ export function processNormalScript(
let map = script.map let map = script.map
const scriptAst = ctx.scriptAst! const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body) const bindings = analyzeScriptBindings(scriptAst.body)
const { source, filename, cssVars } = ctx.descriptor const { cssVars } = ctx.descriptor
const { sourceMap, genDefaultAs, isProd } = ctx.options const { 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
}
}
if (cssVars.length || genDefaultAs) { if (cssVars.length || genDefaultAs) {
const defaultVar = genDefaultAs || normalScriptDefaultVar const defaultVar = genDefaultAs || normalScriptDefaultVar

View File

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

View File

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

View File

@ -38,7 +38,12 @@ const scss: StylePreprocessor = (source, map, options, load = require) => {
if (map) { if (map) {
return { return {
code: result.css.toString(), 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: [], errors: [],
dependencies dependencies
} }

View File

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

View File

@ -16,14 +16,14 @@ export function createSSRCompilerError(
return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError 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_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_NO_TELEPORT_TARGET, X_SSR_NO_TELEPORT_TARGET,
X_SSR_INVALID_AST_NODE X_SSR_INVALID_AST_NODE
} }
if (__TEST__) { 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 // file, so we have to manually keep them in sync. this check ensures it
// errors out if there are collisions. // errors out if there are collisions.
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) { if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {

View File

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

View File

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

View File

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

View File

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

View File

@ -292,14 +292,15 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
} }
} else { } else {
// special case: value on <textarea> // 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)) rawChildrenMap.set(node, escapeHtml(prop.value.content))
} else if (!needMergeProps) { } else if (!needMergeProps) {
if (prop.name === 'key' || prop.name === 'ref') { if (name === 'key' || name === 'ref') {
continue continue
} }
// static prop // static prop
if (prop.name === 'class' && prop.value) { if (name === 'class' && prop.value) {
staticClassBinding = JSON.stringify(prop.value.content) staticClassBinding = JSON.stringify(prop.value.content)
} }
openTag.push( 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 { declare interface String {
/** /**
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository. * @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 // mutate n
n.value++ n.value++
// on the 2nd run, plusOne.value should have already updated. // 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', () => { it('should warn if trying to set a readonly computed', () => {
@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
oldValue: 2 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', () => { describe('deferred computed', () => {
const tick = Promise.resolve() test('should not trigger if value did not change', () => {
test('should only trigger once on multiple mutations', async () => {
const src = ref(0) const src = ref(0)
const c = deferredComputed(() => src.value) const c = computed(() => src.value % 2)
const spy = vi.fn() const spy = vi.fn()
effect(() => { effect(() => {
spy(c.value) spy(c.value)
}) })
expect(spy).toHaveBeenCalledTimes(1) 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 src.value = 2
await tick
// should not trigger // should not trigger
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(1)
src.value = 3 src.value = 3
src.value = 4
src.value = 5 src.value = 5
await tick
// should trigger because latest value changes // should trigger because latest value changes
expect(spy).toHaveBeenCalledTimes(2) expect(spy).toHaveBeenCalledTimes(2)
}) })
test('chained computed trigger', async () => { test('chained computed trigger', () => {
const effectSpy = vi.fn() const effectSpy = vi.fn()
const c1Spy = vi.fn() const c1Spy = vi.fn()
const c2Spy = vi.fn() const c2Spy = vi.fn()
const src = ref(0) const src = ref(0)
const c1 = deferredComputed(() => { const c1 = computed(() => {
c1Spy() c1Spy()
return src.value % 2 return src.value % 2
}) })
@ -69,19 +44,18 @@ describe('deferred computed', () => {
expect(effectSpy).toHaveBeenCalledTimes(1) expect(effectSpy).toHaveBeenCalledTimes(1)
src.value = 1 src.value = 1
await tick
expect(c1Spy).toHaveBeenCalledTimes(2) expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c2Spy).toHaveBeenCalledTimes(2) expect(c2Spy).toHaveBeenCalledTimes(2)
expect(effectSpy).toHaveBeenCalledTimes(2) expect(effectSpy).toHaveBeenCalledTimes(2)
}) })
test('chained computed avoid re-compute', async () => { test('chained computed avoid re-compute', () => {
const effectSpy = vi.fn() const effectSpy = vi.fn()
const c1Spy = vi.fn() const c1Spy = vi.fn()
const c2Spy = vi.fn() const c2Spy = vi.fn()
const src = ref(0) const src = ref(0)
const c1 = deferredComputed(() => { const c1 = computed(() => {
c1Spy() c1Spy()
return src.value % 2 return src.value % 2
}) })
@ -98,26 +72,24 @@ describe('deferred computed', () => {
src.value = 2 src.value = 2
src.value = 4 src.value = 4
src.value = 6 src.value = 6
await tick expect(c1Spy).toHaveBeenCalledTimes(4)
// c1 should re-compute once.
expect(c1Spy).toHaveBeenCalledTimes(2)
// c2 should not have to re-compute because c1 did not change. // c2 should not have to re-compute because c1 did not change.
expect(c2Spy).toHaveBeenCalledTimes(1) expect(c2Spy).toHaveBeenCalledTimes(1)
// effect should not trigger because c2 did not change. // effect should not trigger because c2 did not change.
expect(effectSpy).toHaveBeenCalledTimes(1) expect(effectSpy).toHaveBeenCalledTimes(1)
}) })
test('chained computed value invalidation', async () => { test('chained computed value invalidation', () => {
const effectSpy = vi.fn() const effectSpy = vi.fn()
const c1Spy = vi.fn() const c1Spy = vi.fn()
const c2Spy = vi.fn() const c2Spy = vi.fn()
const src = ref(0) const src = ref(0)
const c1 = deferredComputed(() => { const c1 = computed(() => {
c1Spy() c1Spy()
return src.value % 2 return src.value % 2
}) })
const c2 = deferredComputed(() => { const c2 = computed(() => {
c2Spy() c2Spy()
return c1.value + 1 return c1.value + 1
}) })
@ -139,17 +111,17 @@ describe('deferred computed', () => {
expect(c2Spy).toHaveBeenCalledTimes(2) 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 effectSpy = vi.fn()
const c1Spy = vi.fn() const c1Spy = vi.fn()
const c2Spy = vi.fn() const c2Spy = vi.fn()
const src = ref(0) const src = ref(0)
const c1 = deferredComputed(() => { const c1 = computed(() => {
c1Spy() c1Spy()
return src.value % 2 return src.value % 2
}) })
const c2 = deferredComputed(() => { const c2 = computed(() => {
c2Spy() c2Spy()
return c1.value + 1 return c1.value + 1
}) })
@ -162,14 +134,13 @@ describe('deferred computed', () => {
src.value = 1 src.value = 1
// sync access c2 // sync access c2
c2.value c2.value
await tick
expect(effectSpy).toHaveBeenCalledTimes(2) 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 c1Spy = vi.fn()
const src = ref(0) const src = ref(0)
const c1 = deferredComputed(() => { const c1 = computed(() => {
c1Spy() c1Spy()
return src.value % 2 return src.value % 2
}) })
@ -179,7 +150,6 @@ describe('deferred computed', () => {
c1.effect.stop() c1.effect.stop()
// trigger // trigger
src.value++ src.value++
await tick
expect(c1Spy).toHaveBeenCalledTimes(1) expect(c1Spy).toHaveBeenCalledTimes(1)
}) })
}) })

View File

@ -1,5 +1,4 @@
import { import {
ref,
reactive, reactive,
effect, effect,
stop, stop,
@ -12,7 +11,8 @@ import {
readonly, readonly,
ReactiveEffectRunner ReactiveEffectRunner
} from '../src/index' } 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', () => { describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a 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(output.fx2).toBe(1 + 3 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1) expect(fx1Spy).toHaveBeenCalledTimes(1)
// Invoked twice due to change of fx1. // Invoked due to change of fx1.
expect(fx2Spy).toHaveBeenCalledTimes(2) expect(fx2Spy).toHaveBeenCalledTimes(1)
fx1Spy.mockClear() fx1Spy.mockClear()
fx2Spy.mockClear() fx2Spy.mockClear()
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(3) 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', () => { it('events: onStop', () => {
const onStop = vi.fn() const onStop = vi.fn()
const runner = effect(() => {}, { const runner = effect(() => {}, {
@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
expect(has).toBe(false) 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) 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', () => { test('add existing index on Array should not trigger length dependency', () => {
const array = new Array(3) const array = new Array(3)
const observed = reactive(array) const observed = reactive(array)

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { DebuggerOptions, ReactiveEffect } from './effect' import { DebuggerOptions, ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref' import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared' import { hasChanged, isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive' import { toRaw } from './reactive'
import { Dep } from './dep' import { Dep } from './dep'
import { DirtyLevels, ReactiveFlags } from './constants'
declare const ComputedRefSymbol: unique symbol declare const ComputedRefSymbol: unique symbol
@ -15,8 +16,8 @@ export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T> readonly effect: ReactiveEffect<T>
} }
export type ComputedGetter<T> = (...args: any[]) => T export type ComputedGetter<T> = (oldValue?: T) => T
export type ComputedSetter<T> = (v: T) => void export type ComputedSetter<T> = (newValue: T) => void
export interface WritableComputedOptions<T> { export interface WritableComputedOptions<T> {
get: ComputedGetter<T> get: ComputedGetter<T>
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
public readonly __v_isRef = true public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true
public _cacheable: boolean public _cacheable: boolean
constructor( constructor(
@ -41,12 +41,10 @@ export class ComputedRefImpl<T> {
isReadonly: boolean, isReadonly: boolean,
isSSR: boolean isSSR: boolean
) { ) {
this.effect = new ReactiveEffect(getter, () => { this.effect = new ReactiveEffect(
if (!this._dirty) { () => getter(this._value),
this._dirty = true () => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
triggerRefValue(this) )
}
})
this.effect.computed = this this.effect.computed = this
this.effect.active = this._cacheable = !isSSR this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly 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 // the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this) const self = toRaw(this)
trackRefValue(self) trackRefValue(self)
if (self._dirty || !self._cacheable) { if (!self._cacheable || self.effect.dirty) {
self._dirty = false if (hasChanged(self._value, (self._value = self.effect.run()!))) {
self._value = self.effect.run()! triggerRefValue(self, DirtyLevels.ComputedValueDirty)
}
} }
return self._value return self._value
} }
@ -66,6 +65,16 @@ export class ComputedRefImpl<T> {
set value(newValue: T) { set value(newValue: T) {
this._setter(newValue) 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 { computed } from './computed'
import { ReactiveEffect } from './effect'
import { ComputedGetter, ComputedRef } from './computed'
import { ReactiveFlags, toRaw } from './reactive'
import { trackRefValue, triggerRefValue } from './ref'
const tick = /*#__PURE__*/ Promise.resolve() /**
const queue: any[] = [] * @deprecated use `computed` instead. See #5912
let queued = false */
export const deferredComputed = computed
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
}

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 export type Dep = Map<ReactiveEffect, number> & {
cleanup: () => void
/** computed?: ComputedRefImpl<any>
* 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 const createDep = (effects?: ReactiveEffect[]): Dep => { export const createDep = (
const dep = new Set<ReactiveEffect>(effects) as Dep cleanup: () => void,
dep.w = 0 computed?: ComputedRefImpl<any>
dep.n = 0 ): Dep => {
const dep = new Map() as Dep
dep.cleanup = cleanup
dep.computed = computed
return dep 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
}
}

View File

@ -1,34 +1,8 @@
import { TrackOpTypes, TriggerOpTypes } from './operations' import { NOOP, extend } from '@vue/shared'
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' import type { ComputedRefImpl } from './computed'
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
import type { Dep } from './dep'
import { EffectScope, recordEffectScope } from './effectScope' import { EffectScope, recordEffectScope } from './effectScope'
import {
createDep,
Dep,
finalizeDepMarkers,
initDepMarkers,
newTracked,
wasTracked
} from './dep'
import { ComputedRefImpl } from './computed'
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<object, KeyToDepMap>()
// The number of effects currently being tracked recursively.
let effectTrackDepth = 0
export let trackOpBit = 1
/**
* The bitwise track markers support at most 30 levels of recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a full cleanup.
*/
const maxMarkerBits = 30
export type EffectScheduler = (...args: any[]) => any export type EffectScheduler = (...args: any[]) => any
@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = {
export let activeEffect: ReactiveEffect | undefined export let activeEffect: ReactiveEffect | undefined
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
export class ReactiveEffect<T = any> { export class ReactiveEffect<T = any> {
active = true active = true
deps: Dep[] = [] deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/** /**
* Can be attached after creation * Can be attached after creation
@ -64,10 +34,6 @@ export class ReactiveEffect<T = any> {
* @internal * @internal
*/ */
allowRecurse?: boolean allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void onStop?: () => void
// dev only // dev only
@ -75,77 +41,115 @@ export class ReactiveEffect<T = any> {
// dev only // dev only
onTrigger?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void
/**
* @internal
*/
_dirtyLevel = DirtyLevels.Dirty
/**
* @internal
*/
_trackId = 0
/**
* @internal
*/
_runnings = 0
/**
* @internal
*/
_queryings = 0
/**
* @internal
*/
_depsLength = 0
constructor( constructor(
public fn: () => T, public fn: () => T,
public scheduler: EffectScheduler | null = null, public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope scope?: EffectScope
) { ) {
recordEffectScope(this, scope) recordEffectScope(this, scope)
} }
public get dirty() {
if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
this._queryings++
pauseTracking()
for (const dep of this.deps) {
if (dep.computed) {
triggerComputed(dep.computed)
if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
break
}
}
}
resetTracking()
this._queryings--
}
return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
}
public set dirty(v) {
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}
run() { run() {
this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) { if (!this.active) {
return this.fn() return this.fn()
} }
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack let lastShouldTrack = shouldTrack
while (parent) { let lastEffect = activeEffect
if (parent === this) {
return
}
parent = parent.parent
}
try { try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true shouldTrack = true
activeEffect = this
trackOpBit = 1 << ++effectTrackDepth this._runnings++
preCleanupEffect(this)
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn() return this.fn()
} finally { } finally {
if (effectTrackDepth <= maxMarkerBits) { postCleanupEffect(this)
finalizeDepMarkers(this) this._runnings--
} activeEffect = lastEffect
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
} }
} }
stop() { stop() {
// stopped while running itself - defer the cleanup if (this.active) {
if (activeEffect === this) { preCleanupEffect(this)
this.deferStop = true postCleanupEffect(this)
} else if (this.active) { this.onStop?.()
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false this.active = false
} }
} }
} }
function cleanupEffect(effect: ReactiveEffect) { function triggerComputed(computed: ComputedRefImpl<any>) {
const { deps } = effect return computed.value
if (deps.length) { }
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect) function preCleanupEffect(effect: ReactiveEffect) {
effect._trackId++
effect._depsLength = 0
}
function postCleanupEffect(effect: ReactiveEffect) {
if (effect.deps && effect.deps.length > effect._depsLength) {
for (let i = effect._depsLength; i < effect.deps.length; i++) {
cleanupDepEffect(effect.deps[i], effect)
}
effect.deps.length = effect._depsLength
}
}
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
const trackId = dep.get(effect)
if (trackId !== undefined && effect._trackId !== trackId) {
dep.delete(effect)
if (dep.size === 0) {
dep.cleanup()
} }
deps.length = 0
} }
} }
@ -185,7 +189,11 @@ export function effect<T = any>(
fn = (fn as ReactiveEffectRunner).effect.fn fn = (fn as ReactiveEffectRunner).effect.fn
} }
const _effect = new ReactiveEffect(fn) const _effect = new ReactiveEffect(fn, NOOP, () => {
if (_effect.dirty) {
_effect.run()
}
})
if (options) { if (options) {
extend(_effect, options) extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope) if (options.scope) recordEffectScope(_effect, options.scope)
@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) {
} }
export let shouldTrack = true export let shouldTrack = true
export let pauseScheduleStack = 0
const trackStack: boolean[] = [] const trackStack: boolean[] = []
/** /**
@ -234,196 +244,70 @@ export function resetTracking() {
shouldTrack = last === undefined ? true : last shouldTrack = last === undefined ? true : last
} }
/** export function pauseScheduling() {
* Tracks access to a reactive property. pauseScheduleStack++
* }
* This will check which effect is running at the moment and record it as dep
* which records all effects that depend on the reactive property.
*
* @param target - Object holding the reactive property.
* @param type - Defines the type of access to the reactive property.
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__ export function resetScheduling() {
? { effect: activeEffect, target, type, key } pauseScheduleStack--
: undefined while (!pauseScheduleStack && queueEffectSchedulers.length) {
queueEffectSchedulers.shift()!()
trackEffects(dep, eventInfo)
} }
} }
export function trackEffects( export function trackEffect(
effect: ReactiveEffect,
dep: Dep, dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo debuggerEventExtraInfo?: DebuggerEventExtraInfo
) { ) {
let shouldTrack = false if (dep.get(effect) !== effect._trackId) {
if (effectTrackDepth <= maxMarkerBits) { dep.set(effect, effect._trackId)
if (!newTracked(dep)) { const oldDep = effect.deps[effect._depsLength]
dep.n |= trackOpBit // set newly tracked if (oldDep !== dep) {
shouldTrack = !wasTracked(dep) if (oldDep) {
} cleanupDepEffect(oldDep, effect)
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
/**
* Finds all deps associated with the target (or a specific property) and
* triggers the effects stored within.
*
* @param target - The reactive object.
* @param type - Defines the type of the operation that needs to trigger effects.
* @param key - Can be used to target a specific reactive property in the target object.
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
} }
effect.deps[effect._depsLength++] = dep
} else {
effect._depsLength++
} }
if (__DEV__) { if (__DEV__) {
triggerEffects(createDep(effects), eventInfo) effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
} else {
triggerEffects(createDep(effects))
} }
} }
} }
const queueEffectSchedulers: (() => void)[] = []
export function triggerEffects( export function triggerEffects(
dep: Dep | ReactiveEffect[], dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo debuggerEventExtraInfo?: DebuggerEventExtraInfo
) { ) {
// spread into array for stabilization pauseScheduling()
const effects = isArray(dep) ? dep : [...dep] for (const effect of dep.keys()) {
for (const effect of effects) { if (!effect.allowRecurse && effect._runnings) {
if (effect.computed) { continue
triggerEffect(effect, debuggerEventExtraInfo) }
} if (
} effect._dirtyLevel < dirtyLevel &&
for (const effect of effects) { (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
if (!effect.computed) { ) {
triggerEffect(effect, debuggerEventExtraInfo) const lastDirtyLevel = effect._dirtyLevel
effect._dirtyLevel = dirtyLevel
if (
lastDirtyLevel === DirtyLevels.NotDirty &&
(!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
) {
if (__DEV__) {
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
}
}
} }
} }
} resetScheduling()
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
export function getDepFromReactive(object: any, key: string | number | symbol) {
return targetMap.get(object)?.get(key)
} }

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