@@ -20,14 +20,20 @@ import {
20
20
TSTypeReference ,
21
21
TemplateLiteral
22
22
} from '@babel/types'
23
- import { UNKNOWN_TYPE , getId , getImportedName } from './utils'
23
+ import {
24
+ UNKNOWN_TYPE ,
25
+ createGetCanonicalFileName ,
26
+ getId ,
27
+ getImportedName
28
+ } from './utils'
24
29
import { ScriptCompileContext , resolveParserPlugins } from './context'
25
30
import { ImportBinding , SFCScriptCompileOptions } from '../compileScript'
26
31
import { capitalize , hasOwn } from '@vue/shared'
27
- import path from 'path'
28
32
import { parse as babelParse } from '@babel/parser'
29
33
import { parse } from '../parse'
30
34
import { createCache } from '../cache'
35
+ import type TS from 'typescript'
36
+ import { join , extname , dirname } from 'path'
31
37
32
38
type Import = Pick < ImportBinding , 'source' | 'imported' >
33
39
@@ -480,54 +486,82 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] {
480
486
}
481
487
}
482
488
489
+ let ts : typeof TS
490
+
491
+ export function registerTS ( _ts : any ) {
492
+ ts = _ts
493
+ }
494
+
495
+ type FS = NonNullable < SFCScriptCompileOptions [ 'fs' ] >
496
+
483
497
function resolveTypeFromImport (
484
498
ctx : ScriptCompileContext ,
485
499
node : TSTypeReference | TSExpressionWithTypeArguments ,
486
500
name : string ,
487
501
scope : TypeScope
488
502
) : Node | undefined {
489
- const fs = ctx . options . fs
503
+ const fs : FS = ctx . options . fs || ts ?. sys
490
504
if ( ! fs ) {
491
505
ctx . error (
492
- `fs options for compileScript are required for resolving imported types` ,
493
- node ,
494
- scope
506
+ `No fs option provided to \` compileScript\` in non-Node environment. ` +
507
+ `File system access is required for resolving imported types.` ,
508
+ node
495
509
)
496
510
}
497
- // TODO (hmr) register dependency file on ctx
511
+
498
512
const containingFile = scope . filename
499
513
const { source, imported } = scope . imports [ name ]
514
+
515
+ let resolved : string | undefined
516
+
500
517
if ( source . startsWith ( '.' ) ) {
501
518
// relative import - fast path
502
- const filename = path . join ( containingFile , '..' , source )
503
- const resolved = resolveExt ( filename , fs )
504
- if ( resolved ) {
505
- return resolveTypeReference (
506
- ctx ,
519
+ const filename = join ( containingFile , '..' , source )
520
+ resolved = resolveExt ( filename , fs )
521
+ } else {
522
+ // module or aliased import - use full TS resolution, only supported in Node
523
+ if ( ! __NODE_JS__ ) {
524
+ ctx . error (
525
+ `Type import from non-relative sources is not supported in the browser build.` ,
507
526
node ,
508
- fileToScope ( ctx , resolved , fs ) ,
509
- imported ,
510
- true
527
+ scope
511
528
)
512
- } else {
529
+ }
530
+ if ( ! ts ) {
513
531
ctx . error (
514
- `Failed to resolve import source ${ JSON . stringify (
532
+ `Failed to resolve type ${ imported } from module ${ JSON . stringify (
515
533
source
516
- ) } for type ${ name } `,
534
+ ) } . ` +
535
+ `typescript is required as a peer dep for vue in order ` +
536
+ `to support resolving types from module imports.` ,
517
537
node ,
518
538
scope
519
539
)
520
540
}
541
+ resolved = resolveWithTS ( containingFile , source , fs )
542
+ }
543
+
544
+ if ( resolved ) {
545
+ // TODO (hmr) register dependency file on ctx
546
+ return resolveTypeReference (
547
+ ctx ,
548
+ node ,
549
+ fileToScope ( ctx , resolved , fs ) ,
550
+ imported ,
551
+ true
552
+ )
521
553
} else {
522
- // TODO module or aliased import - use full TS resolution
523
- return
554
+ ctx . error (
555
+ `Failed to resolve import source ${ JSON . stringify (
556
+ source
557
+ ) } for type ${ name } `,
558
+ node ,
559
+ scope
560
+ )
524
561
}
525
562
}
526
563
527
- function resolveExt (
528
- filename : string ,
529
- fs : NonNullable < SFCScriptCompileOptions [ 'fs' ] >
530
- ) {
564
+ function resolveExt ( filename : string , fs : FS ) {
531
565
const tryResolve = ( filename : string ) => {
532
566
if ( fs . fileExists ( filename ) ) return filename
533
567
}
@@ -540,23 +574,83 @@ function resolveExt(
540
574
)
541
575
}
542
576
577
+ const tsConfigCache = createCache < {
578
+ options : TS . CompilerOptions
579
+ cache : TS . ModuleResolutionCache
580
+ } > ( )
581
+
582
+ function resolveWithTS (
583
+ containingFile : string ,
584
+ source : string ,
585
+ fs : FS
586
+ ) : string | undefined {
587
+ if ( ! __NODE_JS__ ) return
588
+
589
+ // 1. resolve tsconfig.json
590
+ const configPath = ts . findConfigFile ( containingFile , fs . fileExists )
591
+ // 2. load tsconfig.json
592
+ let options : TS . CompilerOptions
593
+ let cache : TS . ModuleResolutionCache | undefined
594
+ if ( configPath ) {
595
+ const cached = tsConfigCache . get ( configPath )
596
+ if ( ! cached ) {
597
+ // The only case where `fs` is NOT `ts.sys` is during tests.
598
+ // parse config host requires an extra `readDirectory` method
599
+ // during tests, which is stubbed.
600
+ const parseConfigHost = __TEST__
601
+ ? {
602
+ ...fs ,
603
+ useCaseSensitiveFileNames : true ,
604
+ readDirectory : ( ) => [ ]
605
+ }
606
+ : ts . sys
607
+ const parsed = ts . parseJsonConfigFileContent (
608
+ ts . readConfigFile ( configPath , fs . readFile ) . config ,
609
+ parseConfigHost ,
610
+ dirname ( configPath ) ,
611
+ undefined ,
612
+ configPath
613
+ )
614
+ options = parsed . options
615
+ cache = ts . createModuleResolutionCache (
616
+ process . cwd ( ) ,
617
+ createGetCanonicalFileName ( ts . sys . useCaseSensitiveFileNames ) ,
618
+ options
619
+ )
620
+ tsConfigCache . set ( configPath , { options, cache } )
621
+ } else {
622
+ ; ( { options, cache } = cached )
623
+ }
624
+ } else {
625
+ options = { }
626
+ }
627
+
628
+ // 3. resolve
629
+ const res = ts . resolveModuleName ( source , containingFile , options , fs , cache )
630
+
631
+ if ( res . resolvedModule ) {
632
+ return res . resolvedModule . resolvedFileName
633
+ }
634
+ }
635
+
543
636
const fileToScopeCache = createCache < TypeScope > ( )
544
637
545
638
export function invalidateTypeCache ( filename : string ) {
546
639
fileToScopeCache . delete ( filename )
640
+ tsConfigCache . delete ( filename )
547
641
}
548
642
549
643
function fileToScope (
550
644
ctx : ScriptCompileContext ,
551
645
filename : string ,
552
- fs : NonNullable < SFCScriptCompileOptions [ 'fs' ] >
646
+ fs : FS
553
647
) : TypeScope {
554
648
const cached = fileToScopeCache . get ( filename )
555
649
if ( cached ) {
556
650
return cached
557
651
}
558
652
559
- const source = fs . readFile ( filename )
653
+ const source = fs . readFile ( filename ) || ''
560
654
const body = parseFile ( ctx , filename , source )
561
655
const scope : TypeScope = {
562
656
filename,
@@ -577,7 +671,7 @@ function parseFile(
577
671
filename : string ,
578
672
content : string
579
673
) : Statement [ ] {
580
- const ext = path . extname ( filename )
674
+ const ext = extname ( filename )
581
675
if ( ext === '.ts' || ext === '.tsx' ) {
582
676
return babelParse ( content , {
583
677
plugins : resolveParserPlugins (
@@ -705,7 +799,8 @@ function recordType(node: Node, types: Record<string, Node>) {
705
799
switch ( node . type ) {
706
800
case 'TSInterfaceDeclaration' :
707
801
case 'TSEnumDeclaration' :
708
- case 'TSModuleDeclaration' : {
802
+ case 'TSModuleDeclaration' :
803
+ case 'ClassDeclaration' : {
709
804
const id = node . id . type === 'Identifier' ? node . id . name : node . id . value
710
805
types [ id ] = node
711
806
break
@@ -899,6 +994,9 @@ export function inferRuntimeType(
899
994
}
900
995
}
901
996
997
+ case 'ClassDeclaration' :
998
+ return [ 'Object' ]
999
+
902
1000
default :
903
1001
return [ UNKNOWN_TYPE ] // no runtime check
904
1002
}
0 commit comments