Răsfoiți Sursa

Merge branch 'main' into edison/fix/12241

edison 11 luni în urmă
părinte
comite
ec7ff38d37

+ 3 - 0
.github/workflows/test.yml

@@ -104,5 +104,8 @@ jobs:
       - name: Run prettier
         run: pnpm run format-check
 
+      - name: Run tsc
+        run: pnpm run check
+
       - name: Run type declaration tests
         run: pnpm run test-dts

+ 12 - 12
package.json

@@ -1,7 +1,7 @@
 {
   "private": true,
   "version": "3.5.14",
-  "packageManager": "pnpm@10.9.0",
+  "packageManager": "pnpm@10.11.0",
   "type": "module",
   "scripts": {
     "dev": "node scripts/dev.js",
@@ -71,21 +71,21 @@
     "@rollup/plugin-replace": "5.0.4",
     "@swc/core": "^1.11.24",
     "@types/hash-sum": "^1.0.2",
-    "@types/node": "^22.14.1",
+    "@types/node": "^22.15.21",
     "@types/semver": "^7.7.0",
     "@types/serve-handler": "^6.1.4",
-    "@vitest/coverage-v8": "^3.1.3",
-    "@vitest/eslint-plugin": "^1.1.44",
+    "@vitest/coverage-v8": "^3.1.4",
+    "@vitest/eslint-plugin": "^1.2.0",
     "@vue/consolidate": "1.0.0",
     "conventional-changelog-cli": "^5.0.0",
     "enquirer": "^2.4.1",
     "esbuild": "^0.25.4",
     "esbuild-plugin-polyfill-node": "^0.3.0",
-    "eslint": "^9.25.1",
-    "eslint-plugin-import-x": "^4.11.0",
+    "eslint": "^9.27.0",
+    "eslint-plugin-import-x": "^4.12.2",
     "estree-walker": "catalog:",
     "jsdom": "^26.1.0",
-    "lint-staged": "^15.5.1",
+    "lint-staged": "^15.5.2",
     "lodash": "^4.17.21",
     "magic-string": "^0.30.17",
     "markdown-table": "^3.0.4",
@@ -95,21 +95,21 @@
     "prettier": "^3.5.3",
     "pretty-bytes": "^6.1.1",
     "pug": "^3.0.3",
-    "puppeteer": "~24.8.2",
+    "puppeteer": "~24.9.0",
     "rimraf": "^6.0.1",
-    "rollup": "^4.40.2",
+    "rollup": "^4.41.0",
     "rollup-plugin-dts": "^6.2.1",
     "rollup-plugin-esbuild": "^6.2.1",
     "rollup-plugin-polyfill-node": "^0.13.0",
-    "semver": "^7.7.1",
+    "semver": "^7.7.2",
     "serve": "^14.2.4",
     "serve-handler": "^6.1.6",
     "simple-git-hooks": "^2.13.0",
     "todomvc-app-css": "^2.4.3",
     "tslib": "^2.8.1",
     "typescript": "~5.6.2",
-    "typescript-eslint": "^8.31.1",
+    "typescript-eslint": "^8.32.1",
     "vite": "catalog:",
-    "vitest": "^3.1.3"
+    "vitest": "^3.1.4"
   }
 }

+ 29 - 0
packages-private/dts-test/defineComponent.test-d.tsx

@@ -20,6 +20,9 @@ import { type IsAny, type IsUnion, describe, expectType } from './utils'
 describe('with object props', () => {
   interface ExpectedProps {
     a?: number | undefined
+    aa: number
+    aaa: number | null
+    aaaa: number | undefined
     b: string
     e?: Function
     h: boolean
@@ -53,6 +56,19 @@ describe('with object props', () => {
 
   const props = {
     a: Number,
+    aa: {
+      type: Number as PropType<number | undefined>,
+      default: 1,
+    },
+    aaa: {
+      type: Number as PropType<number | null>,
+      default: 1,
+    },
+    aaaa: {
+      type: Number as PropType<number | undefined>,
+      // `as const` prevents widening to `boolean` (keeps literal `true` type)
+      required: true as const,
+    },
     // required should make property non-void
     b: {
       type: String,
@@ -146,6 +162,13 @@ describe('with object props', () => {
     setup(props) {
       // type assertion. See https://github.com/SamVerschueren/tsd
       expectType<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['aa']>(props.aa)
+      expectType<ExpectedProps['aaa']>(props.aaa)
+
+      // @ts-expect-error should included `undefined`
+      expectType<number>(props.aaaa)
+      expectType<ExpectedProps['aaaa']>(props.aaaa)
+
       expectType<ExpectedProps['b']>(props.b)
       expectType<ExpectedProps['e']>(props.e)
       expectType<ExpectedProps['h']>(props.h)
@@ -198,6 +221,8 @@ describe('with object props', () => {
     render() {
       const props = this.$props
       expectType<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['aa']>(props.aa)
+      expectType<ExpectedProps['aaa']>(props.aaa)
       expectType<ExpectedProps['b']>(props.b)
       expectType<ExpectedProps['e']>(props.e)
       expectType<ExpectedProps['h']>(props.h)
@@ -225,6 +250,8 @@ describe('with object props', () => {
 
       // should also expose declared props on `this`
       expectType<ExpectedProps['a']>(this.a)
+      expectType<ExpectedProps['aa']>(this.aa)
+      expectType<ExpectedProps['aaa']>(this.aaa)
       expectType<ExpectedProps['b']>(this.b)
       expectType<ExpectedProps['e']>(this.e)
       expectType<ExpectedProps['h']>(this.h)
@@ -269,6 +296,7 @@ describe('with object props', () => {
   expectType<JSX.Element>(
     <MyComponent
       a={1}
+      aaaa={1}
       b="b"
       bb="bb"
       e={() => {}}
@@ -295,6 +323,7 @@ describe('with object props', () => {
 
   expectType<Component>(
     <MyComponent
+      aaaa={1}
       b="b"
       dd={{ n: 1 }}
       ddd={['ddd']}

+ 5 - 4
packages/compiler-core/src/transforms/transformElement.ts

@@ -594,11 +594,9 @@ export function buildProps(
         hasDynamicKeys = true
         if (exp) {
           if (isVBind) {
-            // #10696 in case a v-bind object contains ref
-            pushRefVForMarker()
-            // have to merge early for compat build check
-            pushMergeArg()
             if (__COMPAT__) {
+              // have to merge early for compat build check
+              pushMergeArg()
               // 2.x v-bind object order compat
               if (__DEV__) {
                 const hasOverridableKeys = mergeArgs.some(arg => {
@@ -641,6 +639,9 @@ export function buildProps(
               }
             }
 
+            // #10696 in case a v-bind object contains ref
+            pushRefVForMarker()
+            pushMergeArg()
             mergeArgs.push(exp)
           } else {
             // v-on="obj" -> toHandlers(obj)

+ 3 - 3
packages/compiler-sfc/__tests__/compileScript.spec.ts

@@ -1007,7 +1007,7 @@ describe('SFC compile <script setup>', () => {
       expect(() =>
         compile(`<script setup>
         let bar = 1
-        defineModel({
+        const model = defineModel({
           default: () => bar
         })
         </script>`),
@@ -1017,7 +1017,7 @@ describe('SFC compile <script setup>', () => {
       expect(() =>
         compile(`<script setup>
         const bar = 1
-        defineModel({
+        const model = defineModel({
           default: () => bar
         })
         </script>`),
@@ -1027,7 +1027,7 @@ describe('SFC compile <script setup>', () => {
       expect(() =>
         compile(`<script setup>
         let bar = 1
-        defineModel({
+        const model = defineModel({
           get: () => bar,
           set: () => bar
         })

+ 12 - 0
packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts

@@ -269,4 +269,16 @@ describe('defineModel()', () => {
       modelValue: BindingTypes.SETUP_REF,
     })
   })
+
+  test('error when defineModel is not assigned to a variable', () => {
+    expect(() =>
+      compile(`
+        <script setup>
+        defineModel()
+        </script>
+      `),
+    ).toThrow(
+      'defineModel() must be assigned to a variable. For example: const model = defineModel()',
+    )
+  })
 })

+ 25 - 1
packages/compiler-sfc/__tests__/compileStyle.spec.ts

@@ -529,7 +529,31 @@ describe('SFC style preprocessors', () => {
       }"
     `)
     expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
-      ".foo[data-v-test] * { color: red;
+      ".foo[data-v-test] [data-v-test] { color: red;
+      }"
+    `)
+    expect(compileScoped(`.foo :active { color: red; }`))
+      .toMatchInlineSnapshot(`
+      ".foo[data-v-test] :active { color: red;
+      }"
+    `)
+    expect(compileScoped(`.foo *:active { color: red; }`))
+      .toMatchInlineSnapshot(`
+      ".foo[data-v-test] [data-v-test]:active { color: red;
+      }"
+    `)
+    expect(compileScoped(`.foo * .bar { color: red; }`)).toMatchInlineSnapshot(`
+      ".foo * .bar[data-v-test] { color: red;
+      }"
+    `)
+    expect(compileScoped(`:last-child * { color: red; }`))
+      .toMatchInlineSnapshot(`
+      "[data-v-test]:last-child [data-v-test] { color: red;
+      }"
+    `)
+    expect(compileScoped(`:last-child *:active { color: red; }`))
+      .toMatchInlineSnapshot(`
+      "[data-v-test]:last-child [data-v-test]:active { color: red;
       }"
     `)
   })

+ 1 - 1
packages/compiler-sfc/package.json

@@ -62,6 +62,6 @@
     "postcss-modules": "^6.0.1",
     "postcss-selector-parser": "^7.1.0",
     "pug": "^3.0.3",
-    "sass": "^1.86.3"
+    "sass": "^1.89.0"
   }
 }

+ 1 - 1
packages/compiler-sfc/src/compileScript.ts

@@ -1324,7 +1324,7 @@ export function mergeSourceMaps(
         },
         original: {
           line: m.originalLine,
-          column: m.originalColumn,
+          column: m.originalColumn!,
         },
         source: m.source,
         name: m.name,

+ 7 - 0
packages/compiler-sfc/src/script/defineModel.ts

@@ -22,6 +22,13 @@ export function processDefineModel(
     return false
   }
 
+  if (!declId) {
+    ctx.error(
+      'defineModel() must be assigned to a variable. For example: const model = defineModel()',
+      node,
+    )
+  }
+
   ctx.hasDefineModelCall = true
 
   const type =

+ 20 - 3
packages/compiler-sfc/src/style/pluginScoped.ts

@@ -102,6 +102,7 @@ function rewriteSelector(
   slotted = false,
 ) {
   let node: selectorParser.Node | null = null
+  let starNode: selectorParser.Node | null = null
   let shouldInject = !deep
   // find the last child node to insert attribute selector
   selector.each(n => {
@@ -216,12 +217,13 @@ function rewriteSelector(
           return false
         }
       }
-      // .foo * -> .foo[xxxxxxx] *
-      if (node) return
+      // store the universal selector so it can be rewritten later
+      // .foo * -> .foo[xxxxxxx] [xxxxxxx]
+      starNode = n
     }
 
     if (
-      (n.type !== 'pseudo' && n.type !== 'combinator') ||
+      (n.type !== 'pseudo' && n.type !== 'combinator' && n.type !== 'universal') ||
       (isPseudoClassIsOrWhere(n) &&
         (!node ||
           n.nodes.some(
@@ -233,6 +235,7 @@ function rewriteSelector(
           )))
     ) {
       node = n
+      starNode = null
     }
   })
 
@@ -279,6 +282,20 @@ function rewriteSelector(
         quoteMark: `"`,
       }),
     )
+    // Used for trailing universal selectors (#12906)
+    // `.foo * {}` -> `.foo[xxxxxxx] [xxxxxxx] {}`
+    if (starNode) {
+      selector.insertBefore(
+        starNode,
+        selectorParser.attribute({
+          attribute: idToAdd,
+          value: idToAdd,
+          raws: {},
+          quoteMark: `"`,
+        }),
+      )
+      selector.removeChild(starNode)
+    }
   }
 }
 

+ 3 - 1
packages/runtime-core/src/componentProps.ts

@@ -143,7 +143,9 @@ type InferPropType<T, NullAsAny = true> = [T] extends [null]
 export type ExtractPropTypes<O> = {
   // use `keyof Pick<O, RequiredKeys<O>>` instead of `RequiredKeys<O>` to
   // support IDE features
-  [K in keyof Pick<O, RequiredKeys<O>>]: InferPropType<O[K]>
+  [K in keyof Pick<O, RequiredKeys<O>>]: O[K] extends { default: any }
+    ? Exclude<InferPropType<O[K]>, undefined>
+    : InferPropType<O[K]>
 } & {
   // use `keyof Pick<O, OptionalKeys<O>>` instead of `OptionalKeys<O>` to
   // support IDE features

+ 116 - 0
packages/runtime-dom/__tests__/customElement.spec.ts

@@ -444,6 +444,36 @@ describe('defineCustomElement', () => {
       const e = container.childNodes[0] as VueElement
       expect(e.shadowRoot!.innerHTML).toBe('hello')
     })
+
+    test('prop types validation', async () => {
+      const E = defineCustomElement({
+        props: {
+          num: {
+            type: [Number, String],
+          },
+          bool: {
+            type: Boolean,
+          },
+        },
+        render() {
+          return h('div', [
+            h('span', [`${this.num} is ${typeof this.num}`]),
+            h('span', [`${this.bool} is ${typeof this.bool}`]),
+          ])
+        },
+      })
+
+      customElements.define('my-el-with-type-props', E)
+      render(h('my-el-with-type-props', { num: 1, bool: true }), container)
+      const e = container.childNodes[0] as VueElement
+      // @ts-expect-error
+      expect(e.num).toBe(1)
+      // @ts-expect-error
+      expect(e.bool).toBe(true)
+      expect(e.shadowRoot!.innerHTML).toBe(
+        '<div><span>1 is number</span><span>true is boolean</span></div>',
+      )
+    })
   })
 
   describe('attrs', () => {
@@ -1226,6 +1256,92 @@ describe('defineCustomElement', () => {
       expect(target.innerHTML).toBe(`<span>default</span>`)
       app.unmount()
     })
+
+    test('toggle nested custom element with shadowRoot: false', async () => {
+      customElements.define(
+        'my-el-child-shadow-false',
+        defineCustomElement(
+          {
+            render(ctx: any) {
+              return h('div', null, [renderSlot(ctx.$slots, 'default')])
+            },
+          },
+          { shadowRoot: false },
+        ),
+      )
+      const ChildWrapper = {
+        render() {
+          return h('my-el-child-shadow-false', null, 'child')
+        },
+      }
+
+      customElements.define(
+        'my-el-parent-shadow-false',
+        defineCustomElement(
+          {
+            props: {
+              isShown: { type: Boolean, required: true },
+            },
+            render(ctx: any, _: any, $props: any) {
+              return $props.isShown
+                ? h('div', { key: 0 }, [renderSlot(ctx.$slots, 'default')])
+                : null
+            },
+          },
+          { shadowRoot: false },
+        ),
+      )
+      const ParentWrapper = {
+        props: {
+          isShown: { type: Boolean, required: true },
+        },
+        render(ctx: any, _: any, $props: any) {
+          return h('my-el-parent-shadow-false', { isShown: $props.isShown }, [
+            renderSlot(ctx.$slots, 'default'),
+          ])
+        },
+      }
+
+      const isShown = ref(true)
+      const App = {
+        render() {
+          return h(ParentWrapper, { isShown: isShown.value } as any, {
+            default: () => [h(ChildWrapper)],
+          })
+        },
+      }
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+      const app = createApp(App)
+      app.mount(container)
+      expect(container.innerHTML).toBe(
+        `<my-el-parent-shadow-false is-shown="" data-v-app="">` +
+          `<div>` +
+          `<my-el-child-shadow-false data-v-app="">` +
+          `<div>child</div>` +
+          `</my-el-child-shadow-false>` +
+          `</div>` +
+          `</my-el-parent-shadow-false>`,
+      )
+
+      isShown.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<my-el-parent-shadow-false data-v-app=""><!----></my-el-parent-shadow-false>`,
+      )
+
+      isShown.value = true
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<my-el-parent-shadow-false data-v-app="" is-shown="">` +
+          `<div>` +
+          `<my-el-child-shadow-false data-v-app="">` +
+          `<div>child</div>` +
+          `</my-el-child-shadow-false>` +
+          `</div>` +
+          `</my-el-parent-shadow-false>`,
+      )
+    })
   })
 
   describe('helpers', () => {

+ 4 - 14
packages/runtime-dom/src/apiCustomElement.ts

@@ -269,18 +269,14 @@ export class VueElement
         this._root = this
       }
     }
-
-    if (!(this._def as ComponentOptions).__asyncLoader) {
-      // for sync component defs we can immediately resolve props
-      this._resolveProps(this._def)
-    }
   }
 
   connectedCallback(): void {
     // avoid resolving component if it's not connected
     if (!this.isConnected) return
 
-    if (!this.shadowRoot) {
+    // avoid re-parsing slots if already resolved
+    if (!this.shadowRoot && !this._resolved) {
       this._parseSlots()
     }
     this._connected = true
@@ -298,8 +294,7 @@ export class VueElement
 
     if (!this._instance) {
       if (this._resolved) {
-        this._setParent()
-        this._update()
+        this._mount(this._def)
       } else {
         if (parent && parent._pendingResolve) {
           this._pendingResolve = parent._pendingResolve.then(() => {
@@ -391,12 +386,7 @@ export class VueElement
         }
       }
       this._numberProps = numberProps
-
-      if (isAsync) {
-        // defining getter/setters on prototype
-        // for sync defs, this already happened in the constructor
-        this._resolveProps(def)
-      }
+      this._resolveProps(def)
 
       // apply CSS
       if (this.shadowRoot) {

+ 1 - 1
packages/server-renderer/__tests__/webStream.spec.ts

@@ -49,7 +49,7 @@ test('pipeToWebWritable', async () => {
   }
 
   const { readable, writable } = new TransformStream()
-  pipeToWebWritable(createApp(App), {}, writable)
+  pipeToWebWritable(createApp(App), {}, writable as any)
 
   const reader = readable.getReader()
   const decoder = new TextDecoder()

+ 10 - 0
packages/vue-compat/__tests__/compiler.spec.ts

@@ -93,6 +93,16 @@ test('COMPILER_V_BIND_OBJECT_ORDER', () => {
   ).toHaveBeenWarned()
 })
 
+test('should not warn COMPILER_V_BIND_OBJECT_ORDER work with vFor', () => {
+  const vm = new Vue({
+    template: `<div><div v-bind="{ id: 'bar', class: 'baz' }" v-for="item in 5" /></div>`,
+  }).$mount()
+  expect(vm.$el).toBeInstanceOf(HTMLDivElement)
+  expect(
+    CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
+  ).not.toHaveBeenWarned()
+})
+
 test('COMPILER_V_ON_NATIVE', () => {
   const spy = vi.fn()
   const vm = new Vue({

Fișier diff suprimat deoarece este prea mare
+ 227 - 204
pnpm-lock.yaml


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff