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