@@ -22,12 +22,20 @@ import * as pirates from 'pirates';
22
22
import * as sourceMapSupport from 'source-map-support' ;
23
23
import * as url from 'url' ;
24
24
import type { Location } from './types' ;
25
- import { TsConfigLoaderResult } from './third_party/tsconfig-loader' ;
25
+ import { tsConfigLoader , TsConfigLoaderResult } from './third_party/tsconfig-loader' ;
26
26
27
27
const version = 6 ;
28
28
const cacheDir = process . env . PWTEST_CACHE_DIR || path . join ( os . tmpdir ( ) , 'playwright-transform-cache' ) ;
29
29
const sourceMaps : Map < string , string > = new Map ( ) ;
30
30
31
+ type ParsedTsConfigData = {
32
+ absoluteBaseUrl : string ,
33
+ singlePath : { [ key : string ] : string } ,
34
+ hash : string ,
35
+ alias : { [ key : string ] : string | ( ( s : string [ ] ) => string ) } ,
36
+ } ;
37
+ const cachedTSConfigs = new Map < string , ParsedTsConfigData | undefined > ( ) ;
38
+
31
39
const kStackTraceLimit = 15 ;
32
40
Error . stackTraceLimit = kStackTraceLimit ;
33
41
@@ -47,9 +55,9 @@ sourceMapSupport.install({
47
55
}
48
56
} ) ;
49
57
50
- function calculateCachePath ( tsconfig : TsConfigLoaderResult , content : string , filePath : string ) : string {
58
+ function calculateCachePath ( tsconfigData : ParsedTsConfigData | undefined , content : string , filePath : string ) : string {
51
59
const hash = crypto . createHash ( 'sha1' )
52
- . update ( tsconfig . serialized || '' )
60
+ . update ( tsconfigData ?. hash || '' )
53
61
. update ( process . env . PW_EXPERIMENTAL_TS_ESM ? 'esm' : 'no_esm' )
54
62
. update ( content )
55
63
. update ( filePath )
@@ -59,10 +67,64 @@ function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, fil
59
67
return path . join ( cacheDir , hash [ 0 ] + hash [ 1 ] , fileName ) ;
60
68
}
61
69
62
- export function transformHook ( code : string , filename : string , tsconfig : TsConfigLoaderResult , isModule = false ) : string {
70
+ function validateTsConfig ( tsconfig : TsConfigLoaderResult ) : ParsedTsConfigData | undefined {
71
+ if ( ! tsconfig . tsConfigPath || ! tsconfig . paths || ! tsconfig . baseUrl )
72
+ return ;
73
+
74
+ const paths = tsconfig . paths ;
75
+ // Path that only contains "*", ".", "/" and "\" is too ambiguous.
76
+ const ambiguousPath = Object . keys ( paths ) . find ( key => key . match ( / ^ [ * . / \\ ] + $ / ) ) ;
77
+ if ( ambiguousPath )
78
+ return ;
79
+ const multiplePath = Object . keys ( paths ) . find ( key => paths [ key ] . length > 1 ) ;
80
+ if ( multiplePath )
81
+ return ;
82
+ // Only leave a single path mapping.
83
+ const singlePath = Object . fromEntries ( Object . entries ( paths ) . map ( ( [ key , values ] ) => ( [ key , values [ 0 ] ] ) ) ) ;
84
+ // Make 'baseUrl' absolute, because it is relative to the tsconfig.json, not to cwd.
85
+ const absoluteBaseUrl = path . resolve ( path . dirname ( tsconfig . tsConfigPath ) , tsconfig . baseUrl ) ;
86
+ const hash = JSON . stringify ( { absoluteBaseUrl, singlePath } ) ;
87
+
88
+ const alias : ParsedTsConfigData [ 'alias' ] = { } ;
89
+ for ( const [ key , value ] of Object . entries ( singlePath ) ) {
90
+ const regexKey = '^' + key . replace ( '*' , '.*' ) ;
91
+ alias [ regexKey ] = ( [ name ] ) => {
92
+ let relative : string ;
93
+ if ( key . endsWith ( '/*' ) )
94
+ relative = value . substring ( 0 , value . length - 1 ) + name . substring ( key . length - 1 ) ;
95
+ else
96
+ relative = value ;
97
+ relative = relative . replace ( / \/ / g, path . sep ) ;
98
+ return path . resolve ( absoluteBaseUrl , relative ) ;
99
+ } ;
100
+ }
101
+
102
+ return {
103
+ absoluteBaseUrl,
104
+ singlePath,
105
+ hash,
106
+ alias,
107
+ } ;
108
+ }
109
+
110
+ function loadAndValidateTsconfigForFile ( file : string ) : ParsedTsConfigData | undefined {
111
+ const cwd = path . dirname ( file ) ;
112
+ if ( ! cachedTSConfigs . has ( cwd ) ) {
113
+ const loaded = tsConfigLoader ( {
114
+ getEnv : ( name : string ) => process . env [ name ] ,
115
+ cwd
116
+ } ) ;
117
+ cachedTSConfigs . set ( cwd , validateTsConfig ( loaded ) ) ;
118
+ }
119
+ return cachedTSConfigs . get ( cwd ) ;
120
+ }
121
+
122
+ export function transformHook ( code : string , filename : string , isModule = false ) : string {
63
123
if ( isComponentImport ( filename ) )
64
124
return componentStub ( ) ;
65
- const cachePath = calculateCachePath ( tsconfig , code , filename ) ;
125
+
126
+ const tsconfigData = loadAndValidateTsconfigForFile ( filename ) ;
127
+ const cachePath = calculateCachePath ( tsconfigData , code , filename ) ;
66
128
const codePath = cachePath + '.js' ;
67
129
const sourceMapPath = cachePath + '.map' ;
68
130
sourceMaps . set ( filename , sourceMapPath ) ;
@@ -73,30 +135,6 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
73
135
process . env . BROWSERSLIST_IGNORE_OLD_DATA = 'true' ;
74
136
const babel : typeof import ( '@babel/core' ) = require ( '@babel/core' ) ;
75
137
76
- const hasBaseUrl = ! ! tsconfig . baseUrl ;
77
- const extensions = [ '' , '.js' , '.ts' , '.mjs' , ...( process . env . PW_COMPONENT_TESTING ? [ '.tsx' , '.jsx' ] : [ ] ) ] ; const alias : { [ key : string ] : string | ( ( s : string [ ] ) => string ) } = { } ;
78
- for ( const [ key , values ] of Object . entries ( tsconfig . paths || { '*' : '*' } ) ) {
79
- const regexKey = '^' + key . replace ( '*' , '.*' ) ;
80
- alias [ regexKey ] = ( [ name ] ) => {
81
- for ( const value of values ) {
82
- let relative : string ;
83
- if ( key === '*' && value === '*' )
84
- relative = name ;
85
- else if ( key . endsWith ( '/*' ) )
86
- relative = value . substring ( 0 , value . length - 1 ) + name . substring ( key . length - 1 ) ;
87
- else
88
- relative = value ;
89
- relative = relative . replace ( / \/ / g, path . sep ) ;
90
- const result = path . resolve ( tsconfig . baseUrl || '' , relative ) ;
91
- for ( const extension of extensions ) {
92
- if ( fs . existsSync ( result + extension ) )
93
- return result + extension ;
94
- }
95
- }
96
- return name ;
97
- } ;
98
- }
99
-
100
138
const plugins = [
101
139
[ require . resolve ( '@babel/plugin-proposal-class-properties' ) ] ,
102
140
[ require . resolve ( '@babel/plugin-proposal-numeric-separator' ) ] ,
@@ -110,10 +148,13 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
110
148
[ require . resolve ( '@babel/plugin-proposal-export-namespace-from' ) ] ,
111
149
] as any ;
112
150
113
- if ( hasBaseUrl ) {
151
+ if ( tsconfigData ) {
114
152
plugins . push ( [ require . resolve ( 'babel-plugin-module-resolver' ) , {
115
153
root : [ './' ] ,
116
- alias
154
+ alias : tsconfigData . alias ,
155
+ // Silences warning 'Could not resovle ...' that we trigger because we resolve
156
+ // into 'foo/bar', and not 'foo/bar.ts'.
157
+ loglevel : 'silent' ,
117
158
} ] ) ;
118
159
}
119
160
@@ -143,16 +184,13 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
143
184
fs . mkdirSync ( path . dirname ( cachePath ) , { recursive : true } ) ;
144
185
if ( result . map )
145
186
fs . writeFileSync ( sourceMapPath , JSON . stringify ( result . map ) , 'utf8' ) ;
146
- // Compiled files with base URL depend on the FS state during compilation,
147
- // never cache them.
148
- if ( ! hasBaseUrl )
149
- fs . writeFileSync ( codePath , result . code , 'utf8' ) ;
187
+ fs . writeFileSync ( codePath , result . code , 'utf8' ) ;
150
188
}
151
189
return result . code || '' ;
152
190
}
153
191
154
- export function installTransform ( tsconfig : TsConfigLoaderResult ) : ( ) => void {
155
- return pirates . addHook ( ( code : string , filename : string ) => transformHook ( code , filename , tsconfig ) , { exts : [ '.ts' , '.tsx' ] } ) ;
192
+ export function installTransform ( ) : ( ) => void {
193
+ return pirates . addHook ( ( code : string , filename : string ) => transformHook ( code , filename ) , { exts : [ '.ts' , '.tsx' ] } ) ;
156
194
}
157
195
158
196
export function wrapFunctionWithLocation < A extends any [ ] , R > ( func : ( location : Location , ...args : A ) => R ) : ( ...args : A ) => R {
0 commit comments