@@ -11,6 +11,7 @@ import type { RawSourceMap } from '@ampproject/remapping'
1111import type { InternalModuleFormat , SourceMap } from 'rollup'
1212import type { TSConfckParseResult } from 'tsconfck'
1313import { TSConfckCache , TSConfckParseError , parse } from 'tsconfck'
14+ import type { FSWatcher } from 'dep-types/chokidar'
1415import {
1516 combineSourcemaps ,
1617 createDebugger ,
@@ -41,10 +42,6 @@ export const defaultEsbuildSupported = {
4142 'import-meta' : true ,
4243}
4344
44- // TODO: rework to avoid caching the server for this module.
45- // If two servers are created in the same process, they will interfere with each other.
46- let server : ViteDevServer
47-
4845export interface ESBuildOptions extends TransformOptions {
4946 include ?: string | RegExp | string [ ] | RegExp [ ]
5047 exclude ?: string | RegExp | string [ ] | RegExp [ ]
@@ -83,6 +80,8 @@ export async function transformWithEsbuild(
8380 filename : string ,
8481 options ?: TransformOptions ,
8582 inMap ?: object ,
83+ config ?: ResolvedConfig ,
84+ watcher ?: FSWatcher ,
8685) : Promise < ESBuildTransformResult > {
8786 let loader = options ?. loader
8887
@@ -123,14 +122,29 @@ export async function transformWithEsbuild(
123122 ]
124123 const compilerOptionsForFile : TSCompilerOptions = { }
125124 if ( loader === 'ts' || loader === 'tsx' ) {
126- const loadedTsconfig = await loadTsconfigJsonForFile ( filename )
127- const loadedCompilerOptions = loadedTsconfig . compilerOptions ?? { }
125+ try {
126+ const { tsconfig : loadedTsconfig , tsconfigFile } =
127+ await loadTsconfigJsonForFile ( filename , config )
128+ // tsconfig could be out of root, make sure it is watched on dev
129+ if ( watcher && tsconfigFile && config ) {
130+ ensureWatchedFile ( watcher , tsconfigFile , config . root )
131+ }
132+ const loadedCompilerOptions = loadedTsconfig . compilerOptions ?? { }
128133
129- for ( const field of meaningfulFields ) {
130- if ( field in loadedCompilerOptions ) {
131- // @ts -expect-error TypeScript can't tell they are of the same type
132- compilerOptionsForFile [ field ] = loadedCompilerOptions [ field ]
134+ for ( const field of meaningfulFields ) {
135+ if ( field in loadedCompilerOptions ) {
136+ // @ts -expect-error TypeScript can't tell they are of the same type
137+ compilerOptionsForFile [ field ] = loadedCompilerOptions [ field ]
138+ }
133139 }
140+ } catch ( e ) {
141+ if ( e instanceof TSConfckParseError ) {
142+ // tsconfig could be out of root, make sure it is watched on dev
143+ if ( watcher && e . tsconfigFile && config ) {
144+ ensureWatchedFile ( watcher , e . tsconfigFile , config . root )
145+ }
146+ }
147+ throw e
134148 }
135149 }
136150
@@ -251,22 +265,23 @@ export function esbuildPlugin(config: ResolvedConfig): Plugin {
251265 } ,
252266 }
253267
268+ let server : ViteDevServer
269+
254270 return {
255271 name : 'vite:esbuild' ,
256272 configureServer ( _server ) {
257273 server = _server
258- server . watcher
259- . on ( 'add' , reloadOnTsconfigChange )
260- . on ( 'change' , reloadOnTsconfigChange )
261- . on ( 'unlink' , reloadOnTsconfigChange )
262- } ,
263- buildEnd ( ) {
264- // recycle serve to avoid preventing Node self-exit (#6815)
265- server = null as any
266274 } ,
267275 async transform ( code , id ) {
268276 if ( filter ( id ) || filter ( cleanUrl ( id ) ) ) {
269- const result = await transformWithEsbuild ( code , id , transformOptions )
277+ const result = await transformWithEsbuild (
278+ code ,
279+ id ,
280+ transformOptions ,
281+ undefined ,
282+ config ,
283+ server ?. watcher ,
284+ )
270285 if ( result . warnings . length ) {
271286 result . warnings . forEach ( ( m ) => {
272287 this . warn ( prettifyMessage ( m , code ) )
@@ -317,7 +332,13 @@ export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => {
317332 return null
318333 }
319334
320- const res = await transformWithEsbuild ( code , chunk . fileName , options )
335+ const res = await transformWithEsbuild (
336+ code ,
337+ chunk . fileName ,
338+ options ,
339+ undefined ,
340+ config ,
341+ )
321342
322343 if ( config . build . lib ) {
323344 // #7188, esbuild adds helpers out of the UMD and IIFE wrappers, and the
@@ -448,63 +469,69 @@ function prettifyMessage(m: Message, code: string): string {
448469 return res + `\n`
449470}
450471
451- let tsconfckCache : TSConfckCache < TSConfckParseResult > | undefined
472+ let globalTSConfckCache : TSConfckCache < TSConfckParseResult > | undefined
473+ const tsconfckCacheMap = new WeakMap <
474+ ResolvedConfig ,
475+ TSConfckCache < TSConfckParseResult >
476+ > ( )
477+
478+ function getTSConfckCache ( config ?: ResolvedConfig ) {
479+ if ( ! config ) {
480+ return ( globalTSConfckCache ??= new TSConfckCache < TSConfckParseResult > ( ) )
481+ }
482+ let cache = tsconfckCacheMap . get ( config )
483+ if ( ! cache ) {
484+ cache = new TSConfckCache < TSConfckParseResult > ( )
485+ tsconfckCacheMap . set ( config , cache )
486+ }
487+ return cache
488+ }
452489
453490export async function loadTsconfigJsonForFile (
454491 filename : string ,
455- ) : Promise < TSConfigJSON > {
456- try {
457- if ( ! tsconfckCache ) {
458- tsconfckCache = new TSConfckCache < TSConfckParseResult > ( )
459- }
460- const result = await parse ( filename , {
461- cache : tsconfckCache ,
462- ignoreNodeModules : true ,
463- } )
464- // tsconfig could be out of root, make sure it is watched on dev
465- if ( server && result . tsconfigFile ) {
466- ensureWatchedFile ( server . watcher , result . tsconfigFile , server . config . root )
467- }
468- return result . tsconfig
469- } catch ( e ) {
470- if ( e instanceof TSConfckParseError ) {
471- // tsconfig could be out of root, make sure it is watched on dev
472- if ( server && e . tsconfigFile ) {
473- ensureWatchedFile ( server . watcher , e . tsconfigFile , server . config . root )
474- }
475- }
476- throw e
477- }
492+ config ?: ResolvedConfig ,
493+ ) : Promise < { tsconfigFile : string ; tsconfig : TSConfigJSON } > {
494+ const { tsconfig, tsconfigFile } = await parse ( filename , {
495+ cache : getTSConfckCache ( config ) ,
496+ ignoreNodeModules : true ,
497+ } )
498+ return { tsconfigFile, tsconfig }
478499}
479500
480- async function reloadOnTsconfigChange ( changedFile : string ) {
481- // server could be closed externally after a file change is detected
482- if ( ! server ) return
501+ export async function reloadOnTsconfigChange (
502+ server : ViteDevServer ,
503+ changedFile : string ,
504+ ) : Promise < void > {
483505 // any tsconfig.json that's added in the workspace could be closer to a code file than a previously cached one
484506 // any json file in the tsconfig cache could have been used to compile ts
485- if (
486- path . basename ( changedFile ) === 'tsconfig.json' ||
487- ( changedFile . endsWith ( '.json' ) &&
488- tsconfckCache ?. hasParseResult ( changedFile ) )
489- ) {
490- server . config . logger . info (
491- `changed tsconfig file detected: ${ changedFile } - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.` ,
492- { clear : server . config . clearScreen , timestamp : true } ,
493- )
494-
495- // clear module graph to remove code compiled with outdated config
496- server . moduleGraph . invalidateAll ( )
497-
498- // reset tsconfck so that recompile works with up2date configs
499- tsconfckCache ?. clear ( )
500-
501- // server may not be available if vite config is updated at the same time
502- if ( server ) {
503- // force full reload
504- server . hot . send ( {
505- type : 'full-reload' ,
506- path : '*' ,
507- } )
507+ if ( changedFile . endsWith ( '.json' ) ) {
508+ const cache = getTSConfckCache ( server . config )
509+ if (
510+ changedFile . endsWith ( '/tsconfig.json' ) ||
511+ cache . hasParseResult ( changedFile )
512+ ) {
513+ server . config . logger . info (
514+ `changed tsconfig file detected: ${ changedFile } - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.` ,
515+ { clear : server . config . clearScreen , timestamp : true } ,
516+ )
517+
518+ // TODO: more finegrained invalidation than the nuclear option below
519+
520+ // clear module graph to remove code compiled with outdated config
521+ for ( const environment of Object . values ( server . environments ) ) {
522+ environment . moduleGraph . invalidateAll ( )
523+ }
524+
525+ // reset tsconfck cache so that recompile works with up2date configs
526+ cache . clear ( )
527+
528+ // reload environments
529+ for ( const environment of Object . values ( server . environments ) ) {
530+ environment . hot . send ( {
531+ type : 'full-reload' ,
532+ path : '*' ,
533+ } )
534+ }
508535 }
509536 }
510537}
0 commit comments