diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db874b1240e..d4ec93e3e22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: cache: 'pnpm' - run: pnpm install - - run: node node_modules/puppeteer/install.js + - run: node node_modules/puppeteer/install.mjs - name: Run e2e tests run: pnpm run test-e2e diff --git a/CHANGELOG.md b/CHANGELOG.md index 12be7dc8a3a..c8cdb94b5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24) + + +### Bug Fixes + +* **compiler-sfc:** avoid gen useCssVars when targeting SSR ([#6979](https://github.com/vuejs/core/issues/6979)) ([c568778](https://github.com/vuejs/core/commit/c568778ea3265d8e57f788b00864c9509bf88a4e)), closes [#6926](https://github.com/vuejs/core/issues/6926) +* **compiler-ssr:** proper scope analysis for ssr vnode slot fallback ([#7184](https://github.com/vuejs/core/issues/7184)) ([e09c26b](https://github.com/vuejs/core/commit/e09c26bc9bc4394c2c2d928806d382515c2676f3)), closes [#7095](https://github.com/vuejs/core/issues/7095) +* correctly resolve types from relative paths on Windows ([#9446](https://github.com/vuejs/core/issues/9446)) ([089d36d](https://github.com/vuejs/core/commit/089d36d167dc7834065b03ca689f9b6a44eead8a)), closes [#8671](https://github.com/vuejs/core/issues/8671) +* **hmr:** fix hmr error for hoisted children array in v-for ([7334376](https://github.com/vuejs/core/commit/733437691f70ebca8dd6cc3bc8356f5b57d4d5d8)), closes [#6978](https://github.com/vuejs/core/issues/6978) [#7114](https://github.com/vuejs/core/issues/7114) +* **reactivity:** assigning array.length while observing a symbol property ([#7568](https://github.com/vuejs/core/issues/7568)) ([e9e2778](https://github.com/vuejs/core/commit/e9e2778e9ec5cca07c1df5f0c9b7b3595a1a3244)) +* **scheduler:** ensure jobs are in the correct order ([#7748](https://github.com/vuejs/core/issues/7748)) ([a8f6638](https://github.com/vuejs/core/commit/a8f663867b8cd2736b82204bc58756ef02441276)), closes [#7576](https://github.com/vuejs/core/issues/7576) +* **ssr:** fix hydration mismatch for disabled teleport at component root ([#9399](https://github.com/vuejs/core/issues/9399)) ([d8990fc](https://github.com/vuejs/core/commit/d8990fc6182d1c2cf0a8eab7b35a9d04df668507)), closes [#6152](https://github.com/vuejs/core/issues/6152) +* **Suspense:** calling hooks before the transition finishes ([#9388](https://github.com/vuejs/core/issues/9388)) ([00de3e6](https://github.com/vuejs/core/commit/00de3e61ed7a55e7d6c2e1987551d66ad0f909ff)), closes [#5844](https://github.com/vuejs/core/issues/5844) [#5952](https://github.com/vuejs/core/issues/5952) +* **transition/ssr:** make transition appear work with SSR ([#8859](https://github.com/vuejs/core/issues/8859)) ([5ea8a8a](https://github.com/vuejs/core/commit/5ea8a8a4fab4e19a71e123e4d27d051f5e927172)), closes [#6951](https://github.com/vuejs/core/issues/6951) +* **types:** fix ComponentCustomProps augmentation ([#9468](https://github.com/vuejs/core/issues/9468)) ([7374e93](https://github.com/vuejs/core/commit/7374e93f0281f273b90ab5a6724cc47332a01d6c)), closes [#8376](https://github.com/vuejs/core/issues/8376) +* **types:** improve `h` overload to support union of string and component ([#5432](https://github.com/vuejs/core/issues/5432)) ([16ecb44](https://github.com/vuejs/core/commit/16ecb44c89cd8299a3b8de33cccc2e2cc36f065b)), closes [#5431](https://github.com/vuejs/core/issues/5431) + + + ## [3.3.6](https://github.com/vuejs/core/compare/v3.3.5...v3.3.6) (2023-10-20) diff --git a/package.json b/package.json index d903294ab1e..092feef1382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.3.6", + "version": "3.3.7", "packageManager": "pnpm@8.9.2", "type": "module", "scripts": { @@ -66,16 +66,16 @@ "@rollup/plugin-replace": "^5.0.4", "@rollup/plugin-terser": "^0.4.4", "@types/hash-sum": "^1.0.1", - "@types/node": "^18.18.6", + "@types/node": "^20.8.7", "@typescript-eslint/parser": "^6.8.0", - "@vitest/coverage-istanbul": "^0.34.4", + "@vitest/coverage-istanbul": "^0.34.6", "@vue/consolidate": "0.17.3", "conventional-changelog-cli": "^4.1.0", "enquirer": "^2.4.1", "esbuild": "^0.19.5", "esbuild-plugin-polyfill-node": "^0.3.0", - "eslint": "^8.51.0", - "eslint-plugin-jest": "^27.4.2", + "eslint": "^8.52.0", + "eslint-plugin-jest": "^27.4.3", "estree-walker": "^2.0.2", "execa": "^8.0.1", "jsdom": "^22.1.0", @@ -90,9 +90,9 @@ "prettier": "^3.0.3", "pretty-bytes": "^6.1.1", "pug": "^3.0.2", - "puppeteer": "~21.2.1", + "puppeteer": "~21.4.0", "rimraf": "^5.0.5", - "rollup": "^3.29.4", + "rollup": "^4.1.4", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-polyfill-node": "^0.12.0", @@ -100,11 +100,11 @@ "serve": "^14.2.1", "simple-git-hooks": "^2.9.0", "terser": "^5.22.0", - "todomvc-app-css": "^2.4.2", + "todomvc-app-css": "^2.4.3", "tslib": "^2.6.2", "tsx": "^3.14.0", - "typescript": "^5.1.6", - "vite": "^4.3.0", - "vitest": "^0.34.4" + "typescript": "^5.2.2", + "vite": "^4.5.0", + "vitest": "^0.34.6" } } diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts index eec5a76d363..49ad7ad8982 100644 --- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -593,5 +593,17 @@ describe('compiler: hoistStatic transform', () => { expect(root.hoists.length).toBe(2) expect(generate(root).code).toMatchSnapshot() }) + + test('clone hoisted array children in HMR mode', () => { + const root = transformWithHoist(`
`, { + hmr: true + }) + expect(root.hoists.length).toBe(2) + expect(root.codegenNode).toMatchObject({ + children: { + content: '[..._hoisted_2]' + } + }) + }) }) }) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index f885e2046e1..8c9f06f3543 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.6", + "version": "3.3.7", "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.23.0", - "@vue/shared": "3.3.6", + "@vue/shared": "3.3.7", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 65bbcb36dd6..abfba98e35c 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -256,6 +256,12 @@ export interface TransformOptions * needed to render inline CSS variables on component root */ ssrCssVars?: string + /** + * Whether to compile the template assuming it needs to handle HMR. + * Some edge cases may need to generate different code for HMR to work + * correctly, e.g. #6938, #7138 + */ + hmr?: boolean } export interface CodegenOptions extends SharedTransformCodegenOptions { diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index d26c11bba20..04f85679cae 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -129,6 +129,7 @@ export function createTransformContext( filename = '', prefixIdentifiers = false, hoistStatic = false, + hmr = false, cacheHandlers = false, nodeTransforms = [], directiveTransforms = {}, @@ -155,6 +156,7 @@ export function createTransformContext( selfName: nameMatch && capitalize(camelize(nameMatch[1])), prefixIdentifiers, hoistStatic, + hmr, cacheHandlers, nodeTransforms, directiveTransforms, diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 5526163c6f9..fd443496ca7 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -140,9 +140,16 @@ function walk( node.codegenNode.type === NodeTypes.VNODE_CALL && isArray(node.codegenNode.children) ) { - node.codegenNode.children = context.hoist( + const hoisted = context.hoist( createArrayExpression(node.codegenNode.children) ) + // #6978, #7138, #7114 + // a hoisted children array inside v-for can caused HMR errors since + // it might be mutated when mounting the v-for list + if (context.hmr) { + hoisted.content = `[...${hoisted.content}]` + } + node.codegenNode.children = hoisted } } diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index c4416dd45f7..ffa90ea1171 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => { export type SlotFnBuilder = ( slotProps: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, slotChildren: TemplateChildNode[], loc: SourceLocation ) => FunctionExpression -const buildClientSlotFn: SlotFnBuilder = (props, children, loc) => +const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => createFunctionExpression( props, children, @@ -149,7 +150,7 @@ export function buildSlots( slotsProperties.push( createObjectProperty( arg || createSimpleExpression('default', true), - buildSlotFn(exp, children, loc) + buildSlotFn(exp, undefined, children, loc) ) ) } @@ -201,11 +202,17 @@ export function buildSlots( hasDynamicSlots = true } - const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc) + const vFor = findDir(slotElement, 'for') + const slotFunction = buildSlotFn( + slotProps, + vFor?.exp, + slotChildren, + slotLoc + ) + // check if this slot is conditional (v-if/v-for) let vIf: DirectiveNode | undefined let vElse: DirectiveNode | undefined - let vFor: DirectiveNode | undefined if ((vIf = findDir(slotElement, 'if'))) { hasDynamicSlots = true dynamicSlots.push( @@ -257,7 +264,7 @@ export function buildSlots( createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc) ) } - } else if ((vFor = findDir(slotElement, 'for'))) { + } else if (vFor) { hasDynamicSlots = true const parseResult = vFor.parseResult || @@ -306,7 +313,7 @@ export function buildSlots( props: ExpressionNode | undefined, children: TemplateChildNode[] ) => { - const fn = buildSlotFn(props, children, loc) + const fn = buildSlotFn(props, undefined, children, loc) if (__COMPAT__ && context.compatConfig) { fn.isNonScopedSlot = true } diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 8bde209e292..4e6ea338bb9 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.6", + "version": "3.3.7", "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.6", - "@vue/compiler-core": "3.3.6" + "@vue/shared": "3.3.7", + "@vue/compiler-core": "3.3.7" } } diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 607654a952b..fc600f1a518 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1,3 +1,4 @@ +import { normalize } from 'node:path' import { Identifier } from '@babel/types' import { SFCScriptCompileOptions, parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' @@ -478,6 +479,33 @@ describe('resolveType', () => { expect(deps && [...deps]).toStrictEqual(Object.keys(files)) }) + test.runIf(process.platform === 'win32')('relative ts on Windows', () => { + const files = { + 'C:\\Test\\foo.ts': 'export type P = { foo: number }', + 'C:\\Test\\bar.d.ts': + 'type X = { bar: string }; export { X as Y };' + + // verify that we can parse syntax that is only valid in d.ts + 'export const baz: boolean' + } + const { props, deps } = resolve( + ` + import { P } from './foo' + import { Y as PP } from './bar' + defineProps

