-
Notifications
You must be signed in to change notification settings - Fork 1
/
m13-es-transformer.ts
196 lines (168 loc) · 7.9 KB
/
m13-es-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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import * as typescript from 'typescript'
import * as path from 'path'
import { existsSync } from 'fs'
import { normalize } from 'path'
// this transformer applies the baseUrl & paths rules from the tsconfig.json file
// and appends .js to imports/exports, so that the typescript compiler's output
// can be used directly in ES6 module workflows without additional bundlers
// how TypeScript's rootDirs, baseUrl & paths work is documented in the TypeScript
// source in src/compiler/moduleNameResolver.ts as follows:
// * if a baseUrl is given, then absolute paths will be prefixed with the baseUrl
// * "paths": { PATTERN: [SUBSTITUTION, ...], ... }
// * PATTERN and SUBSTITUTION can have 0-1 '*' (which means that we can just split at '*')
// * if multiple patterns match, match with the longest prefix is choosen
// * the * in PATTERN is to be replaced with the * in SUBSTITUTION
type ImportOrExportDeclaration = (typescript.ImportDeclaration | typescript.ExportDeclaration) & { moduleSpecifier: typescript.StringLiteral }
const transformer = (program: typescript.Program) => (transformationContext: typescript.TransformationContext) => (sourceFile: typescript.SourceFile) => {
function transformAST(node: typescript.Node): typescript.VisitResult<typescript.Node> {
if (isImportExportDeclaration(node)) {
const compilerOptions = program.getCompilerOptions()
if (compilerOptions.baseUrl &&
!node.moduleSpecifier.text.startsWith('.')
) {
let newModuleName = resolveBaseUrlAndPath(program, node)
if (newModuleName !== undefined) {
if (!newModuleName.endsWith(".js")) {
newModuleName += ".js"
}
const newModuleSpecifier = typescript.factory.createStringLiteral(newModuleName)
if (typescript.isImportDeclaration(node)) {
return typescript.factory.updateImportDeclaration(node, node.modifiers, node.importClause, newModuleSpecifier, node.assertClause)
} else if (typescript.isExportDeclaration(node)) {
return typescript.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, newModuleSpecifier, node.assertClause)
}
}
}
if (shouldMutateModuleSpecifier(node)) {
const newModuleSpecifier = typescript.factory.createStringLiteral(`${node.moduleSpecifier.text}.js`)
if (typescript.isImportDeclaration(node)) {
return typescript.factory.updateImportDeclaration(node, node.modifiers, node.importClause, newModuleSpecifier, node.assertClause)
} else if (typescript.isExportDeclaration(node)) {
return typescript.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, newModuleSpecifier, node.assertClause)
}
}
}
return typescript.visitEachChild(node, transformAST, transformationContext)
}
function isImportExportDeclaration(node: typescript.Node): node is ImportOrExportDeclaration {
return (typescript.isImportDeclaration(node) || typescript.isExportDeclaration(node))
&& node.moduleSpecifier !== undefined
&& typescript.isStringLiteral(node.moduleSpecifier)
}
function shouldMutateModuleSpecifier(node: ImportOrExportDeclaration) {
// only when path is relative
if (!node.moduleSpecifier.text.startsWith('./') &&
!node.moduleSpecifier.text.startsWith('../')) {
return false
}
// only when module specifier has no extension
if (path.extname(node.moduleSpecifier.text) !== '') {
return false
}
return true
}
return typescript.visitNode(sourceFile, transformAST)
}
export default transformer
function resolveBaseUrlAndPath(program: typescript.Program, node: ImportOrExportDeclaration): string | undefined {
let moduleName = node.moduleSpecifier.text
const compilerOptions = program.getCompilerOptions()
if (compilerOptions?.baseUrl == undefined) {
return undefined
}
let filename = ((node as any)?.parent?.fileName) ?? (node as any)?.original?.parent?.fileName
if (filename === undefined) {
return
}
if (filename.startsWith(compilerOptions.baseUrl)) {
for (let adjustedModuleName of tsPathResolver(compilerOptions, moduleName)) {
let found = false
for (let ext of ["ts", "tsx", "js", "jsx"]) {
const absoluteModuleName = normalize(`${compilerOptions.baseUrl}/${adjustedModuleName}.${ext}`)
if (existsSync(`${absoluteModuleName}`)) {
found = true
break
}
}
if (!found) {
continue
}
const relativeModuleName = fromToFile(filename.substring(compilerOptions.baseUrl.length + 1), adjustedModuleName)
return relativeModuleName
}
}
}
interface TsPathResolverConfig {
baseUrl?: string
paths?: { [key: string]: string[] }
}
function tsPathResolver(
config: TsPathResolverConfig,
moduleName: string
): Array<string> {
if (config.baseUrl === undefined || config.paths === undefined) {
return [moduleName]
}
if (moduleName.charAt(0) == ".") {
return [moduleName]
}
// find the best pattern
let bestPattern: string | undefined
let bestPatternsHeadLength = 0
for (let pattern in config.paths) {
const asterisk = pattern.indexOf("*")
if (asterisk !== -1) {
const pathHead = pattern.substring(0, asterisk)
const pathTail = pattern.substring(asterisk + 1)
if (pathHead.length > bestPatternsHeadLength &&
moduleName.startsWith(pathHead) &&
moduleName.endsWith(pathTail)
) {
bestPatternsHeadLength = pathHead.length
bestPattern = pattern
}
} else {
if (pattern.length >= bestPatternsHeadLength &&
pattern === moduleName
) {
bestPatternsHeadLength = pattern.length
bestPattern = moduleName
}
}
}
// apply the best pattern's substitutions
if (bestPattern !== undefined) {
const asterisk = bestPattern.indexOf("*")
if (asterisk !== -1) {
const pathTail = bestPattern.substring(asterisk + 1)
const middle = moduleName.substring(asterisk, moduleName.length - pathTail.length)
const substitutions = config.paths[bestPattern]
return substitutions.map(substitution => {
const subAsterisk = substitution.indexOf("*")
const subHead = substitution.substring(0, subAsterisk)
const subTail = substitution.substring(subAsterisk + 1)
return `${subHead}${middle}${subTail}`
})
} else {
return config.paths[bestPattern]
}
}
return [moduleName]
}
function fromToFile(fromFile: string, toFile: string) {
const fromFileParts = fromFile.split("/")
const toFileParts = toFile.split("/")
const maxDepth = Math.min(fromFileParts.length, toFileParts.length)
let equalUntilDepth = 0
while (equalUntilDepth < maxDepth && fromFileParts[equalUntilDepth] == toFileParts[equalUntilDepth]) {
++equalUntilDepth
}
let relativeModuleName = "../".repeat(fromFileParts.length - equalUntilDepth - 1) // -1 is the file itself
if (relativeModuleName.length == 0) {
relativeModuleName = "./"
}
for (; equalUntilDepth < toFileParts.length; ++equalUntilDepth) {
relativeModuleName += toFileParts[equalUntilDepth] + "/"
}
return relativeModuleName = relativeModuleName.substring(0, relativeModuleName.length - 1) // strip last "/"
}