Skip to content

Commit

Permalink
fix: tsconfig.json edge-cases in dependencies (#377)
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
  • Loading branch information
glsignal and privatenumber committed Jul 16, 2024
1 parent 6592c42 commit e252654
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 8 deletions.
37 changes: 29 additions & 8 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { LoaderOptions } from './types.js';

const tsconfigCache = new Map<string, TsConfigResult>();

const tsExtensionsPattern = /\.(?:[cm]?ts|[tj]sx)$/;

async function ESBuildLoader(

Check warning on line 20 in src/loader.ts

View workflow job for this annotation

GitHub Actions / Test

Async function 'ESBuildLoader' has a complexity of 20. Maximum allowed is 10
this: webpack.loader.LoaderContext<LoaderOptions>,
source: string,
Expand All @@ -35,28 +37,33 @@ 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
* of whether it actually matches
*
* 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);
Expand All @@ -73,16 +80,30 @@ 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`),
);
}

transformOptions.tsconfigRaw = tsconfig.config as TransformOptions['tsconfigRaw'];
} 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'];
Expand Down
178 changes: 178 additions & 0 deletions tests/specs/tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down

0 comments on commit e252654

Please sign in to comment.