-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathduel.js
executable file
·224 lines (197 loc) · 6.71 KB
/
duel.js
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env node
import { argv, platform } from 'node:process'
import { join, dirname, resolve, relative } from 'node:path'
import { spawn } from 'node:child_process'
import { writeFile, rm, rename, mkdir, cp } from 'node:fs/promises'
import { randomBytes } from 'node:crypto'
import { performance } from 'node:perf_hooks'
import { glob } from 'glob'
import { findUp, pathExists } from 'find-up'
import { specifier } from '@knighted/specifier'
import { transform } from '@knighted/module'
import { init } from './init.js'
import { getRealPathAsFileUrl, getCompileFiles, logError, log } from './util.js'
const handleErrorAndExit = message => {
const exitCode = Number(message)
if (isNaN(exitCode)) {
logError(message)
process.exit(1)
} else {
logError('Compilation errors found.')
process.exit(exitCode)
}
}
const duel = async args => {
const ctx = await init(args)
if (ctx) {
const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx
const tsc = await findUp(
async dir => {
const tscBin = join(dir, 'node_modules', '.bin', 'tsc')
if (await pathExists(tscBin)) {
return tscBin
}
},
{ cwd: projectDir },
)
const runBuild = (project, outDir) => {
return new Promise((resolve, reject) => {
const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project]
const build = spawn(tsc, args, { stdio: 'inherit', shell: platform === 'win32' })
build.on('error', err => {
reject(new Error(`Failed to compile: ${err.message}`))
})
build.on('exit', code => {
if (code > 0) {
return reject(new Error(code))
}
resolve(code)
})
})
}
const pkgDir = dirname(pkg.path)
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist'
const absoluteOutDir = resolve(projectDir, outDir)
const originalType = pkg.packageJson.type ?? 'commonjs'
const isCjsBuild = originalType !== 'commonjs'
const targetExt = isCjsBuild ? '.cjs' : '.mjs'
const hex = randomBytes(4).toString('hex')
const getOverrideTsConfig = () => {
return {
...tsconfig,
compilerOptions: {
...tsconfig.compilerOptions,
module: 'NodeNext',
moduleResolution: 'NodeNext',
},
}
}
const runPrimaryBuild = () => {
return runBuild(
configPath,
dirs
? isCjsBuild
? join(absoluteOutDir, 'esm')
: join(absoluteOutDir, 'cjs')
: absoluteOutDir,
)
}
const updateSpecifiersAndFileExtensions = async filenames => {
for (const filename of filenames) {
const dts = /(\.d\.ts)$/
const outFilename = dts.test(filename)
? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts')
: filename.replace(/\.js$/, targetExt)
const { code, error } = await specifier.update(filename, ({ value }) => {
// Collapse any BinaryExpression or NewExpression to test for a relative specifier
const collapsed = value.replace(/['"`+)\s]|new String\(/g, '')
const relative = /^(?:\.|\.\.)\//
if (relative.test(collapsed)) {
// $2 is for any closing quotation/parens around BE or NE
return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`)
}
})
if (code && !error) {
await writeFile(outFilename, code)
await rm(filename, { force: true })
}
}
}
const logSuccess = start => {
log(
`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(
performance.now() - start,
)}ms.`,
)
}
log('Starting primary build...')
let success = false
const startTime = performance.now()
try {
await runPrimaryBuild()
success = true
} catch ({ message }) {
handleErrorAndExit(message)
}
if (success) {
const subDir = join(projectDir, `_${hex}_`)
const absoluteDualOutDir = join(
projectDir,
isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'),
)
const tsconfigDual = getOverrideTsConfig()
const pkgRename = 'package.json.bak'
let dualConfigPath = join(projectDir, `tsconfig.${hex}.json`)
let errorMsg = ''
if (modules) {
const compileFiles = getCompileFiles(tsc, projectDir)
dualConfigPath = join(subDir, `tsconfig.${hex}.json`)
await mkdir(subDir)
await Promise.all(
compileFiles.map(file =>
cp(file, join(subDir, relative(projectDir, file).replace(/^(\.\.\/)*/, ''))),
),
)
/**
* Transform ambiguous modules for the target dual build.
* @see https://github.com/microsoft/TypeScript/issues/58658
*/
const toTransform = await glob(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, {
ignore: 'node_modules/**',
})
for (const file of toTransform) {
/**
* Maybe include the option to transform modules implicitly
* (modules: true) so that `exports` are correctly converted
* when targeting a CJS dual build. Depends on @knighted/module
* supporting he `modules` option.
*
* @see https://github.com/microsoft/TypeScript/issues/58658
*/
await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' })
}
}
/**
* Create a new package.json with updated `type` field.
* Create a new tsconfig.json.
*/
await rename(pkg.path, join(pkgDir, pkgRename))
await writeFile(
pkg.path,
JSON.stringify({
type: isCjsBuild ? 'commonjs' : 'module',
}),
)
await writeFile(dualConfigPath, JSON.stringify(tsconfigDual))
// Build dual
log('Starting dual build...')
try {
await runBuild(dualConfigPath, absoluteDualOutDir)
} catch ({ message }) {
success = false
errorMsg = message
} finally {
// Cleanup and restore
await rm(dualConfigPath, { force: true })
await rm(pkg.path, { force: true })
await rm(subDir, { force: true, recursive: true })
await rename(join(pkgDir, pkgRename), pkg.path)
if (errorMsg) {
handleErrorAndExit(errorMsg)
}
}
if (success) {
const filenames = await glob(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, {
ignore: 'node_modules/**',
})
await updateSpecifiersAndFileExtensions(filenames)
logSuccess(startTime)
}
}
}
}
const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1])
if (import.meta.url === realFileUrlArgv1) {
await duel()
}
export { duel }