Skip to content

Commit f4066b1

Browse files
committed
fix: combine artifacts markings generation with transform to create accurate artifacts markings
1 parent 8ccebe7 commit f4066b1

File tree

2 files changed

+353
-10
lines changed

2 files changed

+353
-10
lines changed

src/transform/index.ts

Lines changed: 327 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {
2727
CallExpression,
2828
VariableDeclaration,
2929
ExportDefaultDeclaration,
30-
ExportNamedDeclaration
30+
ExportNamedDeclaration,
31+
RestElement,
32+
Pattern
3133
} from '@babel/types'
3234
import generate from '@babel/generator'
3335

@@ -41,7 +43,7 @@ import {
4143
} from './artifacts/artifacts'
4244
import { findIgnoredImports, shouldIgnoreCall } from './packageIgnores'
4345
import { shouldIgnoreFunctionName } from './excludes'
44-
import { FlytrapConfig } from '../core/types'
46+
import { ArtifactMarking, FlytrapConfig } from '../core/types'
4547
import { _babelInterop } from './util'
4648
import { parseCode } from './parser'
4749
import { 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+
}

test/artifacts.test.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from '../src/transform/artifacts/artifacts'
77
import babelTraverse from '@babel/traverse'
88
import { parse } from '@babel/parser'
9-
import { flytrapTransformUff } from '../src/transform/index'
9+
import { flytrapTransformWithArtifacts } from '../src/transform/index'
1010
import { config } from 'dotenv'
1111
import { getParseConfig } from '../src/transform/config'
1212
import { _babelInterop } from '../src/transform/util'
@@ -155,18 +155,36 @@ function Home() {
155155
function submit() {}
156156
return null
157157
}
158+
159+
export const DashboardLayout = ({ children }: any) => {
160+
const supabaseClient = useSupabaseClient()
161+
const user = useUser()
162+
163+
useEffect(() => {
164+
console.log(user)
165+
}, [user])
166+
167+
function signOut() {
168+
console.log("Signed out")
169+
}
170+
171+
return (
172+
<h1>{user?.name}</h1>
173+
)
174+
}
158175
`
159176

160177
it('generates values same as transform', () => {
161-
const artifactMarkingsResult = addArtifactMarkings(pageCodeFixture, '/file.js')
162-
if (artifactMarkingsResult.err) {
163-
throw new Error(artifactMarkingsResult.val.toString())
164-
}
165-
const transformedCode = flytrapTransformUff(pageCodeFixture, '/file.js')
178+
const { code, artifactMarkings } = flytrapTransformWithArtifacts(
179+
pageCodeFixture,
180+
'/file.js',
181+
undefined,
182+
true
183+
)
184+
const functionIds = artifactMarkings.map((a) => a.functionOrCallId)
166185

167-
const functionIds = artifactMarkingsResult.val.map((e) => e.functionOrCallId).filter(Boolean)
168186
for (let i = 0; i < functionIds.length; i++) {
169-
expect(transformedCode.code).toContain(functionIds[i] as string)
187+
expect(code.code).toContain(functionIds[i] as string)
170188
}
171189
})
172190

0 commit comments

Comments
 (0)