From 19e17a951c3387cbd6a1597e6cd9048a4aad4528 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 9 Apr 2023 16:50:00 +0800 Subject: [PATCH 001/142] perf(compiler): use source-map-js --- .../compiler-core/__tests__/compile.spec.ts | 2 +- packages/compiler-core/package.json | 2 +- packages/compiler-core/src/codegen.ts | 2 +- packages/compiler-sfc/__tests__/parse.spec.ts | 2 +- packages/compiler-sfc/package.json | 2 +- packages/compiler-sfc/src/compileScript.ts | 2 +- packages/compiler-sfc/src/compileStyle.ts | 2 +- packages/compiler-sfc/src/compileTemplate.ts | 6 +++++- packages/compiler-sfc/src/parse.ts | 2 +- .../compiler-sfc/src/style/preprocessors.ts | 2 +- packages/template-explorer/package.json | 2 +- packages/template-explorer/src/index.ts | 2 +- packages/vue-compat/package.json | 2 +- pnpm-lock.yaml | 17 +++++++++-------- 14 files changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/compiler-core/__tests__/compile.spec.ts b/packages/compiler-core/__tests__/compile.spec.ts index 33766f71b3b..dc4f57ad3d2 100644 --- a/packages/compiler-core/__tests__/compile.spec.ts +++ b/packages/compiler-core/__tests__/compile.spec.ts @@ -1,5 +1,5 @@ import { baseCompile as compile } from '../src' -import { SourceMapConsumer, RawSourceMap } from 'source-map' +import { SourceMapConsumer, RawSourceMap } from 'source-map-js' describe('compiler: integration tests', () => { const source = ` diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index a7c20e8e1b8..5b4b0da895c 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -35,7 +35,7 @@ "@babel/parser": "^7.21.3", "@vue/shared": "3.3.0-alpha.9", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" }, "devDependencies": { "@babel/types": "^7.21.3" diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index e8da0a4cb41..2b88ab0cfbd 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -28,7 +28,7 @@ import { getVNodeBlockHelper, getVNodeHelper } from './ast' -import { SourceMapGenerator, RawSourceMap } from 'source-map' +import { SourceMapGenerator, RawSourceMap } from 'source-map-js' import { advancePositionWithMutation, assert, diff --git a/packages/compiler-sfc/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts index 5f1db5e2499..c7a17ab1739 100644 --- a/packages/compiler-sfc/__tests__/parse.spec.ts +++ b/packages/compiler-sfc/__tests__/parse.spec.ts @@ -1,6 +1,6 @@ import { parse } from '../src' import { baseParse, baseCompile } from '@vue/compiler-core' -import { SourceMapConsumer } from 'source-map' +import { SourceMapConsumer } from 'source-map-js' describe('compiler:sfc', () => { describe('source map', () => { diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 8c97d3b3b9b..d43f0c6355f 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -41,7 +41,7 @@ "estree-walker": "^2.0.2", "magic-string": "^0.30.0", "postcss": "^8.1.10", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" }, "devDependencies": { "@babel/types": "^7.21.3", diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 35e690bb00e..575ce6caade 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -48,7 +48,7 @@ import { TSEnumDeclaration } from '@babel/types' import { walk } from 'estree-walker' -import { RawSourceMap } from 'source-map' +import { RawSourceMap } from 'source-map-js' import { CSS_VARS_HELPER, genCssVarsCode, diff --git a/packages/compiler-sfc/src/compileStyle.ts b/packages/compiler-sfc/src/compileStyle.ts index 1885569635d..47f90e824d6 100644 --- a/packages/compiler-sfc/src/compileStyle.ts +++ b/packages/compiler-sfc/src/compileStyle.ts @@ -13,7 +13,7 @@ import { StylePreprocessorResults, PreprocessLang } from './style/preprocessors' -import { RawSourceMap } from 'source-map' +import { RawSourceMap } from 'source-map-js' import { cssVarsPlugin } from './style/cssVars' import postcssModules from 'postcss-modules' diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index 9ada0e7557f..fbd100c9784 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -6,7 +6,11 @@ import { ParserOptions, RootNode } from '@vue/compiler-core' -import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map' +import { + SourceMapConsumer, + SourceMapGenerator, + RawSourceMap +} from 'source-map-js' import { transformAssetUrl, AssetURLOptions, diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts index d2b98756c49..590a0e61dd1 100644 --- a/packages/compiler-sfc/src/parse.ts +++ b/packages/compiler-sfc/src/parse.ts @@ -7,7 +7,7 @@ import { BindingMetadata } from '@vue/compiler-core' import * as CompilerDOM from '@vue/compiler-dom' -import { RawSourceMap, SourceMapGenerator } from 'source-map' +import { RawSourceMap, SourceMapGenerator } from 'source-map-js' import { TemplateCompiler } from './compileTemplate' import { parseCssVars } from './style/cssVars' import { createCache } from './cache' diff --git a/packages/compiler-sfc/src/style/preprocessors.ts b/packages/compiler-sfc/src/style/preprocessors.ts index 06122f2e6b3..96c1153f05d 100644 --- a/packages/compiler-sfc/src/style/preprocessors.ts +++ b/packages/compiler-sfc/src/style/preprocessors.ts @@ -1,5 +1,5 @@ import merge from 'merge-source-map' -import { RawSourceMap } from 'source-map' +import { RawSourceMap } from 'source-map-js' import { SFCStyleCompileOptions } from '../compileStyle' import { isFunction } from '@vue/shared' diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index 81852352839..b75666ae9dd 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "monaco-editor": "^0.20.0", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } } diff --git a/packages/template-explorer/src/index.ts b/packages/template-explorer/src/index.ts index 3cf9c6b52cf..bace011ed89 100644 --- a/packages/template-explorer/src/index.ts +++ b/packages/template-explorer/src/index.ts @@ -8,7 +8,7 @@ import { ssrMode } from './options' import { toRaw, watchEffect } from '@vue/runtime-dom' -import { SourceMapConsumer } from 'source-map' +import { SourceMapConsumer } from 'source-map-js' import theme from './theme' declare global { diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 5032c134cf8..ce29f6a5752 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -40,7 +40,7 @@ "dependencies": { "@babel/parser": "^7.21.3", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" }, "peerDependencies": { "vue": "3.3.0-alpha.9" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43e31dc8855..63322096743 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,12 +104,12 @@ importers: '@babel/types': ^7.21.3 '@vue/shared': 3.3.0-alpha.9 estree-walker: ^2.0.2 - source-map: ^0.6.1 + source-map-js: ^1.0.2 dependencies: '@babel/parser': 7.21.3 '@vue/shared': link:../shared estree-walker: 2.0.2 - source-map: 0.6.1 + source-map-js: 1.0.2 devDependencies: '@babel/types': 7.21.3 @@ -143,7 +143,7 @@ importers: postcss-selector-parser: ^6.0.4 pug: ^3.0.1 sass: ^1.26.9 - source-map: ^0.6.1 + source-map-js: ^1.0.2 dependencies: '@babel/parser': 7.21.3 '@vue/compiler-core': link:../compiler-core @@ -154,7 +154,7 @@ importers: estree-walker: 2.0.2 magic-string: 0.30.0 postcss: 8.4.21 - source-map: 0.6.1 + source-map-js: 1.0.2 devDependencies: '@babel/types': 7.21.3 '@types/estree': 0.0.48 @@ -270,10 +270,10 @@ importers: packages/template-explorer: specifiers: monaco-editor: ^0.20.0 - source-map: ^0.6.1 + source-map-js: ^1.0.2 dependencies: monaco-editor: 0.20.0 - source-map: 0.6.1 + source-map-js: 1.0.2 packages/vue: specifiers: @@ -293,11 +293,11 @@ importers: specifiers: '@babel/parser': ^7.21.3 estree-walker: ^2.0.2 - source-map: ^0.6.1 + source-map-js: ^1.0.2 dependencies: '@babel/parser': 7.21.3 estree-walker: 2.0.2 - source-map: 0.6.1 + source-map-js: 1.0.2 packages: @@ -4910,6 +4910,7 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + dev: true /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} From 37701a88c0a092d3d12e0125320122eaebf55e74 Mon Sep 17 00:00:00 2001 From: JayFate <48240828+JayFate@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:06:28 +0800 Subject: [PATCH 002/142] chore: replace brotli with node:zlib (#8045) Signed-off-by: JayFate <48240828+JayFate@users.noreply.github.com> --- package.json | 1 - packages/size-check/brotli.js | 4 ++-- pnpm-lock.yaml | 8 -------- scripts/build.js | 5 ++--- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 4f565fdb2d9..35d89e177a4 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "@typescript-eslint/parser": "^5.56.0", "@vitest/coverage-istanbul": "^0.29.7", "@vue/consolidate": "0.17.3", - "brotli": "^1.3.2", "chalk": "^4.1.0", "conventional-changelog-cli": "^2.0.31", "enquirer": "^2.3.2", diff --git a/packages/size-check/brotli.js b/packages/size-check/brotli.js index 1e7ea0c774b..f9dedac0b1c 100644 --- a/packages/size-check/brotli.js +++ b/packages/size-check/brotli.js @@ -1,6 +1,6 @@ -const { compress } = require('brotli') +const { brotliCompressSync } = require('zlib') const file = require('fs').readFileSync('dist/index.js') -const compressed = compress(file) +const compressed = brotliCompressSync(file) const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' console.log(`brotli: ${compressedSize}`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63322096743..73e9b2c6c81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,6 @@ importers: '@typescript-eslint/parser': ^5.56.0 '@vitest/coverage-istanbul': ^0.29.7 '@vue/consolidate': 0.17.3 - brotli: ^1.3.2 chalk: ^4.1.0 conventional-changelog-cli: ^2.0.31 enquirer: ^2.3.2 @@ -65,7 +64,6 @@ importers: '@typescript-eslint/parser': 5.56.0_qesohl5arz7pvqyycxtsqomlr4 '@vitest/coverage-istanbul': 0.29.7_vitest@0.29.7 '@vue/consolidate': 0.17.3 - brotli: 1.3.3 chalk: 4.1.2 conventional-changelog-cli: 2.2.2 enquirer: 2.3.6 @@ -1467,12 +1465,6 @@ packages: fill-range: 7.0.1 dev: true - /brotli/1.3.3: - resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - dependencies: - base64-js: 1.5.1 - dev: true - /browserslist/4.21.5: resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} diff --git a/scripts/build.js b/scripts/build.js index 5272c49c388..05ed32ebc5c 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -20,8 +20,7 @@ import fs from 'node:fs/promises' import { existsSync, readFileSync, rmSync } from 'node:fs' import path from 'node:path' import minimist from 'minimist' -import { gzipSync } from 'node:zlib' -import { compress } from 'brotli' +import { gzipSync, brotliCompressSync } from 'node:zlib' import chalk from 'chalk' import execa from 'execa' import { cpus } from 'node:os' @@ -143,7 +142,7 @@ function checkFileSize(filePath) { const minSize = (file.length / 1024).toFixed(2) + 'kb' const gzipped = gzipSync(file) const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb' - const compressed = compress(file) + const compressed = brotliCompressSync(file) // @ts-ignore const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' console.log( From 2ce23f0e0971304ebbad9ee92afa417eb081eb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Mon, 10 Apr 2023 14:06:56 +0800 Subject: [PATCH 003/142] chore(reactivity): remove unnecessary type assertions. (#8046) --- packages/reactivity/src/reactive.ts | 2 +- packages/reactivity/src/ref.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index b51a5027972..1881955cf1c 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -414,4 +414,4 @@ export const toReactive = (value: T): T => * @param value - The value for which a readonly proxy shall be created. */ export const toReadonly = (value: T): T => - isObject(value) ? readonly(value as Record) : value + isObject(value) ? readonly(value) : value diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index b04c393669b..a5224d7f281 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -432,7 +432,7 @@ export function toRef( if (isRef(source)) { return source } else if (isFunction(source)) { - return new GetterRefImpl(source as () => unknown) as any + return new GetterRefImpl(source) as any } else if (isObject(source) && arguments.length > 1) { return propertyToRef(source, key!, defaultValue) } else { From c94ef02421d7422bc59d10cf2eee9f4e7dcea6c8 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Mon, 10 Apr 2023 02:06:21 -0500 Subject: [PATCH 004/142] fix(runtime-core): properly merge props and emits options from mixins (#8052) close #7989 --- .../__tests__/componentEmits.spec.ts | 54 ++++++++++++++++++- packages/runtime-core/src/apiSetupHelpers.ts | 9 +++- packages/runtime-core/src/componentOptions.ts | 36 +++++++++++-- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts index 0e5f1e3983d..d8774b078b6 100644 --- a/packages/runtime-core/__tests__/componentEmits.spec.ts +++ b/packages/runtime-core/__tests__/componentEmits.spec.ts @@ -8,7 +8,8 @@ import { h, nodeOps, toHandlers, - nextTick + nextTick, + ComponentPublicInstance } from '@vue/runtime-test' import { isEmitListener } from '../src/componentEmits' @@ -454,4 +455,55 @@ describe('component: emit', () => { await nextTick() expect(fn).not.toHaveBeenCalled() }) + + test('merge string array emits', async () => { + const ComponentA = defineComponent({ + emits: ['one', 'two'] + }) + const ComponentB = defineComponent({ + emits: ['three'] + }) + const renderFn = vi.fn(function (this: ComponentPublicInstance) { + expect(this.$options.emits).toEqual(['one', 'two', 'three']) + return h('div') + }) + const ComponentC = defineComponent({ + render: renderFn, + mixins: [ComponentA, ComponentB] + }) + const el = nodeOps.createElement('div') + expect(renderFn).toHaveBeenCalledTimes(0) + render(h(ComponentC), el) + expect(renderFn).toHaveBeenCalledTimes(1) + }) + + test('merge object emits', async () => { + const twoFn = vi.fn((v: unknown) => !v) + const ComponentA = defineComponent({ + emits: { + one: null, + two: twoFn + } + }) + const ComponentB = defineComponent({ + emits: ['three'] + }) + const renderFn = vi.fn(function (this: ComponentPublicInstance) { + expect(this.$options.emits).toEqual({ + one: null, + two: twoFn, + three: null + }) + expect(this.$options.emits.two).toBe(twoFn) + return h('div') + }) + const ComponentC = defineComponent({ + render: renderFn, + mixins: [ComponentA, ComponentB] + }) + const el = nodeOps.createElement('div') + expect(renderFn).toHaveBeenCalledTimes(0) + render(h(ComponentC), el) + expect(renderFn).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index e0fe434210f..de7426ad325 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -396,10 +396,15 @@ function getContext(): SetupContext { return i.setupContext || (i.setupContext = createSetupContext(i)) } -function normalizePropsOrEmits(props: ComponentPropsOptions | EmitsOptions) { +/** + * @internal + */ +export function normalizePropsOrEmits( + props: ComponentPropsOptions | EmitsOptions +) { return isArray(props) ? props.reduce( - (normalized, p) => ((normalized[p] = {}), normalized), + (normalized, p) => ((normalized[p] = null), normalized), {} as ComponentObjectPropsOptions | ObjectEmitsOptions ) : props diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 481c2adb67e..bba5ade9ad4 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -51,7 +51,8 @@ import { import { ComponentObjectPropsOptions, ExtractPropTypes, - ExtractDefaultPropTypes + ExtractDefaultPropTypes, + ComponentPropsOptions } from './componentProps' import { EmitsOptions, EmitsToProps } from './componentEmits' import { Directive } from './directives' @@ -75,6 +76,7 @@ import { import { OptionMergeFunction } from './apiCreateApp' import { LifecycleHooks } from './enums' import { SlotsType } from './componentSlots' +import { normalizePropsOrEmits } from './apiSetupHelpers' /** * Interface for declaring custom options. @@ -1069,8 +1071,8 @@ export function mergeOptions( export const internalOptionMergeStrats: Record = { data: mergeDataFn, - props: mergeObjectOptions, // TODO - emits: mergeObjectOptions, // TODO + props: mergeEmitsOrPropsOptions, + emits: mergeEmitsOrPropsOptions, // objects methods: mergeObjectOptions, computed: mergeObjectOptions, @@ -1147,7 +1149,33 @@ function mergeAsArray(to: T[] | T | undefined, from: T | T[]) { } function mergeObjectOptions(to: Object | undefined, from: Object | undefined) { - return to ? extend(extend(Object.create(null), to), from) : from + return to ? extend(Object.create(null), to, from) : from +} + +function mergeEmitsOrPropsOptions( + to: EmitsOptions | undefined, + from: EmitsOptions | undefined +): EmitsOptions | undefined +function mergeEmitsOrPropsOptions( + to: ComponentPropsOptions | undefined, + from: ComponentPropsOptions | undefined +): ComponentPropsOptions | undefined +function mergeEmitsOrPropsOptions( + to: ComponentPropsOptions | EmitsOptions | undefined, + from: ComponentPropsOptions | EmitsOptions | undefined +) { + if (to) { + if (isArray(to) && isArray(from)) { + return [...new Set([...to, ...from])] + } + return extend( + Object.create(null), + normalizePropsOrEmits(to), + normalizePropsOrEmits(from ?? {}) + ) + } else { + return from + } } function mergeWatchOptions( From b16866d56bdd278fd128f9cc2241262304e3259f Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 10 Apr 2023 15:16:29 +0800 Subject: [PATCH 005/142] chore: update treeshaken deps list --- rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.js b/rollup.config.js index 21ee72b4ea3..0b5ffa3b067 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -215,7 +215,7 @@ function createConfig(format, output, plugins = []) { } function resolveExternal() { - const treeShakenDeps = ['source-map', '@babel/parser', 'estree-walker'] + const treeShakenDeps = ['source-map-js', '@babel/parser', 'estree-walker'] if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) { if (!packageOptions.enableNonBrowserBranches) { From acd7eb22cfa298ce9cc75e9876e625fa0ff20bd1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 11 Apr 2023 13:06:04 +0800 Subject: [PATCH 006/142] refactor(compiler-sfc): (wip) split compileScript logic, use context, move defineProps --- packages/compiler-sfc/src/compileScript.ts | 415 +++++------------- packages/compiler-sfc/src/script/context.ts | 153 +++++++ .../compiler-sfc/src/script/defineProps.ts | 148 +++++++ ...structure.ts => definePropsDestructure.ts} | 0 .../compiler-sfc/src/script/resolveType.ts | 114 +++++ packages/compiler-sfc/src/script/utils.ts | 14 + 6 files changed, 543 insertions(+), 301 deletions(-) create mode 100644 packages/compiler-sfc/src/script/context.ts create mode 100644 packages/compiler-sfc/src/script/defineProps.ts rename packages/compiler-sfc/src/script/{propsDestructure.ts => definePropsDestructure.ts} (100%) create mode 100644 packages/compiler-sfc/src/script/resolveType.ts create mode 100644 packages/compiler-sfc/src/script/utils.ts diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 575ce6caade..5dacc84d552 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -15,12 +15,7 @@ import { isCallOf } from '@vue/compiler-dom' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' -import { - parse as _parse, - parseExpression, - ParserOptions, - ParserPlugin -} from '@babel/parser' +import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser' import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared' import { Node, @@ -41,7 +36,6 @@ import { TSInterfaceBody, TSTypeElement, AwaitExpression, - Program, ObjectMethod, LVal, Expression, @@ -59,13 +53,19 @@ import { warnOnce } from './warn' import { rewriteDefaultAST } from './rewriteDefault' import { createCache } from './cache' import { shouldTransform, transformAST } from '@vue/reactivity-transform' -import { transformDestructuredProps } from './script/propsDestructure' +import { transformDestructuredProps } from './script/definePropsDestructure' +import { resolveObjectKey, FromNormalScript } from './script/utils' +import { ScriptCompileContext } from './script/context' +import { + processDefineProps, + DEFINE_PROPS, + WITH_DEFAULTS, + PropsDeclType +} from './script/defineProps' // Special compiler macros -const DEFINE_PROPS = 'defineProps' const DEFINE_EMITS = 'defineEmits' const DEFINE_EXPOSE = 'defineExpose' -const WITH_DEFAULTS = 'withDefaults' const DEFINE_OPTIONS = 'defineOptions' const DEFINE_SLOTS = 'defineSlots' const DEFINE_MODEL = 'defineModel' @@ -149,8 +149,6 @@ export type PropsDestructureBindings = Record< } > -type FromNormalScript = T & { __fromNormalScript?: boolean | null } -type PropsDeclType = FromNormalScript type EmitsDeclType = FromNormalScript< TSFunctionType | TSTypeLiteral | TSInterfaceBody > @@ -195,41 +193,15 @@ export function compileScript( ? `const ${options.genDefaultAs} =` : `export default` const normalScriptDefaultVar = `__default__` - const isJS = - scriptLang === 'js' || - scriptLang === 'jsx' || - scriptSetupLang === 'js' || - scriptSetupLang === 'jsx' - const isTS = - scriptLang === 'ts' || - scriptLang === 'tsx' || - scriptSetupLang === 'ts' || - scriptSetupLang === 'tsx' - - // resolve parser plugins - const plugins: ParserPlugin[] = [] - if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { - plugins.push('jsx') - } else { - // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins - if (options.babelParserPlugins) - options.babelParserPlugins = options.babelParserPlugins.filter( - n => n !== 'jsx' - ) - } - if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) - if (isTS) { - plugins.push('typescript') - if (!plugins.includes('decorators')) { - plugins.push('decorators-legacy') - } - } + + const ctx = new ScriptCompileContext(sfc, options) + const { isTS } = ctx if (!scriptSetup) { if (!script) { throw new Error(`[@vue/compiler-sfc] SFC contains no - `) - // should generate working code - assertCode(content) - // should analyze bindings - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.LITERAL_CONST, - props: BindingTypes.SETUP_REACTIVE_CONST - }) - - // should remove defineOptions import and call - expect(content).not.toMatch('defineProps') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { expose: __expose }) {`) - // should assign user identifier to it - expect(content).toMatch(`const props = __props`) - // should include context options in default export - expect(content).toMatch(`export default { - props: { - foo: String -},`) - }) - - test('defineProps w/ external definition', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`export default { - props: propsModel,`) - }) - - // #4764 - test('defineProps w/ leading code', () => { - const { content } = compile(` - - `) - // props declaration should be inside setup, not moved along with the import - expect(content).not.toMatch(`const props = __props\nimport`) - assertCode(content) - }) - - test('defineEmits()', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(bindings).toStrictEqual({ - myEmit: BindingTypes.SETUP_CONST - }) - // should remove defineEmits import and call - expect(content).not.toMatch('defineEmits') - // should generate correct setup signature - expect(content).toMatch( - `setup(__props, { expose: __expose, emit: myEmit }) {` - ) - // should include context options in default export - expect(content).toMatch(`export default { - emits: ['foo', 'bar'],`) - }) - test('defineProps/defineEmits in multi-variable declaration', () => { const { content } = compile(` - `) - assertCode(content) - // should remove defineOptions import and call - expect(content).not.toMatch('defineOptions') - // should include context options in default export - expect(content).toMatch( - `export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, ` - ) - }) - - test('empty argument', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`export default {`) - // should remove defineOptions import and call - expect(content).not.toMatch('defineOptions') - }) - - it('should emit an error with two defineProps', () => { - expect(() => - compile(` - - `) - ).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call') - }) - - it('should emit an error with props or emits property', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead.' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead.' - ) - }) - - it('should emit an error with type generic', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot accept type arguments' - ) - }) - - it('should emit an error with type assertion', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' - ) - }) - - it('should emit an error with declaring props/emits/slots/expose', () => { - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead' - ) - - expect(() => - compile(` - - `) - ).toThrowError( - '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead' - ) - }) - }) - - test('defineExpose()', () => { - const { content } = compile(` - - `) - assertCode(content) - // should remove defineOptions import and call - expect(content).not.toMatch('defineExpose') - // should generate correct setup signature - expect(content).toMatch(`setup(__props, { expose: __expose }) {`) - // should replace callee - expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/) - }) - - describe('defineModel()', () => { - test('basic usage', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch('props: {') - expect(content).toMatch('"modelValue": { required: true },') - expect(content).toMatch('"count": {},') - expect(content).toMatch('emits: ["update:modelValue", "update:count"],') - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const c = _useModel(__props, "count")`) - expect(content).toMatch(`return { modelValue, c }`) - expect(content).not.toMatch('defineModel') - - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - count: BindingTypes.PROPS, - c: BindingTypes.SETUP_REF - }) - }) - - test('w/ defineProps and defineEmits', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch(`props: _mergeModels({ foo: String }`) - expect(content).toMatch(`"modelValue": { default: 0 }`) - expect(content).toMatch(`const count = _useModel(__props, "modelValue")`) - expect(content).not.toMatch('defineModel') - expect(bindings).toStrictEqual({ - count: BindingTypes.SETUP_REF, - foo: BindingTypes.PROPS, - modelValue: BindingTypes.PROPS - }) - }) - - test('w/ array props', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], { - "count": {}, - })`) - expect(content).toMatch(`const count = _useModel(__props, "count")`) - expect(content).not.toMatch('defineModel') - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - count: BindingTypes.SETUP_REF - }) - }) - - test('w/ local flag', () => { - const { content } = compile( - ``, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch( - `_useModel(__props, "modelValue", { local: true })` - ) - expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`) - expect(content).toMatch(`_useModel(__props, "baz", { ...x })`) - expect(content).toMatch(`_useModel(__props, "qux", x)`) - expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`) - expect(content).toMatch(`_useModel(__props, "hoist", { local })`) - }) - }) - - test(' - - `) - assertCode(content) - }) - describe(' - `) - assertCode(content) - expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ - props: { foo: String }, - emits: ['a', 'b'], - setup(__props, { expose: __expose, emit }) {`) - }) - - test('defineProps w/ type', () => { - const { content, bindings } = compile(` - `) - assertCode(content) - expect(content).toMatch(`string: { type: String, required: true }`) - expect(content).toMatch(`number: { type: Number, required: true }`) - expect(content).toMatch(`boolean: { type: Boolean, required: true }`) - expect(content).toMatch(`object: { type: Object, required: true }`) - expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) - expect(content).toMatch(`fn: { type: Function, required: true }`) - expect(content).toMatch(`functionRef: { type: Function, required: true }`) - expect(content).toMatch(`objectRef: { type: Object, required: true }`) - expect(content).toMatch(`dateTime: { type: Date, required: true }`) - expect(content).toMatch(`array: { type: Array, required: true }`) - expect(content).toMatch(`arrayRef: { type: Array, required: true }`) - expect(content).toMatch(`tuple: { type: Array, required: true }`) - expect(content).toMatch(`set: { type: Set, required: true }`) - expect(content).toMatch(`literal: { type: String, required: true }`) - expect(content).toMatch(`optional: { type: null, required: false }`) - expect(content).toMatch(`recordRef: { type: Object, required: true }`) - expect(content).toMatch(`interface: { type: Object, required: true }`) - expect(content).toMatch(`alias: { type: Array, required: true }`) - expect(content).toMatch(`method: { type: Function, required: true }`) - expect(content).toMatch(`symbol: { type: Symbol, required: true }`) - expect(content).toMatch( - `objectOrFn: { type: [Function, Object], required: true },` - ) - expect(content).toMatch(`extract: { type: Number, required: true }`) - expect(content).toMatch( - `exclude: { type: [Number, Boolean], required: true }` - ) - expect(content).toMatch(`uppercase: { type: String, required: true }`) - expect(content).toMatch(`params: { type: Array, required: true }`) - expect(content).toMatch(`nonNull: { type: String, required: true }`) - expect(content).toMatch( - `union: { type: [String, Number], required: true }` - ) - expect(content).toMatch(`literalUnion: { type: String, required: true }`) - expect(content).toMatch( - `literalUnionNumber: { type: Number, required: true }` - ) - expect(content).toMatch( - `literalUnionMixed: { type: [String, Number, Boolean], required: true }` - ) - expect(content).toMatch(`intersection: { type: Object, required: true }`) - expect(content).toMatch(`intersection2: { type: String, required: true }`) - expect(content).toMatch(`foo: { type: [Function, null], required: true }`) - expect(content).toMatch(`unknown: { type: null, required: true }`) - // uninon containing unknown type: skip check - expect(content).toMatch(`unknownUnion: { type: null, required: true }`) - // intersection containing unknown type: narrow to the known types - expect(content).toMatch( - `unknownIntersection: { type: Object, required: true },` - ) - expect(content).toMatch( - `unknownUnionWithBoolean: { type: Boolean, required: true, skipCheck: true },` - ) - expect(content).toMatch( - `unknownUnionWithFunction: { type: Function, required: true, skipCheck: true }` - ) - expect(bindings).toStrictEqual({ - string: BindingTypes.PROPS, - number: BindingTypes.PROPS, - boolean: BindingTypes.PROPS, - object: BindingTypes.PROPS, - objectLiteral: BindingTypes.PROPS, - fn: BindingTypes.PROPS, - functionRef: BindingTypes.PROPS, - objectRef: BindingTypes.PROPS, - dateTime: BindingTypes.PROPS, - array: BindingTypes.PROPS, - arrayRef: BindingTypes.PROPS, - tuple: BindingTypes.PROPS, - set: BindingTypes.PROPS, - literal: BindingTypes.PROPS, - optional: BindingTypes.PROPS, - recordRef: BindingTypes.PROPS, - interface: BindingTypes.PROPS, - alias: BindingTypes.PROPS, - method: BindingTypes.PROPS, - symbol: BindingTypes.PROPS, - objectOrFn: BindingTypes.PROPS, - extract: BindingTypes.PROPS, - exclude: BindingTypes.PROPS, - union: BindingTypes.PROPS, - literalUnion: BindingTypes.PROPS, - literalUnionNumber: BindingTypes.PROPS, - literalUnionMixed: BindingTypes.PROPS, - intersection: BindingTypes.PROPS, - intersection2: BindingTypes.PROPS, - foo: BindingTypes.PROPS, - uppercase: BindingTypes.PROPS, - params: BindingTypes.PROPS, - nonNull: BindingTypes.PROPS, - unknown: BindingTypes.PROPS, - unknownUnion: BindingTypes.PROPS, - unknownIntersection: BindingTypes.PROPS, - unknownUnionWithBoolean: BindingTypes.PROPS, - unknownUnionWithFunction: BindingTypes.PROPS - }) - }) - - test('defineProps w/ interface', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ extends interface', () => { - const { content, bindings } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`z: { type: Number, required: true }`) - expect(content).toMatch(`y: { type: String, required: true }`) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS, - y: BindingTypes.PROPS, - z: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported interface', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported interface in normal script', () => { - const { content, bindings } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ type alias', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ exported type alias', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`x: { type: Number, required: false }`) - expect(bindings).toStrictEqual({ - x: BindingTypes.PROPS - }) - }) - - test('defineProps w/ TS assertion', () => { - const { content, bindings } = compile(` - - `) - expect(content).toMatch(`props: ['foo']`) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS - }) - }) - - test('withDefaults (static)', () => { - const { content, bindings } = compile(` - - `) - assertCode(content) - expect(content).toMatch( - `foo: { type: String, required: false, default: 'hi' }` - ) - expect(content).toMatch(`bar: { type: Number, required: false }`) - expect(content).toMatch(`baz: { type: Boolean, required: true }`) - expect(content).toMatch( - `qux: { type: Function, required: false, default() { return 1 } }` - ) - expect(content).toMatch( - `quux: { type: Function, required: false, default() { } }` - ) - expect(content).toMatch( - `quuxx: { type: Promise, required: false, async default() { return await Promise.resolve('hi') } }` - ) - expect(content).toMatch( - `fred: { type: String, required: false, get default() { return 'fred' } }` - ) - expect(content).toMatch(`const props = __props`) - expect(bindings).toStrictEqual({ - foo: BindingTypes.PROPS, - bar: BindingTypes.PROPS, - baz: BindingTypes.PROPS, - qux: BindingTypes.PROPS, - quux: BindingTypes.PROPS, - quuxx: BindingTypes.PROPS, - fred: BindingTypes.PROPS, - props: BindingTypes.SETUP_CONST - }) - }) - - test('withDefaults (static) + normal script', () => { - const { content } = compile(` - - - `) - assertCode(content) - }) - - // #7111 - test('withDefaults (static) w/ production mode', () => { - const { content } = compile( - ` - - `, - { isProd: true } - ) - assertCode(content) - expect(content).toMatch(`const props = __props`) - - // foo has no default value, the Function can be dropped - expect(content).toMatch(`foo: {}`) - expect(content).toMatch(`bar: { type: Boolean }`) - expect(content).toMatch( - `baz: { type: [Boolean, Function], default: true }` - ) - expect(content).toMatch(`qux: { default: 'hi' }`) - }) - - test('withDefaults (dynamic)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: String, required: false }, - bar: { type: Number, required: false }, - baz: { type: Boolean, required: true } - }, { ...defaults })`.trim() - ) - }) - - test('withDefaults (reference)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: String, required: false }, - bar: { type: Number, required: false }, - baz: { type: Boolean, required: true } - }, defaults)`.trim() - ) - }) - - // #7111 - test('withDefaults (dynamic) w/ production mode', () => { - const { content } = compile( - ` - - `, - { isProd: true } - ) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: Function }, - bar: { type: Boolean }, - baz: { type: [Boolean, Function] }, - qux: {} - }, { ...defaults })`.trim() - ) - }) - - test('withDefaults w/ dynamic object method', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) - expect(content).toMatch( - ` - _mergeDefaults({ - foo: { type: Function, required: false } - }, { - ['fo' + 'o']() { return 'foo' } - })`.trim() - ) - }) - - test('defineEmits w/ type', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (union)', () => { - const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` - expect(() => - compile(` - - `) - ).toThrow() - }) - - test('defineEmits w/ type (type literal w/ call signatures)', () => { - const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}` - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar", "baz"]`) - }) - - test('defineEmits w/ type (interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported interface)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type from normal script', () => { - const { content } = compile(` - - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (exported type alias)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - test('defineEmits w/ type (referenced exported function type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ["foo", "bar"]`) - }) - - // #5393 - test('defineEmits w/ type (interface ts type)', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`emits: ['foo']`) - }) - - test('defineEmits w/ type (property syntax)', () => { - const { content } = compile(` - - `) - expect(content).toMatch(`emits: ["foo", "bar"]`) - assertCode(content) - }) - - // #8040 - test('defineEmits w/ type (property syntax string literal)', () => { - const { content } = compile(` - - `) - expect(content).toMatch(`emits: ["foo:bar"]`) - assertCode(content) - }) - - describe('defineSlots()', () => { - test('basic usage', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`const slots = _useSlots()`) - expect(content).not.toMatch('defineSlots') - }) - - test('w/o return value', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).not.toMatch('defineSlots') - expect(content).not.toMatch(`_useSlots`) - }) - - test('w/o generic params', () => { - const { content } = compile(` - - `) - assertCode(content) - expect(content).toMatch(`const slots = _useSlots()`) - expect(content).not.toMatch('defineSlots') - }) - }) - - describe('defineModel()', () => { - test('basic usage', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true } - ) - assertCode(content) - expect(content).toMatch('"modelValue": { type: [Boolean, String] }') - expect(content).toMatch('"count": { type: Number }') - expect(content).toMatch( - '"disabled": { type: Number, ...{ required: false } }' - ) - expect(content).toMatch('"any": { type: Boolean, skipCheck: true }') - expect(content).toMatch( - 'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]' - ) - - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const count = _useModel(__props, "count")`) - expect(content).toMatch( - `const disabled = _useModel(__props, "disabled")` - ) - expect(content).toMatch(`const any = _useModel(__props, "any")`) - - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - count: BindingTypes.SETUP_REF, - disabled: BindingTypes.SETUP_REF, - any: BindingTypes.SETUP_REF - }) - }) - - test('w/ production mode', () => { - const { content, bindings } = compile( - ` - - `, - { defineModel: true, isProd: true } - ) - assertCode(content) - expect(content).toMatch('"modelValue": { type: Boolean }') - expect(content).toMatch('"fn": {}') - expect(content).toMatch( - '"fnWithDefault": { type: Function, ...{ default: () => null } },' - ) - expect(content).toMatch('"str": {}') - expect(content).toMatch('"optional": { required: false }') - expect(content).toMatch( - 'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]' - ) - expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")` - ) - expect(content).toMatch(`const fn = _useModel(__props, "fn")`) - expect(content).toMatch(`const str = _useModel(__props, "str")`) - expect(bindings).toStrictEqual({ - modelValue: BindingTypes.SETUP_REF, - fn: BindingTypes.SETUP_REF, - fnWithDefault: BindingTypes.SETUP_REF, - str: BindingTypes.SETUP_REF, - optional: BindingTypes.SETUP_REF - }) - }) - }) - test('runtime Enum', () => { const { content, bindings } = compile( ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: Number`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: String`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: [String, Number]`) - - expect( - compile( - ``, - { hoistStatic: true } - ).content - ).toMatch(`foo: { type: Number`) - }) - test('import type', () => { const { content } = compile( ``) - }).toThrow(`cannot accept both type and non-type arguments`) - - expect(() => { - compile(``) - }).toThrow(`cannot accept both type and non-type arguments`) - }) - test('defineProps/Emit() referencing local var', () => { expect(() => compile(``).content ) }) - - test('mixed usage of property / call signature in defineEmits', () => { - expect(() => - compile(``) - ).toThrow( - `defineEmits() type cannot mixed call signature and property syntax.` - ) - }) }) }) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap new file mode 100644 index 00000000000..5add78a28b3 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap @@ -0,0 +1,232 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defineEmits > basic usage 1`] = ` +"export default { + emits: ['foo', 'bar'], + setup(__props, { expose: __expose, emit: myEmit }) { + __expose(); + + + +return { myEmit } +} + +}" +`; + +exports[`defineEmits > w/ runtime options 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: ['a', 'b'], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (exported interface) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (exported type alias) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export type Emits = { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (interface ts type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Emits { (e: 'foo'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: ['foo'], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (interface) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (property syntax string literal) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo:bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (property syntax) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (referenced exported function type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +export type Emits = (e: 'foo' | 'bar') => void + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (referenced function type) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +type Emits = (e: 'foo' | 'bar') => void + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (type alias) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +type Emits = { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (type literal w/ call signatures) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\", \\"baz\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type from normal script 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + + export interface Emits { (e: 'foo' | 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + +return { emit } +} + +})" +`; diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap new file mode 100644 index 00000000000..d72726460bf --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineExpose.spec.ts.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` + `) + assertCode(content) + expect(bindings).toStrictEqual({ + myEmit: BindingTypes.SETUP_CONST + }) + // should remove defineEmits import and call + expect(content).not.toMatch('defineEmits') + // should generate correct setup signature + expect(content).toMatch( + `setup(__props, { expose: __expose, emit: myEmit }) {` + ) + // should include context options in default export + expect(content).toMatch(`export default { + emits: ['foo', 'bar'],`) + }) + + test('w/ runtime options', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ + emits: ['a', 'b'], + setup(__props, { expose: __expose, emit }) {`) + }) + + test('w/ type', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (union)', () => { + const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` + expect(() => + compile(` + + `) + ).toThrow() + }) + + test('w/ type (type literal w/ call signatures)', () => { + const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}` + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar", "baz"]`) + }) + + test('w/ type (interface)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (exported interface)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type from normal script', () => { + const { content } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (type alias)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (exported type alias)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (referenced function type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + test('w/ type (referenced exported function type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar"]`) + }) + + // #5393 + test('w/ type (interface ts type)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ['foo']`) + }) + + test('w/ type (property syntax)', () => { + const { content } = compile(` + + `) + expect(content).toMatch(`emits: ["foo", "bar"]`) + assertCode(content) + }) + + // #8040 + test('w/ type (property syntax string literal)', () => { + const { content } = compile(` + + `) + expect(content).toMatch(`emits: ["foo:bar"]`) + assertCode(content) + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + expect(() => { + compile(``) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + + test('mixed usage of property / call signature', () => { + expect(() => + compile(``) + ).toThrow( + `defineEmits() type cannot mixed call signature and property syntax.` + ) + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts new file mode 100644 index 00000000000..8ddd28a89e6 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineExpose.spec.ts @@ -0,0 +1,26 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +test('defineExpose()', () => { + const { content } = compile(` + +`) + assertCode(content) + // should remove defineOptions import and call + expect(content).not.toMatch('defineExpose') + // should generate correct setup signature + expect(content).toMatch(`setup(__props, { expose: __expose }) {`) + // should replace callee + expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/) +}) + +test(' + + `) + assertCode(content) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts new file mode 100644 index 00000000000..61a9adcbe0d --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts @@ -0,0 +1,179 @@ +import { BindingTypes } from '@vue/compiler-core' +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineModel()', () => { + test('basic usage', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch('props: {') + expect(content).toMatch('"modelValue": { required: true },') + expect(content).toMatch('"count": {},') + expect(content).toMatch('emits: ["update:modelValue", "update:count"],') + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const c = _useModel(__props, "count")`) + expect(content).toMatch(`return { modelValue, c }`) + expect(content).not.toMatch('defineModel') + + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + count: BindingTypes.PROPS, + c: BindingTypes.SETUP_REF + }) + }) + + test('w/ defineProps and defineEmits', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`props: _mergeModels({ foo: String }`) + expect(content).toMatch(`"modelValue": { default: 0 }`) + expect(content).toMatch(`const count = _useModel(__props, "modelValue")`) + expect(content).not.toMatch('defineModel') + expect(bindings).toStrictEqual({ + count: BindingTypes.SETUP_REF, + foo: BindingTypes.PROPS, + modelValue: BindingTypes.PROPS + }) + }) + + test('w/ array props', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], { + "count": {}, + })`) + expect(content).toMatch(`const count = _useModel(__props, "count")`) + expect(content).not.toMatch('defineModel') + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + count: BindingTypes.SETUP_REF + }) + }) + + test('w/ local flag', () => { + const { content } = compile( + ``, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch(`_useModel(__props, "modelValue", { local: true })`) + expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`) + expect(content).toMatch(`_useModel(__props, "baz", { ...x })`) + expect(content).toMatch(`_useModel(__props, "qux", x)`) + expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`) + expect(content).toMatch(`_useModel(__props, "hoist", { local })`) + }) + + test('w/ types, basic usage', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true } + ) + assertCode(content) + expect(content).toMatch('"modelValue": { type: [Boolean, String] }') + expect(content).toMatch('"count": { type: Number }') + expect(content).toMatch( + '"disabled": { type: Number, ...{ required: false } }' + ) + expect(content).toMatch('"any": { type: Boolean, skipCheck: true }') + expect(content).toMatch( + 'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]' + ) + + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const count = _useModel(__props, "count")`) + expect(content).toMatch(`const disabled = _useModel(__props, "disabled")`) + expect(content).toMatch(`const any = _useModel(__props, "any")`) + + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + count: BindingTypes.SETUP_REF, + disabled: BindingTypes.SETUP_REF, + any: BindingTypes.SETUP_REF + }) + }) + + test('w/ types, production mode', () => { + const { content, bindings } = compile( + ` + + `, + { defineModel: true, isProd: true } + ) + assertCode(content) + expect(content).toMatch('"modelValue": { type: Boolean }') + expect(content).toMatch('"fn": {}') + expect(content).toMatch( + '"fnWithDefault": { type: Function, ...{ default: () => null } },' + ) + expect(content).toMatch('"str": {}') + expect(content).toMatch('"optional": { required: false }') + expect(content).toMatch( + 'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]' + ) + expect(content).toMatch( + `const modelValue = _useModel(__props, "modelValue")` + ) + expect(content).toMatch(`const fn = _useModel(__props, "fn")`) + expect(content).toMatch(`const str = _useModel(__props, "str")`) + expect(bindings).toStrictEqual({ + modelValue: BindingTypes.SETUP_REF, + fn: BindingTypes.SETUP_REF, + fnWithDefault: BindingTypes.SETUP_REF, + str: BindingTypes.SETUP_REF, + optional: BindingTypes.SETUP_REF + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts new file mode 100644 index 00000000000..5337a53917c --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineOptions.spec.ts @@ -0,0 +1,149 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineOptions()', () => { + test('basic usage', () => { + const { content } = compile(` + + `) + assertCode(content) + // should remove defineOptions import and call + expect(content).not.toMatch('defineOptions') + // should include context options in default export + expect(content).toMatch( + `export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, ` + ) + }) + + test('empty argument', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default {`) + // should remove defineOptions import and call + expect(content).not.toMatch('defineOptions') + }) + + it('should emit an error with two defineProps', () => { + expect(() => + compile(` + + `) + ).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call') + }) + + it('should emit an error with props or emits property', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead.' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead.' + ) + }) + + it('should emit an error with type generic', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot accept type arguments' + ) + }) + + it('should emit an error with type assertion', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.' + ) + }) + + it('should emit an error with declaring props/emits/slots/expose', () => { + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead' + ) + + expect(() => + compile(` + + `) + ).toThrowError( + '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead' + ) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts new file mode 100644 index 00000000000..cf61c98406c --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts @@ -0,0 +1,583 @@ +import { BindingTypes } from '@vue/compiler-core' +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineProps', () => { + test('basic usage', () => { + const { content, bindings } = compile(` + + `) + // should generate working code + assertCode(content) + // should analyze bindings + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.LITERAL_CONST, + props: BindingTypes.SETUP_REACTIVE_CONST + }) + + // should remove defineOptions import and call + expect(content).not.toMatch('defineProps') + // should generate correct setup signature + expect(content).toMatch(`setup(__props, { expose: __expose }) {`) + // should assign user identifier to it + expect(content).toMatch(`const props = __props`) + // should include context options in default export + expect(content).toMatch(`export default { + props: { + foo: String +},`) + }) + + test('w/ external definition', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default { + props: propsModel,`) + }) + + // #4764 + test('w/ leading code', () => { + const { content } = compile(` + + `) + // props declaration should be inside setup, not moved along with the import + expect(content).not.toMatch(`const props = __props\nimport`) + assertCode(content) + }) + + test('defineProps w/ runtime options', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ + props: { foo: String }, + setup(__props, { expose: __expose }) {`) + }) + + test('w/ type', () => { + const { content, bindings } = compile(` + `) + assertCode(content) + expect(content).toMatch(`string: { type: String, required: true }`) + expect(content).toMatch(`number: { type: Number, required: true }`) + expect(content).toMatch(`boolean: { type: Boolean, required: true }`) + expect(content).toMatch(`object: { type: Object, required: true }`) + expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) + expect(content).toMatch(`fn: { type: Function, required: true }`) + expect(content).toMatch(`functionRef: { type: Function, required: true }`) + expect(content).toMatch(`objectRef: { type: Object, required: true }`) + expect(content).toMatch(`dateTime: { type: Date, required: true }`) + expect(content).toMatch(`array: { type: Array, required: true }`) + expect(content).toMatch(`arrayRef: { type: Array, required: true }`) + expect(content).toMatch(`tuple: { type: Array, required: true }`) + expect(content).toMatch(`set: { type: Set, required: true }`) + expect(content).toMatch(`literal: { type: String, required: true }`) + expect(content).toMatch(`optional: { type: null, required: false }`) + expect(content).toMatch(`recordRef: { type: Object, required: true }`) + expect(content).toMatch(`interface: { type: Object, required: true }`) + expect(content).toMatch(`alias: { type: Array, required: true }`) + expect(content).toMatch(`method: { type: Function, required: true }`) + expect(content).toMatch(`symbol: { type: Symbol, required: true }`) + expect(content).toMatch( + `objectOrFn: { type: [Function, Object], required: true },` + ) + expect(content).toMatch(`extract: { type: Number, required: true }`) + expect(content).toMatch( + `exclude: { type: [Number, Boolean], required: true }` + ) + expect(content).toMatch(`uppercase: { type: String, required: true }`) + expect(content).toMatch(`params: { type: Array, required: true }`) + expect(content).toMatch(`nonNull: { type: String, required: true }`) + expect(content).toMatch(`union: { type: [String, Number], required: true }`) + expect(content).toMatch(`literalUnion: { type: String, required: true }`) + expect(content).toMatch( + `literalUnionNumber: { type: Number, required: true }` + ) + expect(content).toMatch( + `literalUnionMixed: { type: [String, Number, Boolean], required: true }` + ) + expect(content).toMatch(`intersection: { type: Object, required: true }`) + expect(content).toMatch(`intersection2: { type: String, required: true }`) + expect(content).toMatch(`foo: { type: [Function, null], required: true }`) + expect(content).toMatch(`unknown: { type: null, required: true }`) + // uninon containing unknown type: skip check + expect(content).toMatch(`unknownUnion: { type: null, required: true }`) + // intersection containing unknown type: narrow to the known types + expect(content).toMatch( + `unknownIntersection: { type: Object, required: true },` + ) + expect(content).toMatch( + `unknownUnionWithBoolean: { type: Boolean, required: true, skipCheck: true },` + ) + expect(content).toMatch( + `unknownUnionWithFunction: { type: Function, required: true, skipCheck: true }` + ) + expect(bindings).toStrictEqual({ + string: BindingTypes.PROPS, + number: BindingTypes.PROPS, + boolean: BindingTypes.PROPS, + object: BindingTypes.PROPS, + objectLiteral: BindingTypes.PROPS, + fn: BindingTypes.PROPS, + functionRef: BindingTypes.PROPS, + objectRef: BindingTypes.PROPS, + dateTime: BindingTypes.PROPS, + array: BindingTypes.PROPS, + arrayRef: BindingTypes.PROPS, + tuple: BindingTypes.PROPS, + set: BindingTypes.PROPS, + literal: BindingTypes.PROPS, + optional: BindingTypes.PROPS, + recordRef: BindingTypes.PROPS, + interface: BindingTypes.PROPS, + alias: BindingTypes.PROPS, + method: BindingTypes.PROPS, + symbol: BindingTypes.PROPS, + objectOrFn: BindingTypes.PROPS, + extract: BindingTypes.PROPS, + exclude: BindingTypes.PROPS, + union: BindingTypes.PROPS, + literalUnion: BindingTypes.PROPS, + literalUnionNumber: BindingTypes.PROPS, + literalUnionMixed: BindingTypes.PROPS, + intersection: BindingTypes.PROPS, + intersection2: BindingTypes.PROPS, + foo: BindingTypes.PROPS, + uppercase: BindingTypes.PROPS, + params: BindingTypes.PROPS, + nonNull: BindingTypes.PROPS, + unknown: BindingTypes.PROPS, + unknownUnion: BindingTypes.PROPS, + unknownIntersection: BindingTypes.PROPS, + unknownUnionWithBoolean: BindingTypes.PROPS, + unknownUnionWithFunction: BindingTypes.PROPS + }) + }) + + test('w/ interface', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ extends interface', () => { + const { content, bindings } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`z: { type: Number, required: true }`) + expect(content).toMatch(`y: { type: String, required: true }`) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS, + y: BindingTypes.PROPS, + z: BindingTypes.PROPS + }) + }) + + test('w/ exported interface', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ exported interface in normal script', () => { + const { content, bindings } = compile(` + + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ type alias', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ exported type alias', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`x: { type: Number, required: false }`) + expect(bindings).toStrictEqual({ + x: BindingTypes.PROPS + }) + }) + + test('w/ TS assertion', () => { + const { content, bindings } = compile(` + + `) + expect(content).toMatch(`props: ['foo']`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS + }) + }) + + test('withDefaults (static)', () => { + const { content, bindings } = compile(` + + `) + assertCode(content) + expect(content).toMatch( + `foo: { type: String, required: false, default: 'hi' }` + ) + expect(content).toMatch(`bar: { type: Number, required: false }`) + expect(content).toMatch(`baz: { type: Boolean, required: true }`) + expect(content).toMatch( + `qux: { type: Function, required: false, default() { return 1 } }` + ) + expect(content).toMatch( + `quux: { type: Function, required: false, default() { } }` + ) + expect(content).toMatch( + `quuxx: { type: Promise, required: false, async default() { return await Promise.resolve('hi') } }` + ) + expect(content).toMatch( + `fred: { type: String, required: false, get default() { return 'fred' } }` + ) + expect(content).toMatch(`const props = __props`) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + baz: BindingTypes.PROPS, + qux: BindingTypes.PROPS, + quux: BindingTypes.PROPS, + quuxx: BindingTypes.PROPS, + fred: BindingTypes.PROPS, + props: BindingTypes.SETUP_CONST + }) + }) + + test('withDefaults (static) + normal script', () => { + const { content } = compile(` + + + `) + assertCode(content) + }) + + // #7111 + test('withDefaults (static) w/ production mode', () => { + const { content } = compile( + ` + + `, + { isProd: true } + ) + assertCode(content) + expect(content).toMatch(`const props = __props`) + + // foo has no default value, the Function can be dropped + expect(content).toMatch(`foo: {}`) + expect(content).toMatch(`bar: { type: Boolean }`) + expect(content).toMatch(`baz: { type: [Boolean, Function], default: true }`) + expect(content).toMatch(`qux: { default: 'hi' }`) + }) + + test('withDefaults (dynamic)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: String, required: false }, + bar: { type: Number, required: false }, + baz: { type: Boolean, required: true } + }, { ...defaults })`.trim() + ) + }) + + test('withDefaults (reference)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: String, required: false }, + bar: { type: Number, required: false }, + baz: { type: Boolean, required: true } + }, defaults)`.trim() + ) + }) + + // #7111 + test('withDefaults (dynamic) w/ production mode', () => { + const { content } = compile( + ` + + `, + { isProd: true } + ) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: Function }, + bar: { type: Boolean }, + baz: { type: [Boolean, Function] }, + qux: {} + }, { ...defaults })`.trim() + ) + }) + + test('withDefaults w/ dynamic object method', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) + expect(content).toMatch( + ` + _mergeDefaults({ + foo: { type: Function, required: false } + }, { + ['fo' + 'o']() { return 'foo' } + })`.trim() + ) + }) + + test('runtime inference for Enum', () => { + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: Number`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: String`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: [String, Number]`) + + expect( + compile( + ``, + { hoistStatic: true } + ).content + ).toMatch(`foo: { type: Number`) + }) + + describe('errors', () => { + test('w/ both type and non-type args', () => { + expect(() => { + compile(``) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts similarity index 99% rename from packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts index e00d7d48b97..a459d80ff29 100644 --- a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts @@ -1,6 +1,6 @@ import { BindingTypes } from '@vue/compiler-core' -import { SFCScriptCompileOptions } from '../src' -import { compileSFCScript, assertCode } from './utils' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' describe('sfc props transform', () => { function compile(src: string, options?: Partial) { diff --git a/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts new file mode 100644 index 00000000000..c7becacc02a --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineSlots.spec.ts @@ -0,0 +1,40 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineSlots()', () => { + test('basic usage', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`const slots = _useSlots()`) + expect(content).not.toMatch('defineSlots') + }) + + test('w/o return value', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).not.toMatch('defineSlots') + expect(content).not.toMatch(`_useSlots`) + }) + + test('w/o generic params', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`const slots = _useSlots()`) + expect(content).not.toMatch('defineSlots') + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts similarity index 97% rename from packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts index 4879dd5f924..614a5e75bce 100644 --- a/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts @@ -1,6 +1,6 @@ import { BindingTypes } from '@vue/compiler-core' -import { SFCScriptCompileOptions } from '../src' -import { compileSFCScript, assertCode } from './utils' +import { SFCScriptCompileOptions } from '../../src' +import { compileSFCScript, assertCode } from '../utils' describe('sfc hoist static', () => { function compile(src: string, options?: Partial) { diff --git a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts similarity index 98% rename from packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts rename to packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts index 8ae5275661e..44d51c14e75 100644 --- a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/reactivityTransform.spec.ts @@ -1,5 +1,6 @@ +// TODO remove in 3.4 import { BindingTypes } from '@vue/compiler-core' -import { compileSFCScript as compile, assertCode } from './utils' +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 /packages/reactivity-transform/__tests__ From d1f973bff82581fb335d6fc05623d1ad3d84fb7c Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Apr 2023 21:17:57 +0800 Subject: [PATCH 018/142] feat(compiler-sfc): support intersection and union types in macros close #7553 --- .../__snapshots__/defineEmits.spec.ts.snap | 16 ++ .../compileScript/defineEmits.spec.ts | 6 +- .../compileScript/resolveType.spec.ts | 179 ++++++++++++++++++ .../compiler-sfc/src/script/defineProps.ts | 21 +- .../compiler-sfc/src/script/resolveType.ts | 68 ++++++- 5 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap index 5add78a28b3..729c019a555 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap @@ -191,6 +191,22 @@ export default /*#__PURE__*/_defineComponent({ +return { emit } +} + +})" +`; + +exports[`defineEmits > w/ type (union) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"foo\\", \\"bar\\", \\"baz\\"], + setup(__props, { expose: __expose, emit }) { + __expose(); + + + return { emit } } diff --git a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts index 3920f08efb8..67d9674b54c 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts @@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b']) test('w/ type (union)', () => { const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` - expect(() => - compile(` + const { content } = compile(` `) - ).toThrow() + assertCode(content) + expect(content).toMatch(`emits: ["foo", "bar", "baz"]`) }) test('w/ type (type literal w/ call signatures)', () => { diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts new file mode 100644 index 00000000000..12d18e40687 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -0,0 +1,179 @@ +import { TSTypeAliasDeclaration } from '@babel/types' +import { parse } from '../../src' +import { ScriptCompileContext } from '../../src/script/context' +import { + inferRuntimeType, + resolveTypeElements +} from '../../src/script/resolveType' + +describe('resolveType', () => { + test('type literal', () => { + const { elements, callSignatures } = resolve(`type Target = { + foo: number // property + bar(): void // method + 'baz': string // string literal key + (e: 'foo'): void // call signature + (e: 'bar'): void + }`) + expect(elements).toStrictEqual({ + foo: ['Number'], + bar: ['Function'], + baz: ['String'] + }) + expect(callSignatures?.length).toBe(2) + }) + + test('reference type', () => { + expect( + resolve(` + type Aliased = { foo: number } + type Target = Aliased + `).elements + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference exported type', () => { + expect( + resolve(` + export type Aliased = { foo: number } + type Target = Aliased + `).elements + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference interface', () => { + expect( + resolve(` + interface Aliased { foo: number } + type Target = Aliased + `).elements + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference exported interface', () => { + expect( + resolve(` + export interface Aliased { foo: number } + type Target = Aliased + `).elements + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + test('reference interface extends', () => { + expect( + resolve(` + export interface A { a(): void } + export interface B extends A { b: boolean } + interface C { c: string } + interface Aliased extends B, C { foo: number } + type Target = Aliased + `).elements + ).toStrictEqual({ + a: ['Function'], + b: ['Boolean'], + c: ['String'], + foo: ['Number'] + }) + }) + + test('function type', () => { + expect( + resolve(` + type Target = (e: 'foo') => void + `).callSignatures?.length + ).toBe(1) + }) + + test('reference function type', () => { + expect( + resolve(` + type Fn = (e: 'foo') => void + type Target = Fn + `).callSignatures?.length + ).toBe(1) + }) + + test('intersection type', () => { + expect( + resolve(` + type Foo = { foo: number } + type Bar = { bar: string } + type Baz = { bar: string | boolean } + type Target = { self: any } & Foo & Bar & Baz + `).elements + ).toStrictEqual({ + self: ['Unknown'], + foo: ['Number'], + // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be + // preferred + bar: ['String', 'Boolean'] + }) + }) + + // #7553 + test('union type', () => { + expect( + resolve(` + interface CommonProps { + size?: 'xl' | 'l' | 'm' | 's' | 'xs' + } + + type ConditionalProps = + | { + color: 'normal' | 'primary' | 'secondary' + appearance: 'normal' | 'outline' | 'text' + } + | { + color: number + appearance: 'outline' + note: string + } + + type Target = CommonProps & ConditionalProps + `).elements + ).toStrictEqual({ + size: ['String'], + color: ['String', 'Number'], + appearance: ['String'], + note: ['String'] + }) + }) + + // describe('built-in utility types', () => { + + // }) + + describe('errors', () => { + test('error on computed keys', () => { + expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( + `computed keys are not supported in types referenced by SFC macros` + ) + }) + }) +}) + +function resolve(code: string) { + const { descriptor } = parse(``) + const ctx = new ScriptCompileContext(descriptor, { id: 'test' }) + const targetDecl = ctx.scriptSetupAst!.body.find( + s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' + ) as TSTypeAliasDeclaration + const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation) + const elements: Record = {} + for (const key in raw) { + elements[key] = inferRuntimeType(ctx, raw[key]) + } + return { + elements, + callSignatures: raw.__callSignatures, + raw + } +} diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index bd462a2a8ea..ee8b5e55734 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -193,20 +193,15 @@ function resolveRuntimePropsFromType( const elements = resolveTypeElements(ctx, node) for (const key in elements) { const e = elements[key] - let type: string[] | undefined + let type = inferRuntimeType(ctx, e) let skipCheck = false - if (e.type === 'TSMethodSignature') { - type = ['Function'] - } else if (e.typeAnnotation) { - type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation) - // skip check for result containing unknown types - if (type.includes(UNKNOWN_TYPE)) { - if (type.includes('Boolean') || type.includes('Function')) { - type = type.filter(t => t !== UNKNOWN_TYPE) - skipCheck = true - } else { - type = ['null'] - } + // skip check for result containing unknown types + if (type.includes(UNKNOWN_TYPE)) { + if (type.includes('Boolean') || type.includes('Function')) { + type = type.filter(t => t !== UNKNOWN_TYPE) + skipCheck = true + } else { + type = ['null'] } } props.push({ diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index ba41757069e..6711784a7af 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils' import { ScriptCompileContext } from './context' import { ImportBinding } from '../compileScript' import { TSInterfaceDeclaration } from '@babel/types' -import { hasOwn } from '@vue/shared' +import { hasOwn, isArray } from '@vue/shared' +import { Expression } from '@babel/types' export interface TypeScope { filename: string @@ -63,24 +64,37 @@ function innerResolveTypeElements( addCallSignature(ret, node) return ret } - case 'TSExpressionWithTypeArguments': + case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': return resolveTypeElements(ctx, resolveTypeReference(ctx, node)) + case 'TSUnionType': + case 'TSIntersectionType': + return mergeElements( + node.types.map(t => resolveTypeElements(ctx, t)), + node.type + ) } ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) } function addCallSignature( elements: ResolvedElements, - node: TSCallSignatureDeclaration | TSFunctionType + node: + | TSCallSignatureDeclaration + | TSFunctionType + | (TSCallSignatureDeclaration | TSFunctionType)[] ) { if (!elements.__callSignatures) { Object.defineProperty(elements, '__callSignatures', { enumerable: false, - value: [node] + value: isArray(node) ? node : [node] }) } else { - elements.__callSignatures.push(node) + if (isArray(node)) { + elements.__callSignatures.push(...node) + } else { + elements.__callSignatures.push(node) + } } } @@ -112,6 +126,45 @@ function typeElementsToMap( return ret } +function mergeElements( + maps: ResolvedElements[], + type: 'TSUnionType' | 'TSIntersectionType' +): ResolvedElements { + const res: ResolvedElements = Object.create(null) + for (const m of maps) { + for (const key in m) { + if (!(key in res)) { + res[key] = m[key] + } else { + res[key] = createProperty(res[key].key, type, [res[key], m[key]]) + } + } + if (m.__callSignatures) { + addCallSignature(res, m.__callSignatures) + } + } + return res +} + +function createProperty( + key: Expression, + type: 'TSUnionType' | 'TSIntersectionType', + types: Node[] +): TSPropertySignature { + return { + type: 'TSPropertySignature', + key, + kind: 'get', + typeAnnotation: { + type: 'TSTypeAnnotation', + typeAnnotation: { + type, + types: types as TSType[] + } + } + } +} + function resolveInterfaceMembers( ctx: ScriptCompileContext, node: TSInterfaceDeclaration @@ -252,6 +305,11 @@ export function inferRuntimeType( } return types.size ? Array.from(types) : ['Object'] } + case 'TSPropertySignature': + if (node.typeAnnotation) { + return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation) + } + case 'TSMethodSignature': case 'TSFunctionType': return ['Function'] case 'TSArrayType': From fb8ecc803e58bfef0971346c63fefc529812daa7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 09:59:54 +0800 Subject: [PATCH 019/142] feat(compiler-sfc): support mapped types, string types & template type in macros --- .../compileScript/resolveType.spec.ts | 43 +++++- .../compiler-sfc/src/script/resolveType.ts | 144 ++++++++++++++---- 2 files changed, 159 insertions(+), 28 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 12d18e40687..3d5e3750798 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -147,9 +147,48 @@ describe('resolveType', () => { }) }) - // describe('built-in utility types', () => { + test('template string type', () => { + expect( + resolve(` + type T = 'foo' | 'bar' + type S = 'x' | 'y' + type Target = { + [\`_\${T}_\${S}_\`]: string + } + `).elements + ).toStrictEqual({ + _foo_x_: ['String'], + _foo_y_: ['String'], + _bar_x_: ['String'], + _bar_y_: ['String'] + }) + }) - // }) + test('mapped types w/ string manipulation', () => { + expect( + resolve(` + type T = 'foo' | 'bar' + type Target = { [K in T]: string | number } & { + [K in 'optional']?: boolean + } & { + [K in Capitalize]: string + } & { + [K in Uppercase>]: string + } & { + [K in \`x\${T}\`]: string + } + `).elements + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String', 'Number'], + Foo: ['String'], + Bar: ['String'], + FOO: ['String'], + xfoo: ['String'], + xbar: ['String'], + optional: ['Boolean'] + }) + }) describe('errors', () => { test('error on computed keys', () => { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 6711784a7af..101f3b4f0a8 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -5,18 +5,20 @@ import { TSEnumDeclaration, TSExpressionWithTypeArguments, TSFunctionType, + TSMappedType, TSMethodSignature, TSPropertySignature, TSType, TSTypeAnnotation, TSTypeElement, - TSTypeReference + TSTypeReference, + TemplateLiteral } from '@babel/types' import { UNKNOWN_TYPE } from './utils' import { ScriptCompileContext } from './context' import { ImportBinding } from '../compileScript' import { TSInterfaceDeclaration } from '@babel/types' -import { hasOwn, isArray } from '@vue/shared' +import { capitalize, hasOwn, isArray } from '@vue/shared' import { Expression } from '@babel/types' export interface TypeScope { @@ -65,14 +67,23 @@ function innerResolveTypeElements( return ret } case 'TSExpressionWithTypeArguments': // referenced by interface extends - case 'TSTypeReference': - return resolveTypeElements(ctx, resolveTypeReference(ctx, node)) + case 'TSTypeReference': { + const resolved = resolveTypeReference(ctx, node) + if (resolved) { + return resolveTypeElements(ctx, resolved) + } else { + // TODO Pick / Omit + ctx.error(`Failed to resolved type reference`, node) + } + } case 'TSUnionType': case 'TSIntersectionType': return mergeElements( node.types.map(t => resolveTypeElements(ctx, t)), node.type ) + case 'TSMappedType': + return resolveMappedType(ctx, node) } ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) } @@ -113,6 +124,10 @@ function typeElementsToMap( : null if (name && !e.computed) { ret[name] = e + } else if (e.key.type === 'TemplateLiteral') { + for (const key of resolveTemplateKeys(ctx, e.key)) { + ret[key] = e + } } else { ctx.error( `computed keys are not supported in types referenced by SFC macros.`, @@ -136,7 +151,11 @@ function mergeElements( if (!(key in res)) { res[key] = m[key] } else { - res[key] = createProperty(res[key].key, type, [res[key], m[key]]) + res[key] = createProperty(res[key].key, { + type, + // @ts-ignore + types: [res[key], m[key]] + }) } } if (m.__callSignatures) { @@ -148,8 +167,7 @@ function mergeElements( function createProperty( key: Expression, - type: 'TSUnionType' | 'TSIntersectionType', - types: Node[] + typeAnnotation: TSType ): TSPropertySignature { return { type: 'TSPropertySignature', @@ -157,10 +175,7 @@ function createProperty( kind: 'get', typeAnnotation: { type: 'TSTypeAnnotation', - typeAnnotation: { - type, - types: types as TSType[] - } + typeAnnotation } } } @@ -183,22 +198,102 @@ function resolveInterfaceMembers( return base } -function resolveTypeReference( +function resolveMappedType( ctx: ScriptCompileContext, - node: TSTypeReference | TSExpressionWithTypeArguments, - scope?: TypeScope -): Node -function resolveTypeReference( + node: TSMappedType +): ResolvedElements { + const res: ResolvedElements = {} + if (!node.typeParameter.constraint) { + ctx.error(`mapped type used in macros must have a finite constraint.`, node) + } + const keys = resolveStringType(ctx, node.typeParameter.constraint) + for (const key of keys) { + res[key] = createProperty( + { + type: 'Identifier', + name: key + }, + node.typeAnnotation! + ) + } + return res +} + +function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] { + switch (node.type) { + case 'StringLiteral': + return [node.value] + case 'TSLiteralType': + return resolveStringType(ctx, node.literal) + case 'TSUnionType': + return node.types.map(t => resolveStringType(ctx, t)).flat() + case 'TemplateLiteral': { + return resolveTemplateKeys(ctx, node) + } + case 'TSTypeReference': { + const resolved = resolveTypeReference(ctx, node) + if (resolved) { + return resolveStringType(ctx, resolved) + } + if (node.typeName.type === 'Identifier') { + const getParam = (index = 0) => + resolveStringType(ctx, node.typeParameters!.params[index]) + switch (node.typeName.name) { + case 'Extract': + return getParam(1) + case 'Exclude': { + const excluded = getParam(1) + return getParam().filter(s => !excluded.includes(s)) + } + case 'Uppercase': + return getParam().map(s => s.toUpperCase()) + case 'Lowercase': + return getParam().map(s => s.toLowerCase()) + case 'Capitalize': + return getParam().map(capitalize) + case 'Uncapitalize': + return getParam().map(s => s[0].toLowerCase() + s.slice(1)) + default: + ctx.error('Failed to resolve type reference', node) + } + } + } + } + ctx.error('Failed to resolve string type into finite keys', node) +} + +function resolveTemplateKeys( ctx: ScriptCompileContext, - node: TSTypeReference | TSExpressionWithTypeArguments, - scope: TypeScope, - bail: false -): Node | undefined + node: TemplateLiteral +): string[] { + if (!node.expressions.length) { + return [node.quasis[0].value.raw] + } + + const res: string[] = [] + const e = node.expressions[0] + const q = node.quasis[0] + const leading = q ? q.value.raw : `` + const resolved = resolveStringType(ctx, e) + const restResolved = resolveTemplateKeys(ctx, { + ...node, + expressions: node.expressions.slice(1), + quasis: q ? node.quasis.slice(1) : node.quasis + }) + + for (const r of resolved) { + for (const rr of restResolved) { + res.push(leading + r + rr) + } + } + + return res +} + function resolveTypeReference( ctx: ScriptCompileContext, node: TSTypeReference | TSExpressionWithTypeArguments, - scope = getRootScope(ctx), - bail = true + scope = getRootScope(ctx) ): Node | undefined { const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression if (ref.type === 'Identifier') { @@ -211,9 +306,6 @@ function resolveTypeReference( // TODO qualified name, e.g. Foo.Bar // return resolveTypeReference() } - if (bail) { - ctx.error('Failed to resolve type reference.', node) - } } function getRootScope(ctx: ScriptCompileContext): TypeScope { @@ -332,7 +424,7 @@ export function inferRuntimeType( case 'TSTypeReference': if (node.typeName.type === 'Identifier') { - const resolved = resolveTypeReference(ctx, node, scope, false) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { return inferRuntimeType(ctx, resolved, scope) } From 1cfab4c695b0c28f549f8c97faee5099581792a7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 10:28:22 +0800 Subject: [PATCH 020/142] feat(compiler-sfc): support limited built-in utility types in macros --- .../compileScript/resolveType.spec.ts | 25 ++++++ .../compiler-sfc/src/script/resolveType.ts | 79 +++++++++++++++++-- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 3d5e3750798..3e61e1b231a 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -190,6 +190,31 @@ describe('resolveType', () => { }) }) + test('utility type: Pick', () => { + expect( + resolve(` + type T = { foo: number, bar: string, baz: boolean } + type K = 'foo' | 'bar' + type Target = Pick + `).elements + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('utility type: Omit', () => { + expect( + resolve(` + type T = { foo: number, bar: string, baz: boolean } + type K = 'foo' | 'bar' + type Target = Omit + `).elements + ).toStrictEqual({ + baz: ['Boolean'] + }) + }) + describe('errors', () => { test('error on computed keys', () => { expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 101f3b4f0a8..0f6a4eb5230 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -72,8 +72,18 @@ function innerResolveTypeElements( if (resolved) { return resolveTypeElements(ctx, resolved) } else { - // TODO Pick / Omit - ctx.error(`Failed to resolved type reference`, node) + const typeName = getReferenceName(node) + if ( + typeof typeName === 'string' && + // @ts-ignore + SupportedBuiltinsSet.has(typeName) + ) { + return resolveBuiltin(ctx, node, typeName as any) + } + ctx.error( + `Failed to resolved type reference, or unsupported built-in utlility type.`, + node + ) } } case 'TSUnionType': @@ -290,17 +300,60 @@ function resolveTemplateKeys( return res } +const SupportedBuiltinsSet = new Set([ + 'Partial', + 'Required', + 'Readonly', + 'Pick', + 'Omit' +] as const) + +type GetSetType = T extends Set ? V : never + +function resolveBuiltin( + ctx: ScriptCompileContext, + node: TSTypeReference | TSExpressionWithTypeArguments, + name: GetSetType +): ResolvedElements { + const t = resolveTypeElements(ctx, node.typeParameters!.params[0]) + switch (name) { + case 'Partial': + case 'Required': + case 'Readonly': + return t + case 'Pick': { + const picked = resolveStringType(ctx, node.typeParameters!.params[1]) + const res: ResolvedElements = {} + if (t.__callSignatures) addCallSignature(res, t.__callSignatures) + for (const key of picked) { + res[key] = t[key] + } + return res + } + case 'Omit': + const omitted = resolveStringType(ctx, node.typeParameters!.params[1]) + const res: ResolvedElements = {} + if (t.__callSignatures) addCallSignature(res, t.__callSignatures) + for (const key in t) { + if (!omitted.includes(key)) { + res[key] = t[key] + } + } + return res + } +} + function resolveTypeReference( ctx: ScriptCompileContext, node: TSTypeReference | TSExpressionWithTypeArguments, scope = getRootScope(ctx) ): Node | undefined { - const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression - if (ref.type === 'Identifier') { - if (scope.imports[ref.name]) { + const name = getReferenceName(node) + if (typeof name === 'string') { + if (scope.imports[name]) { // TODO external import - } else if (scope.types[ref.name]) { - return scope.types[ref.name] + } else if (scope.types[name]) { + return scope.types[name] } } else { // TODO qualified name, e.g. Foo.Bar @@ -308,6 +361,18 @@ function resolveTypeReference( } } +function getReferenceName( + node: TSTypeReference | TSExpressionWithTypeArguments +): string | string[] { + const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression + if (ref.type === 'Identifier') { + return ref.name + } else { + // TODO qualified name, e.g. Foo.Bar + return [] + } +} + function getRootScope(ctx: ScriptCompileContext): TypeScope { if (ctx.scope) { return ctx.scope From 51773d5d1d7f8559ff42bc3fffa76ab8cc9ec62b Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 10:42:15 +0800 Subject: [PATCH 021/142] refactor: adjust ResolvedElements shape --- .../compileScript/resolveType.spec.ts | 42 ++++----- .../compiler-sfc/src/script/defineEmits.ts | 8 +- .../compiler-sfc/src/script/defineProps.ts | 4 +- .../compiler-sfc/src/script/resolveType.ts | 88 +++++++------------ 4 files changed, 58 insertions(+), 84 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 3e61e1b231a..5d5a424f26b 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -8,19 +8,19 @@ import { describe('resolveType', () => { test('type literal', () => { - const { elements, callSignatures } = resolve(`type Target = { + const { props, calls } = resolve(`type Target = { foo: number // property bar(): void // method 'baz': string // string literal key (e: 'foo'): void // call signature (e: 'bar'): void }`) - expect(elements).toStrictEqual({ + expect(props).toStrictEqual({ foo: ['Number'], bar: ['Function'], baz: ['String'] }) - expect(callSignatures?.length).toBe(2) + expect(calls?.length).toBe(2) }) test('reference type', () => { @@ -28,7 +28,7 @@ describe('resolveType', () => { resolve(` type Aliased = { foo: number } type Target = Aliased - `).elements + `).props ).toStrictEqual({ foo: ['Number'] }) @@ -39,7 +39,7 @@ describe('resolveType', () => { resolve(` export type Aliased = { foo: number } type Target = Aliased - `).elements + `).props ).toStrictEqual({ foo: ['Number'] }) @@ -50,7 +50,7 @@ describe('resolveType', () => { resolve(` interface Aliased { foo: number } type Target = Aliased - `).elements + `).props ).toStrictEqual({ foo: ['Number'] }) @@ -61,7 +61,7 @@ describe('resolveType', () => { resolve(` export interface Aliased { foo: number } type Target = Aliased - `).elements + `).props ).toStrictEqual({ foo: ['Number'] }) @@ -75,7 +75,7 @@ describe('resolveType', () => { interface C { c: string } interface Aliased extends B, C { foo: number } type Target = Aliased - `).elements + `).props ).toStrictEqual({ a: ['Function'], b: ['Boolean'], @@ -88,7 +88,7 @@ describe('resolveType', () => { expect( resolve(` type Target = (e: 'foo') => void - `).callSignatures?.length + `).calls?.length ).toBe(1) }) @@ -97,7 +97,7 @@ describe('resolveType', () => { resolve(` type Fn = (e: 'foo') => void type Target = Fn - `).callSignatures?.length + `).calls?.length ).toBe(1) }) @@ -108,7 +108,7 @@ describe('resolveType', () => { type Bar = { bar: string } type Baz = { bar: string | boolean } type Target = { self: any } & Foo & Bar & Baz - `).elements + `).props ).toStrictEqual({ self: ['Unknown'], foo: ['Number'], @@ -138,7 +138,7 @@ describe('resolveType', () => { } type Target = CommonProps & ConditionalProps - `).elements + `).props ).toStrictEqual({ size: ['String'], color: ['String', 'Number'], @@ -155,7 +155,7 @@ describe('resolveType', () => { type Target = { [\`_\${T}_\${S}_\`]: string } - `).elements + `).props ).toStrictEqual({ _foo_x_: ['String'], _foo_y_: ['String'], @@ -177,7 +177,7 @@ describe('resolveType', () => { } & { [K in \`x\${T}\`]: string } - `).elements + `).props ).toStrictEqual({ foo: ['String', 'Number'], bar: ['String', 'Number'], @@ -196,7 +196,7 @@ describe('resolveType', () => { type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' type Target = Pick - `).elements + `).props ).toStrictEqual({ foo: ['Number'], bar: ['String'] @@ -209,7 +209,7 @@ describe('resolveType', () => { type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' type Target = Omit - `).elements + `).props ).toStrictEqual({ baz: ['Boolean'] }) @@ -231,13 +231,13 @@ function resolve(code: string) { s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' ) as TSTypeAliasDeclaration const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation) - const elements: Record = {} - for (const key in raw) { - elements[key] = inferRuntimeType(ctx, raw[key]) + const props: Record = {} + for (const key in raw.props) { + props[key] = inferRuntimeType(ctx, raw.props[key]) } return { - elements, - callSignatures: raw.__callSignatures, + props, + calls: raw.calls, raw } } diff --git a/packages/compiler-sfc/src/script/defineEmits.ts b/packages/compiler-sfc/src/script/defineEmits.ts index 0e080b4fed4..e615cd71a0d 100644 --- a/packages/compiler-sfc/src/script/defineEmits.ts +++ b/packages/compiler-sfc/src/script/defineEmits.ts @@ -69,22 +69,22 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set { return emits } - const elements = resolveTypeElements(ctx, node) + const { props, calls } = resolveTypeElements(ctx, node) let hasProperty = false - for (const key in elements) { + for (const key in props) { emits.add(key) hasProperty = true } - if (elements.__callSignatures) { + if (calls) { if (hasProperty) { ctx.error( `defineEmits() type cannot mixed call signature and property syntax.`, node ) } - for (const call of elements.__callSignatures) { + for (const call of calls) { extractEventNames(call.parameters[0], emits) } } diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index ee8b5e55734..16ea02fe3cf 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -191,8 +191,8 @@ function resolveRuntimePropsFromType( ): PropTypeData[] { const props: PropTypeData[] = [] const elements = resolveTypeElements(ctx, node) - for (const key in elements) { - const e = elements[key] + for (const key in elements.props) { + const e = elements.props[key] let type = inferRuntimeType(ctx, e) let skipCheck = false // skip check for result containing unknown types diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 0f6a4eb5230..ecd3838be7b 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -18,7 +18,7 @@ import { UNKNOWN_TYPE } from './utils' import { ScriptCompileContext } from './context' import { ImportBinding } from '../compileScript' import { TSInterfaceDeclaration } from '@babel/types' -import { capitalize, hasOwn, isArray } from '@vue/shared' +import { capitalize, hasOwn } from '@vue/shared' import { Expression } from '@babel/types' export interface TypeScope { @@ -28,11 +28,9 @@ export interface TypeScope { types: Record } -type ResolvedElements = Record< - string, - TSPropertySignature | TSMethodSignature -> & { - __callSignatures?: (TSCallSignatureDeclaration | TSFunctionType)[] +interface ResolvedElements { + props: Record + calls?: (TSCallSignatureDeclaration | TSFunctionType)[] } /** @@ -62,9 +60,7 @@ function innerResolveTypeElements( case 'TSParenthesizedType': return resolveTypeElements(ctx, node.typeAnnotation) case 'TSFunctionType': { - const ret: ResolvedElements = {} - addCallSignature(ret, node) - return ret + return { props: {}, calls: [node] } } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { @@ -98,32 +94,11 @@ function innerResolveTypeElements( ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) } -function addCallSignature( - elements: ResolvedElements, - node: - | TSCallSignatureDeclaration - | TSFunctionType - | (TSCallSignatureDeclaration | TSFunctionType)[] -) { - if (!elements.__callSignatures) { - Object.defineProperty(elements, '__callSignatures', { - enumerable: false, - value: isArray(node) ? node : [node] - }) - } else { - if (isArray(node)) { - elements.__callSignatures.push(...node) - } else { - elements.__callSignatures.push(node) - } - } -} - function typeElementsToMap( ctx: ScriptCompileContext, elements: TSTypeElement[] ): ResolvedElements { - const ret: ResolvedElements = {} + const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { const name = @@ -133,10 +108,10 @@ function typeElementsToMap( ? e.key.value : null if (name && !e.computed) { - ret[name] = e + res.props[name] = e } else if (e.key.type === 'TemplateLiteral') { for (const key of resolveTemplateKeys(ctx, e.key)) { - ret[key] = e + res.props[key] = e } } else { ctx.error( @@ -145,31 +120,32 @@ function typeElementsToMap( ) } } else if (e.type === 'TSCallSignatureDeclaration') { - addCallSignature(ret, e) + ;(res.calls || (res.calls = [])).push(e) } } - return ret + return res } function mergeElements( maps: ResolvedElements[], type: 'TSUnionType' | 'TSIntersectionType' ): ResolvedElements { - const res: ResolvedElements = Object.create(null) - for (const m of maps) { - for (const key in m) { - if (!(key in res)) { - res[key] = m[key] + const res: ResolvedElements = { props: {} } + const { props: baseProps } = res + for (const { props, calls } of maps) { + for (const key in props) { + if (!hasOwn(baseProps, key)) { + baseProps[key] = props[key] } else { - res[key] = createProperty(res[key].key, { + baseProps[key] = createProperty(baseProps[key].key, { type, // @ts-ignore - types: [res[key], m[key]] + types: [baseProps[key], props[key]] }) } } - if (m.__callSignatures) { - addCallSignature(res, m.__callSignatures) + if (calls) { + ;(res.calls || (res.calls = [])).push(...calls) } } return res @@ -197,10 +173,10 @@ function resolveInterfaceMembers( const base = typeElementsToMap(ctx, node.body.body) if (node.extends) { for (const ext of node.extends) { - const resolvedExt = resolveTypeElements(ctx, ext) - for (const key in resolvedExt) { - if (!hasOwn(base, key)) { - base[key] = resolvedExt[key] + const { props } = resolveTypeElements(ctx, ext) + for (const key in props) { + if (!hasOwn(base.props, key)) { + base.props[key] = props[key] } } } @@ -212,13 +188,13 @@ function resolveMappedType( ctx: ScriptCompileContext, node: TSMappedType ): ResolvedElements { - const res: ResolvedElements = {} + const res: ResolvedElements = { props: {} } if (!node.typeParameter.constraint) { ctx.error(`mapped type used in macros must have a finite constraint.`, node) } const keys = resolveStringType(ctx, node.typeParameter.constraint) for (const key of keys) { - res[key] = createProperty( + res.props[key] = createProperty( { type: 'Identifier', name: key @@ -323,20 +299,18 @@ function resolveBuiltin( return t case 'Pick': { const picked = resolveStringType(ctx, node.typeParameters!.params[1]) - const res: ResolvedElements = {} - if (t.__callSignatures) addCallSignature(res, t.__callSignatures) + const res: ResolvedElements = { props: {}, calls: t.calls } for (const key of picked) { - res[key] = t[key] + res.props[key] = t.props[key] } return res } case 'Omit': const omitted = resolveStringType(ctx, node.typeParameters!.params[1]) - const res: ResolvedElements = {} - if (t.__callSignatures) addCallSignature(res, t.__callSignatures) - for (const key in t) { + const res: ResolvedElements = { props: {}, calls: t.calls } + for (const key in t.props) { if (!omitted.includes(key)) { - res[key] = t[key] + res.props[key] = t.props[key] } } return res From 3f779ddbf85054c8915fa4537f8a79baab392d5c Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 11:21:09 +0800 Subject: [PATCH 022/142] feat(compiler-sfc): support string indexed type in macros --- .../compileScript/resolveType.spec.ts | 24 +++++++ .../compiler-sfc/src/script/resolveType.ts | 67 +++++++++++++++---- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 5d5a424f26b..1485cfe83ae 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -215,6 +215,30 @@ describe('resolveType', () => { }) }) + test('indexed access type', () => { + expect( + resolve(` + type T = { bar: number } + type S = { nested: { foo: T['bar'] }} + type Target = S['nested'] + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) + + // test('namespace', () => { + // expect( + // resolve(` + // type T = { foo: number, bar: string, baz: boolean } + // type K = 'foo' | 'bar' + // type Target = Omit + // `).props + // ).toStrictEqual({ + // baz: ['Boolean'] + // }) + // }) + describe('errors', () => { test('error on computed keys', () => { expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index ecd3838be7b..0b6969ad8f0 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,4 +1,5 @@ import { + Identifier, Node, Statement, TSCallSignatureDeclaration, @@ -8,6 +9,7 @@ import { TSMappedType, TSMethodSignature, TSPropertySignature, + TSQualifiedName, TSType, TSTypeAnnotation, TSTypeElement, @@ -62,6 +64,34 @@ function innerResolveTypeElements( case 'TSFunctionType': { return { props: {}, calls: [node] } } + case 'TSUnionType': + case 'TSIntersectionType': + return mergeElements( + node.types.map(t => resolveTypeElements(ctx, t)), + node.type + ) + case 'TSMappedType': + return resolveMappedType(ctx, node) + case 'TSIndexedAccessType': { + if ( + node.indexType.type === 'TSLiteralType' && + node.indexType.literal.type === 'StringLiteral' + ) { + const resolved = resolveTypeElements(ctx, node.objectType) + const key = node.indexType.literal.value + const targetType = resolved.props[key].typeAnnotation + if (targetType) { + return resolveTypeElements(ctx, targetType.typeAnnotation) + } else { + break + } + } else { + ctx.error( + `Unsupported index type: ${node.indexType.type}`, + node.indexType + ) + } + } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { const resolved = resolveTypeReference(ctx, node) @@ -82,16 +112,8 @@ function innerResolveTypeElements( ) } } - case 'TSUnionType': - case 'TSIntersectionType': - return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t)), - node.type - ) - case 'TSMappedType': - return resolveMappedType(ctx, node) } - ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) + ctx.error(`Unresolvable type in SFC macro: ${node.type}`, node) } function typeElementsToMap( @@ -342,8 +364,15 @@ function getReferenceName( if (ref.type === 'Identifier') { return ref.name } else { - // TODO qualified name, e.g. Foo.Bar - return [] + return qualifiedNameToPath(ref) + } +} + +function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { + if (node.type === 'Identifier') { + return [node.name] + } else { + return [...qualifiedNameToPath(node.left), node.right.name] } } @@ -376,8 +405,11 @@ function recordType(node: Node, types: Record) { switch (node.type) { case 'TSInterfaceDeclaration': case 'TSEnumDeclaration': - types[node.id.name] = node + case 'TSModuleDeclaration': { + const id = node.id.type === 'Identifier' ? node.id.name : node.id.value + types[id] = node break + } case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation break @@ -542,6 +574,17 @@ export function inferRuntimeType( case 'TSSymbolKeyword': return ['Symbol'] + case 'TSIndexedAccessType': { + if ( + node.indexType.type === 'TSLiteralType' && + node.indexType.literal.type === 'StringLiteral' + ) { + const resolved = resolveTypeElements(ctx, node.objectType) + const key = node.indexType.literal.value + return inferRuntimeType(ctx, resolved.props[key]) + } + } + default: return [UNKNOWN_TYPE] // no runtime check } From 5ff40bb0dc2918b7db15fe9f49db2a135a925572 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 12:32:52 +0800 Subject: [PATCH 023/142] feat(compiler-sfc): support namespace members type in macros --- .../compileScript/resolveType.spec.ts | 29 +++--- .../compiler-sfc/src/script/resolveType.ts | 94 ++++++++++++++----- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 1485cfe83ae..38bcdf988ee 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -227,17 +227,24 @@ describe('resolveType', () => { }) }) - // test('namespace', () => { - // expect( - // resolve(` - // type T = { foo: number, bar: string, baz: boolean } - // type K = 'foo' | 'bar' - // type Target = Omit - // `).props - // ).toStrictEqual({ - // baz: ['Boolean'] - // }) - // }) + test('namespace', () => { + expect( + resolve(` + type X = string + namespace Foo { + type X = number + export namespace Bar { + export type A = { + foo: X + } + } + } + type Target = Foo.Bar.A + `).props + ).toStrictEqual({ + foo: ['Number'] + }) + }) describe('errors', () => { test('error on computed keys', () => { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 0b6969ad8f0..f3b20a7ee15 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,6 +1,6 @@ import { Identifier, - Node, + Node as _Node, Statement, TSCallSignatureDeclaration, TSEnumDeclaration, @@ -8,6 +8,8 @@ import { TSFunctionType, TSMappedType, TSMethodSignature, + TSModuleBlock, + TSModuleDeclaration, TSPropertySignature, TSQualifiedName, TSType, @@ -25,23 +27,32 @@ import { Expression } from '@babel/types' export interface TypeScope { filename: string - body: Statement[] imports: Record types: Record + parent?: TypeScope +} + +interface WithScope { + _ownerScope?: TypeScope } interface ResolvedElements { - props: Record + props: Record calls?: (TSCallSignatureDeclaration | TSFunctionType)[] } +type Node = _Node & + WithScope & { + _resolvedElements?: ResolvedElements + } + /** * Resolve arbitrary type node to a list of type elements that can be then * mapped to runtime props or emits. */ export function resolveTypeElements( ctx: ScriptCompileContext, - node: Node & { _resolvedElements?: ResolvedElements } + node: Node ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements @@ -55,7 +66,7 @@ function innerResolveTypeElements( ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': - return typeElementsToMap(ctx, node.members) + return typeElementsToMap(ctx, node.members, node._ownerScope) case 'TSInterfaceDeclaration': return resolveInterfaceMembers(ctx, node) case 'TSTypeAliasDeclaration': @@ -118,11 +129,13 @@ function innerResolveTypeElements( function typeElementsToMap( ctx: ScriptCompileContext, - elements: TSTypeElement[] + elements: TSTypeElement[], + scope = ctxToScope(ctx) ): ResolvedElements { const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { + ;(e as Node)._ownerScope = scope const name = e.key.type === 'Identifier' ? e.key.name @@ -190,9 +203,9 @@ function createProperty( function resolveInterfaceMembers( ctx: ScriptCompileContext, - node: TSInterfaceDeclaration + node: TSInterfaceDeclaration & WithScope ): ResolvedElements { - const base = typeElementsToMap(ctx, node.body.body) + const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) if (node.extends) { for (const ext of node.extends) { const { props } = resolveTypeElements(ctx, ext) @@ -341,10 +354,22 @@ function resolveBuiltin( function resolveTypeReference( ctx: ScriptCompileContext, - node: TSTypeReference | TSExpressionWithTypeArguments, - scope = getRootScope(ctx) + node: (TSTypeReference | TSExpressionWithTypeArguments) & { + _resolvedReference?: Node + }, + scope = ctxToScope(ctx) ): Node | undefined { + if (node._resolvedReference) { + return node._resolvedReference + } const name = getReferenceName(node) + return (node._resolvedReference = innerResolveTypeReference(scope, name)) +} + +function innerResolveTypeReference( + scope: TypeScope, + name: string | string[] +): Node | undefined { if (typeof name === 'string') { if (scope.imports[name]) { // TODO external import @@ -352,8 +377,14 @@ function resolveTypeReference( return scope.types[name] } } else { - // TODO qualified name, e.g. Foo.Bar - // return resolveTypeReference() + const ns = innerResolveTypeReference(scope, name[0]) + if (ns && ns.type === 'TSModuleDeclaration') { + const childScope = moduleDeclToScope(ns, scope) + return innerResolveTypeReference( + childScope, + name.length > 2 ? name.slice(1) : name[name.length - 1] + ) + } } } @@ -376,7 +407,7 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } -function getRootScope(ctx: ScriptCompileContext): TypeScope { +function ctxToScope(ctx: ScriptCompileContext): TypeScope { if (ctx.scope) { return ctx.scope } @@ -388,13 +419,34 @@ function getRootScope(ctx: ScriptCompileContext): TypeScope { return (ctx.scope = { filename: ctx.descriptor.filename, imports: ctx.userImports, - types: recordTypes(body), - body + types: recordTypes(body) }) } -function recordTypes(body: Statement[]) { - const types: Record = Object.create(null) +function moduleDeclToScope( + node: TSModuleDeclaration & { _resolvedChildScope?: TypeScope }, + parent: TypeScope +): TypeScope { + if (node._resolvedChildScope) { + return node._resolvedChildScope + } + const types: TypeScope['types'] = Object.create(parent.types) + const scope: TypeScope = { + filename: parent.filename, + imports: Object.create(parent.imports), + types: recordTypes((node.body as TSModuleBlock).body, types), + parent + } + for (const key of Object.keys(types)) { + types[key]._ownerScope = scope + } + return (node._resolvedChildScope = scope) +} + +function recordTypes( + body: Statement[], + types: Record = Object.create(null) +) { for (const s of body) { recordType(s, types) } @@ -414,8 +466,8 @@ function recordType(node: Node, types: Record) { types[node.id.name] = node.typeAnnotation break case 'ExportNamedDeclaration': { - if (node.exportKind === 'type') { - recordType(node.declaration!, types) + if (node.declaration) { + recordType(node.declaration, types) } break } @@ -437,7 +489,7 @@ function recordType(node: Node, types: Record) { export function inferRuntimeType( ctx: ScriptCompileContext, node: Node, - scope = getRootScope(ctx) + scope = node._ownerScope || ctxToScope(ctx) ): string[] { switch (node.type) { case 'TSStringKeyword': @@ -470,7 +522,7 @@ export function inferRuntimeType( } case 'TSPropertySignature': if (node.typeAnnotation) { - return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation) + return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation, scope) } case 'TSMethodSignature': case 'TSFunctionType': From 1c06fe1d021572fcdcf187794ce8313a7593ac21 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 15:45:58 +0800 Subject: [PATCH 024/142] chore: improve sfc-playground typing + bump repl for 3.3 external type resolve support close #8051 --- packages/global.d.ts | 7 ------- packages/sfc-playground/package.json | 2 +- packages/sfc-playground/src/App.vue | 12 ++++++------ packages/sfc-playground/src/Header.vue | 9 +++++++-- packages/sfc-playground/src/download/download.ts | 3 ++- pnpm-lock.yaml | 8 ++++---- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/global.d.ts b/packages/global.d.ts index 5992d0840d1..c814ad55eaa 100644 --- a/packages/global.d.ts +++ b/packages/global.d.ts @@ -33,13 +33,6 @@ declare module 'file-saver' { export function saveAs(blob: any, name: any): void } -declare module '@vue/repl' { - import { ComponentOptions } from '@vue/runtime-core' - const Repl: ComponentOptions - const ReplStore: any - export { Repl, ReplStore } -} - declare interface String { /** * @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository. diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json index 3652e7ff6d3..848e3ee97b8 100644 --- a/packages/sfc-playground/package.json +++ b/packages/sfc-playground/package.json @@ -12,7 +12,7 @@ "vite": "^4.2.0" }, "dependencies": { - "@vue/repl": "^1.3.5", + "@vue/repl": "^1.4.0", "file-saver": "^2.0.5", "jszip": "^3.6.0", "vue": "workspace:*" diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue index 1bba8d6f2ae..f7db1b7951b 100644 --- a/packages/sfc-playground/src/App.vue +++ b/packages/sfc-playground/src/App.vue @@ -1,6 +1,6 @@ ', + 'bar.vue': + '' + } + ).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('relative (chained)', () => { + expect( + resolve( + ` + import { P } from './foo' + type Target = P + `, + { + 'foo.ts': `import type { P as PP } from './nested/bar.vue' + export type P = { foo: number } & PP`, + 'nested/bar.vue': + '' + } + ).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('relative (chained, re-export)', () => { + expect( + resolve( + ` + import { PP as P } from './foo' + type Target = P + `, + { + 'foo.ts': `export { P as PP } from './bar'`, + 'bar.ts': 'export type P = { bar: string }' + } + ).props + ).toStrictEqual({ + bar: ['String'] + }) + }) + }) + describe('errors', () => { test('error on computed keys', () => { expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( @@ -255,9 +335,26 @@ describe('resolveType', () => { }) }) -function resolve(code: string) { - const { descriptor } = parse(``) - const ctx = new ScriptCompileContext(descriptor, { id: 'test' }) +function resolve(code: string, files: Record = {}) { + const { descriptor } = parse(``, { + filename: 'Test.vue' + }) + const ctx = new ScriptCompileContext(descriptor, { + id: 'test', + fs: { + fileExists(file) { + return !!files[file] + }, + readFile(file) { + return files[file] + } + } + }) + + // ctx.userImports is collected when calling compileScript(), but we are + // skipping that here, so need to manually register imports + ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any + const targetDecl = ctx.scriptSetupAst!.body.find( s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' ) as TSTypeAliasDeclaration diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index c828d77f6d5..593e8e072c6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -2,8 +2,7 @@ import { BindingTypes, UNREF, isFunctionType, - walkIdentifiers, - getImportedName + walkIdentifiers } from '@vue/compiler-dom' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' import { parse as _parse, ParserPlugin } from '@babel/parser' @@ -45,7 +44,12 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose' import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions' import { processDefineSlots } from './script/defineSlots' import { DEFINE_MODEL, processDefineModel } from './script/defineModel' -import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils' +import { + isLiteralNode, + unwrapTSNode, + isCallOf, + getImportedName +} from './script/utils' import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' @@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions { * (**Experimental**) Enable macro `defineModel` */ defineModel?: boolean + /** + * + */ + fs?: { + fileExists(file: string): boolean + readFile(file: string): string + } } export interface ImportBinding { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 718f23da5ca..1928ea900fc 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -1,13 +1,13 @@ import { Node, ObjectPattern, Program } from '@babel/types' import { SFCDescriptor } from '../parse' import { generateCodeFrame } from '@vue/shared' -import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser' +import { parse as babelParse, ParserPlugin } from '@babel/parser' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { PropsDestructureBindings } from './defineProps' import { ModelDecl } from './defineModel' import { BindingMetadata } from '../../../compiler-core/src' import MagicString from 'magic-string' -import { TypeScope } from './resolveType' +import { TypeScope, WithScope } from './resolveType' export class ScriptCompileContext { isJS: boolean @@ -83,31 +83,17 @@ export class ScriptCompileContext { scriptSetupLang === 'tsx' // resolve parser plugins - const plugins: ParserPlugin[] = [] - if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { - plugins.push('jsx') - } else { - // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins - if (options.babelParserPlugins) - options.babelParserPlugins = options.babelParserPlugins.filter( - n => n !== 'jsx' - ) - } - if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) - if (this.isTS) { - plugins.push('typescript') - if (!plugins.includes('decorators')) { - plugins.push('decorators-legacy') - } - } + const plugins: ParserPlugin[] = resolveParserPlugins( + (scriptLang || scriptSetupLang)!, + options.babelParserPlugins + ) - function parse( - input: string, - options: ParserOptions, - offset: number - ): Program { + function parse(input: string, offset: number): Program { try { - return babelParse(input, options).program + return babelParse(input, { + plugins, + sourceType: 'module' + }).program } catch (e: any) { e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ descriptor.filename @@ -124,23 +110,12 @@ export class ScriptCompileContext { this.descriptor.script && parse( this.descriptor.script.content, - { - plugins, - sourceType: 'module' - }, this.descriptor.script.loc.start.offset ) this.scriptSetupAst = this.descriptor.scriptSetup && - parse( - this.descriptor.scriptSetup!.content, - { - plugins: [...plugins, 'topLevelAwait'], - sourceType: 'module' - }, - this.startOffset! - ) + parse(this.descriptor.scriptSetup!.content, this.startOffset!) } getString(node: Node, scriptSetup = true): string { @@ -150,19 +125,39 @@ export class ScriptCompileContext { return block.content.slice(node.start!, node.end!) } - error( - msg: string, - node: Node, - end: number = node.end! + this.startOffset! - ): never { + error(msg: string, node: Node & WithScope, scope?: TypeScope): never { throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ this.descriptor.filename }\n${generateCodeFrame( this.descriptor.source, node.start! + this.startOffset!, - end + node.end! + this.startOffset! )}` ) } } + +export function resolveParserPlugins( + lang: string, + userPlugins?: ParserPlugin[] +) { + const plugins: ParserPlugin[] = [] + if (lang === 'jsx' || lang === 'tsx') { + plugins.push('jsx') + } else if (userPlugins) { + // If don't match the case of adding jsx + // should remove the jsx from user options + userPlugins = userPlugins.filter(p => p !== 'jsx') + } + if (lang === 'ts' || lang === 'tsx') { + plugins.push('typescript') + if (!plugins.includes('decorators')) { + plugins.push('decorators-legacy') + } + } + if (userPlugins) { + plugins.push(...userPlugins) + } + return plugins +} diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f3b20a7ee15..1e82d2c8326 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,11 +1,13 @@ import { + Expression, Identifier, - Node as _Node, + Node, Statement, TSCallSignatureDeclaration, TSEnumDeclaration, TSExpressionWithTypeArguments, TSFunctionType, + TSInterfaceDeclaration, TSMappedType, TSMethodSignature, TSModuleBlock, @@ -18,81 +20,108 @@ import { TSTypeReference, TemplateLiteral } from '@babel/types' -import { UNKNOWN_TYPE } from './utils' -import { ScriptCompileContext } from './context' -import { ImportBinding } from '../compileScript' -import { TSInterfaceDeclaration } from '@babel/types' +import { UNKNOWN_TYPE, getId, getImportedName } from './utils' +import { ScriptCompileContext, resolveParserPlugins } from './context' +import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { capitalize, hasOwn } from '@vue/shared' -import { Expression } from '@babel/types' +import path from 'path' +import { parse as babelParse } from '@babel/parser' +import { parse } from '../parse' + +type Import = Pick export interface TypeScope { filename: string - imports: Record - types: Record - parent?: TypeScope + source: string + imports: Record + types: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > + exportedTypes: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > } -interface WithScope { +export interface WithScope { _ownerScope?: TypeScope } interface ResolvedElements { - props: Record + props: Record< + string, + (TSPropertySignature | TSMethodSignature) & { + // resolved props always has ownerScope attached + _ownerScope: TypeScope + } + > calls?: (TSCallSignatureDeclaration | TSFunctionType)[] } -type Node = _Node & - WithScope & { - _resolvedElements?: ResolvedElements - } - /** * Resolve arbitrary type node to a list of type elements that can be then * mapped to runtime props or emits. */ export function resolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node & WithScope & { _resolvedElements?: ResolvedElements }, + scope?: TypeScope ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements } - return (node._resolvedElements = innerResolveTypeElements(ctx, node)) + return (node._resolvedElements = innerResolveTypeElements( + ctx, + node, + node._ownerScope || scope || ctxToScope(ctx) + )) } function innerResolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node, + scope: TypeScope ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': - return typeElementsToMap(ctx, node.members, node._ownerScope) + return typeElementsToMap(ctx, node.members, scope) case 'TSInterfaceDeclaration': - return resolveInterfaceMembers(ctx, node) + return resolveInterfaceMembers(ctx, node, scope) case 'TSTypeAliasDeclaration': case 'TSParenthesizedType': - return resolveTypeElements(ctx, node.typeAnnotation) + return resolveTypeElements(ctx, node.typeAnnotation, scope) case 'TSFunctionType': { return { props: {}, calls: [node] } } case 'TSUnionType': case 'TSIntersectionType': return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t)), + node.types.map(t => resolveTypeElements(ctx, t, scope)), node.type ) case 'TSMappedType': - return resolveMappedType(ctx, node) + return resolveMappedType(ctx, node, scope) case 'TSIndexedAccessType': { if ( node.indexType.type === 'TSLiteralType' && node.indexType.literal.type === 'StringLiteral' ) { - const resolved = resolveTypeElements(ctx, node.objectType) + const resolved = resolveTypeElements(ctx, node.objectType, scope) const key = node.indexType.literal.value const targetType = resolved.props[key].typeAnnotation if (targetType) { - return resolveTypeElements(ctx, targetType.typeAnnotation) + return resolveTypeElements( + ctx, + targetType.typeAnnotation, + resolved.props[key]._ownerScope + ) } else { break } @@ -105,9 +134,9 @@ function innerResolveTypeElements( } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { - const resolved = resolveTypeReference(ctx, node) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveTypeElements(ctx, resolved) + return resolveTypeElements(ctx, resolved, resolved._ownerScope) } else { const typeName = getReferenceName(node) if ( @@ -118,7 +147,7 @@ function innerResolveTypeElements( return resolveBuiltin(ctx, node, typeName as any) } ctx.error( - `Failed to resolved type reference, or unsupported built-in utlility type.`, + `Failed to resolve type reference, or unsupported built-in utlility type.`, node ) } @@ -135,18 +164,13 @@ function typeElementsToMap( const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { - ;(e as Node)._ownerScope = scope - const name = - e.key.type === 'Identifier' - ? e.key.name - : e.key.type === 'StringLiteral' - ? e.key.value - : null + ;(e as WithScope)._ownerScope = scope + const name = getId(e.key) if (name && !e.computed) { - res.props[name] = e + res.props[name] = e as ResolvedElements['props'][string] } else if (e.key.type === 'TemplateLiteral') { for (const key of resolveTemplateKeys(ctx, e.key)) { - res.props[key] = e + res.props[key] = e as ResolvedElements['props'][string] } } else { ctx.error( @@ -172,11 +196,15 @@ function mergeElements( if (!hasOwn(baseProps, key)) { baseProps[key] = props[key] } else { - baseProps[key] = createProperty(baseProps[key].key, { - type, - // @ts-ignore - types: [baseProps[key], props[key]] - }) + baseProps[key] = createProperty( + baseProps[key].key, + { + type, + // @ts-ignore + types: [baseProps[key], props[key]] + }, + baseProps[key]._ownerScope + ) } } if (calls) { @@ -188,8 +216,9 @@ function mergeElements( function createProperty( key: Expression, - typeAnnotation: TSType -): TSPropertySignature { + typeAnnotation: TSType, + scope: TypeScope +): TSPropertySignature & { _ownerScope: TypeScope } { return { type: 'TSPropertySignature', key, @@ -197,18 +226,20 @@ function createProperty( typeAnnotation: { type: 'TSTypeAnnotation', typeAnnotation - } + }, + _ownerScope: scope } } function resolveInterfaceMembers( ctx: ScriptCompileContext, - node: TSInterfaceDeclaration & WithScope + node: TSInterfaceDeclaration & WithScope, + scope: TypeScope ): ResolvedElements { const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) if (node.extends) { for (const ext of node.extends) { - const { props } = resolveTypeElements(ctx, ext) + const { props } = resolveTypeElements(ctx, ext, scope) for (const key in props) { if (!hasOwn(base.props, key)) { base.props[key] = props[key] @@ -221,7 +252,8 @@ function resolveInterfaceMembers( function resolveMappedType( ctx: ScriptCompileContext, - node: TSMappedType + node: TSMappedType, + scope: TypeScope ): ResolvedElements { const res: ResolvedElements = { props: {} } if (!node.typeParameter.constraint) { @@ -234,7 +266,8 @@ function resolveMappedType( type: 'Identifier', name: key }, - node.typeAnnotation! + node.typeAnnotation!, + scope ) } return res @@ -357,32 +390,52 @@ function resolveTypeReference( node: (TSTypeReference | TSExpressionWithTypeArguments) & { _resolvedReference?: Node }, - scope = ctxToScope(ctx) -): Node | undefined { + scope?: TypeScope, + name?: string, + onlyExported = false +): (Node & WithScope) | undefined { if (node._resolvedReference) { return node._resolvedReference } - const name = getReferenceName(node) - return (node._resolvedReference = innerResolveTypeReference(scope, name)) + return (node._resolvedReference = innerResolveTypeReference( + ctx, + scope || ctxToScope(ctx), + name || getReferenceName(node), + node, + onlyExported + )) } function innerResolveTypeReference( + ctx: ScriptCompileContext, scope: TypeScope, - name: string | string[] + name: string | string[], + node: TSTypeReference | TSExpressionWithTypeArguments, + onlyExported: boolean ): Node | undefined { if (typeof name === 'string') { if (scope.imports[name]) { - // TODO external import - } else if (scope.types[name]) { - return scope.types[name] + return resolveTypeFromImport(ctx, scope, scope.imports[name], node) + } else { + const types = onlyExported ? scope.exportedTypes : scope.types + return types[name] } } else { - const ns = innerResolveTypeReference(scope, name[0]) + const ns = innerResolveTypeReference( + ctx, + scope, + name[0], + node, + onlyExported + ) if (ns && ns.type === 'TSModuleDeclaration') { const childScope = moduleDeclToScope(ns, scope) return innerResolveTypeReference( + ctx, childScope, - name.length > 2 ? name.slice(1) : name[name.length - 1] + name.length > 2 ? name.slice(1) : name[name.length - 1], + node, + true ) } } @@ -407,20 +460,125 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } +function resolveTypeFromImport( + ctx: ScriptCompileContext, + scope: TypeScope, + { source, imported }: Import, + node: TSTypeReference | TSExpressionWithTypeArguments +): Node | undefined { + const fs = ctx.options.fs + if (!fs) { + ctx.error( + `fs options for compileScript are required for resolving imported types`, + node + ) + } + // TODO (hmr) register dependency file on ctx + const containingFile = scope.filename + if (source.startsWith('.')) { + // relative import - fast path + const filename = path.join(containingFile, '..', source) + const resolved = resolveExt(filename, fs) + if (resolved) { + return resolveTypeReference( + ctx, + node, + fileToScope(ctx, resolved, fs), + imported, + true + ) + } else { + ctx.error(`Failed to resolve import source for type`, node) + } + } else { + // TODO module or aliased import - use full TS resolution + return + } +} + +function resolveExt( + filename: string, + fs: NonNullable +) { + const tryResolve = (filename: string) => { + if (fs.fileExists(filename)) return filename + } + return ( + tryResolve(filename) || + tryResolve(filename + `.ts`) || + tryResolve(filename + `.d.ts`) || + tryResolve(filename + `/index.ts`) || + tryResolve(filename + `/index.d.ts`) + ) +} + +function fileToScope( + ctx: ScriptCompileContext, + filename: string, + fs: NonNullable +): TypeScope { + // TODO cache + const source = fs.readFile(filename) + const body = parseFile(ctx, filename, source) + const scope: TypeScope = { + filename, + source, + types: Object.create(null), + exportedTypes: Object.create(null), + imports: recordImports(body) + } + recordTypes(body, scope) + return scope +} + +function parseFile( + ctx: ScriptCompileContext, + filename: string, + content: string +): Statement[] { + const ext = path.extname(filename) + if (ext === '.ts' || ext === '.tsx') { + return babelParse(content, { + plugins: resolveParserPlugins( + ext.slice(1), + ctx.options.babelParserPlugins + ), + sourceType: 'module' + }).program.body + } else if (ext === '.vue') { + const { + descriptor: { script, scriptSetup } + } = parse(content) + const scriptContent = (script?.content || '') + (scriptSetup?.content || '') + const lang = script?.lang || scriptSetup?.lang + return babelParse(scriptContent, { + plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), + sourceType: 'module' + }).program.body + } + return [] +} + function ctxToScope(ctx: ScriptCompileContext): TypeScope { if (ctx.scope) { return ctx.scope } + const scope: TypeScope = { + filename: ctx.descriptor.filename, + source: ctx.descriptor.source, + imports: Object.create(ctx.userImports), + types: Object.create(null), + exportedTypes: Object.create(null) + } + const body = ctx.scriptAst ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] : ctx.scriptSetupAst!.body - return (ctx.scope = { - filename: ctx.descriptor.filename, - imports: ctx.userImports, - types: recordTypes(body) - }) + recordTypes(body, scope) + + return (ctx.scope = scope) } function moduleDeclToScope( @@ -430,27 +588,56 @@ function moduleDeclToScope( if (node._resolvedChildScope) { return node._resolvedChildScope } - const types: TypeScope['types'] = Object.create(parent.types) const scope: TypeScope = { - filename: parent.filename, - imports: Object.create(parent.imports), - types: recordTypes((node.body as TSModuleBlock).body, types), - parent - } - for (const key of Object.keys(types)) { - types[key]._ownerScope = scope + ...parent, + types: Object.create(parent.types), + imports: Object.create(parent.imports) } + recordTypes((node.body as TSModuleBlock).body, scope) return (node._resolvedChildScope = scope) } -function recordTypes( - body: Statement[], - types: Record = Object.create(null) -) { - for (const s of body) { - recordType(s, types) +function recordTypes(body: Statement[], scope: TypeScope) { + const { types, exportedTypes, imports } = scope + for (const stmt of body) { + recordType(stmt, types) + } + for (const stmt of body) { + if (stmt.type === 'ExportNamedDeclaration') { + if (stmt.declaration) { + recordType(stmt.declaration, types) + recordType(stmt.declaration, exportedTypes) + } else { + for (const spec of stmt.specifiers) { + if (spec.type === 'ExportSpecifier') { + const local = spec.local.name + const exported = getId(spec.exported) + if (stmt.source) { + // re-export, register an import + export as a type reference + imports[local] = { + source: stmt.source.value, + imported: local + } + exportedTypes[exported] = { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: local + }, + _ownerScope: scope + } + } else if (types[local]) { + // exporting local defined type + exportedTypes[exported] = types[local] + } + } + } + } + } + } + for (const key of Object.keys(types)) { + types[key]._ownerScope = scope } - return types } function recordType(node: Node, types: Record) { @@ -465,12 +652,6 @@ function recordType(node: Node, types: Record) { case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation break - case 'ExportNamedDeclaration': { - if (node.declaration) { - recordType(node.declaration, types) - } - break - } case 'VariableDeclaration': { if (node.declare) { for (const decl of node.declarations) { @@ -486,9 +667,29 @@ function recordType(node: Node, types: Record) { } } +export function recordImports(body: Statement[]) { + const imports: TypeScope['imports'] = Object.create(null) + for (const s of body) { + recordImport(s, imports) + } + return imports +} + +function recordImport(node: Node, imports: TypeScope['imports']) { + if (node.type !== 'ImportDeclaration') { + return + } + for (const s of node.specifiers) { + imports[s.local.name] = { + imported: getImportedName(s), + source: node.source.value + } + } +} + export function inferRuntimeType( ctx: ScriptCompileContext, - node: Node, + node: Node & WithScope, scope = node._ownerScope || ctxToScope(ctx) ): string[] { switch (node.type) { diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 11bc011820e..780c780e2cc 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -1,4 +1,13 @@ -import { CallExpression, Node } from '@babel/types' +import { + CallExpression, + Expression, + Identifier, + ImportDefaultSpecifier, + ImportNamespaceSpecifier, + ImportSpecifier, + Node, + StringLiteral +} from '@babel/types' import { TS_NODE_TYPES } from '@vue/compiler-dom' export const UNKNOWN_TYPE = 'Unknown' @@ -48,3 +57,24 @@ export function isCallOf( export function toRuntimeTypeString(types: string[]) { return types.length > 1 ? `[${types.join(', ')}]` : types[0] } + +export function getImportedName( + specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier +) { + if (specifier.type === 'ImportSpecifier') + return specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value + else if (specifier.type === 'ImportNamespaceSpecifier') return '*' + return 'default' +} + +export function getId(node: Identifier | StringLiteral): string +export function getId(node: Expression): string | null +export function getId(node: Expression) { + return node.type === 'Identifier' + ? node.name + : node.type === 'StringLiteral' + ? node.value + : null +} From c93c11710e8a7572dee9f737cfba56cf0e2eb3f4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 21:36:23 +0800 Subject: [PATCH 026/142] refactor: improve type resolve error output --- .../compileScript/resolveType.spec.ts | 24 +++- packages/compiler-sfc/src/script/context.ts | 9 +- .../compiler-sfc/src/script/resolveType.ts | 116 +++++++++++------- 3 files changed, 100 insertions(+), 49 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 7f25ae4888d..c686a99ff93 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -327,16 +327,34 @@ describe('resolveType', () => { }) describe('errors', () => { - test('error on computed keys', () => { + test('failed type reference', () => { + expect(() => resolve(`type Target = X`)).toThrow( + `Unresolvable type reference` + ) + }) + + test('unsupported computed keys', () => { expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( - `computed keys are not supported in types referenced by SFC macros` + `Unsupported computed key in type referenced by a macro` + ) + }) + + test('unsupported index type', () => { + expect(() => resolve(`type Target = X[K]`)).toThrow( + `Unsupported index type` ) }) + + test('failed improt source resolve', () => { + expect(() => + resolve(`import { X } from './foo'; type Target = X`) + ).toThrow(`Failed to resolve import source "./foo" for type X`) + }) }) }) function resolve(code: string, files: Record = {}) { - const { descriptor } = parse(``, { + const { descriptor } = parse(``, { filename: 'Test.vue' }) const ctx = new ScriptCompileContext(descriptor, { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 1928ea900fc..efdf1368c34 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -126,13 +126,14 @@ export class ScriptCompileContext { } error(msg: string, node: Node & WithScope, scope?: TypeScope): never { + const offset = scope ? scope.offset || 0 : this.startOffset! throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ - this.descriptor.filename + (scope || this.descriptor).filename }\n${generateCodeFrame( - this.descriptor.source, - node.start! + this.startOffset!, - node.end! + this.startOffset! + (scope || this.descriptor).source, + node.start! + offset, + node.end! + offset )}` ) } diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 1e82d2c8326..f885b970212 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -33,6 +33,7 @@ type Import = Pick export interface TypeScope { filename: string source: string + offset: number imports: Record types: Record< string, @@ -128,7 +129,8 @@ function innerResolveTypeElements( } else { ctx.error( `Unsupported index type: ${node.indexType.type}`, - node.indexType + node.indexType, + scope ) } } @@ -144,16 +146,17 @@ function innerResolveTypeElements( // @ts-ignore SupportedBuiltinsSet.has(typeName) ) { - return resolveBuiltin(ctx, node, typeName as any) + return resolveBuiltin(ctx, node, typeName as any, scope) } ctx.error( - `Failed to resolve type reference, or unsupported built-in utlility type.`, - node + `Unresolvable type reference or unsupported built-in utlility type`, + node, + scope ) } } } - ctx.error(`Unresolvable type in SFC macro: ${node.type}`, node) + ctx.error(`Unresolvable type: ${node.type}`, node, scope) } function typeElementsToMap( @@ -169,13 +172,14 @@ function typeElementsToMap( if (name && !e.computed) { res.props[name] = e as ResolvedElements['props'][string] } else if (e.key.type === 'TemplateLiteral') { - for (const key of resolveTemplateKeys(ctx, e.key)) { + for (const key of resolveTemplateKeys(ctx, e.key, scope)) { res.props[key] = e as ResolvedElements['props'][string] } } else { ctx.error( - `computed keys are not supported in types referenced by SFC macros.`, - e + `Unsupported computed key in type referenced by a macro`, + e.key, + scope ) } } else if (e.type === 'TSCallSignatureDeclaration') { @@ -256,10 +260,7 @@ function resolveMappedType( scope: TypeScope ): ResolvedElements { const res: ResolvedElements = { props: {} } - if (!node.typeParameter.constraint) { - ctx.error(`mapped type used in macros must have a finite constraint.`, node) - } - const keys = resolveStringType(ctx, node.typeParameter.constraint) + const keys = resolveStringType(ctx, node.typeParameter.constraint!, scope) for (const key of keys) { res.props[key] = createProperty( { @@ -273,25 +274,29 @@ function resolveMappedType( return res } -function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] { +function resolveStringType( + ctx: ScriptCompileContext, + node: Node, + scope: TypeScope +): string[] { switch (node.type) { case 'StringLiteral': return [node.value] case 'TSLiteralType': - return resolveStringType(ctx, node.literal) + return resolveStringType(ctx, node.literal, scope) case 'TSUnionType': - return node.types.map(t => resolveStringType(ctx, t)).flat() + return node.types.map(t => resolveStringType(ctx, t, scope)).flat() case 'TemplateLiteral': { - return resolveTemplateKeys(ctx, node) + return resolveTemplateKeys(ctx, node, scope) } case 'TSTypeReference': { - const resolved = resolveTypeReference(ctx, node) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveStringType(ctx, resolved) + return resolveStringType(ctx, resolved, scope) } if (node.typeName.type === 'Identifier') { const getParam = (index = 0) => - resolveStringType(ctx, node.typeParameters!.params[index]) + resolveStringType(ctx, node.typeParameters!.params[index], scope) switch (node.typeName.name) { case 'Extract': return getParam(1) @@ -308,17 +313,18 @@ function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] { case 'Uncapitalize': return getParam().map(s => s[0].toLowerCase() + s.slice(1)) default: - ctx.error('Failed to resolve type reference', node) + ctx.error('Failed to resolve type reference', node, scope) } } } } - ctx.error('Failed to resolve string type into finite keys', node) + ctx.error('Failed to resolve string type into finite keys', node, scope) } function resolveTemplateKeys( ctx: ScriptCompileContext, - node: TemplateLiteral + node: TemplateLiteral, + scope: TypeScope ): string[] { if (!node.expressions.length) { return [node.quasis[0].value.raw] @@ -328,12 +334,16 @@ function resolveTemplateKeys( const e = node.expressions[0] const q = node.quasis[0] const leading = q ? q.value.raw : `` - const resolved = resolveStringType(ctx, e) - const restResolved = resolveTemplateKeys(ctx, { - ...node, - expressions: node.expressions.slice(1), - quasis: q ? node.quasis.slice(1) : node.quasis - }) + const resolved = resolveStringType(ctx, e, scope) + const restResolved = resolveTemplateKeys( + ctx, + { + ...node, + expressions: node.expressions.slice(1), + quasis: q ? node.quasis.slice(1) : node.quasis + }, + scope + ) for (const r of resolved) { for (const rr of restResolved) { @@ -357,7 +367,8 @@ type GetSetType = T extends Set ? V : never function resolveBuiltin( ctx: ScriptCompileContext, node: TSTypeReference | TSExpressionWithTypeArguments, - name: GetSetType + name: GetSetType, + scope: TypeScope ): ResolvedElements { const t = resolveTypeElements(ctx, node.typeParameters!.params[0]) switch (name) { @@ -366,7 +377,11 @@ function resolveBuiltin( case 'Readonly': return t case 'Pick': { - const picked = resolveStringType(ctx, node.typeParameters!.params[1]) + const picked = resolveStringType( + ctx, + node.typeParameters!.params[1], + scope + ) const res: ResolvedElements = { props: {}, calls: t.calls } for (const key of picked) { res.props[key] = t.props[key] @@ -374,7 +389,11 @@ function resolveBuiltin( return res } case 'Omit': - const omitted = resolveStringType(ctx, node.typeParameters!.params[1]) + const omitted = resolveStringType( + ctx, + node.typeParameters!.params[1], + scope + ) const res: ResolvedElements = { props: {}, calls: t.calls } for (const key in t.props) { if (!omitted.includes(key)) { @@ -415,7 +434,7 @@ function innerResolveTypeReference( ): Node | undefined { if (typeof name === 'string') { if (scope.imports[name]) { - return resolveTypeFromImport(ctx, scope, scope.imports[name], node) + return resolveTypeFromImport(ctx, node, name, scope) } else { const types = onlyExported ? scope.exportedTypes : scope.types return types[name] @@ -462,19 +481,21 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { function resolveTypeFromImport( ctx: ScriptCompileContext, - scope: TypeScope, - { source, imported }: Import, - node: TSTypeReference | TSExpressionWithTypeArguments + node: TSTypeReference | TSExpressionWithTypeArguments, + name: string, + scope: TypeScope ): Node | undefined { const fs = ctx.options.fs if (!fs) { ctx.error( `fs options for compileScript are required for resolving imported types`, - node + node, + scope ) } // TODO (hmr) register dependency file on ctx const containingFile = scope.filename + const { source, imported } = scope.imports[name] if (source.startsWith('.')) { // relative import - fast path const filename = path.join(containingFile, '..', source) @@ -488,7 +509,13 @@ function resolveTypeFromImport( true ) } else { - ctx.error(`Failed to resolve import source for type`, node) + ctx.error( + `Failed to resolve import source ${JSON.stringify( + source + )} for type ${name}`, + node, + scope + ) } } else { // TODO module or aliased import - use full TS resolution @@ -519,10 +546,11 @@ function fileToScope( ): TypeScope { // TODO cache const source = fs.readFile(filename) - const body = parseFile(ctx, filename, source) + const [body, offset] = parseFile(ctx, filename, source) const scope: TypeScope = { filename, source, + offset, types: Object.create(null), exportedTypes: Object.create(null), imports: recordImports(body) @@ -535,10 +563,12 @@ function parseFile( ctx: ScriptCompileContext, filename: string, content: string -): Statement[] { +): [Statement[], number] { + let body: Statement[] = [] + let offset = 0 const ext = path.extname(filename) if (ext === '.ts' || ext === '.tsx') { - return babelParse(content, { + body = babelParse(content, { plugins: resolveParserPlugins( ext.slice(1), ctx.options.babelParserPlugins @@ -551,12 +581,13 @@ function parseFile( } = parse(content) const scriptContent = (script?.content || '') + (scriptSetup?.content || '') const lang = script?.lang || scriptSetup?.lang - return babelParse(scriptContent, { + body = babelParse(scriptContent, { plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), sourceType: 'module' }).program.body + offset = scriptSetup ? scriptSetup.loc.start.offset : 0 } - return [] + return [body, offset] } function ctxToScope(ctx: ScriptCompileContext): TypeScope { @@ -567,6 +598,7 @@ function ctxToScope(ctx: ScriptCompileContext): TypeScope { const scope: TypeScope = { filename: ctx.descriptor.filename, source: ctx.descriptor.source, + offset: ctx.startOffset!, imports: Object.create(ctx.userImports), types: Object.create(null), exportedTypes: Object.create(null) From 8451b92a7a231a8c4b8e936f0918e321d38ce690 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 22:38:00 +0800 Subject: [PATCH 027/142] wip: cache fileToScope + improve vue file offset --- .../compileScript/resolveType.spec.ts | 5 ++ packages/compiler-sfc/src/cache.ts | 10 ++-- packages/compiler-sfc/src/index.ts | 1 + packages/compiler-sfc/src/script/context.ts | 2 +- .../compiler-sfc/src/script/resolveType.ts | 51 +++++++++++++++---- 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index c686a99ff93..3e2a5ee1776 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -3,6 +3,7 @@ import { parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' import { inferRuntimeType, + invalidateTypeCache, recordImports, resolveTypeElements } from '../../src/script/resolveType' @@ -369,6 +370,10 @@ function resolve(code: string, files: Record = {}) { } }) + for (const file in files) { + invalidateTypeCache(file) + } + // ctx.userImports is collected when calling compileScript(), but we are // skipping that here, so need to manually register imports ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts index 510dfee3547..eb6bad0f86d 100644 --- a/packages/compiler-sfc/src/cache.ts +++ b/packages/compiler-sfc/src/cache.ts @@ -1,7 +1,11 @@ import LRU from 'lru-cache' export function createCache(size = 500) { - return __GLOBAL__ || __ESM_BROWSER__ - ? new Map() - : (new LRU(size) as any as Map) + if (__GLOBAL__ || __ESM_BROWSER__) { + return new Map() + } + const cache = new LRU(size) + // @ts-expect-error + cache.delete = cache.del.bind(cache) + return cache as any as Map } diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 6ba097b2466..0b936553a32 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -6,6 +6,7 @@ export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' +export { invalidateTypeCache } from './script/resolveType' export { shouldTransform as shouldTransformRef, transform as transformRef, diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index efdf1368c34..ec6bbe8d0f1 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -126,7 +126,7 @@ export class ScriptCompileContext { } error(msg: string, node: Node & WithScope, scope?: TypeScope): never { - const offset = scope ? scope.offset || 0 : this.startOffset! + const offset = scope ? scope.offset : this.startOffset! throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ (scope || this.descriptor).filename diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f885b970212..c48e192f641 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -27,6 +27,7 @@ import { capitalize, hasOwn } from '@vue/shared' import path from 'path' import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' +import { createCache } from '../cache' type Import = Pick @@ -539,23 +540,35 @@ function resolveExt( ) } +const fileToScopeCache = createCache() + +export function invalidateTypeCache(filename: string) { + fileToScopeCache.delete(filename) +} + function fileToScope( ctx: ScriptCompileContext, filename: string, fs: NonNullable ): TypeScope { - // TODO cache + const cached = fileToScopeCache.get(filename) + if (cached) { + return cached + } + const source = fs.readFile(filename) - const [body, offset] = parseFile(ctx, filename, source) + const body = parseFile(ctx, filename, source) const scope: TypeScope = { filename, source, - offset, + offset: 0, types: Object.create(null), exportedTypes: Object.create(null), imports: recordImports(body) } recordTypes(body, scope) + + fileToScopeCache.set(filename, scope) return scope } @@ -563,12 +576,10 @@ function parseFile( ctx: ScriptCompileContext, filename: string, content: string -): [Statement[], number] { - let body: Statement[] = [] - let offset = 0 +): Statement[] { const ext = path.extname(filename) if (ext === '.ts' || ext === '.tsx') { - body = babelParse(content, { + return babelParse(content, { plugins: resolveParserPlugins( ext.slice(1), ctx.options.babelParserPlugins @@ -579,15 +590,33 @@ function parseFile( const { descriptor: { script, scriptSetup } } = parse(content) - const scriptContent = (script?.content || '') + (scriptSetup?.content || '') + if (!script && !scriptSetup) { + return [] + } + + // ensure the correct offset with original source + const scriptOffset = script ? script.loc.start.offset : Infinity + const scriptSetupOffset = scriptSetup + ? scriptSetup.loc.start.offset + : Infinity + const firstBlock = scriptOffset < scriptSetupOffset ? script : scriptSetup + const secondBlock = scriptOffset < scriptSetupOffset ? scriptSetup : script + + let scriptContent = + ' '.repeat(Math.min(scriptOffset, scriptSetupOffset)) + + firstBlock!.content + if (secondBlock) { + scriptContent += + ' '.repeat(secondBlock.loc.start.offset - script!.loc.end.offset) + + secondBlock.content + } const lang = script?.lang || scriptSetup?.lang - body = babelParse(scriptContent, { + return babelParse(scriptContent, { plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), sourceType: 'module' }).program.body - offset = scriptSetup ? scriptSetup.loc.start.offset : 0 } - return [body, offset] + return [] } function ctxToScope(ctx: ScriptCompileContext): TypeScope { From 3982bef533b451d1b59fa243560184a13fe8c18c Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 14 Apr 2023 17:27:50 +0800 Subject: [PATCH 028/142] feat(compiler-sfc): support resolving type imports from modules --- .../compileScript/resolveType.spec.ts | 68 ++++++-- packages/compiler-sfc/src/compileScript.ts | 2 +- packages/compiler-sfc/src/index.ts | 4 +- .../compiler-sfc/src/script/resolveType.ts | 154 ++++++++++++++---- packages/compiler-sfc/src/script/utils.ts | 19 +++ packages/sfc-playground/src/Header.vue | 2 +- packages/sfc-playground/vite.config.ts | 13 +- packages/vue/compiler-sfc/index.js | 2 + packages/vue/compiler-sfc/index.mjs | 4 +- packages/vue/compiler-sfc/package.json | 2 +- packages/vue/compiler-sfc/register-ts.js | 5 + 11 files changed, 231 insertions(+), 44 deletions(-) create mode 100644 packages/vue/compiler-sfc/register-ts.js diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 3e2a5ee1776..6045cbd3d7a 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -5,9 +5,13 @@ import { inferRuntimeType, invalidateTypeCache, recordImports, - resolveTypeElements + resolveTypeElements, + registerTS } from '../../src/script/resolveType' +import ts from 'typescript' +registerTS(ts) + describe('resolveType', () => { test('type literal', () => { const { props, calls } = resolve(`type Target = { @@ -86,6 +90,19 @@ describe('resolveType', () => { }) }) + test('reference class', () => { + expect( + resolve(` + class Foo {} + type Target = { + foo: Foo + } + `).props + ).toStrictEqual({ + foo: ['Object'] + }) + }) + test('function type', () => { expect( resolve(` @@ -258,8 +275,8 @@ describe('resolveType', () => { type Target = P & PP `, { - 'foo.ts': 'export type P = { foo: number }', - 'bar.d.ts': 'type X = { bar: string }; export { X as Y }' + '/foo.ts': 'export type P = { foo: number }', + '/bar.d.ts': 'type X = { bar: string }; export { X as Y }' } ).props ).toStrictEqual({ @@ -277,9 +294,9 @@ describe('resolveType', () => { type Target = P & PP `, { - 'foo.vue': + '/foo.vue': '', - 'bar.vue': + '/bar.vue': '' } ).props @@ -297,9 +314,9 @@ describe('resolveType', () => { type Target = P `, { - 'foo.ts': `import type { P as PP } from './nested/bar.vue' + '/foo.ts': `import type { P as PP } from './nested/bar.vue' export type P = { foo: number } & PP`, - 'nested/bar.vue': + '/nested/bar.vue': '' } ).props @@ -317,11 +334,42 @@ describe('resolveType', () => { type Target = P `, { - 'foo.ts': `export { P as PP } from './bar'`, - 'bar.ts': 'export type P = { bar: string }' + '/foo.ts': `export { P as PP } from './bar'`, + '/bar.ts': 'export type P = { bar: string }' + } + ).props + ).toStrictEqual({ + bar: ['String'] + }) + }) + + test('ts module resolve', () => { + expect( + resolve( + ` + import { P } from 'foo' + import { PP } from 'bar' + type Target = P & PP + `, + { + '/node_modules/foo/package.json': JSON.stringify({ + name: 'foo', + version: '1.0.0', + types: 'index.d.ts' + }), + '/node_modules/foo/index.d.ts': 'export type P = { foo: number }', + '/tsconfig.json': JSON.stringify({ + compilerOptions: { + paths: { + bar: ['./other/bar.ts'] + } + } + }), + '/other/bar.ts': 'export type PP = { bar: string }' } ).props ).toStrictEqual({ + foo: ['Number'], bar: ['String'] }) }) @@ -356,7 +404,7 @@ describe('resolveType', () => { function resolve(code: string, files: Record = {}) { const { descriptor } = parse(``, { - filename: 'Test.vue' + filename: '/Test.vue' }) const ctx = new ScriptCompileContext(descriptor, { id: 'test', diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 593e8e072c6..989f61cb444 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -115,7 +115,7 @@ export interface SFCScriptCompileOptions { */ fs?: { fileExists(file: string): boolean - readFile(file: string): string + readFile(file: string): string | undefined } } diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 0b936553a32..e171ac0885c 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -6,7 +6,6 @@ export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' -export { invalidateTypeCache } from './script/resolveType' export { shouldTransform as shouldTransformRef, transform as transformRef, @@ -29,6 +28,9 @@ export { isStaticProperty } from '@vue/compiler-core' +// Internals for type resolution +export { invalidateTypeCache, registerTS } from './script/resolveType' + // Types export type { SFCParseOptions, diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index c48e192f641..9d306d7bc5c 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -20,14 +20,20 @@ import { TSTypeReference, TemplateLiteral } from '@babel/types' -import { UNKNOWN_TYPE, getId, getImportedName } from './utils' +import { + UNKNOWN_TYPE, + createGetCanonicalFileName, + getId, + getImportedName +} from './utils' import { ScriptCompileContext, resolveParserPlugins } from './context' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { capitalize, hasOwn } from '@vue/shared' -import path from 'path' import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' import { createCache } from '../cache' +import type TS from 'typescript' +import { join, extname, dirname } from 'path' type Import = Pick @@ -480,54 +486,82 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } +let ts: typeof TS + +export function registerTS(_ts: any) { + ts = _ts +} + +type FS = NonNullable + function resolveTypeFromImport( ctx: ScriptCompileContext, node: TSTypeReference | TSExpressionWithTypeArguments, name: string, scope: TypeScope ): Node | undefined { - const fs = ctx.options.fs + const fs: FS = ctx.options.fs || ts?.sys if (!fs) { ctx.error( - `fs options for compileScript are required for resolving imported types`, - node, - scope + `No fs option provided to \`compileScript\` in non-Node environment. ` + + `File system access is required for resolving imported types.`, + node ) } - // TODO (hmr) register dependency file on ctx + const containingFile = scope.filename const { source, imported } = scope.imports[name] + + let resolved: string | undefined + if (source.startsWith('.')) { // relative import - fast path - const filename = path.join(containingFile, '..', source) - const resolved = resolveExt(filename, fs) - if (resolved) { - return resolveTypeReference( - ctx, + const filename = join(containingFile, '..', source) + resolved = resolveExt(filename, fs) + } else { + // module or aliased import - use full TS resolution, only supported in Node + if (!__NODE_JS__) { + ctx.error( + `Type import from non-relative sources is not supported in the browser build.`, node, - fileToScope(ctx, resolved, fs), - imported, - true + scope ) - } else { + } + if (!ts) { ctx.error( - `Failed to resolve import source ${JSON.stringify( + `Failed to resolve type ${imported} from module ${JSON.stringify( source - )} for type ${name}`, + )}. ` + + `typescript is required as a peer dep for vue in order ` + + `to support resolving types from module imports.`, node, scope ) } + resolved = resolveWithTS(containingFile, source, fs) + } + + if (resolved) { + // TODO (hmr) register dependency file on ctx + return resolveTypeReference( + ctx, + node, + fileToScope(ctx, resolved, fs), + imported, + true + ) } else { - // TODO module or aliased import - use full TS resolution - return + ctx.error( + `Failed to resolve import source ${JSON.stringify( + source + )} for type ${name}`, + node, + scope + ) } } -function resolveExt( - filename: string, - fs: NonNullable -) { +function resolveExt(filename: string, fs: FS) { const tryResolve = (filename: string) => { if (fs.fileExists(filename)) return filename } @@ -540,23 +574,83 @@ function resolveExt( ) } +const tsConfigCache = createCache<{ + options: TS.CompilerOptions + cache: TS.ModuleResolutionCache +}>() + +function resolveWithTS( + containingFile: string, + source: string, + fs: FS +): string | undefined { + if (!__NODE_JS__) return + + // 1. resolve tsconfig.json + const configPath = ts.findConfigFile(containingFile, fs.fileExists) + // 2. load tsconfig.json + let options: TS.CompilerOptions + let cache: TS.ModuleResolutionCache | undefined + if (configPath) { + const cached = tsConfigCache.get(configPath) + if (!cached) { + // The only case where `fs` is NOT `ts.sys` is during tests. + // parse config host requires an extra `readDirectory` method + // during tests, which is stubbed. + const parseConfigHost = __TEST__ + ? { + ...fs, + useCaseSensitiveFileNames: true, + readDirectory: () => [] + } + : ts.sys + const parsed = ts.parseJsonConfigFileContent( + ts.readConfigFile(configPath, fs.readFile).config, + parseConfigHost, + dirname(configPath), + undefined, + configPath + ) + options = parsed.options + cache = ts.createModuleResolutionCache( + process.cwd(), + createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames), + options + ) + tsConfigCache.set(configPath, { options, cache }) + } else { + ;({ options, cache } = cached) + } + } else { + options = {} + } + + // 3. resolve + const res = ts.resolveModuleName(source, containingFile, options, fs, cache) + + if (res.resolvedModule) { + return res.resolvedModule.resolvedFileName + } +} + const fileToScopeCache = createCache() export function invalidateTypeCache(filename: string) { fileToScopeCache.delete(filename) + tsConfigCache.delete(filename) } function fileToScope( ctx: ScriptCompileContext, filename: string, - fs: NonNullable + fs: FS ): TypeScope { const cached = fileToScopeCache.get(filename) if (cached) { return cached } - const source = fs.readFile(filename) + const source = fs.readFile(filename) || '' const body = parseFile(ctx, filename, source) const scope: TypeScope = { filename, @@ -577,7 +671,7 @@ function parseFile( filename: string, content: string ): Statement[] { - const ext = path.extname(filename) + const ext = extname(filename) if (ext === '.ts' || ext === '.tsx') { return babelParse(content, { plugins: resolveParserPlugins( @@ -705,7 +799,8 @@ function recordType(node: Node, types: Record) { switch (node.type) { case 'TSInterfaceDeclaration': case 'TSEnumDeclaration': - case 'TSModuleDeclaration': { + case 'TSModuleDeclaration': + case 'ClassDeclaration': { const id = node.id.type === 'Identifier' ? node.id.name : node.id.value types[id] = node break @@ -899,6 +994,9 @@ export function inferRuntimeType( } } + case 'ClassDeclaration': + return ['Object'] + default: return [UNKNOWN_TYPE] // no runtime check } diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 780c780e2cc..6d874f8a6db 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -78,3 +78,22 @@ export function getId(node: Expression) { ? node.value : null } + +const identity = (str: string) => str +const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g +const toLowerCase = (str: string) => str.toLowerCase() + +function toFileNameLowerCase(x: string) { + return fileNameLowerCaseRegExp.test(x) + ? x.replace(fileNameLowerCaseRegExp, toLowerCase) + : x +} + +/** + * We need `getCanonicalFileName` when creating ts module resolution cache, + * but TS does not expose it directly. This implementation is repllicated from + * the TS source code. + */ +export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean) { + return useCaseSensitiveFileNames ? identity : toFileNameLowerCase +} diff --git a/packages/sfc-playground/src/Header.vue b/packages/sfc-playground/src/Header.vue index 91ce3efc46e..b55f0240906 100644 --- a/packages/sfc-playground/src/Header.vue +++ b/packages/sfc-playground/src/Header.vue @@ -6,7 +6,7 @@ import Moon from './icons/Moon.vue' import Share from './icons/Share.vue' import Download from './icons/Download.vue' import GitHub from './icons/GitHub.vue' -import { ReplStore } from '@vue/repl' +import type { ReplStore } from '@vue/repl' const props = defineProps<{ store: ReplStore diff --git a/packages/sfc-playground/vite.config.ts b/packages/sfc-playground/vite.config.ts index 44d5a53509f..5176b9cf061 100644 --- a/packages/sfc-playground/vite.config.ts +++ b/packages/sfc-playground/vite.config.ts @@ -7,7 +7,18 @@ import execa from 'execa' const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7) export default defineConfig({ - plugins: [vue(), copyVuePlugin()], + plugins: [ + vue({ + script: { + // @ts-ignore + fs: { + fileExists: fs.existsSync, + readFile: file => fs.readFileSync(file, 'utf-8') + } + } + }), + copyVuePlugin() + ], define: { __COMMIT__: JSON.stringify(commit), __VUE_PROD_DEVTOOLS__: JSON.stringify(true) diff --git a/packages/vue/compiler-sfc/index.js b/packages/vue/compiler-sfc/index.js index 774f9da2742..2b85ad129ef 100644 --- a/packages/vue/compiler-sfc/index.js +++ b/packages/vue/compiler-sfc/index.js @@ -1 +1,3 @@ module.exports = require('@vue/compiler-sfc') + +require('./register-ts.js') diff --git a/packages/vue/compiler-sfc/index.mjs b/packages/vue/compiler-sfc/index.mjs index 8df9a989d18..ae5d6e8e5ca 100644 --- a/packages/vue/compiler-sfc/index.mjs +++ b/packages/vue/compiler-sfc/index.mjs @@ -1 +1,3 @@ -export * from '@vue/compiler-sfc' \ No newline at end of file +export * from '@vue/compiler-sfc' + +import './register-ts.js' diff --git a/packages/vue/compiler-sfc/package.json b/packages/vue/compiler-sfc/package.json index 1b15fb844ac..778c7ebf51c 100644 --- a/packages/vue/compiler-sfc/package.json +++ b/packages/vue/compiler-sfc/package.json @@ -2,4 +2,4 @@ "main": "index.js", "module": "index.mjs", "types": "index.d.ts" -} \ No newline at end of file +} diff --git a/packages/vue/compiler-sfc/register-ts.js b/packages/vue/compiler-sfc/register-ts.js new file mode 100644 index 00000000000..87f61b64863 --- /dev/null +++ b/packages/vue/compiler-sfc/register-ts.js @@ -0,0 +1,5 @@ +if (typeof require !== 'undefined') { + try { + require('@vue/compiler-sfc').registerTS(require('typescript')) + } catch (e) {} +} From 34a007d00dd7fe6a16dd0ec1c999725cb0753704 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 14 Apr 2023 17:43:14 +0800 Subject: [PATCH 029/142] test: refactor resolveType test --- .../compileScript/resolveType.spec.ts | 119 +++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 6045cbd3d7a..2f5b233091d 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1,4 +1,4 @@ -import { TSTypeAliasDeclaration } from '@babel/types' +import { Identifier } from '@babel/types' import { parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' import { @@ -14,13 +14,13 @@ registerTS(ts) describe('resolveType', () => { test('type literal', () => { - const { props, calls } = resolve(`type Target = { + const { props, calls } = resolve(`defineProps<{ foo: number // property bar(): void // method 'baz': string // string literal key (e: 'foo'): void // call signature (e: 'bar'): void - }`) + }>()`) expect(props).toStrictEqual({ foo: ['Number'], bar: ['Function'], @@ -33,7 +33,7 @@ describe('resolveType', () => { expect( resolve(` type Aliased = { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -44,7 +44,7 @@ describe('resolveType', () => { expect( resolve(` export type Aliased = { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -55,7 +55,7 @@ describe('resolveType', () => { expect( resolve(` interface Aliased { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -66,7 +66,7 @@ describe('resolveType', () => { expect( resolve(` export interface Aliased { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -80,7 +80,7 @@ describe('resolveType', () => { export interface B extends A { b: boolean } interface C { c: string } interface Aliased extends B, C { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ a: ['Function'], @@ -94,9 +94,7 @@ describe('resolveType', () => { expect( resolve(` class Foo {} - type Target = { - foo: Foo - } + defineProps<{ foo: Foo }>() `).props ).toStrictEqual({ foo: ['Object'] @@ -106,7 +104,7 @@ describe('resolveType', () => { test('function type', () => { expect( resolve(` - type Target = (e: 'foo') => void + defineProps<(e: 'foo') => void>() `).calls?.length ).toBe(1) }) @@ -115,7 +113,7 @@ describe('resolveType', () => { expect( resolve(` type Fn = (e: 'foo') => void - type Target = Fn + defineProps() `).calls?.length ).toBe(1) }) @@ -126,7 +124,7 @@ describe('resolveType', () => { type Foo = { foo: number } type Bar = { bar: string } type Baz = { bar: string | boolean } - type Target = { self: any } & Foo & Bar & Baz + defineProps<{ self: any } & Foo & Bar & Baz>() `).props ).toStrictEqual({ self: ['Unknown'], @@ -156,7 +154,7 @@ describe('resolveType', () => { note: string } - type Target = CommonProps & ConditionalProps + defineProps() `).props ).toStrictEqual({ size: ['String'], @@ -171,9 +169,9 @@ describe('resolveType', () => { resolve(` type T = 'foo' | 'bar' type S = 'x' | 'y' - type Target = { + defineProps<{ [\`_\${T}_\${S}_\`]: string - } + }>() `).props ).toStrictEqual({ _foo_x_: ['String'], @@ -187,7 +185,7 @@ describe('resolveType', () => { expect( resolve(` type T = 'foo' | 'bar' - type Target = { [K in T]: string | number } & { + defineProps<{ [K in T]: string | number } & { [K in 'optional']?: boolean } & { [K in Capitalize]: string @@ -195,7 +193,7 @@ describe('resolveType', () => { [K in Uppercase>]: string } & { [K in \`x\${T}\`]: string - } + }>() `).props ).toStrictEqual({ foo: ['String', 'Number'], @@ -214,7 +212,7 @@ describe('resolveType', () => { resolve(` type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' - type Target = Pick + defineProps>() `).props ).toStrictEqual({ foo: ['Number'], @@ -227,7 +225,7 @@ describe('resolveType', () => { resolve(` type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' - type Target = Omit + defineProps>() `).props ).toStrictEqual({ baz: ['Boolean'] @@ -239,7 +237,7 @@ describe('resolveType', () => { resolve(` type T = { bar: number } type S = { nested: { foo: T['bar'] }} - type Target = S['nested'] + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -258,7 +256,7 @@ describe('resolveType', () => { } } } - type Target = Foo.Bar.A + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -272,7 +270,7 @@ describe('resolveType', () => { ` import { P } from './foo' import { Y as PP } from './bar' - type Target = P & PP + defineProps

() `, { '/foo.ts': 'export type P = { foo: number }', @@ -291,7 +289,7 @@ describe('resolveType', () => { ` import { P } from './foo.vue' import { P as PP } from './bar.vue' - type Target = P & PP + defineProps

() `, { '/foo.vue': @@ -311,7 +309,7 @@ describe('resolveType', () => { resolve( ` import { P } from './foo' - type Target = P + defineProps

() `, { '/foo.ts': `import type { P as PP } from './nested/bar.vue' @@ -331,7 +329,7 @@ describe('resolveType', () => { resolve( ` import { PP as P } from './foo' - type Target = P + defineProps

() `, { '/foo.ts': `export { P as PP } from './bar'`, @@ -344,31 +342,31 @@ describe('resolveType', () => { }) test('ts module resolve', () => { - expect( - resolve( - ` + const files = { + '/node_modules/foo/package.json': JSON.stringify({ + types: 'index.d.ts' + }), + '/node_modules/foo/index.d.ts': 'export type P = { foo: number }', + '/tsconfig.json': JSON.stringify({ + compilerOptions: { + paths: { + bar: ['./pp.ts'] + } + } + }), + '/pp.ts': 'export type PP = { bar: string }' + } + + const { props } = resolve( + ` import { P } from 'foo' import { PP } from 'bar' - type Target = P & PP + defineProps

() `, - { - '/node_modules/foo/package.json': JSON.stringify({ - name: 'foo', - version: '1.0.0', - types: 'index.d.ts' - }), - '/node_modules/foo/index.d.ts': 'export type P = { foo: number }', - '/tsconfig.json': JSON.stringify({ - compilerOptions: { - paths: { - bar: ['./other/bar.ts'] - } - } - }), - '/other/bar.ts': 'export type PP = { bar: string }' - } - ).props - ).toStrictEqual({ + files + ) + + expect(props).toStrictEqual({ foo: ['Number'], bar: ['String'] }) @@ -377,26 +375,26 @@ describe('resolveType', () => { describe('errors', () => { test('failed type reference', () => { - expect(() => resolve(`type Target = X`)).toThrow( + expect(() => resolve(`defineProps()`)).toThrow( `Unresolvable type reference` ) }) test('unsupported computed keys', () => { - expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( + expect(() => resolve(`defineProps<{ [Foo]: string }>()`)).toThrow( `Unsupported computed key in type referenced by a macro` ) }) test('unsupported index type', () => { - expect(() => resolve(`type Target = X[K]`)).toThrow( + expect(() => resolve(`defineProps()`)).toThrow( `Unsupported index type` ) }) test('failed improt source resolve', () => { expect(() => - resolve(`import { X } from './foo'; type Target = X`) + resolve(`import { X } from './foo'; defineProps()`) ).toThrow(`Failed to resolve import source "./foo" for type X`) }) }) @@ -426,10 +424,17 @@ function resolve(code: string, files: Record = {}) { // skipping that here, so need to manually register imports ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any - const targetDecl = ctx.scriptSetupAst!.body.find( - s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' - ) as TSTypeAliasDeclaration - const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation) + let target: any + for (const s of ctx.scriptSetupAst!.body) { + if ( + s.type === 'ExpressionStatement' && + s.expression.type === 'CallExpression' && + (s.expression.callee as Identifier).name === 'defineProps' + ) { + target = s.expression.typeParameters!.params[0] + } + } + const raw = resolveTypeElements(ctx, target) const props: Record = {} for (const key in raw.props) { props[key] = inferRuntimeType(ctx, raw.props[key]) From a9f5e14c7c7eb279bea538f69548fc6badd7df92 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 14 Apr 2023 20:53:21 +0800 Subject: [PATCH 030/142] chore: comments [ci skip] --- packages/compiler-sfc/src/compileScript.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 989f61cb444..c561a77a49d 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -111,7 +111,9 @@ export interface SFCScriptCompileOptions { */ defineModel?: boolean /** - * + * File system access methods to be used when resolving types + * imported in SFC macros. Defaults to ts.sys in Node.js, can be overwritten + * to use a virtual file system for use in browsers (e.g. in REPLs) */ fs?: { fileExists(file: string): boolean From 075498c959c547d0532e8b216bd6ac4c2d19230d Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 15 Apr 2023 17:59:50 +0800 Subject: [PATCH 031/142] refactor: avoid hard error when inferring runtime type --- .../compileScript/resolveType.spec.ts | 5 +++++ packages/compiler-sfc/src/parse.ts | 7 +++++++ packages/compiler-sfc/src/script/context.ts | 10 ++++++--- .../compiler-sfc/src/script/resolveType.ts | 21 ++++++++++++++----- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 2f5b233091d..6f07cfcfab4 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -397,6 +397,11 @@ describe('resolveType', () => { resolve(`import { X } from './foo'; defineProps()`) ).toThrow(`Failed to resolve import source "./foo" for type X`) }) + + test('should not error on unresolved type when inferring runtime type', () => { + expect(() => resolve(`defineProps<{ foo: T }>()`)).not.toThrow() + expect(() => resolve(`defineProps<{ foo: T['bar'] }>()`)).not.toThrow() + }) }) }) diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts index b46c5ea1332..29c91cc1977 100644 --- a/packages/compiler-sfc/src/parse.ts +++ b/packages/compiler-sfc/src/parse.ts @@ -47,6 +47,13 @@ export interface SFCScriptBlock extends SFCBlock { imports?: Record scriptAst?: import('@babel/types').Statement[] scriptSetupAst?: import('@babel/types').Statement[] + warnings?: string[] + /** + * Fully resolved dependency file paths (unix slashes) with imported types + * used in macros, used for HMR cache busting in @vitejs/plugin-vue and + * vue-loader. + */ + deps?: string[] } export interface SFCStyleBlock extends SFCBlock { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index ec6bbe8d0f1..1f96584507b 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -7,7 +7,7 @@ import { PropsDestructureBindings } from './defineProps' import { ModelDecl } from './defineModel' import { BindingMetadata } from '../../../compiler-core/src' import MagicString from 'magic-string' -import { TypeScope, WithScope } from './resolveType' +import { TypeScope } from './resolveType' export class ScriptCompileContext { isJS: boolean @@ -56,13 +56,17 @@ export class ScriptCompileContext { // codegen bindingMetadata: BindingMetadata = {} - helperImports: Set = new Set() helper(key: string): string { this.helperImports.add(key) return `_${key}` } + /** + * to be exposed on compiled script block for HMR cache busting + */ + deps?: string[] + constructor( public descriptor: SFCDescriptor, public options: SFCScriptCompileOptions @@ -125,7 +129,7 @@ export class ScriptCompileContext { return block.content.slice(node.start!, node.end!) } - error(msg: string, node: Node & WithScope, scope?: TypeScope): never { + error(msg: string, node: Node, scope?: TypeScope): never { const offset = scope ? scope.offset : this.startOffset! throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 9d306d7bc5c..bbbbd4c5bc0 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -134,6 +134,7 @@ function innerResolveTypeElements( break } } else { + // TODO support `number` and `string` index type when possible ctx.error( `Unsupported index type: ${node.indexType.type}`, node.indexType, @@ -320,7 +321,11 @@ function resolveStringType( case 'Uncapitalize': return getParam().map(s => s[0].toLowerCase() + s.slice(1)) default: - ctx.error('Failed to resolve type reference', node, scope) + ctx.error( + 'Unsupported type when resolving string type', + node.typeName, + scope + ) } } } @@ -906,7 +911,7 @@ export function inferRuntimeType( if (node.typeName.type === 'Identifier') { const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return inferRuntimeType(ctx, resolved, scope) + return inferRuntimeType(ctx, resolved, resolved._ownerScope) } switch (node.typeName.name) { case 'Array': @@ -988,9 +993,15 @@ export function inferRuntimeType( node.indexType.type === 'TSLiteralType' && node.indexType.literal.type === 'StringLiteral' ) { - const resolved = resolveTypeElements(ctx, node.objectType) - const key = node.indexType.literal.value - return inferRuntimeType(ctx, resolved.props[key]) + try { + const resolved = resolveTypeElements(ctx, node.objectType, scope) + const key = node.indexType.literal.value + const prop = resolved.props[key] + return inferRuntimeType(ctx, prop, prop._ownerScope) + } catch (e) { + // avoid hard error, fallback to unknown + return [UNKNOWN_TYPE] + } } } From 8d8ddd686c832b2ea29b87ef47666b13c4ad5d4c Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 15 Apr 2023 18:06:48 +0800 Subject: [PATCH 032/142] feat(compiler-sfc): expose type import deps on compiled script block --- .../compileScript/resolveType.spec.ts | 100 ++++++++++-------- packages/compiler-sfc/src/compileScript.ts | 3 +- packages/compiler-sfc/src/script/context.ts | 2 +- .../compiler-sfc/src/script/resolveType.ts | 4 +- 4 files changed, 60 insertions(+), 49 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 6f07cfcfab4..a6aad1ea197 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -264,81 +264,85 @@ describe('resolveType', () => { }) describe('external type imports', () => { + const files = { + '/foo.ts': 'export type P = { foo: number }', + '/bar.d.ts': 'type X = { bar: string }; export { X as Y }' + } test('relative ts', () => { - expect( - resolve( - ` + const { props, deps } = resolve( + ` import { P } from './foo' import { Y as PP } from './bar' defineProps

() - `, - { - '/foo.ts': 'export type P = { foo: number }', - '/bar.d.ts': 'type X = { bar: string }; export { X as Y }' - } - ).props - ).toStrictEqual({ + `, + files + ) + expect(props).toStrictEqual({ foo: ['Number'], bar: ['String'] }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) test('relative vue', () => { - expect( - resolve( - ` + const files = { + '/foo.vue': + '', + '/bar.vue': + '' + } + const { props, deps } = resolve( + ` import { P } from './foo.vue' import { P as PP } from './bar.vue' defineProps

() - `, - { - '/foo.vue': - '', - '/bar.vue': - '' - } - ).props - ).toStrictEqual({ + `, + files + ) + expect(props).toStrictEqual({ foo: ['Number'], bar: ['String'] }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) test('relative (chained)', () => { - expect( - resolve( - ` + const files = { + '/foo.ts': `import type { P as PP } from './nested/bar.vue' + export type P = { foo: number } & PP`, + '/nested/bar.vue': + '' + } + const { props, deps } = resolve( + ` import { P } from './foo' defineProps

() - `, - { - '/foo.ts': `import type { P as PP } from './nested/bar.vue' - export type P = { foo: number } & PP`, - '/nested/bar.vue': - '' - } - ).props - ).toStrictEqual({ + `, + files + ) + expect(props).toStrictEqual({ foo: ['Number'], bar: ['String'] }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) test('relative (chained, re-export)', () => { - expect( - resolve( - ` + const files = { + '/foo.ts': `export { P as PP } from './bar'`, + '/bar.ts': 'export type P = { bar: string }' + } + const { props, deps } = resolve( + ` import { PP as P } from './foo' defineProps

() - `, - { - '/foo.ts': `export { P as PP } from './bar'`, - '/bar.ts': 'export type P = { bar: string }' - } - ).props - ).toStrictEqual({ + `, + files + ) + expect(props).toStrictEqual({ bar: ['String'] }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) test('ts module resolve', () => { @@ -357,7 +361,7 @@ describe('resolveType', () => { '/pp.ts': 'export type PP = { bar: string }' } - const { props } = resolve( + const { props, deps } = resolve( ` import { P } from 'foo' import { PP } from 'bar' @@ -370,6 +374,10 @@ describe('resolveType', () => { foo: ['Number'], bar: ['String'] }) + expect(deps && [...deps]).toStrictEqual([ + '/node_modules/foo/index.d.ts', + '/pp.ts' + ]) }) }) @@ -447,6 +455,6 @@ function resolve(code: string, files: Record = {}) { return { props, calls: raw.calls, - raw + deps: ctx.deps } } diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index c561a77a49d..e5e1bea4fd1 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -1035,7 +1035,8 @@ export function compileScript( }) as unknown as RawSourceMap) : undefined, scriptAst: scriptAst?.body, - scriptSetupAst: scriptSetupAst?.body + scriptSetupAst: scriptSetupAst?.body, + deps: ctx.deps ? [...ctx.deps] : undefined } } diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 1f96584507b..9141b95c572 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -65,7 +65,7 @@ export class ScriptCompileContext { /** * to be exposed on compiled script block for HMR cache busting */ - deps?: string[] + deps?: Set constructor( public descriptor: SFCDescriptor, diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index bbbbd4c5bc0..d5661c871aa 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -547,7 +547,9 @@ function resolveTypeFromImport( } if (resolved) { - // TODO (hmr) register dependency file on ctx + // (hmr) register dependency file on ctx + ;(ctx.deps || (ctx.deps = new Set())).add(resolved) + return resolveTypeReference( ctx, node, From 760755f4f83680bee13ad546cdab2e48ade38dff Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 15 Apr 2023 21:46:44 +0800 Subject: [PATCH 033/142] feat(compiler-sfc): support string/number indexed types in macros --- .../compileScript/resolveType.spec.ts | 35 +++++- .../compiler-sfc/src/script/resolveType.ts | 117 ++++++++++++------ 2 files changed, 111 insertions(+), 41 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index a6aad1ea197..7aa08d595f5 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -232,7 +232,7 @@ describe('resolveType', () => { }) }) - test('indexed access type', () => { + test('indexed access type (literal)', () => { expect( resolve(` type T = { bar: number } @@ -244,6 +244,37 @@ describe('resolveType', () => { }) }) + test('indexed access type (advanced)', () => { + expect( + resolve(` + type K = 'foo' | 'bar' + type T = { foo: string, bar: number } + type S = { foo: { foo: T[string] }, bar: { bar: string } } + defineProps() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'] + }) + }) + + test('indexed access type (number)', () => { + expect( + resolve(` + type A = (string | number)[] + type AA = Array + type T = [1, 'foo'] + type TT = [foo: 1, bar: 'foo'] + defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'], + tuple: ['Number', 'String'], + namedTuple: ['Number', 'String'] + }) + }) + test('namespace', () => { expect( resolve(` @@ -396,7 +427,7 @@ describe('resolveType', () => { test('unsupported index type', () => { expect(() => resolve(`defineProps()`)).toThrow( - `Unsupported index type` + `Unsupported type when resolving index type` ) }) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index d5661c871aa..f1fce65a287 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -7,6 +7,7 @@ import { TSEnumDeclaration, TSExpressionWithTypeArguments, TSFunctionType, + TSIndexedAccessType, TSInterfaceDeclaration, TSMappedType, TSMethodSignature, @@ -117,30 +118,11 @@ function innerResolveTypeElements( case 'TSMappedType': return resolveMappedType(ctx, node, scope) case 'TSIndexedAccessType': { - if ( - node.indexType.type === 'TSLiteralType' && - node.indexType.literal.type === 'StringLiteral' - ) { - const resolved = resolveTypeElements(ctx, node.objectType, scope) - const key = node.indexType.literal.value - const targetType = resolved.props[key].typeAnnotation - if (targetType) { - return resolveTypeElements( - ctx, - targetType.typeAnnotation, - resolved.props[key]._ownerScope - ) - } else { - break - } - } else { - // TODO support `number` and `string` index type when possible - ctx.error( - `Unsupported index type: ${node.indexType.type}`, - node.indexType, - scope - ) - } + const types = resolveIndexType(ctx, node, scope) + return mergeElements( + types.map(t => resolveTypeElements(ctx, t, t._ownerScope)), + 'TSUnionType' + ) } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { @@ -201,6 +183,7 @@ function mergeElements( maps: ResolvedElements[], type: 'TSUnionType' | 'TSIntersectionType' ): ResolvedElements { + if (maps.length === 1) return maps[0] const res: ResolvedElements = { props: {} } const { props: baseProps } = res for (const { props, calls } of maps) { @@ -282,6 +265,66 @@ function resolveMappedType( return res } +function resolveIndexType( + ctx: ScriptCompileContext, + node: TSIndexedAccessType, + scope: TypeScope +): (TSType & WithScope)[] { + if (node.indexType.type === 'TSNumberKeyword') { + return resolveArrayElementType(ctx, node.objectType, scope) + } + + const { indexType, objectType } = node + const types: TSType[] = [] + let keys: string[] + let resolved: ResolvedElements + if (indexType.type === 'TSStringKeyword') { + resolved = resolveTypeElements(ctx, objectType, scope) + keys = Object.keys(resolved.props) + } else { + keys = resolveStringType(ctx, indexType, scope) + resolved = resolveTypeElements(ctx, objectType, scope) + } + for (const key of keys) { + const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation + if (targetType) { + ;(targetType as TSType & WithScope)._ownerScope = + resolved.props[key]._ownerScope + types.push(targetType) + } + } + return types +} + +function resolveArrayElementType( + ctx: ScriptCompileContext, + node: Node, + scope: TypeScope +): TSType[] { + // type[] + if (node.type === 'TSArrayType') { + return [node.elementType] + } + // tuple + if (node.type === 'TSTupleType') { + return node.elementTypes.map(t => + t.type === 'TSNamedTupleMember' ? t.elementType : t + ) + } + if (node.type === 'TSTypeReference') { + // Array + if (getReferenceName(node) === 'Array' && node.typeParameters) { + return node.typeParameters.params + } else { + const resolved = resolveTypeReference(ctx, node, scope) + if (resolved) { + return resolveArrayElementType(ctx, resolved, scope) + } + } + } + ctx.error('Failed to resolve element type from target type', node) +} + function resolveStringType( ctx: ScriptCompileContext, node: Node, @@ -322,7 +365,7 @@ function resolveStringType( return getParam().map(s => s[0].toLowerCase() + s.slice(1)) default: ctx.error( - 'Unsupported type when resolving string type', + 'Unsupported type when resolving index type', node.typeName, scope ) @@ -330,7 +373,7 @@ function resolveStringType( } } } - ctx.error('Failed to resolve string type into finite keys', node, scope) + ctx.error('Failed to resolve index type into finite keys', node, scope) } function resolveTemplateKeys( @@ -991,19 +1034,12 @@ export function inferRuntimeType( return ['Symbol'] case 'TSIndexedAccessType': { - if ( - node.indexType.type === 'TSLiteralType' && - node.indexType.literal.type === 'StringLiteral' - ) { - try { - const resolved = resolveTypeElements(ctx, node.objectType, scope) - const key = node.indexType.literal.value - const prop = resolved.props[key] - return inferRuntimeType(ctx, prop, prop._ownerScope) - } catch (e) { - // avoid hard error, fallback to unknown - return [UNKNOWN_TYPE] - } + try { + const types = resolveIndexType(ctx, node, scope) + return flattenTypes(ctx, types, scope) + } catch (e) { + // avoid hard error, fallback to unknown + return [UNKNOWN_TYPE] } } @@ -1020,6 +1056,9 @@ function flattenTypes( types: TSType[], scope: TypeScope ): string[] { + if (types.length === 1) { + return inferRuntimeType(ctx, types[0], scope) + } return [ ...new Set( ([] as string[]).concat( From 6b13e04b4c83fcdbb180dc1d59f536a1309c2960 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 15 Apr 2023 17:15:50 +0800 Subject: [PATCH 034/142] feat(compiler-sfc): mark props destructure as experimental and require explicit opt-in --- .../definePropsDestructure.spec.ts.snap | 22 +++++++++---------- .../definePropsDestructure.spec.ts | 3 ++- packages/compiler-sfc/src/compileScript.ts | 22 ++++++++++++------- .../src/script/definePropsDestructure.ts | 8 +++++++ 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap index 37c334e838d..35926709cec 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`sfc props transform > aliasing 1`] = ` +exports[`sfc reactive props destructure > aliasing 1`] = ` "import { toDisplayString as _toDisplayString } from \\"vue\\" @@ -20,7 +20,7 @@ return (_ctx, _cache) => { }" `; -exports[`sfc props transform > basic usage 1`] = ` +exports[`sfc reactive props destructure > basic usage 1`] = ` "import { toDisplayString as _toDisplayString } from \\"vue\\" @@ -39,7 +39,7 @@ return (_ctx, _cache) => { }" `; -exports[`sfc props transform > computed static key 1`] = ` +exports[`sfc reactive props destructure > computed static key 1`] = ` "import { toDisplayString as _toDisplayString } from \\"vue\\" @@ -58,7 +58,7 @@ return (_ctx, _cache) => { }" `; -exports[`sfc props transform > default values w/ array runtime declaration 1`] = ` +exports[`sfc reactive props destructure > default values w/ array runtime declaration 1`] = ` "import { mergeDefaults as _mergeDefaults } from 'vue' export default { @@ -77,7 +77,7 @@ return () => {} }" `; -exports[`sfc props transform > default values w/ object runtime declaration 1`] = ` +exports[`sfc reactive props destructure > default values w/ object runtime declaration 1`] = ` "import { mergeDefaults as _mergeDefaults } from 'vue' export default { @@ -97,7 +97,7 @@ return () => {} }" `; -exports[`sfc props transform > default values w/ type declaration 1`] = ` +exports[`sfc reactive props destructure > default values w/ type declaration 1`] = ` "import { defineComponent as _defineComponent } from 'vue' export default /*#__PURE__*/_defineComponent({ @@ -116,7 +116,7 @@ return () => {} })" `; -exports[`sfc props transform > default values w/ type declaration, prod mode 1`] = ` +exports[`sfc reactive props destructure > default values w/ type declaration, prod mode 1`] = ` "import { defineComponent as _defineComponent } from 'vue' export default /*#__PURE__*/_defineComponent({ @@ -138,7 +138,7 @@ return () => {} })" `; -exports[`sfc props transform > multiple variable declarations 1`] = ` +exports[`sfc reactive props destructure > multiple variable declarations 1`] = ` "import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" @@ -156,7 +156,7 @@ return (_ctx, _cache) => { }" `; -exports[`sfc props transform > nested scope 1`] = ` +exports[`sfc reactive props destructure > nested scope 1`] = ` "export default { props: ['foo', 'bar'], setup(__props) { @@ -173,7 +173,7 @@ return () => {} }" `; -exports[`sfc props transform > non-identifier prop names 1`] = ` +exports[`sfc reactive props destructure > non-identifier prop names 1`] = ` "import { toDisplayString as _toDisplayString } from \\"vue\\" @@ -192,7 +192,7 @@ return (_ctx, _cache) => { }" `; -exports[`sfc props transform > rest spread 1`] = ` +exports[`sfc reactive props destructure > rest spread 1`] = ` "import { createPropsRestProxy as _createPropsRestProxy } from 'vue' export default { diff --git a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts index a459d80ff29..b8912092afd 100644 --- a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts @@ -2,10 +2,11 @@ import { BindingTypes } from '@vue/compiler-core' import { SFCScriptCompileOptions } from '../../src' import { compileSFCScript, assertCode } from '../utils' -describe('sfc props transform', () => { +describe('sfc reactive props destructure', () => { function compile(src: string, options?: Partial) { return compileSFCScript(src, { inlineTemplate: true, + propsDestructure: true, ...options }) } diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index e5e1bea4fd1..0d09f02538a 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -72,14 +72,6 @@ export interface SFCScriptCompileOptions { * https://babeljs.io/docs/en/babel-parser#plugins */ babelParserPlugins?: ParserPlugin[] - /** - * (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 /** * Compile the template and inline the resulting render function * directly inside setup(). @@ -108,8 +100,14 @@ export interface SFCScriptCompileOptions { hoistStatic?: boolean /** * (**Experimental**) Enable macro `defineModel` + * @default false */ defineModel?: boolean + /** + * (**Experimental**) Enable reactive destructure for `defineProps` + * @default false + */ + propsDestructure?: boolean /** * File system access methods to be used when resolving types * imported in SFC macros. Defaults to ts.sys in Node.js, can be overwritten @@ -119,6 +117,14 @@ 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 { diff --git a/packages/compiler-sfc/src/script/definePropsDestructure.ts b/packages/compiler-sfc/src/script/definePropsDestructure.ts index 220c7ce41db..9f693f8519d 100644 --- a/packages/compiler-sfc/src/script/definePropsDestructure.ts +++ b/packages/compiler-sfc/src/script/definePropsDestructure.ts @@ -26,6 +26,10 @@ export function processPropsDestructure( ctx: ScriptCompileContext, declId: ObjectPattern ) { + if (!ctx.options.propsDestructure) { + return + } + ctx.propsDestructureDecl = declId const registerBinding = ( @@ -91,6 +95,10 @@ export function transformDestructuredProps( ctx: ScriptCompileContext, vueImportAliases: Record ) { + if (!ctx.options.propsDestructure) { + return + } + const rootScope: Scope = {} const scopeStack: Scope[] = [rootScope] let currentScope: Scope = rootScope From f22e32e365bf6292cb606cb7289609e82da8b790 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 16 Apr 2023 11:11:26 +0800 Subject: [PATCH 035/142] feat(compiler-sfc): expose type resolve APIs --- packages/compiler-sfc/src/index.ts | 6 ++ packages/compiler-sfc/src/script/context.ts | 8 +- .../compiler-sfc/src/script/resolveType.ts | 102 ++++++++++++------ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index e171ac0885c..78a89c7b498 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -6,6 +6,7 @@ export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' +export { resolveTypeElements, inferRuntimeType } from './script/resolveType' export { shouldTransform as shouldTransformRef, transform as transformRef, @@ -52,6 +53,11 @@ export type { SFCStyleCompileResults } from './compileStyle' export type { SFCScriptCompileOptions } from './compileScript' +export type { ScriptCompileContext } from './script/context' +export type { + TypeResolveContext, + SimpleTypeResolveContext +} from './script/resolveType' export type { AssetURLOptions, AssetURLTagConfig diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 9141b95c572..641e463741f 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -16,12 +16,14 @@ export class ScriptCompileContext { scriptAst: Program | null scriptSetupAst: Program | null - s = new MagicString(this.descriptor.source) + source = this.descriptor.source + filename = this.descriptor.filename + s = new MagicString(this.source) startOffset = this.descriptor.scriptSetup?.loc.start.offset endOffset = this.descriptor.scriptSetup?.loc.end.offset // import / type analysis - scope: TypeScope | undefined + scope?: TypeScope userImports: Record = Object.create(null) // macros presence check @@ -69,7 +71,7 @@ export class ScriptCompileContext { constructor( public descriptor: SFCDescriptor, - public options: SFCScriptCompileOptions + public options: Partial ) { const { script, scriptSetup } = descriptor const scriptLang = script && script.lang diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f1fce65a287..1e89c5712dc 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -36,6 +36,32 @@ import { createCache } from '../cache' import type TS from 'typescript' import { join, extname, dirname } from 'path' +/** + * TypeResolveContext is compatible with ScriptCompileContext + * but also allows a simpler version of it with minimal required properties + * when resolveType needs to be used in a non-SFC context, e.g. in a babel + * plugin. The simplest context can be just: + * ```ts + * const ctx: SimpleTypeResolveContext = { + * filename: '...', + * source: '...', + * options: {}, + * error() {}, + * ast: [] + * } + * ``` + */ +export type SimpleTypeResolveContext = Pick< + ScriptCompileContext, + // required + 'source' | 'filename' | 'error' | 'options' +> & + Partial> & { + ast: Statement[] + } + +export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext + type Import = Pick export interface TypeScope { @@ -79,7 +105,7 @@ interface ResolvedElements { * mapped to runtime props or emits. */ export function resolveTypeElements( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node & WithScope & { _resolvedElements?: ResolvedElements }, scope?: TypeScope ): ResolvedElements { @@ -94,7 +120,7 @@ export function resolveTypeElements( } function innerResolveTypeElements( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node, scope: TypeScope ): ResolvedElements { @@ -138,7 +164,7 @@ function innerResolveTypeElements( ) { return resolveBuiltin(ctx, node, typeName as any, scope) } - ctx.error( + return ctx.error( `Unresolvable type reference or unsupported built-in utlility type`, node, scope @@ -146,11 +172,11 @@ function innerResolveTypeElements( } } } - ctx.error(`Unresolvable type: ${node.type}`, node, scope) + return ctx.error(`Unresolvable type: ${node.type}`, node, scope) } function typeElementsToMap( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, elements: TSTypeElement[], scope = ctxToScope(ctx) ): ResolvedElements { @@ -227,7 +253,7 @@ function createProperty( } function resolveInterfaceMembers( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TSInterfaceDeclaration & WithScope, scope: TypeScope ): ResolvedElements { @@ -246,7 +272,7 @@ function resolveInterfaceMembers( } function resolveMappedType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TSMappedType, scope: TypeScope ): ResolvedElements { @@ -266,7 +292,7 @@ function resolveMappedType( } function resolveIndexType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TSIndexedAccessType, scope: TypeScope ): (TSType & WithScope)[] { @@ -297,7 +323,7 @@ function resolveIndexType( } function resolveArrayElementType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node, scope: TypeScope ): TSType[] { @@ -322,11 +348,15 @@ function resolveArrayElementType( } } } - ctx.error('Failed to resolve element type from target type', node) + return ctx.error( + 'Failed to resolve element type from target type', + node, + scope + ) } function resolveStringType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node, scope: TypeScope ): string[] { @@ -373,11 +403,11 @@ function resolveStringType( } } } - ctx.error('Failed to resolve index type into finite keys', node, scope) + return ctx.error('Failed to resolve index type into finite keys', node, scope) } function resolveTemplateKeys( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TemplateLiteral, scope: TypeScope ): string[] { @@ -420,7 +450,7 @@ const SupportedBuiltinsSet = new Set([ type GetSetType = T extends Set ? V : never function resolveBuiltin( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TSTypeReference | TSExpressionWithTypeArguments, name: GetSetType, scope: TypeScope @@ -460,7 +490,7 @@ function resolveBuiltin( } function resolveTypeReference( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: (TSTypeReference | TSExpressionWithTypeArguments) & { _resolvedReference?: Node }, @@ -481,7 +511,7 @@ function resolveTypeReference( } function innerResolveTypeReference( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, scope: TypeScope, name: string | string[], node: TSTypeReference | TSExpressionWithTypeArguments, @@ -536,6 +566,9 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { let ts: typeof TS +/** + * @private + */ export function registerTS(_ts: any) { ts = _ts } @@ -543,7 +576,7 @@ export function registerTS(_ts: any) { type FS = NonNullable function resolveTypeFromImport( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: TSTypeReference | TSExpressionWithTypeArguments, name: string, scope: TypeScope @@ -685,13 +718,16 @@ function resolveWithTS( const fileToScopeCache = createCache() +/** + * @private + */ export function invalidateTypeCache(filename: string) { fileToScopeCache.delete(filename) tsConfigCache.delete(filename) } function fileToScope( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, filename: string, fs: FS ): TypeScope { @@ -717,7 +753,7 @@ function fileToScope( } function parseFile( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, filename: string, content: string ): Statement[] { @@ -763,24 +799,30 @@ function parseFile( return [] } -function ctxToScope(ctx: ScriptCompileContext): TypeScope { +function ctxToScope(ctx: TypeResolveContext): TypeScope { if (ctx.scope) { return ctx.scope } + const body = + 'ast' in ctx + ? ctx.ast + : ctx.scriptAst + ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] + : ctx.scriptSetupAst!.body + const scope: TypeScope = { - filename: ctx.descriptor.filename, - source: ctx.descriptor.source, - offset: ctx.startOffset!, - imports: Object.create(ctx.userImports), + filename: ctx.filename, + source: ctx.source, + offset: 'startOffset' in ctx ? ctx.startOffset! : 0, + imports: + 'userImports' in ctx + ? Object.create(ctx.userImports) + : recordImports(body), types: Object.create(null), exportedTypes: Object.create(null) } - const body = ctx.scriptAst - ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] - : ctx.scriptSetupAst!.body - recordTypes(body, scope) return (ctx.scope = scope) @@ -894,7 +936,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) { } export function inferRuntimeType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node & WithScope, scope = node._ownerScope || ctxToScope(ctx) ): string[] { @@ -1052,7 +1094,7 @@ export function inferRuntimeType( } function flattenTypes( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, types: TSType[], scope: TypeScope ): string[] { From 4e028b966991937c83fb2529973fd3d41080bb61 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 16 Apr 2023 15:49:41 +0800 Subject: [PATCH 036/142] feat(compiler-sfc): support specifying global types for sfc macros ref: https://github.com/vuejs/core/pull/8083#issuecomment-1508468713 --- .../compileScript/resolveType.spec.ts | 37 +++- packages/compiler-sfc/src/compileScript.ts | 5 + packages/compiler-sfc/src/script/context.ts | 14 +- .../compiler-sfc/src/script/resolveType.ts | 172 +++++++++++------- 4 files changed, 149 insertions(+), 79 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 7aa08d595f5..0f2a47d6b99 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1,5 +1,5 @@ import { Identifier } from '@babel/types' -import { parse } from '../../src' +import { SFCScriptCompileOptions, parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' import { inferRuntimeType, @@ -410,6 +410,32 @@ describe('resolveType', () => { '/pp.ts' ]) }) + + test('global types', () => { + const files = { + // ambient + '/app.d.ts': + 'declare namespace App { interface User { name: string } }', + // module - should only respect the declare global block + '/global.d.ts': ` + declare type PP = { bar: number } + declare global { + type PP = { bar: string } + } + export {} + ` + } + + const { props, deps } = resolve(`defineProps()`, files, { + globalTypeFiles: Object.keys(files) + }) + + expect(props).toStrictEqual({ + name: ['String'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) }) describe('errors', () => { @@ -444,7 +470,11 @@ describe('resolveType', () => { }) }) -function resolve(code: string, files: Record = {}) { +function resolve( + code: string, + files: Record = {}, + options?: Partial +) { const { descriptor } = parse(``, { filename: '/Test.vue' }) @@ -457,7 +487,8 @@ function resolve(code: string, files: Record = {}) { readFile(file) { return files[file] } - } + }, + ...options }) for (const file in files) { diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 0d09f02538a..1f525005c4d 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -72,6 +72,11 @@ export interface SFCScriptCompileOptions { * https://babeljs.io/docs/en/babel-parser#plugins */ babelParserPlugins?: ParserPlugin[] + /** + * A list of files to parse for global types to be made available for type + * resolving in SFC macros. The list must be fully resolved file system paths. + */ + globalTypeFiles?: string[] /** * Compile the template and inline the resulting render function * directly inside setup(). diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 641e463741f..d2c5dabd194 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -24,6 +24,7 @@ export class ScriptCompileContext { // import / type analysis scope?: TypeScope + globalScopes?: TypeScope[] userImports: Record = Object.create(null) // macros presence check @@ -101,7 +102,7 @@ export class ScriptCompileContext { sourceType: 'module' }).program } catch (e: any) { - e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ + e.message = `[vue/compiler-sfc] ${e.message}\n\n${ descriptor.filename }\n${generateCodeFrame( descriptor.source, @@ -113,15 +114,12 @@ export class ScriptCompileContext { } this.scriptAst = - this.descriptor.script && - parse( - this.descriptor.script.content, - this.descriptor.script.loc.start.offset - ) + descriptor.script && + parse(descriptor.script.content, descriptor.script.loc.start.offset) this.scriptSetupAst = - this.descriptor.scriptSetup && - parse(this.descriptor.scriptSetup!.content, this.startOffset!) + descriptor.scriptSetup && + parse(descriptor.scriptSetup!.content, this.startOffset!) } getString(node: Node, scriptSetup = true): string { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 1e89c5712dc..8efa04579f8 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -56,7 +56,7 @@ export type SimpleTypeResolveContext = Pick< // required 'source' | 'filename' | 'error' | 'options' > & - Partial> & { + Partial> & { ast: Statement[] } @@ -64,25 +64,18 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext type Import = Pick +type ScopeTypeNode = Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope +} + export interface TypeScope { filename: string source: string offset: number imports: Record - types: Record< - string, - Node & { - // scope types always has ownerScope attached - _ownerScope: TypeScope - } - > - exportedTypes: Record< - string, - Node & { - // scope types always has ownerScope attached - _ownerScope: TypeScope - } - > + types: Record + exportedTypes: Record } export interface WithScope { @@ -492,12 +485,12 @@ function resolveBuiltin( function resolveTypeReference( ctx: TypeResolveContext, node: (TSTypeReference | TSExpressionWithTypeArguments) & { - _resolvedReference?: Node + _resolvedReference?: ScopeTypeNode }, scope?: TypeScope, name?: string, onlyExported = false -): (Node & WithScope) | undefined { +): ScopeTypeNode | undefined { if (node._resolvedReference) { return node._resolvedReference } @@ -516,13 +509,26 @@ function innerResolveTypeReference( name: string | string[], node: TSTypeReference | TSExpressionWithTypeArguments, onlyExported: boolean -): Node | undefined { +): ScopeTypeNode | undefined { if (typeof name === 'string') { if (scope.imports[name]) { return resolveTypeFromImport(ctx, node, name, scope) } else { const types = onlyExported ? scope.exportedTypes : scope.types - return types[name] + if (types[name]) { + return types[name] + } else { + // fallback to global + const globalScopes = resolveGlobalScope(ctx) + if (globalScopes) { + for (const s of globalScopes) { + if (s.types[name]) { + ;(ctx.deps || (ctx.deps = new Set())).add(s.filename) + return s.types[name] + } + } + } + } } } else { const ns = innerResolveTypeReference( @@ -539,7 +545,7 @@ function innerResolveTypeReference( childScope, name.length > 2 ? name.slice(1) : name[name.length - 1], node, - true + !ns.declare ) } } @@ -564,6 +570,19 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } +function resolveGlobalScope(ctx: TypeResolveContext): TypeScope[] | undefined { + if (ctx.options.globalTypeFiles) { + const fs: FS = ctx.options.fs || ts?.sys + if (!fs) { + throw new Error('[vue/compiler-sfc] globalTypeFiles requires fs access.') + } + return ctx.options.globalTypeFiles.map(file => + // TODO: differentiate ambient vs non-ambient module + fileToScope(file, fs, ctx.options.babelParserPlugins, true) + ) + } +} + let ts: typeof TS /** @@ -580,7 +599,7 @@ function resolveTypeFromImport( node: TSTypeReference | TSExpressionWithTypeArguments, name: string, scope: TypeScope -): Node | undefined { +): ScopeTypeNode | undefined { const fs: FS = ctx.options.fs || ts?.sys if (!fs) { ctx.error( @@ -629,7 +648,7 @@ function resolveTypeFromImport( return resolveTypeReference( ctx, node, - fileToScope(ctx, resolved, fs), + fileToScope(resolved, fs, ctx.options.babelParserPlugins), imported, true ) @@ -726,10 +745,11 @@ export function invalidateTypeCache(filename: string) { tsConfigCache.delete(filename) } -function fileToScope( - ctx: TypeResolveContext, +export function fileToScope( filename: string, - fs: FS + fs: FS, + parserPlugins: SFCScriptCompileOptions['babelParserPlugins'], + asGlobal = false ): TypeScope { const cached = fileToScopeCache.get(filename) if (cached) { @@ -737,33 +757,30 @@ function fileToScope( } const source = fs.readFile(filename) || '' - const body = parseFile(ctx, filename, source) + const body = parseFile(filename, source, parserPlugins) const scope: TypeScope = { filename, source, offset: 0, + imports: recordImports(body), types: Object.create(null), - exportedTypes: Object.create(null), - imports: recordImports(body) + exportedTypes: Object.create(null) } - recordTypes(body, scope) + recordTypes(body, scope, asGlobal) fileToScopeCache.set(filename, scope) return scope } function parseFile( - ctx: TypeResolveContext, filename: string, - content: string + content: string, + parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'] ): Statement[] { const ext = extname(filename) if (ext === '.ts' || ext === '.tsx') { return babelParse(content, { - plugins: resolveParserPlugins( - ext.slice(1), - ctx.options.babelParserPlugins - ), + plugins: resolveParserPlugins(ext.slice(1), parserPlugins), sourceType: 'module' }).program.body } else if (ext === '.vue') { @@ -792,7 +809,7 @@ function parseFile( } const lang = script?.lang || scriptSetup?.lang return babelParse(scriptContent, { - plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), + plugins: resolveParserPlugins(lang!, parserPlugins), sourceType: 'module' }).program.body } @@ -830,52 +847,71 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope { function moduleDeclToScope( node: TSModuleDeclaration & { _resolvedChildScope?: TypeScope }, - parent: TypeScope + parentScope: TypeScope ): TypeScope { if (node._resolvedChildScope) { return node._resolvedChildScope } const scope: TypeScope = { - ...parent, - types: Object.create(parent.types), - imports: Object.create(parent.imports) + ...parentScope, + types: Object.create(parentScope.types), + imports: Object.create(parentScope.imports) } recordTypes((node.body as TSModuleBlock).body, scope) return (node._resolvedChildScope = scope) } -function recordTypes(body: Statement[], scope: TypeScope) { +const importExportRE = /^Import|^Export/ + +function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) { const { types, exportedTypes, imports } = scope + const isAmbient = asGlobal + ? !body.some(s => importExportRE.test(s.type)) + : false for (const stmt of body) { - recordType(stmt, types) + if (asGlobal) { + if (isAmbient) { + if ((stmt as any).declare) { + recordType(stmt, types) + } + } else if (stmt.type === 'TSModuleDeclaration' && stmt.global) { + for (const s of (stmt.body as TSModuleBlock).body) { + recordType(s, types) + } + } + } else { + recordType(stmt, types) + } } - for (const stmt of body) { - if (stmt.type === 'ExportNamedDeclaration') { - if (stmt.declaration) { - recordType(stmt.declaration, types) - recordType(stmt.declaration, exportedTypes) - } else { - for (const spec of stmt.specifiers) { - if (spec.type === 'ExportSpecifier') { - const local = spec.local.name - const exported = getId(spec.exported) - if (stmt.source) { - // re-export, register an import + export as a type reference - imports[local] = { - source: stmt.source.value, - imported: local - } - exportedTypes[exported] = { - type: 'TSTypeReference', - typeName: { - type: 'Identifier', - name: local - }, - _ownerScope: scope + if (!asGlobal) { + for (const stmt of body) { + if (stmt.type === 'ExportNamedDeclaration') { + if (stmt.declaration) { + recordType(stmt.declaration, types) + recordType(stmt.declaration, exportedTypes) + } else { + for (const spec of stmt.specifiers) { + if (spec.type === 'ExportSpecifier') { + const local = spec.local.name + const exported = getId(spec.exported) + if (stmt.source) { + // re-export, register an import + export as a type reference + imports[local] = { + source: stmt.source.value, + imported: local + } + exportedTypes[exported] = { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: local + }, + _ownerScope: scope + } + } else if (types[local]) { + // exporting local defined type + exportedTypes[exported] = types[local] } - } else if (types[local]) { - // exporting local defined type - exportedTypes[exported] = types[local] } } } From 4b5b384485cf8f6124f6738b89e3d047358f3a11 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 17 Apr 2023 18:05:17 +0800 Subject: [PATCH 037/142] fix(hmr): invalidate cached props/emits options on hmr --- packages/runtime-core/src/hmr.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index c5039f62b6f..fe8ca132bc8 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -123,6 +123,8 @@ function reload(id: string, newComp: HMRComponent) { } // 3. invalidate options resolution cache + instance.appContext.propsCache.delete(instance.type as any) + instance.appContext.emitsCache.delete(instance.type as any) instance.appContext.optionsCache.delete(instance.type as any) // 4. actually update From 33adc2a17ae450069470385eb1c62cabfc10a780 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 17 Apr 2023 18:15:17 +0800 Subject: [PATCH 038/142] release: v3.3.0-alpha.10 --- CHANGELOG.md | 31 +++++++++++++ package.json | 2 +- packages/compiler-core/package.json | 4 +- packages/compiler-dom/package.json | 6 +-- packages/compiler-sfc/package.json | 12 ++--- packages/compiler-ssr/package.json | 6 +-- packages/dts-test/package.json | 2 +- packages/reactivity-transform/package.json | 6 +-- packages/reactivity/package.json | 4 +- packages/runtime-core/package.json | 6 +-- packages/runtime-dom/package.json | 6 +-- packages/runtime-test/package.json | 6 +-- packages/server-renderer/package.json | 8 ++-- packages/sfc-playground/package.json | 2 +- packages/shared/package.json | 2 +- packages/size-check/package.json | 2 +- packages/template-explorer/package.json | 2 +- packages/vue-compat/package.json | 4 +- packages/vue/package.json | 12 ++--- pnpm-lock.yaml | 52 +++++++++++----------- 20 files changed, 103 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59681946234..fe7596228cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# [3.3.0-alpha.10](https://github.com/vuejs/core/compare/v3.3.0-alpha.9...v3.3.0-alpha.10) (2023-04-17) + + +### Bug Fixes + +* **hmr:** invalidate cached props/emits options on hmr ([4b5b384](https://github.com/vuejs/core/commit/4b5b384485cf8f6124f6738b89e3d047358f3a11)) +* **runtime-core:** properly merge props and emits options from mixins ([#8052](https://github.com/vuejs/core/issues/8052)) ([c94ef02](https://github.com/vuejs/core/commit/c94ef02421d7422bc59d10cf2eee9f4e7dcea6c8)), closes [#7989](https://github.com/vuejs/core/issues/7989) + + +### Features + +* **compiler-sfc:** expose type import deps on compiled script block ([8d8ddd6](https://github.com/vuejs/core/commit/8d8ddd686c832b2ea29b87ef47666b13c4ad5d4c)) +* **compiler-sfc:** expose type resolve APIs ([f22e32e](https://github.com/vuejs/core/commit/f22e32e365bf6292cb606cb7289609e82da8b790)) +* **compiler-sfc:** mark props destructure as experimental and require explicit opt-in ([6b13e04](https://github.com/vuejs/core/commit/6b13e04b4c83fcdbb180dc1d59f536a1309c2960)) +* **compiler-sfc:** support intersection and union types in macros ([d1f973b](https://github.com/vuejs/core/commit/d1f973bff82581fb335d6fc05623d1ad3d84fb7c)), closes [#7553](https://github.com/vuejs/core/issues/7553) +* **compiler-sfc:** support limited built-in utility types in macros ([1cfab4c](https://github.com/vuejs/core/commit/1cfab4c695b0c28f549f8c97faee5099581792a7)) +* **compiler-sfc:** support mapped types, string types & template type in macros ([fb8ecc8](https://github.com/vuejs/core/commit/fb8ecc803e58bfef0971346c63fefc529812daa7)) +* **compiler-sfc:** support namespace members type in macros ([5ff40bb](https://github.com/vuejs/core/commit/5ff40bb0dc2918b7db15fe9f49db2a135a925572)) +* **compiler-sfc:** support relative imported types in macros ([8aa4ea8](https://github.com/vuejs/core/commit/8aa4ea81d6e4d3110aa1619cca594543da4c9b63)) +* **compiler-sfc:** support resolving type imports from modules ([3982bef](https://github.com/vuejs/core/commit/3982bef533b451d1b59fa243560184a13fe8c18c)) +* **compiler-sfc:** support specifying global types for sfc macros ([4e028b9](https://github.com/vuejs/core/commit/4e028b966991937c83fb2529973fd3d41080bb61)), closes [/github.com/vuejs/core/pull/8083#issuecomment-1508468713](https://github.com//github.com/vuejs/core/pull/8083/issues/issuecomment-1508468713) +* **compiler-sfc:** support string indexed type in macros ([3f779dd](https://github.com/vuejs/core/commit/3f779ddbf85054c8915fa4537f8a79baab392d5c)) +* **compiler-sfc:** support string/number indexed types in macros ([760755f](https://github.com/vuejs/core/commit/760755f4f83680bee13ad546cdab2e48ade38dff)) + + +### Performance Improvements + +* **compiler:** use source-map-js ([19e17a9](https://github.com/vuejs/core/commit/19e17a951c3387cbd6a1597e6cd9048a4aad4528)) + + + # [3.3.0-alpha.9](https://github.com/vuejs/core/compare/v3.3.0-alpha.8...v3.3.0-alpha.9) (2023-04-08) diff --git a/package.json b/package.json index 35d89e177a4..4e28fef6f77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "packageManager": "pnpm@7.26.0", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 5b4b0da895c..4bda8ec8828 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -33,7 +33,7 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.0-alpha.9", + "@vue/shared": "3.3.0-alpha.10", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index a73978cec7e..b07b66e5a9f 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/compiler-core": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-core": "3.3.0-alpha.10" } } diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index d43f0c6355f..70d0a020451 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -33,11 +33,11 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.9", - "@vue/compiler-dom": "3.3.0-alpha.9", - "@vue/compiler-ssr": "3.3.0-alpha.9", - "@vue/reactivity-transform": "3.3.0-alpha.9", - "@vue/shared": "3.3.0-alpha.9", + "@vue/compiler-core": "3.3.0-alpha.10", + "@vue/compiler-dom": "3.3.0-alpha.10", + "@vue/compiler-ssr": "3.3.0-alpha.10", + "@vue/reactivity-transform": "3.3.0-alpha.10", + "@vue/shared": "3.3.0-alpha.10", "estree-walker": "^2.0.2", "magic-string": "^0.30.0", "postcss": "^8.1.10", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index b1809422b1d..928ac6969eb 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/compiler-dom": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-dom": "3.3.0-alpha.10" } } diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index 833b92e5008..6717f6d3806 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -4,5 +4,5 @@ "dependencies": { "vue": "workspace:*" }, - "version": "3.3.0-alpha.9" + "version": "3.3.0-alpha.10" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index 5045a40a818..000a9b2ddab 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -29,8 +29,8 @@ "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.9", - "@vue/shared": "3.3.0-alpha.9", + "@vue/compiler-core": "3.3.0-alpha.10", + "@vue/shared": "3.3.0-alpha.10", "estree-walker": "^2.0.2", "magic-string": "^0.30.0" }, diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 4c7988e82a6..71577a27848 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10" } } diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 606fef5414a..c9facdc653a 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/reactivity": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/reactivity": "3.3.0-alpha.10" } } diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 25da3f9657a..223ccb4bae2 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", @@ -35,8 +35,8 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/runtime-core": "3.3.0-alpha.9", + "@vue/shared": "3.3.0-alpha.10", + "@vue/runtime-core": "3.3.0-alpha.10", "csstype": "^3.1.1" } } diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json index 202161c9474..67718fe4c4c 100644 --- a/packages/runtime-test/package.json +++ b/packages/runtime-test/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-test", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/runtime-test", "private": true, "main": "index.js", @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/runtime-core": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/runtime-core": "3.3.0-alpha.10" } } diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index 9f8a2f8eafd..676c0eb46ed 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", @@ -32,10 +32,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "peerDependencies": { - "vue": "3.3.0-alpha.9" + "vue": "3.3.0-alpha.10" }, "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/compiler-ssr": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-ssr": "3.3.0-alpha.10" } } diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json index 848e3ee97b8..e067028e040 100644 --- a/packages/sfc-playground/package.json +++ b/packages/sfc-playground/package.json @@ -1,6 +1,6 @@ { "name": "@vue/sfc-playground", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "private": true, "scripts": { "dev": "vite", diff --git a/packages/shared/package.json b/packages/shared/package.json index a7ceb93b4ae..d3c5665d3cf 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/size-check/package.json b/packages/size-check/package.json index 34c7df41c92..96c8b1f2969 100644 --- a/packages/size-check/package.json +++ b/packages/size-check/package.json @@ -1,6 +1,6 @@ { "name": "@vue/size-check", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "private": true, "scripts": { "build": "vite build" diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index b75666ae9dd..c8a4fd03ebe 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "private": true, "buildOptions": { "formats": [ diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index ce29f6a5752..047b5d0f1c9 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -43,6 +43,6 @@ "source-map-js": "^1.0.2" }, "peerDependencies": { - "vue": "3.3.0-alpha.9" + "vue": "3.3.0-alpha.10" } } diff --git a/packages/vue/package.json b/packages/vue/package.json index 5a2e32f0e5a..fe1da15c0cc 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.3.0-alpha.9", + "version": "3.3.0-alpha.10", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -81,10 +81,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.9", - "@vue/compiler-dom": "3.3.0-alpha.9", - "@vue/runtime-dom": "3.3.0-alpha.9", - "@vue/compiler-sfc": "3.3.0-alpha.9", - "@vue/server-renderer": "3.3.0-alpha.9" + "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-dom": "3.3.0-alpha.10", + "@vue/runtime-dom": "3.3.0-alpha.10", + "@vue/compiler-sfc": "3.3.0-alpha.10", + "@vue/server-renderer": "3.3.0-alpha.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b32007ae6ce..2c63903452d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: specifiers: '@babel/parser': ^7.21.3 '@babel/types': ^7.21.3 - '@vue/shared': 3.3.0-alpha.9 + '@vue/shared': 3.3.0-alpha.10 estree-walker: ^2.0.2 source-map-js: ^1.0.2 dependencies: @@ -113,8 +113,8 @@ importers: packages/compiler-dom: specifiers: - '@vue/compiler-core': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/compiler-core': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/compiler-core': link:../compiler-core '@vue/shared': link:../shared @@ -125,12 +125,12 @@ importers: '@babel/types': ^7.21.3 '@types/estree': ^0.0.48 '@types/lru-cache': ^5.1.0 - '@vue/compiler-core': 3.3.0-alpha.9 - '@vue/compiler-dom': 3.3.0-alpha.9 - '@vue/compiler-ssr': 3.3.0-alpha.9 + '@vue/compiler-core': 3.3.0-alpha.10 + '@vue/compiler-dom': 3.3.0-alpha.10 + '@vue/compiler-ssr': 3.3.0-alpha.10 '@vue/consolidate': ^0.17.3 - '@vue/reactivity-transform': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/reactivity-transform': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 estree-walker: ^2.0.2 hash-sum: ^2.0.0 lru-cache: ^5.1.1 @@ -168,8 +168,8 @@ importers: packages/compiler-ssr: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/compiler-dom': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/shared': link:../shared @@ -182,7 +182,7 @@ importers: packages/reactivity: specifiers: - '@vue/shared': 3.3.0-alpha.9 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/shared': link:../shared @@ -191,8 +191,8 @@ importers: '@babel/core': ^7.21.3 '@babel/parser': ^7.20.15 '@babel/types': ^7.21.3 - '@vue/compiler-core': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/compiler-core': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 estree-walker: ^2.0.2 magic-string: ^0.30.0 dependencies: @@ -207,16 +207,16 @@ importers: packages/runtime-core: specifiers: - '@vue/reactivity': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/reactivity': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/reactivity': link:../reactivity '@vue/shared': link:../shared packages/runtime-dom: specifiers: - '@vue/runtime-core': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/runtime-core': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 csstype: ^3.1.1 dependencies: '@vue/runtime-core': link:../runtime-core @@ -225,16 +225,16 @@ importers: packages/runtime-test: specifiers: - '@vue/runtime-core': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/runtime-core': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/runtime-core': link:../runtime-core '@vue/shared': link:../shared packages/server-renderer: specifiers: - '@vue/compiler-ssr': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/compiler-ssr': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/compiler-ssr': link:../compiler-ssr '@vue/shared': link:../shared @@ -275,11 +275,11 @@ importers: packages/vue: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.9 - '@vue/compiler-sfc': 3.3.0-alpha.9 - '@vue/runtime-dom': 3.3.0-alpha.9 - '@vue/server-renderer': 3.3.0-alpha.9 - '@vue/shared': 3.3.0-alpha.9 + '@vue/compiler-dom': 3.3.0-alpha.10 + '@vue/compiler-sfc': 3.3.0-alpha.10 + '@vue/runtime-dom': 3.3.0-alpha.10 + '@vue/server-renderer': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.10 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/compiler-sfc': link:../compiler-sfc From 271df09470c61d073185ba6cf3cf50358713c500 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 17 Apr 2023 20:59:03 +0800 Subject: [PATCH 039/142] fix(compiler-sfc): normalize windows paths when resolving types --- .../compiler-sfc/src/script/resolveType.ts | 21 +++++++++++-------- packages/compiler-sfc/src/script/utils.ts | 8 +++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 8efa04579f8..247bac3a3be 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -25,7 +25,8 @@ import { UNKNOWN_TYPE, createGetCanonicalFileName, getId, - getImportedName + getImportedName, + normalizePath } from './utils' import { ScriptCompileContext, resolveParserPlugins } from './context' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' @@ -34,7 +35,7 @@ import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' import { createCache } from '../cache' import type TS from 'typescript' -import { join, extname, dirname } from 'path' +import path from 'path' /** * TypeResolveContext is compatible with ScriptCompileContext @@ -577,8 +578,7 @@ function resolveGlobalScope(ctx: TypeResolveContext): TypeScope[] | undefined { throw new Error('[vue/compiler-sfc] globalTypeFiles requires fs access.') } return ctx.options.globalTypeFiles.map(file => - // TODO: differentiate ambient vs non-ambient module - fileToScope(file, fs, ctx.options.babelParserPlugins, true) + fileToScope(normalizePath(file), fs, ctx.options.babelParserPlugins, true) ) } } @@ -616,7 +616,7 @@ function resolveTypeFromImport( if (source.startsWith('.')) { // relative import - fast path - const filename = join(containingFile, '..', source) + const filename = path.join(containingFile, '..', source) resolved = resolveExt(filename, fs) } else { // module or aliased import - use full TS resolution, only supported in Node @@ -642,6 +642,8 @@ function resolveTypeFromImport( } if (resolved) { + resolved = normalizePath(resolved) + // (hmr) register dependency file on ctx ;(ctx.deps || (ctx.deps = new Set())).add(resolved) @@ -694,7 +696,8 @@ function resolveWithTS( let options: TS.CompilerOptions let cache: TS.ModuleResolutionCache | undefined if (configPath) { - const cached = tsConfigCache.get(configPath) + const normalizedConfigPath = normalizePath(configPath) + const cached = tsConfigCache.get(normalizedConfigPath) if (!cached) { // The only case where `fs` is NOT `ts.sys` is during tests. // parse config host requires an extra `readDirectory` method @@ -709,7 +712,7 @@ function resolveWithTS( const parsed = ts.parseJsonConfigFileContent( ts.readConfigFile(configPath, fs.readFile).config, parseConfigHost, - dirname(configPath), + path.dirname(configPath), undefined, configPath ) @@ -719,7 +722,7 @@ function resolveWithTS( createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames), options ) - tsConfigCache.set(configPath, { options, cache }) + tsConfigCache.set(normalizedConfigPath, { options, cache }) } else { ;({ options, cache } = cached) } @@ -777,7 +780,7 @@ function parseFile( content: string, parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'] ): Statement[] { - const ext = extname(filename) + const ext = path.extname(filename) if (ext === '.ts' || ext === '.tsx') { return babelParse(content, { plugins: resolveParserPlugins(ext.slice(1), parserPlugins), diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 6d874f8a6db..04df22a2b64 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -8,6 +8,7 @@ import { Node, StringLiteral } from '@babel/types' +import path from 'path' import { TS_NODE_TYPES } from '@vue/compiler-dom' export const UNKNOWN_TYPE = 'Unknown' @@ -97,3 +98,10 @@ function toFileNameLowerCase(x: string) { export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean) { return useCaseSensitiveFileNames ? identity : toFileNameLowerCase } + +const windowsSlashRE = /\\/g +export function normalizePath(p: string) { + // in the browser build, the polyfill doesn't expose posix, but defualts to + // posix behavior. + return (path.posix || path).normalize(p.replace(windowsSlashRE, '/')) +} From 57f0fbe76ae6454e10817771800e80d98f683e55 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 17 Apr 2023 21:17:17 +0800 Subject: [PATCH 040/142] release: v3.3.0-alpha.11 --- CHANGELOG.md | 9 ++++ package.json | 2 +- packages/compiler-core/package.json | 4 +- packages/compiler-dom/package.json | 6 +-- packages/compiler-sfc/package.json | 12 ++--- packages/compiler-ssr/package.json | 6 +-- packages/dts-test/package.json | 2 +- packages/reactivity-transform/package.json | 6 +-- packages/reactivity/package.json | 4 +- packages/runtime-core/package.json | 6 +-- packages/runtime-dom/package.json | 6 +-- packages/runtime-test/package.json | 6 +-- packages/server-renderer/package.json | 8 ++-- packages/sfc-playground/package.json | 2 +- packages/shared/package.json | 2 +- packages/size-check/package.json | 2 +- packages/template-explorer/package.json | 2 +- packages/vue-compat/package.json | 4 +- packages/vue/package.json | 12 ++--- pnpm-lock.yaml | 52 +++++++++++----------- 20 files changed, 81 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe7596228cb..69af50d2351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [3.3.0-alpha.11](https://github.com/vuejs/core/compare/v3.3.0-alpha.10...v3.3.0-alpha.11) (2023-04-17) + + +### Bug Fixes + +* **compiler-sfc:** normalize windows paths when resolving types ([271df09](https://github.com/vuejs/core/commit/271df09470c61d073185ba6cf3cf50358713c500)) + + + # [3.3.0-alpha.10](https://github.com/vuejs/core/compare/v3.3.0-alpha.9...v3.3.0-alpha.10) (2023-04-17) diff --git a/package.json b/package.json index 4e28fef6f77..3817cbeaea5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "packageManager": "pnpm@7.26.0", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 4bda8ec8828..d0f00cf98cb 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -33,7 +33,7 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.0-alpha.10", + "@vue/shared": "3.3.0-alpha.11", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index b07b66e5a9f..12f902a9dbd 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/compiler-core": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-core": "3.3.0-alpha.11" } } diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 70d0a020451..fb1c1bb34a4 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -33,11 +33,11 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.10", - "@vue/compiler-dom": "3.3.0-alpha.10", - "@vue/compiler-ssr": "3.3.0-alpha.10", - "@vue/reactivity-transform": "3.3.0-alpha.10", - "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-core": "3.3.0-alpha.11", + "@vue/compiler-dom": "3.3.0-alpha.11", + "@vue/compiler-ssr": "3.3.0-alpha.11", + "@vue/reactivity-transform": "3.3.0-alpha.11", + "@vue/shared": "3.3.0-alpha.11", "estree-walker": "^2.0.2", "magic-string": "^0.30.0", "postcss": "^8.1.10", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index 928ac6969eb..c7da262ea2d 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/compiler-dom": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-dom": "3.3.0-alpha.11" } } diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index 6717f6d3806..9fe3201bfd0 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -4,5 +4,5 @@ "dependencies": { "vue": "workspace:*" }, - "version": "3.3.0-alpha.10" + "version": "3.3.0-alpha.11" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index 000a9b2ddab..fae33dc3c2c 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -29,8 +29,8 @@ "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.10", - "@vue/shared": "3.3.0-alpha.10", + "@vue/compiler-core": "3.3.0-alpha.11", + "@vue/shared": "3.3.0-alpha.11", "estree-walker": "^2.0.2", "magic-string": "^0.30.0" }, diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 71577a27848..6aab8e70881 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11" } } diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index c9facdc653a..a6024f0941b 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/reactivity": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/reactivity": "3.3.0-alpha.11" } } diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 223ccb4bae2..16070ec700b 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", @@ -35,8 +35,8 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/runtime-core": "3.3.0-alpha.10", + "@vue/shared": "3.3.0-alpha.11", + "@vue/runtime-core": "3.3.0-alpha.11", "csstype": "^3.1.1" } } diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json index 67718fe4c4c..9417bbeb2d8 100644 --- a/packages/runtime-test/package.json +++ b/packages/runtime-test/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-test", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/runtime-test", "private": true, "main": "index.js", @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/runtime-core": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/runtime-core": "3.3.0-alpha.11" } } diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index 676c0eb46ed..e2fa4fc1a44 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", @@ -32,10 +32,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "peerDependencies": { - "vue": "3.3.0-alpha.10" + "vue": "3.3.0-alpha.11" }, "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/compiler-ssr": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-ssr": "3.3.0-alpha.11" } } diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json index e067028e040..56380d969d5 100644 --- a/packages/sfc-playground/package.json +++ b/packages/sfc-playground/package.json @@ -1,6 +1,6 @@ { "name": "@vue/sfc-playground", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "private": true, "scripts": { "dev": "vite", diff --git a/packages/shared/package.json b/packages/shared/package.json index d3c5665d3cf..219e29b3a28 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/size-check/package.json b/packages/size-check/package.json index 96c8b1f2969..e9550f808d6 100644 --- a/packages/size-check/package.json +++ b/packages/size-check/package.json @@ -1,6 +1,6 @@ { "name": "@vue/size-check", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "private": true, "scripts": { "build": "vite build" diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index c8a4fd03ebe..0d1c25a68d3 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "private": true, "buildOptions": { "formats": [ diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 047b5d0f1c9..0c349b48883 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -43,6 +43,6 @@ "source-map-js": "^1.0.2" }, "peerDependencies": { - "vue": "3.3.0-alpha.10" + "vue": "3.3.0-alpha.11" } } diff --git a/packages/vue/package.json b/packages/vue/package.json index fe1da15c0cc..cd4a392fb21 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.3.0-alpha.10", + "version": "3.3.0-alpha.11", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -81,10 +81,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.10", - "@vue/compiler-dom": "3.3.0-alpha.10", - "@vue/runtime-dom": "3.3.0-alpha.10", - "@vue/compiler-sfc": "3.3.0-alpha.10", - "@vue/server-renderer": "3.3.0-alpha.10" + "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-dom": "3.3.0-alpha.11", + "@vue/runtime-dom": "3.3.0-alpha.11", + "@vue/compiler-sfc": "3.3.0-alpha.11", + "@vue/server-renderer": "3.3.0-alpha.11" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c63903452d..08edce44f29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: specifiers: '@babel/parser': ^7.21.3 '@babel/types': ^7.21.3 - '@vue/shared': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.11 estree-walker: ^2.0.2 source-map-js: ^1.0.2 dependencies: @@ -113,8 +113,8 @@ importers: packages/compiler-dom: specifiers: - '@vue/compiler-core': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/compiler-core': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/compiler-core': link:../compiler-core '@vue/shared': link:../shared @@ -125,12 +125,12 @@ importers: '@babel/types': ^7.21.3 '@types/estree': ^0.0.48 '@types/lru-cache': ^5.1.0 - '@vue/compiler-core': 3.3.0-alpha.10 - '@vue/compiler-dom': 3.3.0-alpha.10 - '@vue/compiler-ssr': 3.3.0-alpha.10 + '@vue/compiler-core': 3.3.0-alpha.11 + '@vue/compiler-dom': 3.3.0-alpha.11 + '@vue/compiler-ssr': 3.3.0-alpha.11 '@vue/consolidate': ^0.17.3 - '@vue/reactivity-transform': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/reactivity-transform': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 estree-walker: ^2.0.2 hash-sum: ^2.0.0 lru-cache: ^5.1.1 @@ -168,8 +168,8 @@ importers: packages/compiler-ssr: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/compiler-dom': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/shared': link:../shared @@ -182,7 +182,7 @@ importers: packages/reactivity: specifiers: - '@vue/shared': 3.3.0-alpha.10 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/shared': link:../shared @@ -191,8 +191,8 @@ importers: '@babel/core': ^7.21.3 '@babel/parser': ^7.20.15 '@babel/types': ^7.21.3 - '@vue/compiler-core': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/compiler-core': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 estree-walker: ^2.0.2 magic-string: ^0.30.0 dependencies: @@ -207,16 +207,16 @@ importers: packages/runtime-core: specifiers: - '@vue/reactivity': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/reactivity': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/reactivity': link:../reactivity '@vue/shared': link:../shared packages/runtime-dom: specifiers: - '@vue/runtime-core': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/runtime-core': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 csstype: ^3.1.1 dependencies: '@vue/runtime-core': link:../runtime-core @@ -225,16 +225,16 @@ importers: packages/runtime-test: specifiers: - '@vue/runtime-core': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/runtime-core': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/runtime-core': link:../runtime-core '@vue/shared': link:../shared packages/server-renderer: specifiers: - '@vue/compiler-ssr': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/compiler-ssr': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/compiler-ssr': link:../compiler-ssr '@vue/shared': link:../shared @@ -275,11 +275,11 @@ importers: packages/vue: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.10 - '@vue/compiler-sfc': 3.3.0-alpha.10 - '@vue/runtime-dom': 3.3.0-alpha.10 - '@vue/server-renderer': 3.3.0-alpha.10 - '@vue/shared': 3.3.0-alpha.10 + '@vue/compiler-dom': 3.3.0-alpha.11 + '@vue/compiler-sfc': 3.3.0-alpha.11 + '@vue/runtime-dom': 3.3.0-alpha.11 + '@vue/server-renderer': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.11 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/compiler-sfc': link:../compiler-sfc From 7f1b546a99bae9c70502470a13301faabaed184b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 18 Apr 2023 11:39:21 +0800 Subject: [PATCH 041/142] workflow: support building types in build script --- rollup.dts.config.js | 8 +++++++- scripts/build.js | 26 ++++++++++++++++++++------ scripts/release.js | 6 +++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/rollup.dts.config.js b/rollup.dts.config.js index 1b4df21adac..bb146c46279 100644 --- a/rollup.dts.config.js +++ b/rollup.dts.config.js @@ -12,7 +12,13 @@ if (!existsSync('temp/packages')) { process.exit(1) } -export default readdirSync('temp/packages').map(pkg => { +const packages = readdirSync('temp/packages') +const targets = process.env.TARGETS ? process.env.TARGETS.split(',') : null +const targetPackages = targets + ? packages.filter(pkg => targets.includes(pkg)) + : packages + +export default targetPackages.map(pkg => { return { input: `./temp/packages/${pkg}/src/index.d.ts`, output: { diff --git a/scripts/build.js b/scripts/build.js index 05ed32ebc5c..689d3dc9351 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -34,6 +34,7 @@ const targets = args._ const formats = args.formats || args.f const devOnly = args.devOnly || args.d const prodOnly = !devOnly && (args.prodOnly || args.p) +const buildTypes = args.withTypes || args.t const sourceMap = args.sourcemap || args.s const isRelease = args.release const buildAllMatching = args.all || args.a @@ -44,12 +45,25 @@ run() async function run() { const removeCache = scanEnums() try { - if (!targets.length) { - await buildAll(allTargets) - checkAllSizes(allTargets) - } else { - await buildAll(fuzzyMatchTarget(targets, buildAllMatching)) - checkAllSizes(fuzzyMatchTarget(targets, buildAllMatching)) + const resolvedTargets = targets.length + ? fuzzyMatchTarget(targets, buildAllMatching) + : allTargets + await buildAll(resolvedTargets) + checkAllSizes(resolvedTargets) + if (buildTypes) { + await execa( + 'pnpm', + [ + 'run', + 'build-dts', + ...(targets.length + ? ['--environment', `TARGETS:${resolvedTargets.join(',')}`] + : []) + ], + { + stdio: 'inherit' + } + ) } } finally { removeCache() diff --git a/scripts/release.js b/scripts/release.js index 15130174149..4eabe911bee 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -199,9 +199,9 @@ async function main() { // build all packages with types step('\nBuilding all packages...') if (!skipBuild && !isDryRun) { - await run('pnpm', ['run', 'build']) - step('\nBuilding and testing types...') - await run('pnpm', ['test-dts']) + await run('pnpm', ['run', 'build', '--withTypes']) + step('\nTesting built types...') + await run('pnpm', ['test-dts-only']) } else { console.log(`(skipped)`) } From 0f77a2b1d1047d66ccdfda70382d1a223886130c Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 18 Apr 2023 11:39:53 +0800 Subject: [PATCH 042/142] fix(compiler): fix expression codegen for literal const bindings in non-inline mode --- .../transforms/transformExpressions.spec.ts | 2 +- .../src/transforms/transformExpression.ts | 7 ++++--- .../__tests__/compileScript/hoistStatic.spec.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts index 66f988d3474..5a77b2eddbc 100644 --- a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts @@ -549,7 +549,7 @@ describe('compiler: expression transform', () => { test('literal const handling, non-inline mode', () => { const { code } = compileWithBindingMetadata(`

{{ literal }}
`) - expect(code).toMatch(`toDisplayString(literal)`) + expect(code).toMatch(`toDisplayString($setup.literal)`) // #7973 should skip patch for literal const expect(code).not.toMatch( `${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */` diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 466027682b1..35fc278ac86 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -197,13 +197,14 @@ export function processExpression( return genPropsAccessExp(bindingMetadata.__propsAliases![raw]) } } else { - if (type && type.startsWith('setup')) { + if ( + (type && type.startsWith('setup')) || + type === BindingTypes.LITERAL_CONST + ) { // setup bindings in non-inline mode return `$setup.${raw}` } else if (type === BindingTypes.PROPS_ALIASED) { return `$props['${bindingMetadata.__propsAliases![raw]}']` - } else if (type === BindingTypes.LITERAL_CONST) { - return raw } else if (type) { return `$${type}.${raw}` } diff --git a/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts index 614a5e75bce..7b3a8a813c5 100644 --- a/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/hoistStatic.spec.ts @@ -202,4 +202,16 @@ describe('sfc hoist static', () => { }) assertCode(content) }) + + test('template binding access in inline mode', () => { + const { content } = compile( + ` + + + ` + ) + expect(content).toMatch('_toDisplayString(foo)') + }) }) From 72be89423da29841682d294bf70acb9e20594330 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 18 Apr 2023 22:18:56 +0800 Subject: [PATCH 043/142] release: v3.3.0-alpha.12 --- CHANGELOG.md | 9 ++++ package.json | 2 +- packages/compiler-core/package.json | 4 +- packages/compiler-dom/package.json | 6 +-- packages/compiler-sfc/package.json | 12 ++--- packages/compiler-ssr/package.json | 6 +-- packages/dts-test/package.json | 2 +- packages/reactivity-transform/package.json | 6 +-- packages/reactivity/package.json | 4 +- packages/runtime-core/package.json | 6 +-- packages/runtime-dom/package.json | 6 +-- packages/runtime-test/package.json | 6 +-- packages/server-renderer/package.json | 8 ++-- packages/sfc-playground/package.json | 2 +- packages/shared/package.json | 2 +- packages/size-check/package.json | 2 +- packages/template-explorer/package.json | 2 +- packages/vue-compat/package.json | 4 +- packages/vue/package.json | 12 ++--- pnpm-lock.yaml | 52 +++++++++++----------- 20 files changed, 81 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69af50d2351..dba7c533dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [3.3.0-alpha.12](https://github.com/vuejs/core/compare/v3.3.0-alpha.11...v3.3.0-alpha.12) (2023-04-18) + + +### Bug Fixes + +* **compiler:** fix expression codegen for literal const bindings in non-inline mode ([0f77a2b](https://github.com/vuejs/core/commit/0f77a2b1d1047d66ccdfda70382d1a223886130c)) + + + # [3.3.0-alpha.11](https://github.com/vuejs/core/compare/v3.3.0-alpha.10...v3.3.0-alpha.11) (2023-04-17) diff --git a/package.json b/package.json index 3817cbeaea5..9aadea2b94c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "packageManager": "pnpm@7.26.0", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index d0f00cf98cb..3e04a35b48c 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -33,7 +33,7 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.0-alpha.11", + "@vue/shared": "3.3.0-alpha.12", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 12f902a9dbd..77315a776aa 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/compiler-core": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/compiler-core": "3.3.0-alpha.12" } } diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index fb1c1bb34a4..7bdce2ae52f 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -33,11 +33,11 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.11", - "@vue/compiler-dom": "3.3.0-alpha.11", - "@vue/compiler-ssr": "3.3.0-alpha.11", - "@vue/reactivity-transform": "3.3.0-alpha.11", - "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-core": "3.3.0-alpha.12", + "@vue/compiler-dom": "3.3.0-alpha.12", + "@vue/compiler-ssr": "3.3.0-alpha.12", + "@vue/reactivity-transform": "3.3.0-alpha.12", + "@vue/shared": "3.3.0-alpha.12", "estree-walker": "^2.0.2", "magic-string": "^0.30.0", "postcss": "^8.1.10", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index c7da262ea2d..00a19fd30fb 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/compiler-dom": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/compiler-dom": "3.3.0-alpha.12" } } diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index 9fe3201bfd0..35f773f4c8f 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -4,5 +4,5 @@ "dependencies": { "vue": "workspace:*" }, - "version": "3.3.0-alpha.11" + "version": "3.3.0-alpha.12" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index fae33dc3c2c..8f9f27714a2 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -29,8 +29,8 @@ "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.0-alpha.11", - "@vue/shared": "3.3.0-alpha.11", + "@vue/compiler-core": "3.3.0-alpha.12", + "@vue/shared": "3.3.0-alpha.12", "estree-walker": "^2.0.2", "magic-string": "^0.30.0" }, diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 6aab8e70881..976c806f7d3 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12" } } diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index a6024f0941b..262062392b3 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/reactivity": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/reactivity": "3.3.0-alpha.12" } } diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 16070ec700b..75f5abfaed3 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", @@ -35,8 +35,8 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/runtime-core": "3.3.0-alpha.11", + "@vue/shared": "3.3.0-alpha.12", + "@vue/runtime-core": "3.3.0-alpha.12", "csstype": "^3.1.1" } } diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json index 9417bbeb2d8..410b18e315a 100644 --- a/packages/runtime-test/package.json +++ b/packages/runtime-test/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-test", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/runtime-test", "private": true, "main": "index.js", @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/runtime-core": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/runtime-core": "3.3.0-alpha.12" } } diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index e2fa4fc1a44..a33085b4b4c 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", @@ -32,10 +32,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "peerDependencies": { - "vue": "3.3.0-alpha.11" + "vue": "3.3.0-alpha.12" }, "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/compiler-ssr": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/compiler-ssr": "3.3.0-alpha.12" } } diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json index 56380d969d5..fa21efb2cd2 100644 --- a/packages/sfc-playground/package.json +++ b/packages/sfc-playground/package.json @@ -1,6 +1,6 @@ { "name": "@vue/sfc-playground", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "private": true, "scripts": { "dev": "vite", diff --git a/packages/shared/package.json b/packages/shared/package.json index 219e29b3a28..f45c496a974 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/size-check/package.json b/packages/size-check/package.json index e9550f808d6..ec6535ed06d 100644 --- a/packages/size-check/package.json +++ b/packages/size-check/package.json @@ -1,6 +1,6 @@ { "name": "@vue/size-check", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "private": true, "scripts": { "build": "vite build" diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index 0d1c25a68d3..a3864701851 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "private": true, "buildOptions": { "formats": [ diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 0c349b48883..0a1f8117f26 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -43,6 +43,6 @@ "source-map-js": "^1.0.2" }, "peerDependencies": { - "vue": "3.3.0-alpha.11" + "vue": "3.3.0-alpha.12" } } diff --git a/packages/vue/package.json b/packages/vue/package.json index cd4a392fb21..7769f075d1b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.3.0-alpha.11", + "version": "3.3.0-alpha.12", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -81,10 +81,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme", "dependencies": { - "@vue/shared": "3.3.0-alpha.11", - "@vue/compiler-dom": "3.3.0-alpha.11", - "@vue/runtime-dom": "3.3.0-alpha.11", - "@vue/compiler-sfc": "3.3.0-alpha.11", - "@vue/server-renderer": "3.3.0-alpha.11" + "@vue/shared": "3.3.0-alpha.12", + "@vue/compiler-dom": "3.3.0-alpha.12", + "@vue/runtime-dom": "3.3.0-alpha.12", + "@vue/compiler-sfc": "3.3.0-alpha.12", + "@vue/server-renderer": "3.3.0-alpha.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08edce44f29..1d880dbbd69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: specifiers: '@babel/parser': ^7.21.3 '@babel/types': ^7.21.3 - '@vue/shared': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.12 estree-walker: ^2.0.2 source-map-js: ^1.0.2 dependencies: @@ -113,8 +113,8 @@ importers: packages/compiler-dom: specifiers: - '@vue/compiler-core': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/compiler-core': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/compiler-core': link:../compiler-core '@vue/shared': link:../shared @@ -125,12 +125,12 @@ importers: '@babel/types': ^7.21.3 '@types/estree': ^0.0.48 '@types/lru-cache': ^5.1.0 - '@vue/compiler-core': 3.3.0-alpha.11 - '@vue/compiler-dom': 3.3.0-alpha.11 - '@vue/compiler-ssr': 3.3.0-alpha.11 + '@vue/compiler-core': 3.3.0-alpha.12 + '@vue/compiler-dom': 3.3.0-alpha.12 + '@vue/compiler-ssr': 3.3.0-alpha.12 '@vue/consolidate': ^0.17.3 - '@vue/reactivity-transform': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/reactivity-transform': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 estree-walker: ^2.0.2 hash-sum: ^2.0.0 lru-cache: ^5.1.1 @@ -168,8 +168,8 @@ importers: packages/compiler-ssr: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/compiler-dom': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/shared': link:../shared @@ -182,7 +182,7 @@ importers: packages/reactivity: specifiers: - '@vue/shared': 3.3.0-alpha.11 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/shared': link:../shared @@ -191,8 +191,8 @@ importers: '@babel/core': ^7.21.3 '@babel/parser': ^7.20.15 '@babel/types': ^7.21.3 - '@vue/compiler-core': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/compiler-core': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 estree-walker: ^2.0.2 magic-string: ^0.30.0 dependencies: @@ -207,16 +207,16 @@ importers: packages/runtime-core: specifiers: - '@vue/reactivity': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/reactivity': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/reactivity': link:../reactivity '@vue/shared': link:../shared packages/runtime-dom: specifiers: - '@vue/runtime-core': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/runtime-core': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 csstype: ^3.1.1 dependencies: '@vue/runtime-core': link:../runtime-core @@ -225,16 +225,16 @@ importers: packages/runtime-test: specifiers: - '@vue/runtime-core': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/runtime-core': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/runtime-core': link:../runtime-core '@vue/shared': link:../shared packages/server-renderer: specifiers: - '@vue/compiler-ssr': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/compiler-ssr': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/compiler-ssr': link:../compiler-ssr '@vue/shared': link:../shared @@ -275,11 +275,11 @@ importers: packages/vue: specifiers: - '@vue/compiler-dom': 3.3.0-alpha.11 - '@vue/compiler-sfc': 3.3.0-alpha.11 - '@vue/runtime-dom': 3.3.0-alpha.11 - '@vue/server-renderer': 3.3.0-alpha.11 - '@vue/shared': 3.3.0-alpha.11 + '@vue/compiler-dom': 3.3.0-alpha.12 + '@vue/compiler-sfc': 3.3.0-alpha.12 + '@vue/runtime-dom': 3.3.0-alpha.12 + '@vue/server-renderer': 3.3.0-alpha.12 + '@vue/shared': 3.3.0-alpha.12 dependencies: '@vue/compiler-dom': link:../compiler-dom '@vue/compiler-sfc': link:../compiler-sfc From 9b5a34bf8c0d1b4c6ec3cf1434076b7e25065f84 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 18 Apr 2023 22:21:29 +0800 Subject: [PATCH 044/142] fix(compiler-sfc): normalize filename when invalidating cache --- packages/compiler-sfc/src/script/resolveType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 247bac3a3be..6557b589bf1 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -744,6 +744,7 @@ const fileToScopeCache = createCache() * @private */ export function invalidateTypeCache(filename: string) { + filename = normalizePath(filename) fileToScopeCache.delete(filename) tsConfigCache.delete(filename) } From 94fa67a4f73b3646c8c1e29512a71b17bd56efc3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 20 Apr 2023 10:06:06 +0800 Subject: [PATCH 045/142] fix(hmr): force update cached slots during HMR close #7155 close #7158 --- packages/runtime-core/__tests__/hmr.spec.ts | 31 +++++++ packages/runtime-core/src/component.ts | 86 +++++++++++++------ .../src/componentPublicInstance.ts | 2 + packages/runtime-core/src/componentSlots.ts | 3 + 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index d1392b78465..b81a8b3af63 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -537,4 +537,35 @@ describe('hot module replacement', () => { render(h(Foo), root) expect(serializeInner(root)).toBe('bar') }) + + // #7155 - force HMR on slots content update + test('force update slot content change', () => { + const root = nodeOps.createElement('div') + const parentId = 'test-force-computed-parent' + const childId = 'test-force-computed-child' + + const Child: ComponentOptions = { + __hmrId: childId, + computed: { + slotContent() { + return this.$slots.default?.() + } + }, + render: compileToFunction(``) + } + createRecord(childId, Child) + + const Parent: ComponentOptions = { + __hmrId: parentId, + components: { Child }, + render: compileToFunction(`1`) + } + createRecord(parentId, Parent) + + render(h(Parent), root) + expect(serializeInner(root)).toBe(`1`) + + rerender(parentId, compileToFunction(`2`)) + expect(serializeInner(root)).toBe(`2`) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 941231b393d..087e901354b 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -349,6 +349,10 @@ export interface ComponentInternalInstance { slots: InternalSlots refs: Data emit: EmitFn + + attrsProxy: Data | null + slotsProxy: Slots | null + /** * used for keeping track of .once event handlers on components * @internal @@ -536,6 +540,9 @@ export function createComponentInstance( setupState: EMPTY_OBJ, setupContext: null, + attrsProxy: null, + slotsProxy: null, + // suspense related suspense, suspenseId: suspense ? suspense.pendingId : 0, @@ -923,31 +930,57 @@ export function finishComponentSetup( } } -function createAttrsProxy(instance: ComponentInternalInstance): Data { - return new Proxy( - instance.attrs, - __DEV__ - ? { - get(target, key: string) { - markAttrsAccessed() - track(instance, TrackOpTypes.GET, '$attrs') - return target[key] - }, - set() { - warn(`setupContext.attrs is readonly.`) - return false - }, - deleteProperty() { - warn(`setupContext.attrs is readonly.`) - return false +function getAttrsProxy(instance: ComponentInternalInstance): Data { + return ( + instance.attrsProxy || + (instance.attrsProxy = new Proxy( + instance.attrs, + __DEV__ + ? { + get(target, key: string) { + markAttrsAccessed() + track(instance, TrackOpTypes.GET, '$attrs') + return target[key] + }, + set() { + warn(`setupContext.attrs is readonly.`) + return false + }, + deleteProperty() { + warn(`setupContext.attrs is readonly.`) + return false + } } - } - : { - get(target, key: string) { - track(instance, TrackOpTypes.GET, '$attrs') - return target[key] + : { + get(target, key: string) { + track(instance, TrackOpTypes.GET, '$attrs') + return target[key] + } } - } + )) + ) +} + +/** + * Dev-only + */ +function getSlotsProxy(instance: ComponentInternalInstance): Slots { + return ( + instance.slotsProxy || + (instance.slotsProxy = new Proxy(instance.slots, { + get(target, key: string) { + track(instance, TrackOpTypes.GET, '$slots') + return target[key] + }, + set() { + warn(`setupContext.slots is readonly.`) + return false + }, + deleteProperty() { + warn(`setupContext.slots is readonly.`) + return false + } + })) ) } @@ -978,16 +1011,15 @@ export function createSetupContext( instance.exposed = exposed || {} } - let attrs: Data if (__DEV__) { // We use getters in dev in case libs like test-utils overwrite instance // properties (overwrites should not be done in prod) return Object.freeze({ get attrs() { - return attrs || (attrs = createAttrsProxy(instance)) + return getAttrsProxy(instance) }, get slots() { - return shallowReadonly(instance.slots) + return getSlotsProxy(instance) }, get emit() { return (event: string, ...args: any[]) => instance.emit(event, ...args) @@ -997,7 +1029,7 @@ export function createSetupContext( } else { return { get attrs() { - return attrs || (attrs = createAttrsProxy(instance)) + return getAttrsProxy(instance) }, slots: instance.slots, emit: instance.emit, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7b0ccf77ac9..dd2d29670e6 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -356,6 +356,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { if (key === '$attrs') { track(instance, TrackOpTypes.GET, key) __DEV__ && markAttrsAccessed() + } else if (__DEV__ && key === '$slots') { + track(instance, TrackOpTypes.GET, key) } return publicGetter(instance) } else if ( diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 81988599981..8f59099d833 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -23,6 +23,8 @@ import { ContextualRenderFn, withCtx } from './componentRenderContext' import { isHmrUpdating } from './hmr' import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig' import { toRaw } from '@vue/reactivity' +import { trigger } from '@vue/reactivity' +import { TriggerOpTypes } from '@vue/reactivity' export type Slot = ( ...args: IfAny @@ -196,6 +198,7 @@ export const updateSlots = ( // Parent was HMR updated so slot content may have changed. // force update slots and mark instance for hmr as well extend(slots, children as Slots) + trigger(instance, TriggerOpTypes.SET, '$slots') } else if (optimized && type === SlotFlags.STABLE) { // compiled AND stable. // no need to update, and skip stale slots removal. From a58785945d112827185faac801f15828df642da8 Mon Sep 17 00:00:00 2001 From: agoni1212 <22545824+agoni1212@users.noreply.github.com> Date: Thu, 20 Apr 2023 10:07:31 +0800 Subject: [PATCH 046/142] chore: typo (#8108) [ci skip] --- packages/compiler-sfc/src/script/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 04df22a2b64..e8a0518b570 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -101,7 +101,7 @@ export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean) { const windowsSlashRE = /\\/g export function normalizePath(p: string) { - // in the browser build, the polyfill doesn't expose posix, but defualts to + // in the browser build, the polyfill doesn't expose posix, but defaults to // posix behavior. return (path.posix || path).normalize(p.replace(windowsSlashRE, '/')) } From f630555caa2d57b078454c9a57851a3f7fab327e Mon Sep 17 00:00:00 2001 From: n028 Date: Thu, 20 Apr 2023 04:08:00 +0200 Subject: [PATCH 047/142] chore: fix typo (#8113) [ci skip] --- packages/compiler-sfc/src/script/resolveType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 6557b589bf1..022c259f79e 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -159,7 +159,7 @@ function innerResolveTypeElements( return resolveBuiltin(ctx, node, typeName as any, scope) } return ctx.error( - `Unresolvable type reference or unsupported built-in utlility type`, + `Unresolvable type reference or unsupported built-in utility type`, node, scope ) From 2f9f6eceb9aeffa5ef20a416dd4d1d17eb998111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=9B=BE=E4=B8=89=E8=AF=AD?= <32354856+baiwusanyu-c@users.noreply.github.com> Date: Thu, 20 Apr 2023 10:11:22 +0800 Subject: [PATCH 048/142] chore: delete outdated content in readme (#8093) [ci skip] close #8084 --- packages/reactivity/README.md | 2 +- packages/runtime-core/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/reactivity/README.md b/packages/reactivity/README.md index 93685ea5a1b..e4780740dab 100644 --- a/packages/reactivity/README.md +++ b/packages/reactivity/README.md @@ -4,7 +4,7 @@ This package is inlined into Global & Browser ESM builds of user-facing renderers (e.g. `@vue/runtime-dom`), but also published as a package that can be used standalone. The standalone build should not be used alongside a pre-bundled build of a user-facing renderer, as they will have different internal storage for reactivity connections. A user-facing renderer should re-export all APIs from this package. -For full exposed APIs, see `src/index.ts`. You can also run `pnpm build reactivity --types` from repo root, which will generate an API report at `temp/reactivity.api.md`. +For full exposed APIs, see `src/index.ts`. ## Credits diff --git a/packages/runtime-core/README.md b/packages/runtime-core/README.md index 753378bb749..3a5b2981203 100644 --- a/packages/runtime-core/README.md +++ b/packages/runtime-core/README.md @@ -2,7 +2,7 @@ > This package is published only for typing and building custom renderers. It is NOT meant to be used in applications. -For full exposed APIs, see `src/index.ts`. You can also run `pnpm build runtime-core --types` from repo root, which will generate an API report at `temp/runtime-core.api.md`. +For full exposed APIs, see `src/index.ts`. ## Building a Custom Renderer From 5510ce385abfa151c07a5253cccf4abccabdd01d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 20 Apr 2023 04:12:18 +0200 Subject: [PATCH 049/142] feat: hasInjectionContext() for libraries (#8111) --- .../runtime-core/__tests__/apiInject.spec.ts | 31 +++++++++++++++++-- packages/runtime-core/src/apiInject.ts | 9 ++++++ packages/runtime-core/src/index.ts | 2 +- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/__tests__/apiInject.spec.ts b/packages/runtime-core/__tests__/apiInject.spec.ts index 87a415aa972..a7aae7ebfa9 100644 --- a/packages/runtime-core/__tests__/apiInject.spec.ts +++ b/packages/runtime-core/__tests__/apiInject.spec.ts @@ -8,9 +8,10 @@ import { Ref, readonly, reactive, - defineComponent + defineComponent, + hasInjectionContext } from '../src/index' -import { render, nodeOps, serialize } from '@vue/runtime-test' +import { render, nodeOps, serialize, createApp } from '@vue/runtime-test' // reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject describe('api: provide/inject', () => { @@ -347,4 +348,30 @@ describe('api: provide/inject', () => { render(h(Comp), root) expect(serialize(root)).toBe(`
`) }) + + describe('hasInjectionContext', () => { + it('should be false outside of setup', () => { + expect(hasInjectionContext()).toBe(false) + }) + + it('should be true within setup', () => { + expect.assertions(1) + const Comp = { + setup() { + expect(hasInjectionContext()).toBe(true) + return () => null + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + }) + + it('should be true within app.runWithContext()', () => { + expect.assertions(1) + createApp({}).runWithContext(() => { + expect(hasInjectionContext()).toBe(true) + }) + }) + }) }) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 6eedee88c09..4559c1b702f 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -73,3 +73,12 @@ export function inject( warn(`inject() can only be used inside setup() or functional components.`) } } + +/** + * Returns true if `inject()` can be used without warning about being called in the wrong place (e.g. outside of + * setup()). This is used by libraries that want to use `inject()` internally without triggering a warning to the end + * user. One example is `useRoute()` in `vue-router`. + */ +export function hasInjectionContext(): boolean { + return !!(currentInstance || currentRenderingInstance || currentApp) +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e427773a70e..a115b0179c1 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -56,7 +56,7 @@ export { onErrorCaptured, onServerPrefetch } from './apiLifecycle' -export { provide, inject } from './apiInject' +export { provide, inject, hasInjectionContext } from './apiInject' export { nextTick } from './scheduler' export { defineComponent } from './apiDefineComponent' export { defineAsyncComponent } from './apiAsyncComponent' From d53e157805678db7a3b9ca2fccc74530e1dfbc48 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 20 Apr 2023 14:13:08 +0800 Subject: [PATCH 050/142] fix(compiler-sfc): handle type merging + fix namespace access when inferring type close #8102 --- .../compileScript/resolveType.spec.ts | 106 ++++++++++++ .../compiler-sfc/src/script/resolveType.ts | 160 ++++++++++++++---- 2 files changed, 229 insertions(+), 37 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 0f2a47d6b99..960ef592ca7 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -294,6 +294,84 @@ describe('resolveType', () => { }) }) + test('interface merging', () => { + expect( + resolve(` + interface Foo { + a: string + } + interface Foo { + b: number + } + defineProps<{ + foo: Foo['a'], + bar: Foo['b'] + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('namespace merging', () => { + expect( + resolve(` + namespace Foo { + export type A = string + } + namespace Foo { + export type B = number + } + defineProps<{ + foo: Foo.A, + bar: Foo.B + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('namespace merging with other types', () => { + expect( + resolve(` + namespace Foo { + export type A = string + } + interface Foo { + b: number + } + defineProps<{ + foo: Foo.A, + bar: Foo['b'] + }>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'] + }) + }) + + test('enum merging', () => { + expect( + resolve(` + enum Foo { + A = 1 + } + enum Foo { + B = 'hi' + } + defineProps<{ + foo: Foo + }>() + `).props + ).toStrictEqual({ + foo: ['Number', 'String'] + }) + }) + describe('external type imports', () => { const files = { '/foo.ts': 'export type P = { foo: number }', @@ -436,6 +514,34 @@ describe('resolveType', () => { }) expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) + + test('global types with ambient references', () => { + const files = { + // with references + '/backend.d.ts': ` + declare namespace App.Data { + export type AircraftData = { + id: string + manufacturer: App.Data.Listings.ManufacturerData + } + } + declare namespace App.Data.Listings { + export type ManufacturerData = { + id: string + } + } + ` + } + + const { props } = resolve(`defineProps()`, files, { + globalTypeFiles: Object.keys(files) + }) + + expect(props).toStrictEqual({ + id: ['String'], + manufacturer: ['Object'] + }) + }) }) describe('errors', () => { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 022c259f79e..d34c8046970 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -65,11 +65,14 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext type Import = Pick -type ScopeTypeNode = Node & { - // scope types always has ownerScope attached +interface WithScope { _ownerScope: TypeScope } +// scope types always has ownerScope attached +type ScopeTypeNode = Node & + WithScope & { _ns?: TSModuleDeclaration & WithScope } + export interface TypeScope { filename: string source: string @@ -79,7 +82,7 @@ export interface TypeScope { exportedTypes: Record } -export interface WithScope { +export interface MaybeWithScope { _ownerScope?: TypeScope } @@ -100,7 +103,7 @@ interface ResolvedElements { */ export function resolveTypeElements( ctx: TypeResolveContext, - node: Node & WithScope & { _resolvedElements?: ResolvedElements }, + node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements }, scope?: TypeScope ): ResolvedElements { if (node._resolvedElements) { @@ -177,7 +180,7 @@ function typeElementsToMap( const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { - ;(e as WithScope)._ownerScope = scope + ;(e as MaybeWithScope)._ownerScope = scope const name = getId(e.key) if (name && !e.computed) { res.props[name] = e as ResolvedElements['props'][string] @@ -248,7 +251,7 @@ function createProperty( function resolveInterfaceMembers( ctx: TypeResolveContext, - node: TSInterfaceDeclaration & WithScope, + node: TSInterfaceDeclaration & MaybeWithScope, scope: TypeScope ): ResolvedElements { const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) @@ -289,7 +292,7 @@ function resolveIndexType( ctx: TypeResolveContext, node: TSIndexedAccessType, scope: TypeScope -): (TSType & WithScope)[] { +): (TSType & MaybeWithScope)[] { if (node.indexType.type === 'TSNumberKeyword') { return resolveArrayElementType(ctx, node.objectType, scope) } @@ -308,7 +311,7 @@ function resolveIndexType( for (const key of keys) { const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation if (targetType) { - ;(targetType as TSType & WithScope)._ownerScope = + ;(targetType as TSType & MaybeWithScope)._ownerScope = resolved.props[key]._ownerScope types.push(targetType) } @@ -532,22 +535,22 @@ function innerResolveTypeReference( } } } else { - const ns = innerResolveTypeReference( - ctx, - scope, - name[0], - node, - onlyExported - ) - if (ns && ns.type === 'TSModuleDeclaration') { - const childScope = moduleDeclToScope(ns, scope) - return innerResolveTypeReference( - ctx, - childScope, - name.length > 2 ? name.slice(1) : name[name.length - 1], - node, - !ns.declare - ) + let ns = innerResolveTypeReference(ctx, scope, name[0], node, onlyExported) + if (ns) { + if (ns.type !== 'TSModuleDeclaration') { + // namespace merged with other types, attached as _ns + ns = ns._ns + } + if (ns) { + const childScope = moduleDeclToScope(ns, ns._ownerScope || scope) + return innerResolveTypeReference( + ctx, + childScope, + name.length > 2 ? name.slice(1) : name[name.length - 1], + node, + !ns.declare + ) + } } } } @@ -771,7 +774,6 @@ export function fileToScope( exportedTypes: Object.create(null) } recordTypes(body, scope, asGlobal) - fileToScopeCache.set(filename, scope) return scope } @@ -858,10 +860,21 @@ function moduleDeclToScope( } const scope: TypeScope = { ...parentScope, + imports: Object.create(parentScope.imports), + // TODO this seems wrong types: Object.create(parentScope.types), - imports: Object.create(parentScope.imports) + exportedTypes: Object.create(null) + } + + if (node.body.type === 'TSModuleDeclaration') { + const decl = node.body as TSModuleDeclaration & WithScope + decl._ownerScope = scope + const id = getId(decl.id) + scope.types[id] = scope.exportedTypes[id] = decl + } else { + recordTypes(node.body.body, scope) } - recordTypes((node.body as TSModuleBlock).body, scope) + return (node._resolvedChildScope = scope) } @@ -923,7 +936,9 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) { } } for (const key of Object.keys(types)) { - types[key]._ownerScope = scope + const node = types[key] + node._ownerScope = scope + if (node._ns) node._ns._ownerScope = scope } } @@ -931,12 +946,42 @@ function recordType(node: Node, types: Record) { switch (node.type) { case 'TSInterfaceDeclaration': case 'TSEnumDeclaration': - case 'TSModuleDeclaration': - case 'ClassDeclaration': { - const id = node.id.type === 'Identifier' ? node.id.name : node.id.value - types[id] = node + case 'TSModuleDeclaration': { + const id = getId(node.id) + let existing = types[id] + if (existing) { + if (node.type === 'TSModuleDeclaration') { + if (existing.type === 'TSModuleDeclaration') { + mergeNamespaces(existing as typeof node, node) + } else { + attachNamespace(existing, node) + } + break + } + if (existing.type === 'TSModuleDeclaration') { + // replace and attach namespace + types[id] = node + attachNamespace(node, existing) + break + } + + if (existing.type !== node.type) { + // type-level error + break + } + if (node.type === 'TSInterfaceDeclaration') { + ;(existing as typeof node).body.body.push(...node.body.body) + } else { + ;(existing as typeof node).members.push(...node.members) + } + } else { + types[id] = node + } break } + case 'ClassDeclaration': + types[getId(node.id)] = node + break case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation break @@ -955,6 +1000,47 @@ function recordType(node: Node, types: Record) { } } +function mergeNamespaces(to: TSModuleDeclaration, from: TSModuleDeclaration) { + const toBody = to.body + const fromBody = from.body + if (toBody.type === 'TSModuleDeclaration') { + if (fromBody.type === 'TSModuleDeclaration') { + // both decl + mergeNamespaces(toBody, fromBody) + } else { + // to: decl -> from: block + fromBody.body.push({ + type: 'ExportNamedDeclaration', + declaration: toBody, + exportKind: 'type', + specifiers: [] + }) + } + } else if (fromBody.type === 'TSModuleDeclaration') { + // to: block <- from: decl + toBody.body.push({ + type: 'ExportNamedDeclaration', + declaration: fromBody, + exportKind: 'type', + specifiers: [] + }) + } else { + // both block + toBody.body.push(...fromBody.body) + } +} + +function attachNamespace( + to: Node & { _ns?: TSModuleDeclaration }, + ns: TSModuleDeclaration +) { + if (!to._ns) { + to._ns = ns + } else { + mergeNamespaces(to._ns, ns) + } +} + export function recordImports(body: Statement[]) { const imports: TypeScope['imports'] = Object.create(null) for (const s of body) { @@ -977,7 +1063,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) { export function inferRuntimeType( ctx: TypeResolveContext, - node: Node & WithScope, + node: Node & MaybeWithScope, scope = node._ownerScope || ctxToScope(ctx) ): string[] { switch (node.type) { @@ -1035,11 +1121,11 @@ export function inferRuntimeType( } case 'TSTypeReference': + const resolved = resolveTypeReference(ctx, node, scope) + if (resolved) { + return inferRuntimeType(ctx, resolved, resolved._ownerScope) + } if (node.typeName.type === 'Identifier') { - const resolved = resolveTypeReference(ctx, node, scope) - if (resolved) { - return inferRuntimeType(ctx, resolved, resolved._ownerScope) - } switch (node.typeName.name) { case 'Array': case 'Function': From f17a82c769cfb60ee6785ef5d34d91191d153542 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 20 Apr 2023 14:31:54 +0800 Subject: [PATCH 051/142] fix(hmr): always traverse static children in dev fix #7921 close #8100 --- packages/runtime-core/src/renderer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index b1e048e588b..413355508b9 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -835,7 +835,8 @@ function baseCreateRenderer( areChildrenSVG, slotScopeIds ) - if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { + if (__DEV__) { + // necessary for HMR traverseStaticChildren(n1, n2) } } else if (!optimized) { @@ -1110,7 +1111,8 @@ function baseCreateRenderer( isSVG, slotScopeIds ) - if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { + if (__DEV__) { + // necessary for HMR traverseStaticChildren(n1, n2) } else if ( // #2080 if the stable fragment has a key, it's a