Skip to content

Commit b66542e

Browse files
committed
feat: drastically improve the go to source ast
1 parent f07c642 commit b66542e

8 files changed

Lines changed: 646 additions & 37 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Button } from "./button"
2+
13
export const ButtonWithProps = (props: { children: React.ReactNode }) => {
2-
return <button children={props.children} />
4+
return <Button children={props.children} />
35
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { Button } from './button'
2+
import { ButtonWithProps } from './button-with-props-only'
23

34
export const Feature = () => {
45
return (
56
<div>
67
<h2>Feature component</h2>
78
<Button>Nested</Button>
9+
<ButtonWithProps>
10+
With props
11+
</ButtonWithProps>
812
</div>
913
)
1014
}

packages/devtools-vite/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"devDependencies": {
6363
"@types/babel__core": "^7.20.5",
6464
"@types/babel__generator": "^7.27.0",
65-
"@types/babel__traverse": "^7.28.0"
65+
"@types/babel__traverse": "^7.28.0",
66+
"happy-dom": "^18.0.1"
6667
}
6768
}

packages/devtools-vite/src/inject-source.ts

Lines changed: 117 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,131 @@
11
import { normalizePath } from 'vite'
22
import { gen, parse, t, trav } from './babel'
3-
import type { types as Babel } from '@babel/core'
3+
import type { types as Babel, NodePath } from '@babel/core'
44
import type { ParseResult } from '@babel/parser'
55

6+
const getPropsNameFromFunctionDeclaration = (functionDeclaration: t.VariableDeclarator | t.FunctionDeclaration) => {
7+
let propsName: string | null = null;
8+
if (functionDeclaration.type === "FunctionDeclaration") {
9+
const firstArgument = functionDeclaration.params[0];
10+
// handles (props) => {}
11+
if (firstArgument && firstArgument.type === 'Identifier') {
12+
propsName = firstArgument.name;
13+
}
14+
// handles ({ ...props }) => {}
15+
if (firstArgument && firstArgument.type === "ObjectPattern") {
16+
firstArgument.properties.forEach(prop => {
17+
if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
18+
propsName = prop.argument.name;
19+
}
20+
})
21+
}
22+
return propsName;
23+
}
24+
// Arrow function case
25+
if (functionDeclaration.init?.type === "ArrowFunctionExpression" || functionDeclaration.init?.type === "FunctionExpression") {
26+
const firstArgument = functionDeclaration.init.params[0]
27+
// handles (props) => {}
28+
if (firstArgument && firstArgument.type === 'Identifier') {
29+
propsName = firstArgument.name;
30+
}
31+
// handles ({ ...props }) => {}
32+
if (firstArgument && firstArgument.type === "ObjectPattern") {
33+
firstArgument.properties.forEach(prop => {
34+
if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
35+
propsName = prop.argument.name;
36+
}
37+
})
38+
}
39+
}
40+
return propsName;
41+
}
42+
43+
const transformJSX = (element: NodePath<t.JSXOpeningElement>, propsName: string | null, file: string) => {
44+
const loc = element.node.loc
45+
if (!loc) return
46+
const line = loc.start.line
47+
const column = loc.start.column
48+
49+
// Check if props are spread and element name starts with lowercase
50+
const hasSpread = element.node.attributes.some(
51+
(attr) => attr.type === 'JSXSpreadAttribute' && attr.argument.type === "Identifier" && attr.argument.name === propsName,
52+
)
53+
54+
if (hasSpread) {
55+
// Do not inject if props are spread and element is lowercase (native HTML)
56+
return
57+
}
58+
if (propsName) {
59+
// inject data-source either via props or default to a string "<file>:<line>:<column>"
60+
// Inject data-source via props
61+
element.node.attributes.push(
62+
t.jsxAttribute(
63+
t.jsxIdentifier('data-tsd-source'),
64+
t.jsxExpressionContainer(
65+
t.logicalExpression(
66+
'??',
67+
t.memberExpression(
68+
t.identifier(propsName),
69+
t.stringLiteral('data-tsd-source'),
70+
true,
71+
),
72+
t.stringLiteral(`${file}:${line}:${column}`),
73+
),
74+
),
75+
),
76+
)
77+
78+
return true
79+
}
80+
81+
// Inject data-source as a string: "<file>:<line>:<column>"
82+
element.node.attributes.push(
83+
t.jsxAttribute(
84+
t.jsxIdentifier('data-tsd-source'),
85+
t.stringLiteral(`${file}:${line}:${column}`),
86+
),
87+
)
88+
89+
90+
return true
91+
}
92+
93+
694
const transform = (ast: ParseResult<Babel.File>, file: string) => {
795
let didTransform = false
896
trav(ast, {
9-
JSXOpeningElement(path) {
10-
const loc = path.node.loc
11-
if (!loc) return
12-
const line = loc.start.line
13-
const column = loc.start.column
14-
15-
// Check if props are spread and element name starts with lowercase
16-
const hasSpread = path.node.attributes.some(
17-
(attr) => attr.type === 'JSXSpreadAttribute',
18-
)
19-
let isLowercase = false
20-
const nameNode = path.node.name
21-
if (nameNode.type === 'JSXIdentifier') {
22-
isLowercase = /^[a-z]/.test(nameNode.name)
23-
}
24-
25-
if (hasSpread && isLowercase) {
26-
// Do not inject if props are spread and element is lowercase (native HTML)
97+
FunctionDeclaration(functionDeclaration) {
98+
const propsName = getPropsNameFromFunctionDeclaration(functionDeclaration.node)
99+
functionDeclaration.traverse({
100+
JSXOpeningElement(element) {
101+
const transformed = transformJSX(element, propsName, file)
102+
if (transformed) {
103+
didTransform = true
104+
}
105+
}
106+
})
107+
},
108+
VariableDeclaration(path) {
109+
const functionDeclaration = path.node.declarations.find(decl => {
110+
return decl.init?.type === "ArrowFunctionExpression" || decl.init?.type === "FunctionExpression"
111+
})
112+
if (!functionDeclaration) {
27113
return
28114
}
115+
const propsName = getPropsNameFromFunctionDeclaration(functionDeclaration)
29116

30-
// Inject data-source as a string: "<file>:<line>:<column>"
31-
path.node.attributes.push(
32-
t.jsxAttribute(
33-
t.jsxIdentifier('data-tsd-source'),
34-
t.stringLiteral(`${file}:${line}:${column}`),
35-
),
36-
)
117+
path.traverse({
118+
JSXOpeningElement(element) {
119+
const transformed = transformJSX(element, propsName, file)
120+
if (transformed) {
121+
didTransform = true
122+
}
123+
}
124+
})
125+
}
126+
},
37127

38-
didTransform = true
39-
},
40-
})
128+
)
41129

42130
return didTransform
43131
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { describe, it } from 'vitest'
1+
import { describe, expect, it } from 'vitest'
22

33
describe('test', () => {
44
it('works', () => {
5-
// test implementation
5+
expect(true).toBe(true)
66
})
77
})

0 commit comments

Comments
 (0)