-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtransformer.ts
171 lines (142 loc) · 5.59 KB
/
transformer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import * as fs from 'fs'
import * as path from 'path'
import * as ts from 'typescript'
const stubModuleSource = fs.readFileSync(path.join(__dirname, 'index.d.ts'), 'utf8')
export default function transformer (program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
return (context) => (source) => visitNodeAndChildren(source, program, context)
}
function visitNodeAndChildren (node: ts.SourceFile, program: ts.Program, context: ts.TransformationContext): ts.SourceFile
function visitNodeAndChildren (node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.VisitResult<ts.Node>
function visitNodeAndChildren (node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.VisitResult<ts.Node> {
const newNode = visitNode(node, program)
return newNode
? ts.visitEachChild(newNode, (child) => visitNodeAndChildren(child, program, context), context)
: undefined
}
function visitNode (node: ts.Node, program: ts.Program): ts.Node | undefined {
try {
if (ts.isCallExpression(node)) {
return visitCallExpression(node, program)
}
if (ts.isImportDeclaration(node)) {
return visitImportClause(node, program)
}
return node
} catch (err) {
if (err instanceof Error) {
enhanceErrorStack(err, node)
}
throw err
}
}
function visitCallExpression (node: ts.CallExpression, program: ts.Program): ts.Node {
const typeChecker = program.getTypeChecker()
const signature = typeChecker.getResolvedSignature(node)
if (!signature) {
return node
}
const { declaration } = signature
if (!declaration
|| ts.isJSDocSignature(declaration)
|| !isOurStubModule(declaration.getSourceFile())) {
return node
}
const funcName = declaration.name?.getText()
if (!funcName) {
return node
}
return handleInlineCallExpression(node, funcName)
}
function visitImportClause (node: ts.ImportDeclaration, program: ts.Program): ts.Node | undefined {
if (!node.importClause) {
return node
}
const namedBindings = node.importClause.namedBindings
if (!node.importClause.name && !namedBindings) {
return node
}
const importSymbol = program.getTypeChecker().getSymbolAtLocation(node.moduleSpecifier)
if (!importSymbol?.valueDeclaration || !isOurStubModule(importSymbol.valueDeclaration.getSourceFile())) {
return node
}
return undefined // drop the import
}
function handleInlineCallExpression (node: ts.CallExpression, funcName: string): ts.Node {
const arg0 = node.arguments[0]
if (!arg0 || !ts.isStringLiteral(arg0)) {
throw TypeError(`The first argument of ${funcName} function must be a string literal`)
}
const filename = arg0.text
const baseDir = path.dirname(node.getSourceFile().fileName)
const content = fs.readFileSync(path.resolve(baseDir, filename), 'utf-8')
switch (funcName) {
case '$INLINE_FILE': {
return ts.factory.createStringLiteral(content)
}
case '$INLINE_JSON': {
const parent = node.parent
let obj: unknown = JSON.parse(content)
if (ts.isVariableDeclaration(parent) && ts.isObjectBindingPattern(parent.name)) {
if (typeof obj !== 'object') {
throw TypeError(`${filename} does not contain an object as expected`)
}
if (obj !== null) {
obj = filterObjectByBindingPattern(obj, parent.name)
}
}
return jsonToAST(obj)
}
default: {
throw RangeError(`Unknown function: ${funcName}`)
}
}
}
function isOurStubModule (sourceFile: ts.SourceFile): boolean {
// Comparing of the file names may not be reliable, it doesn't work e.g. with
// yarn's "link" resolution used in this module's end-to-end tests.
return sourceFile.text === stubModuleSource
}
function filterObjectByBindingPattern (obj: object, binding: ts.ObjectBindingPattern): any {
return binding.elements.reduce<Record<string, unknown>>((acc, { propertyName, name }) => {
const propName = propertyName && ts.isIdentifier(propertyName) ? propertyName.text
: ts.isIdentifier(name) ? name.text
: undefined
if (propName) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
acc[propName] = (obj as any)[propName]
}
return acc
}, {})
}
function jsonToAST (obj: unknown): ts.Expression {
switch (typeof obj) {
case 'object': {
if (obj === null) {
return ts.factory.createNull()
}
if (Array.isArray(obj)) {
return ts.factory.createArrayLiteralExpression(obj.map(jsonToAST))
}
return ts.factory.createObjectLiteralExpression(Object.keys(obj).map(key => {
const propName = ts.factory.createStringLiteral(key)
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return ts.factory.createPropertyAssignment(propName, jsonToAST((obj as any)[key]))
}))
}
case 'number': return ts.factory.createNumericLiteral(obj)
case 'boolean': return obj ? ts.factory.createTrue() : ts.factory.createFalse()
case 'string': return ts.factory.createStringLiteral(obj, /* isSingleQuote */ undefined)
default: throw TypeError(`Unexpected type in JSON object: "${String(obj)}" (${typeof obj})`)
}
}
function enhanceErrorStack (err: Error, node: ts.Node): void {
if (err.stack == null) { return }
const lines = err.stack.split('\n')
const line1 = lines[1] || ''
const indent = ' '.repeat(line1.length - line1.trimStart().length)
const source = node.getSourceFile()
const loc = ts.getLineAndCharacterOfPosition(source, node.pos)
const newLine = `${indent}at ${source.fileName}:${loc.line + 1}:${loc.character + 1}`
lines.splice(1, 0, newLine)
err.stack = lines.join('\n')
}