Skip to content

Commit d68a150

Browse files
committed
loader: TSError that survives thread comms channel
When an error is thrown in the loader thread, it must be passed through the comms channel to be printed in the main thread. Node has some heuristics to try to reconstitute errors properly, but they don't function very well if the error has a custom inspect method, or properties that are not compatible with JSON.stringify, so the TSErrors raised by the source transforms don't get printed in any sort of useful way. This catches those errors, and creates a new error that can go through the comms channel intact. Another possible approach would be to update the shape of the errors raised by source transforms, but that would be a much more extensive change with further reaching consequences.
1 parent db1fdf9 commit d68a150

File tree

2 files changed

+28
-12
lines changed

2 files changed

+28
-12
lines changed

src/esm.ts

Lines changed: 27 additions & 11 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, type 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';
@@ -223,7 +223,7 @@ export function createEsmHooks(tsNodeService: Service) {
223223
format: NodeLoaderHooksFormat;
224224
source: string | Buffer | undefined;
225225
}> {
226-
return addShortCircuitFlag(async () => {
226+
return await addShortCircuitFlag(async () => {
227227
// If we get a format hint from resolve() on the context then use it
228228
// otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node
229229
const format =
@@ -251,8 +251,23 @@ export function createEsmHooks(tsNodeService: Service) {
251251
});
252252

253253
// Call the old hook
254-
const { source: transformedSource } = await transformSource(rawSource, { url, format }, defaultTransformSource);
255-
source = transformedSource;
254+
try {
255+
const { source: transformedSource } = await transformSource(
256+
rawSource,
257+
{ url, format },
258+
defaultTransformSource
259+
);
260+
source = transformedSource;
261+
} catch (er) {
262+
// throw an error that can make it through the loader thread
263+
// comms channel intact.
264+
const tsErr = er as TSError;
265+
const err = new Error(tsErr.message.trimEnd());
266+
const { diagnosticCodes } = tsErr;
267+
Object.assign(err, { diagnosticCodes });
268+
Error.captureStackTrace(err, load);
269+
throw err;
270+
}
256271
}
257272

258273
return { format, source };
@@ -360,11 +375,12 @@ export function createEsmHooks(tsNodeService: Service) {
360375
}
361376

362377
async function addShortCircuitFlag<T>(fn: () => Promise<T>) {
363-
const ret = await fn();
364-
// Not sure if this is necessary; being lazy. Can revisit in the future.
365-
if (ret == null) return ret;
366-
return {
367-
...ret,
368-
shortCircuit: true,
369-
};
378+
return fn().then((ret) => {
379+
// Not sure if this is necessary; being lazy. Can revisit in the future.
380+
if (ret == null) return ret;
381+
return {
382+
...ret,
383+
shortCircuit: true,
384+
};
385+
});
370386
}

tests/esm-custom-loader/loader.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ const tsNodeInstance = register({
1111
},
1212
});
1313

14-
export const { resolve, getFormat, transformSource, load } = createEsmHooks(tsNodeInstance);
14+
export const { globalPreload, resolve, getFormat, transformSource, load } = createEsmHooks(tsNodeInstance);

0 commit comments

Comments
 (0)