Skip to content

Commit

Permalink
refactor: separate tsconfck caches per config in a weakmap (#17317)
Browse files Browse the repository at this point in the history
Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
  • Loading branch information
dominikg and sapphi-red authored Oct 30, 2024
1 parent eccf663 commit b9b01d5
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 75 deletions.
10 changes: 7 additions & 3 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,9 +1090,13 @@ export async function extractExportsData(
debug?.(
`Unable to parse: ${filePath}.\n Trying again with a ${loader} transform.`,
)
const transformed = await transformWithEsbuild(entryContent, filePath, {
loader,
})
const transformed = await transformWithEsbuild(
entryContent,
filePath,
{ loader },
undefined,
environment.config,
)
parseResult = parse(transformed.code)
usedJsxLoader = true
}
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/optimizer/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,10 @@ async function prepareEsbuildScanner(
// Therefore, we use the closest tsconfig.json from the root to make it work in most cases.
let tsconfigRaw = esbuildOptions.tsconfigRaw
if (!tsconfigRaw && !esbuildOptions.tsconfig) {
const tsconfigResult = await loadTsconfigJsonForFile(
const { tsconfig } = await loadTsconfigJsonForFile(
path.join(environment.config.root, '_dummy.js'),
)
if (tsconfigResult.compilerOptions?.experimentalDecorators) {
if (tsconfig.compilerOptions?.experimentalDecorators) {
tsconfigRaw = { compilerOptions: { experimentalDecorators: true } }
}
}
Expand Down
167 changes: 97 additions & 70 deletions packages/vite/src/node/plugins/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { RawSourceMap } from '@ampproject/remapping'
import type { InternalModuleFormat, SourceMap } from 'rollup'
import type { TSConfckParseResult } from 'tsconfck'
import { TSConfckCache, TSConfckParseError, parse } from 'tsconfck'
import type { FSWatcher } from 'dep-types/chokidar'
import {
combineSourcemaps,
createDebugger,
Expand Down Expand Up @@ -41,10 +42,6 @@ export const defaultEsbuildSupported = {
'import-meta': true,
}

// TODO: rework to avoid caching the server for this module.
// If two servers are created in the same process, they will interfere with each other.
let server: ViteDevServer

export interface ESBuildOptions extends TransformOptions {
include?: string | RegExp | string[] | RegExp[]
exclude?: string | RegExp | string[] | RegExp[]
Expand Down Expand Up @@ -83,6 +80,8 @@ export async function transformWithEsbuild(
filename: string,
options?: TransformOptions,
inMap?: object,
config?: ResolvedConfig,
watcher?: FSWatcher,
): Promise<ESBuildTransformResult> {
let loader = options?.loader

Expand Down Expand Up @@ -123,14 +122,29 @@ export async function transformWithEsbuild(
]
const compilerOptionsForFile: TSCompilerOptions = {}
if (loader === 'ts' || loader === 'tsx') {
const loadedTsconfig = await loadTsconfigJsonForFile(filename)
const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {}
try {
const { tsconfig: loadedTsconfig, tsconfigFile } =
await loadTsconfigJsonForFile(filename, config)
// tsconfig could be out of root, make sure it is watched on dev
if (watcher && tsconfigFile && config) {
ensureWatchedFile(watcher, tsconfigFile, config.root)
}
const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {}

for (const field of meaningfulFields) {
if (field in loadedCompilerOptions) {
// @ts-expect-error TypeScript can't tell they are of the same type
compilerOptionsForFile[field] = loadedCompilerOptions[field]
for (const field of meaningfulFields) {
if (field in loadedCompilerOptions) {
// @ts-expect-error TypeScript can't tell they are of the same type
compilerOptionsForFile[field] = loadedCompilerOptions[field]
}
}
} catch (e) {
if (e instanceof TSConfckParseError) {
// tsconfig could be out of root, make sure it is watched on dev
if (watcher && e.tsconfigFile && config) {
ensureWatchedFile(watcher, e.tsconfigFile, config.root)
}
}
throw e
}
}

Expand Down Expand Up @@ -251,22 +265,23 @@ export function esbuildPlugin(config: ResolvedConfig): Plugin {
},
}

let server: ViteDevServer

return {
name: 'vite:esbuild',
configureServer(_server) {
server = _server
server.watcher
.on('add', reloadOnTsconfigChange)
.on('change', reloadOnTsconfigChange)
.on('unlink', reloadOnTsconfigChange)
},
buildEnd() {
// recycle serve to avoid preventing Node self-exit (#6815)
server = null as any
},
async transform(code, id) {
if (filter(id) || filter(cleanUrl(id))) {
const result = await transformWithEsbuild(code, id, transformOptions)
const result = await transformWithEsbuild(
code,
id,
transformOptions,
undefined,
config,
server?.watcher,
)
if (result.warnings.length) {
result.warnings.forEach((m) => {
this.warn(prettifyMessage(m, code))
Expand Down Expand Up @@ -317,7 +332,13 @@ export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => {
return null
}

const res = await transformWithEsbuild(code, chunk.fileName, options)
const res = await transformWithEsbuild(
code,
chunk.fileName,
options,
undefined,
config,
)

if (config.build.lib) {
// #7188, esbuild adds helpers out of the UMD and IIFE wrappers, and the
Expand Down Expand Up @@ -448,63 +469,69 @@ function prettifyMessage(m: Message, code: string): string {
return res + `\n`
}

let tsconfckCache: TSConfckCache<TSConfckParseResult> | undefined
let globalTSConfckCache: TSConfckCache<TSConfckParseResult> | undefined
const tsconfckCacheMap = new WeakMap<
ResolvedConfig,
TSConfckCache<TSConfckParseResult>
>()

function getTSConfckCache(config?: ResolvedConfig) {
if (!config) {
return (globalTSConfckCache ??= new TSConfckCache<TSConfckParseResult>())
}
let cache = tsconfckCacheMap.get(config)
if (!cache) {
cache = new TSConfckCache<TSConfckParseResult>()
tsconfckCacheMap.set(config, cache)
}
return cache
}

export async function loadTsconfigJsonForFile(
filename: string,
): Promise<TSConfigJSON> {
try {
if (!tsconfckCache) {
tsconfckCache = new TSConfckCache<TSConfckParseResult>()
}
const result = await parse(filename, {
cache: tsconfckCache,
ignoreNodeModules: true,
})
// tsconfig could be out of root, make sure it is watched on dev
if (server && result.tsconfigFile) {
ensureWatchedFile(server.watcher, result.tsconfigFile, server.config.root)
}
return result.tsconfig
} catch (e) {
if (e instanceof TSConfckParseError) {
// tsconfig could be out of root, make sure it is watched on dev
if (server && e.tsconfigFile) {
ensureWatchedFile(server.watcher, e.tsconfigFile, server.config.root)
}
}
throw e
}
config?: ResolvedConfig,
): Promise<{ tsconfigFile: string; tsconfig: TSConfigJSON }> {
const { tsconfig, tsconfigFile } = await parse(filename, {
cache: getTSConfckCache(config),
ignoreNodeModules: true,
})
return { tsconfigFile, tsconfig }
}

async function reloadOnTsconfigChange(changedFile: string) {
// server could be closed externally after a file change is detected
if (!server) return
export async function reloadOnTsconfigChange(
server: ViteDevServer,
changedFile: string,
): Promise<void> {
// any tsconfig.json that's added in the workspace could be closer to a code file than a previously cached one
// any json file in the tsconfig cache could have been used to compile ts
if (
path.basename(changedFile) === 'tsconfig.json' ||
(changedFile.endsWith('.json') &&
tsconfckCache?.hasParseResult(changedFile))
) {
server.config.logger.info(
`changed tsconfig file detected: ${changedFile} - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.`,
{ clear: server.config.clearScreen, timestamp: true },
)

// clear module graph to remove code compiled with outdated config
server.moduleGraph.invalidateAll()

// reset tsconfck so that recompile works with up2date configs
tsconfckCache?.clear()

// server may not be available if vite config is updated at the same time
if (server) {
// force full reload
server.hot.send({
type: 'full-reload',
path: '*',
})
if (changedFile.endsWith('.json')) {
const cache = getTSConfckCache(server.config)
if (
changedFile.endsWith('/tsconfig.json') ||
cache.hasParseResult(changedFile)
) {
server.config.logger.info(
`changed tsconfig file detected: ${changedFile} - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.`,
{ clear: server.config.clearScreen, timestamp: true },
)

// TODO: more finegrained invalidation than the nuclear option below

// clear module graph to remove code compiled with outdated config
for (const environment of Object.values(server.environments)) {
environment.moduleGraph.invalidateAll()
}

// reset tsconfck cache so that recompile works with up2date configs
cache.clear()

// reload environments
for (const environment of Object.values(server.environments)) {
environment.hot.send({
type: 'full-reload',
path: '*',
})
}
}
}
}
3 changes: 3 additions & 0 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { getFsUtils } from '../fsUtils'
import { ssrLoadModule } from '../ssr/ssrModuleLoader'
import { ssrFixStacktrace, ssrRewriteStacktrace } from '../ssr/ssrStacktrace'
import { ssrTransform } from '../ssr/ssrTransform'
import { reloadOnTsconfigChange } from '../plugins/esbuild'
import { bindCLIShortcuts } from '../shortcuts'
import type { BindCLIShortcutsOptions } from '../shortcuts'
import {
Expand Down Expand Up @@ -761,6 +762,7 @@ export async function _createServer(

const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
file = normalizePath(file)
reloadOnTsconfigChange(server, file)

await pluginContainer.watchChange(file, {
event: isUnlink ? 'delete' : 'create',
Expand Down Expand Up @@ -794,6 +796,7 @@ export async function _createServer(

watcher.on('change', async (file) => {
file = normalizePath(file)
reloadOnTsconfigChange(server, file)

await pluginContainer.watchChange(file, { event: 'update' })
// invalidate module graph cache on file change
Expand Down

0 comments on commit b9b01d5

Please sign in to comment.