Explorar el Código

Merge branch 'main' into edison/fix/inDOMSlotName

edison hace 3 meses
padre
commit
bfa00cc03a

+ 48 - 0
packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap

@@ -192,6 +192,32 @@ return () => {}
 }"
 `;
 
+exports[`sfc reactive props destructure > for-of loop variable shadowing 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+interface Props {
+        msg: string;
+        input: string[];
+      }
+      
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    msg: { type: String, required: true },
+    input: { type: Array, required: true }
+  },
+  setup(__props: any) {
+
+      
+      for (const msg of __props.input) {
+        console.log('MESSAGE', msg);
+      }
+      console.log('NOT FAIL', { msg: __props.msg });
+      
+return () => {}
+}
+
+})"
+`;
+
 exports[`sfc reactive props destructure > handle function parameters with same name as destructured props 1`] = `
 "
 export default {
@@ -309,6 +335,28 @@ return (_ctx, _cache) => {
 }"
 `;
 
+exports[`sfc reactive props destructure > regular for loop variable shadowing 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    i: { type: Number, required: true },
+    len: { type: Number, required: true }
+  },
+  setup(__props: any) {
+
+      
+      for (let i = 0; i < __props.len; i++) {
+        console.log('INDEX', i);
+      }
+      console.log('AFTER', { i: __props.i });
+      
+return () => {}
+}
+
+})"
+`;
+
 exports[`sfc reactive props destructure > rest spread 1`] = `
 "import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
 

+ 40 - 0
packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts

@@ -68,6 +68,46 @@ describe('sfc reactive props destructure', () => {
     })
   })
 
+  test('for-of loop variable shadowing', () => {
+    const { content } = compile(`
+      <script setup lang="ts">
+      interface Props {
+        msg: string;
+        input: string[];
+      }
+      const { msg, input } = defineProps<Props>();
+      for (const msg of input) {
+        console.log('MESSAGE', msg);
+      }
+      console.log('NOT FAIL', { msg });
+      </script>
+    `)
+    // inside loop: should use local variable
+    expect(content).toMatch(`for (const msg of __props.input)`)
+    expect(content).toMatch(`console.log('MESSAGE', msg)`)
+    // after loop: should restore to prop reference
+    expect(content).toMatch(`console.log('NOT FAIL', { msg: __props.msg })`)
+    assertCode(content)
+  })
+
+  test('regular for loop variable shadowing', () => {
+    const { content } = compile(`
+      <script setup lang="ts">
+      const { i, len } = defineProps<{ i: number; len: number }>();
+      for (let i = 0; i < len; i++) {
+        console.log('INDEX', i);
+      }
+      console.log('AFTER', { i });
+      </script>
+    `)
+    // inside loop: should use local variable
+    expect(content).toMatch(`for (let i = 0; i < __props.len; i++)`)
+    expect(content).toMatch(`console.log('INDEX', i)`)
+    // after loop: should restore to prop reference
+    expect(content).toMatch(`console.log('AFTER', { i: __props.i })`)
+    assertCode(content)
+  })
+
   test('default values w/ array runtime declaration', () => {
     const { content } = compile(`
       <script setup>

+ 78 - 0
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

@@ -1136,6 +1136,64 @@ describe('resolveType', () => {
       expect(deps && [...deps]).toStrictEqual(Object.keys(files))
     })
 
+    test('relative import with indexed access type', () => {
+      const files = {
+        '/foo.ts': `
+          type Booleanish = boolean | 'true' | 'false';
+          export interface InputHTMLAttributes {
+            required?: Booleanish | undefined;
+          }
+        `,
+      }
+      const { props, deps } = resolve(
+        `
+        import { InputHTMLAttributes } from './foo.ts'
+        type ImportedType = InputHTMLAttributes['required']
+        defineProps<{
+          required: ImportedType,
+        }>()
+      `,
+        files,
+      )
+      expect(props).toStrictEqual({
+        required: ['Boolean', 'String', 'Unknown'],
+      })
+      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
+    })
+
+    test('relative import with indexed access type with unresolvable extends', () => {
+      const files = {
+        '/foo.ts': `
+          type EventHandlers<E> = {
+            [K in keyof E]?: E[K] extends (...args: any) => any 
+            ? E[K] 
+            : (payload: E[K]) => void;
+          };
+          export interface Events {
+            onCopy: ClipboardEvent;
+          }
+          type Booleanish = boolean | 'true' | 'false';
+          export interface InputHTMLAttributes extends EventHandlers<Events>{
+            required?: Booleanish | undefined;
+          }
+        `,
+      }
+      const { props, deps } = resolve(
+        `
+        import { InputHTMLAttributes } from './foo.ts'
+        type ImportedType = InputHTMLAttributes['required']
+        defineProps<{
+          required: ImportedType,
+        }>()
+      `,
+        files,
+      )
+      expect(props).toStrictEqual({
+        required: ['Boolean', 'String', 'Unknown'],
+      })
+      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
+    })
+
     // #8339
     test('relative, .js import', () => {
       const files = {
@@ -1467,6 +1525,26 @@ describe('resolveType', () => {
       })
     })
 
+    test('declare global with indexed access type', () => {
+      const files = {
+        '/global.d.ts': `
+          declare global {
+            type Options = {
+              code: {
+                selected: boolean
+              }
+            }
+          }`,
+      }
+      const { props } = resolve(`defineProps<Options["code"]>()`, files, {
+        globalTypeFiles: Object.keys(files),
+      })
+
+      expect(props).toStrictEqual({
+        selected: ['Boolean'],
+      })
+    })
+
     // #9871
     test('shared generics with different args', () => {
       const files = {

+ 21 - 6
packages/compiler-sfc/src/script/definePropsDestructure.ts

@@ -147,11 +147,6 @@ export function transformDestructuredProps(
       ) {
         if (stmt.declare || !stmt.id) continue
         registerLocalBinding(stmt.id)
-      } else if (
-        (stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
-        stmt.left.type === 'VariableDeclaration'
-      ) {
-        walkVariableDeclaration(stmt.left)
       } else if (
         stmt.type === 'ExportNamedDeclaration' &&
         stmt.declaration &&
@@ -269,6 +264,23 @@ export function transformDestructuredProps(
         return
       }
 
+      // for loops: loop variable should be scoped to the loop
+      if (
+        node.type === 'ForOfStatement' ||
+        node.type === 'ForInStatement' ||
+        node.type === 'ForStatement'
+      ) {
+        pushScope()
+        const varDecl = node.type === 'ForStatement' ? node.init : node.left
+        if (varDecl && varDecl.type === 'VariableDeclaration') {
+          walkVariableDeclaration(varDecl)
+        }
+        if (node.body.type === 'BlockStatement') {
+          walkScope(node.body)
+        }
+        return
+      }
+
       // non-function block scopes
       if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
         pushScope()
@@ -292,7 +304,10 @@ export function transformDestructuredProps(
       if (
         (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
         isFunctionType(node) ||
-        node.type === 'CatchClause'
+        node.type === 'CatchClause' ||
+        node.type === 'ForOfStatement' ||
+        node.type === 'ForInStatement' ||
+        node.type === 'ForStatement'
       ) {
         popScope()
       }

+ 54 - 13
packages/compiler-sfc/src/script/resolveType.ts

@@ -74,6 +74,7 @@ export type SimpleTypeResolveContext = Pick<
 
   // utils
   | 'error'
+  | 'warn'
   | 'helper'
   | 'getString'
 
@@ -95,7 +96,12 @@ export type SimpleTypeResolveContext = Pick<
     options: SimpleTypeResolveOptions
   }
 
-export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
+export type TypeResolveContext = (
+  | ScriptCompileContext
+  | SimpleTypeResolveContext
+) & {
+  silentOnExtendsFailure?: boolean
+}
 
 type Import = Pick<ImportBinding, 'source' | 'imported'>
 
@@ -429,16 +435,21 @@ function resolveInterfaceMembers(
           ;(base.calls || (base.calls = [])).push(...calls)
         }
       } catch (e) {
-        ctx.error(
-          `Failed to resolve extends base type.\nIf this previously worked in 3.2, ` +
-            `you can instruct the compiler to ignore this extend by adding ` +
-            `/* @vue-ignore */ before it, for example:\n\n` +
-            `interface Props extends /* @vue-ignore */ Base {}\n\n` +
-            `Note: both in 3.2 or with the ignore, the properties in the base ` +
-            `type are treated as fallthrough attrs at runtime.`,
-          ext,
-          scope,
-        )
+        // when called from inferRuntimeType context, silently ignore extends
+        // resolution failure so that properties defined in the interface can
+        // still be correctly resolved
+        if (!ctx.silentOnExtendsFailure) {
+          ctx.error(
+            `Failed to resolve extends base type.\nIf this previously worked in 3.2, ` +
+              `you can instruct the compiler to ignore this extend by adding ` +
+              `/* @vue-ignore */ before it, for example:\n\n` +
+              `interface Props extends /* @vue-ignore */ Base {}\n\n` +
+              `Note: both in 3.2 or with the ignore, the properties in the base ` +
+              `type are treated as fallthrough attrs at runtime.`,
+            ext,
+            scope,
+          )
+        }
       }
     }
   }
@@ -1388,6 +1399,18 @@ function recordType(
     case 'TSInterfaceDeclaration':
     case 'TSEnumDeclaration':
     case 'TSModuleDeclaration': {
+      // Handle `declare global { ... }` blocks by recursively processing their contents
+      if (node.type === 'TSModuleDeclaration' && node.global) {
+        const body = node.body as TSModuleBlock
+        for (const s of body.body) {
+          if (s.type === 'ExportNamedDeclaration' && s.declaration) {
+            recordType(s.declaration, types, declares)
+          } else {
+            recordType(s, types, declares)
+          }
+        }
+        break
+      }
       const id = overwriteId || getId(node.id)
       let existing = types[id]
       if (existing) {
@@ -1519,6 +1542,10 @@ export function inferRuntimeType(
     return [UNKNOWN_TYPE]
   }
 
+  // set flag to silence extends resolution errors in this context
+  const prevSilent = ctx.silentOnExtendsFailure
+  ctx.silentOnExtendsFailure = true
+
   try {
     switch (node.type) {
       case 'TSStringKeyword':
@@ -1886,6 +1913,8 @@ export function inferRuntimeType(
     }
   } catch (e) {
     // always soft fail on failed runtime type inference
+  } finally {
+    ctx.silentOnExtendsFailure = prevSilent
   }
   return [UNKNOWN_TYPE] // no runtime check
 }
@@ -1898,13 +1927,25 @@ function flattenTypes(
   typeParameters: Record<string, Node> | undefined = undefined,
 ): string[] {
   if (types.length === 1) {
-    return inferRuntimeType(ctx, types[0], scope, isKeyOf, typeParameters)
+    return inferRuntimeType(
+      ctx,
+      types[0],
+      (types[0] as MaybeWithScope)._ownerScope || scope,
+      isKeyOf,
+      typeParameters,
+    )
   }
   return [
     ...new Set(
       ([] as string[]).concat(
         ...types.map(t =>
-          inferRuntimeType(ctx, t, scope, isKeyOf, typeParameters),
+          inferRuntimeType(
+            ctx,
+            t,
+            (t as MaybeWithScope)._ownerScope || scope,
+            isKeyOf,
+            typeParameters,
+          ),
         ),
       ),
     ),

+ 25 - 0
packages/reactivity/__tests__/collections/Map.spec.ts

@@ -471,5 +471,30 @@ describe('reactivity/collections', () => {
       const result = map.set('a', 'a')
       expect(result).toBe(map)
     })
+
+    it('should wrapped iterator inherit all iterator properties', () => {
+      const raw = new Map([['key', 'value']])
+      const map = reactive(raw)
+
+      const rawIterator = raw.entries()
+      const wrappedIterator = map.entries()
+
+      // Wrapped iterator should have the same properties as original iterator
+      expect(typeof wrappedIterator.next).toBe('function')
+      expect(typeof wrappedIterator[Symbol.iterator]).toBe('function')
+      expect(wrappedIterator[Symbol.iterator]()).toBe(wrappedIterator)
+
+      // Check inherited iterator helper methods if they exist on the original
+      for (const key of Object.getOwnPropertyNames(
+        Object.getPrototypeOf(rawIterator),
+      )) {
+        expect(key in wrappedIterator).toBe(true)
+      }
+      for (const key of Object.getOwnPropertySymbols(
+        Object.getPrototypeOf(rawIterator),
+      )) {
+        expect(key in wrappedIterator).toBe(true)
+      }
+    })
   })
 })

+ 15 - 15
packages/reactivity/src/collectionHandlers.ts

@@ -55,22 +55,22 @@ function createIterableMethod(
       )
     // return a wrapped iterator which returns observed versions of the
     // values emitted from the real iterator
-    return {
-      // iterator protocol
-      next() {
-        const { value, done } = innerIterator.next()
-        return done
-          ? { value, done }
-          : {
-              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
-              done,
-            }
-      },
-      // iterable protocol
-      [Symbol.iterator]() {
-        return this
+    return extend(
+      // inheriting all iterator properties
+      Object.create(innerIterator),
+      {
+        // iterator protocol
+        next() {
+          const { value, done } = innerIterator.next()
+          return done
+            ? { value, done }
+            : {
+                value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
+                done,
+              }
+        },
       },
-    }
+    )
   }
 }
 

+ 1 - 0
packages/reactivity/src/computed.ts

@@ -194,6 +194,7 @@ export function computed<T, S = T>(
   options: WritableComputedOptions<T, S>,
   debugOptions?: DebuggerOptions,
 ): WritableComputedRef<T, S>
+/*@__NO_SIDE_EFFECTS__*/
 export function computed<T>(
   getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
   debugOptions?: DebuggerOptions,

+ 9 - 0
packages/reactivity/src/reactive.ts

@@ -89,6 +89,7 @@ export type Reactive<T> = UnwrapNestedRefs<T> &
  * @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
  */
 export function reactive<T extends object>(target: T): Reactive<T>
+/*@__NO_SIDE_EFFECTS__*/
 export function reactive(target: object) {
   // if trying to observe a readonly proxy, return the readonly version.
   if (isReadonly(target)) {
@@ -137,6 +138,7 @@ export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
  * @param target - The source object.
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreactive}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function shallowReactive<T extends object>(
   target: T,
 ): ShallowReactive<T> {
@@ -202,6 +204,7 @@ export type DeepReadonly<T> = T extends Builtin
  * @param target - The source object.
  * @see {@link https://vuejs.org/api/reactivity-core.html#readonly}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function readonly<T extends object>(
   target: T,
 ): DeepReadonly<UnwrapNestedRefs<T>> {
@@ -244,6 +247,7 @@ export function readonly<T extends object>(
  * @param target - The source object.
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreadonly}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function shallowReadonly<T extends object>(target: T): Readonly<T> {
   return createReactiveObject(
     target,
@@ -315,6 +319,7 @@ function createReactiveObject(
  * @param value - The value to check.
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#isreactive}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function isReactive(value: unknown): boolean {
   if (isReadonly(value)) {
     return isReactive((value as Target)[ReactiveFlags.RAW])
@@ -333,10 +338,12 @@ export function isReactive(value: unknown): boolean {
  * @param value - The value to check.
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#isreadonly}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function isReadonly(value: unknown): boolean {
   return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
 }
 
+/*@__NO_SIDE_EFFECTS__*/
 export function isShallow(value: unknown): boolean {
   return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
 }
@@ -348,6 +355,7 @@ export function isShallow(value: unknown): boolean {
  * @param value - The value to check.
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function isProxy(value: any): boolean {
   return value ? !!value[ReactiveFlags.RAW] : false
 }
@@ -375,6 +383,7 @@ export function isProxy(value: any): boolean {
  * @param observed - The object for which the "raw" value is requested.
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#toraw}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function toRaw<T>(observed: T): T {
   const raw = observed && (observed as Target)[ReactiveFlags.RAW]
   return raw ? toRaw(raw) : observed

+ 5 - 0
packages/reactivity/src/ref.ts

@@ -43,6 +43,7 @@ export interface Ref<T = any, S = T> {
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#isref}
  */
 export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
+/*@__NO_SIDE_EFFECTS__*/
 export function isRef(r: any): r is Ref {
   return r ? r[ReactiveFlags.IS_REF] === true : false
 }
@@ -58,6 +59,7 @@ export function ref<T>(
   value: T,
 ): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T>
 export function ref<T = any>(): Ref<T | undefined>
+/*@__NO_SIDE_EFFECTS__*/
 export function ref(value?: unknown) {
   return createRef(value, false)
 }
@@ -93,6 +95,7 @@ export function shallowRef<T>(
     : ShallowRef<T>
   : ShallowRef<T>
 export function shallowRef<T = any>(): ShallowRef<T | undefined>
+/*@__NO_SIDE_EFFECTS__*/
 export function shallowRef(value?: unknown) {
   return createRef(value, true)
 }
@@ -338,6 +341,7 @@ export type ToRefs<T = any> = {
  * @param object - Reactive object to be made into an object of linked refs.
  * @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs}
  */
+/*@__NO_SIDE_EFFECTS__*/
 export function toRefs<T extends object>(object: T): ToRefs<T> {
   if (__DEV__ && !isProxy(object)) {
     warn(`toRefs() expects a reactive object but received a plain one.`)
@@ -474,6 +478,7 @@ export function toRef<T extends object, K extends keyof T>(
   key: K,
   defaultValue: T[K],
 ): ToRef<Exclude<T[K], undefined>>
+/*@__NO_SIDE_EFFECTS__*/
 export function toRef(
   source: Record<string, any> | MaybeRef,
   key?: string,

+ 18 - 0
packages/runtime-core/__tests__/hydration.spec.ts

@@ -1597,6 +1597,24 @@ describe('SSR hydration', () => {
     expect((container.firstChild as any).foo).toBe(msg.value)
   })
 
+  // #14274
+  test('should not render ref on custom element during hydration', () => {
+    const container = document.createElement('div')
+    container.innerHTML = '<my-element>hello</my-element>'
+    const root = ref()
+    const app = createSSRApp({
+      render: () =>
+        h('my-element', {
+          ref: root,
+          innerHTML: 'hello',
+        }),
+    })
+    app.mount(container)
+    expect(container.innerHTML).toBe('<my-element>hello</my-element>')
+    expect((container.firstChild as Element).hasAttribute('ref')).toBe(false)
+    expect(root.value).toBe(container.firstChild)
+  })
+
   // #5728
   test('empty text node in slot', () => {
     const Comp = {

+ 1 - 1
packages/runtime-core/src/hydration.ts

@@ -507,7 +507,7 @@ export function createHydrationFunctions(
               (isOn(key) && !isReservedProp(key)) ||
               // force hydrate v-bind with .prop modifiers
               key[0] === '.' ||
-              isCustomElement
+              (isCustomElement && !isReservedProp(key))
             ) {
               patchProp(el, key, null, props[key], undefined, parentComponent)
             }

+ 18 - 0
packages/server-renderer/__tests__/render.spec.ts

@@ -1229,5 +1229,23 @@ function testRender(type: string, render: typeof renderToString) {
       // during the render phase
       expect(getterSpy).toHaveBeenCalledTimes(2)
     })
+
+    test('props modifiers in render attrs', async () => {
+      const app = createApp({
+        setup() {
+          return () =>
+            h(
+              'div',
+              {
+                '^attr': 'attr',
+                '.prop': 'prop',
+              },
+              'Functional Component',
+            )
+        },
+      })
+      const html = await render(app)
+      expect(html).toBe(`<div attr="attr">Functional Component</div>`)
+    })
   })
 }

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

@@ -154,7 +154,7 @@ describe('ssr: renderClass', () => {
       ssrRenderAttrs({
         className: ['foo', 'bar'],
       }),
-    ).toBe(` class="foo,bar"`)
+    ).toBe(` class="foo bar"`)
   })
 })
 

+ 7 - 5
packages/server-renderer/src/helpers/ssrRenderAttrs.ts

@@ -29,21 +29,23 @@ export function ssrRenderAttrs(
   tag?: string,
 ): string {
   let ret = ''
-  for (const key in props) {
+  for (let key in props) {
     if (
       shouldIgnoreProp(key) ||
       isOn(key) ||
-      (tag === 'textarea' && key === 'value')
+      (tag === 'textarea' && key === 'value') ||
+      // force as property (not rendered in SSR)
+      key.startsWith('.')
     ) {
       continue
     }
     const value = props[key]
-    if (key === 'class') {
+    // force as attribute
+    if (key.startsWith('^')) key = key.slice(1)
+    if (key === 'class' || key === 'className') {
       ret += ` class="${ssrRenderClass(value)}"`
     } else if (key === 'style') {
       ret += ` style="${ssrRenderStyle(value)}"`
-    } else if (key === 'className') {
-      ret += ` class="${String(value)}"`
     } else {
       ret += ssrRenderDynamicAttr(key, value, tag)
     }