diff --git a/src/loader.ts b/src/loader.ts index c8453b3..4cb70a7 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -15,6 +15,8 @@ import type { LoaderOptions } from './types.js'; const tsconfigCache = new Map(); +const tsExtensionsPattern = /\.(?:[cm]?ts|[tj]sx)$/; + async function ESBuildLoader( this: webpack.loader.LoaderContext, source: string, @@ -35,20 +37,25 @@ async function ESBuildLoader( ); return; } - const transform = implementation?.transform ?? defaultEsbuildTransform; + const { resourcePath } = this; const transformOptions = { ...esbuildTransformOptions, target: options.target ?? 'es2015', loader: options.loader ?? 'default', sourcemap: this.sourceMap, - sourcefile: this.resourcePath, + sourcefile: resourcePath, }; - if (!('tsconfigRaw' in transformOptions)) { - const { resourcePath } = this; + const isDependency = resourcePath.includes(`${path.sep}node_modules${path.sep}`); + if ( + !('tsconfigRaw' in transformOptions) + // If file is local project, always try to apply tsconfig.json (e.g. allowJs) + // If file is dependency, only apply tsconfig.json if .ts + && (!isDependency || tsExtensionsPattern.test(resourcePath)) + ) { /** * If a tsconfig.json path is specified, force apply it * Same way a provided tsconfigRaw is applied regardless @@ -56,7 +63,7 @@ async function ESBuildLoader( * * However in this case, we also warn if it doesn't match */ - if (tsconfigPath) { + if (!isDependency && tsconfigPath) { const tsconfigFullPath = path.resolve(tsconfigPath); const cacheKey = `esbuild-loader:${tsconfigFullPath}`; let tsconfig = tsconfigCache.get(cacheKey); @@ -73,7 +80,7 @@ async function ESBuildLoader( if (!matches) { this.emitWarning( - new Error(`[esbuild-loader] The specified tsconfig at "${tsconfigFullPath}" was applied to the file "${resourcePath}" but does not match its "include" patterns`), + new Error(`esbuild-loader] The specified tsconfig at "${tsconfigFullPath}" was applied to the file "${resourcePath}" but does not match its "include" patterns`), ); } @@ -81,8 +88,22 @@ async function ESBuildLoader( } else { /* Detect tsconfig file */ - // Webpack shouldn't be loading the same path multiple times so doesn't need to be cached - const tsconfig = getTsconfig(resourcePath, 'tsconfig.json', tsconfigCache); + let tsconfig; + + try { + // Webpack shouldn't be loading the same path multiple times so doesn't need to be cached + tsconfig = getTsconfig(resourcePath, 'tsconfig.json', tsconfigCache); + } catch (error) { + if (error instanceof Error) { + const tsconfigError = new Error(`[esbuild-loader] Error parsing tsconfig.json:\n${error.message}`); + if (isDependency) { + this.emitWarning(tsconfigError); + } else { + return done(tsconfigError); + } + } + } + if (tsconfig) { const fileMatcher = createFilesMatcher(tsconfig); transformOptions.tsconfigRaw = fileMatcher(resourcePath) as TransformOptions['tsconfigRaw']; diff --git a/tests/specs/tsconfig.ts b/tests/specs/tsconfig.ts index 57e6a28..f0b0b63 100644 --- a/tests/specs/tsconfig.ts +++ b/tests/specs/tsconfig.ts @@ -262,6 +262,184 @@ export default testSuite(({ describe }) => { const code2 = await fixture.readFile('dist/index2.js', 'utf8'); expect(code2).toMatch('__publicField(this, "foo", 100);'); }); + + test('fails on invalid tsconfig.json', async () => { + await using fixture = await createFixture({ + 'tsconfig.json': tsconfigJson({ + extends: 'unresolvable-dep', + }), + src: { + 'index.ts': ` + console.log('Hello, world!' as numer); + `, + }, + 'webpack.config.js': ` + module.exports = { + mode: 'production', + + optimization: { + minimize: false, + }, + + resolveLoader: { + alias: { + 'esbuild-loader': ${JSON.stringify(esbuildLoader)}, + }, + }, + + resolve: { + extensions: ['.ts', '.js'], + }, + + module: { + rules: [ + { + test: /.[tj]sx?$/, + loader: 'esbuild-loader', + options: { + target: 'es2015', + } + } + ], + }, + + entry: { + index: './src/index.ts', + }, + }; + `, + }); + + const { stdout, exitCode } = await execa(webpackCli, { + cwd: fixture.path, + reject: false, + }); + + expect(stdout).toMatch('Error parsing tsconfig.json:\nFile \'unresolvable-dep\' not found.'); + expect(exitCode).toBe(1); + }); + + test('ignores invalid tsconfig.json in JS dependencies', async () => { + await using fixture = await createFixture({ + 'node_modules/fake-lib': { + 'package.json': JSON.stringify({ + name: 'fake-lib', + }), + 'tsconfig.json': tsconfigJson({ + extends: 'unresolvable-dep', + }), + 'index.js': 'export function testFn() { return "Hi!" }', + }, + 'src/index.ts': ` + import { testFn } from "fake-lib"; + testFn(); + `, + 'webpack.config.js': ` + module.exports = { + mode: 'production', + + optimization: { + minimize: false, + }, + + resolveLoader: { + alias: { + 'esbuild-loader': ${JSON.stringify(esbuildLoader)}, + }, + }, + + resolve: { + extensions: ['.ts', '.js'], + }, + + module: { + rules: [ + { + test: /.[tj]sx?$/, + loader: 'esbuild-loader', + options: { + target: 'es2015', + } + } + ], + }, + + entry: { + index: './src/index.ts', + }, + }; + `, + }); + + const { stdout, exitCode } = await execa(webpackCli, { + cwd: fixture.path, + }); + + expect(stdout).not.toMatch('Error parsing tsconfig.json'); + expect(exitCode).toBe(0); + }); + + test('warns on invalid tsconfig.json in TS dependencies', async () => { + await using fixture = await createFixture({ + 'node_modules/fake-lib': { + 'package.json': JSON.stringify({ + name: 'fake-lib', + }), + 'tsconfig.json': tsconfigJson({ + extends: 'unresolvable-dep', + }), + 'index.ts': 'export function testFn(): string { return "Hi!" }', + }, + 'src/index.ts': ` + import { testFn } from "fake-lib"; + testFn(); + `, + 'webpack.config.js': ` + module.exports = { + mode: 'production', + + optimization: { + minimize: false, + }, + + resolveLoader: { + alias: { + 'esbuild-loader': ${JSON.stringify(esbuildLoader)}, + }, + }, + + resolve: { + extensions: ['.ts', '.js'], + }, + + module: { + rules: [ + { + test: /.[tj]sx?$/, + loader: 'esbuild-loader', + options: { + target: 'es2015', + } + } + ], + }, + + entry: { + index: './src/index.ts', + }, + }; + `, + }); + + const { stdout, exitCode } = await execa(webpackCli, { + cwd: fixture.path, + }); + + expect(stdout).toMatch('Error parsing tsconfig.json:\nFile \'unresolvable-dep\' not found.'); + + // Warning so doesn't fail + expect(exitCode).toBe(0); + }); }); describe('plugin', ({ test }) => {