@@ -27,7 +27,9 @@ import {
2727 CallExpression ,
2828 VariableDeclaration ,
2929 ExportDefaultDeclaration ,
30- ExportNamedDeclaration
30+ ExportNamedDeclaration ,
31+ RestElement ,
32+ Pattern
3133} from '@babel/types'
3234import generate from '@babel/generator'
3335
@@ -41,7 +43,7 @@ import {
4143} from './artifacts/artifacts'
4244import { findIgnoredImports , shouldIgnoreCall } from './packageIgnores'
4345import { shouldIgnoreFunctionName } from './excludes'
44- import { FlytrapConfig } from '../core/types'
46+ import { ArtifactMarking , FlytrapConfig } from '../core/types'
4547import { _babelInterop } from './util'
4648import { parseCode } from './parser'
4749import { createHumanLog } from '../core/errors'
@@ -335,3 +337,326 @@ export function flytrapTransformUff(
335337
336338 return _babelInterop ( generate ) ( ast )
337339}
340+
341+ /**
342+ * Transforms code using the new more minimal `uff` wrapper & gathers artifact markings
343+ * at the same time.
344+ *
345+ * @param code
346+ * @param filePath
347+ * @param config
348+ * @returns The generated code and sourcemap
349+ */
350+ export function flytrapTransformWithArtifacts (
351+ code : string ,
352+ filePath : string ,
353+ config ?: Partial < FlytrapConfig > ,
354+ returnArtifacts = false
355+ ) {
356+ const parseResult = parseCode ( code , filePath , config ?. babel ?. parserOptions )
357+
358+ if ( parseResult . err ) {
359+ log . error ( 'error' , parseResult . val . toString ( ) )
360+ throw new Error ( parseResult . val . toString ( ) )
361+ }
362+
363+ const ast = parseResult . val
364+
365+ const artifactMarkings : ArtifactMarking [ ] = [ ]
366+
367+ const extractParamsLocation = ( params : ( Identifier | RestElement | Pattern ) [ ] ) => {
368+ if ( params . length === 0 ) {
369+ return undefined
370+ }
371+ if ( ! params [ 0 ] . start || ! params [ 0 ] . end ) {
372+ // @todo : improved error
373+ throw new Error ( 'invalid params start or end' )
374+ }
375+ const startIndex = params [ 0 ] . start - 1
376+ let endIndex = params [ 0 ] . end + 1
377+ for ( let i = 0 ; i < params . length ; i ++ ) {
378+ endIndex = ( params [ i ] . end as number ) + 1
379+ }
380+
381+ return [ startIndex , endIndex ]
382+ }
383+
384+ const ignoredImports = config ?. packageIgnores
385+ ? findIgnoredImports ( code , config . packageIgnores )
386+ : undefined
387+
388+ try {
389+ _babelInterop ( babelTraverse ) ( ast , {
390+ ...( ! config ?. transformOptions ?. disableTransformation ?. includes ( 'arrow-function' ) && {
391+ ArrowFunctionExpression ( path ) {
392+ if ( ! shouldBeWrappedUff ( path ) ) return
393+
394+ const functionName = extractFunctionName (
395+ path . parent as VariableDeclarator | ObjectProperty
396+ )
397+ const scopes = extractCurrentScope ( path )
398+ const functionId = extractFunctionId ( path , filePath , functionName , scopes )
399+
400+ if ( returnArtifacts ) {
401+ const paramsLocation = extractParamsLocation ( path . node . params )
402+ const firstIndexOfOpenParen = _babelInterop ( generate ) ( path . node ) . code . indexOf ( '(' )
403+
404+ if ( paramsLocation || firstIndexOfOpenParen !== - 1 ) {
405+ artifactMarkings . push ( {
406+ type : 'params' ,
407+ functionOrCallId : functionId ,
408+ startIndex : paramsLocation ?. [ 0 ] ?? path . node . start ! + firstIndexOfOpenParen ,
409+ endIndex : paramsLocation ?. [ 1 ] ?? path . node . start ! + firstIndexOfOpenParen + 2
410+ } )
411+ }
412+ }
413+
414+ const newNode = callExpression ( identifier ( 'uff' ) , [ path . node , stringLiteral ( functionId ) ] )
415+
416+ path . replaceWith ( newNode )
417+ }
418+ } ) ,
419+
420+ ...( ! config ?. transformOptions ?. disableTransformation ?. includes ( 'function-declaration' ) && {
421+ FunctionDeclaration ( path ) {
422+ if ( ! shouldBeWrappedUff ( path ) ) return
423+
424+ const functionName = extractFunctionName ( path . node )
425+ const scopes = extractCurrentScope ( path )
426+ const functionId = extractFunctionId ( path , filePath , functionName , scopes )
427+
428+ if ( returnArtifacts ) {
429+ const paramsLocation = extractParamsLocation ( path . node . params )
430+ const firstIndexOfOpenParen = _babelInterop ( generate ) ( path . node ) . code . indexOf ( '(' )
431+
432+ artifactMarkings . push ( {
433+ type : 'function' ,
434+ functionOrCallId : functionId ,
435+ startIndex : path . node . start ! ,
436+ // @ts -expect-error
437+ endIndex : path . node . id . end
438+ } )
439+ if ( paramsLocation || firstIndexOfOpenParen !== - 1 ) {
440+ artifactMarkings . push ( {
441+ type : 'params' ,
442+ functionOrCallId : functionId ,
443+ startIndex : paramsLocation ?. [ 0 ] ?? path . node . start ! + firstIndexOfOpenParen ,
444+ endIndex : paramsLocation ?. [ 1 ] ?? path . node . start ! + firstIndexOfOpenParen + 2
445+ } )
446+ }
447+ }
448+
449+ const useFlytrapCallExpressionNode = callExpression ( identifier ( 'uff' ) , [
450+ toExpression ( path . node ) ,
451+ stringLiteral ( functionId )
452+ // objectExpression([objectProperty(identifier('id'), stringLiteral(functionId))])
453+ ] )
454+
455+ let transformedNode :
456+ | CallExpression
457+ | VariableDeclaration
458+ | ExportNamedDeclaration
459+ | ExportDefaultDeclaration
460+ | undefined = undefined
461+
462+ // Handle default / named export(s)
463+ if ( path . parent . type === 'ExportDefaultDeclaration' ) {
464+ transformedNode = exportDefaultDeclaration ( useFlytrapCallExpressionNode )
465+ } else if ( path . parent . type === 'ExportNamedDeclaration' ) {
466+ transformedNode = exportNamedDeclaration (
467+ variableDeclaration ( 'const' , [
468+ variableDeclarator (
469+ // @ts -ignore
470+ identifier ( path . node . id . name ) ,
471+ useFlytrapCallExpressionNode
472+ )
473+ ] )
474+ )
475+ } else {
476+ transformedNode = variableDeclaration ( 'const' , [
477+ variableDeclarator (
478+ // @ts -ignore
479+ identifier ( path . node . id . name ) ,
480+ useFlytrapCallExpressionNode
481+ )
482+ ] )
483+ }
484+
485+ // Handle function declaration hoisting
486+ if ( config ?. disableFunctionDeclarationHoisting ) {
487+ path . replaceWith ( transformedNode )
488+ return
489+ }
490+
491+ const scopePath = path . findParent ( ( parentPath ) => {
492+ return parentPath . isBlockStatement ( ) || parentPath . isProgram ( )
493+ } )
494+
495+ if ( scopePath ) {
496+ let lastImportPath : NodePath < ImportDeclaration > | undefined = undefined
497+ const bodyNode = scopePath . get ( 'body' )
498+ if ( Array . isArray ( bodyNode ) ) {
499+ bodyNode . forEach ( ( bodyPath ) => {
500+ if ( bodyPath . isImportDeclaration ( ) ) {
501+ lastImportPath = bodyPath
502+ }
503+ } )
504+ } else if ( bodyNode . isImportDeclaration ( ) ) {
505+ lastImportPath = bodyNode
506+ }
507+
508+ if ( lastImportPath ) {
509+ // Insert after the last import statement
510+ lastImportPath . insertAfter ( transformedNode )
511+ } else {
512+ // @ts -expect-error: Otherwise, insert at the top of the current scope
513+ scopePath . unshiftContainer ( 'body' , transformedNode )
514+ }
515+
516+ // Remove the original function declaration
517+ path . remove ( )
518+ return
519+ } else {
520+ const humanLog = createHumanLog ( {
521+ events : [ 'transform_hoisting_failed' ] ,
522+ explanations : [ 'transform_parent_scope_not_found' ] ,
523+ solutions : [ 'open_issue' , 'join_discord' ] ,
524+ params : {
525+ fileNamePath : filePath ,
526+ functionName
527+ }
528+ } )
529+
530+ log . warn ( 'transform' , humanLog . toString ( ) )
531+ }
532+ // No hoisting if there is no parent scope
533+ path . replaceWith ( transformedNode )
534+ }
535+ } ) ,
536+
537+ ...( ! config ?. transformOptions ?. disableTransformation ?. includes ( 'function-expression' ) && {
538+ FunctionExpression ( path ) {
539+ if ( ! shouldBeWrappedUff ( path ) ) return
540+ if ( isVariableDeclarator ( path . parent ) ) {
541+ const functionName =
542+ path . node . id ?. name ??
543+ extractFunctionName ( path . parent as VariableDeclarator | ObjectProperty )
544+ const scopes = extractCurrentScope ( path )
545+ const functionId = extractFunctionId ( path , filePath , functionName , scopes )
546+
547+ if ( returnArtifacts ) {
548+ const paramsLocation = extractParamsLocation ( path . node . params )
549+ const firstIndexOfOpenParen = _babelInterop ( generate ) ( path . node ) . code . indexOf ( '(' )
550+
551+ artifactMarkings . push ( {
552+ type : 'function' ,
553+ functionOrCallId : functionId ,
554+ startIndex : path . node . start ! ,
555+ endIndex : path . node . start ! + firstIndexOfOpenParen
556+ } )
557+ if ( paramsLocation || firstIndexOfOpenParen !== - 1 ) {
558+ artifactMarkings . push ( {
559+ type : 'params' ,
560+ functionOrCallId : functionId ,
561+ startIndex : paramsLocation ?. [ 0 ] ?? path . node . start ! + firstIndexOfOpenParen ,
562+ endIndex : paramsLocation ?. [ 1 ] ?? path . node . start ! + firstIndexOfOpenParen + 2
563+ } )
564+ }
565+ }
566+
567+ const transformedNode = callExpression ( identifier ( 'uff' ) , [
568+ path . node ,
569+ stringLiteral ( functionId )
570+ // objectExpression([objectProperty(identifier('id'), stringLiteral(functionId))])
571+ ] )
572+ path . replaceWith ( transformedNode )
573+ }
574+ }
575+ } ) ,
576+
577+ ...( ! config ?. transformOptions ?. disableTransformation ?. includes ( 'call-expression' ) && {
578+ CallExpression ( path ) {
579+ if ( ! shouldBeWrappedUff ( path ) ) return
580+
581+ // Ignored calls (eg. packageIgnores & reserved words)
582+ if (
583+ shouldIgnoreCall ( path , ignoredImports ?? [ ] ) ||
584+ shouldIgnoreFunctionName ( path , config ?. excludeFunctionNames ?? [ ] )
585+ ) {
586+ return
587+ }
588+
589+ const fullFunctionCallName = _babelInterop ( generate ) ( {
590+ ...path . node ,
591+ arguments : [ ]
592+ } ) . code . replaceAll ( '()' , '' )
593+
594+ if ( fullFunctionCallName === 'this' || fullFunctionCallName . split ( '.' ) . at ( 0 ) === 'this' ) {
595+ return
596+ }
597+ const functionCallName = fullFunctionCallName . split ( '.' ) . at ( - 1 ) !
598+ const scopes = extractCurrentScope ( path )
599+ const functionCallId = extractFunctionCallId ( path , filePath , functionCallName , scopes )
600+
601+ if ( returnArtifacts ) {
602+ const paramsLocation = extractParamsLocation ( path . node . arguments as Identifier [ ] )
603+ const firstIndexOfOpenParen = _babelInterop ( generate ) ( path . node ) . code . indexOf ( '(' )
604+ artifactMarkings . push ( {
605+ type : 'call' ,
606+ startIndex : path . node . start ! ,
607+ endIndex : path . node . start ! + firstIndexOfOpenParen ,
608+ functionOrCallId : functionCallId
609+ } )
610+ if ( paramsLocation || firstIndexOfOpenParen !== - 1 ) {
611+ artifactMarkings . push ( {
612+ type : 'arguments' ,
613+ functionOrCallId : functionCallId ,
614+ startIndex : paramsLocation ?. [ 0 ] ?? path . node . start ! + firstIndexOfOpenParen ,
615+ endIndex : paramsLocation ?. [ 1 ] ?? path . node . start ! + firstIndexOfOpenParen + 2
616+ } )
617+ }
618+ }
619+
620+ const useFunctionName = isAwaitExpression ( path . parent ) ? 'ufc' : 'ufc'
621+
622+ // @ts -ignore
623+ const { callee, accessorKey } = getCalleeAndAccessorKey ( path . node . callee )
624+
625+ if ( ! callee ) {
626+ throw new Error ( 'Callee is undefined. CODE: ' + generate ( path . node ) . code )
627+ }
628+
629+ const newNode = callExpression ( identifier ( useFunctionName ) , [
630+ callee ,
631+ objectExpression ( [
632+ objectProperty ( identifier ( 'id' ) , stringLiteral ( functionCallId ) ) ,
633+ // @ts -ignore
634+ objectProperty ( identifier ( 'args' ) , arrayExpression ( path . node . arguments ) ) ,
635+ objectProperty (
636+ identifier ( 'name' ) ,
637+ // @ts -expect-error
638+ accessorKey ? accessorKey : stringLiteral ( functionCallName )
639+ )
640+ ] )
641+ ] )
642+
643+ path . replaceWith ( newNode )
644+ }
645+ } )
646+ } )
647+ } catch ( e ) {
648+ const errorLog = createHumanLog ( {
649+ events : [ 'transform_file_failed' ] ,
650+ explanations : [ 'traverse_failed' ] ,
651+ solutions : [ 'open_issue' ] ,
652+ params : {
653+ fileNamePath : filePath ,
654+ traverseError : String ( e )
655+ }
656+ } )
657+
658+ throw errorLog . toString ( )
659+ }
660+
661+ return { code : _babelInterop ( generate ) ( ast ) , artifactMarkings }
662+ }
0 commit comments