Skip to content

Commit 49ea0c5

Browse files
fix: serialize ESM loader TypeScript errors
1 parent ddb05ef commit 49ea0c5

5 files changed

Lines changed: 71 additions & 6 deletions

File tree

dprint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"/website/readme-sources",
2525
"/website/static",
2626
"tests/main-realpath/symlink/tsconfig.json",
27+
"tests/esm-invalid-tsconfig/tsconfig.json",
2728
"tests/throw error.ts",
2829
"tests/throw error react tsx.tsx",
2930
"tests/esm/throw error.ts",

src/esm.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { register, RegisterOptions, Service } from './index';
1+
import { register, RegisterOptions, Service, TSError } from './index';
22
import { parse as parseUrl, format as formatUrl, UrlWithStringQuery, fileURLToPath, pathToFileURL } from 'url';
33
import { extname, resolve as pathResolve } from 'path';
44
import * as assert from 'assert';
@@ -101,7 +101,12 @@ export function filterHooksByAPIVersion(
101101
/** @internal */
102102
export function registerAndCreateEsmHooks(opts?: RegisterOptions) {
103103
// Automatically performs registration just like `-r ts-node/register`
104-
const tsNodeInstance = register(opts);
104+
let tsNodeInstance: Service;
105+
try {
106+
tsNodeInstance = register(opts);
107+
} catch (error) {
108+
throw makeSerializableLoaderError(error);
109+
}
105110

106111
return createEsmHooks(tsNodeInstance);
107112
}
@@ -113,10 +118,10 @@ export function createEsmHooks(tsNodeService: Service) {
113118
const extensions = tsNodeService.extensions;
114119

115120
const hooksAPI = filterHooksByAPIVersion({
116-
resolve,
117-
load,
118-
getFormat,
119-
transformSource,
121+
resolve: wrapHook(resolve),
122+
load: wrapHook(load),
123+
getFormat: wrapHook(getFormat),
124+
transformSource: wrapHook(transformSource),
120125
});
121126

122127
function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) {
@@ -356,3 +361,48 @@ async function addShortCircuitFlag<T>(fn: () => Promise<T>) {
356361
shortCircuit: true,
357362
};
358363
}
364+
365+
function wrapHook<T extends (...args: any[]) => Promise<any>>(hook: T): T {
366+
return (async (...args: Parameters<T>) => {
367+
try {
368+
return await hook(...args);
369+
} catch (error) {
370+
throw makeSerializableLoaderError(error);
371+
}
372+
}) as T;
373+
}
374+
375+
function makeSerializableLoaderError(error: unknown) {
376+
if (error instanceof TSError || isTSError(error)) {
377+
const serializable = new Error(error.message);
378+
serializable.name = error.name;
379+
if (typeof error.stack === 'string') {
380+
serializable.stack = error.stack;
381+
}
382+
Object.defineProperty(serializable, 'diagnosticText', {
383+
configurable: true,
384+
enumerable: true,
385+
writable: true,
386+
value: error.diagnosticText,
387+
});
388+
Object.defineProperty(serializable, 'diagnosticCodes', {
389+
configurable: true,
390+
enumerable: true,
391+
writable: true,
392+
value: error.diagnosticCodes,
393+
});
394+
return serializable;
395+
}
396+
return error;
397+
}
398+
399+
function isTSError(error: unknown): error is TSError {
400+
return (
401+
typeof error === 'object' &&
402+
error !== null &&
403+
(error as TSError).name === 'TSError' &&
404+
typeof (error as TSError).message === 'string' &&
405+
typeof (error as TSError).diagnosticText === 'string' &&
406+
Array.isArray((error as TSError).diagnosticCodes)
407+
);
408+
}

src/test/esm-loader.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ test.suite('esm', (test) => {
115115
expect(r.err).toBe(null);
116116
expect(r.stdout).toBe('');
117117
});
118+
test('reports invalid tsconfig diagnostics from the ESM loader', async () => {
119+
const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} -e "console.log(1)"`, {
120+
cwd: join(TEST_DIR, './esm-invalid-tsconfig'),
121+
});
122+
123+
expect(r.err).not.toBe(null);
124+
expect(r.stderr).toMatch('Unable to compile TypeScript');
125+
expect(r.stderr).toMatch("tsconfig.json(1,5): error TS1005: ':' expected.");
126+
expect(r.stderr).not.toMatch('[Object: null prototype]');
127+
});
118128
test('should throw type errors without transpile-only enabled', async () => {
119129
const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, {
120130
cwd: join(TEST_DIR, './esm-transpile-only'),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ 1 }

0 commit comments

Comments
 (0)