diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 60cee8e16bc0dd..68fc1f4db941cf 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -38,6 +38,7 @@ import { import type { PluginContainer } from '../server/pluginContainer' import { createPluginContainer } from '../server/pluginContainer' import { transformGlobImport } from '../plugins/importMetaGlob' +import { loadTsconfigJsonForFile } from '../plugins/esbuild' type ResolveIdOptions = Parameters[2] @@ -217,6 +218,21 @@ async function prepareEsbuildScanner( const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {} + // The plugin pipeline automatically loads the closest tsconfig.json. + // But esbuild doesn't support reading tsconfig.json if the plugin has resolved the path (https://github.com/evanw/esbuild/issues/2265). + // Due to syntax incompatibilities between the experimental decorators in TypeScript and TC39 decorators, + // we cannot simply set `"experimentalDecorators": true` or `false`. (https://github.com/vitejs/vite/pull/15206#discussion_r1417414715) + // 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( + path.join(config.root, '_dummy.js'), + ) + if (tsconfigResult.compilerOptions?.experimentalDecorators) { + tsconfigRaw = { compilerOptions: { experimentalDecorators: true } } + } + } + return await esbuild.context({ absWorkingDir: process.cwd(), write: false, @@ -229,6 +245,7 @@ async function prepareEsbuildScanner( logLevel: 'silent', plugins: [...plugins, plugin], ...esbuildOptions, + tsconfigRaw, }) } diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index ea0c1604beac4a..015e7dc137c3b9 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -440,7 +440,7 @@ function prettifyMessage(m: Message, code: string): string { let tsconfckCache: TSConfckCache | undefined -async function loadTsconfigJsonForFile( +export async function loadTsconfigJsonForFile( filename: string, ): Promise { try { diff --git a/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts b/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts index 4ecdff687fbaa2..a798c51b9bc7cf 100644 --- a/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts +++ b/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts @@ -2,7 +2,7 @@ import path from 'node:path' import fs from 'node:fs' import { transformWithEsbuild } from 'vite' import { describe, expect, test } from 'vitest' -import { browserLogs } from '~utils' +import { browserLogs, isServe, serverLogs } from '~utils' test('should respected each `tsconfig.json`s compilerOptions', () => { // main side effect should be called (because of `"importsNotUsedAsValues": "preserve"`) @@ -21,6 +21,16 @@ test('should respected each `tsconfig.json`s compilerOptions', () => { expect(browserLogs).toContain('data setter in NestedWithExtendsBase') }) +test.runIf(isServe)('scanner should not error with decorators', () => { + expect(serverLogs).not.toStrictEqual( + expect.arrayContaining([ + expect.stringContaining( + 'Parameter decorators only work when experimental decorators are enabled', + ), + ]), + ) +}) + describe('transformWithEsbuild', () => { test('merge tsconfigRaw object', async () => { const main = path.resolve(__dirname, '../src/main.ts') diff --git a/playground/tsconfig-json/src/decorator.ts b/playground/tsconfig-json/src/decorator.ts index 2dc056ec09c809..35ac0600ba4569 100644 --- a/playground/tsconfig-json/src/decorator.ts +++ b/playground/tsconfig-json/src/decorator.ts @@ -1,11 +1,11 @@ +// @ts-nocheck playground/tsconfig.json does not have decorators enabled function first() { return function (...args: any[]) {} } export class Foo { @first() - // @ts-expect-error we intentionally not enable `experimentalDecorators` to test esbuild compat - method(@first test: string) { + method(@first() test: string) { return test } }