@@ -212,6 +212,7 @@ interface AsyncFunctionIdentifiers {
212
212
markSyntheticPromise : babel . types . Identifier ;
213
213
isSyntheticPromise : babel . types . Identifier ;
214
214
syntheticPromiseSymbol : babel . types . Identifier ;
215
+ demangleError : babel . types . Identifier ;
215
216
}
216
217
/**
217
218
* The second step that performs the heavy lifting of turning regular functions
@@ -232,7 +233,7 @@ interface AsyncFunctionIdentifiers {
232
233
*
233
234
* The README file has more complete code snippets.
234
235
*/
235
- export const makeMaybeAsyncFunctionPlugin = ( { types : t } : { types : typeof BabelTypes } ) : babel . PluginObj < { } > => {
236
+ export const makeMaybeAsyncFunctionPlugin = ( { types : t } : { types : typeof BabelTypes } ) : babel . PluginObj < { file : babel . types . File } > => {
236
237
// We mark certain AST nodes as 'already visited' using these symbols.
237
238
function asNodeKey ( v : any ) : keyof babel . types . Node { return v ; }
238
239
const isGeneratedInnerFunction = asNodeKey ( Symbol ( 'isGeneratedInnerFunction' ) ) ;
@@ -283,13 +284,17 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
283
284
}
284
285
` ) ;
285
286
287
+ // Function.prototype is a good no-op function in situations where
288
+ // function literals receive special treatment :)
286
289
const wrapperFunctionTemplate = babel . template . statements ( `
287
290
let FUNCTION_STATE_IDENTIFIER = "sync",
288
291
SYNC_RETURN_VALUE_IDENTIFIER,
289
292
EXPRESSION_HOLDER_IDENTIFIER;
290
293
291
294
const ASYNC_RETURN_VALUE_IDENTIFIER = (ASYNC_TRY_CATCH_WRAPPER)();
292
295
296
+ if (FUNCTION_STATE_IDENTIFIER !== "sync")
297
+ ASYNC_RETURN_VALUE_IDENTIFIER.catch(Function.prototype);
293
298
if (FUNCTION_STATE_IDENTIFIER === "returned")
294
299
return SYNC_RETURN_VALUE_IDENTIFIER;
295
300
else if (FUNCTION_STATE_IDENTIFIER === "threw")
@@ -299,13 +304,41 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
299
304
` ) ;
300
305
301
306
const awaitSyntheticPromiseTemplate = babel . template . expression ( `(
307
+ ORIGINAL_SOURCE,
302
308
EXPRESSION_HOLDER = NODE,
303
309
ISP_IDENTIFIER(EXPRESSION_HOLDER) ? await EXPRESSION_HOLDER : EXPRESSION_HOLDER
304
310
)` , {
305
311
allowAwaitOutsideFunction : true
306
312
} ) ;
307
313
314
+ const rethrowTemplate = babel . template . statement ( `
315
+ try {
316
+ ORIGINAL_CODE;
317
+ } catch (err) {
318
+ throw err;
319
+ }
320
+ ` ) ;
321
+
322
+ // If we encounter an error object, we fix up the error message from something
323
+ // like `("a" , foo(...)(...)) is not a function` to `a is not a function`.
324
+ // For that, we look for a) the U+FEFF markers we use to tag the original source
325
+ // code with, and b) drop everything else in this parenthesis group (this uses
326
+ // the fact that currently, parentheses in error messages are nested at most
327
+ // two levels deep, which makes it something that we can tackle with regexps).
328
+ const demangleErrorTemplate = babel . template . statement ( String . raw `
329
+ function DE_IDENTIFIER(err) {
330
+ if (Object.prototype.toString.call(err) === '[object Error]' &&
331
+ err.message.includes('\ufeff')) {
332
+ err.message = err.message.replace(/\(\s*"\ufeff(.+?)\ufeff"\s*,(?:[^\(]|\([^\)]*\))*\)/g, '$1');
333
+ }
334
+ return err;
335
+ }
336
+ ` , { placeholderPattern : false , placeholderWhitelist : new Set ( [ 'DE_IDENTIFIER' ] ) } ) ;
337
+
308
338
return {
339
+ pre ( file : babel . types . File ) {
340
+ this . file = file ;
341
+ } ,
309
342
visitor : {
310
343
BlockStatement ( path ) {
311
344
// This might be a function body. If it's what we're looking for, wrap it.
@@ -330,7 +363,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
330
363
331
364
// A parent function might have a set of existing helper methods.
332
365
// If it does, we re-use the functionally equivalent ones.
333
- const existingIdentifiers =
366
+ const existingIdentifiers : AsyncFunctionIdentifiers | null =
334
367
path . findParent ( path => ! ! path . getData ( identifierGroupKey ) ) ?. getData ( identifierGroupKey ) ;
335
368
336
369
// Generate and store a set of identifiers for helpers.
@@ -341,14 +374,16 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
341
374
const markSyntheticPromise = existingIdentifiers ?. markSyntheticPromise ?? path . scope . generateUidIdentifier ( 'msp' ) ;
342
375
const isSyntheticPromise = existingIdentifiers ?. isSyntheticPromise ?? path . scope . generateUidIdentifier ( 'isp' ) ;
343
376
const syntheticPromiseSymbol = existingIdentifiers ?. syntheticPromiseSymbol ?? path . scope . generateUidIdentifier ( 'sp' ) ;
377
+ const demangleError = existingIdentifiers ?. demangleError ?? path . scope . generateUidIdentifier ( 'de' ) ;
344
378
const identifiersGroup : AsyncFunctionIdentifiers = {
345
379
functionState,
346
380
synchronousReturnValue,
347
381
asynchronousReturnValue,
348
382
expressionHolder,
349
383
markSyntheticPromise,
350
384
isSyntheticPromise,
351
- syntheticPromiseSymbol
385
+ syntheticPromiseSymbol,
386
+ demangleError
352
387
} ;
353
388
path . parentPath . setData ( identifierGroupKey , identifiersGroup ) ;
354
389
@@ -385,15 +420,25 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
385
420
SP_IDENTIFIER : syntheticPromiseSymbol
386
421
} ) ,
387
422
{ [ isGeneratedHelper ] : true }
423
+ ) ,
424
+ Object . assign (
425
+ demangleErrorTemplate ( {
426
+ DE_IDENTIFIER : demangleError
427
+ } ) ,
428
+ { [ isGeneratedHelper ] : true }
388
429
)
389
430
] ;
390
431
391
432
if ( path . parentPath . node . async ) {
392
433
// If we are in an async function, no async wrapping is necessary.
393
- // We still want to have the runtime helpers available.
434
+ // We still want to have the runtime helpers available, and we add
435
+ // a re-throwing try/catch around the body so that we can perform
436
+ // error message adjustment through the CatchClause handler below.
394
437
path . replaceWith ( t . blockStatement ( [
395
438
...promiseHelpers ,
396
- ...path . node . body
439
+ rethrowTemplate ( {
440
+ ORIGINAL_CODE : path . node . body
441
+ } )
397
442
] ) ) ;
398
443
return ;
399
444
}
@@ -521,22 +566,62 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
521
566
}
522
567
523
568
// Transform expression `foo` into
524
- // `(ex = foo, isSyntheticPromise(ex) ? await ex : ex)`
569
+ // `('\uFEFFfoo\uFEFF', ex = foo, isSyntheticPromise(ex) ? await ex : ex)`
570
+ // The first part of the sequence expression is used to identify this
571
+ // expression for re-writing error messages, so that we can transform
572
+ // TypeError: ((intermediate value)(intermediate value) , (intermediate value)(intermediate value)(intermediate value)).findx is not a function
573
+ // back into
574
+ // TypeError: db.test.findx is not a function
575
+ // The U+FEFF markers are only used to rule out any practical chance of
576
+ // user code accidentally being recognized as the original source code.
577
+ // We limit the string length so that long expressions (e.g. those
578
+ // containing functions) are not included in full length.
525
579
const { expressionHolder, isSyntheticPromise } = identifierGroup ;
580
+ const originalSource = t . stringLiteral (
581
+ '\ufeff' + limitStringLength (
582
+ ( this . file as any ) . code . slice ( path . node . start , path . node . end ) , 24 ) +
583
+ '\ufeff' ) ;
526
584
path . replaceWith ( Object . assign (
527
585
awaitSyntheticPromiseTemplate ( {
586
+ ORIGINAL_SOURCE : originalSource ,
528
587
EXPRESSION_HOLDER : expressionHolder ,
529
588
ISP_IDENTIFIER : isSyntheticPromise ,
530
589
NODE : path . node
531
590
} ) ,
532
591
{ [ isGeneratedHelper ] : true }
533
592
) ) ;
534
593
}
594
+ } ,
595
+ CatchClause : {
596
+ exit ( path ) {
597
+ if ( path . node [ isGeneratedHelper ] || ! path . node . param || path . node . param . type !== 'Identifier' ) return ;
598
+ const existingIdentifiers : AsyncFunctionIdentifiers | null =
599
+ path . findParent ( path => ! ! path . getData ( identifierGroupKey ) ) ?. getData ( identifierGroupKey ) ;
600
+ if ( ! existingIdentifiers ) return ;
601
+ // Turn `... catch (err) { ... }` into `... catch (err) { err = demangleError(err); ... }`
602
+ path . replaceWith ( Object . assign (
603
+ t . catchClause ( path . node . param ,
604
+ t . blockStatement ( [
605
+ t . expressionStatement (
606
+ t . assignmentExpression ( '=' , path . node . param ,
607
+ t . callExpression ( existingIdentifiers . demangleError , [ path . node . param ] ) ) ) ,
608
+ path . node . body
609
+ ] ) ) ,
610
+ { [ isGeneratedHelper ] : true }
611
+ ) ) ;
612
+ }
535
613
}
536
614
}
537
615
} ;
538
616
} ;
539
617
618
+ function limitStringLength ( input : string , maxLength : number ) {
619
+ if ( input . length <= maxLength ) return input ;
620
+ return input . slice ( 0 , ( maxLength - 5 ) * 0.7 ) +
621
+ ' ... ' +
622
+ input . slice ( input . length - ( maxLength - 5 ) * 0.3 ) ;
623
+ }
624
+
540
625
export default class AsyncWriter {
541
626
step ( code : string , plugins : babel . PluginItem [ ] ) : string {
542
627
return babel . transformSync ( code , {
0 commit comments