Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: separate tsconfck caches per config in a weakmap #17317

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -299,10 +299,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 @@ -751,6 +752,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 @@ -784,6 +786,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
Loading