@tamagui/cli 2.7.7 → 3.0.0-beta.1093.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add.cjs +76 -87
- package/dist/build.cjs +425 -278
- package/dist/cli.cjs +342 -340
- package/dist/generate-prompt.cjs +339 -359
- package/dist/generate.cjs +45 -54
- package/dist/index.cjs +1 -1
- package/dist/migrate.cjs +427 -0
- package/dist/setup-prompt.cjs +157 -0
- package/dist/to-tailwind-default-config.cjs +789 -0
- package/dist/to-tailwind.cjs +213 -0
- package/dist/update-template.cjs +43 -52
- package/dist/update.cjs +14 -18
- package/dist/upgrade.cjs +406 -398
- package/dist/utils.cjs +84 -101
- package/metro.cjs +1 -0
- package/metro.d.ts +1 -0
- package/metro.mjs +1 -0
- package/package.json +43 -13
- package/src/build.ts +594 -362
- package/src/cli.ts +96 -12
- package/src/generate-prompt.ts +16 -30
- package/src/migrate.ts +412 -0
- package/src/setup-prompt.ts +139 -0
- package/src/to-tailwind-default-config.ts +767 -0
- package/src/to-tailwind.ts +313 -0
- package/src/utils.ts +11 -9
- package/types/add.d.ts +1 -1
- package/types/add.d.ts.map +1 -1
- package/types/build.d.ts +2 -1
- package/types/build.d.ts.map +1 -1
- package/types/migrate.d.ts +7 -0
- package/types/migrate.d.ts.map +1 -0
- package/types/setup-prompt.d.ts +3 -0
- package/types/setup-prompt.d.ts.map +1 -0
- package/types/to-tailwind-default-config.d.ts +53 -0
- package/types/to-tailwind-default-config.d.ts.map +1 -0
- package/types/to-tailwind.d.ts +16 -0
- package/types/to-tailwind.d.ts.map +1 -0
- package/types/utils.d.ts.map +1 -1
- package/vite.cjs +1 -0
- package/vite.d.ts +1 -0
- package/vite.mjs +1 -0
package/src/build.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
extractToClassNames,
|
|
4
|
-
extractToNative,
|
|
2
|
+
CompilerFrontend,
|
|
5
3
|
loadTamagui,
|
|
6
4
|
loadTamaguiBuildConfigSync,
|
|
5
|
+
compilerProjectStamp,
|
|
6
|
+
type CompilerProject,
|
|
7
7
|
} from '@tamagui/static'
|
|
8
8
|
import type { CLIResolvedOptions, TamaguiOptions } from '@tamagui/types'
|
|
9
9
|
import chokidar from 'chokidar'
|
|
@@ -11,8 +11,22 @@ import { copyFile, mkdir, readFile, rm, stat, writeFile } from 'fs-extra'
|
|
|
11
11
|
import MicroMatch from 'micromatch'
|
|
12
12
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
|
13
13
|
import { tmpdir } from 'node:os'
|
|
14
|
-
import {
|
|
14
|
+
import { spawn } from 'node:child_process'
|
|
15
15
|
import { createHash } from 'node:crypto'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import {
|
|
18
|
+
findConfigFile,
|
|
19
|
+
nodeModuleNameResolver,
|
|
20
|
+
parseJsonConfigFileContent,
|
|
21
|
+
readConfigFile,
|
|
22
|
+
sys,
|
|
23
|
+
} from 'typescript'
|
|
24
|
+
|
|
25
|
+
const cliVersion = (
|
|
26
|
+
createRequire(typeof __filename === 'string' ? __filename : import.meta.url)(
|
|
27
|
+
'@tamagui/cli/package.json'
|
|
28
|
+
) as { version: string }
|
|
29
|
+
).version
|
|
16
30
|
|
|
17
31
|
export type BuildStats = {
|
|
18
32
|
filesProcessed: number
|
|
@@ -20,6 +34,7 @@ export type BuildStats = {
|
|
|
20
34
|
flattened: number
|
|
21
35
|
styled: number
|
|
22
36
|
found: number
|
|
37
|
+
bailed: number
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
export type TrackedFile = {
|
|
@@ -68,418 +83,635 @@ export const build = async (
|
|
|
68
83
|
const outputAround = options.outputAround || false
|
|
69
84
|
const promises: Promise<void>[] = []
|
|
70
85
|
const isDryRun = options.dryRun || false
|
|
86
|
+
const trackedFiles: TrackedFile[] = []
|
|
87
|
+
const trackedPaths = new Set<string>()
|
|
88
|
+
const restoreDir = options.runCommand
|
|
89
|
+
? join(tmpdir(), `tamagui-restore-${process.pid}`)
|
|
90
|
+
: null
|
|
71
91
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
92
|
+
if (restoreDir) {
|
|
93
|
+
await mkdir(restoreDir, { recursive: true })
|
|
74
94
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await
|
|
95
|
+
let didRestore = false
|
|
96
|
+
const restoreTrackedFiles = async (force = false) => {
|
|
97
|
+
if (didRestore) return
|
|
98
|
+
await restoreFiles(trackedFiles, restoreDir, force)
|
|
99
|
+
didRestore = true
|
|
79
100
|
}
|
|
80
101
|
|
|
81
|
-
|
|
102
|
+
try {
|
|
103
|
+
const trackFile = async (filePath: string): Promise<void> => {
|
|
104
|
+
if (!restoreDir || trackedPaths.has(filePath)) return
|
|
105
|
+
trackedPaths.add(filePath)
|
|
82
106
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
const buildOptions = {
|
|
90
|
-
...loadedOptions,
|
|
91
|
-
disable: false,
|
|
92
|
-
disableExtraction: false,
|
|
93
|
-
}
|
|
107
|
+
const currentStat = await stat(filePath).catch(() => null)
|
|
108
|
+
if (!currentStat) {
|
|
109
|
+
trackedFiles.push({ path: filePath, hardlinkPath: '', mtimeAfterWrite: 0 })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
:
|
|
113
|
+
const hash = createHash('md5').update(filePath).digest('hex')
|
|
114
|
+
const backupPath = join(restoreDir, hash)
|
|
115
|
+
await copyFile(filePath, backupPath)
|
|
116
|
+
trackedFiles.push({ path: filePath, hardlinkPath: backupPath, mtimeAfterWrite: 0 })
|
|
117
|
+
}
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
const recordMtime = async (filePath: string): Promise<void> => {
|
|
120
|
+
if (!restoreDir) return
|
|
121
|
+
const tracked = trackedFiles.find((item) => item.path === filePath)
|
|
122
|
+
if (tracked) {
|
|
123
|
+
const fileStat = await stat(filePath)
|
|
124
|
+
tracked.mtimeAfterWrite = fileStat.mtimeMs
|
|
125
|
+
}
|
|
126
|
+
}
|
|
105
127
|
|
|
106
|
-
|
|
128
|
+
if (isDryRun) {
|
|
129
|
+
console.info('[dry-run] no files will be written\n')
|
|
130
|
+
}
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
// create output directory if specified
|
|
133
|
+
if (outputDir) {
|
|
134
|
+
await mkdir(outputDir, { recursive: true })
|
|
135
|
+
}
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
const watchPattern = sourceDir.match(/\.(tsx|jsx)$/)
|
|
113
|
-
? sourceDir // Single file
|
|
114
|
-
: `${sourceDir}/**/*.{tsx,jsx}` // Directory
|
|
137
|
+
const loadedOptions = loadTamaguiBuildConfigSync(options.tamaguiOptions)
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
139
|
+
// when running CLI build directly, ignore disable since user explicitly wants to build
|
|
140
|
+
if (loadedOptions.disable) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`[tamagui] Note: "disable" option in tamagui.build.ts is being ignored for CLI build command`
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
const buildOptions = {
|
|
146
|
+
...loadedOptions,
|
|
147
|
+
disable: false,
|
|
148
|
+
disableExtraction: false,
|
|
149
|
+
}
|
|
123
150
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
const targets =
|
|
152
|
+
options.target === 'both' || !options.target
|
|
153
|
+
? (['web', 'native'] as const)
|
|
154
|
+
: ([options.target] as const)
|
|
155
|
+
|
|
156
|
+
const root = process.cwd()
|
|
157
|
+
const outputCSSPath = buildOptions.outputCSS
|
|
158
|
+
? resolve(root, buildOptions.outputCSS)
|
|
159
|
+
: null
|
|
160
|
+
if (outputCSSPath) {
|
|
161
|
+
await trackFile(outputCSSPath)
|
|
162
|
+
}
|
|
163
|
+
const require = createRequire(
|
|
164
|
+
typeof __filename === 'string' ? __filename : import.meta.url
|
|
165
|
+
)
|
|
166
|
+
const configPath =
|
|
167
|
+
findConfigFile(root, sys.fileExists, 'tsconfig.json') ||
|
|
168
|
+
findConfigFile(root, sys.fileExists, 'jsconfig.json')
|
|
169
|
+
const compilerOptions = configPath
|
|
170
|
+
? parseJsonConfigFileContent(
|
|
171
|
+
(() => {
|
|
172
|
+
const loaded = readConfigFile(configPath, sys.readFile)
|
|
173
|
+
if (loaded.error) throw new Error(String(loaded.error.messageText))
|
|
174
|
+
return loaded.config
|
|
175
|
+
})(),
|
|
176
|
+
sys,
|
|
177
|
+
dirname(configPath)
|
|
178
|
+
).options
|
|
179
|
+
: {}
|
|
180
|
+
const resolveCompilerId = (specifier: string, importer: string): string | null => {
|
|
181
|
+
const resolved = nodeModuleNameResolver(specifier, importer, compilerOptions, sys)
|
|
182
|
+
.resolvedModule?.resolvedFileName
|
|
183
|
+
if (resolved && !resolved.endsWith('.d.ts')) return resolved
|
|
184
|
+
try {
|
|
185
|
+
return require.resolve(specifier, { paths: [dirname(importer), root] })
|
|
186
|
+
} catch {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const compilerFrontends = new Map<'web' | 'native', CompilerFrontend>()
|
|
191
|
+
const compilerProjects = new Map<'web' | 'native', CompilerProject>()
|
|
192
|
+
|
|
193
|
+
for (const target of targets) {
|
|
194
|
+
const targetOptions = { ...buildOptions, platform: target } satisfies TamaguiOptions
|
|
195
|
+
const projectInfo = await loadTamagui(targetOptions)
|
|
196
|
+
if (!projectInfo) throw new Error(`Unable to load Tamagui for the ${target} build`)
|
|
197
|
+
const componentModules = [
|
|
198
|
+
...new Set(['@tamagui/core', ...(targetOptions.components ?? [])]),
|
|
199
|
+
].map((moduleName) => {
|
|
200
|
+
const id = resolveCompilerId(moduleName, join(root, '__tamagui_cli__.tsx'))
|
|
201
|
+
if (!id) throw new Error(`Unable to resolve compiler component ${moduleName}`)
|
|
202
|
+
return { moduleName, id }
|
|
203
|
+
})
|
|
204
|
+
compilerProjects.set(target, {
|
|
205
|
+
projectInfo,
|
|
206
|
+
componentModules,
|
|
207
|
+
generation: createHash('sha256')
|
|
208
|
+
.update(target)
|
|
209
|
+
.update('\0')
|
|
210
|
+
.update(buildOptions.config ?? 'tamagui.config.ts')
|
|
211
|
+
.update('\0')
|
|
212
|
+
.update(JSON.stringify(targetOptions.components ?? []))
|
|
213
|
+
.update('\0')
|
|
214
|
+
.update(String(!!targetOptions.disablePartialExtraction))
|
|
215
|
+
.update('\0')
|
|
216
|
+
.update(
|
|
217
|
+
String(target === 'native' && !!targetOptions.experimental?.nativeFastPath)
|
|
218
|
+
)
|
|
219
|
+
.digest('hex'),
|
|
220
|
+
disablePartialExtraction: targetOptions.disablePartialExtraction,
|
|
221
|
+
experimentalNativeFastPath:
|
|
222
|
+
target === 'native' && targetOptions.experimental?.nativeFastPath === true,
|
|
223
|
+
cacheStamp: compilerProjectStamp({
|
|
224
|
+
stampSources: projectInfo.stampSources ?? [],
|
|
225
|
+
hostVersions: [`@tamagui/cli@${cliVersion}`],
|
|
226
|
+
target,
|
|
227
|
+
componentModules,
|
|
228
|
+
disablePartialExtraction: !!targetOptions.disablePartialExtraction,
|
|
229
|
+
experimentalNativeFastPath:
|
|
230
|
+
target === 'native' && targetOptions.experimental?.nativeFastPath === true,
|
|
231
|
+
zeroRuntime: false,
|
|
232
|
+
development: process.env.NODE_ENV === 'development',
|
|
233
|
+
}),
|
|
234
|
+
})
|
|
235
|
+
compilerFrontends.set(target, new CompilerFrontend())
|
|
236
|
+
}
|
|
237
|
+
if (outputCSSPath) {
|
|
238
|
+
await recordMtime(outputCSSPath)
|
|
239
|
+
}
|
|
130
240
|
|
|
131
|
-
|
|
241
|
+
const compileTarget = async (
|
|
242
|
+
target: 'web' | 'native',
|
|
243
|
+
sourcePath: string,
|
|
244
|
+
source: string
|
|
245
|
+
) => {
|
|
246
|
+
const compiler = compilerFrontends.get(target)!
|
|
247
|
+
const project = compilerProjects.get(target)!
|
|
248
|
+
return compiler.compile({
|
|
249
|
+
id: sourcePath,
|
|
250
|
+
source,
|
|
251
|
+
root,
|
|
252
|
+
target,
|
|
253
|
+
project,
|
|
254
|
+
resolve: async (specifier, importer) => {
|
|
255
|
+
const id = resolveCompilerId(specifier, importer)
|
|
256
|
+
return id ? { id, external: id.includes('/node_modules/') } : null
|
|
257
|
+
},
|
|
258
|
+
load: async (id) => {
|
|
259
|
+
try {
|
|
260
|
+
return await readFile(id.split(/[?#]/, 1)[0]!, 'utf8')
|
|
261
|
+
} catch {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
},
|
|
132
265
|
})
|
|
133
|
-
|
|
134
|
-
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Collect all files first
|
|
269
|
+
const allFiles: string[] = []
|
|
270
|
+
|
|
271
|
+
// Handle both directory and specific file paths
|
|
272
|
+
const watchPattern = sourceDir.match(/\.(tsx|jsx)$/)
|
|
273
|
+
? sourceDir // Single file
|
|
274
|
+
: `${sourceDir}/**/*.{tsx,jsx}` // Directory
|
|
275
|
+
const sourceRoot = sourceDir.match(/\.(tsx|jsx)$/)
|
|
276
|
+
? dirname(resolve(sourceDir))
|
|
277
|
+
: resolve(sourceDir)
|
|
278
|
+
|
|
279
|
+
await new Promise<void>((res) => {
|
|
280
|
+
const watcher = chokidar.watch(watchPattern, {
|
|
281
|
+
ignoreInitial: false,
|
|
135
282
|
})
|
|
136
|
-
|
|
283
|
+
watcher
|
|
284
|
+
.on('add', (relativePath) => {
|
|
285
|
+
const sourcePath = resolve(process.cwd(), relativePath)
|
|
137
286
|
|
|
138
|
-
|
|
139
|
-
|
|
287
|
+
if (options.exclude && MicroMatch.contains(relativePath, options.exclude)) {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (options.include && !MicroMatch.contains(relativePath, options.include)) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
140
293
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
294
|
+
allFiles.push(sourcePath)
|
|
295
|
+
})
|
|
296
|
+
.on('ready', () => {
|
|
297
|
+
watcher.close().then(() => res())
|
|
298
|
+
})
|
|
299
|
+
})
|
|
144
300
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
301
|
+
// chokidar emits "add" in filesystem/stat-completion order, which varies between
|
|
302
|
+
// runs. sort so file processing order (and therefore output) is reproducible.
|
|
303
|
+
allFiles.sort()
|
|
304
|
+
|
|
305
|
+
// Now determine what to optimize for each file
|
|
306
|
+
const fileToTargets = new Map<string, ('web' | 'native')[]>()
|
|
307
|
+
|
|
308
|
+
for (const sourcePath of allFiles) {
|
|
309
|
+
const platformMatch = sourcePath.match(/\.(web|native|ios|android)\.(tsx|jsx)$/)
|
|
310
|
+
let filePlatforms: ('web' | 'native')[] = []
|
|
311
|
+
|
|
312
|
+
if (platformMatch) {
|
|
313
|
+
// Platform-specific file - only optimize for that platform
|
|
314
|
+
const platform = platformMatch[1]
|
|
315
|
+
if (platform === 'web') {
|
|
316
|
+
filePlatforms = ['web']
|
|
317
|
+
} else if (
|
|
318
|
+
platform === 'native' ||
|
|
319
|
+
platform === 'ios' ||
|
|
320
|
+
platform === 'android'
|
|
321
|
+
) {
|
|
322
|
+
filePlatforms = ['native']
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
// Base file without platform extension
|
|
326
|
+
// Check if platform-specific versions exist in the collected files
|
|
327
|
+
const basePath = sourcePath.replace(/\.(tsx|jsx)$/, '')
|
|
328
|
+
const hasNative = allFiles.some(
|
|
329
|
+
(f) =>
|
|
330
|
+
f === `${basePath}.native.tsx` ||
|
|
331
|
+
f === `${basePath}.native.jsx` ||
|
|
332
|
+
f === `${basePath}.ios.tsx` ||
|
|
333
|
+
f === `${basePath}.ios.jsx` ||
|
|
334
|
+
f === `${basePath}.android.tsx` ||
|
|
335
|
+
f === `${basePath}.android.jsx`
|
|
336
|
+
)
|
|
337
|
+
const hasWeb = allFiles.some(
|
|
338
|
+
(f) => f === `${basePath}.web.tsx` || f === `${basePath}.web.jsx`
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
// Only optimize for targets that don't have platform-specific files
|
|
342
|
+
filePlatforms = targets.filter((target) => {
|
|
343
|
+
if (target === 'native' && hasNative) return false
|
|
344
|
+
if (target === 'web' && hasWeb) return false
|
|
345
|
+
return true
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// Special case: if BOTH .web and .native exist, don't touch base file at all
|
|
349
|
+
if (hasWeb && hasNative) {
|
|
350
|
+
filePlatforms = []
|
|
351
|
+
}
|
|
152
352
|
}
|
|
153
|
-
} else {
|
|
154
|
-
// Base file without platform extension
|
|
155
|
-
// Check if platform-specific versions exist in the collected files
|
|
156
|
-
const basePath = sourcePath.replace(/\.(tsx|jsx)$/, '')
|
|
157
|
-
const hasNative = allFiles.some(
|
|
158
|
-
(f) =>
|
|
159
|
-
f === `${basePath}.native.tsx` ||
|
|
160
|
-
f === `${basePath}.native.jsx` ||
|
|
161
|
-
f === `${basePath}.ios.tsx` ||
|
|
162
|
-
f === `${basePath}.ios.jsx` ||
|
|
163
|
-
f === `${basePath}.android.tsx` ||
|
|
164
|
-
f === `${basePath}.android.jsx`
|
|
165
|
-
)
|
|
166
|
-
const hasWeb = allFiles.some(
|
|
167
|
-
(f) => f === `${basePath}.web.tsx` || f === `${basePath}.web.jsx`
|
|
168
|
-
)
|
|
169
353
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
354
|
+
if (filePlatforms.length > 0) {
|
|
355
|
+
fileToTargets.set(sourcePath, filePlatforms)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Track overall statistics
|
|
360
|
+
const stats: BuildStats = {
|
|
361
|
+
filesProcessed: 0,
|
|
362
|
+
optimized: 0,
|
|
363
|
+
flattened: 0,
|
|
364
|
+
styled: 0,
|
|
365
|
+
found: 0,
|
|
366
|
+
bailed: 0,
|
|
367
|
+
}
|
|
176
368
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
369
|
+
// both target passes report through here, so a `--target native` run stops
|
|
370
|
+
// summarizing itself as all zeros. a file counts once even when it compiles for
|
|
371
|
+
// both targets, otherwise `files` double-counts every shared component.
|
|
372
|
+
const countedFiles = new Set<string>()
|
|
373
|
+
const addStats = (
|
|
374
|
+
sourcePath: string,
|
|
375
|
+
fileStats: Awaited<ReturnType<typeof compileTarget>>['plan']['stats']
|
|
376
|
+
) => {
|
|
377
|
+
if (!countedFiles.has(sourcePath)) {
|
|
378
|
+
countedFiles.add(sourcePath)
|
|
379
|
+
stats.filesProcessed++
|
|
180
380
|
}
|
|
381
|
+
stats.optimized += fileStats.lowered - fileStats.flattened
|
|
382
|
+
stats.flattened += fileStats.flattened
|
|
383
|
+
stats.styled += fileStats.styled
|
|
384
|
+
stats.found += fileStats.found
|
|
385
|
+
stats.bailed += fileStats.bailed
|
|
181
386
|
}
|
|
182
387
|
|
|
183
|
-
if (
|
|
184
|
-
|
|
388
|
+
if (options.debug) {
|
|
389
|
+
process.env.NODE_ENV ||= 'development'
|
|
185
390
|
}
|
|
186
|
-
}
|
|
187
391
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
392
|
+
// Read each original source ONCE, up front, so both target passes compile
|
|
393
|
+
// identical input (the web pass rewrites the file on disk).
|
|
394
|
+
const sources = new Map<string, string>()
|
|
395
|
+
await Promise.all(
|
|
396
|
+
[...fileToTargets.keys()].map(async (sourcePath) => {
|
|
397
|
+
sources.set(sourcePath, await readFile(sourcePath, 'utf-8'))
|
|
398
|
+
})
|
|
399
|
+
)
|
|
196
400
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
401
|
+
const buildWebFile = async (sourcePath: string, originalSource: string) => {
|
|
402
|
+
if (isDryRun) {
|
|
403
|
+
console.info(`\n${sourcePath} [web]`)
|
|
404
|
+
}
|
|
405
|
+
const out = await compileTarget('web', sourcePath, originalSource)
|
|
202
406
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
407
|
+
if (out.output.changed || out.plan.stats.found > 0) {
|
|
408
|
+
addStats(sourcePath, out.plan.stats)
|
|
206
409
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
410
|
+
if (isDryRun) {
|
|
411
|
+
if (out.plan.css) {
|
|
412
|
+
console.info(`\ncss:\n${out.plan.css}`)
|
|
413
|
+
}
|
|
414
|
+
console.info(`\njs:\n${out.output.code}`)
|
|
415
|
+
} else {
|
|
416
|
+
// compute relative path to preserve directory structure in output
|
|
417
|
+
const relPath = outputDir
|
|
418
|
+
? relative(sourceRoot, sourcePath)
|
|
419
|
+
: basename(sourcePath)
|
|
420
|
+
const cssName = '_' + basename(sourcePath, extname(sourcePath))
|
|
421
|
+
const outputBase = outputDir
|
|
422
|
+
? join(outputDir, dirname(relPath))
|
|
423
|
+
: dirname(sourcePath)
|
|
424
|
+
|
|
425
|
+
// ensure output subdirectory exists
|
|
426
|
+
if (outputDir) {
|
|
427
|
+
await mkdir(outputBase, { recursive: true })
|
|
428
|
+
}
|
|
217
429
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const fileStat = await stat(filePath)
|
|
224
|
-
tracked.mtimeAfterWrite = fileStat.mtimeMs
|
|
225
|
-
}
|
|
226
|
-
}
|
|
430
|
+
const stylePath = join(outputBase, cssName + '.css')
|
|
431
|
+
const cssImport = `import "./${cssName}.css"`
|
|
432
|
+
const code = out.plan.css
|
|
433
|
+
? insertCssImport(out.output.code, cssImport)
|
|
434
|
+
: out.output.code
|
|
227
435
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
promises.push(
|
|
231
|
-
(async () => {
|
|
232
|
-
if (options.debug) {
|
|
233
|
-
process.env.NODE_ENV ||= 'development'
|
|
234
|
-
}
|
|
235
|
-
// Read original source ONCE for both targets
|
|
236
|
-
const originalSource = await readFile(sourcePath, 'utf-8')
|
|
436
|
+
// Determine output path for JS (preserve directory structure)
|
|
437
|
+
const webOutputPath = outputDir ? join(outputDir, relPath) : sourcePath
|
|
237
438
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
439
|
+
// Track original file before modifying (skip if using output dir)
|
|
440
|
+
if (!outputDir) {
|
|
441
|
+
await trackFile(sourcePath)
|
|
442
|
+
}
|
|
241
443
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (out) {
|
|
261
|
-
stats.filesProcessed++
|
|
262
|
-
stats.optimized += out.stats.optimized
|
|
263
|
-
stats.flattened += out.stats.flattened
|
|
264
|
-
stats.styled += out.stats.styled
|
|
265
|
-
stats.found += out.stats.found
|
|
266
|
-
|
|
267
|
-
if (isDryRun) {
|
|
268
|
-
const jsContent =
|
|
269
|
-
typeof out.js === 'string' ? out.js : out.js.toString('utf-8')
|
|
270
|
-
if (out.styles) {
|
|
271
|
-
console.info(`\ncss:\n${out.styles}`)
|
|
272
|
-
}
|
|
273
|
-
console.info(`\njs:\n${jsContent}`)
|
|
274
|
-
} else {
|
|
275
|
-
// compute relative path to preserve directory structure in output
|
|
276
|
-
const relPath = outputDir
|
|
277
|
-
? relative(resolve(sourceDir), sourcePath)
|
|
278
|
-
: basename(sourcePath)
|
|
279
|
-
const cssName = '_' + basename(sourcePath, extname(sourcePath))
|
|
280
|
-
const outputBase = outputDir
|
|
281
|
-
? join(outputDir, dirname(relPath))
|
|
282
|
-
: dirname(sourcePath)
|
|
283
|
-
|
|
284
|
-
// ensure output subdirectory exists
|
|
285
|
-
if (outputDir) {
|
|
286
|
-
await mkdir(outputBase, { recursive: true })
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const stylePath = join(outputBase, cssName + '.css')
|
|
290
|
-
const cssImport = `import "./${cssName}.css"`
|
|
291
|
-
const jsContent =
|
|
292
|
-
typeof out.js === 'string' ? out.js : out.js.toString('utf-8')
|
|
293
|
-
const code = insertCssImport(jsContent, cssImport)
|
|
294
|
-
|
|
295
|
-
// Determine output path for JS (preserve directory structure)
|
|
296
|
-
const webOutputPath = outputDir ? join(outputDir, relPath) : sourcePath
|
|
297
|
-
|
|
298
|
-
// Track original file before modifying (skip if using output dir)
|
|
299
|
-
if (!outputDir) {
|
|
300
|
-
await trackFile(sourcePath)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Write web output
|
|
304
|
-
await writeFile(webOutputPath, code, 'utf-8')
|
|
305
|
-
if (!outputDir) {
|
|
306
|
-
await recordMtime(sourcePath)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// CSS file is new, track for cleanup (skip if using output dir)
|
|
310
|
-
await writeFile(stylePath, out.styles, 'utf-8')
|
|
311
|
-
if (!outputDir) {
|
|
312
|
-
// Note: CSS files are new (generated), we'll delete them on restore
|
|
313
|
-
trackedFiles.push({
|
|
314
|
-
path: stylePath,
|
|
315
|
-
hardlinkPath: '', // Empty means delete on restore
|
|
316
|
-
mtimeAfterWrite: (await stat(stylePath)).mtimeMs,
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
} else if (isDryRun) {
|
|
321
|
-
console.info(` web: no output`)
|
|
444
|
+
// Write web output
|
|
445
|
+
await writeFile(webOutputPath, code, 'utf-8')
|
|
446
|
+
if (!outputDir) {
|
|
447
|
+
await recordMtime(sourcePath)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// CSS file is new, track for cleanup (skip if using output dir)
|
|
451
|
+
if (out.plan.css) {
|
|
452
|
+
await writeFile(stylePath, out.plan.css, 'utf-8')
|
|
453
|
+
}
|
|
454
|
+
if (!outputDir && out.plan.css) {
|
|
455
|
+
// Note: CSS files are new (generated), we'll delete them on restore
|
|
456
|
+
trackedFiles.push({
|
|
457
|
+
path: stylePath,
|
|
458
|
+
hardlinkPath: '', // Empty means delete on restore
|
|
459
|
+
mtimeAfterWrite: (await stat(stylePath)).mtimeMs,
|
|
460
|
+
})
|
|
322
461
|
}
|
|
323
462
|
}
|
|
463
|
+
} else if (isDryRun) {
|
|
464
|
+
console.info(` web: no output`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
324
467
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
468
|
+
// Build native version from original source (NOT from the web-optimized version)
|
|
469
|
+
const buildNativeFile = async (
|
|
470
|
+
sourcePath: string,
|
|
471
|
+
filePlatforms: ('web' | 'native')[],
|
|
472
|
+
originalSource: string
|
|
473
|
+
) => {
|
|
474
|
+
if (isDryRun) {
|
|
475
|
+
console.info(`\n${sourcePath} [native]`)
|
|
476
|
+
}
|
|
477
|
+
// Use the ORIGINAL source, not what was just written to disk
|
|
478
|
+
const nativeOut = await compileTarget('native', sourcePath, originalSource)
|
|
479
|
+
|
|
480
|
+
if (nativeOut.output.changed || nativeOut.plan.stats.found > 0) {
|
|
481
|
+
addStats(sourcePath, nativeOut.plan.stats)
|
|
482
|
+
}
|
|
339
483
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
484
|
+
if (isDryRun) {
|
|
485
|
+
if (nativeOut.output.code) {
|
|
486
|
+
console.info(`\nnative:\n${nativeOut.output.code}`)
|
|
487
|
+
} else {
|
|
488
|
+
console.info(` native: no output`)
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// Determine output path:
|
|
492
|
+
// - If --output-around, write .native.tsx next to source
|
|
493
|
+
// - If --output specified, preserve directory structure
|
|
494
|
+
// - If this IS a .native.tsx file, overwrite it
|
|
495
|
+
// - If building both targets from base file, create .native.tsx
|
|
496
|
+
// - If single native target, overwrite source
|
|
497
|
+
let nativeOutputPath = sourcePath
|
|
498
|
+
const isPlatformSpecific = /\.(web|native|ios|android)\.(tsx|jsx)$/.test(
|
|
499
|
+
sourcePath
|
|
500
|
+
)
|
|
501
|
+
const needsNativeSuffix =
|
|
502
|
+
!isPlatformSpecific && (filePlatforms.length > 1 || outputAround)
|
|
503
|
+
|
|
504
|
+
if (outputAround) {
|
|
505
|
+
// Output .native.tsx next to source file
|
|
506
|
+
nativeOutputPath = sourcePath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
507
|
+
// Check if file exists - error if so
|
|
508
|
+
const exists = await stat(nativeOutputPath).catch(() => null)
|
|
509
|
+
if (exists) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`--output-around: ${nativeOutputPath} already exists. Remove it first or use --output instead.`
|
|
356
512
|
)
|
|
357
|
-
const needsNativeSuffix =
|
|
358
|
-
!isPlatformSpecific && (filePlatforms.length > 1 || outputAround)
|
|
359
|
-
|
|
360
|
-
if (outputAround) {
|
|
361
|
-
// Output .native.tsx next to source file
|
|
362
|
-
nativeOutputPath = sourcePath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
363
|
-
// Check if file exists - error if so
|
|
364
|
-
const exists = await stat(nativeOutputPath).catch(() => null)
|
|
365
|
-
if (exists) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`--output-around: ${nativeOutputPath} already exists. Remove it first or use --output instead.`
|
|
368
|
-
)
|
|
369
|
-
}
|
|
370
|
-
} else if (outputDir) {
|
|
371
|
-
// preserve directory structure in output
|
|
372
|
-
const relPath = relative(resolve(sourceDir), sourcePath)
|
|
373
|
-
// add .native suffix when building both targets to avoid overwriting web output
|
|
374
|
-
const outputRelPath = needsNativeSuffix
|
|
375
|
-
? relPath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
376
|
-
: relPath
|
|
377
|
-
nativeOutputPath = join(outputDir, outputRelPath)
|
|
378
|
-
// ensure output subdirectory exists
|
|
379
|
-
await mkdir(dirname(nativeOutputPath), { recursive: true })
|
|
380
|
-
} else if (needsNativeSuffix) {
|
|
381
|
-
// Base file building both targets - create separate .native.tsx
|
|
382
|
-
nativeOutputPath = sourcePath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (nativeOut.code) {
|
|
386
|
-
// check if extraction actually happened by looking for our markers
|
|
387
|
-
const hasExtraction =
|
|
388
|
-
nativeOut.code.includes('__ReactNativeStyleSheet') ||
|
|
389
|
-
nativeOut.code.includes('_withStableStyle')
|
|
390
|
-
if (hasExtraction) {
|
|
391
|
-
stats.filesProcessed++
|
|
392
|
-
// count styled wrappers as flattened (native extraction flattens styles)
|
|
393
|
-
const wrapperMatches = nativeOut.code.match(/_withStableStyle/g)
|
|
394
|
-
if (wrapperMatches) {
|
|
395
|
-
stats.flattened += wrapperMatches.length
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Track original if overwriting existing file (skip if using output dir or outputAround)
|
|
400
|
-
if (
|
|
401
|
-
!outputDir &&
|
|
402
|
-
!outputAround &&
|
|
403
|
-
(nativeOutputPath === sourcePath || filePlatforms.length === 1)
|
|
404
|
-
) {
|
|
405
|
-
await trackFile(nativeOutputPath)
|
|
406
|
-
}
|
|
407
|
-
await writeFile(nativeOutputPath, nativeOut.code, 'utf-8')
|
|
408
|
-
if (!outputDir && !outputAround) {
|
|
409
|
-
await recordMtime(nativeOutputPath)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// If creating new .native.tsx, track for deletion (skip if using output dir or outputAround)
|
|
413
|
-
if (
|
|
414
|
-
!outputDir &&
|
|
415
|
-
!outputAround &&
|
|
416
|
-
nativeOutputPath !== sourcePath &&
|
|
417
|
-
filePlatforms.length > 1
|
|
418
|
-
) {
|
|
419
|
-
trackedFiles.push({
|
|
420
|
-
path: nativeOutputPath,
|
|
421
|
-
hardlinkPath: '', // Empty = delete on restore
|
|
422
|
-
mtimeAfterWrite: (await stat(nativeOutputPath)).mtimeMs,
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (outputAround) {
|
|
427
|
-
console.info(` → ${nativeOutputPath}`)
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
513
|
}
|
|
514
|
+
} else if (outputDir) {
|
|
515
|
+
// preserve directory structure in output
|
|
516
|
+
const relPath = relative(sourceRoot, sourcePath)
|
|
517
|
+
// add .native suffix when building both targets to avoid overwriting web output
|
|
518
|
+
const outputRelPath = needsNativeSuffix
|
|
519
|
+
? relPath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
520
|
+
: relPath
|
|
521
|
+
nativeOutputPath = join(outputDir, outputRelPath)
|
|
522
|
+
// ensure output subdirectory exists
|
|
523
|
+
await mkdir(dirname(nativeOutputPath), { recursive: true })
|
|
524
|
+
} else if (needsNativeSuffix) {
|
|
525
|
+
// Base file building both targets - create separate .native.tsx
|
|
526
|
+
nativeOutputPath = sourcePath.replace(/\.(tsx|jsx)$/, '.native.$1')
|
|
431
527
|
}
|
|
432
|
-
})()
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
528
|
|
|
436
|
-
|
|
529
|
+
if (nativeOut.output.code) {
|
|
530
|
+
if (nativeOut.output.changed) {
|
|
531
|
+
stats.filesProcessed++
|
|
532
|
+
stats.flattened += nativeOut.plan.stats.flattened
|
|
533
|
+
}
|
|
437
534
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
535
|
+
// Track original if overwriting existing file (skip if using output dir or outputAround)
|
|
536
|
+
if (
|
|
537
|
+
!outputDir &&
|
|
538
|
+
!outputAround &&
|
|
539
|
+
(nativeOutputPath === sourcePath || filePlatforms.length === 1)
|
|
540
|
+
) {
|
|
541
|
+
await trackFile(nativeOutputPath)
|
|
542
|
+
}
|
|
543
|
+
await writeFile(nativeOutputPath, nativeOut.output.code, 'utf-8')
|
|
544
|
+
if (!outputDir && !outputAround) {
|
|
545
|
+
await recordMtime(nativeOutputPath)
|
|
546
|
+
}
|
|
443
547
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
548
|
+
// If creating new .native.tsx, track for deletion (skip if using output dir or outputAround)
|
|
549
|
+
if (
|
|
550
|
+
!outputDir &&
|
|
551
|
+
!outputAround &&
|
|
552
|
+
nativeOutputPath !== sourcePath &&
|
|
553
|
+
filePlatforms.length > 1
|
|
554
|
+
) {
|
|
555
|
+
trackedFiles.push({
|
|
556
|
+
path: nativeOutputPath,
|
|
557
|
+
hardlinkPath: '', // Empty = delete on restore
|
|
558
|
+
mtimeAfterWrite: (await stat(nativeOutputPath)).mtimeMs,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (outputAround) {
|
|
563
|
+
console.info(` → ${nativeOutputPath}`)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// process.env.TAMAGUI_TARGET is process-global, and many tamagui modules read it
|
|
570
|
+
// at module-evaluation time (font family/size tables, isWeb, CSS-variable
|
|
571
|
+
// emission). Compiling files concurrently while flipping it per file let one
|
|
572
|
+
// file's target leak into another's in-flight compile, so the same source could
|
|
573
|
+
// emit web classes built from native config. Finish every file for one target
|
|
574
|
+
// before switching targets.
|
|
575
|
+
for (const target of targets) {
|
|
576
|
+
process.env.TAMAGUI_TARGET = target
|
|
577
|
+
const targetFiles = [...fileToTargets].filter(([, filePlatforms]) =>
|
|
578
|
+
filePlatforms.includes(target)
|
|
579
|
+
)
|
|
580
|
+
const targetPromises = targetFiles.map(([sourcePath, filePlatforms]) =>
|
|
581
|
+
target === 'web'
|
|
582
|
+
? buildWebFile(sourcePath, sources.get(sourcePath)!)
|
|
583
|
+
: buildNativeFile(sourcePath, filePlatforms, sources.get(sourcePath)!)
|
|
450
584
|
)
|
|
451
|
-
|
|
452
|
-
|
|
585
|
+
promises.push(...targetPromises)
|
|
586
|
+
await Promise.all(targetPromises)
|
|
453
587
|
}
|
|
454
|
-
console.info(
|
|
455
|
-
`\n✓ Met optimization target: ${totalOptimizations} >= ${options.expectOptimizations}`
|
|
456
|
-
)
|
|
457
|
-
}
|
|
458
588
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
589
|
+
if (isDryRun) {
|
|
590
|
+
console.info(
|
|
591
|
+
`\n${stats.filesProcessed} files | ${stats.found} found | ${stats.optimized} optimized | ${stats.flattened} flattened | ${stats.styled} styled | ${stats.bailed} bailed\n`
|
|
592
|
+
)
|
|
593
|
+
}
|
|
463
594
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
595
|
+
// Verify expected optimizations if specified
|
|
596
|
+
if (options.expectOptimizations !== undefined) {
|
|
597
|
+
const totalOptimizations = stats.optimized + stats.flattened
|
|
598
|
+
if (totalOptimizations < options.expectOptimizations) {
|
|
599
|
+
console.error(
|
|
600
|
+
`\nExpected at least ${options.expectOptimizations} optimizations but only got ${totalOptimizations}`
|
|
601
|
+
)
|
|
602
|
+
console.error(`Stats: ${JSON.stringify(stats, null, 2)}`)
|
|
603
|
+
await restoreTrackedFiles(true)
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Expected at least ${options.expectOptimizations} optimizations but only got ${totalOptimizations}`
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
console.info(
|
|
609
|
+
`\n✓ Met optimization target: ${totalOptimizations} >= ${options.expectOptimizations}`
|
|
610
|
+
)
|
|
472
611
|
}
|
|
612
|
+
|
|
613
|
+
// If a command was provided, run it and then restore files
|
|
614
|
+
if (options.runCommand && options.runCommand.length > 0) {
|
|
615
|
+
let commandFailed = true
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
await runCommand(options.runCommand, () => restoreTrackedFiles(true))
|
|
619
|
+
commandFailed = false
|
|
620
|
+
} catch (err: any) {
|
|
621
|
+
console.error(`\nCommand failed with exit code ${err.status || 1}`)
|
|
622
|
+
throw err
|
|
623
|
+
} finally {
|
|
624
|
+
// Always restore files
|
|
625
|
+
await restoreTrackedFiles(commandFailed)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return { stats, trackedFiles }
|
|
630
|
+
} catch (error) {
|
|
631
|
+
await Promise.allSettled(promises)
|
|
632
|
+
await restoreTrackedFiles(true)
|
|
633
|
+
throw error
|
|
473
634
|
}
|
|
635
|
+
}
|
|
474
636
|
|
|
475
|
-
|
|
637
|
+
async function runCommand(
|
|
638
|
+
command: string[],
|
|
639
|
+
restore: () => Promise<void>
|
|
640
|
+
): Promise<void> {
|
|
641
|
+
await new Promise<void>((resolveCommand, rejectCommand) => {
|
|
642
|
+
const child = spawn(command[0], command.slice(1), { stdio: 'inherit' })
|
|
643
|
+
let settled = false
|
|
644
|
+
let listenersRestored = false
|
|
645
|
+
const interruptListeners = process.listeners('SIGINT')
|
|
646
|
+
const terminateListeners = process.listeners('SIGTERM')
|
|
647
|
+
process.removeAllListeners('SIGINT')
|
|
648
|
+
process.removeAllListeners('SIGTERM')
|
|
649
|
+
|
|
650
|
+
const cleanup = () => {
|
|
651
|
+
process.off('SIGINT', onInterrupt)
|
|
652
|
+
process.off('SIGTERM', onTerminate)
|
|
653
|
+
if (listenersRestored) return
|
|
654
|
+
listenersRestored = true
|
|
655
|
+
for (const listener of interruptListeners) process.on('SIGINT', listener)
|
|
656
|
+
for (const listener of terminateListeners) process.on('SIGTERM', listener)
|
|
657
|
+
}
|
|
658
|
+
const finishSignal = (signal: NodeJS.Signals) => {
|
|
659
|
+
if (settled) return
|
|
660
|
+
settled = true
|
|
661
|
+
void (async () => {
|
|
662
|
+
const childExited =
|
|
663
|
+
child.exitCode === null && child.signalCode === null
|
|
664
|
+
? new Promise<void>((resolveExit) => child.once('exit', () => resolveExit()))
|
|
665
|
+
: Promise.resolve()
|
|
666
|
+
child.kill(signal)
|
|
667
|
+
await childExited
|
|
668
|
+
try {
|
|
669
|
+
await restore()
|
|
670
|
+
} finally {
|
|
671
|
+
cleanup()
|
|
672
|
+
process.exit(signal === 'SIGINT' ? 130 : 143)
|
|
673
|
+
}
|
|
674
|
+
})()
|
|
675
|
+
}
|
|
676
|
+
const onInterrupt = () => finishSignal('SIGINT')
|
|
677
|
+
const onTerminate = () => finishSignal('SIGTERM')
|
|
678
|
+
process.once('SIGINT', onInterrupt)
|
|
679
|
+
process.once('SIGTERM', onTerminate)
|
|
680
|
+
console.info(`\nRunning: ${command.join(' ')}\n`)
|
|
681
|
+
|
|
682
|
+
child.once('error', (error) => {
|
|
683
|
+
if (settled) return
|
|
684
|
+
settled = true
|
|
685
|
+
cleanup()
|
|
686
|
+
rejectCommand(error)
|
|
687
|
+
})
|
|
688
|
+
child.once('exit', (code, signal) => {
|
|
689
|
+
if (settled) return
|
|
690
|
+
settled = true
|
|
691
|
+
cleanup()
|
|
692
|
+
if (code === 0) {
|
|
693
|
+
resolveCommand()
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
const error = new Error(
|
|
697
|
+
signal ? `Command terminated by ${signal}` : `Command exited with code ${code}`
|
|
698
|
+
) as Error & { status?: number }
|
|
699
|
+
error.status = code ?? 1
|
|
700
|
+
rejectCommand(error)
|
|
701
|
+
})
|
|
702
|
+
})
|
|
476
703
|
}
|
|
477
704
|
|
|
478
705
|
async function restoreFiles(
|
|
479
706
|
trackedFiles: TrackedFile[],
|
|
480
|
-
restoreDir: string | null
|
|
707
|
+
restoreDir: string | null,
|
|
708
|
+
force = false
|
|
481
709
|
): Promise<void> {
|
|
482
|
-
if (!restoreDir
|
|
710
|
+
if (!restoreDir) return
|
|
711
|
+
if (trackedFiles.length === 0) {
|
|
712
|
+
await rm(restoreDir, { recursive: true, force: true })
|
|
713
|
+
return
|
|
714
|
+
}
|
|
483
715
|
|
|
484
716
|
console.info(`\nRestoring ${trackedFiles.length} files...`)
|
|
485
717
|
let restored = 0
|
|
@@ -491,7 +723,7 @@ async function restoreFiles(
|
|
|
491
723
|
const currentStat = await stat(tracked.path).catch(() => null)
|
|
492
724
|
|
|
493
725
|
// Check if file was modified during command execution
|
|
494
|
-
if (currentStat && currentStat.mtimeMs !== tracked.mtimeAfterWrite) {
|
|
726
|
+
if (!force && currentStat && currentStat.mtimeMs !== tracked.mtimeAfterWrite) {
|
|
495
727
|
console.warn(` Skipping ${tracked.path} - modified during build`)
|
|
496
728
|
skipped++
|
|
497
729
|
continue
|