mirror of https://github.com/vuejs/core.git
Merge remote-tracking branch 'upstream/minor'
This commit is contained in:
commit
377723d8b2
|
|
@ -3,6 +3,15 @@
|
|||
const DOMGlobals = ['window', 'document']
|
||||
const NodeGlobals = ['module', 'require']
|
||||
|
||||
const banConstEnum = {
|
||||
selector: 'TSEnumDeclaration[const=true]',
|
||||
message:
|
||||
'Please use non-const enums. This project automatically inlines enums.'
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('eslint-define-config').ESLintConfig}
|
||||
*/
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
|
@ -16,6 +25,7 @@ module.exports = {
|
|||
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
banConstEnum,
|
||||
// since we target ES2015 for baseline support, we need to forbid object
|
||||
// rest spread usage in destructure as it compiles into a verbose helper.
|
||||
'ObjectPattern > RestElement',
|
||||
|
|
@ -52,12 +62,10 @@ module.exports = {
|
|||
},
|
||||
// Packages targeting Node
|
||||
{
|
||||
files: [
|
||||
'packages/{compiler-sfc,compiler-ssr,server-renderer,reactivity-transform}/**'
|
||||
],
|
||||
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
|
||||
rules: {
|
||||
'no-restricted-globals': ['error', ...DOMGlobals],
|
||||
'no-restricted-syntax': 'off'
|
||||
'no-restricted-syntax': ['error', banConstEnum]
|
||||
}
|
||||
},
|
||||
// Private package, browser only + no syntax restrictions
|
||||
|
|
@ -65,7 +73,7 @@ module.exports = {
|
|||
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
|
||||
rules: {
|
||||
'no-restricted-globals': ['error', ...NodeGlobals],
|
||||
'no-restricted-syntax': 'off'
|
||||
'no-restricted-syntax': ['error', banConstEnum]
|
||||
}
|
||||
},
|
||||
// JavaScript files
|
||||
|
|
@ -81,7 +89,7 @@ module.exports = {
|
|||
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
|
||||
rules: {
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-syntax': 'off'
|
||||
'no-restricted-syntax': ['error', banConstEnum]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
69
CHANGELOG.md
69
CHANGELOG.md
|
|
@ -1,3 +1,59 @@
|
|||
# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update compiler to ^7.23.4 ([#9681](https://github.com/vuejs/core/issues/9681)) ([31f6ebc](https://github.com/vuejs/core/commit/31f6ebc4df84490ed29fb75e7bf4259200eb51f0))
|
||||
* **parser:** directive arg should be undefined on shorthands with no arg ([e49dffc](https://github.com/vuejs/core/commit/e49dffc9ece86bddf094b9ad4ad15eb4856d6277))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **dx:** link errors to docs in prod build ([#9165](https://github.com/vuejs/core/issues/9165)) ([9f8ba98](https://github.com/vuejs/core/commit/9f8ba9821fe166f77e63fa940e9e7e13ec3344fa))
|
||||
|
||||
|
||||
|
||||
# [3.4.0-alpha.2](https://github.com/vuejs/core/compare/v3.3.9...v3.4.0-alpha.2) (2023-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid confusing breakage in @vitejs/plugin-vue ([ceec69c](https://github.com/vuejs/core/commit/ceec69c8ccb96c433a4a506ad2e85e276998bade))
|
||||
* **compiler-core:** fix line/column tracking when fast forwarding ([2e65ea4](https://github.com/vuejs/core/commit/2e65ea481f74db8649df8110a031cbdc98f98c84))
|
||||
* **compiler-sfc:** fix ast reuse for ssr ([fb619cf](https://github.com/vuejs/core/commit/fb619cf9a440239f0ba88e327d10001a6a3c8171))
|
||||
* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([c6083dc](https://github.com/vuejs/core/commit/c6083dcad31f3e9292c687fada9e32f287e2317f))
|
||||
* **compiler-sfc:** use correct compiler when re-parsing in ssr mode ([678378a](https://github.com/vuejs/core/commit/678378afd559481badb486b243722b6287862e09))
|
||||
|
||||
|
||||
* feat!: remove reactivity transform (#9321) ([79b8a09](https://github.com/vuejs/core/commit/79b8a0905bf363bf82edd2096fef10c3db6d9c3c)), closes [#9321](https://github.com/vuejs/core/issues/9321)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-core:** support specifying root namespace when parsing ([40f72d5](https://github.com/vuejs/core/commit/40f72d5e50b389cb11b7ca13461aa2a75ddacdb4))
|
||||
* **compiler-core:** support v-bind shorthand for key and value with the same name ([#9451](https://github.com/vuejs/core/issues/9451)) ([26399aa](https://github.com/vuejs/core/commit/26399aa6fac1596b294ffeba06bb498d86f5508c))
|
||||
* **compiler:** improve parsing tolerance for language-tools ([41ff68e](https://github.com/vuejs/core/commit/41ff68ea579d933333392146625560359acb728a))
|
||||
* **reactivity:** expose last result for computed getter ([#9497](https://github.com/vuejs/core/issues/9497)) ([48b47a1](https://github.com/vuejs/core/commit/48b47a1ab63577e2dbd91947eea544e3ef185b85))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* avoid sfc source map unnecessary serialization and parsing ([f15d2f6](https://github.com/vuejs/core/commit/f15d2f6cf69c0c39f8dfb5c33122790c68bf92e2))
|
||||
* **codegen:** optimize line / column calculation during codegen ([3be53d9](https://github.com/vuejs/core/commit/3be53d9b974dae1a10eb795cade71ae765e17574))
|
||||
* **codegen:** optimize source map generation ([c11002f](https://github.com/vuejs/core/commit/c11002f16afd243a2b15b546816e73882eea9e4d))
|
||||
* **compiler-sfc:** remove magic-string trim on script ([e8e3ec6](https://github.com/vuejs/core/commit/e8e3ec6ca7392e43975c75b56eaaa711d5ea9410))
|
||||
* **compiler-sfc:** use faster source map addMapping ([50cde7c](https://github.com/vuejs/core/commit/50cde7cfbcc49022ba88f5f69fa9b930b483c282))
|
||||
* optimize away isBuiltInType ([66c0ed0](https://github.com/vuejs/core/commit/66c0ed0a3c1c6f37dafc6b1c52b75c6bf60e3136))
|
||||
* optimize makeMap ([ae6fba9](https://github.com/vuejs/core/commit/ae6fba94954bac6430902f77b0d1113a98a75b18))
|
||||
* optimize position cloning ([2073236](https://github.com/vuejs/core/commit/20732366b9b3530d33b842cf1fc985919afb9317))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html
|
||||
|
||||
|
||||
|
||||
## [3.3.9](https://github.com/vuejs/core/compare/v3.3.8...v3.3.9) (2023-11-25)
|
||||
|
||||
|
||||
|
|
@ -47,6 +103,19 @@
|
|||
|
||||
|
||||
|
||||
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd))
|
||||
* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433))
|
||||
* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b))
|
||||
* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464)
|
||||
* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d))
|
||||
|
||||
|
||||
|
||||
## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24)
|
||||
|
||||
|
||||
|
|
|
|||
17
package.json
17
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"version": "0.0.0-vapor",
|
||||
"packageManager": "pnpm@8.10.5",
|
||||
"packageManager": "pnpm@8.11.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js vue vue-vapor",
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"serve": "serve",
|
||||
"open": "open http://localhost:3000/packages/template-explorer/local.html",
|
||||
"build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
|
||||
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity reactivity-transform shared -af cjs",
|
||||
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
|
||||
"build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
|
||||
"build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
|
||||
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
"node": ">=18.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.23.3",
|
||||
"@babel/types": "^7.23.3",
|
||||
"@babel/parser": "^7.23.4",
|
||||
"@babel/types": "^7.23.4",
|
||||
"@rollup/plugin-alias": "^5.0.1",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-json": "^6.0.1",
|
||||
|
|
@ -68,8 +68,10 @@
|
|||
"@rollup/plugin-replace": "^5.0.4",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/hash-sum": "^1.0.2",
|
||||
"@types/node": "^20.9.2",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/semver": "^7.5.5",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"@vitest/coverage-istanbul": "^0.34.6",
|
||||
"@vue/consolidate": "0.17.3",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
|
|
@ -77,6 +79,7 @@
|
|||
"esbuild": "^0.19.5",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-define-config": "^1.24.1",
|
||||
"eslint-plugin-jest": "^27.6.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"execa": "^8.0.1",
|
||||
|
|
@ -104,7 +107,7 @@
|
|||
"terser": "^5.22.0",
|
||||
"todomvc-app-css": "^2.4.3",
|
||||
"tslib": "^2.6.2",
|
||||
"tsx": "^4.1.4",
|
||||
"tsx": "^4.5.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0",
|
||||
"vitest": "^0.34.6"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -40,6 +40,7 @@ import { PatchFlags } from '@vue/shared'
|
|||
function createRoot(options: Partial<RootNode> = {}): RootNode {
|
||||
return {
|
||||
type: NodeTypes.ROOT,
|
||||
source: '',
|
||||
children: [],
|
||||
helpers: new Set(),
|
||||
components: [],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -56,7 +56,6 @@ export function createElementWithCodegen(
|
|||
ns: Namespaces.HTML,
|
||||
tag: 'div',
|
||||
tagType: ElementTypes.ELEMENT,
|
||||
isSelfClosing: false,
|
||||
props: [],
|
||||
children: [],
|
||||
codegenNode: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { baseParse } from '../src/parse'
|
||||
import { baseParse } from '../src/parser'
|
||||
import { transform, NodeTransform } from '../src/transform'
|
||||
import {
|
||||
ElementNode,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|||
onClick: () => {
|
||||
for (let i = 0; i < _ctx.list.length; i++) {
|
||||
_ctx.log(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 8 /* PROPS */, [\\"onClick\\"]))
|
||||
}"
|
||||
|
|
@ -36,7 +36,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|||
onClick: () => {
|
||||
for (const x in _ctx.list) {
|
||||
_ctx.log(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 8 /* PROPS */, [\\"onClick\\"]))
|
||||
}"
|
||||
|
|
@ -50,7 +50,7 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|||
onClick: () => {
|
||||
for (const x of _ctx.list) {
|
||||
_ctx.log(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 8 /* PROPS */, [\\"onClick\\"]))
|
||||
}"
|
||||
|
|
|
|||
|
|
@ -1195,25 +1195,13 @@ describe('compiler: element transform', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// TODO remove in 3.4
|
||||
test('v-is', () => {
|
||||
const { node, root } = parseWithBind(`<div v-is="'foo'" />`)
|
||||
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
|
||||
test('is casting', () => {
|
||||
const { node, root } = parseWithBind(`<div is="vue:foo" />`)
|
||||
expect(root.helpers).toContain(RESOLVE_COMPONENT)
|
||||
expect(node).toMatchObject({
|
||||
tag: {
|
||||
callee: RESOLVE_DYNAMIC_COMPONENT,
|
||||
arguments: [
|
||||
{
|
||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||
content: `'foo'`,
|
||||
isStatic: false
|
||||
}
|
||||
]
|
||||
},
|
||||
// should skip v-is runtime check
|
||||
directives: undefined
|
||||
type: NodeTypes.VNODE_CALL,
|
||||
tag: '_component_foo'
|
||||
})
|
||||
expect('v-is="component-name" has been deprecated').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
// #3934
|
||||
|
|
|
|||
|
|
@ -128,51 +128,24 @@ describe('compiler: expression transform', () => {
|
|||
{
|
||||
content: `_ctx.foo`,
|
||||
loc: {
|
||||
source: `foo`,
|
||||
start: {
|
||||
offset: 3,
|
||||
line: 1,
|
||||
column: 4
|
||||
},
|
||||
end: {
|
||||
offset: 6,
|
||||
line: 1,
|
||||
column: 7
|
||||
}
|
||||
start: { offset: 3, line: 1, column: 4 },
|
||||
end: { offset: 6, line: 1, column: 7 }
|
||||
}
|
||||
},
|
||||
`(`,
|
||||
{
|
||||
content: `_ctx.baz`,
|
||||
loc: {
|
||||
source: `baz`,
|
||||
start: {
|
||||
offset: 7,
|
||||
line: 1,
|
||||
column: 8
|
||||
},
|
||||
end: {
|
||||
offset: 10,
|
||||
line: 1,
|
||||
column: 11
|
||||
}
|
||||
start: { offset: 7, line: 1, column: 8 },
|
||||
end: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
},
|
||||
` + 1, { key: `,
|
||||
{
|
||||
content: `_ctx.kuz`,
|
||||
loc: {
|
||||
source: `kuz`,
|
||||
start: {
|
||||
offset: 23,
|
||||
line: 1,
|
||||
column: 24
|
||||
},
|
||||
end: {
|
||||
offset: 26,
|
||||
line: 1,
|
||||
column: 27
|
||||
}
|
||||
start: { offset: 23, line: 1, column: 24 },
|
||||
end: { offset: 26, line: 1, column: 27 }
|
||||
}
|
||||
},
|
||||
` })`
|
||||
|
|
@ -539,7 +512,7 @@ describe('compiler: expression transform', () => {
|
|||
`<div @click="() => {
|
||||
for (const x in list) {
|
||||
log(x)
|
||||
}
|
||||
}
|
||||
}"/>`
|
||||
)
|
||||
expect(code).not.toMatch(`_ctx.x`)
|
||||
|
|
@ -551,7 +524,7 @@ describe('compiler: expression transform', () => {
|
|||
`<div @click="() => {
|
||||
for (const x of list) {
|
||||
log(x)
|
||||
}
|
||||
}
|
||||
}"/>`
|
||||
)
|
||||
expect(code).not.toMatch(`_ctx.x`)
|
||||
|
|
@ -563,7 +536,7 @@ describe('compiler: expression transform', () => {
|
|||
`<div @click="() => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
log(i)
|
||||
}
|
||||
}
|
||||
}"/>`
|
||||
)
|
||||
expect(code).not.toMatch(`_ctx.i`)
|
||||
|
|
|
|||
|
|
@ -376,7 +376,6 @@ describe('compiler: transform <slot> outlets', () => {
|
|||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||
loc: {
|
||||
source: `v-foo`,
|
||||
start: {
|
||||
offset: index,
|
||||
line: 1,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,44 @@ describe('compiler: transform v-bind', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('no expression', () => {
|
||||
const node = parseWithVBind(`<div v-bind:id />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `id`,
|
||||
isStatic: true,
|
||||
loc: {
|
||||
start: { line: 1, column: 13, offset: 12 },
|
||||
end: { line: 1, column: 15, offset: 14 }
|
||||
}
|
||||
},
|
||||
value: {
|
||||
content: `id`,
|
||||
isStatic: false,
|
||||
loc: {
|
||||
start: { line: 1, column: 13, offset: 12 },
|
||||
end: { line: 1, column: 15, offset: 14 }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('no expression (shorthand)', () => {
|
||||
const node = parseWithVBind(`<div :id />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `id`,
|
||||
isStatic: true
|
||||
},
|
||||
value: {
|
||||
content: `id`,
|
||||
isStatic: false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('dynamic arg', () => {
|
||||
const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
|
||||
const props = (node.codegenNode as VNodeCall).props as CallExpression
|
||||
|
|
@ -98,9 +136,9 @@ describe('compiler: transform v-bind', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('should error if no expression', () => {
|
||||
test('should error if empty expression', () => {
|
||||
const onError = vi.fn()
|
||||
const node = parseWithVBind(`<div v-bind:arg />`, { onError })
|
||||
const node = parseWithVBind(`<div v-bind:arg="" />`, { onError })
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
|
||||
|
|
@ -111,7 +149,7 @@ describe('compiler: transform v-bind', () => {
|
|||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 16
|
||||
column: 19
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -142,6 +180,21 @@ describe('compiler: transform v-bind', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('.camel modifier w/ no expression', () => {
|
||||
const node = parseWithVBind(`<div v-bind:foo-bar.camel />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `fooBar`,
|
||||
isStatic: true
|
||||
},
|
||||
value: {
|
||||
content: `fooBar`,
|
||||
isStatic: false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('.camel modifier w/ dynamic arg', () => {
|
||||
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
|
||||
const props = (node.codegenNode as VNodeCall).props as CallExpression
|
||||
|
|
@ -219,6 +272,21 @@ describe('compiler: transform v-bind', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('.prop modifier w/ no expression', () => {
|
||||
const node = parseWithVBind(`<div v-bind:fooBar.prop />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `.fooBar`,
|
||||
isStatic: true
|
||||
},
|
||||
value: {
|
||||
content: `fooBar`,
|
||||
isStatic: false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('.prop modifier w/ dynamic arg', () => {
|
||||
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
|
||||
const props = (node.codegenNode as VNodeCall).props as CallExpression
|
||||
|
|
@ -296,6 +364,21 @@ describe('compiler: transform v-bind', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('.prop modifier (shortband) w/ no expression', () => {
|
||||
const node = parseWithVBind(`<div .fooBar />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `.fooBar`,
|
||||
isStatic: true
|
||||
},
|
||||
value: {
|
||||
content: `fooBar`,
|
||||
isStatic: false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('.attr modifier', () => {
|
||||
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
|
|
@ -310,4 +393,19 @@ describe('compiler: transform v-bind', () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('.attr modifier w/ no expression', () => {
|
||||
const node = parseWithVBind(`<div v-bind:foo-bar.attr />`)
|
||||
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
|
||||
expect(props.properties[0]).toMatchObject({
|
||||
key: {
|
||||
content: `^foo-bar`,
|
||||
isStatic: true
|
||||
},
|
||||
value: {
|
||||
content: `fooBar`,
|
||||
isStatic: false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { baseParse as parse } from '../../src/parse'
|
||||
import { baseParse as parse } from '../../src/parser'
|
||||
import { transform } from '../../src/transform'
|
||||
import { transformIf } from '../../src/transforms/vIf'
|
||||
import { transformFor } from '../../src/transforms/vFor'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { baseParse as parse } from '../../src/parse'
|
||||
import { baseParse as parse } from '../../src/parser'
|
||||
import { transform } from '../../src/transform'
|
||||
import { transformIf } from '../../src/transforms/vIf'
|
||||
import { transformElement } from '../../src/transforms/transformElement'
|
||||
|
|
|
|||
|
|
@ -850,7 +850,6 @@ describe('compiler: transform component slots', () => {
|
|||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
|
||||
loc: {
|
||||
source: `bar`,
|
||||
start: {
|
||||
offset: index,
|
||||
line: 1,
|
||||
|
|
@ -873,7 +872,6 @@ describe('compiler: transform component slots', () => {
|
|||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES,
|
||||
loc: {
|
||||
source: `#foo`,
|
||||
start: {
|
||||
offset: index,
|
||||
line: 1,
|
||||
|
|
@ -896,7 +894,6 @@ describe('compiler: transform component slots', () => {
|
|||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE,
|
||||
loc: {
|
||||
source: `#foo`,
|
||||
start: {
|
||||
offset: index,
|
||||
line: 1,
|
||||
|
|
@ -919,7 +916,6 @@ describe('compiler: transform component slots', () => {
|
|||
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||
code: ErrorCodes.X_V_SLOT_MISPLACED,
|
||||
loc: {
|
||||
source: `v-slot`,
|
||||
start: {
|
||||
offset: index,
|
||||
line: 1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { TransformContext } from '../src'
|
||||
import { Position } from '../src/ast'
|
||||
import {
|
||||
getInnerRange,
|
||||
advancePositionWithClone,
|
||||
isMemberExpressionNode,
|
||||
isMemberExpressionBrowser,
|
||||
|
|
@ -41,32 +40,6 @@ describe('advancePositionWithClone', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('getInnerRange', () => {
|
||||
const loc1 = {
|
||||
source: 'foo\nbar\nbaz',
|
||||
start: p(1, 1, 0),
|
||||
end: p(3, 3, 11)
|
||||
}
|
||||
|
||||
test('at start', () => {
|
||||
const loc2 = getInnerRange(loc1, 0, 4)
|
||||
expect(loc2.start).toEqual(loc1.start)
|
||||
expect(loc2.end.column).toBe(1)
|
||||
expect(loc2.end.line).toBe(2)
|
||||
expect(loc2.end.offset).toBe(4)
|
||||
})
|
||||
|
||||
test('in between', () => {
|
||||
const loc2 = getInnerRange(loc1, 4, 3)
|
||||
expect(loc2.start.column).toBe(1)
|
||||
expect(loc2.start.line).toBe(2)
|
||||
expect(loc2.start.offset).toBe(4)
|
||||
expect(loc2.end.column).toBe(4)
|
||||
expect(loc2.end.line).toBe(2)
|
||||
expect(loc2.end.offset).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMemberExpression', () => {
|
||||
function commonAssertions(fn: (str: string) => boolean) {
|
||||
// should work
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-core",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0-alpha.3",
|
||||
"description": "@vue/compiler-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-core.esm-bundler.js",
|
||||
|
|
@ -32,12 +32,13 @@
|
|||
},
|
||||
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-core#readme",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.3",
|
||||
"@babel/parser": "^7.23.4",
|
||||
"@vue/shared": "workspace:*",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.23.3"
|
||||
"@babel/types": "^7.23.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { isString } from '@vue/shared'
|
||||
import { ForParseResult } from './transforms/vFor'
|
||||
import {
|
||||
RENDER_SLOT,
|
||||
CREATE_SLOTS,
|
||||
|
|
@ -17,15 +16,16 @@ import { PropsExpression } from './transforms/transformElement'
|
|||
import { ImportItem, TransformContext } from './transform'
|
||||
|
||||
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
||||
// More namespaces like SVG and MathML are declared by platform specific
|
||||
// compilers.
|
||||
// More namespaces can be declared by platform specific compilers.
|
||||
export type Namespace = number
|
||||
|
||||
export const enum Namespaces {
|
||||
HTML
|
||||
export enum Namespaces {
|
||||
HTML,
|
||||
SVG,
|
||||
MATH_ML
|
||||
}
|
||||
|
||||
export const enum NodeTypes {
|
||||
export enum NodeTypes {
|
||||
ROOT,
|
||||
ELEMENT,
|
||||
TEXT,
|
||||
|
|
@ -59,7 +59,7 @@ export const enum NodeTypes {
|
|||
JS_RETURN_STATEMENT
|
||||
}
|
||||
|
||||
export const enum ElementTypes {
|
||||
export enum ElementTypes {
|
||||
ELEMENT,
|
||||
COMPONENT,
|
||||
SLOT,
|
||||
|
|
@ -102,6 +102,7 @@ export type TemplateChildNode =
|
|||
|
||||
export interface RootNode extends Node {
|
||||
type: NodeTypes.ROOT
|
||||
source: string
|
||||
children: TemplateChildNode[]
|
||||
helpers: Set<symbol>
|
||||
components: string[]
|
||||
|
|
@ -112,6 +113,7 @@ export interface RootNode extends Node {
|
|||
temps: number
|
||||
ssrHelpers?: symbol[]
|
||||
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
|
||||
transformed?: boolean
|
||||
|
||||
// v2 compat only
|
||||
filters?: string[]
|
||||
|
|
@ -128,9 +130,10 @@ export interface BaseElementNode extends Node {
|
|||
ns: Namespace
|
||||
tag: string
|
||||
tagType: ElementTypes
|
||||
isSelfClosing: boolean
|
||||
props: Array<AttributeNode | DirectiveNode>
|
||||
children: TemplateChildNode[]
|
||||
isSelfClosing?: boolean
|
||||
innerLoc?: SourceLocation // only for SFC root level elements
|
||||
}
|
||||
|
||||
export interface PlainElementNode extends BaseElementNode {
|
||||
|
|
@ -182,19 +185,28 @@ export interface CommentNode extends Node {
|
|||
export interface AttributeNode extends Node {
|
||||
type: NodeTypes.ATTRIBUTE
|
||||
name: string
|
||||
nameLoc: SourceLocation
|
||||
value: TextNode | undefined
|
||||
}
|
||||
|
||||
export interface DirectiveNode extends Node {
|
||||
type: NodeTypes.DIRECTIVE
|
||||
/**
|
||||
* the normalized name without prefix or shorthands, e.g. "bind", "on"
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* the raw attribute name, preserving shorthand, and including arg & modifiers
|
||||
* this is only used during parse.
|
||||
*/
|
||||
rawName?: string
|
||||
exp: ExpressionNode | undefined
|
||||
arg: ExpressionNode | undefined
|
||||
modifiers: string[]
|
||||
/**
|
||||
* optional property to cache the expression parse result for v-for
|
||||
*/
|
||||
parseResult?: ForParseResult
|
||||
forParseResult?: ForParseResult
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -202,7 +214,7 @@ export interface DirectiveNode extends Node {
|
|||
* Higher levels implies lower levels. e.g. a node that can be stringified
|
||||
* can always be hoisted and skipped for patch.
|
||||
*/
|
||||
export const enum ConstantTypes {
|
||||
export enum ConstantTypes {
|
||||
NOT_CONSTANT = 0,
|
||||
CAN_SKIP_PATCH,
|
||||
CAN_HOIST,
|
||||
|
|
@ -276,6 +288,14 @@ export interface ForNode extends Node {
|
|||
codegenNode?: ForCodegenNode
|
||||
}
|
||||
|
||||
export interface ForParseResult {
|
||||
source: ExpressionNode
|
||||
value: ExpressionNode | undefined
|
||||
key: ExpressionNode | undefined
|
||||
index: ExpressionNode | undefined
|
||||
finalized: boolean
|
||||
}
|
||||
|
||||
export interface TextCallNode extends Node {
|
||||
type: NodeTypes.TEXT_CALL
|
||||
content: TextNode | InterpolationNode | CompoundExpressionNode
|
||||
|
|
@ -547,17 +567,18 @@ export interface ForIteratorExpression extends FunctionExpression {
|
|||
// associated with template nodes, so their source locations are just a stub.
|
||||
// Container types like CompoundExpression also don't need a real location.
|
||||
export const locStub: SourceLocation = {
|
||||
source: '',
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 }
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
source: ''
|
||||
}
|
||||
|
||||
export function createRoot(
|
||||
children: TemplateChildNode[],
|
||||
loc = locStub
|
||||
source = ''
|
||||
): RootNode {
|
||||
return {
|
||||
type: NodeTypes.ROOT,
|
||||
source,
|
||||
children,
|
||||
helpers: new Set(),
|
||||
components: [],
|
||||
|
|
@ -567,7 +588,7 @@ export function createRoot(
|
|||
cached: 0,
|
||||
temps: 0,
|
||||
codegenNode: undefined,
|
||||
loc
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@ export interface CodegenResult {
|
|||
map?: RawSourceMap
|
||||
}
|
||||
|
||||
enum NewlineType {
|
||||
Start = 0,
|
||||
End = -1,
|
||||
None = -2,
|
||||
Unknown = -3
|
||||
}
|
||||
|
||||
export interface CodegenContext
|
||||
extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
|
||||
source: string
|
||||
|
|
@ -80,7 +87,7 @@ export interface CodegenContext
|
|||
pure: boolean
|
||||
map?: SourceMapGenerator
|
||||
helper(key: symbol): string
|
||||
push(code: string, node?: CodegenNode): void
|
||||
push(code: string, newlineIndex?: number, node?: CodegenNode): void
|
||||
indent(): void
|
||||
deindent(withoutNewLine?: boolean): void
|
||||
newline(): void
|
||||
|
|
@ -116,7 +123,7 @@ function createCodegenContext(
|
|||
ssr,
|
||||
isTS,
|
||||
inSSR,
|
||||
source: ast.loc.source,
|
||||
source: ast.source,
|
||||
code: ``,
|
||||
column: 1,
|
||||
line: 1,
|
||||
|
|
@ -127,7 +134,7 @@ function createCodegenContext(
|
|||
helper(key) {
|
||||
return `_${helperNameMap[key]}`
|
||||
},
|
||||
push(code, node) {
|
||||
push(code, newlineIndex = NewlineType.None, node) {
|
||||
context.code += code
|
||||
if (!__BROWSER__ && context.map) {
|
||||
if (node) {
|
||||
|
|
@ -140,7 +147,41 @@ function createCodegenContext(
|
|||
}
|
||||
addMapping(node.loc.start, name)
|
||||
}
|
||||
advancePositionWithMutation(context, code)
|
||||
if (newlineIndex === NewlineType.Unknown) {
|
||||
// multiple newlines, full iteration
|
||||
advancePositionWithMutation(context, code)
|
||||
} else {
|
||||
// fast paths
|
||||
context.offset += code.length
|
||||
if (newlineIndex === NewlineType.None) {
|
||||
// no newlines; fast path to avoid newline detection
|
||||
if (__TEST__ && code.includes('\n')) {
|
||||
throw new Error(
|
||||
`CodegenContext.push() called newlineIndex: none, but contains` +
|
||||
`newlines: ${code.replace(/\n/g, '\\n')}`
|
||||
)
|
||||
}
|
||||
context.column += code.length
|
||||
} else {
|
||||
// single newline at known index
|
||||
if (newlineIndex === NewlineType.End) {
|
||||
newlineIndex = code.length - 1
|
||||
}
|
||||
if (
|
||||
__TEST__ &&
|
||||
(code.charAt(newlineIndex) !== '\n' ||
|
||||
code.slice(0, newlineIndex).includes('\n') ||
|
||||
code.slice(newlineIndex + 1).includes('\n'))
|
||||
) {
|
||||
throw new Error(
|
||||
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
|
||||
`but does not conform: ${code.replace(/\n/g, '\\n')}`
|
||||
)
|
||||
}
|
||||
context.line++
|
||||
context.column = code.length - newlineIndex
|
||||
}
|
||||
}
|
||||
if (node && node.loc !== locStub) {
|
||||
addMapping(node.loc.end)
|
||||
}
|
||||
|
|
@ -162,28 +203,31 @@ function createCodegenContext(
|
|||
}
|
||||
|
||||
function newline(n: number) {
|
||||
context.push('\n' + ` `.repeat(n))
|
||||
context.push('\n' + ` `.repeat(n), NewlineType.Start)
|
||||
}
|
||||
|
||||
function addMapping(loc: Position, name?: string) {
|
||||
context.map!.addMapping({
|
||||
name,
|
||||
source: context.filename,
|
||||
original: {
|
||||
line: loc.line,
|
||||
column: loc.column - 1 // source-map column is 0 based
|
||||
},
|
||||
generated: {
|
||||
line: context.line,
|
||||
column: context.column - 1
|
||||
}
|
||||
function addMapping(loc: Position, name: string | null = null) {
|
||||
// we use the private property to directly add the mapping
|
||||
// because the addMapping() implementation in source-map-js has a bunch of
|
||||
// unnecessary arg and validation checks that are pure overhead in our case.
|
||||
const { _names, _mappings } = context.map!
|
||||
if (name !== null && !_names.has(name)) _names.add(name)
|
||||
_mappings.add({
|
||||
originalLine: loc.line,
|
||||
originalColumn: loc.column - 1, // source-map column is 0 based
|
||||
generatedLine: context.line,
|
||||
generatedColumn: context.column - 1,
|
||||
source: filename,
|
||||
// @ts-ignore it is possible to be null
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
if (!__BROWSER__ && sourceMap) {
|
||||
// lazy require source-map implementation, only in non-browser builds
|
||||
context.map = new SourceMapGenerator()
|
||||
context.map!.setSourceContent(filename, context.source)
|
||||
context.map.setSourceContent(filename, context.source)
|
||||
context.map._sources.add(filename)
|
||||
}
|
||||
|
||||
return context
|
||||
|
|
@ -250,8 +294,10 @@ export function generate(
|
|||
// function mode const declarations should be inside with block
|
||||
// also they should be renamed to avoid collision with user properties
|
||||
if (hasHelpers) {
|
||||
push(`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue`)
|
||||
push(`\n`)
|
||||
push(
|
||||
`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
newline()
|
||||
}
|
||||
}
|
||||
|
|
@ -282,7 +328,7 @@ export function generate(
|
|||
}
|
||||
}
|
||||
if (ast.components.length || ast.directives.length || ast.temps) {
|
||||
push(`\n`)
|
||||
push(`\n`, NewlineType.Start)
|
||||
newline()
|
||||
}
|
||||
|
||||
|
|
@ -308,8 +354,7 @@ export function generate(
|
|||
ast,
|
||||
code: context.code,
|
||||
preamble: isSetupInlined ? preambleContext.code : ``,
|
||||
// SourceMapGenerator does have toJSON() method but it's not in the types
|
||||
map: context.map ? (context.map as any).toJSON() : undefined
|
||||
map: context.map ? context.map.toJSON() : undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,11 +379,14 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
|
|||
const helpers = Array.from(ast.helpers)
|
||||
if (helpers.length > 0) {
|
||||
if (!__BROWSER__ && prefixIdentifiers) {
|
||||
push(`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`)
|
||||
push(
|
||||
`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
} else {
|
||||
// "with" mode.
|
||||
// save Vue in a separate variable to avoid collision
|
||||
push(`const _Vue = ${VueBinding}\n`)
|
||||
push(`const _Vue = ${VueBinding}\n`, NewlineType.End)
|
||||
// in "with" mode, helpers are declared inside the with block to avoid
|
||||
// has check cost, but hoists are lifted out of the function - we need
|
||||
// to provide the helper here.
|
||||
|
|
@ -353,7 +401,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
|
|||
.filter(helper => helpers.includes(helper))
|
||||
.map(aliasHelper)
|
||||
.join(', ')
|
||||
push(`const { ${staticHelpers} } = _Vue\n`)
|
||||
push(`const { ${staticHelpers} } = _Vue\n`, NewlineType.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -363,7 +411,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
|
|||
push(
|
||||
`const { ${ast.ssrHelpers
|
||||
.map(aliasHelper)
|
||||
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
|
||||
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
|
|
@ -402,18 +451,21 @@ function genModulePreamble(
|
|||
push(
|
||||
`import { ${helpers
|
||||
.map(s => helperNameMap[s])
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
push(
|
||||
`\n// Binding optimization for webpack code-split\nconst ${helpers
|
||||
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
|
||||
.join(', ')}\n`
|
||||
.join(', ')}\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
} else {
|
||||
push(
|
||||
`import { ${helpers
|
||||
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -422,7 +474,8 @@ function genModulePreamble(
|
|||
push(
|
||||
`import { ${ast.ssrHelpers
|
||||
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
|
||||
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
|
||||
.join(', ')} } from "${ssrRuntimeModuleName}"\n`,
|
||||
NewlineType.End
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +607,7 @@ function genNodeList(
|
|||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (isString(node)) {
|
||||
push(node)
|
||||
push(node, NewlineType.Unknown)
|
||||
} else if (isArray(node)) {
|
||||
genNodeListAsArray(node, context)
|
||||
} else {
|
||||
|
|
@ -573,7 +626,7 @@ function genNodeList(
|
|||
|
||||
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
|
||||
if (isString(node)) {
|
||||
context.push(node)
|
||||
context.push(node, NewlineType.Unknown)
|
||||
return
|
||||
}
|
||||
if (isSymbol(node)) {
|
||||
|
|
@ -671,12 +724,16 @@ function genText(
|
|||
node: TextNode | SimpleExpressionNode,
|
||||
context: CodegenContext
|
||||
) {
|
||||
context.push(JSON.stringify(node.content), node)
|
||||
context.push(JSON.stringify(node.content), NewlineType.Unknown, node)
|
||||
}
|
||||
|
||||
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
|
||||
const { content, isStatic } = node
|
||||
context.push(isStatic ? JSON.stringify(content) : content, node)
|
||||
context.push(
|
||||
isStatic ? JSON.stringify(content) : content,
|
||||
NewlineType.Unknown,
|
||||
node
|
||||
)
|
||||
}
|
||||
|
||||
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
|
||||
|
|
@ -694,7 +751,7 @@ function genCompoundExpression(
|
|||
for (let i = 0; i < node.children!.length; i++) {
|
||||
const child = node.children![i]
|
||||
if (isString(child)) {
|
||||
context.push(child)
|
||||
context.push(child, NewlineType.Unknown)
|
||||
} else {
|
||||
genNode(child, context)
|
||||
}
|
||||
|
|
@ -715,9 +772,9 @@ function genExpressionAsPropertyKey(
|
|||
const text = isSimpleIdentifier(node.content)
|
||||
? node.content
|
||||
: JSON.stringify(node.content)
|
||||
push(text, node)
|
||||
push(text, NewlineType.None, node)
|
||||
} else {
|
||||
push(`[${node.content}]`, node)
|
||||
push(`[${node.content}]`, NewlineType.Unknown, node)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -726,7 +783,11 @@ function genComment(node: CommentNode, context: CodegenContext) {
|
|||
if (pure) {
|
||||
push(PURE_ANNOTATION)
|
||||
}
|
||||
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
|
||||
push(
|
||||
`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`,
|
||||
NewlineType.Unknown,
|
||||
node
|
||||
)
|
||||
}
|
||||
|
||||
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
|
||||
|
|
@ -754,7 +815,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
|
|||
const callHelper: symbol = isBlock
|
||||
? getVNodeBlockHelper(context.inSSR, isComponent)
|
||||
: getVNodeHelper(context.inSSR, isComponent)
|
||||
push(helper(callHelper) + `(`, node)
|
||||
push(helper(callHelper) + `(`, NewlineType.None, node)
|
||||
genNodeList(
|
||||
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
|
||||
context
|
||||
|
|
@ -785,7 +846,7 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
|
|||
if (pure) {
|
||||
push(PURE_ANNOTATION)
|
||||
}
|
||||
push(callee + `(`, node)
|
||||
push(callee + `(`, NewlineType.None, node)
|
||||
genNodeList(node.arguments, context)
|
||||
push(`)`)
|
||||
}
|
||||
|
|
@ -794,7 +855,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
|
|||
const { push, indent, deindent, newline } = context
|
||||
const { properties } = node
|
||||
if (!properties.length) {
|
||||
push(`{}`, node)
|
||||
push(`{}`, NewlineType.None, node)
|
||||
return
|
||||
}
|
||||
const multilines =
|
||||
|
|
@ -834,7 +895,7 @@ function genFunctionExpression(
|
|||
// wrap slot functions with owner context
|
||||
push(`_${helperNameMap[WITH_CTX]}(`)
|
||||
}
|
||||
push(`(`, node)
|
||||
push(`(`, NewlineType.None, node)
|
||||
if (isArray(params)) {
|
||||
genNodeList(params, context)
|
||||
} else if (params) {
|
||||
|
|
@ -934,7 +995,7 @@ function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
|
|||
for (let i = 0; i < l; i++) {
|
||||
const e = node.elements[i]
|
||||
if (isString(e)) {
|
||||
push(e.replace(/(`|\$|\\)/g, '\\$1'))
|
||||
push(e.replace(/(`|\$|\\)/g, '\\$1'), NewlineType.Unknown)
|
||||
} else {
|
||||
push('${')
|
||||
if (multilines) indent()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { SourceLocation } from '../ast'
|
||||
import { CompilerError } from '../errors'
|
||||
import { ParserContext } from '../parse'
|
||||
import { MergedParserOptions } from '../parser'
|
||||
import { TransformContext } from '../transform'
|
||||
|
||||
export type CompilerCompatConfig = Partial<
|
||||
|
|
@ -13,10 +13,9 @@ export interface CompilerCompatOptions {
|
|||
compatConfig?: CompilerCompatConfig
|
||||
}
|
||||
|
||||
export const enum CompilerDeprecationTypes {
|
||||
export enum CompilerDeprecationTypes {
|
||||
COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
|
||||
COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC',
|
||||
COMPILER_V_BIND_PROP = 'COMPILER_V_BIND_PROP',
|
||||
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
|
||||
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
|
||||
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
|
||||
|
|
@ -47,12 +46,6 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
|
|||
link: `https://v3-migration.vuejs.org/breaking-changes/v-model.html`
|
||||
},
|
||||
|
||||
[CompilerDeprecationTypes.COMPILER_V_BIND_PROP]: {
|
||||
message:
|
||||
`.prop modifier for v-bind has been removed and no longer necessary. ` +
|
||||
`Vue 3 will automatically set a binding as DOM property when appropriate.`
|
||||
},
|
||||
|
||||
[CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: {
|
||||
message:
|
||||
`v-bind="obj" usage is now order sensitive and behaves like JavaScript ` +
|
||||
|
|
@ -100,12 +93,9 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
|
|||
|
||||
function getCompatValue(
|
||||
key: CompilerDeprecationTypes | 'MODE',
|
||||
context: ParserContext | TransformContext
|
||||
{ compatConfig }: MergedParserOptions | TransformContext
|
||||
) {
|
||||
const config = (context as ParserContext).options
|
||||
? (context as ParserContext).options.compatConfig
|
||||
: (context as TransformContext).compatConfig
|
||||
const value = config && config[key]
|
||||
const value = compatConfig && compatConfig[key]
|
||||
if (key === 'MODE') {
|
||||
return value || 3 // compiler defaults to v3 behavior
|
||||
} else {
|
||||
|
|
@ -115,7 +105,7 @@ function getCompatValue(
|
|||
|
||||
export function isCompatEnabled(
|
||||
key: CompilerDeprecationTypes,
|
||||
context: ParserContext | TransformContext
|
||||
context: MergedParserOptions | TransformContext
|
||||
) {
|
||||
const mode = getCompatValue('MODE', context)
|
||||
const value = getCompatValue(key, context)
|
||||
|
|
@ -126,7 +116,7 @@ export function isCompatEnabled(
|
|||
|
||||
export function checkCompatEnabled(
|
||||
key: CompilerDeprecationTypes,
|
||||
context: ParserContext | TransformContext,
|
||||
context: MergedParserOptions | TransformContext,
|
||||
loc: SourceLocation | null,
|
||||
...args: any[]
|
||||
): boolean {
|
||||
|
|
@ -139,7 +129,7 @@ export function checkCompatEnabled(
|
|||
|
||||
export function warnDeprecation(
|
||||
key: CompilerDeprecationTypes,
|
||||
context: ParserContext | TransformContext,
|
||||
context: MergedParserOptions | TransformContext,
|
||||
loc: SourceLocation | null,
|
||||
...args: any[]
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CompilerOptions } from './options'
|
||||
import { baseParse } from './parse'
|
||||
import { baseParse } from './parser'
|
||||
import { transform, NodeTransform, DirectiveTransform } from './transform'
|
||||
import { generate, CodegenResult } from './codegen'
|
||||
import { RootNode } from './ast'
|
||||
|
|
@ -59,7 +59,7 @@ export function getBaseTransformPreset(
|
|||
// we name it `baseCompile` so that higher order compilers like
|
||||
// @vue/compiler-dom can export `compile` while re-exporting everything else.
|
||||
export function baseCompile(
|
||||
template: string | RootNode,
|
||||
source: string | RootNode,
|
||||
options: CompilerOptions = {}
|
||||
): CodegenResult {
|
||||
const onError = options.onError || defaultOnError
|
||||
|
|
@ -82,7 +82,7 @@ export function baseCompile(
|
|||
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
|
||||
}
|
||||
|
||||
const ast = isString(template) ? baseParse(template, options) : template
|
||||
const ast = isString(source) ? baseParse(source, options) : source
|
||||
const [nodeTransforms, directiveTransforms] =
|
||||
getBaseTransformPreset(prefixIdentifiers)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ export function createCompilerError<T extends number>(
|
|||
const msg =
|
||||
__DEV__ || !__BROWSER__
|
||||
? (messages || errorMessages)[code] + (additionalMessage || ``)
|
||||
: code
|
||||
: `https://vuejs.org/errors/#compiler-${code}`
|
||||
const error = new SyntaxError(String(msg)) as InferCompilerError<T>
|
||||
error.code = code
|
||||
error.loc = loc
|
||||
return error
|
||||
}
|
||||
|
||||
export const enum ErrorCodes {
|
||||
export enum ErrorCodes {
|
||||
// parse errors
|
||||
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
|
||||
CDATA_IN_HTML_CONTENT,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export {
|
|||
type BindingMetadata,
|
||||
BindingTypes
|
||||
} from './options'
|
||||
export { baseParse, TextModes } from './parse'
|
||||
export { baseParse } from './parser'
|
||||
export {
|
||||
transform,
|
||||
type TransformContext,
|
||||
|
|
@ -24,6 +24,7 @@ export {
|
|||
export { generate, type CodegenContext, type CodegenResult } from './codegen'
|
||||
export {
|
||||
ErrorCodes,
|
||||
errorMessages,
|
||||
createCompilerError,
|
||||
type CoreCompilerError,
|
||||
type CompilerError
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
|
||||
import { TextModes } from './parse'
|
||||
import {
|
||||
ElementNode,
|
||||
Namespace,
|
||||
TemplateChildNode,
|
||||
ParentNode,
|
||||
Namespaces
|
||||
} from './ast'
|
||||
import { CompilerError } from './errors'
|
||||
import {
|
||||
NodeTransform,
|
||||
|
|
@ -17,6 +22,24 @@ export interface ErrorHandlingOptions {
|
|||
export interface ParserOptions
|
||||
extends ErrorHandlingOptions,
|
||||
CompilerCompatOptions {
|
||||
/**
|
||||
* Base mode is platform agnostic and only parses HTML-like template syntax,
|
||||
* treating all tags the same way. Specific tag parsing behavior can be
|
||||
* configured by higher-level compilers.
|
||||
*
|
||||
* HTML mode adds additional logic for handling special parsing behavior in
|
||||
* `<script>`, `<style>`,`<title>` and `<textarea>`.
|
||||
* The logic is handled inside compiler-core for efficiency.
|
||||
*
|
||||
* SFC mode treats content of all root-level tags except `<template>` as plain
|
||||
* text.
|
||||
*/
|
||||
parseMode?: 'base' | 'html' | 'sfc'
|
||||
/**
|
||||
* Specify the root namespace to use when parsing a template.
|
||||
* Defaults to `Namespaces.HTML` (0).
|
||||
*/
|
||||
ns?: Namespaces
|
||||
/**
|
||||
* e.g. platform native elements, e.g. `<div>` for browsers
|
||||
*/
|
||||
|
|
@ -40,14 +63,11 @@ export interface ParserOptions
|
|||
/**
|
||||
* Get tag namespace
|
||||
*/
|
||||
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
|
||||
/**
|
||||
* Get text parsing mode for this element
|
||||
*/
|
||||
getTextMode?: (
|
||||
node: ElementNode,
|
||||
parent: ElementNode | undefined
|
||||
) => TextModes
|
||||
getNamespace?: (
|
||||
tag: string,
|
||||
parent: ElementNode | undefined,
|
||||
rootNamespace: Namespace
|
||||
) => Namespace
|
||||
/**
|
||||
* @default ['{{', '}}']
|
||||
*/
|
||||
|
|
@ -57,7 +77,8 @@ export interface ParserOptions
|
|||
*/
|
||||
whitespace?: 'preserve' | 'condense'
|
||||
/**
|
||||
* Only needed for DOM compilers
|
||||
* Only used for DOM compilers that runs in the browser.
|
||||
* In non-browser builds, this option is ignored.
|
||||
*/
|
||||
decodeEntities?: (rawText: string, asAttr: boolean) => string
|
||||
/**
|
||||
|
|
@ -73,7 +94,7 @@ export type HoistTransform = (
|
|||
parent: ParentNode
|
||||
) => void
|
||||
|
||||
export const enum BindingTypes {
|
||||
export enum BindingTypes {
|
||||
/**
|
||||
* returned from data()
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
|
@ -334,6 +334,7 @@ export function transform(root: RootNode, options: TransformOptions) {
|
|||
root.hoists = context.hoists
|
||||
root.temps = context.temps
|
||||
root.cached = context.cached
|
||||
root.transformed = true
|
||||
|
||||
if (__COMPAT__) {
|
||||
root.filters = [...context.filters!]
|
||||
|
|
|
|||
|
|
@ -49,12 +49,10 @@ import {
|
|||
GUARD_REACTIVE_PROPS
|
||||
} from '../runtimeHelpers'
|
||||
import {
|
||||
getInnerRange,
|
||||
toValidAssetId,
|
||||
findProp,
|
||||
isCoreComponent,
|
||||
isStaticArgOf,
|
||||
findDir,
|
||||
isStaticExp
|
||||
} from '../utils'
|
||||
import { buildSlots } from './vSlot'
|
||||
|
|
@ -285,19 +283,6 @@ export function resolveComponentType(
|
|||
}
|
||||
}
|
||||
|
||||
// 1.5 v-is (TODO: remove in 3.4)
|
||||
const isDir = !isExplicitDynamic && findDir(node, 'is')
|
||||
if (isDir && isDir.exp) {
|
||||
if (__DEV__) {
|
||||
context.onWarn(
|
||||
createCompilerError(ErrorCodes.DEPRECATION_V_IS, isDir.loc)
|
||||
)
|
||||
}
|
||||
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
|
||||
isDir.exp
|
||||
])
|
||||
}
|
||||
|
||||
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
|
||||
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
|
||||
if (builtIn) {
|
||||
|
|
@ -489,7 +474,7 @@ export function buildProps(
|
|||
// static attribute
|
||||
const prop = props[i]
|
||||
if (prop.type === NodeTypes.ATTRIBUTE) {
|
||||
const { loc, name, value } = prop
|
||||
const { loc, name, nameLoc, value } = prop
|
||||
let isStatic = true
|
||||
if (name === 'ref') {
|
||||
hasRef = true
|
||||
|
|
@ -536,11 +521,7 @@ export function buildProps(
|
|||
}
|
||||
properties.push(
|
||||
createObjectProperty(
|
||||
createSimpleExpression(
|
||||
name,
|
||||
true,
|
||||
getInnerRange(loc, 0, name.length)
|
||||
),
|
||||
createSimpleExpression(name, true, nameLoc),
|
||||
createSimpleExpression(
|
||||
value ? value.content : '',
|
||||
isStatic,
|
||||
|
|
|
|||
|
|
@ -336,9 +336,9 @@ export function processExpression(
|
|||
id.name,
|
||||
false,
|
||||
{
|
||||
source,
|
||||
start: advancePositionWithClone(node.loc.start, source, start),
|
||||
end: advancePositionWithClone(node.loc.start, source, end)
|
||||
end: advancePositionWithClone(node.loc.start, source, end),
|
||||
source
|
||||
},
|
||||
id.isConstant ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,14 +8,25 @@ import {
|
|||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { camelize } from '@vue/shared'
|
||||
import { CAMELIZE } from '../runtimeHelpers'
|
||||
import { processExpression } from './transformExpression'
|
||||
|
||||
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting
|
||||
// codegen for the entire props object. This transform here is only for v-bind
|
||||
// *with* args.
|
||||
export const transformBind: DirectiveTransform = (dir, _node, context) => {
|
||||
const { exp, modifiers, loc } = dir
|
||||
const { modifiers, loc } = dir
|
||||
const arg = dir.arg!
|
||||
|
||||
// :arg is replaced by :arg="arg"
|
||||
let { exp } = dir
|
||||
if (!exp && arg.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||
const propName = camelize(arg.content)
|
||||
exp = dir.exp = createSimpleExpression(propName, false, arg.loc)
|
||||
if (!__BROWSER__) {
|
||||
exp = dir.exp = processExpression(exp, context)
|
||||
}
|
||||
}
|
||||
|
||||
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
|
||||
arg.children.unshift(`(`)
|
||||
arg.children.push(`) || ""`)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
NodeTypes,
|
||||
ExpressionNode,
|
||||
createSimpleExpression,
|
||||
SourceLocation,
|
||||
SimpleExpressionNode,
|
||||
createCallExpression,
|
||||
createFunctionExpression,
|
||||
|
|
@ -28,17 +27,16 @@ import {
|
|||
createBlockStatement,
|
||||
createCompoundExpression,
|
||||
getVNodeBlockHelper,
|
||||
getVNodeHelper
|
||||
getVNodeHelper,
|
||||
ForParseResult
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
getInnerRange,
|
||||
findProp,
|
||||
isTemplateNode,
|
||||
isSlotOutlet,
|
||||
injectProp,
|
||||
findDir,
|
||||
forAliasRE
|
||||
findDir
|
||||
} from '../utils'
|
||||
import {
|
||||
RENDER_LIST,
|
||||
|
|
@ -256,12 +254,7 @@ export function processFor(
|
|||
return
|
||||
}
|
||||
|
||||
const parseResult = parseForExpression(
|
||||
// can only be simple expression because vFor transform is applied
|
||||
// before expression transform.
|
||||
dir.exp as SimpleExpressionNode,
|
||||
context
|
||||
)
|
||||
const parseResult = dir.forParseResult
|
||||
|
||||
if (!parseResult) {
|
||||
context.onError(
|
||||
|
|
@ -270,6 +263,8 @@ export function processFor(
|
|||
return
|
||||
}
|
||||
|
||||
finalizeForParseResult(parseResult, context)
|
||||
|
||||
const { addIdentifiers, removeIdentifiers, scopes } = context
|
||||
const { source, value, key, index } = parseResult
|
||||
|
||||
|
|
@ -309,107 +304,56 @@ export function processFor(
|
|||
}
|
||||
}
|
||||
|
||||
// This regex doesn't cover the case if key or index aliases have destructuring,
|
||||
// but those do not make sense in the first place, so this works in practice.
|
||||
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
||||
const stripParensRE = /^\(|\)$/g
|
||||
|
||||
export interface ForParseResult {
|
||||
source: ExpressionNode
|
||||
value: ExpressionNode | undefined
|
||||
key: ExpressionNode | undefined
|
||||
index: ExpressionNode | undefined
|
||||
}
|
||||
|
||||
export function parseForExpression(
|
||||
input: SimpleExpressionNode,
|
||||
export function finalizeForParseResult(
|
||||
result: ForParseResult,
|
||||
context: TransformContext
|
||||
): ForParseResult | undefined {
|
||||
const loc = input.loc
|
||||
const exp = input.content
|
||||
const inMatch = exp.match(forAliasRE)
|
||||
if (!inMatch) return
|
||||
) {
|
||||
if (result.finalized) return
|
||||
|
||||
const [, LHS, RHS] = inMatch
|
||||
|
||||
const result: ForParseResult = {
|
||||
source: createAliasExpression(
|
||||
loc,
|
||||
RHS.trim(),
|
||||
exp.indexOf(RHS, LHS.length)
|
||||
),
|
||||
value: undefined,
|
||||
key: undefined,
|
||||
index: undefined
|
||||
}
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
result.source = processExpression(
|
||||
result.source as SimpleExpressionNode,
|
||||
context
|
||||
)
|
||||
if (result.key) {
|
||||
result.key = processExpression(
|
||||
result.key as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
if (result.index) {
|
||||
result.index = processExpression(
|
||||
result.index as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
if (result.value) {
|
||||
result.value = processExpression(
|
||||
result.value as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
if (__DEV__ && __BROWSER__) {
|
||||
validateBrowserExpression(result.source as SimpleExpressionNode, context)
|
||||
}
|
||||
|
||||
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
|
||||
const trimmedOffset = LHS.indexOf(valueContent)
|
||||
|
||||
const iteratorMatch = valueContent.match(forIteratorRE)
|
||||
if (iteratorMatch) {
|
||||
valueContent = valueContent.replace(forIteratorRE, '').trim()
|
||||
|
||||
const keyContent = iteratorMatch[1].trim()
|
||||
let keyOffset: number | undefined
|
||||
if (keyContent) {
|
||||
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
|
||||
result.key = createAliasExpression(loc, keyContent, keyOffset)
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
result.key = processExpression(result.key, context, true)
|
||||
}
|
||||
if (__DEV__ && __BROWSER__) {
|
||||
validateBrowserExpression(
|
||||
result.key as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
if (result.key) {
|
||||
validateBrowserExpression(
|
||||
result.key as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
if (iteratorMatch[2]) {
|
||||
const indexContent = iteratorMatch[2].trim()
|
||||
|
||||
if (indexContent) {
|
||||
result.index = createAliasExpression(
|
||||
loc,
|
||||
indexContent,
|
||||
exp.indexOf(
|
||||
indexContent,
|
||||
result.key
|
||||
? keyOffset! + keyContent.length
|
||||
: trimmedOffset + valueContent.length
|
||||
)
|
||||
)
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
result.index = processExpression(result.index, context, true)
|
||||
}
|
||||
if (__DEV__ && __BROWSER__) {
|
||||
validateBrowserExpression(
|
||||
result.index as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
if (result.index) {
|
||||
validateBrowserExpression(
|
||||
result.index as SimpleExpressionNode,
|
||||
context,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (valueContent) {
|
||||
result.value = createAliasExpression(loc, valueContent, trimmedOffset)
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
result.value = processExpression(result.value, context, true)
|
||||
}
|
||||
if (__DEV__ && __BROWSER__) {
|
||||
if (result.value) {
|
||||
validateBrowserExpression(
|
||||
result.value as SimpleExpressionNode,
|
||||
context,
|
||||
|
|
@ -417,20 +361,7 @@ export function parseForExpression(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createAliasExpression(
|
||||
range: SourceLocation,
|
||||
content: string,
|
||||
offset: number
|
||||
): SimpleExpressionNode {
|
||||
return createSimpleExpression(
|
||||
content,
|
||||
false,
|
||||
getInnerRange(range, offset, content.length)
|
||||
)
|
||||
result.finalized = true
|
||||
}
|
||||
|
||||
export function createForLoopParams(
|
||||
|
|
|
|||
|
|
@ -30,13 +30,7 @@ import { createCompilerError, ErrorCodes } from '../errors'
|
|||
import { processExpression } from './transformExpression'
|
||||
import { validateBrowserExpression } from '../validateExpression'
|
||||
import { FRAGMENT, CREATE_COMMENT } from '../runtimeHelpers'
|
||||
import {
|
||||
injectProp,
|
||||
findDir,
|
||||
findProp,
|
||||
isBuiltInType,
|
||||
getMemoedVNodeCall
|
||||
} from '../utils'
|
||||
import { injectProp, findDir, findProp, getMemoedVNodeCall } from '../utils'
|
||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
|
||||
export const transformIf = createStructuralDirectiveTransform(
|
||||
|
|
@ -165,7 +159,8 @@ export function processIf(
|
|||
!(
|
||||
context.parent &&
|
||||
context.parent.type === NodeTypes.ELEMENT &&
|
||||
isBuiltInType(context.parent.tag, 'transition')
|
||||
(context.parent.tag === 'transition' ||
|
||||
context.parent.tag === 'Transition')
|
||||
)
|
||||
) {
|
||||
branch.children = [...comments, ...branch.children]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
|||
return createTransformProps()
|
||||
}
|
||||
|
||||
// we assume v-model directives are always parsed
|
||||
// (not artificially created by a transform)
|
||||
const rawExp = exp.loc.source
|
||||
const expString =
|
||||
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
SourceLocation,
|
||||
createConditionalExpression,
|
||||
ConditionalExpression,
|
||||
SimpleExpressionNode,
|
||||
FunctionExpression,
|
||||
CallExpression,
|
||||
createCallExpression,
|
||||
|
|
@ -32,7 +31,7 @@ import {
|
|||
isStaticExp
|
||||
} from '../utils'
|
||||
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
|
||||
import { parseForExpression, createForLoopParams } from './vFor'
|
||||
import { createForLoopParams, finalizeForParseResult } from './vFor'
|
||||
import { SlotFlags, slotFlagsText } from '@vue/shared'
|
||||
|
||||
const defaultFallback = createSimpleExpression(`undefined`, false)
|
||||
|
|
@ -78,11 +77,9 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
|
|||
node.props.some(isVSlot) &&
|
||||
(vFor = findDir(node, 'for'))
|
||||
) {
|
||||
const result = (vFor.parseResult = parseForExpression(
|
||||
vFor.exp as SimpleExpressionNode,
|
||||
context
|
||||
))
|
||||
const result = vFor.forParseResult
|
||||
if (result) {
|
||||
finalizeForParseResult(result, context)
|
||||
const { value, key, index } = result
|
||||
const { addIdentifiers, removeIdentifiers } = context
|
||||
value && addIdentifiers(value)
|
||||
|
|
@ -100,7 +97,7 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
|
|||
|
||||
export type SlotFnBuilder = (
|
||||
slotProps: ExpressionNode | undefined,
|
||||
vForExp: ExpressionNode | undefined,
|
||||
vFor: DirectiveNode | undefined,
|
||||
slotChildren: TemplateChildNode[],
|
||||
loc: SourceLocation
|
||||
) => FunctionExpression
|
||||
|
|
@ -203,12 +200,7 @@ export function buildSlots(
|
|||
}
|
||||
|
||||
const vFor = findDir(slotElement, 'for')
|
||||
const slotFunction = buildSlotFn(
|
||||
slotProps,
|
||||
vFor?.exp,
|
||||
slotChildren,
|
||||
slotLoc
|
||||
)
|
||||
const slotFunction = buildSlotFn(slotProps, vFor, slotChildren, slotLoc)
|
||||
|
||||
// check if this slot is conditional (v-if/v-for)
|
||||
let vIf: DirectiveNode | undefined
|
||||
|
|
@ -266,10 +258,9 @@ export function buildSlots(
|
|||
}
|
||||
} else if (vFor) {
|
||||
hasDynamicSlots = true
|
||||
const parseResult =
|
||||
vFor.parseResult ||
|
||||
parseForExpression(vFor.exp as SimpleExpressionNode, context)
|
||||
const parseResult = vFor.forParseResult
|
||||
if (parseResult) {
|
||||
finalizeForParseResult(parseResult, context)
|
||||
// Render the dynamic slots as an array and add it to the createSlot()
|
||||
// args. The runtime knows how to handle it appropriately.
|
||||
dynamicSlots.push(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
SourceLocation,
|
||||
Position,
|
||||
ElementNode,
|
||||
NodeTypes,
|
||||
|
|
@ -37,7 +36,7 @@ import {
|
|||
GUARD_REACTIVE_PROPS,
|
||||
WITH_MEMO
|
||||
} from './runtimeHelpers'
|
||||
import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
|
||||
import { isString, isObject, NOOP } from '@vue/shared'
|
||||
import { PropsExpression } from './transforms/transformElement'
|
||||
import { parseExpression } from '@babel/parser'
|
||||
import { Expression } from '@babel/types'
|
||||
|
|
@ -45,18 +44,20 @@ import { Expression } from '@babel/types'
|
|||
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
||||
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
|
||||
|
||||
export const isBuiltInType = (tag: string, expected: string): boolean =>
|
||||
tag === expected || tag === hyphenate(expected)
|
||||
|
||||
export function isCoreComponent(tag: string): symbol | void {
|
||||
if (isBuiltInType(tag, 'Teleport')) {
|
||||
return TELEPORT
|
||||
} else if (isBuiltInType(tag, 'Suspense')) {
|
||||
return SUSPENSE
|
||||
} else if (isBuiltInType(tag, 'KeepAlive')) {
|
||||
return KEEP_ALIVE
|
||||
} else if (isBuiltInType(tag, 'BaseTransition')) {
|
||||
return BASE_TRANSITION
|
||||
switch (tag) {
|
||||
case 'Teleport':
|
||||
case 'teleport':
|
||||
return TELEPORT
|
||||
case 'Suspense':
|
||||
case 'suspense':
|
||||
return SUSPENSE
|
||||
case 'KeepAlive':
|
||||
case 'keep-alive':
|
||||
return KEEP_ALIVE
|
||||
case 'BaseTransition':
|
||||
case 'base-transition':
|
||||
return BASE_TRANSITION
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ const nonIdentifierRE = /^\d|[^\$\w]/
|
|||
export const isSimpleIdentifier = (name: string): boolean =>
|
||||
!nonIdentifierRE.test(name)
|
||||
|
||||
const enum MemberExpLexState {
|
||||
enum MemberExpLexState {
|
||||
inMemberExp,
|
||||
inBrackets,
|
||||
inParens,
|
||||
|
|
@ -174,38 +175,17 @@ export const isMemberExpression = __BROWSER__
|
|||
? isMemberExpressionBrowser
|
||||
: isMemberExpressionNode
|
||||
|
||||
export function getInnerRange(
|
||||
loc: SourceLocation,
|
||||
offset: number,
|
||||
length: number
|
||||
): SourceLocation {
|
||||
__TEST__ && assert(offset <= loc.source.length)
|
||||
const source = loc.source.slice(offset, offset + length)
|
||||
const newLoc: SourceLocation = {
|
||||
source,
|
||||
start: advancePositionWithClone(loc.start, loc.source, offset),
|
||||
end: loc.end
|
||||
}
|
||||
|
||||
if (length != null) {
|
||||
__TEST__ && assert(offset + length <= loc.source.length)
|
||||
newLoc.end = advancePositionWithClone(
|
||||
loc.start,
|
||||
loc.source,
|
||||
offset + length
|
||||
)
|
||||
}
|
||||
|
||||
return newLoc
|
||||
}
|
||||
|
||||
export function advancePositionWithClone(
|
||||
pos: Position,
|
||||
source: string,
|
||||
numberOfCharacters: number = source.length
|
||||
): Position {
|
||||
return advancePositionWithMutation(
|
||||
extend({}, pos),
|
||||
{
|
||||
offset: pos.offset,
|
||||
line: pos.line,
|
||||
column: pos.column
|
||||
},
|
||||
source,
|
||||
numberOfCharacters
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import {
|
|||
NodeTypes,
|
||||
ElementNode,
|
||||
TextNode,
|
||||
ErrorCodes,
|
||||
ElementTypes,
|
||||
InterpolationNode,
|
||||
AttributeNode,
|
||||
ConstantTypes
|
||||
ConstantTypes,
|
||||
Namespaces
|
||||
} from '@vue/compiler-core'
|
||||
import { parserOptions, DOMNamespaces } from '../src/parserOptions'
|
||||
import { parserOptions } from '../src/parserOptions'
|
||||
|
||||
describe('DOM parser', () => {
|
||||
describe('Text', () => {
|
||||
|
|
@ -32,7 +32,7 @@ describe('DOM parser', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('textarea handles character references', () => {
|
||||
test('textarea handles entities', () => {
|
||||
const ast = parse('<textarea>&</textarea>', parserOptions)
|
||||
const element = ast.children[0] as ElementNode
|
||||
const text = element.children[0] as TextNode
|
||||
|
|
@ -277,11 +277,10 @@ describe('DOM parser', () => {
|
|||
|
||||
expect(element).toStrictEqual({
|
||||
type: NodeTypes.ELEMENT,
|
||||
ns: DOMNamespaces.HTML,
|
||||
ns: Namespaces.HTML,
|
||||
tag: 'img',
|
||||
tagType: ElementTypes.ELEMENT,
|
||||
props: [],
|
||||
isSelfClosing: false,
|
||||
children: [],
|
||||
loc: {
|
||||
start: { offset: 0, line: 1, column: 1 },
|
||||
|
|
@ -316,15 +315,8 @@ describe('DOM parser', () => {
|
|||
|
||||
test('Strict end tag detection for textarea.', () => {
|
||||
const ast = parse(
|
||||
'<textarea>hello</textarea</textarea0></texTArea a="<>">',
|
||||
{
|
||||
...parserOptions,
|
||||
onError: err => {
|
||||
if (err.code !== ErrorCodes.END_TAG_WITH_ATTRIBUTES) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
'<textarea>hello</textarea</textarea0></texTArea>',
|
||||
parserOptions
|
||||
)
|
||||
const element = ast.children[0] as ElementNode
|
||||
const text = element.children[0] as TextNode
|
||||
|
|
@ -347,21 +339,21 @@ describe('DOM parser', () => {
|
|||
const ast = parse('<html>test</html>', parserOptions)
|
||||
const element = ast.children[0] as ElementNode
|
||||
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('SVG namespace', () => {
|
||||
const ast = parse('<svg>test</svg>', parserOptions)
|
||||
const element = ast.children[0] as ElementNode
|
||||
|
||||
expect(element.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(element.ns).toBe(Namespaces.SVG)
|
||||
})
|
||||
|
||||
test('MATH_ML namespace', () => {
|
||||
const ast = parse('<math>test</math>', parserOptions)
|
||||
const element = ast.children[0] as ElementNode
|
||||
|
||||
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(element.ns).toBe(Namespaces.MATH_ML)
|
||||
})
|
||||
|
||||
test('SVG in MATH_ML namespace', () => {
|
||||
|
|
@ -373,8 +365,8 @@ describe('DOM parser', () => {
|
|||
const elementAnnotation = elementMath.children[0] as ElementNode
|
||||
const elementSvg = elementAnnotation.children[0] as ElementNode
|
||||
|
||||
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
|
||||
expect(elementSvg.ns).toBe(Namespaces.SVG)
|
||||
})
|
||||
|
||||
test('html text/html in MATH_ML namespace', () => {
|
||||
|
|
@ -387,8 +379,8 @@ describe('DOM parser', () => {
|
|||
const elementAnnotation = elementMath.children[0] as ElementNode
|
||||
const element = elementAnnotation.children[0] as ElementNode
|
||||
|
||||
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('html application/xhtml+xml in MATH_ML namespace', () => {
|
||||
|
|
@ -400,8 +392,8 @@ describe('DOM parser', () => {
|
|||
const elementAnnotation = elementMath.children[0] as ElementNode
|
||||
const element = elementAnnotation.children[0] as ElementNode
|
||||
|
||||
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('mtext malignmark in MATH_ML namespace', () => {
|
||||
|
|
@ -413,8 +405,8 @@ describe('DOM parser', () => {
|
|||
const elementText = elementMath.children[0] as ElementNode
|
||||
const element = elementText.children[0] as ElementNode
|
||||
|
||||
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
|
||||
expect(element.ns).toBe(Namespaces.MATH_ML)
|
||||
})
|
||||
|
||||
test('mtext and not malignmark tag in MATH_ML namespace', () => {
|
||||
|
|
@ -423,8 +415,8 @@ describe('DOM parser', () => {
|
|||
const elementText = elementMath.children[0] as ElementNode
|
||||
const element = elementText.children[0] as ElementNode
|
||||
|
||||
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('foreignObject tag in SVG namespace', () => {
|
||||
|
|
@ -436,8 +428,8 @@ describe('DOM parser', () => {
|
|||
const elementForeignObject = elementSvg.children[0] as ElementNode
|
||||
const element = elementForeignObject.children[0] as ElementNode
|
||||
|
||||
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementSvg.ns).toBe(Namespaces.SVG)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('desc tag in SVG namespace', () => {
|
||||
|
|
@ -446,8 +438,8 @@ describe('DOM parser', () => {
|
|||
const elementDesc = elementSvg.children[0] as ElementNode
|
||||
const element = elementDesc.children[0] as ElementNode
|
||||
|
||||
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementSvg.ns).toBe(Namespaces.SVG)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('title tag in SVG namespace', () => {
|
||||
|
|
@ -456,8 +448,8 @@ describe('DOM parser', () => {
|
|||
const elementTitle = elementSvg.children[0] as ElementNode
|
||||
const element = elementTitle.children[0] as ElementNode
|
||||
|
||||
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(element.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(elementSvg.ns).toBe(Namespaces.SVG)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('SVG in HTML namespace', () => {
|
||||
|
|
@ -465,8 +457,8 @@ describe('DOM parser', () => {
|
|||
const elementHtml = ast.children[0] as ElementNode
|
||||
const element = elementHtml.children[0] as ElementNode
|
||||
|
||||
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(element.ns).toBe(DOMNamespaces.SVG)
|
||||
expect(elementHtml.ns).toBe(Namespaces.HTML)
|
||||
expect(element.ns).toBe(Namespaces.SVG)
|
||||
})
|
||||
|
||||
test('MATH in HTML namespace', () => {
|
||||
|
|
@ -474,8 +466,35 @@ describe('DOM parser', () => {
|
|||
const elementHtml = ast.children[0] as ElementNode
|
||||
const element = elementHtml.children[0] as ElementNode
|
||||
|
||||
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
|
||||
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
|
||||
expect(elementHtml.ns).toBe(Namespaces.HTML)
|
||||
expect(element.ns).toBe(Namespaces.MATH_ML)
|
||||
})
|
||||
|
||||
test('root ns', () => {
|
||||
const ast = parse('<foreignObject><test/></foreignObject>', {
|
||||
...parserOptions,
|
||||
ns: Namespaces.SVG
|
||||
})
|
||||
const elementForieng = ast.children[0] as ElementNode
|
||||
const element = elementForieng.children[0] as ElementNode
|
||||
|
||||
expect(elementForieng.ns).toBe(Namespaces.SVG)
|
||||
expect(element.ns).toBe(Namespaces.HTML)
|
||||
})
|
||||
|
||||
test('correct XML handling with root ns', () => {
|
||||
// when root ns is an XML namespace, there should be no special content
|
||||
// treatment for <script>, <style>, <textarea> etc.
|
||||
const ast = parse('<script><g/><g/></script>', {
|
||||
...parserOptions,
|
||||
ns: Namespaces.SVG
|
||||
})
|
||||
const elementSvg = ast.children[0] as ElementNode
|
||||
// should parse as nodes instead of text
|
||||
expect(elementSvg.children).toMatchObject([
|
||||
{ type: NodeTypes.ELEMENT, tag: 'g' },
|
||||
{ type: NodeTypes.ELEMENT, tag: 'g' }
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-dom",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0-alpha.3",
|
||||
"description": "@vue/compiler-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-dom.esm-bundler.js",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ export function createDOMCompilerError(
|
|||
) as DOMCompilerError
|
||||
}
|
||||
|
||||
export const enum DOMErrorCodes {
|
||||
export enum DOMErrorCodes {
|
||||
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
|
||||
X_V_HTML_WITH_CHILDREN,
|
||||
X_V_TEXT_NO_EXPRESSION,
|
||||
|
|
@ -36,7 +36,7 @@ export const enum DOMErrorCodes {
|
|||
}
|
||||
|
||||
if (__TEST__) {
|
||||
// esbuild cannot infer const enum increments if first value is from another
|
||||
// esbuild cannot infer enum increments if first value is from another
|
||||
// file, so we have to manually keep them in sync. this check ensures it
|
||||
// errors out if there are collisions.
|
||||
if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
|
|||
}
|
||||
|
||||
export function compile(
|
||||
template: string,
|
||||
src: string | RootNode,
|
||||
options: CompilerOptions = {}
|
||||
): CodegenResult {
|
||||
return baseCompile(
|
||||
template,
|
||||
src,
|
||||
extend({}, parserOptions, options, {
|
||||
nodeTransforms: [
|
||||
// ignore <script> and <tag>
|
||||
|
|
@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
|
|||
|
||||
export * from './runtimeHelpers'
|
||||
export { transformStyle } from './transforms/transformStyle'
|
||||
export { createDOMCompilerError, DOMErrorCodes } from './errors'
|
||||
export {
|
||||
createDOMCompilerError,
|
||||
DOMErrorCodes,
|
||||
DOMErrorMessages
|
||||
} from './errors'
|
||||
export * from '@vue/compiler-core'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,48 +1,30 @@
|
|||
import {
|
||||
TextModes,
|
||||
ParserOptions,
|
||||
ElementNode,
|
||||
NodeTypes,
|
||||
isBuiltInType
|
||||
} from '@vue/compiler-core'
|
||||
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
|
||||
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
|
||||
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
|
||||
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
|
||||
import { decodeHtml } from './decodeHtml'
|
||||
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
|
||||
|
||||
const isRawTextContainer = /*#__PURE__*/ makeMap(
|
||||
'style,iframe,script,noscript',
|
||||
true
|
||||
)
|
||||
|
||||
export const enum DOMNamespaces {
|
||||
HTML = 0 /* Namespaces.HTML */,
|
||||
SVG,
|
||||
MATH_ML
|
||||
}
|
||||
|
||||
export const parserOptions: ParserOptions = {
|
||||
parseMode: 'html',
|
||||
isVoidTag,
|
||||
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
|
||||
isPreTag: tag => tag === 'pre',
|
||||
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : decodeHtml,
|
||||
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
|
||||
|
||||
isBuiltInComponent: (tag: string): symbol | undefined => {
|
||||
if (isBuiltInType(tag, `Transition`)) {
|
||||
isBuiltInComponent: tag => {
|
||||
if (tag === 'Transition' || tag === 'transition') {
|
||||
return TRANSITION
|
||||
} else if (isBuiltInType(tag, `TransitionGroup`)) {
|
||||
} else if (tag === 'TransitionGroup' || tag === 'transition-group') {
|
||||
return TRANSITION_GROUP
|
||||
}
|
||||
},
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
|
||||
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
|
||||
let ns = parent ? parent.ns : DOMNamespaces.HTML
|
||||
|
||||
if (parent && ns === DOMNamespaces.MATH_ML) {
|
||||
getNamespace(tag, parent, rootNamespace) {
|
||||
let ns = parent ? parent.ns : rootNamespace
|
||||
if (parent && ns === Namespaces.MATH_ML) {
|
||||
if (parent.tag === 'annotation-xml') {
|
||||
if (tag === 'svg') {
|
||||
return DOMNamespaces.SVG
|
||||
return Namespaces.SVG
|
||||
}
|
||||
if (
|
||||
parent.props.some(
|
||||
|
|
@ -54,46 +36,33 @@ export const parserOptions: ParserOptions = {
|
|||
a.value.content === 'application/xhtml+xml')
|
||||
)
|
||||
) {
|
||||
ns = DOMNamespaces.HTML
|
||||
ns = Namespaces.HTML
|
||||
}
|
||||
} else if (
|
||||
/^m(?:[ions]|text)$/.test(parent.tag) &&
|
||||
tag !== 'mglyph' &&
|
||||
tag !== 'malignmark'
|
||||
) {
|
||||
ns = DOMNamespaces.HTML
|
||||
ns = Namespaces.HTML
|
||||
}
|
||||
} else if (parent && ns === DOMNamespaces.SVG) {
|
||||
} else if (parent && ns === Namespaces.SVG) {
|
||||
if (
|
||||
parent.tag === 'foreignObject' ||
|
||||
parent.tag === 'desc' ||
|
||||
parent.tag === 'title'
|
||||
) {
|
||||
ns = DOMNamespaces.HTML
|
||||
ns = Namespaces.HTML
|
||||
}
|
||||
}
|
||||
|
||||
if (ns === DOMNamespaces.HTML) {
|
||||
if (ns === Namespaces.HTML) {
|
||||
if (tag === 'svg') {
|
||||
return DOMNamespaces.SVG
|
||||
return Namespaces.SVG
|
||||
}
|
||||
if (tag === 'math') {
|
||||
return DOMNamespaces.MATH_ML
|
||||
return Namespaces.MATH_ML
|
||||
}
|
||||
}
|
||||
return ns
|
||||
},
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
|
||||
getTextMode({ tag, ns }: ElementNode): TextModes {
|
||||
if (ns === DOMNamespaces.HTML) {
|
||||
if (tag === 'textarea' || tag === 'title') {
|
||||
return TextModes.RCDATA
|
||||
}
|
||||
if (isRawTextContainer(tag)) {
|
||||
return TextModes.RAWTEXT
|
||||
}
|
||||
}
|
||||
return TextModes.DATA
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const transformTransition: NodeTransform = (node, context) => {
|
|||
node.props.push({
|
||||
type: NodeTypes.ATTRIBUTE,
|
||||
name: 'persisted',
|
||||
nameLoc: node.loc,
|
||||
value: undefined,
|
||||
loc: node.loc
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
PlainElementNode,
|
||||
JSChildNode,
|
||||
TextCallNode,
|
||||
ConstantTypes
|
||||
ConstantTypes,
|
||||
Namespaces
|
||||
} from '@vue/compiler-core'
|
||||
import {
|
||||
isVoidTag,
|
||||
|
|
@ -31,9 +32,8 @@ import {
|
|||
isKnownSvgAttr,
|
||||
isBooleanAttr
|
||||
} from '@vue/shared'
|
||||
import { DOMNamespaces } from '../parserOptions'
|
||||
|
||||
export const enum StringifyThresholds {
|
||||
export enum StringifyThresholds {
|
||||
ELEMENT_WITH_BINDING_COUNT = 5,
|
||||
NODE_COUNT = 20
|
||||
}
|
||||
|
|
@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
|
|||
node.codegenNode.hoisted
|
||||
|
||||
const dataAriaRE = /^(data|aria)-/
|
||||
const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
|
||||
const isStringifiableAttr = (name: string, ns: Namespaces) => {
|
||||
return (
|
||||
(ns === DOMNamespaces.HTML
|
||||
(ns === Namespaces.HTML
|
||||
? isKnownHtmlAttr(name)
|
||||
: ns === DOMNamespaces.SVG
|
||||
: ns === Namespaces.SVG
|
||||
? isKnownSvgAttr(name)
|
||||
: false) || dataAriaRE.test(name)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ return { a }
|
|||
`;
|
||||
|
||||
exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (call) 1`] = `
|
||||
"import { defineComponent } from 'vue'
|
||||
"
|
||||
import { defineComponent } from 'vue'
|
||||
const __default__ = defineComponent({
|
||||
name: 'Baz'
|
||||
})
|
||||
|
|
@ -30,7 +31,8 @@ return { a, defineComponent }
|
|||
`;
|
||||
|
||||
exports[`SFC analyze <script> bindings > auto name inference > do not overwrite manual name (object) 1`] = `
|
||||
"const __default__ = {
|
||||
"
|
||||
const __default__ = {
|
||||
name: 'Baz'
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +47,8 @@ return { a }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > export call expression as default 1`] = `
|
||||
"function fn() {
|
||||
"
|
||||
function fn() {
|
||||
return \\"hello, world\\";
|
||||
}
|
||||
const __default__ = fn();
|
||||
|
|
@ -63,7 +66,8 @@ return { fn }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > <script> and <script setup> co-usage > keep original semi style 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['item'],
|
||||
emits: ['change'],
|
||||
setup(__props, { expose: __expose, emit: __emit }) {
|
||||
|
|
@ -515,27 +519,28 @@ return { }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > async/await detection > ref 1`] = `
|
||||
"import { withAsyncContext as _withAsyncContext, ref as _ref } from 'vue'
|
||||
"import { withAsyncContext as _withAsyncContext } from 'vue'
|
||||
|
||||
export default {
|
||||
async setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
let __temp, __restore
|
||||
let a = _ref(1 + ((
|
||||
let a = ref(1 + ((
|
||||
([__temp,__restore] = _withAsyncContext(() => foo)),
|
||||
__temp = await __temp,
|
||||
__restore(),
|
||||
__temp
|
||||
)))
|
||||
return { a }
|
||||
return { get a() { return a }, set a(v) { a = v } }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
async function foo() { await bar }
|
||||
|
|
@ -546,7 +551,8 @@ return { foo }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 2`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const foo = async () => { await bar }
|
||||
|
|
@ -557,7 +563,8 @@ return { foo }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 3`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const obj = { async method() { await bar }}
|
||||
|
|
@ -568,7 +575,8 @@ return { obj }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > async/await detection > should ignore await inside functions 4`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const cls = class Foo { async method() { await bar }}
|
||||
|
|
@ -618,7 +626,8 @@ return { a }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > binding analysis for destructure 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
|
@ -818,23 +827,29 @@ return { bar }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > imports > dedupe between user & helper 1`] = `
|
||||
"import { ref as _ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
|
||||
import { useCssVars, ref } from 'vue'
|
||||
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
let foo = _ref(1)
|
||||
_useCssVars(_ctx => ({
|
||||
\\"xxxxxxxx-msg\\": (msg.value)
|
||||
}))
|
||||
|
||||
const msg = ref()
|
||||
|
||||
return { foo, ref }
|
||||
return { msg, useCssVars, ref }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > imports > import dedupe between <script> and <script setup> 1`] = `
|
||||
"import { x } from './x'
|
||||
"
|
||||
|
||||
import { x } from './x'
|
||||
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
|
@ -898,7 +913,9 @@ return { ref }
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > imports > should support module string names syntax 1`] = `
|
||||
"import { \\"😏\\" as foo } from './foo'
|
||||
"
|
||||
|
||||
import { \\"😏\\" as foo } from './foo'
|
||||
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
|
@ -1185,7 +1202,8 @@ return (_ctx, _cache) => {
|
|||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
||||
const count = ref(0)
|
||||
|
|
@ -1348,7 +1366,8 @@ return { a }
|
|||
`;
|
||||
|
||||
exports[`SFC genDefaultAs > <script> + <script setup> 1`] = `
|
||||
"const __default__ = {}
|
||||
"
|
||||
const __default__ = {}
|
||||
|
||||
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
|
@ -1363,7 +1382,8 @@ return { a }
|
|||
`;
|
||||
|
||||
exports[`SFC genDefaultAs > <script> + <script setup> 2`] = `
|
||||
"const __default__ = {}
|
||||
"
|
||||
const __default__ = {}
|
||||
|
||||
const _sfc_ = /*#__PURE__*/Object.assign(__default__, {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
|
|
|||
|
|
@ -58,28 +58,9 @@ export function ssrRender(_ctx, _push, _parent, _attrs) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`source map 1`] = `
|
||||
{
|
||||
"mappings": ";;;wBACE,oBAA8B;IAAzB,oBAAmB,4BAAbA,WAAM",
|
||||
"names": [
|
||||
"render",
|
||||
],
|
||||
"sources": [
|
||||
"example.vue",
|
||||
],
|
||||
"sourcesContent": [
|
||||
"
|
||||
<div><p>{{ render }}</p></div>
|
||||
",
|
||||
],
|
||||
"version": 3,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`template errors 1`] = `
|
||||
[
|
||||
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],
|
||||
[SyntaxError: v-bind is missing expression.],
|
||||
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.],
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -240,14 +240,22 @@ describe('SFC compile <script setup>', () => {
|
|||
const { content } = compile(
|
||||
`
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
let foo = $ref(1)
|
||||
import { useCssVars, ref } from 'vue'
|
||||
const msg = ref()
|
||||
</script>
|
||||
`,
|
||||
{ reactivityTransform: true }
|
||||
|
||||
<style>
|
||||
.foo {
|
||||
color: v-bind(msg)
|
||||
}
|
||||
</style>
|
||||
`
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`import { ref } from 'vue'`)
|
||||
expect(content).toMatch(
|
||||
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`
|
||||
)
|
||||
expect(content).toMatch(`import { useCssVars, ref } from 'vue'`)
|
||||
})
|
||||
|
||||
test('import dedupe between <script> and <script setup>', () => {
|
||||
|
|
@ -891,9 +899,7 @@ describe('SFC compile <script setup>', () => {
|
|||
|
||||
describe('async/await detection', () => {
|
||||
function assertAwaitDetection(code: string, shouldAsync = true) {
|
||||
const { content } = compile(`<script setup>${code}</script>`, {
|
||||
reactivityTransform: true
|
||||
})
|
||||
const { content } = compile(`<script setup>${code}</script>`)
|
||||
if (shouldAsync) {
|
||||
expect(content).toMatch(`let __temp, __restore`)
|
||||
}
|
||||
|
|
@ -911,7 +917,7 @@ describe('SFC compile <script setup>', () => {
|
|||
})
|
||||
|
||||
test('ref', () => {
|
||||
assertAwaitDetection(`let a = $ref(1 + (await foo))`)
|
||||
assertAwaitDetection(`let a = ref(1 + (await foo))`)
|
||||
})
|
||||
|
||||
// #4448
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`defineEmits > basic usage 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
emits: ['foo', 'bar'],
|
||||
setup(__props, { expose: __expose, emit: __emit }) {
|
||||
__expose();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ return { n, get x() { return x } }
|
|||
`;
|
||||
|
||||
exports[`defineExpose() 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
||||
__expose({ foo: 123 })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`defineOptions() > basic usage 1`] = `
|
||||
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
|
||||
"
|
||||
export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
|
@ -14,7 +15,8 @@ return { }
|
|||
`;
|
||||
|
||||
exports[`defineOptions() > empty argument 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > defineProps/defineEmits in multi-variable declaration (full removal) 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['item'],
|
||||
emits: ['a'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
|
@ -192,7 +193,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > multi-variable declaration 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['item'],
|
||||
setup(__props) {
|
||||
|
||||
|
|
@ -205,7 +207,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > multi-variable declaration fix #6757 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['item'],
|
||||
setup(__props) {
|
||||
|
||||
|
|
@ -218,7 +221,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > multi-variable declaration fix #7422 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['item'],
|
||||
setup(__props) {
|
||||
|
||||
|
|
@ -250,7 +254,8 @@ return (_ctx, _cache) => {
|
|||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > nested scope 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
props: ['foo', 'bar'],
|
||||
setup(__props) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`sfc hoist static > should enable when only script setup 1`] = `
|
||||
"const foo = 'bar'
|
||||
"
|
||||
const foo = 'bar'
|
||||
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
|
@ -91,7 +92,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc hoist static > should not hoist a function or class 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
const fn = () => {}
|
||||
|
|
@ -105,7 +107,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc hoist static > should not hoist a object or array 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
const obj = { foo: 'bar' }
|
||||
|
|
@ -118,7 +121,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc hoist static > should not hoist a variable 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
let KEY1 = 'default value'
|
||||
|
|
@ -133,7 +137,8 @@ return () => {}
|
|||
`;
|
||||
|
||||
exports[`sfc hoist static > should not hoist when disabled 1`] = `
|
||||
"export default {
|
||||
"
|
||||
export default {
|
||||
setup(__props) {
|
||||
|
||||
const foo = 'bar'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
|
@ -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`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
|
||||
import {
|
||||
compileTemplate,
|
||||
SFCTemplateCompileOptions
|
||||
|
|
@ -107,24 +108,193 @@ test('source map', () => {
|
|||
const template = parse(
|
||||
`
|
||||
<template>
|
||||
<div><p>{{ render }}</p></div>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`,
|
||||
{ filename: 'example.vue', sourceMap: true }
|
||||
).descriptor.template as SFCTemplateBlock
|
||||
).descriptor.template!
|
||||
|
||||
const result = compile({
|
||||
const { code, map } = compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content
|
||||
})
|
||||
|
||||
expect(result.map).toMatchSnapshot()
|
||||
expect(map!.sources).toEqual([`example.vue`])
|
||||
expect(map!.sourcesContent).toEqual([template.content])
|
||||
|
||||
const consumer = new SourceMapConsumer(map as RawSourceMap)
|
||||
expect(
|
||||
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
|
||||
).toMatchObject(getPositionInCode(template.content, `foobar`))
|
||||
})
|
||||
|
||||
test('should work w/ AST from descriptor', () => {
|
||||
const source = `
|
||||
<template>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`
|
||||
const template = parse(source, {
|
||||
filename: 'example.vue',
|
||||
sourceMap: true
|
||||
}).descriptor.template!
|
||||
|
||||
expect(template.ast!.source).toBe(source)
|
||||
|
||||
const { code, map } = compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content,
|
||||
ast: template.ast
|
||||
})
|
||||
|
||||
expect(map!.sources).toEqual([`example.vue`])
|
||||
// when reusing AST from SFC parse for template compile,
|
||||
// the source corresponds to the entire SFC
|
||||
expect(map!.sourcesContent).toEqual([source])
|
||||
|
||||
const consumer = new SourceMapConsumer(map as RawSourceMap)
|
||||
expect(
|
||||
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
|
||||
).toMatchObject(getPositionInCode(source, `foobar`))
|
||||
|
||||
expect(code).toBe(
|
||||
compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content
|
||||
}).code
|
||||
)
|
||||
})
|
||||
|
||||
test('should work w/ AST from descriptor in SSR mode', () => {
|
||||
const source = `
|
||||
<template>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`
|
||||
const template = parse(source, {
|
||||
filename: 'example.vue',
|
||||
sourceMap: true
|
||||
}).descriptor.template!
|
||||
|
||||
expect(template.ast!.source).toBe(source)
|
||||
|
||||
const { code, map } = compile({
|
||||
filename: 'example.vue',
|
||||
source: '', // make sure it's actually using the AST instead of source
|
||||
ast: template.ast,
|
||||
ssr: true
|
||||
})
|
||||
|
||||
expect(map!.sources).toEqual([`example.vue`])
|
||||
// when reusing AST from SFC parse for template compile,
|
||||
// the source corresponds to the entire SFC
|
||||
expect(map!.sourcesContent).toEqual([source])
|
||||
|
||||
const consumer = new SourceMapConsumer(map as RawSourceMap)
|
||||
expect(
|
||||
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
|
||||
).toMatchObject(getPositionInCode(source, `foobar`))
|
||||
|
||||
expect(code).toBe(
|
||||
compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content,
|
||||
ssr: true
|
||||
}).code
|
||||
)
|
||||
})
|
||||
|
||||
test('should not reuse AST if using custom compiler', () => {
|
||||
const source = `
|
||||
<template>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`
|
||||
const template = parse(source, {
|
||||
filename: 'example.vue',
|
||||
sourceMap: true
|
||||
}).descriptor.template!
|
||||
|
||||
const { code } = compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content,
|
||||
ast: template.ast,
|
||||
compiler: {
|
||||
parse: () => null as any,
|
||||
// @ts-ignore
|
||||
compile: input => ({ code: input })
|
||||
}
|
||||
})
|
||||
|
||||
// what we really want to assert is that the `input` received by the custom
|
||||
// compiler is the source string, not the AST.
|
||||
expect(code).toBe(template.content)
|
||||
})
|
||||
|
||||
test('should force re-parse on already transformed AST', () => {
|
||||
const source = `
|
||||
<template>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`
|
||||
const template = parse(source, {
|
||||
filename: 'example.vue',
|
||||
sourceMap: true
|
||||
}).descriptor.template!
|
||||
|
||||
// force set to empty, if this is reused then it won't generate proper code
|
||||
template.ast!.children = []
|
||||
template.ast!.transformed = true
|
||||
|
||||
const { code } = compile({
|
||||
filename: 'example.vue',
|
||||
source: '',
|
||||
ast: template.ast
|
||||
})
|
||||
|
||||
expect(code).toBe(
|
||||
compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content
|
||||
}).code
|
||||
)
|
||||
})
|
||||
|
||||
test('should force re-parse with correct compiler in SSR mode', () => {
|
||||
const source = `
|
||||
<template>
|
||||
<div><p>{{ foobar }}</p></div>
|
||||
</template>
|
||||
`
|
||||
const template = parse(source, {
|
||||
filename: 'example.vue',
|
||||
sourceMap: true
|
||||
}).descriptor.template!
|
||||
|
||||
// force set to empty, if this is reused then it won't generate proper code
|
||||
template.ast!.children = []
|
||||
template.ast!.transformed = true
|
||||
|
||||
const { code } = compile({
|
||||
filename: 'example.vue',
|
||||
source: '',
|
||||
ast: template.ast,
|
||||
ssr: true
|
||||
})
|
||||
|
||||
expect(code).toBe(
|
||||
compile({
|
||||
filename: 'example.vue',
|
||||
source: template.content,
|
||||
ssr: true
|
||||
}).code
|
||||
)
|
||||
})
|
||||
|
||||
test('template errors', () => {
|
||||
const result = compile({
|
||||
filename: 'example.vue',
|
||||
source: `<div :foo
|
||||
source: `<div
|
||||
:bar="a[" v-model="baz"/>`
|
||||
})
|
||||
expect(result.errors).toMatchSnapshot()
|
||||
|
|
@ -199,3 +369,36 @@ test('dynamic v-on + static v-on should merged', () => {
|
|||
|
||||
expect(result.code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
interface Pos {
|
||||
line: number
|
||||
column: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
function getPositionInCode(
|
||||
code: string,
|
||||
token: string,
|
||||
expectName: string | boolean = false
|
||||
): Pos {
|
||||
const generatedOffset = code.indexOf(token)
|
||||
let line = 1
|
||||
let lastNewLinePos = -1
|
||||
for (let i = 0; i < generatedOffset; i++) {
|
||||
if (code.charCodeAt(i) === 10 /* newline char code */) {
|
||||
line++
|
||||
lastNewLinePos = i
|
||||
}
|
||||
}
|
||||
const res: Pos = {
|
||||
line,
|
||||
column:
|
||||
lastNewLinePos === -1
|
||||
? generatedOffset
|
||||
: generatedOffset - lastNewLinePos - 1
|
||||
}
|
||||
if (expectName) {
|
||||
res.name = typeof expectName === 'string' ? expectName : token
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { parse } from '../src'
|
||||
import { baseParse, baseCompile } from '@vue/compiler-core'
|
||||
import { baseCompile, createRoot } from '@vue/compiler-core'
|
||||
import { SourceMapConsumer } from 'source-map-js'
|
||||
|
||||
describe('compiler:sfc', () => {
|
||||
|
|
@ -7,15 +7,61 @@ describe('compiler:sfc', () => {
|
|||
test('style block', () => {
|
||||
// Padding determines how many blank lines will there be before the style block
|
||||
const padding = Math.round(Math.random() * 10)
|
||||
const style = parse(
|
||||
`${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n`
|
||||
).descriptor.styles[0]
|
||||
const src =
|
||||
`${'\n'.repeat(padding)}` +
|
||||
`<style>
|
||||
.css {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
expect(style.map).not.toBeUndefined()
|
||||
<style module>
|
||||
.css-module {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
const consumer = new SourceMapConsumer(style.map!)
|
||||
<style scoped>
|
||||
.css-scoped {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.css-scoped-nested {
|
||||
color: red;
|
||||
.dummy {
|
||||
color: green;
|
||||
}
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>`
|
||||
const {
|
||||
descriptor: { styles }
|
||||
} = parse(src)
|
||||
|
||||
expect(styles[0].map).not.toBeUndefined()
|
||||
const consumer = new SourceMapConsumer(styles[0].map!)
|
||||
const lineOffset =
|
||||
src.slice(0, src.indexOf(`<style>`)).split('\n').length - 1
|
||||
consumer.eachMapping(mapping => {
|
||||
expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
|
||||
expect(mapping.generatedLine + lineOffset).toBe(mapping.originalLine)
|
||||
})
|
||||
|
||||
expect(styles[1].map).not.toBeUndefined()
|
||||
const consumer1 = new SourceMapConsumer(styles[1].map!)
|
||||
const lineOffset1 =
|
||||
src.slice(0, src.indexOf(`<style module>`)).split('\n').length - 1
|
||||
consumer1.eachMapping(mapping => {
|
||||
expect(mapping.generatedLine + lineOffset1).toBe(mapping.originalLine)
|
||||
})
|
||||
|
||||
expect(styles[2].map).not.toBeUndefined()
|
||||
const consumer2 = new SourceMapConsumer(styles[2].map!)
|
||||
const lineOffset2 =
|
||||
src.slice(0, src.indexOf(`<style scoped>`)).split('\n').length - 1
|
||||
consumer2.eachMapping(mapping => {
|
||||
expect(mapping.generatedLine + lineOffset2).toBe(mapping.originalLine)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -122,8 +168,7 @@ h1 { color: red }
|
|||
line: 3,
|
||||
column: 1,
|
||||
offset: 10 + content.length
|
||||
},
|
||||
source: content
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -132,9 +177,8 @@ h1 { color: red }
|
|||
expect(descriptor.template).toBeTruthy()
|
||||
expect(descriptor.template!.content).toBeFalsy()
|
||||
expect(descriptor.template!.loc).toMatchObject({
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
source: ''
|
||||
start: { line: 1, column: 12, offset: 11 },
|
||||
end: { line: 1, column: 12, offset: 11 }
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -144,8 +188,7 @@ h1 { color: red }
|
|||
expect(descriptor.template!.content).toBeFalsy()
|
||||
expect(descriptor.template!.loc).toMatchObject({
|
||||
start: { line: 1, column: 11, offset: 10 },
|
||||
end: { line: 1, column: 11, offset: 10 },
|
||||
source: ''
|
||||
end: { line: 1, column: 11, offset: 10 }
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -167,6 +210,11 @@ h1 { color: red }
|
|||
expect(descriptor.script!.attrs['src']).toBe('com')
|
||||
})
|
||||
|
||||
test('should not expose ast on template node if has src import', () => {
|
||||
const { descriptor } = parse(`<template src="./foo.html"/>`)
|
||||
expect(descriptor.template!.ast).toBeUndefined()
|
||||
})
|
||||
|
||||
test('ignoreEmpty: false', () => {
|
||||
const { descriptor } = parse(
|
||||
`<script></script>\n<script setup>\n</script>`,
|
||||
|
|
@ -176,14 +224,12 @@ h1 { color: red }
|
|||
)
|
||||
expect(descriptor.script).toBeTruthy()
|
||||
expect(descriptor.script!.loc).toMatchObject({
|
||||
source: '',
|
||||
start: { line: 1, column: 9, offset: 8 },
|
||||
end: { line: 1, column: 9, offset: 8 }
|
||||
})
|
||||
|
||||
expect(descriptor.scriptSetup).toBeTruthy()
|
||||
expect(descriptor.scriptSetup!.loc).toMatchObject({
|
||||
source: '\n',
|
||||
start: { line: 2, column: 15, offset: 32 },
|
||||
end: { line: 3, column: 1, offset: 33 }
|
||||
})
|
||||
|
|
@ -208,13 +254,15 @@ h1 { color: red }
|
|||
})
|
||||
|
||||
// #1120
|
||||
test('alternative template lang should be treated as plain text', () => {
|
||||
const content = `p(v-if="1 < 2") test`
|
||||
test('template with preprocessor lang should be treated as plain text', () => {
|
||||
const content = `p(v-if="1 < 2") test <div/>`
|
||||
const { descriptor, errors } = parse(
|
||||
`<template lang="pug">` + content + `</template>`
|
||||
)
|
||||
expect(errors.length).toBe(0)
|
||||
expect(descriptor.template!.content).toBe(content)
|
||||
// should not attempt to parse the content
|
||||
expect(descriptor.template!.ast!.children.length).toBe(1)
|
||||
})
|
||||
|
||||
//#2566
|
||||
|
|
@ -260,11 +308,18 @@ h1 { color: red }
|
|||
test('custom compiler', () => {
|
||||
const { errors } = parse(`<template><input></template>`, {
|
||||
compiler: {
|
||||
parse: baseParse,
|
||||
parse: (_, options) => {
|
||||
options.onError!(new Error('foo') as any)
|
||||
return createRoot([])
|
||||
},
|
||||
compile: baseCompile
|
||||
}
|
||||
})
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors.length).toBe(2)
|
||||
// error thrown by the custom parse
|
||||
expect(errors[0].message).toBe('foo')
|
||||
// error thrown based on the returned root
|
||||
expect(errors[1].message).toMatch('At least one')
|
||||
})
|
||||
|
||||
test('treat custom blocks as raw text', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-sfc",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0-alpha.3",
|
||||
"description": "@vue/compiler-sfc",
|
||||
"main": "dist/compiler-sfc.cjs.js",
|
||||
"module": "dist/compiler-sfc.esm-browser.js",
|
||||
|
|
@ -32,11 +32,10 @@
|
|||
},
|
||||
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.3",
|
||||
"@babel/parser": "^7.23.4",
|
||||
"@vue/compiler-core": "workspace:*",
|
||||
"@vue/compiler-dom": "workspace:*",
|
||||
"@vue/compiler-ssr": "workspace:*",
|
||||
"@vue/reactivity-transform": "workspace:*",
|
||||
"@vue/shared": "workspace:*",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5",
|
||||
|
|
@ -44,10 +43,10 @@
|
|||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.23.3",
|
||||
"@babel/types": "^7.23.4",
|
||||
"@vue/consolidate": "^0.17.3",
|
||||
"hash-sum": "^2.0.0",
|
||||
"lru-cache": "^10.0.3",
|
||||
"lru-cache": "^10.1.0",
|
||||
"merge-source-map": "^1.1.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"postcss-modules": "^4.3.1",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import {
|
|||
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
|
||||
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
|
||||
import { warnOnce } from './warn'
|
||||
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
||||
import { transformDestructuredProps } from './script/definePropsDestructure'
|
||||
import { ScriptCompileContext } from './script/context'
|
||||
import {
|
||||
|
|
@ -122,14 +121,6 @@ export interface SFCScriptCompileOptions {
|
|||
fileExists(file: string): boolean
|
||||
readFile(file: string): string | undefined
|
||||
}
|
||||
/**
|
||||
* (Experimental) Enable syntax transform for using refs without `.value` and
|
||||
* using destructured props with reactivity
|
||||
* @deprecated the Reactivity Transform proposal has been dropped. This
|
||||
* feature will be removed from Vue core in 3.4. If you intend to continue
|
||||
* using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
|
||||
*/
|
||||
reactivityTransform?: boolean
|
||||
}
|
||||
|
||||
export interface ImportBinding {
|
||||
|
|
@ -165,8 +156,6 @@ export function compileScript(
|
|||
const scriptLang = script && script.lang
|
||||
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
||||
|
||||
// TODO remove in 3.4
|
||||
const enableReactivityTransform = !!options.reactivityTransform
|
||||
let refBindings: string[] | undefined
|
||||
|
||||
if (!scriptSetup) {
|
||||
|
|
@ -478,20 +467,6 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
// apply reactivity transform
|
||||
// TODO remove in 3.4
|
||||
if (enableReactivityTransform && shouldTransform(script.content)) {
|
||||
const { rootRefs, importedHelpers } = transformAST(
|
||||
scriptAst,
|
||||
ctx.s,
|
||||
scriptStartOffset!
|
||||
)
|
||||
refBindings = rootRefs
|
||||
for (const h of importedHelpers) {
|
||||
ctx.helperImports.add(h)
|
||||
}
|
||||
}
|
||||
|
||||
// <script> after <script setup>
|
||||
// we need to move the block up so that `const __default__` is
|
||||
// declared before being used in the actual component definition
|
||||
|
|
@ -687,26 +662,7 @@ export function compileScript(
|
|||
transformDestructuredProps(ctx, vueImportAliases)
|
||||
}
|
||||
|
||||
// 4. Apply reactivity transform
|
||||
// TODO remove in 3.4
|
||||
if (
|
||||
enableReactivityTransform &&
|
||||
// normal <script> had ref bindings that maybe used in <script setup>
|
||||
(refBindings || shouldTransform(scriptSetup.content))
|
||||
) {
|
||||
const { rootRefs, importedHelpers } = transformAST(
|
||||
scriptSetupAst,
|
||||
ctx.s,
|
||||
startOffset,
|
||||
refBindings
|
||||
)
|
||||
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
|
||||
for (const h of importedHelpers) {
|
||||
ctx.helperImports.add(h)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. check macro args to make sure it doesn't reference setup scope
|
||||
// 4. check macro args to make sure it doesn't reference setup scope
|
||||
// variables
|
||||
checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
|
||||
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
|
||||
|
|
@ -714,7 +670,7 @@ export function compileScript(
|
|||
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
|
||||
checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS)
|
||||
|
||||
// 6. remove non-script content
|
||||
// 5. remove non-script content
|
||||
if (script) {
|
||||
if (startOffset < scriptStartOffset!) {
|
||||
// <script setup> before <script>
|
||||
|
|
@ -733,7 +689,7 @@ export function compileScript(
|
|||
ctx.s.remove(endOffset, source.length)
|
||||
}
|
||||
|
||||
// 7. analyze binding metadata
|
||||
// 6. analyze binding metadata
|
||||
// `defineProps` & `defineModel` also register props bindings
|
||||
if (scriptAst) {
|
||||
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
|
||||
|
|
@ -762,7 +718,7 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
// 8. inject `useCssVars` calls
|
||||
// 7. inject `useCssVars` calls
|
||||
if (
|
||||
sfc.cssVars.length &&
|
||||
// no need to do this when targeting SSR
|
||||
|
|
@ -781,7 +737,7 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
// 9. finalize setup() argument signature
|
||||
// 8. finalize setup() argument signature
|
||||
let args = `__props`
|
||||
if (ctx.propsTypeDecl) {
|
||||
// mark as any and only cast on assignment
|
||||
|
|
@ -832,7 +788,7 @@ export function compileScript(
|
|||
args += `, { ${destructureElements.join(', ')} }`
|
||||
}
|
||||
|
||||
// 10. generate return statement
|
||||
// 9. generate return statement
|
||||
let returned
|
||||
if (
|
||||
!options.inlineTemplate ||
|
||||
|
|
@ -948,7 +904,7 @@ export function compileScript(
|
|||
ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
|
||||
}
|
||||
|
||||
// 11. finalize default export
|
||||
// 10. finalize default export
|
||||
const genDefaultAs = options.genDefaultAs
|
||||
? `const ${options.genDefaultAs} =`
|
||||
: `export default`
|
||||
|
|
@ -1022,7 +978,7 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
// 12. finalize Vue helper imports
|
||||
// 11. finalize Vue helper imports
|
||||
if (ctx.helperImports.size > 0) {
|
||||
ctx.s.prepend(
|
||||
`import { ${[...ctx.helperImports]
|
||||
|
|
@ -1031,8 +987,6 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
ctx.s.trim()
|
||||
|
||||
return {
|
||||
...scriptSetup,
|
||||
bindings: ctx.bindingMetadata,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import {
|
|||
CompilerError,
|
||||
NodeTransform,
|
||||
ParserOptions,
|
||||
RootNode
|
||||
RootNode,
|
||||
NodeTypes,
|
||||
ElementNode,
|
||||
createRoot
|
||||
} from '@vue/compiler-core'
|
||||
import {
|
||||
SourceMapConsumer,
|
||||
|
|
@ -30,7 +33,7 @@ import { warnOnce } from './warn'
|
|||
import { genCssVarsFromList } from './style/cssVars'
|
||||
|
||||
export interface TemplateCompiler {
|
||||
compile(template: string, options: CompilerOptions): CodegenResult
|
||||
compile(source: string | RootNode, options: CompilerOptions): CodegenResult
|
||||
parse(template: string, options: ParserOptions): RootNode
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +49,7 @@ export interface SFCTemplateCompileResults {
|
|||
|
||||
export interface SFCTemplateCompileOptions {
|
||||
source: string
|
||||
ast?: RootNode
|
||||
filename: string
|
||||
id: string
|
||||
scoped?: boolean
|
||||
|
|
@ -131,7 +135,8 @@ export function compileTemplate(
|
|||
try {
|
||||
return doCompileTemplate({
|
||||
...options,
|
||||
source: preprocess(options, preprocessor)
|
||||
source: preprocess(options, preprocessor),
|
||||
ast: undefined // invalidate AST if template goes through preprocessor
|
||||
})
|
||||
} catch (e: any) {
|
||||
return {
|
||||
|
|
@ -164,10 +169,11 @@ function doCompileTemplate({
|
|||
slotted,
|
||||
inMap,
|
||||
source,
|
||||
ast: inAST,
|
||||
ssr = false,
|
||||
ssrCssVars,
|
||||
isProd = false,
|
||||
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
|
||||
compiler,
|
||||
compilerOptions = {},
|
||||
transformAssetUrls
|
||||
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
|
||||
|
|
@ -199,7 +205,30 @@ function doCompileTemplate({
|
|||
const shortId = id.replace(/^data-v-/, '')
|
||||
const longId = `data-v-${shortId}`
|
||||
|
||||
let { code, ast, preamble, map } = compiler.compile(source, {
|
||||
const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
|
||||
compiler = compiler || defaultCompiler
|
||||
|
||||
if (compiler !== defaultCompiler) {
|
||||
// user using custom compiler, this means we cannot reuse the AST from
|
||||
// the descriptor as they might be different.
|
||||
inAST = undefined
|
||||
}
|
||||
|
||||
if (inAST?.transformed) {
|
||||
// If input AST has already been transformed, then it cannot be reused.
|
||||
// We need to parse a fresh one. Can't just use `source` here since we need
|
||||
// the AST location info to be relative to the entire SFC.
|
||||
const newAST = (ssr ? CompilerDOM : compiler).parse(inAST.source, {
|
||||
parseMode: 'sfc',
|
||||
onError: e => errors.push(e)
|
||||
})
|
||||
const template = newAST.children.find(
|
||||
node => node.type === NodeTypes.ELEMENT && node.tag === 'template'
|
||||
) as ElementNode
|
||||
inAST = createRoot(template.children, inAST.source)
|
||||
}
|
||||
|
||||
let { code, ast, preamble, map } = compiler.compile(inAST || source, {
|
||||
mode: 'module',
|
||||
prefixIdentifiers: true,
|
||||
hoistStatic: true,
|
||||
|
|
@ -222,7 +251,7 @@ function doCompileTemplate({
|
|||
// inMap should be the map produced by ./parse.ts which is a simple line-only
|
||||
// mapping. If it is present, we need to adjust the final map and errors to
|
||||
// reflect the original line numbers.
|
||||
if (inMap) {
|
||||
if (inMap && !inAST) {
|
||||
if (map) {
|
||||
map = mapLines(inMap, map)
|
||||
}
|
||||
|
|
@ -235,7 +264,7 @@ function doCompileTemplate({
|
|||
let msg = w.message
|
||||
if (w.loc) {
|
||||
msg += `\n${generateCodeFrame(
|
||||
source,
|
||||
inAST?.source || source,
|
||||
w.loc.start.offset,
|
||||
w.loc.end.offset
|
||||
)}`
|
||||
|
|
|
|||
|
|
@ -12,13 +12,6 @@ import { SFCParseResult, parseCache as _parseCache } from './parse'
|
|||
// #9521 export parseCache as a simple map to avoid exposing LRU types
|
||||
export const parseCache = _parseCache as Map<string, SFCParseResult>
|
||||
|
||||
// TODO remove in 3.4
|
||||
export {
|
||||
shouldTransform as shouldTransformRef,
|
||||
transform as transformRef,
|
||||
transformAST as transformRefAST
|
||||
} from '@vue/reactivity-transform'
|
||||
|
||||
// Utilities
|
||||
export { parse as babelParse } from '@babel/parser'
|
||||
import MagicString from 'magic-string'
|
||||
|
|
@ -37,6 +30,8 @@ export {
|
|||
|
||||
// Internals for type resolution
|
||||
export { invalidateTypeCache, registerTS } from './script/resolveType'
|
||||
export { extractRuntimeProps } from './script/defineProps'
|
||||
export { extractRuntimeEmits } from './script/defineEmits'
|
||||
|
||||
// Types
|
||||
export type {
|
||||
|
|
@ -62,6 +57,7 @@ export type { SFCScriptCompileOptions } from './compileScript'
|
|||
export type { ScriptCompileContext } from './script/context'
|
||||
export type {
|
||||
TypeResolveContext,
|
||||
SimpleTypeResolveOptions,
|
||||
SimpleTypeResolveContext
|
||||
} from './script/resolveType'
|
||||
export type {
|
||||
|
|
@ -73,3 +69,10 @@ export type {
|
|||
CompilerError,
|
||||
BindingMetadata
|
||||
} from '@vue/compiler-core'
|
||||
|
||||
/**
|
||||
* @deprecated this is preserved to avoid breaking vite-plugin-vue < 5.0
|
||||
* with reactivityTransform: true. The desired behavior should be silently
|
||||
* ignoring the option instead of breaking.
|
||||
*/
|
||||
export const shouldTransformRef = () => false
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import {
|
|||
ElementNode,
|
||||
SourceLocation,
|
||||
CompilerError,
|
||||
TextModes,
|
||||
BindingMetadata
|
||||
BindingMetadata,
|
||||
RootNode,
|
||||
createRoot
|
||||
} from '@vue/compiler-core'
|
||||
import * as CompilerDOM from '@vue/compiler-dom'
|
||||
import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
|
||||
|
|
@ -37,7 +38,7 @@ export interface SFCBlock {
|
|||
|
||||
export interface SFCTemplateBlock extends SFCBlock {
|
||||
type: 'template'
|
||||
ast: ElementNode
|
||||
ast?: RootNode
|
||||
}
|
||||
|
||||
export interface SFCScriptBlock extends SFCBlock {
|
||||
|
|
@ -128,31 +129,7 @@ export function parse(
|
|||
|
||||
const errors: (CompilerError | SyntaxError)[] = []
|
||||
const ast = compiler.parse(source, {
|
||||
// there are no components at SFC parsing level
|
||||
isNativeTag: () => true,
|
||||
// preserve all whitespaces
|
||||
isPreTag: () => true,
|
||||
getTextMode: ({ tag, props }, parent) => {
|
||||
// all top level elements except <template> are parsed as raw text
|
||||
// containers
|
||||
if (
|
||||
(!parent && tag !== 'template') ||
|
||||
// <template lang="xxx"> should also be treated as raw text
|
||||
(tag === 'template' &&
|
||||
props.some(
|
||||
p =>
|
||||
p.type === NodeTypes.ATTRIBUTE &&
|
||||
p.name === 'lang' &&
|
||||
p.value &&
|
||||
p.value.content &&
|
||||
p.value.content !== 'html'
|
||||
))
|
||||
) {
|
||||
return TextModes.RAWTEXT
|
||||
} else {
|
||||
return TextModes.DATA
|
||||
}
|
||||
},
|
||||
parseMode: 'sfc',
|
||||
onError: e => {
|
||||
errors.push(e)
|
||||
}
|
||||
|
|
@ -161,7 +138,8 @@ export function parse(
|
|||
if (node.type !== NodeTypes.ELEMENT) {
|
||||
return
|
||||
}
|
||||
// we only want to keep the nodes that are not empty (when the tag is not a template)
|
||||
// we only want to keep the nodes that are not empty
|
||||
// (when the tag is not a template)
|
||||
if (
|
||||
ignoreEmpty &&
|
||||
node.tag !== 'template' &&
|
||||
|
|
@ -178,7 +156,10 @@ export function parse(
|
|||
source,
|
||||
false
|
||||
) as SFCTemplateBlock)
|
||||
templateBlock.ast = node
|
||||
|
||||
if (!templateBlock.attrs.src) {
|
||||
templateBlock.ast = createRoot(node.children, source)
|
||||
}
|
||||
|
||||
// warn against 2.x <template functional>
|
||||
if (templateBlock.attrs.functional) {
|
||||
|
|
@ -188,7 +169,9 @@ export function parse(
|
|||
`difference from stateful ones. Just use a normal <template> ` +
|
||||
`instead.`
|
||||
) as CompilerError
|
||||
err.loc = node.props.find(p => p.name === 'functional')!.loc
|
||||
err.loc = node.props.find(
|
||||
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'functional'
|
||||
)!.loc
|
||||
errors.push(err)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -307,32 +290,11 @@ function createBlock(
|
|||
pad: SFCParseOptions['pad']
|
||||
): SFCBlock {
|
||||
const type = node.tag
|
||||
let { start, end } = node.loc
|
||||
let content = ''
|
||||
if (node.children.length) {
|
||||
start = node.children[0].loc.start
|
||||
end = node.children[node.children.length - 1].loc.end
|
||||
content = source.slice(start.offset, end.offset)
|
||||
} else {
|
||||
const offset = node.loc.source.indexOf(`</`)
|
||||
if (offset > -1) {
|
||||
start = {
|
||||
line: start.line,
|
||||
column: start.column + offset,
|
||||
offset: start.offset + offset
|
||||
}
|
||||
}
|
||||
end = { ...start }
|
||||
}
|
||||
const loc = {
|
||||
source: content,
|
||||
start,
|
||||
end
|
||||
}
|
||||
const loc = node.innerLoc!
|
||||
const attrs: Record<string, string | true> = {}
|
||||
const block: SFCBlock = {
|
||||
type,
|
||||
content,
|
||||
content: source.slice(loc.start.offset, loc.end.offset),
|
||||
loc,
|
||||
attrs
|
||||
}
|
||||
|
|
@ -341,18 +303,19 @@ function createBlock(
|
|||
}
|
||||
node.props.forEach(p => {
|
||||
if (p.type === NodeTypes.ATTRIBUTE) {
|
||||
attrs[p.name] = p.value ? p.value.content || true : true
|
||||
if (p.name === 'lang') {
|
||||
const name = p.name
|
||||
attrs[name] = p.value ? p.value.content || true : true
|
||||
if (name === 'lang') {
|
||||
block.lang = p.value && p.value.content
|
||||
} else if (p.name === 'src') {
|
||||
} else if (name === 'src') {
|
||||
block.src = p.value && p.value.content
|
||||
} else if (type === 'style') {
|
||||
if (p.name === 'scoped') {
|
||||
if (name === 'scoped') {
|
||||
;(block as SFCStyleBlock).scoped = true
|
||||
} else if (p.name === 'module') {
|
||||
;(block as SFCStyleBlock).module = attrs[p.name]
|
||||
} else if (name === 'module') {
|
||||
;(block as SFCStyleBlock).module = attrs[name]
|
||||
}
|
||||
} else if (type === 'script' && p.name === 'setup') {
|
||||
} else if (type === 'script' && name === 'setup') {
|
||||
;(block as SFCScriptBlock).setup = attrs.setup
|
||||
}
|
||||
}
|
||||
|
|
@ -376,28 +339,27 @@ function generateSourceMap(
|
|||
sourceRoot: sourceRoot.replace(/\\/g, '/')
|
||||
})
|
||||
map.setSourceContent(filename, source)
|
||||
map._sources.add(filename)
|
||||
generated.split(splitRE).forEach((line, index) => {
|
||||
if (!emptyRE.test(line)) {
|
||||
const originalLine = index + 1 + lineOffset
|
||||
const generatedLine = index + 1
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
if (!/\s/.test(line[i])) {
|
||||
map.addMapping({
|
||||
map._mappings.add({
|
||||
originalLine,
|
||||
originalColumn: i,
|
||||
generatedLine,
|
||||
generatedColumn: i,
|
||||
source: filename,
|
||||
original: {
|
||||
line: originalLine,
|
||||
column: i
|
||||
},
|
||||
generated: {
|
||||
line: generatedLine,
|
||||
column: i
|
||||
}
|
||||
// @ts-ignore
|
||||
name: null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return JSON.parse(map.toString())
|
||||
return map.toJSON()
|
||||
}
|
||||
|
||||
function padContent(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import {
|
|||
} from '@babel/types'
|
||||
import { isCallOf } from './utils'
|
||||
import { ScriptCompileContext } from './context'
|
||||
import { resolveTypeElements, resolveUnionType } from './resolveType'
|
||||
import {
|
||||
TypeResolveContext,
|
||||
resolveTypeElements,
|
||||
resolveUnionType
|
||||
} from './resolveType'
|
||||
|
||||
export const DEFINE_EMITS = 'defineEmits'
|
||||
|
||||
|
|
@ -64,7 +68,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
|
|||
return emitsDecl
|
||||
}
|
||||
|
||||
function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
|
||||
export function extractRuntimeEmits(ctx: TypeResolveContext): Set<string> {
|
||||
const emits = new Set<string>()
|
||||
const node = ctx.emitsTypeDecl!
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
|
|||
}
|
||||
|
||||
function extractEventNames(
|
||||
ctx: ScriptCompileContext,
|
||||
ctx: TypeResolveContext,
|
||||
eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
|
||||
emits: Set<string>
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import {
|
|||
} from '@babel/types'
|
||||
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
|
||||
import { ScriptCompileContext } from './context'
|
||||
import { inferRuntimeType, resolveTypeElements } from './resolveType'
|
||||
import {
|
||||
TypeResolveContext,
|
||||
inferRuntimeType,
|
||||
resolveTypeElements
|
||||
} from './resolveType'
|
||||
import {
|
||||
resolveObjectKey,
|
||||
UNKNOWN_TYPE,
|
||||
|
|
@ -150,7 +154,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
|
|||
}
|
||||
}
|
||||
} else if (ctx.propsTypeDecl) {
|
||||
propsDecls = genRuntimePropsFromTypes(ctx)
|
||||
propsDecls = extractRuntimeProps(ctx)
|
||||
}
|
||||
|
||||
const modelsDecls = genModelProps(ctx)
|
||||
|
|
@ -162,7 +166,9 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
|
||||
export function extractRuntimeProps(
|
||||
ctx: TypeResolveContext
|
||||
): string | undefined {
|
||||
// this is only called if propsTypeDecl exists
|
||||
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
|
||||
if (!props.length) {
|
||||
|
|
@ -175,7 +181,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
|
|||
for (const prop of props) {
|
||||
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
|
||||
// register bindings
|
||||
if (!(prop.key in ctx.bindingMetadata)) {
|
||||
if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
|
||||
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
|
||||
}
|
||||
}
|
||||
|
|
@ -193,7 +199,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
|
|||
}
|
||||
|
||||
function resolveRuntimePropsFromType(
|
||||
ctx: ScriptCompileContext,
|
||||
ctx: TypeResolveContext,
|
||||
node: Node
|
||||
): PropTypeData[] {
|
||||
const props: PropTypeData[] = []
|
||||
|
|
@ -222,7 +228,7 @@ function resolveRuntimePropsFromType(
|
|||
}
|
||||
|
||||
function genRuntimePropFromType(
|
||||
ctx: ScriptCompileContext,
|
||||
ctx: TypeResolveContext,
|
||||
{ key, required, type, skipCheck }: PropTypeData,
|
||||
hasStaticDefaults: boolean
|
||||
): string {
|
||||
|
|
@ -284,7 +290,7 @@ function genRuntimePropFromType(
|
|||
* static properties, we can directly generate more optimized default
|
||||
* declarations. Otherwise we will have to fallback to runtime merging.
|
||||
*/
|
||||
function hasStaticWithDefaults(ctx: ScriptCompileContext) {
|
||||
function hasStaticWithDefaults(ctx: TypeResolveContext) {
|
||||
return !!(
|
||||
ctx.propsRuntimeDefaults &&
|
||||
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
|
||||
|
|
@ -297,7 +303,7 @@ function hasStaticWithDefaults(ctx: ScriptCompileContext) {
|
|||
}
|
||||
|
||||
function genDestructuredDefaultValue(
|
||||
ctx: ScriptCompileContext,
|
||||
ctx: TypeResolveContext,
|
||||
key: string,
|
||||
inferredType?: string[]
|
||||
):
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function processPropsDestructure(
|
|||
ctx: ScriptCompileContext,
|
||||
declId: ObjectPattern
|
||||
) {
|
||||
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
|
||||
if (!ctx.options.propsDestructure) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ export function transformDestructuredProps(
|
|||
ctx: ScriptCompileContext,
|
||||
vueImportAliases: Record<string, string>
|
||||
) {
|
||||
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
|
||||
if (!ctx.options.propsDestructure) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import { SFCDescriptor } from '../parse'
|
|||
import {
|
||||
NodeTypes,
|
||||
SimpleExpressionNode,
|
||||
createRoot,
|
||||
forAliasRE,
|
||||
parserOptions,
|
||||
transform,
|
||||
walkIdentifiers
|
||||
walkIdentifiers,
|
||||
TemplateChildNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { createCache } from '../cache'
|
||||
import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
|
||||
|
|
@ -35,53 +34,54 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
|
|||
}
|
||||
|
||||
let code = ''
|
||||
transform(createRoot([ast]), {
|
||||
nodeTransforms: [
|
||||
node => {
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
if (
|
||||
!parserOptions.isNativeTag!(node.tag) &&
|
||||
!parserOptions.isBuiltInComponent!(node.tag)
|
||||
) {
|
||||
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
|
||||
}
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const prop = node.props[i]
|
||||
if (prop.type === NodeTypes.DIRECTIVE) {
|
||||
if (!isBuiltInDirective(prop.name)) {
|
||||
code += `,v${capitalize(camelize(prop.name))}`
|
||||
}
|
||||
|
||||
// process dynamic directive arguments
|
||||
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
|
||||
code += `,${stripStrings(
|
||||
(prop.arg as SimpleExpressionNode).content
|
||||
)}`
|
||||
}
|
||||
ast!.children.forEach(walk)
|
||||
|
||||
if (prop.exp) {
|
||||
code += `,${processExp(
|
||||
(prop.exp as SimpleExpressionNode).content,
|
||||
prop.name
|
||||
)}`
|
||||
}
|
||||
}
|
||||
if (
|
||||
prop.type === NodeTypes.ATTRIBUTE &&
|
||||
prop.name === 'ref' &&
|
||||
prop.value?.content
|
||||
) {
|
||||
code += `,${prop.value.content}`
|
||||
}
|
||||
}
|
||||
} else if (node.type === NodeTypes.INTERPOLATION) {
|
||||
code += `,${processExp(
|
||||
(node.content as SimpleExpressionNode).content
|
||||
)}`
|
||||
function walk(node: TemplateChildNode) {
|
||||
switch (node.type) {
|
||||
case NodeTypes.ELEMENT:
|
||||
if (
|
||||
!parserOptions.isNativeTag!(node.tag) &&
|
||||
!parserOptions.isBuiltInComponent!(node.tag)
|
||||
) {
|
||||
code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const prop = node.props[i]
|
||||
if (prop.type === NodeTypes.DIRECTIVE) {
|
||||
if (!isBuiltInDirective(prop.name)) {
|
||||
code += `,v${capitalize(camelize(prop.name))}`
|
||||
}
|
||||
|
||||
// process dynamic directive arguments
|
||||
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
|
||||
code += `,${stripStrings(
|
||||
(prop.arg as SimpleExpressionNode).content
|
||||
)}`
|
||||
}
|
||||
|
||||
if (prop.exp) {
|
||||
code += `,${processExp(
|
||||
(prop.exp as SimpleExpressionNode).content,
|
||||
prop.name
|
||||
)}`
|
||||
}
|
||||
}
|
||||
if (
|
||||
prop.type === NodeTypes.ATTRIBUTE &&
|
||||
prop.name === 'ref' &&
|
||||
prop.value?.content
|
||||
) {
|
||||
code += `,${prop.value.content}`
|
||||
}
|
||||
}
|
||||
node.children.forEach(walk)
|
||||
break
|
||||
case NodeTypes.INTERPOLATION:
|
||||
code += `,${processExp((node.content as SimpleExpressionNode).content)}`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
code += ';'
|
||||
templateUsageCheckCache.set(content, code)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
||||
import { analyzeScriptBindings } from './analyzeScriptBindings'
|
||||
import { ScriptCompileContext } from './context'
|
||||
import MagicString from 'magic-string'
|
||||
import { RawSourceMap } from 'source-map-js'
|
||||
import { rewriteDefaultAST } from '../rewriteDefault'
|
||||
import { genNormalScriptCssVarsCode } from '../style/cssVars'
|
||||
|
||||
|
|
@ -22,33 +20,8 @@ export function processNormalScript(
|
|||
let map = script.map
|
||||
const scriptAst = ctx.scriptAst!
|
||||
const bindings = analyzeScriptBindings(scriptAst.body)
|
||||
const { source, filename, cssVars } = ctx.descriptor
|
||||
const { sourceMap, genDefaultAs, isProd } = ctx.options
|
||||
|
||||
// TODO remove in 3.4
|
||||
if (ctx.options.reactivityTransform && shouldTransform(content)) {
|
||||
const s = new MagicString(source)
|
||||
const startOffset = script.loc.start.offset
|
||||
const endOffset = script.loc.end.offset
|
||||
const { importedHelpers } = transformAST(scriptAst, s, startOffset)
|
||||
if (importedHelpers.length) {
|
||||
s.prepend(
|
||||
`import { ${importedHelpers
|
||||
.map(h => `${h} as _${h}`)
|
||||
.join(', ')} } from 'vue'\n`
|
||||
)
|
||||
}
|
||||
s.remove(0, startOffset)
|
||||
s.remove(endOffset, source.length)
|
||||
content = s.toString()
|
||||
if (sourceMap !== false) {
|
||||
map = s.generateMap({
|
||||
source: filename,
|
||||
hires: true,
|
||||
includeContent: true
|
||||
}) as unknown as RawSourceMap
|
||||
}
|
||||
}
|
||||
const { cssVars } = ctx.descriptor
|
||||
const { genDefaultAs, isProd } = ctx.options
|
||||
|
||||
if (cssVars.length || genDefaultAs) {
|
||||
const defaultVar = genDefaultAs || normalScriptDefaultVar
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ import { extname, dirname, join } from 'path'
|
|||
import { minimatch as isMatch } from 'minimatch'
|
||||
import * as process from 'process'
|
||||
|
||||
export type SimpleTypeResolveOptions = Partial<
|
||||
Pick<
|
||||
SFCScriptCompileOptions,
|
||||
'globalTypeFiles' | 'fs' | 'babelParserPlugins' | 'isProd'
|
||||
>
|
||||
>
|
||||
|
||||
/**
|
||||
* TypeResolveContext is compatible with ScriptCompileContext
|
||||
* but also allows a simpler version of it with minimal required properties
|
||||
|
|
@ -60,13 +67,28 @@ import * as process from 'process'
|
|||
*/
|
||||
export type SimpleTypeResolveContext = Pick<
|
||||
ScriptCompileContext,
|
||||
// required
|
||||
'source' | 'filename' | 'error' | 'options'
|
||||
// file
|
||||
| 'source'
|
||||
| 'filename'
|
||||
|
||||
// utils
|
||||
| 'error'
|
||||
| 'helper'
|
||||
| 'getString'
|
||||
|
||||
// props
|
||||
| 'propsTypeDecl'
|
||||
| 'propsRuntimeDefaults'
|
||||
| 'propsDestructuredBindings'
|
||||
|
||||
// emits
|
||||
| 'emitsTypeDecl'
|
||||
> &
|
||||
Partial<
|
||||
Pick<ScriptCompileContext, 'scope' | 'globalScopes' | 'deps' | 'fs'>
|
||||
> & {
|
||||
ast: Statement[]
|
||||
options: SimpleTypeResolveOptions
|
||||
}
|
||||
|
||||
export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
|
|||
return vars
|
||||
}
|
||||
|
||||
const enum LexerState {
|
||||
enum LexerState {
|
||||
inParens,
|
||||
inSingleQuoteString,
|
||||
inDoubleQuoteString
|
||||
|
|
|
|||
|
|
@ -38,7 +38,12 @@ const scss: StylePreprocessor = (source, map, options, load = require) => {
|
|||
if (map) {
|
||||
return {
|
||||
code: result.css.toString(),
|
||||
map: merge(map, JSON.parse(result.map.toString())),
|
||||
map: merge(
|
||||
map,
|
||||
result.map.toJSON
|
||||
? result.map.toJSON()
|
||||
: JSON.parse(result.map.toString())
|
||||
),
|
||||
errors: [],
|
||||
dependencies
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-ssr",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0-alpha.3",
|
||||
"description": "@vue/compiler-ssr",
|
||||
"main": "dist/compiler-ssr.cjs.js",
|
||||
"types": "dist/compiler-ssr.d.ts",
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ export function createSSRCompilerError(
|
|||
return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError
|
||||
}
|
||||
|
||||
export const enum SSRErrorCodes {
|
||||
export enum SSRErrorCodes {
|
||||
X_SSR_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
|
||||
X_SSR_NO_TELEPORT_TARGET,
|
||||
X_SSR_INVALID_AST_NODE
|
||||
}
|
||||
|
||||
if (__TEST__) {
|
||||
// esbuild cannot infer const enum increments if first value is from another
|
||||
// esbuild cannot infer enum increments if first value is from another
|
||||
// file, so we have to manually keep them in sync. this check ensures it
|
||||
// errors out if there are collisions.
|
||||
if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import {
|
|||
noopDirectiveTransform,
|
||||
transformBind,
|
||||
transformStyle,
|
||||
transformOn
|
||||
transformOn,
|
||||
RootNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { ssrCodegenTransform } from './ssrCodegenTransform'
|
||||
import { ssrTransformElement } from './transforms/ssrTransformElement'
|
||||
|
|
@ -28,12 +29,11 @@ import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttr
|
|||
import { ssrInjectCssVars } from './transforms/ssrInjectCssVars'
|
||||
|
||||
export function compile(
|
||||
template: string,
|
||||
source: string | RootNode,
|
||||
options: CompilerOptions = {}
|
||||
): CodegenResult {
|
||||
options = {
|
||||
...options,
|
||||
// apply DOM-specific parsing options
|
||||
...parserOptions,
|
||||
ssr: true,
|
||||
inSSR: true,
|
||||
|
|
@ -45,7 +45,7 @@ export function compile(
|
|||
hoistStatic: false
|
||||
}
|
||||
|
||||
const ast = baseParse(template, options)
|
||||
const ast = typeof source === 'string' ? baseParse(source, options) : source
|
||||
|
||||
// Save raw options for AST. This is needed when performing sub-transforms
|
||||
// on slot vnode branches.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import {
|
|||
createSimpleExpression,
|
||||
RootNode,
|
||||
TemplateChildNode,
|
||||
findDir,
|
||||
isBuiltInType
|
||||
findDir
|
||||
} from '@vue/compiler-dom'
|
||||
|
||||
export const ssrInjectCssVars: NodeTransform = (node, context) => {
|
||||
|
|
@ -43,7 +42,7 @@ function injectCssVars(node: RootNode | TemplateChildNode) {
|
|||
node.tagType === ElementTypes.COMPONENT) &&
|
||||
!findDir(node, 'for')
|
||||
) {
|
||||
if (isBuiltInType(node.tag, 'Suspense')) {
|
||||
if (node.tag === 'suspense' || node.tag === 'Suspense') {
|
||||
for (const child of node.children) {
|
||||
if (
|
||||
child.type === NodeTypes.ELEMENT &&
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import {
|
|||
RootNode,
|
||||
TemplateChildNode,
|
||||
ParentNode,
|
||||
findDir,
|
||||
isBuiltInType
|
||||
findDir
|
||||
} from '@vue/compiler-dom'
|
||||
|
||||
const filterChild = (node: ParentNode) =>
|
||||
|
|
@ -28,8 +27,10 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
|
|||
if (
|
||||
node.type === NodeTypes.ELEMENT &&
|
||||
node.tagType === ElementTypes.COMPONENT &&
|
||||
(isBuiltInType(node.tag, 'Transition') ||
|
||||
isBuiltInType(node.tag, 'KeepAlive'))
|
||||
(node.tag === 'transition' ||
|
||||
node.tag === 'Transition' ||
|
||||
node.tag === 'KeepAlive' ||
|
||||
node.tag === 'keep-alive')
|
||||
) {
|
||||
const rootChildren = filterChild(context.root)
|
||||
if (rootChildren.length === 1 && rootChildren[0] === node) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ import {
|
|||
JSChildNode,
|
||||
RESOLVE_DYNAMIC_COMPONENT,
|
||||
TRANSITION,
|
||||
stringifyExpression
|
||||
stringifyExpression,
|
||||
DirectiveNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
|
||||
import {
|
||||
|
|
@ -54,7 +55,7 @@ import {
|
|||
ssrProcessTransitionGroup,
|
||||
ssrTransformTransitionGroup
|
||||
} from './ssrTransformTransitionGroup'
|
||||
import { isSymbol, isObject, isArray } from '@vue/shared'
|
||||
import { isSymbol, isObject, isArray, extend } from '@vue/shared'
|
||||
import { buildSSRProps } from './ssrTransformElement'
|
||||
import {
|
||||
ssrProcessTransition,
|
||||
|
|
@ -278,8 +279,8 @@ const vnodeDirectiveTransforms = {
|
|||
}
|
||||
|
||||
function createVNodeSlotBranch(
|
||||
props: ExpressionNode | undefined,
|
||||
vForExp: ExpressionNode | undefined,
|
||||
slotProps: ExpressionNode | undefined,
|
||||
vFor: DirectiveNode | undefined,
|
||||
children: TemplateChildNode[],
|
||||
parentContext: TransformContext
|
||||
): ReturnStatement {
|
||||
|
|
@ -300,32 +301,28 @@ function createVNodeSlotBranch(
|
|||
}
|
||||
|
||||
// wrap the children with a wrapper template for proper children treatment.
|
||||
// important: provide v-slot="props" and v-for="exp" on the wrapper for
|
||||
// proper scope analysis
|
||||
const wrapperProps: TemplateNode['props'] = []
|
||||
if (slotProps) {
|
||||
wrapperProps.push({
|
||||
type: NodeTypes.DIRECTIVE,
|
||||
name: 'slot',
|
||||
exp: slotProps,
|
||||
arg: undefined,
|
||||
modifiers: [],
|
||||
loc: locStub
|
||||
})
|
||||
}
|
||||
if (vFor) {
|
||||
wrapperProps.push(extend({}, vFor))
|
||||
}
|
||||
const wrapperNode: TemplateNode = {
|
||||
type: NodeTypes.ELEMENT,
|
||||
ns: Namespaces.HTML,
|
||||
tag: 'template',
|
||||
tagType: ElementTypes.TEMPLATE,
|
||||
isSelfClosing: false,
|
||||
// important: provide v-slot="props" and v-for="exp" on the wrapper for
|
||||
// proper scope analysis
|
||||
props: [
|
||||
{
|
||||
type: NodeTypes.DIRECTIVE,
|
||||
name: 'slot',
|
||||
exp: props,
|
||||
arg: undefined,
|
||||
modifiers: [],
|
||||
loc: locStub
|
||||
},
|
||||
{
|
||||
type: NodeTypes.DIRECTIVE,
|
||||
name: 'for',
|
||||
exp: vForExp,
|
||||
arg: undefined,
|
||||
modifiers: [],
|
||||
loc: locStub
|
||||
}
|
||||
],
|
||||
props: wrapperProps,
|
||||
children,
|
||||
loc: locStub,
|
||||
codegenNode: undefined
|
||||
|
|
|
|||
|
|
@ -292,14 +292,15 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
|||
}
|
||||
} else {
|
||||
// special case: value on <textarea>
|
||||
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
|
||||
const name = prop.name
|
||||
if (node.tag === 'textarea' && name === 'value' && prop.value) {
|
||||
rawChildrenMap.set(node, escapeHtml(prop.value.content))
|
||||
} else if (!needMergeProps) {
|
||||
if (prop.name === 'key' || prop.name === 'ref') {
|
||||
if (name === 'key' || name === 'ref') {
|
||||
continue
|
||||
}
|
||||
// static prop
|
||||
if (prop.name === 'class' && prop.value) {
|
||||
if (name === 'class' && prop.value) {
|
||||
staticClassBinding = JSON.stringify(prop.value.content)
|
||||
}
|
||||
openTag.push(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -44,6 +44,18 @@ declare module 'estree-walker' {
|
|||
)
|
||||
}
|
||||
|
||||
declare module 'source-map-js' {
|
||||
export interface SourceMapGenerator {
|
||||
// SourceMapGenerator has this method but the types do not include it
|
||||
toJSON(): RawSourceMap
|
||||
_sources: Set<string>
|
||||
_names: Set<string>
|
||||
_mappings: {
|
||||
add(mapping: MappingItem): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare interface String {
|
||||
/**
|
||||
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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++
|
||||
```
|
||||
|
|
@ -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
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export function plugin() {
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './reactivityTransform'
|
||||
|
|
@ -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`
|
||||
)
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
|
|||
// mutate n
|
||||
n.value++
|
||||
// on the 2nd run, plusOne.value should have already updated.
|
||||
expect(plusOneValues).toMatchObject([1, 2, 2])
|
||||
expect(plusOneValues).toMatchObject([1, 2])
|
||||
})
|
||||
|
||||
it('should warn if trying to set a readonly computed', () => {
|
||||
|
|
@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
|
|||
oldValue: 2
|
||||
})
|
||||
})
|
||||
|
||||
// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
|
||||
it('should query deps dirty sequentially', () => {
|
||||
const cSpy = vi.fn()
|
||||
|
||||
const a = ref<null | { v: number }>({
|
||||
v: 1
|
||||
})
|
||||
const b = computed(() => {
|
||||
return a.value
|
||||
})
|
||||
const c = computed(() => {
|
||||
cSpy()
|
||||
return b.value?.v
|
||||
})
|
||||
const d = computed(() => {
|
||||
if (b.value) {
|
||||
return c.value
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
d.value
|
||||
a.value!.v = 2
|
||||
a.value = null
|
||||
d.value
|
||||
expect(cSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
|
||||
it('chained computed dirty reallocation after querying dirty', () => {
|
||||
let _msg: string | undefined
|
||||
|
||||
const items = ref<number[]>()
|
||||
const isLoaded = computed(() => {
|
||||
return !!items.value
|
||||
})
|
||||
const msg = computed(() => {
|
||||
if (isLoaded.value) {
|
||||
return 'The items are loaded'
|
||||
} else {
|
||||
return 'The items are not loaded'
|
||||
}
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
_msg = msg.value
|
||||
})
|
||||
|
||||
items.value = [1, 2, 3]
|
||||
items.value = [1, 2, 3]
|
||||
items.value = undefined
|
||||
|
||||
expect(_msg).toBe('The items are not loaded')
|
||||
})
|
||||
|
||||
it('chained computed dirty reallocation after trigger computed getter', () => {
|
||||
let _msg: string | undefined
|
||||
|
||||
const items = ref<number[]>()
|
||||
const isLoaded = computed(() => {
|
||||
return !!items.value
|
||||
})
|
||||
const msg = computed(() => {
|
||||
if (isLoaded.value) {
|
||||
return 'The items are loaded'
|
||||
} else {
|
||||
return 'The items are not loaded'
|
||||
}
|
||||
})
|
||||
|
||||
_msg = msg.value
|
||||
items.value = [1, 2, 3]
|
||||
isLoaded.value // <- trigger computed getter
|
||||
_msg = msg.value
|
||||
items.value = undefined
|
||||
_msg = msg.value
|
||||
|
||||
expect(_msg).toBe('The items are not loaded')
|
||||
})
|
||||
|
||||
// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
|
||||
it('deps order should be consistent with the last time get value', () => {
|
||||
const cSpy = vi.fn()
|
||||
|
||||
const a = ref(0)
|
||||
const b = computed(() => {
|
||||
return a.value % 3 !== 0
|
||||
})
|
||||
const c = computed(() => {
|
||||
cSpy()
|
||||
if (a.value % 3 === 2) {
|
||||
return 'expensive'
|
||||
}
|
||||
return 'cheap'
|
||||
})
|
||||
const d = computed(() => {
|
||||
return a.value % 3 === 2
|
||||
})
|
||||
const e = computed(() => {
|
||||
if (b.value) {
|
||||
if (d.value) {
|
||||
return 'Avoiding expensive calculation'
|
||||
}
|
||||
}
|
||||
return c.value
|
||||
})
|
||||
|
||||
e.value
|
||||
a.value++
|
||||
e.value
|
||||
|
||||
expect(e.effect.deps.length).toBe(3)
|
||||
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
|
||||
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
|
||||
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
|
||||
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
a.value++
|
||||
e.value
|
||||
|
||||
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should trigger by the second computed that maybe dirty', () => {
|
||||
const cSpy = vi.fn()
|
||||
|
||||
const src1 = ref(0)
|
||||
const src2 = ref(0)
|
||||
const c1 = computed(() => src1.value)
|
||||
const c2 = computed(() => (src1.value % 2) + src2.value)
|
||||
const c3 = computed(() => {
|
||||
cSpy()
|
||||
c1.value
|
||||
c2.value
|
||||
})
|
||||
|
||||
c3.value
|
||||
src1.value = 2
|
||||
c3.value
|
||||
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||
src2.value = 1
|
||||
c3.value
|
||||
expect(cSpy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should trigger the second effect', () => {
|
||||
const fnSpy = vi.fn()
|
||||
const v = ref(1)
|
||||
const c = computed(() => v.value)
|
||||
|
||||
effect(() => {
|
||||
c.value
|
||||
})
|
||||
effect(() => {
|
||||
c.value
|
||||
fnSpy()
|
||||
})
|
||||
|
||||
expect(fnSpy).toBeCalledTimes(1)
|
||||
v.value = 2
|
||||
expect(fnSpy).toBeCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,57 +1,32 @@
|
|||
import { computed, deferredComputed, effect, ref } from '../src'
|
||||
import { computed, effect, ref } from '../src'
|
||||
|
||||
describe('deferred computed', () => {
|
||||
const tick = Promise.resolve()
|
||||
|
||||
test('should only trigger once on multiple mutations', async () => {
|
||||
test('should not trigger if value did not change', () => {
|
||||
const src = ref(0)
|
||||
const c = deferredComputed(() => src.value)
|
||||
const c = computed(() => src.value % 2)
|
||||
const spy = vi.fn()
|
||||
effect(() => {
|
||||
spy(c.value)
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
src.value = 1
|
||||
src.value = 2
|
||||
src.value = 3
|
||||
// not called yet
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
await tick
|
||||
// should only trigger once
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
expect(spy).toHaveBeenCalledWith(c.value)
|
||||
})
|
||||
|
||||
test('should not trigger if value did not change', async () => {
|
||||
const src = ref(0)
|
||||
const c = deferredComputed(() => src.value % 2)
|
||||
const spy = vi.fn()
|
||||
effect(() => {
|
||||
spy(c.value)
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
src.value = 1
|
||||
src.value = 2
|
||||
|
||||
await tick
|
||||
// should not trigger
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 3
|
||||
src.value = 4
|
||||
src.value = 5
|
||||
await tick
|
||||
// should trigger because latest value changes
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed trigger', async () => {
|
||||
test('chained computed trigger', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
|
@ -69,19 +44,18 @@ describe('deferred computed', () => {
|
|||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
await tick
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed avoid re-compute', async () => {
|
||||
test('chained computed avoid re-compute', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
|
@ -98,26 +72,24 @@ describe('deferred computed', () => {
|
|||
src.value = 2
|
||||
src.value = 4
|
||||
src.value = 6
|
||||
await tick
|
||||
// c1 should re-compute once.
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
expect(c1Spy).toHaveBeenCalledTimes(4)
|
||||
// c2 should not have to re-compute because c1 did not change.
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
// effect should not trigger because c2 did not change.
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('chained computed value invalidation', async () => {
|
||||
test('chained computed value invalidation', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = deferredComputed(() => {
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
|
@ -139,17 +111,17 @@ describe('deferred computed', () => {
|
|||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
|
||||
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = deferredComputed(() => {
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
|
@ -162,14 +134,13 @@ describe('deferred computed', () => {
|
|||
src.value = 1
|
||||
// sync access c2
|
||||
c2.value
|
||||
await tick
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('should not compute if deactivated before scheduler is called', async () => {
|
||||
test('should not compute if deactivated before scheduler is called', () => {
|
||||
const c1Spy = vi.fn()
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
|
@ -179,7 +150,6 @@ describe('deferred computed', () => {
|
|||
c1.effect.stop()
|
||||
// trigger
|
||||
src.value++
|
||||
await tick
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ref,
|
||||
reactive,
|
||||
effect,
|
||||
stop,
|
||||
|
|
@ -12,7 +11,8 @@ import {
|
|||
readonly,
|
||||
ReactiveEffectRunner
|
||||
} from '../src/index'
|
||||
import { ITERATE_KEY } from '../src/effect'
|
||||
import { pauseScheduling, resetScheduling } from '../src/effect'
|
||||
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
|
||||
|
||||
describe('reactivity/effect', () => {
|
||||
it('should run the passed function once (wrapped by a effect)', () => {
|
||||
|
|
@ -574,8 +574,8 @@ describe('reactivity/effect', () => {
|
|||
expect(output.fx2).toBe(1 + 3 + 3)
|
||||
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Invoked twice due to change of fx1.
|
||||
expect(fx2Spy).toHaveBeenCalledTimes(2)
|
||||
// Invoked due to change of fx1.
|
||||
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
fx1Spy.mockClear()
|
||||
fx2Spy.mockClear()
|
||||
|
|
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
|
|||
expect(dummy).toBe(3)
|
||||
})
|
||||
|
||||
// #5707
|
||||
// when an effect completes its run, it should clear the tracking bits of
|
||||
// its tracked deps. However, if the effect stops itself, the deps list is
|
||||
// emptied so their bits are never cleared.
|
||||
it('edge case: self-stopping effect tracking ref', () => {
|
||||
const c = ref(true)
|
||||
const runner = effect(() => {
|
||||
// reference ref
|
||||
if (!c.value) {
|
||||
// stop itself while running
|
||||
stop(runner)
|
||||
}
|
||||
})
|
||||
// trigger run
|
||||
c.value = !c.value
|
||||
// should clear bits
|
||||
expect((c as any).dep.w).toBe(0)
|
||||
expect((c as any).dep.n).toBe(0)
|
||||
})
|
||||
|
||||
it('events: onStop', () => {
|
||||
const onStop = vi.fn()
|
||||
const runner = effect(() => {}, {
|
||||
|
|
@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
|
|||
expect(has).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be triggered once with pauseScheduling', () => {
|
||||
const counter = reactive({ num: 0 })
|
||||
|
||||
const counterSpy = vi.fn(() => counter.num)
|
||||
effect(counterSpy)
|
||||
|
||||
counterSpy.mockClear()
|
||||
|
||||
pauseScheduling()
|
||||
counter.num++
|
||||
counter.num++
|
||||
resetScheduling()
|
||||
expect(counterSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('empty dep cleanup', () => {
|
||||
it('should remove the dep when the effect is stopped', () => {
|
||||
const obj = reactive({ prop: 1 })
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
const runner = effect(() => obj.prop)
|
||||
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||
expect(dep).toHaveLength(1)
|
||||
obj.prop = 2
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
stop(runner)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
obj.prop = 3
|
||||
runner()
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should only remove the dep when the last effect is stopped', () => {
|
||||
const obj = reactive({ prop: 1 })
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
const runner1 = effect(() => obj.prop)
|
||||
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||
expect(dep).toHaveLength(1)
|
||||
const runner2 = effect(() => obj.prop)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(2)
|
||||
obj.prop = 2
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(2)
|
||||
stop(runner1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
obj.prop = 3
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
stop(runner2)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
obj.prop = 4
|
||||
runner1()
|
||||
runner2()
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should remove the dep when it is no longer used by the effect', () => {
|
||||
const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 'a'
|
||||
})
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
effect(() => obj[obj.c])
|
||||
const depC = getDepFromReactive(toRaw(obj), 'c')
|
||||
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
|
||||
expect(depC).toHaveLength(1)
|
||||
obj.c = 'b'
|
||||
obj.a = 4
|
||||
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
|
||||
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
|
||||
expect(depC).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
|
|||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('shift on Array should trigger dependency once', () => {
|
||||
const arr = reactive([1, 2, 3])
|
||||
const fn = vi.fn()
|
||||
effect(() => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i]
|
||||
}
|
||||
fn()
|
||||
})
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
arr.shift()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
//#6018
|
||||
test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
|
||||
const arr = ref([1])
|
||||
const fn1 = vi.fn()
|
||||
const fn2 = vi.fn()
|
||||
effect(() => {
|
||||
fn1()
|
||||
if (arr.value.length > 0) {
|
||||
arr.value.slice()
|
||||
fn2()
|
||||
}
|
||||
})
|
||||
expect(fn1).toHaveBeenCalledTimes(1)
|
||||
expect(fn2).toHaveBeenCalledTimes(1)
|
||||
arr.value.splice(0)
|
||||
expect(fn1).toHaveBeenCalledTimes(2)
|
||||
expect(fn2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('add existing index on Array should not trigger length dependency', () => {
|
||||
const array = new Array(3)
|
||||
const observed = reactive(array)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/reactivity",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0-alpha.3",
|
||||
"description": "@vue/reactivity",
|
||||
"main": "index.js",
|
||||
"module": "dist/reactivity.esm-bundler.js",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
reactive,
|
||||
readonly,
|
||||
toRaw,
|
||||
ReactiveFlags,
|
||||
Target,
|
||||
readonlyMap,
|
||||
reactiveMap,
|
||||
|
|
@ -11,14 +10,14 @@ import {
|
|||
isReadonly,
|
||||
isShallow
|
||||
} from './reactive'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import {
|
||||
track,
|
||||
trigger,
|
||||
ITERATE_KEY,
|
||||
pauseTracking,
|
||||
resetTracking
|
||||
resetTracking,
|
||||
pauseScheduling,
|
||||
resetScheduling
|
||||
} from './effect'
|
||||
import { track, trigger, ITERATE_KEY } from './reactiveEffect'
|
||||
import {
|
||||
isObject,
|
||||
hasOwn,
|
||||
|
|
@ -71,7 +70,9 @@ function createArrayInstrumentations() {
|
|||
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
|
||||
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
|
||||
pauseTracking()
|
||||
pauseScheduling()
|
||||
const res = (toRaw(this) as any)[key].apply(this, args)
|
||||
resetScheduling()
|
||||
resetTracking()
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
|
||||
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { toRaw, toReactive, toReadonly } from './reactive'
|
||||
import {
|
||||
track,
|
||||
trigger,
|
||||
ITERATE_KEY,
|
||||
MAP_KEY_ITERATE_KEY
|
||||
} from './reactiveEffect'
|
||||
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
|
||||
|
||||
export type CollectionTypes = IterableCollections | WeakCollections
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { DebuggerOptions, ReactiveEffect } from './effect'
|
||||
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
||||
import { isFunction, NOOP } from '@vue/shared'
|
||||
import { ReactiveFlags, toRaw } from './reactive'
|
||||
import { hasChanged, isFunction, NOOP } from '@vue/shared'
|
||||
import { toRaw } from './reactive'
|
||||
import { Dep } from './dep'
|
||||
import { DirtyLevels, ReactiveFlags } from './constants'
|
||||
|
||||
declare const ComputedRefSymbol: unique symbol
|
||||
|
||||
|
|
@ -15,8 +16,8 @@ export interface WritableComputedRef<T> extends Ref<T> {
|
|||
readonly effect: ReactiveEffect<T>
|
||||
}
|
||||
|
||||
export type ComputedGetter<T> = (...args: any[]) => T
|
||||
export type ComputedSetter<T> = (v: T) => void
|
||||
export type ComputedGetter<T> = (oldValue?: T) => T
|
||||
export type ComputedSetter<T> = (newValue: T) => void
|
||||
|
||||
export interface WritableComputedOptions<T> {
|
||||
get: ComputedGetter<T>
|
||||
|
|
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
|
|||
public readonly __v_isRef = true
|
||||
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
|
||||
|
||||
public _dirty = true
|
||||
public _cacheable: boolean
|
||||
|
||||
constructor(
|
||||
|
|
@ -41,12 +41,10 @@ export class ComputedRefImpl<T> {
|
|||
isReadonly: boolean,
|
||||
isSSR: boolean
|
||||
) {
|
||||
this.effect = new ReactiveEffect(getter, () => {
|
||||
if (!this._dirty) {
|
||||
this._dirty = true
|
||||
triggerRefValue(this)
|
||||
}
|
||||
})
|
||||
this.effect = new ReactiveEffect(
|
||||
() => getter(this._value),
|
||||
() => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
|
||||
)
|
||||
this.effect.computed = this
|
||||
this.effect.active = this._cacheable = !isSSR
|
||||
this[ReactiveFlags.IS_READONLY] = isReadonly
|
||||
|
|
@ -56,9 +54,10 @@ export class ComputedRefImpl<T> {
|
|||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
||||
const self = toRaw(this)
|
||||
trackRefValue(self)
|
||||
if (self._dirty || !self._cacheable) {
|
||||
self._dirty = false
|
||||
self._value = self.effect.run()!
|
||||
if (!self._cacheable || self.effect.dirty) {
|
||||
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
|
||||
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
|
||||
}
|
||||
}
|
||||
return self._value
|
||||
}
|
||||
|
|
@ -66,6 +65,16 @@ export class ComputedRefImpl<T> {
|
|||
set value(newValue: T) {
|
||||
this._setter(newValue)
|
||||
}
|
||||
|
||||
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
|
||||
get _dirty() {
|
||||
return this.effect.dirty
|
||||
}
|
||||
|
||||
set _dirty(v) {
|
||||
this.effect.dirty = v
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,88 +1,6 @@
|
|||
import { Dep } from './dep'
|
||||
import { ReactiveEffect } from './effect'
|
||||
import { ComputedGetter, ComputedRef } from './computed'
|
||||
import { ReactiveFlags, toRaw } from './reactive'
|
||||
import { trackRefValue, triggerRefValue } from './ref'
|
||||
import { computed } from './computed'
|
||||
|
||||
const tick = /*#__PURE__*/ Promise.resolve()
|
||||
const queue: any[] = []
|
||||
let queued = false
|
||||
|
||||
const scheduler = (fn: any) => {
|
||||
queue.push(fn)
|
||||
if (!queued) {
|
||||
queued = true
|
||||
tick.then(flush)
|
||||
}
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
queue[i]()
|
||||
}
|
||||
queue.length = 0
|
||||
queued = false
|
||||
}
|
||||
|
||||
class DeferredComputedRefImpl<T> {
|
||||
public dep?: Dep = undefined
|
||||
|
||||
private _value!: T
|
||||
private _dirty = true
|
||||
public readonly effect: ReactiveEffect<T>
|
||||
|
||||
public readonly __v_isRef = true
|
||||
public readonly [ReactiveFlags.IS_READONLY] = true
|
||||
|
||||
constructor(getter: ComputedGetter<T>) {
|
||||
let compareTarget: any
|
||||
let hasCompareTarget = false
|
||||
let scheduled = false
|
||||
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
|
||||
if (this.dep) {
|
||||
if (computedTrigger) {
|
||||
compareTarget = this._value
|
||||
hasCompareTarget = true
|
||||
} else if (!scheduled) {
|
||||
const valueToCompare = hasCompareTarget ? compareTarget : this._value
|
||||
scheduled = true
|
||||
hasCompareTarget = false
|
||||
scheduler(() => {
|
||||
if (this.effect.active && this._get() !== valueToCompare) {
|
||||
triggerRefValue(this)
|
||||
}
|
||||
scheduled = false
|
||||
})
|
||||
}
|
||||
// chained upstream computeds are notified synchronously to ensure
|
||||
// value invalidation in case of sync access; normal effects are
|
||||
// deferred to be triggered in scheduler.
|
||||
for (const e of this.dep) {
|
||||
if (e.computed instanceof DeferredComputedRefImpl) {
|
||||
e.scheduler!(true /* computedTrigger */)
|
||||
}
|
||||
}
|
||||
}
|
||||
this._dirty = true
|
||||
})
|
||||
this.effect.computed = this as any
|
||||
}
|
||||
|
||||
private _get() {
|
||||
if (this._dirty) {
|
||||
this._dirty = false
|
||||
return (this._value = this.effect.run()!)
|
||||
}
|
||||
return this._value
|
||||
}
|
||||
|
||||
get value() {
|
||||
trackRefValue(this)
|
||||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
||||
return toRaw(this)._get()
|
||||
}
|
||||
}
|
||||
|
||||
export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
|
||||
return new DeferredComputedRefImpl(getter) as any
|
||||
}
|
||||
/**
|
||||
* @deprecated use `computed` instead. See #5912
|
||||
*/
|
||||
export const deferredComputed = computed
|
||||
|
|
|
|||
|
|
@ -1,57 +1,17 @@
|
|||
import { ReactiveEffect, trackOpBit } from './effect'
|
||||
import type { ReactiveEffect } from './effect'
|
||||
import type { ComputedRefImpl } from './computed'
|
||||
|
||||
export type Dep = Set<ReactiveEffect> & TrackedMarkers
|
||||
|
||||
/**
|
||||
* wasTracked and newTracked maintain the status for several levels of effect
|
||||
* tracking recursion. One bit per level is used to define whether the dependency
|
||||
* was/is tracked.
|
||||
*/
|
||||
type TrackedMarkers = {
|
||||
/**
|
||||
* wasTracked
|
||||
*/
|
||||
w: number
|
||||
/**
|
||||
* newTracked
|
||||
*/
|
||||
n: number
|
||||
export type Dep = Map<ReactiveEffect, number> & {
|
||||
cleanup: () => void
|
||||
computed?: ComputedRefImpl<any>
|
||||
}
|
||||
|
||||
export const createDep = (effects?: ReactiveEffect[]): Dep => {
|
||||
const dep = new Set<ReactiveEffect>(effects) as Dep
|
||||
dep.w = 0
|
||||
dep.n = 0
|
||||
export const createDep = (
|
||||
cleanup: () => void,
|
||||
computed?: ComputedRefImpl<any>
|
||||
): Dep => {
|
||||
const dep = new Map() as Dep
|
||||
dep.cleanup = cleanup
|
||||
dep.computed = computed
|
||||
return dep
|
||||
}
|
||||
|
||||
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
|
||||
|
||||
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
|
||||
|
||||
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
|
||||
if (deps.length) {
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
deps[i].w |= trackOpBit // set was tracked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
|
||||
const { deps } = effect
|
||||
if (deps.length) {
|
||||
let ptr = 0
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
const dep = deps[i]
|
||||
if (wasTracked(dep) && !newTracked(dep)) {
|
||||
dep.delete(effect)
|
||||
} else {
|
||||
deps[ptr++] = dep
|
||||
}
|
||||
// clear bits
|
||||
dep.w &= ~trackOpBit
|
||||
dep.n &= ~trackOpBit
|
||||
}
|
||||
deps.length = ptr
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue