]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): support specifying global types for sfc macros
authorEvan You <yyx990803@gmail.com>
Sun, 16 Apr 2023 07:49:41 +0000 (15:49 +0800)
committerEvan You <yyx990803@gmail.com>
Sun, 16 Apr 2023 07:49:41 +0000 (15:49 +0800)
ref: https://github.com/vuejs/core/pull/8083#issuecomment-1508468713

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/script/context.ts
packages/compiler-sfc/src/script/resolveType.ts

index 7aa08d595f5eaceb938e394c9dc75c1129eed71e..0f2a47d6b99e4f116d4525f50ca57f7983d09266 100644 (file)
@@ -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<App.User & PP>()`, 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<string, string> = {}) {
+function resolve(
+  code: string,
+  files: Record<string, string> = {},
+  options?: Partial<SFCScriptCompileOptions>
+) {
   const { descriptor } = parse(`<script setup lang="ts">\n${code}\n</script>`, {
     filename: '/Test.vue'
   })
@@ -457,7 +487,8 @@ function resolve(code: string, files: Record<string, string> = {}) {
       readFile(file) {
         return files[file]
       }
-    }
+    },
+    ...options
   })
 
   for (const file in files) {
index 0d09f02538a61b1ecbf88dee4cfbeeb6983b3253..1f525005c4d3f26d40fc6ab3ef35996ecd4e881b 100644 (file)
@@ -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().
index 641e463741f16e75c287e403eeb997d94933a0a1..d2c5dabd194a9b4b9b80aa914656af5d3412c58c 100644 (file)
@@ -24,6 +24,7 @@ export class ScriptCompileContext {
 
   // import / type analysis
   scope?: TypeScope
+  globalScopes?: TypeScope[]
   userImports: Record<string, ImportBinding> = 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 {
index 1e89c5712dc4b21045af38ecb4d5b76293bf31c3..8efa04579f8b7730449bec4b8046fe6422a0b930 100644 (file)
@@ -56,7 +56,7 @@ export type SimpleTypeResolveContext = Pick<
   // required
   'source' | 'filename' | 'error' | 'options'
 > &
-  Partial<Pick<ScriptCompileContext, 'scope' | 'deps'>> & {
+  Partial<Pick<ScriptCompileContext, 'scope' | 'globalScopes' | 'deps'>> & {
     ast: Statement[]
   }
 
@@ -64,25 +64,18 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
 
 type Import = Pick<ImportBinding, 'source' | 'imported'>
 
+type ScopeTypeNode = Node & {
+  // scope types always has ownerScope attached
+  _ownerScope: TypeScope
+}
+
 export interface TypeScope {
   filename: string
   source: string
   offset: number
   imports: Record<string, Import>
-  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<string, ScopeTypeNode>
+  exportedTypes: Record<string, ScopeTypeNode>
 }
 
 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]
             }
           }
         }