@@ -23,8 +23,10 @@ import { parse, traverse, types as t } from '../babelBundle';
23
23
import type { ComponentInfo } from '../tsxTransform' ;
24
24
import { collectComponentUsages , componentInfo } from '../tsxTransform' ;
25
25
import type { FullConfig } from '../types' ;
26
+ import { assert } from 'playwright-core/lib/utils' ;
26
27
27
28
let previewServer : PreviewServer ;
29
+ const VERSION = 1 ;
28
30
29
31
export function createPlugin (
30
32
registerSourceFile : string ,
@@ -37,42 +39,68 @@ export function createPlugin(
37
39
const use = config . projects [ 0 ] . use as any ;
38
40
const viteConfig : InlineConfig = use . viteConfig || { } ;
39
41
const port = use . vitePort || 3100 ;
40
-
41
42
configDir = configDirectory ;
42
-
43
43
process . env . PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${ port } /playwright/index.html` ;
44
44
45
- viteConfig . root = viteConfig . root || configDir ;
46
- viteConfig . plugins = viteConfig . plugins || [
47
- frameworkPluginFactory ( )
48
- ] ;
49
- const files = new Set < string > ( ) ;
50
- for ( const project of suite . suites ) {
51
- for ( const file of project . suites )
52
- files . add ( file . location ! . file ) ;
45
+ const rootDir = viteConfig . root || configDir ;
46
+ const outDir = viteConfig ?. build ?. outDir || path . join ( rootDir , 'playwright' , '.cache' ) ;
47
+ const templateDir = path . join ( rootDir , 'playwright' ) ;
48
+
49
+ const buildInfoFile = path . join ( outDir , 'metainfo.json' ) ;
50
+ let buildInfo : BuildInfo ;
51
+ try {
52
+ buildInfo = JSON . parse ( await fs . promises . readFile ( buildInfoFile , 'utf-8' ) ) as BuildInfo ;
53
+ assert ( buildInfo . version === VERSION ) ;
54
+ } catch ( e ) {
55
+ buildInfo = {
56
+ version : VERSION ,
57
+ components : [ ] ,
58
+ tests : { } ,
59
+ sources : { } ,
60
+ } ;
53
61
}
54
- const registerSource = await fs . promises . readFile ( registerSourceFile , 'utf-8' ) ;
55
- viteConfig . plugins . push ( vitePlugin ( registerSource , [ ...files ] ) ) ;
56
- viteConfig . configFile = viteConfig . configFile || false ;
57
- viteConfig . define = viteConfig . define || { } ;
58
- viteConfig . define . __VUE_PROD_DEVTOOLS__ = true ;
59
- viteConfig . css = viteConfig . css || { } ;
60
- viteConfig . css . devSourcemap = true ;
62
+
63
+ const componentRegistry : ComponentRegistry = new Map ( ) ;
64
+ // 1. Re-parse changed tests and collect required components.
65
+ const hasNewTests = await checkNewTests ( suite , buildInfo , componentRegistry ) ;
66
+ // 2. Check if the set of required components has changed.
67
+ const hasNewComponents = await checkNewComponents ( buildInfo , componentRegistry ) ;
68
+ // 3. Check component sources.
69
+ const sourcesDirty = hasNewComponents || await checkSources ( buildInfo ) ;
70
+
71
+ viteConfig . root = rootDir ;
61
72
viteConfig . preview = { port } ;
62
73
viteConfig . build = {
63
- target : 'esnext' ,
64
- minify : false ,
65
- rollupOptions : {
66
- treeshake : false ,
67
- input : {
68
- index : path . join ( viteConfig . root , 'playwright' , 'index.html' )
69
- } ,
70
- } ,
71
- sourcemap : true ,
72
- outDir : viteConfig ?. build ?. outDir || path . join ( viteConfig . root , 'playwright' , 'out' )
74
+ outDir
73
75
} ;
74
76
const { build, preview } = require ( 'vite' ) ;
75
- await build ( viteConfig ) ;
77
+ if ( sourcesDirty ) {
78
+ viteConfig . plugins = viteConfig . plugins || [
79
+ frameworkPluginFactory ( )
80
+ ] ;
81
+ const registerSource = await fs . promises . readFile ( registerSourceFile , 'utf-8' ) ;
82
+ viteConfig . plugins . push ( vitePlugin ( registerSource , buildInfo , componentRegistry ) ) ;
83
+ viteConfig . configFile = viteConfig . configFile || false ;
84
+ viteConfig . define = viteConfig . define || { } ;
85
+ viteConfig . define . __VUE_PROD_DEVTOOLS__ = true ;
86
+ viteConfig . css = viteConfig . css || { } ;
87
+ viteConfig . css . devSourcemap = true ;
88
+ viteConfig . build = {
89
+ ...viteConfig . build ,
90
+ target : 'esnext' ,
91
+ minify : false ,
92
+ rollupOptions : {
93
+ treeshake : false ,
94
+ input : {
95
+ index : path . join ( templateDir , 'index.html' )
96
+ } ,
97
+ } ,
98
+ sourcemap : true ,
99
+ } ;
100
+ await build ( viteConfig ) ;
101
+ }
102
+ if ( hasNewTests || hasNewComponents || sourcesDirty )
103
+ await fs . promises . writeFile ( buildInfoFile , JSON . stringify ( buildInfo , undefined , 2 ) ) ;
76
104
previewServer = await preview ( viteConfig ) ;
77
105
} ,
78
106
@@ -87,57 +115,142 @@ export function createPlugin(
87
115
} ;
88
116
}
89
117
90
- const imports : Map < string , ComponentInfo > = new Map ( ) ;
118
+ type BuildInfo = {
119
+ version : number ,
120
+ sources : {
121
+ [ key : string ] : {
122
+ timestamp : number ;
123
+ }
124
+ } ;
125
+ components : ComponentInfo [ ] ;
126
+ tests : {
127
+ [ key : string ] : {
128
+ timestamp : number ;
129
+ components : string [ ] ;
130
+ }
131
+ } ;
132
+ } ;
133
+
134
+ type ComponentRegistry = Map < string , ComponentInfo > ;
135
+
136
+ async function checkSources ( buildInfo : BuildInfo ) : Promise < boolean > {
137
+ for ( const [ source , sourceInfo ] of Object . entries ( buildInfo . sources ) ) {
138
+ try {
139
+ const timestamp = ( await fs . promises . stat ( source ) ) . mtimeMs ;
140
+ if ( sourceInfo . timestamp !== timestamp )
141
+ return true ;
142
+ } catch ( e ) {
143
+ return true ;
144
+ }
145
+ }
146
+ return false ;
147
+ }
148
+
149
+ async function checkNewTests ( suite : Suite , buildInfo : BuildInfo , componentRegistry : ComponentRegistry ) : Promise < boolean > {
150
+ const testFiles = new Set < string > ( ) ;
151
+ for ( const project of suite . suites ) {
152
+ for ( const file of project . suites )
153
+ testFiles . add ( file . location ! . file ) ;
154
+ }
155
+
156
+ let hasNewTests = false ;
157
+ for ( const testFile of testFiles ) {
158
+ const timestamp = ( await fs . promises . stat ( testFile ) ) . mtimeMs ;
159
+ if ( buildInfo . tests [ testFile ] ?. timestamp !== timestamp ) {
160
+ const components = await parseTestFile ( testFile ) ;
161
+ for ( const component of components )
162
+ componentRegistry . set ( component . fullName , component ) ;
163
+ buildInfo . tests [ testFile ] = { timestamp, components : components . map ( c => c . fullName ) } ;
164
+ hasNewTests = true ;
165
+ } else {
166
+ // The test has not changed, populate component registry from the buildInfo.
167
+ for ( const componentName of buildInfo . tests [ testFile ] . components ) {
168
+ const component = buildInfo . components . find ( c => c . fullName === componentName ) ! ;
169
+ componentRegistry . set ( component . fullName , component ) ;
170
+ }
171
+ }
172
+ }
173
+
174
+ return hasNewTests ;
175
+ }
176
+
177
+ async function checkNewComponents ( buildInfo : BuildInfo , componentRegistry : ComponentRegistry ) : Promise < boolean > {
178
+ const newComponents = [ ...componentRegistry . keys ( ) ] ;
179
+ const oldComponents = new Set ( buildInfo . components . map ( c => c . fullName ) ) ;
180
+
181
+ let hasNewComponents = false ;
182
+ for ( const c of newComponents ) {
183
+ if ( ! oldComponents . has ( c ) ) {
184
+ hasNewComponents = true ;
185
+ break ;
186
+ }
187
+ }
188
+ if ( ! hasNewComponents )
189
+ return false ;
190
+ buildInfo . components = newComponents . map ( n => componentRegistry . get ( n ) ! ) ;
191
+ return true ;
192
+ }
193
+
194
+ async function parseTestFile ( testFile : string ) : Promise < ComponentInfo [ ] > {
195
+ const text = await fs . promises . readFile ( testFile , 'utf-8' ) ;
196
+ const ast = parse ( text , { errorRecovery : true , plugins : [ 'typescript' , 'jsx' ] , sourceType : 'module' } ) ;
197
+ const componentUsages = collectComponentUsages ( ast ) ;
198
+ const result : ComponentInfo [ ] = [ ] ;
199
+
200
+ traverse ( ast , {
201
+ enter : p => {
202
+ if ( t . isImportDeclaration ( p . node ) ) {
203
+ const importNode = p . node ;
204
+ if ( ! t . isStringLiteral ( importNode . source ) )
205
+ return ;
91
206
92
- function vitePlugin ( registerSource : string , files : string [ ] ) : Plugin {
207
+ for ( const specifier of importNode . specifiers ) {
208
+ if ( ! componentUsages . names . has ( specifier . local . name ) )
209
+ continue ;
210
+ if ( t . isImportNamespaceSpecifier ( specifier ) )
211
+ continue ;
212
+ result . push ( componentInfo ( specifier , importNode . source . value , testFile ) ) ;
213
+ }
214
+ }
215
+ }
216
+ } ) ;
217
+
218
+ return result ;
219
+ }
220
+
221
+ function vitePlugin ( registerSource : string , buildInfo : BuildInfo , componentRegistry : ComponentRegistry ) : Plugin {
222
+ buildInfo . sources = { } ;
93
223
return {
94
224
name : 'playwright:component-index' ,
95
225
96
- configResolved : async config => {
97
-
98
- for ( const file of files ) {
99
- const text = await fs . promises . readFile ( file , 'utf-8' ) ;
100
- const ast = parse ( text , { errorRecovery : true , plugins : [ 'typescript' , 'jsx' ] , sourceType : 'module' } ) ;
101
- const components = collectComponentUsages ( ast ) ;
102
-
103
- traverse ( ast , {
104
- enter : p => {
105
- if ( t . isImportDeclaration ( p . node ) ) {
106
- const importNode = p . node ;
107
- if ( ! t . isStringLiteral ( importNode . source ) )
108
- return ;
109
-
110
- for ( const specifier of importNode . specifiers ) {
111
- if ( ! components . names . has ( specifier . local . name ) )
112
- continue ;
113
- if ( t . isImportNamespaceSpecifier ( specifier ) )
114
- continue ;
115
- const info = componentInfo ( specifier , importNode . source . value , file ) ;
116
- imports . set ( info . fullName , info ) ;
117
- }
118
- }
119
- }
120
- } ) ;
226
+ transform : async ( content , id ) => {
227
+ const queryIndex = id . indexOf ( '?' ) ;
228
+ const file = queryIndex !== - 1 ? id . substring ( 0 , queryIndex ) : id ;
229
+ if ( ! buildInfo . sources [ file ] ) {
230
+ try {
231
+ const timestamp = ( await fs . promises . stat ( file ) ) . mtimeMs ;
232
+ buildInfo . sources [ file ] = { timestamp } ;
233
+ } catch {
234
+ // Silent if can't read the file.
235
+ }
121
236
}
122
- } ,
123
237
124
- transform : async ( content , id ) => {
125
238
if ( ! id . endsWith ( 'playwright/index.ts' ) && ! id . endsWith ( 'playwright/index.tsx' ) && ! id . endsWith ( 'playwright/index.js' ) )
126
239
return ;
127
240
128
241
const folder = path . dirname ( id ) ;
129
242
const lines = [ content , '' ] ;
130
243
lines . push ( registerSource ) ;
131
244
132
- for ( const [ alias , value ] of imports ) {
245
+ for ( const [ alias , value ] of componentRegistry ) {
133
246
const importPath = value . isModuleOrAlias ? value . importPath : './' + path . relative ( folder , value . importPath ) . replace ( / \\ / g, '/' ) ;
134
247
if ( value . importedName )
135
248
lines . push ( `import { ${ value . importedName } as ${ alias } } from '${ importPath } ';` ) ;
136
249
else
137
250
lines . push ( `import ${ alias } from '${ importPath } ';` ) ;
138
251
}
139
252
140
- lines . push ( `register({ ${ [ ...imports . keys ( ) ] . join ( ',\n ' ) } });` ) ;
253
+ lines . push ( `register({ ${ [ ...componentRegistry . keys ( ) ] . join ( ',\n ' ) } });` ) ;
141
254
return {
142
255
code : lines . join ( '\n' ) ,
143
256
map : { mappings : '' }
0 commit comments