|
1 | 1 | import { normalizePath } from 'vite' |
2 | 2 | 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' |
4 | 4 | import type { ParseResult } from '@babel/parser' |
5 | 5 |
|
| 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 | + |
6 | 94 | const transform = (ast: ParseResult<Babel.File>, file: string) => { |
7 | 95 | let didTransform = false |
8 | 96 | 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) { |
27 | 113 | return |
28 | 114 | } |
| 115 | + const propsName = getPropsNameFromFunctionDeclaration(functionDeclaration) |
29 | 116 |
|
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 | + }, |
37 | 127 |
|
38 | | - didTransform = true |
39 | | - }, |
40 | | - }) |
| 128 | + ) |
41 | 129 |
|
42 | 130 | return didTransform |
43 | 131 | } |
|
0 commit comments