() + `, + files, + {}, + 'C:\\Test\\Test.vue' + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps].map(normalize)).toStrictEqual( + Object.keys(files).map(normalize) + ) + }) + // #8244 test('utility type in external file', () => { const files = { @@ -898,19 +926,20 @@ describe('resolveType', () => { function resolve( code: string, files: Record = {}, - options?: Partial + options?: Partial, + sourceFileName: string = '/Test.vue' ) { const { descriptor } = parse(``, { - filename: '/Test.vue' + filename: sourceFileName }) const ctx = new ScriptCompileContext(descriptor, { id: 'test', fs: { fileExists(file) { - return !!files[file] + return !!(files[file] ?? files[normalize(file)]) }, readFile(file) { - return files[file] + return files[file] ?? files[normalize(file)] } }, ...options diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts index 5b01d73d772..9fb72d7ad50 100644 --- a/packages/compiler-sfc/__tests__/cssVars.spec.ts +++ b/packages/compiler-sfc/__tests__/cssVars.spec.ts @@ -272,5 +272,73 @@ describe('CSS vars injection', () => { `export default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCssVars(_ctx => ({\n "xxxxxxxx-background": (_unref(background))\n}))` ) }) + + describe('skip codegen in SSR', () => { + test('script setup, inline', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + inlineTemplate: true, + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + + // #6926 + test('script, non-inline', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + inlineTemplate: false, + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + + test('normal script', () => { + const { content } = compileSFCScript( + `\n` + + ``, + { + templateOptions: { + ssr: true + } + } + ) + expect(content).not.toMatch(`_useCssVars`) + }) + }) }) }) diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 6a1fc8bba3a..550b5a7e927 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.6", + "version": "3.3.7", "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.23.0", - "@vue/compiler-core": "3.3.6", - "@vue/compiler-dom": "3.3.6", - "@vue/compiler-ssr": "3.3.6", - "@vue/reactivity-transform": "3.3.6", - "@vue/shared": "3.3.6", + "@vue/compiler-core": "3.3.7", + "@vue/compiler-dom": "3.3.7", + "@vue/compiler-ssr": "3.3.7", + "@vue/reactivity-transform": "3.3.7", + "@vue/shared": "3.3.7", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", "postcss": "^8.4.31", diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index cfcc607c72d..2a33f69936d 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -765,7 +765,7 @@ export function compileScript( if ( sfc.cssVars.length && // no need to do this when targeting SSR - !(options.inlineTemplate && options.templateOptions?.ssr) + !options.templateOptions?.ssr ) { ctx.helperImports.add(CSS_VARS_HELPER) ctx.helperImports.add('unref') diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index fbd100c9784..b036619c794 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -212,6 +212,7 @@ function doCompileTemplate({ slotted, sourceMap: true, ...compilerOptions, + hmr: !isProd, nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []), filename, onError: e => errors.push(e), diff --git a/packages/compiler-sfc/src/script/normalScript.ts b/packages/compiler-sfc/src/script/normalScript.ts index 76b25c66350..d0f16134273 100644 --- a/packages/compiler-sfc/src/script/normalScript.ts +++ b/packages/compiler-sfc/src/script/normalScript.ts @@ -55,7 +55,7 @@ export function processNormalScript( const s = new MagicString(content) rewriteDefaultAST(scriptAst.body, s, defaultVar) content = s.toString() - if (cssVars.length) { + if (cssVars.length && !ctx.options.templateOptions?.ssr) { content += genNormalScriptCssVarsCode( cssVars, bindings, diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 78581432366..215081dc0b7 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -778,7 +778,7 @@ function importSourceToScope( if (!resolved) { if (source.startsWith('.')) { // relative import - fast path - const filename = joinPaths(scope.filename, '..', source) + const filename = joinPaths(dirname(scope.filename), source) resolved = resolveExt(filename, fs) } else { // module or aliased import - use full TS resolution, only supported in Node diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 9391c01e37e..a8ea08a5349 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -181,11 +181,14 @@ describe('ssr: components', () => { }) test('v-for slot', () => { - expect( - compile(` - - `).code - ).toMatchInlineSnapshot(` + const { code } = compile(` + + `) + expect(code).not.toMatch(`_ctx.msg`) + expect(code).not.toMatch(`_ctx.key`) + expect(code).not.toMatch(`_ctx.index`) + expect(code).toMatch(`_ctx.bar`) + expect(code).toMatchInlineSnapshot(` "const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\") const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\") @@ -193,15 +196,15 @@ describe('ssr: components', () => { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [ - _renderList(_ctx.names, (key) => { + _renderList(_ctx.names, (key, index) => { return { name: key, fn: _withCtx(({ msg }, _push, _parent, _scopeId) => { if (_push) { - _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`) + _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`) } else { return [ - _createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */) + _createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */) ] } }) diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts new file mode 100644 index 00000000000..319b3902239 --- /dev/null +++ b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts @@ -0,0 +1,25 @@ +import { compile } from '../src' + +describe('transition', () => { + test('basic', () => { + expect(compile(`

foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`foo\`) + }" + `) + }) + + test('with appear', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index dc4933349f0..556c43f5971 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.6", + "version": "3.3.7", "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.6", - "@vue/compiler-dom": "3.3.6" + "@vue/shared": "3.3.7", + "@vue/compiler-dom": "3.3.7" } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index dc8c6a4ae4f..7a12cb29009 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -56,6 +56,10 @@ import { } from './ssrTransformTransitionGroup' import { isSymbol, isObject, isArray } from '@vue/shared' import { buildSSRProps } from './ssrTransformElement' +import { + ssrProcessTransition, + ssrTransformTransition +} from './ssrTransformTransition' // We need to construct the slot functions in the 1st pass to ensure proper // scope tracking, but the children of each slot cannot be processed until @@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { if (isSymbol(component)) { if (component === SUSPENSE) { return ssrTransformSuspense(node, context) - } - if (component === TRANSITION_GROUP) { + } else if (component === TRANSITION_GROUP) { return ssrTransformTransitionGroup(node, context) + } else if (component === TRANSITION) { + return ssrTransformTransition(node, context) } return // other built-in components: fallthrough } @@ -120,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { // fallback in case the child is render-fn based). Store them in an array // for later use. if (clonedNode.children.length) { - buildSlots(clonedNode, context, (props, children) => { - vnodeBranches.push(createVNodeSlotBranch(props, children, context)) + buildSlots(clonedNode, context, (props, vFor, children) => { + vnodeBranches.push( + createVNodeSlotBranch(props, vFor, children, context) + ) return createFunctionExpression(undefined) }) } @@ -145,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { const wipEntries: WIPSlotEntry[] = [] wipMap.set(node, wipEntries) - const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { + const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => { const param0 = (props && stringifyExpression(props)) || `_` const fn = createFunctionExpression( [param0, `_push`, `_parent`, `_scopeId`], @@ -216,9 +223,8 @@ export function ssrProcessComponent( if ((parent as WIPSlotEntry).type === WIP_SLOT) { context.pushStringPart(``) } - // #5351: filter out comment children inside transition if (component === TRANSITION) { - node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + return ssrProcessTransition(node, context) } processChildren(node, context) } @@ -273,6 +279,7 @@ const vnodeDirectiveTransforms = { function createVNodeSlotBranch( props: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, children: TemplateChildNode[], parentContext: TransformContext ): ReturnStatement { @@ -299,8 +306,8 @@ function createVNodeSlotBranch( tag: 'template', tagType: ElementTypes.TEMPLATE, isSelfClosing: false, - // important: provide v-slot="props" on the wrapper for proper - // scope analysis + // important: provide v-slot="props" and v-for="exp" on the wrapper for + // proper scope analysis props: [ { type: NodeTypes.DIRECTIVE, @@ -309,6 +316,14 @@ function createVNodeSlotBranch( arg: undefined, modifiers: [], loc: locStub + }, + { + type: NodeTypes.DIRECTIVE, + name: 'for', + exp: vForExp, + arg: undefined, + modifiers: [], + loc: locStub } ], children, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts index 207e9348eef..e7efbe1fb73 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts @@ -36,20 +36,24 @@ export function ssrTransformSuspense( wipSlots: [] } wipMap.set(node, wipEntry) - wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => { - const fn = createFunctionExpression( - [], - undefined, // no return, assign body later - true, // newline - false, // suspense slots are not treated as normal slots - loc - ) - wipEntry.wipSlots.push({ - fn, - children - }) - return fn - }).slots + wipEntry.slotsExp = buildSlots( + node, + context, + (_props, _vForExp, children, loc) => { + const fn = createFunctionExpression( + [], + undefined, // no return, assign body later + true, // newline + false, // suspense slots are not treated as normal slots + loc + ) + wipEntry.wipSlots.push({ + fn, + children + }) + return fn + } + ).slots } } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts new file mode 100644 index 00000000000..d09a806f7b0 --- /dev/null +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts @@ -0,0 +1,36 @@ +import { + ComponentNode, + findProp, + NodeTypes, + TransformContext +} from '@vue/compiler-dom' +import { processChildren, SSRTransformContext } from '../ssrCodegenTransform' + +const wipMap = new WeakMap() + +export function ssrTransformTransition( + node: ComponentNode, + context: TransformContext +) { + return () => { + const appear = findProp(node, 'appear', false, true) + wipMap.set(node, !!appear) + } +} + +export function ssrProcessTransition( + node: ComponentNode, + context: SSRTransformContext +) { + // #5351: filter out comment children inside transition + node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + + const appear = wipMap.get(node) + if (appear) { + context.pushStringPart(``) + } else { + processChildren(node, context, false, true) + } +} diff --git a/packages/dts-built-test/README.md b/packages/dts-built-test/README.md new file mode 100644 index 00000000000..8191d66e32e --- /dev/null +++ b/packages/dts-built-test/README.md @@ -0,0 +1,5 @@ +# dts built-package test + +This package is private and for testing only. It is used to verify edge cases for external libraries that build their types using Vue core types - e.g. Vuetify as in [#8376](https://github.com/vuejs/core/issues/8376). + +When running the `build-dts` task, this package's types are built alongside other packages. Then, during `test-dts-only` it is imported and used in [`packages/dts-test/built.test-d.ts`](https://github.com/vuejs/core/blob/main/packages/dts-test/built.test-d.ts) to verify that the built types work correctly. diff --git a/packages/dts-built-test/package.json b/packages/dts-built-test/package.json new file mode 100644 index 00000000000..fb332328fb9 --- /dev/null +++ b/packages/dts-built-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vue/dts-built-test", + "private": true, + "types": "dist/dts-built-test.d.ts", + "dependencies": { + "@vue/shared": "workspace:*", + "@vue/reactivity": "workspace:*", + "vue": "workspace:*" + }, + "version": "3.3.7" +} diff --git a/packages/dts-built-test/src/index.ts b/packages/dts-built-test/src/index.ts new file mode 100644 index 00000000000..2d9d4033254 --- /dev/null +++ b/packages/dts-built-test/src/index.ts @@ -0,0 +1,12 @@ +import { defineComponent } from 'vue' + +const _CustomPropsNotErased = defineComponent({ + props: {}, + setup() {} +}) + +// #8376 +export const CustomPropsNotErased = + _CustomPropsNotErased as typeof _CustomPropsNotErased & { + foo: string + } diff --git a/packages/dts-test/built.test-d.ts b/packages/dts-test/built.test-d.ts new file mode 100644 index 00000000000..8ac3e333f99 --- /dev/null +++ b/packages/dts-test/built.test-d.ts @@ -0,0 +1,13 @@ +import { CustomPropsNotErased } from '@vue/dts-built-test' +import { expectType, describe } from './utils' + +declare module 'vue' { + interface ComponentCustomProps { + custom?: number + } +} + +// #8376 - custom props should not be erased +describe('Custom Props not erased', () => { + expectType(new CustomPropsNotErased().$props.custom) +}) diff --git a/packages/dts-test/h.test-d.ts b/packages/dts-test/h.test-d.ts index 5c700800e94..f2e984b49b8 100644 --- a/packages/dts-test/h.test-d.ts +++ b/packages/dts-test/h.test-d.ts @@ -1,6 +1,7 @@ import { h, defineComponent, + DefineComponent, ref, Fragment, Teleport, @@ -231,3 +232,18 @@ describe('resolveComponent should work', () => { message: '1' }) }) + +// #5431 +describe('h should work with multiple types', () => { + const serializers = { + Paragraph: 'p', + Component: {} as Component, + DefineComponent: {} as DefineComponent + } + + const sampleComponent = serializers['' as keyof typeof serializers] + + h(sampleComponent) + h(sampleComponent, {}) + h(sampleComponent, {}, []) +}) diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index da8424e254c..ac246e704af 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -2,7 +2,8 @@ "name": "dts-test", "private": true, "dependencies": { - "vue": "workspace:*" + "vue": "workspace:*", + "@vue/dts-built-test": "workspace:*" }, - "version": "3.3.6" + "version": "3.3.7" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index 887fedabc6e..b9f8a74f353 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.6", + "version": "3.3.7", "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.23.0", - "@vue/compiler-core": "3.3.6", - "@vue/shared": "3.3.6", + "@vue/compiler-core": "3.3.7", + "@vue/shared": "3.3.7", "estree-walker": "^2.0.2", "magic-string": "^0.30.5" }, diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 635e6534abe..e34c7b31e40 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -243,6 +243,22 @@ describe('reactivity/effect', () => { expect(dummy).toBe(undefined) }) + it('should support manipulating an array while observing symbol keyed properties', () => { + const key = Symbol() + let dummy + const array: any = reactive([1, 2, 3]) + effect(() => (dummy = array[key])) + + expect(dummy).toBe(undefined) + array.pop() + array.shift() + array.splice(0, 1) + expect(dummy).toBe(undefined) + array[key] = 'value' + array.length = 0 + expect(dummy).toBe('value') + }) + it('should observe function valued properties', () => { const oldFunc = () => {} const newFunc = () => {} diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index ac49b4ec7d2..712bb26a0eb 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.6", + "version": "3.3.7", "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.6" + "@vue/shared": "3.3.7" } } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index bbac96a4b2a..c982dbd0b5a 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,5 +1,5 @@ import { TrackOpTypes, TriggerOpTypes } from './operations' -import { extend, isArray, isIntegerKey, isMap } from '@vue/shared' +import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' import { EffectScope, recordEffectScope } from './effectScope' import { createDep, @@ -324,7 +324,7 @@ export function trigger( } else if (key === 'length' && isArray(target)) { const newLength = Number(newValue) depsMap.forEach((dep, key) => { - if (key === 'length' || key >= newLength) { + if (key === 'length' || (!isSymbol(key) && key >= newLength)) { deps.push(dep) } }) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index db713a3f276..2e989e368a3 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -20,7 +20,7 @@ const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ registerRuntimeCompiler(compileToFunction) function compileToFunction(template: string) { - const { code } = baseCompile(template) + const { code } = baseCompile(template, { hoistStatic: true, hmr: true }) const render = new Function('Vue', code)( runtimeTest ) as InternalRenderFunction @@ -567,4 +567,40 @@ describe('hot module replacement', () => { rerender(parentId, compileToFunction(`2`)) expect(serializeInner(root)).toBe(`2`) }) + + // #6978, #7138, #7114 + test('hoisted children array inside v-for', () => { + const root = nodeOps.createElement('div') + const appId = 'test-app-id' + const App: ComponentOptions = { + __hmrId: appId, + render: compileToFunction( + `
+
1
+
+

