diff --git a/.eslintrc.js b/.eslintrc.js index 4c667f902..552802477 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { sourceType: 'module' }, rules: { + 'no-debugger': 'error', 'no-unused-vars': [ 'error', // we are only using this rule to check for unused arguments since TS @@ -16,10 +17,11 @@ module.exports = { // most of the codebase are expected to be env agnostic 'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals], // since we target ES2015 for baseline support, we need to forbid object - // rest spread usage (both assign and destructure) + // rest spread usage in destructure as it compiles into a verbose helper. + // TS now compiles assignment spread into Object.assign() calls so that + // is allowed. 'no-restricted-syntax': [ 'error', - 'ObjectExpression > SpreadElement', 'ObjectPattern > RestElement', 'AwaitExpression' ] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..fa5f6cc10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,68 @@ +name: "\U0001F41E Bug report" +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting bug reports. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://vuejs.org/) + - Ask on [Discord Chat](https://chat.vuejs.org/) + - Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions) + - Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js) + + Also try to search for your issue - it may have already been answered or even fixed in the development branch. + However, if you find that an old, closed issue still persists in the latest version, + you should open a new issue using the form below instead of commenting on the old issue. + - type: input + id: reproduction-link + attributes: + label: Link to minimal reproduction + description: | + The easiest way to provide a reproduction is by showing the bug in [The SFC Playground](https://sfc.vuejs.org/). + If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue). + If neither of these are suitable, you can always provide a GitHub reporistory. + + The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed + to show the bug. See [Bug Reproduction Guidelines](https://github.com/vuejs/core/blob/main/.github/bug-repro-guidelines.md) for more details. + + Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided. + placeholder: Reproduction Link + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: | + What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code. + placeholder: Steps to reproduce + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is expected? + validations: + required: true + - type: textarea + id: actually-happening + attributes: + label: What is actually happening? + validations: + required: true + - type: textarea + id: system-info + attributes: + label: System Info + description: Output of `npx envinfo --system --npmPackages vue --binaries --browsers` + render: shell + placeholder: System, Binaries, Browsers + - type: textarea + id: additional-comments + attributes: + label: Any additional comments? + description: e.g. some background/context of how you ran into this bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d331b5312..af3782c84 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Create new issue - url: https://new-issue.vuejs.org/?repo=vuejs/core - about: Please use the following link to create a new issue. + - name: Discord Chat + url: https://chat.vuejs.org + about: Ask questions and discuss with other Vue users in real time. + - name: Questions & Discussions + url: https://github.com/vuejs/core/discussions + about: Use GitHub discussions for message-board style questions and discussions. - name: Patreon url: https://www.patreon.com/evanyou about: Love Vue.js? Please consider supporting us via Patreon. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..9165eb4d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: "\U0001F680 New feature proposal" +description: Suggest an idea for this project +labels: [":sparkles: feature request"] +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting feature requests. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://vuejs.org/) + - Ask on [Discord Chat](https://chat.vuejs.org/) + - Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions) + - Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js) + + Also try to search for your issue - another user may have already requested something similar! + + - type: textarea + id: problem-description + attributes: + label: What problem does this feature solve? + description: | + Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature? + + An important design goal of Vue is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition. + placeholder: Problem description + validations: + required: true + - type: textarea + id: proposed-API + attributes: + label: What does the proposed API look like? + description: | + Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks. + placeholder: Steps to reproduce + validations: + required: true diff --git a/.github/bug-repro-guidelines.md b/.github/bug-repro-guidelines.md new file mode 100644 index 000000000..19e9a7e2f --- /dev/null +++ b/.github/bug-repro-guidelines.md @@ -0,0 +1,29 @@ +## About Bug Reproductions + +A bug reproduction is a piece of code that can run and demonstrate how a bug can happen. + +### Text is not enough + +It's impossible to fix a bug from mere text descriptions. First, it's very difficult to precisely describe a technical problem while keeping it easy to follow; Second, the real cause may very well be something that you forgot to even mention. A reproduction is the only way that can reliably help us understand what is going on, so please provide one. + +### A repro must be runnable + +Screenshots or videos are NOT reproductions! They only show that the bug exists, but do not provide enough information on why it happens. Only runnable code provides the most complete context and allows us to properly debug the scenario. That said, in some cases videos/gifs can help explain interaction issues that are hard to describe in text. + +### A repro should be minimal + +Some users would give us a link to a real project and hope we can help them figure out what is wrong. We generally do not accept such requests because: + +You are already familiar with your codebase, but we are not. It is extremely time-consuming to hunt a bug in a big and unfamiliar codebase. + +The problematic behavior may very well be caused by your code rather than by a bug in Vue. + +A minimal reproduction means it demonstrates the bug, and the bug only. It should only contain the bare minimum amount of code that can reliably cause the bug. Try your best to get rid of anything that aren't directly related to the problem. + +### How to create a repro + +For Vue 3 core reproductions, try reproducing it in [The SFC Playground](https://sfc.vuejs.org/). + +If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue). + +If neither of these are suitable, you can always provide a GitHub repository. diff --git a/.github/contributing.md b/.github/contributing.md index 333959478..5b1a2cc45 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -17,7 +17,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ## Pull Request Guidelines -- Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch. +- Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. - If adding a new feature: @@ -40,7 +40,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ## Development Setup -You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io). +You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io) **version 7+**. We also recommend installing [ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e02bfc66..aeb4062ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: - main jobs: - test: + unit-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -15,7 +15,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2.0.1 with: - version: 6.15.1 + version: 7.0.1 - name: Set node version to 16 uses: actions/setup-node@v2 @@ -26,9 +26,9 @@ jobs: - run: pnpm install - name: Run unit tests - run: pnpm run test + run: pnpm run test-unit - test-dts: + e2e-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -36,7 +36,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2.0.1 with: - version: 6.15.1 + version: 7.0.1 - name: Set node version to 16 uses: actions/setup-node@v2 @@ -46,6 +46,30 @@ jobs: - run: pnpm install + - name: Run e2e tests + run: pnpm run test-e2e + + lint-and-test-dts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install pnpm + uses: pnpm/action-setup@v2.0.1 + with: + version: 7.0.1 + + - name: Set node version to 16 + uses: actions/setup-node@v2 + with: + node-version: 16 + cache: 'pnpm' + + - run: pnpm install + + - name: Run eslint + run: pnpm run lint + - name: Run type declaration tests run: pnpm run test-dts @@ -59,7 +83,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2.0.1 with: - version: 6.15.1 + version: 7.0.1 - name: Set node version to 16 uses: actions/setup-node@v2 diff --git a/package.json b/package.json index 14f9ed5d5..f42683537 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli", "lint": "eslint --ext .ts packages/*/src/**.ts", "format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"", - "test": "run-s \"test-unit -- {@}\" \"test-e2e -- {@}\" --", + "test": "run-s \"test-unit {@}\" \"test-e2e {@}\"", "test-unit": "jest --filter ./scripts/filter-unit.js", "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand", "test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && npm run test-dts-only", @@ -18,7 +18,7 @@ "release": "node scripts/release.js", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "dev-compiler": "run-p \"dev template-explorer\" serve", - "dev-sfc": "run-p \"dev compiler-sfc -- -f esm-browser\" \"dev vue -- -if esm-bundler-runtime \" serve-sfc-playground", + "dev-sfc": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime \" serve-sfc-playground", "serve-sfc-playground": "vite packages/sfc-playground --host", "serve": "serve", "open": "open http://localhost:5000/packages/template-explorer/local.html", @@ -58,7 +58,7 @@ "@types/jest": "^27.0.1", "@types/node": "^16.4.7", "@types/puppeteer": "^5.0.0", - "@typescript-eslint/parser": "^4.1.1", + "@typescript-eslint/parser": "^5.23.0", "@vue/reactivity": "workspace:*", "@vue/runtime-core": "workspace:*", "@vue/runtime-dom": "workspace:*", @@ -89,9 +89,9 @@ "serve": "^12.0.0", "todomvc-app-css": "^2.3.0", "ts-jest": "^27.0.5", - "tslib": "^2.3.1", - "typescript": "^4.2.2", - "vite": "^2.9.0", + "tslib": "^2.4.0", + "typescript": "^4.6.4", + "vite": "^2.9.8", "vue": "workspace:*", "yorkie": "^2.0.0" } diff --git a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap index a72a43782..a9189ac72 100644 --- a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap @@ -16,7 +16,7 @@ return function render(_ctx, _cache) { ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ _createTextVNode(\\"no\\") - ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), + ], 64 /* STABLE_FRAGMENT */)), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => { return (_openBlock(), _createElementBlock(\\"div\\", null, [ _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) @@ -40,7 +40,7 @@ return function render(_ctx, _cache) { ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ _createTextVNode(\\"no\\") - ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), + ], 64 /* STABLE_FRAGMENT */)), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => { return (_openBlock(), _createElementBlock(\\"div\\", null, [ _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) @@ -63,7 +63,7 @@ export function render(_ctx, _cache) { ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ _createTextVNode(\\"no\\") - ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), + ], 64 /* STABLE_FRAGMENT */)), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => { return (_openBlock(), _createElementBlock(\\"div\\", null, [ _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index f1869739c..4154e306c 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -1037,7 +1037,7 @@ describe('compiler: parse', () => { offset: 0 } }, - ns: 0, + ns: Namespaces.HTML, props: [ { loc: { @@ -1054,7 +1054,7 @@ describe('compiler: parse', () => { } }, name: 'class', - type: 6, + type: NodeTypes.ATTRIBUTE, value: { content: 'c', loc: { @@ -1070,13 +1070,13 @@ describe('compiler: parse', () => { offset: 11 } }, - type: 2 + type: NodeTypes.TEXT } } ], tag: 'div', - tagType: 0, - type: 1 + tagType: ElementTypes.ELEMENT, + type: NodeTypes.ELEMENT }) }) @@ -2023,7 +2023,7 @@ foo isPreTag: tag => tag === 'pre' }) const elementAfterPre = ast.children[1] as ElementNode - // should not affect the and condense its whitepsace inside + // should not affect the and condense its whitespace inside expect((elementAfterPre.children[0] as TextNode).content).toBe(` foo bar`) }) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap index 54fc7cbe7..ee15a6558 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -111,7 +111,7 @@ return function render(_ctx, _cache) { ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 })) : orNot ? (_openBlock(), _createElementBlock(\\"p\\", { key: 1 })) - : (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [\\"fine\\"], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) + : (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [\\"fine\\"], 64 /* STABLE_FRAGMENT */)) } }" `; diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index e7a95622d..b672799ad 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -80,7 +80,7 @@ describe('compiler: element transform', () => { expect(root.components).toContain(`Foo`) }) - test('resolve implcitly self-referencing component', () => { + test('resolve implicitly self-referencing component', () => { const { root } = parseWithElementTransform(``, { filename: `/foo/bar/Example.vue?vue&type=template` }) @@ -807,6 +807,37 @@ describe('compiler: element transform', () => { }) }) + test(':style with array literal', () => { + const { node, root } = parseWithElementTransform( + `
`, + { + nodeTransforms: [transformExpression, transformStyle, transformElement], + directiveTransforms: { + bind: transformBind + }, + prefixIdentifiers: true + } + ) + expect(root.helpers).toContain(NORMALIZE_STYLE) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + type: NodeTypes.JS_PROPERTY, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `style`, + isStatic: true + }, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: NORMALIZE_STYLE + } + } + ] + }) + }) + test(`props merging: class`, () => { const { node, root } = parseWithElementTransform( `
`, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 58fa76abe..d6eda73bb 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -259,6 +259,7 @@ export interface IfBranchNode extends Node { condition: ExpressionNode | undefined // else children: TemplateChildNode[] userKey?: AttributeNode | DirectiveNode + isTemplateIf?: boolean } export interface ForNode extends Node { diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 2b641e9ad..9d715149b 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -58,6 +58,8 @@ import { ImportItem } from './transform' const PURE_ANNOTATION = `/*#__PURE__*/` +const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` + type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode export interface CodegenResult { @@ -247,11 +249,7 @@ 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 { ${ast.helpers - .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`) - .join(', ')} } = _Vue` - ) + push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`) push(`\n`) newline() } @@ -328,7 +326,6 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) { !__BROWSER__ && ssr ? `require(${JSON.stringify(runtimeModuleName)})` : runtimeGlobalName - const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` // Generate const declaration for helpers // In prefix mode, we place the const declaration at top so it's done // only once; But if we not prefixing, we place the declaration inside the diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index a68d23958..6ed7aa5b8 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -59,6 +59,7 @@ export { PropsExpression } from './transforms/transformElement' export { processSlotOutlet } from './transforms/transformSlotOutlet' +export { getConstantType } from './transforms/hoistStatic' export { generateCodeFrame } from '@vue/shared' // v2 compat only diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index a0658b4d7..1221772f7 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -97,6 +97,10 @@ export const enum BindingTypes { * template expressions. */ SETUP_CONST = 'setup-const', + /** + * a const binding that does not need `unref()`, but may be mutated. + */ + SETUP_REACTIVE_CONST = 'setup-reactive-const', /** * a const binding that may be a ref. */ diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 7143963d1..403c5c6e3 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -352,7 +352,9 @@ function resolveSetupReference(name: string, context: TransformContext) { } } - const fromConst = checkType(BindingTypes.SETUP_CONST) + const fromConst = + checkType(BindingTypes.SETUP_CONST) || + checkType(BindingTypes.SETUP_REACTIVE_CONST) if (fromConst) { return context.inline ? // in inline mode, const setup bindings (e.g. imports) can be used as-is @@ -765,10 +767,11 @@ export function buildProps( } if ( styleProp && - !isStaticExp(styleProp.value) && // the static style is compiled into an object, // so use `hasStyleBinding` to ensure that it is a dynamic style binding (hasStyleBinding || + (styleProp.value.type === NodeTypes.SIMPLE_EXPRESSION && + styleProp.value.content.trim()[0] === `[`) || // v-bind:style and style both exist, // v-bind:style with static literal object styleProp.value.type === NodeTypes.JS_ARRAY_EXPRESSION) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 7a5e699ef..e4311ad4f 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -24,7 +24,13 @@ import { walkIdentifiers } from '../babelUtils' import { advancePositionWithClone, isSimpleIdentifier } from '../utils' -import { isGloballyWhitelisted, makeMap, hasOwn, isString } from '@vue/shared' +import { + isGloballyWhitelisted, + makeMap, + hasOwn, + isString, + genPropsAccessExp +} from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { Node, @@ -122,7 +128,11 @@ export function processExpression( const isDestructureAssignment = parent && isInDestructureAssignment(parent, parentStack) - if (type === BindingTypes.SETUP_CONST || localVars[raw]) { + if ( + type === BindingTypes.SETUP_CONST || + type === BindingTypes.SETUP_REACTIVE_CONST || + localVars[raw] + ) { return raw } else if (type === BindingTypes.SETUP_REF) { return `${raw}.value` @@ -181,17 +191,17 @@ export function processExpression( } else if (type === BindingTypes.PROPS) { // use __props which is generated by compileScript so in ts mode // it gets correct type - return `__props.${raw}` + return genPropsAccessExp(raw) } else if (type === BindingTypes.PROPS_ALIASED) { // prop with a different local alias (from defineProps() destructure) - return `__props.${bindingMetadata.__propsAliases![raw]}` + return genPropsAccessExp(bindingMetadata.__propsAliases![raw]) } } else { if (type && type.startsWith('setup')) { // setup bindings in non-inline mode return `$setup.${raw}` } else if (type === BindingTypes.PROPS_ALIASED) { - return `$props.${bindingMetadata.__propsAliases![raw]}` + return `$props['${bindingMetadata.__propsAliases![raw]}']` } else if (type) { return `$${type}.${raw}` } diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 71050a856..2faa16374 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -209,15 +209,14 @@ export function processIf( } function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { + const isTemplateIf = node.tagType === ElementTypes.TEMPLATE return { type: NodeTypes.IF_BRANCH, loc: node.loc, condition: dir.name === 'else' ? undefined : dir.exp, - children: - node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for') - ? node.children - : [node], - userKey: findProp(node, `key`) + children: isTemplateIf && !findDir(node, 'for') ? node.children : [node], + userKey: findProp(node, `key`), + isTemplateIf } } @@ -274,6 +273,7 @@ function createChildrenCodegenNode( // the rest being comments if ( __DEV__ && + !branch.isTemplateIf && children.filter(c => c.type !== NodeTypes.COMMENT).length === 1 ) { patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT diff --git a/packages/compiler-dom/__tests__/transforms/Transition.spec.ts b/packages/compiler-dom/__tests__/transforms/Transition.spec.ts new file mode 100644 index 000000000..1ae713607 --- /dev/null +++ b/packages/compiler-dom/__tests__/transforms/Transition.spec.ts @@ -0,0 +1,166 @@ +import { compile } from '../../src' + +describe('Transition multi children warnings', () => { + function checkWarning( + template: string, + shouldWarn: boolean, + message = ` expects exactly one child element or component.` + ) { + const spy = jest.fn() + compile(template.trim(), { + hoistStatic: true, + transformHoist: null, + onError: err => { + spy(err.message) + } + }) + + if (shouldWarn) expect(spy).toHaveBeenCalledWith(message) + else expect(spy).not.toHaveBeenCalled() + } + + test('warns if multiple children', () => { + checkWarning( + ` + +
hey
+
hey
+
+ `, + true + ) + }) + + test('warns with v-for', () => { + checkWarning( + ` + +
hey
+
+ `, + true + ) + }) + + test('warns with multiple v-if + v-for', () => { + checkWarning( + ` + +
hey
+
hey
+
+ `, + true + ) + }) + + test('warns with template v-if', () => { + checkWarning( + ` + + + + `, + true + ) + }) + + test('warns with multiple templates', () => { + checkWarning( + ` + + + + + `, + true + ) + }) + + test('warns if multiple children with v-if', () => { + checkWarning( + ` + +
hey
+
hey
+
+ `, + true + ) + }) + + test('does not warn with regular element', () => { + checkWarning( + ` + +
hey
+
+ `, + false + ) + }) + + test('does not warn with one single v-if', () => { + checkWarning( + ` + +
hey
+
+ `, + false + ) + }) + + test('does not warn with v-if v-else-if v-else', () => { + checkWarning( + ` + +
hey
+
hey
+
hey
+
+ `, + false + ) + }) + + test('does not warn with v-if v-else', () => { + checkWarning( + ` + +
hey
+
hey
+
+ `, + false + ) + }) +}) + +test('inject persisted when child has v-show', () => { + expect( + compile(` + +
+ + `).code + ).toMatchSnapshot() +}) + +test('the v-if/else-if/else branches in Transition should ignore comments', () => { + expect( + compile(` + +
hey
+ +
hey
+ +
+

+ +

+

+
+ `).code + ).toMatchSnapshot() +}) diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/warnTransitionChildren.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap similarity index 62% rename from packages/compiler-dom/__tests__/transforms/__snapshots__/warnTransitionChildren.spec.ts.snap rename to packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap index 5787b08ff..026fb8aae 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/warnTransitionChildren.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap @@ -1,6 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`the v-if/else-if/else branchs in Transition should ignore comments 1`] = ` +exports[`inject persisted when child has v-show 1`] = ` +"const _Vue = Vue + +return function render(_ctx, _cache) { + with (_ctx) { + const { vShow: _vShow, createElementVNode: _createElementVNode, withDirectives: _withDirectives, Transition: _Transition, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(_Transition, { persisted: \\"\\" }, { + default: _withCtx(() => [ + _withDirectives(_createElementVNode(\\"div\\", null, null, 512 /* NEED_PATCH */), [ + [_vShow, ok] + ]) + ]), + _: 1 /* STABLE */ + })) + } +}" +`; + +exports[`the v-if/else-if/else branches in Transition should ignore comments 1`] = ` "const _Vue = Vue return function render(_ctx, _cache) { diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap index efa75df3f..8427b38fc 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -32,3 +32,23 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2)) }" `; + +exports[`stringify static html stringify v-html 1`] = ` +"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue + +const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"
show-it 
12
\\", 2) + +return function render(_ctx, _cache) { + return _hoisted_1 +}" +`; + +exports[`stringify static html stringify v-text 1`] = ` +"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue + +const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"
<span>show-it </span>
12
\\", 2) + +return function render(_ctx, _cache) { + return _hoisted_1 +}" +`; diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 0beb42585..c737071a8 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -433,4 +433,25 @@ describe('stringify static html', () => { ] }) }) + + // #5439 + test('stringify v-html', () => { + const { code } = compileWithStringify(` +
+
+ 12 +
`) + expect(code).toMatch(`show-it `) + expect(code).toMatchSnapshot() + }) + + test('stringify v-text', () => { + const { code } = compileWithStringify(` +
+
+ 12 +
`) + expect(code).toMatch(`<span>show-it </span>`) + expect(code).toMatchSnapshot() + }) }) diff --git a/packages/compiler-dom/__tests__/transforms/warnTransitionChildren.spec.ts b/packages/compiler-dom/__tests__/transforms/warnTransitionChildren.spec.ts deleted file mode 100644 index 3cccdfffc..000000000 --- a/packages/compiler-dom/__tests__/transforms/warnTransitionChildren.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { compile } from '../../src' - -describe('compiler warnings', () => { - describe('Transition', () => { - function checkWarning( - template: string, - shouldWarn: boolean, - message = ` expects exactly one child element or component.` - ) { - const spy = jest.fn() - compile(template.trim(), { - hoistStatic: true, - transformHoist: null, - onError: err => { - spy(err.message) - } - }) - - if (shouldWarn) expect(spy).toHaveBeenCalledWith(message) - else expect(spy).not.toHaveBeenCalled() - } - - test('warns if multiple children', () => { - checkWarning( - ` - -
hey
-
hey
-
- `, - true - ) - }) - - test('warns with v-for', () => { - checkWarning( - ` - -
hey
-
- `, - true - ) - }) - - test('warns with multiple v-if + v-for', () => { - checkWarning( - ` - -
hey
-
hey
-
- `, - true - ) - }) - - test('warns with template v-if', () => { - checkWarning( - ` - - - - `, - true - ) - }) - - test('warns with multiple templates', () => { - checkWarning( - ` - - - - - `, - true - ) - }) - - test('warns if multiple children with v-if', () => { - checkWarning( - ` - -
hey
-
hey
-
- `, - true - ) - }) - - test('does not warn with regular element', () => { - checkWarning( - ` - -
hey
-
- `, - false - ) - }) - - test('does not warn with one single v-if', () => { - checkWarning( - ` - -
hey
-
- `, - false - ) - }) - - test('does not warn with v-if v-else-if v-else', () => { - checkWarning( - ` - -
hey
-
hey
-
hey
-
- `, - false - ) - }) - - test('does not warn with v-if v-else', () => { - checkWarning( - ` - -
hey
-
hey
-
- `, - false - ) - }) - }) -}) - -test('the v-if/else-if/else branchs in Transition should ignore comments', () => { - expect( - compile(` - -
hey
- -
hey
- -
-

- -

-

-
- `).code - ).toMatchSnapshot() -}) diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 3687d84bd..2c6f71cef 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -16,7 +16,7 @@ import { transformVText } from './transforms/vText' import { transformModel } from './transforms/vModel' import { transformOn } from './transforms/vOn' import { transformShow } from './transforms/vShow' -import { warnTransitionChildren } from './transforms/warnTransitionChildren' +import { transformTransition } from './transforms/Transition' import { stringifyStatic } from './transforms/stringifyStatic' import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags' import { extend } from '@vue/shared' @@ -25,7 +25,7 @@ export { parserOptions } export const DOMNodeTransforms: NodeTransform[] = [ transformStyle, - ...(__DEV__ ? [warnTransitionChildren] : []) + ...(__DEV__ ? [transformTransition] : []) ] export const DOMDirectiveTransforms: Record = { diff --git a/packages/compiler-dom/src/transforms/warnTransitionChildren.ts b/packages/compiler-dom/src/transforms/Transition.ts similarity index 62% rename from packages/compiler-dom/src/transforms/warnTransitionChildren.ts rename to packages/compiler-dom/src/transforms/Transition.ts index 4e9bdba20..56d05ef03 100644 --- a/packages/compiler-dom/src/transforms/warnTransitionChildren.ts +++ b/packages/compiler-dom/src/transforms/Transition.ts @@ -8,7 +8,7 @@ import { import { TRANSITION } from '../runtimeHelpers' import { createDOMCompilerError, DOMErrorCodes } from '../errors' -export const warnTransitionChildren: NodeTransform = (node, context) => { +export const transformTransition: NodeTransform = (node, context) => { if ( node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.COMPONENT @@ -16,7 +16,12 @@ export const warnTransitionChildren: NodeTransform = (node, context) => { const component = context.isBuiltInComponent(node.tag) if (component === TRANSITION) { return () => { - if (node.children.length && hasMultipleChildren(node)) { + if (!node.children.length) { + return + } + + // warn multiple transition children + if (hasMultipleChildren(node)) { context.onError( createDOMCompilerError( DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, @@ -28,6 +33,22 @@ export const warnTransitionChildren: NodeTransform = (node, context) => { ) ) } + + // check if it's s single child w/ v-show + // if yes, inject "persisted: true" to the transition props + const child = node.children[0] + if (child.type === NodeTypes.ELEMENT) { + for (const p of child.props) { + if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') { + node.props.push({ + type: NodeTypes.ATTRIBUTE, + name: 'persisted', + value: undefined, + loc: node.loc + }) + } + } + } } } } diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index e257e3254..a268d86cd 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -279,6 +279,7 @@ function stringifyElement( context: TransformContext ): string { let res = `<${node.tag}` + let innerHTML = '' for (let i = 0; i < node.props.length; i++) { const p = node.props[i] if (p.type === NodeTypes.ATTRIBUTE) { @@ -286,28 +287,38 @@ function stringifyElement( if (p.value) { res += `="${escapeHtml(p.value.content)}"` } - } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { - const exp = p.exp as SimpleExpressionNode - if (exp.content[0] === '_') { - // internally generated string constant references - // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin - res += ` ${(p.arg as SimpleExpressionNode).content}="__VUE_EXP_START__${ - exp.content - }__VUE_EXP_END__"` - continue - } - // constant v-bind, e.g. :foo="1" - let evaluated = evaluateConstant(exp) - if (evaluated != null) { - const arg = p.arg && (p.arg as SimpleExpressionNode).content - if (arg === 'class') { - evaluated = normalizeClass(evaluated) - } else if (arg === 'style') { - evaluated = stringifyStyle(normalizeStyle(evaluated)) + } else if (p.type === NodeTypes.DIRECTIVE) { + if (p.name === 'bind') { + const exp = p.exp as SimpleExpressionNode + if (exp.content[0] === '_') { + // internally generated string constant references + // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin + res += ` ${ + (p.arg as SimpleExpressionNode).content + }="__VUE_EXP_START__${exp.content}__VUE_EXP_END__"` + continue } - res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( - evaluated - )}"` + // constant v-bind, e.g. :foo="1" + let evaluated = evaluateConstant(exp) + if (evaluated != null) { + const arg = p.arg && (p.arg as SimpleExpressionNode).content + if (arg === 'class') { + evaluated = normalizeClass(evaluated) + } else if (arg === 'style') { + evaluated = stringifyStyle(normalizeStyle(evaluated)) + } + res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( + evaluated + )}"` + } + } else if (p.name === 'html') { + // #5439 v-html with constant value + // not sure why would anyone do this but it can happen + innerHTML = evaluateConstant(p.exp as SimpleExpressionNode) + } else if (p.name === 'text') { + innerHTML = escapeHtml( + toDisplayString(evaluateConstant(p.exp as SimpleExpressionNode)) + ) } } } @@ -315,8 +326,12 @@ function stringifyElement( res += ` ${context.scopeId}` } res += `>` - for (let i = 0; i < node.children.length; i++) { - res += stringifyNode(node.children[i], context) + if (innerHTML) { + res += innerHTML + } else { + for (let i = 0; i < node.children.length; i++) { + res += stringifyNode(node.children[i], context) + } } if (!isVoidTag(node.tag)) { res += `` @@ -330,7 +345,7 @@ function stringifyElement( // here, e.g. `{{ 1 }}` or `{{ 'foo' }}` // in addition, constant exps bail on presence of parens so you can't even // run JSFuck in here. But we mark it unsafe for security review purposes. -// (see compiler-core/src/transformExpressions) +// (see compiler-core/src/transforms/transformExpression) function evaluateConstant(exp: ExpressionNode): string { if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { return new Function(`return ${exp.content}`)() diff --git a/packages/compiler-dom/src/transforms/vText.ts b/packages/compiler-dom/src/transforms/vText.ts index 862d2c204..77bf0032b 100644 --- a/packages/compiler-dom/src/transforms/vText.ts +++ b/packages/compiler-dom/src/transforms/vText.ts @@ -3,7 +3,8 @@ import { createObjectProperty, createSimpleExpression, TO_DISPLAY_STRING, - createCallExpression + createCallExpression, + getConstantType } from '@vue/compiler-core' import { createDOMCompilerError, DOMErrorCodes } from '../errors' @@ -25,11 +26,13 @@ export const transformVText: DirectiveTransform = (dir, node, context) => { createObjectProperty( createSimpleExpression(`textContent`, true), exp - ? createCallExpression( - context.helperString(TO_DISPLAY_STRING), - [exp], - loc - ) + ? getConstantType(exp, context) > 0 + ? exp + : createCallExpression( + context.helperString(TO_DISPLAY_STRING), + [exp], + loc + ) : createSimpleExpression('', true) ) ] diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index de223bf91..690051f6d 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1,5 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`SFC analyze + + `) + assertCode(content) + }) + describe(' + + `) + expect(content).toMatch(`return { a, b, Baz }`) + assertCode(content) + }) }) describe('inlineTemplate mode', () => { @@ -502,6 +529,7 @@ defineExpose({ foo: 123 }) import { ref } from 'vue' import Foo, { bar } from './Foo.vue' import other from './util' + import * as tree from './tree' const count = ref(0) const constant = {} const maybe = foo() @@ -511,6 +539,7 @@ defineExpose({ foo: 123 }) `, { inlineTemplate: true } @@ -529,6 +558,8 @@ defineExpose({ foo: 123 }) expect(content).toMatch(`unref(maybe)`) // should unref() on let bindings expect(content).toMatch(`unref(lett)`) + // no need to unref namespace import (this also preserves tree-shaking) + expect(content).toMatch(`tree.foo()`) // no need to unref function declarations expect(content).toMatch(`{ onClick: fn }`) // no need to mark constant fns in patch flag @@ -1164,6 +1195,59 @@ const emit = defineEmits(['a', 'b']) assertAwaitDetection(`if (ok) { await foo } else { await bar }`) }) + test('multiple `if` nested statements', () => { + assertAwaitDetection(`if (ok) { + let a = 'foo' + await 0 + await 1 + await 2 + } else if (a) { + await 10 + if (b) { + await 0 + await 1 + } else { + let a = 'foo' + await 2 + } + if (b) { + await 3 + await 4 + } + } else { + await 5 + }`) + }) + + test('multiple `if while` nested statements', () => { + assertAwaitDetection(`if (ok) { + while (d) { + await 5 + } + while (d) { + await 5 + await 6 + if (c) { + let f = 10 + 10 + await 7 + } else { + await 8 + await 9 + } + } + }`) + }) + + test('multiple `if for` nested statements', () => { + assertAwaitDetection(`if (ok) { + for (let a of [1,2,3]) { + await a + } + for (let a of [1,2,3]) { + await a + await a + } + }`) + }) + test('should ignore await inside functions', () => { // function declaration assertAwaitDetection(`async function foo() { await bar }`, false) @@ -1550,4 +1634,59 @@ describe('SFC analyze + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).toMatch(`export default { + name: 'FooBar'`) + assertCode(content) + }) + + test('do not overwrite manual name (object)', () => { + const { content } = compile( + ` + + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).not.toMatch(`name: 'FooBar'`) + expect(content).toMatch(`name: 'Baz'`) + assertCode(content) + }) + + test('do not overwrite manual name (call)', () => { + const { content } = compile( + ` + + `, + undefined, + { + filename: 'FooBar.vue' + } + ) + expect(content).not.toMatch(`name: 'FooBar'`) + expect(content).toMatch(`name: 'Baz'`) + assertCode(content) + }) + }) }) diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts index 140dbec2e..25fb4bed2 100644 --- a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts @@ -59,7 +59,7 @@ describe('sfc props transform', () => { // function expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], { foo: 1, - bar: () => {} + bar: () => ({}) })`) assertCode(content) }) @@ -74,7 +74,7 @@ describe('sfc props transform', () => { // function expect(content).toMatch(`props: { foo: { type: Number, required: false, default: 1 }, - bar: { type: Object, required: false, default: () => {} } + bar: { type: Object, required: false, default: () => ({}) } }`) assertCode(content) }) @@ -92,11 +92,11 @@ describe('sfc props transform', () => { // function expect(content).toMatch(`props: { foo: { default: 1 }, - bar: { default: () => {} }, + bar: { default: () => ({}) }, baz: null, boola: { type: Boolean }, boolb: { type: [Boolean, Number] }, - func: { type: Function, default: () => () => {} } + func: { type: Function, default: () => (() => {}) } }`) assertCode(content) }) @@ -127,6 +127,28 @@ describe('sfc props transform', () => { }) }) + // #5425 + test('non-identifier prop names', () => { + const { content, bindings } = compile(` + + + `) + expect(content).toMatch(`x = __props["foo.bar"]`) + expect(content).toMatch(`toDisplayString(__props["foo.bar"])`) + assertCode(content) + expect(bindings).toStrictEqual({ + x: BindingTypes.SETUP_LET, + 'foo.bar': BindingTypes.PROPS, + fooBar: BindingTypes.PROPS_ALIASED, + __propsAliases: { + fooBar: 'foo.bar' + } + }) + }) + test('rest spread', () => { const { content, bindings } = compile(`