2

+

3

` + ) + } + createRecord(appId, App) + + render(h(App), root) + expect(serializeInner(root)).toBe( + `
1
1

2

3

` + ) + + // move the

3

into the
1
+ rerender( + appId, + compileToFunction( + `
+
1

3

+
+

2

` + ) + ) + expect(serializeInner(root)).toBe( + `
1

3

1

3

2

` + ) + }) }) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index d3cfd47c6be..759804b97f1 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -18,10 +18,14 @@ import { createVNode, withDirectives, vModelCheckbox, - renderSlot + renderSlot, + Transition, + createCommentVNode, + vShow } from '@vue/runtime-dom' import { renderToString, SSRContext } from '@vue/server-renderer' -import { PatchFlags } from '../../shared/src' +import { PatchFlags } from '@vue/shared' +import { vShowOldKey } from '../../runtime-dom/src/directives/vShow' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -393,6 +397,28 @@ describe('SSR hydration', () => { ) }) + // #6152 + test('Teleport (disabled + as component root)', () => { + const { container } = mountWithHydration( + '
Parent fragment
Teleport content
', + () => [ + h('div', 'Parent fragment'), + h(() => + h(Teleport, { to: 'body', disabled: true }, [ + h('div', 'Teleport content') + ]) + ) + ] + ) + expect(document.body.innerHTML).toBe('') + expect(container.innerHTML).toBe( + '
Parent fragment
Teleport content
' + ) + expect( + `Hydration completed but contains mismatches.` + ).not.toHaveBeenWarned() + }) + test('Teleport (as component root)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport4' @@ -994,6 +1020,74 @@ describe('SSR hydration', () => { expect(`mismatch`).not.toHaveBeenWarned() }) + test('transition appear', () => { + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => h('div', 'foo') + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` +
+ foo +
+ `) + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-if', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => (show ? h('div', 'foo') : createCommentVNode('')) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-show', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => + withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` + + `) + expect((container.firstChild as any)[vShowOldKey]).toBe('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 6246a87e8f7..119d0f7080c 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -143,6 +143,7 @@ describe('scheduler', () => { queueJob(job1) // cb2 should execute before the job queueJob(cb2) + queueJob(cb3) } cb1.pre = true @@ -152,9 +153,60 @@ describe('scheduler', () => { cb2.pre = true cb2.id = 1 + const cb3 = () => { + calls.push('cb3') + } + cb3.pre = true + cb3.id = 1 + queueJob(cb1) await nextTick() - expect(calls).toEqual(['cb1', 'cb2', 'job1']) + expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1']) + }) + + it('should insert jobs after pre jobs with the same id', async () => { + const calls: string[] = [] + const job1 = () => { + calls.push('job1') + } + job1.id = 1 + job1.pre = true + const job2 = () => { + calls.push('job2') + queueJob(job5) + queueJob(job6) + } + job2.id = 2 + job2.pre = true + const job3 = () => { + calls.push('job3') + } + job3.id = 2 + job3.pre = true + const job4 = () => { + calls.push('job4') + } + job4.id = 3 + job4.pre = true + const job5 = () => { + calls.push('job5') + } + job5.id = 2 + const job6 = () => { + calls.push('job6') + } + job6.id = 2 + job6.pre = true + + // We need several jobs to test this properly, otherwise + // findInsertionIndex can yield the correct index by chance + queueJob(job4) + queueJob(job2) + queueJob(job3) + queueJob(job1) + + await nextTick() + expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4']) }) it('preFlushCb inside queueJob', async () => { diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 9d2c04d757f..a6335626bea 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.6", + "version": "3.3.7", "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.6", - "@vue/reactivity": "3.3.6" + "@vue/shared": "3.3.7", + "@vue/reactivity": "3.3.7" } } diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index dc575aafff9..b7ef1e07302 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -206,11 +206,9 @@ export type ComponentPublicInstance< > = { $: ComponentInternalInstance $data: D - $props: Prettify< - MakeDefaultsOptional extends true - ? Partial & Omit

- : P & PublicProps - > + $props: MakeDefaultsOptional extends true + ? Partial & Omit & PublicProps, keyof Defaults> + : Prettify

& PublicProps $attrs: Data $refs: Data $slots: UnwrapSlotsType diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 0fa07d9beec..3640733d734 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -491,10 +491,12 @@ function createSuspenseBoundary( container } = suspense + // if there's a transition happening we need to wait it to finish. + let delayEnter: boolean | null = false if (suspense.isHydrating) { suspense.isHydrating = false } else if (!resume) { - const delayEnter = + delayEnter = activeBranch && pendingBranch!.transition && pendingBranch!.transition.mode === 'out-in' @@ -502,6 +504,7 @@ function createSuspenseBoundary( activeBranch!.transition!.afterLeave = () => { if (pendingId === suspense.pendingId) { move(pendingBranch!, container, anchor, MoveType.ENTER) + queuePostFlushCb(effects) } } } @@ -538,8 +541,8 @@ function createSuspenseBoundary( } parent = parent.parent } - // no pending parent suspense, flush all jobs - if (!hasUnresolvedAncestor) { + // no pending parent suspense nor transition, flush all jobs + if (!hasUnresolvedAncestor && !delayEnter) { queuePostFlushCb(effects) } suspense.effects = [] diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index 73b27107b8b..4ca90262f2a 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -174,6 +174,14 @@ export function h

( children?: RawChildren | RawSlots ): VNode +// catch all types +export function h(type: string | Component, children?: RawChildren): VNode +export function h

( + type: string | Component

, + props?: (RawProps & P) | ({} extends P ? null : never), + children?: RawChildren | RawSlots +): VNode + // Actual implementation export function h(type: any, propsOrChildren?: any, children?: any): VNode { const l = arguments.length diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 89a00886332..4e91cb3d1cb 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -15,7 +15,7 @@ import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' -import { RendererInternals } from './renderer' +import { needTransition, RendererInternals } from './renderer' import { setRef } from './rendererTemplateRef' import { SuspenseImpl, @@ -146,7 +146,17 @@ export function createHydrationFunctions( break case Comment: if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { - nextNode = onMismatch() + if ((node as Element).tagName.toLowerCase() === 'template') { + const content = (vnode.el! as HTMLTemplateElement).content + .firstChild! + + // replace