@exodus/stasis-plugins 1.0.0-beta.8 → 1.0.0-rc.0
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/package.json +6 -3
- package/src/esbuild.js +168 -115
- package/src/metro-babel-transformer.js +129 -0
- package/src/metro-native.js +201 -0
- package/src/metro-transformer.js +68 -55
- package/src/metro.js +258 -282
- package/src/plugins.js +102 -111
- package/src/rollup.js +64 -188
- package/src/webpack.js +61 -181
package/src/webpack.js
CHANGED
|
@@ -1,57 +1,31 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { isUtf8 } from 'node:buffer'
|
|
3
1
|
import { existsSync } from 'node:fs'
|
|
4
2
|
import { createRequire, isBuiltin } from 'node:module'
|
|
5
3
|
import path from 'node:path'
|
|
6
4
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
5
|
|
|
8
|
-
import { resolvePluginState } from './plugins.js'
|
|
6
|
+
import { assertCodeUtf8, attestedImport, classifyOrThrow, resolvePluginState } from './plugins.js'
|
|
9
7
|
import { State } from '@exodus/stasis-core/state'
|
|
10
|
-
import { realReadFileSync } from '@exodus/stasis-core/state-util'
|
|
11
|
-
import { classifyExtension } from '@exodus/stasis-core/util'
|
|
8
|
+
import { realReadFileSync, servedByBundle } from '@exodus/stasis-core/state-util'
|
|
12
9
|
|
|
13
|
-
// CJS require anchored here, used only to locate the PROJECT's babel-plugin-module-resolver below.
|
|
14
10
|
const require = createRequire(import.meta.url)
|
|
15
11
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// so afterResolve records an edge for each). Module-level so the single patch and every instance's
|
|
19
|
-
// lookup share it; never cleared (keys deterministic, values deduped).
|
|
12
|
+
// The package.json "imports" subpaths ('#app/abc.js') babel-plugin-module-resolver rewrote, by (importer, rewritten
|
|
13
|
+
// request) -> the '#'-specifiers (several aliases can collide on one key). Shared by the one patch and every instance.
|
|
20
14
|
const subpathOriginals = new Map()
|
|
21
15
|
const edgeKey = (importerAbsolute, request) => `${importerAbsolute}\u0000${request}`
|
|
22
16
|
|
|
23
|
-
// Marks an already-wrapped chokepoint so we don't double-wrap babel-plugin-module-resolver.
|
|
24
17
|
const PATCHED = Symbol('stasis.modresolver.patched')
|
|
25
18
|
|
|
26
|
-
//
|
|
27
|
-
// size; mtime/mode aren't security-relevant (the lockfile attests content via hash).
|
|
19
|
+
// For a bundle-served file under load: webpack's resolver needs only existence and size, the hash attests content.
|
|
28
20
|
function bundleStat(size, isDir = false) {
|
|
29
21
|
return {
|
|
30
22
|
isFile: () => !isDir,
|
|
31
23
|
isDirectory: () => isDir,
|
|
32
|
-
isSymbolicLink
|
|
33
|
-
isBlockDevice: () => false,
|
|
34
|
-
isCharacterDevice: () => false,
|
|
35
|
-
isFIFO: () => false,
|
|
36
|
-
isSocket: () => false,
|
|
24
|
+
...Object.fromEntries(['isSymbolicLink', 'isBlockDevice', 'isCharacterDevice', 'isFIFO', 'isSocket'].map((name) => [name, () => false])),
|
|
37
25
|
size,
|
|
38
|
-
mtime
|
|
39
|
-
mtimeMs: 0,
|
|
40
|
-
atime: new Date(0),
|
|
41
|
-
atimeMs: 0,
|
|
42
|
-
ctime: new Date(0),
|
|
43
|
-
ctimeMs: 0,
|
|
44
|
-
birthtime: new Date(0),
|
|
45
|
-
birthtimeMs: 0,
|
|
26
|
+
...Object.fromEntries(['mtime', 'atime', 'ctime', 'birthtime'].flatMap((name) => [[name, new Date(0)], [`${name}Ms`, 0]])),
|
|
46
27
|
mode: isDir ? 0o040755 : 0o100644,
|
|
47
|
-
uid: 0,
|
|
48
|
-
gid: 0,
|
|
49
|
-
ino: 0,
|
|
50
|
-
nlink: 1,
|
|
51
|
-
rdev: 0,
|
|
52
|
-
blksize: 4096,
|
|
53
|
-
blocks: 0,
|
|
54
|
-
dev: 0,
|
|
28
|
+
uid: 0, gid: 0, ino: 0, nlink: 1, rdev: 0, blksize: 4096, blocks: 0, dev: 0,
|
|
55
29
|
}
|
|
56
30
|
}
|
|
57
31
|
|
|
@@ -59,101 +33,66 @@ export class StasisWebpack {
|
|
|
59
33
|
#seen = new Set()
|
|
60
34
|
#state
|
|
61
35
|
#resources
|
|
62
|
-
|
|
63
|
-
// Number of compilers this instance drives (final by the first `done`): 1 for a lone compiler,
|
|
64
|
-
// N for a MultiCompiler. Only the multi case accumulates across N `done`s and defers its write.
|
|
36
|
+
// The compilers this instance drives, final by the first `done`: N for a MultiCompiler, whose write is deferred.
|
|
65
37
|
#captureApplyCount = 0
|
|
66
|
-
|
|
67
|
-
// Deferred-write bookkeeping for the multi-compiler case (see #deferWrite). #armed: the single
|
|
68
|
-
// flush pair is registered; #dirty: a clean build completed since the last flush.
|
|
69
38
|
#deferredWriteArmed = false
|
|
70
39
|
#deferredWriteDirty = false
|
|
71
40
|
|
|
72
41
|
constructor(options = {}) {
|
|
73
42
|
const { state } = resolvePluginState('StasisWebpack', options, process.cwd())
|
|
74
|
-
this.#state = state
|
|
75
|
-
// Cache the resolved resources Set for the per-file classify hot path.
|
|
43
|
+
this.#state = state
|
|
76
44
|
this.#resources = state?.config.resources ?? new Set()
|
|
77
45
|
}
|
|
78
46
|
|
|
79
47
|
apply(compiler) {
|
|
80
48
|
if (!this.#state) return
|
|
81
|
-
if (this.#state.config.loadBundle)
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
this.#applyCaptureMode(compiler)
|
|
49
|
+
if (this.#state.config.loadBundle) this.#applyLoadMode(compiler)
|
|
50
|
+
else this.#applyCaptureMode(compiler)
|
|
86
51
|
}
|
|
87
52
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// "The bundle" is the FAMILY -- this State plus its parent when running as a sidecar (both under
|
|
92
|
-
// one shared lockfile). KNOWN LIMITATION: package.json the resolver reads via fs.readFile
|
|
93
|
-
// (DescriptionFilePlugin) that no family bundle carries still fails closed -- see tests/webpack.test.js TODO.
|
|
53
|
+
// As `stasis run --bundle=load`: an in-scope path comes from the bundle (getFile re-verifies the hash), never from
|
|
54
|
+
// disk. The bundle is the FAMILY: this State, then its parent where it is a sidecar (one shared lockfile). KNOWN
|
|
55
|
+
// LIMITATION: a package.json the resolver reads that no family bundle carries fails closed (tests/webpack.test.js).
|
|
94
56
|
#applyLoadMode(compiler) {
|
|
95
57
|
const state = this.#state
|
|
96
58
|
const orig = compiler.inputFileSystem
|
|
97
|
-
|
|
98
|
-
// True iff `p` is in scope for load mode. state.relative throws outside state.root (treated
|
|
99
|
-
// as out-of-scope: system files, scratch).
|
|
100
|
-
const inScope = (p) => {
|
|
101
|
-
if (typeof p !== 'string') return false
|
|
102
|
-
try { state.relative(p) } catch { return false }
|
|
103
|
-
if (state.config.full) return true
|
|
104
|
-
return state.inNodeModules(pathToFileURL(p).toString())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Family = the sidecar's own app-graph bundle first, then its parent (loaders + resolution-path
|
|
108
|
-
// dirs the ambient preload captured). Both share one lockfile; no parent -> just [state].
|
|
59
|
+
const served = (p) => typeof p === 'string' && p !== '' && servedByBundle(state, p)
|
|
109
60
|
const family = state.parent ? [state, state.parent] : [state]
|
|
110
|
-
// First family State that attests `url` (file or dir), else undefined. Pure existence check.
|
|
111
61
|
const ownerOf = (url) => family.find((s) => s.getFsStat(url) !== undefined)
|
|
112
|
-
|
|
113
|
-
// Serve from whichever family bundle carries the path; getFile re-verifies the hash and throws
|
|
114
|
-
// on a miss (the fail-closed gate -- never a silent disk fallback).
|
|
62
|
+
// Throws on a miss: the fail-closed gate.
|
|
115
63
|
const serveFromBundle = (p) => {
|
|
116
64
|
const url = pathToFileURL(p).toString()
|
|
117
65
|
return (ownerOf(url) ?? state).getFile(url).source
|
|
118
66
|
}
|
|
119
67
|
|
|
120
|
-
// Proxy (not Object.assign): CachedInputFileSystem stores state on `this`, so forwarded calls
|
|
121
|
-
// must keep `this` = orig; we override only readFile and stat.
|
|
122
68
|
const wrappedReadFile = (p, options, callback) => {
|
|
123
69
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
124
|
-
if (
|
|
70
|
+
if (served(p)) {
|
|
125
71
|
try {
|
|
126
72
|
const src = serveFromBundle(p)
|
|
127
|
-
|
|
128
|
-
return callback(null, buf)
|
|
73
|
+
return callback(null, Buffer.isBuffer(src) ? src : Buffer.from(src))
|
|
129
74
|
} catch (err) {
|
|
130
75
|
return callback(err)
|
|
131
76
|
}
|
|
132
77
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
if (options === undefined) return orig.readFile(p, callback)
|
|
136
|
-
return orig.readFile(p, options, callback)
|
|
78
|
+
// CachedInputFileSystem.readFile is (path, callback): an undefined middle arg would land in the callback slot.
|
|
79
|
+
return options === undefined ? orig.readFile(p, callback) : orig.readFile(p, options, callback)
|
|
137
80
|
}
|
|
138
81
|
const wrappedStat = (p, callback) => {
|
|
139
|
-
if (
|
|
140
|
-
//
|
|
141
|
-
// existence record (getFsStat) -- webpack's resolver stat()s intermediate dirs while walking.
|
|
82
|
+
if (served(p)) {
|
|
83
|
+
// The resolver stat()s the dirs it walks, which have no bundle content but an existence record.
|
|
142
84
|
const url = pathToFileURL(p).toString()
|
|
143
|
-
if (ownerOf(url)?.getFsStat(url) === 'directory')
|
|
144
|
-
return callback(null, bundleStat(0, true))
|
|
145
|
-
}
|
|
85
|
+
if (ownerOf(url)?.getFsStat(url) === 'directory') return callback(null, bundleStat(0, true))
|
|
146
86
|
try {
|
|
147
87
|
const src = serveFromBundle(p)
|
|
148
|
-
|
|
149
|
-
const size = typeof src === 'string' ? Buffer.byteLength(src) : src.length
|
|
150
|
-
return callback(null, bundleStat(size))
|
|
88
|
+
return callback(null, bundleStat(typeof src === 'string' ? Buffer.byteLength(src) : src.length))
|
|
151
89
|
} catch (err) {
|
|
152
90
|
return callback(err)
|
|
153
91
|
}
|
|
154
92
|
}
|
|
155
93
|
return orig.stat(p, callback)
|
|
156
94
|
}
|
|
95
|
+
// A Proxy, not Object.assign: CachedInputFileSystem keeps its state on `this`, so forwarded calls keep orig.
|
|
157
96
|
compiler.inputFileSystem = new Proxy(orig, {
|
|
158
97
|
get(target, prop) {
|
|
159
98
|
if (prop === 'readFile') return wrappedReadFile
|
|
@@ -166,48 +105,29 @@ export class StasisWebpack {
|
|
|
166
105
|
compiler.hooks.normalModuleFactory.tap('Stasis', (nmf) => {
|
|
167
106
|
nmf.hooks.beforeResolve.tap('Stasis', (data) => {
|
|
168
107
|
const issuer = data.contextInfo?.issuer
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Built-ins are never bundled or recorded as edges; skip up front. SECURITY: never consult
|
|
173
|
-
// getImport for a builtin name, so a tampered bundle can't redirect `fs` to an arbitrary file.
|
|
108
|
+
// An entry is absolute already, and an out-of-scope parent webpack's to resolve.
|
|
109
|
+
if (!issuer || !served(issuer)) return
|
|
110
|
+
// SECURITY: never consult getImport for a builtin, so a tampered bundle can't redirect `fs`.
|
|
174
111
|
if (isBuiltin(data.request)) return
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
resolved = state.getImport(parentURL, data.request)
|
|
179
|
-
} catch (err) {
|
|
180
|
-
// No attested edge -- at a successful capture that's an EXTERNAL (never recorded as an
|
|
181
|
-
// edge), so defer to webpack. File BYTES still fail closed at the inputFileSystem wrapper.
|
|
182
|
-
// A non-MODULE_NOT_FOUND error is a real fault -- rethrow.
|
|
183
|
-
if (err?.code === 'ERR_MODULE_NOT_FOUND') return
|
|
184
|
-
throw err
|
|
185
|
-
}
|
|
186
|
-
// Redirect the resolver to the absolute bundle path; the stat wrapper then reports it exists.
|
|
112
|
+
const resolved = attestedImport(state, pathToFileURL(issuer).toString(), data.request)
|
|
113
|
+
if (resolved === undefined) return
|
|
114
|
+
// The stat wrapper then reports it exists.
|
|
187
115
|
data.request = fileURLToPath(resolved.url)
|
|
188
116
|
})
|
|
189
117
|
})
|
|
190
118
|
}
|
|
191
119
|
|
|
192
|
-
// webpack 4's enhanced-resolve has no imports
|
|
193
|
-
//
|
|
194
|
-
// '
|
|
195
|
-
// v3/v4 lib/utils.mapPathString) to record (importer, rewritten) -> original, so afterResolve can
|
|
196
|
-
// key the edge by the portable original. Best-effort, side-effect-only passthrough; absent package
|
|
197
|
-
// skips the patch; PATCHED guards against re-wrapping.
|
|
120
|
+
// webpack 4's enhanced-resolve has no imports field, so babel-plugin-module-resolver rewrites '#app/abc.js' before
|
|
121
|
+
// the parser and afterResolve sees only the rewrite. Wrapping its rewrite chokepoint (v5's lib/mapPath default, or
|
|
122
|
+
// v3/v4's lib/utils.mapPathString) records the original, so the edge is keyed by the portable specifier.
|
|
198
123
|
#patchModuleResolver() {
|
|
199
124
|
const paths = [this.#state.root, process.cwd()]
|
|
200
|
-
|
|
201
|
-
const candidates = [
|
|
202
|
-
{ sub: 'babel-plugin-module-resolver/lib/mapPath', prop: 'default' },
|
|
203
|
-
{ sub: 'babel-plugin-module-resolver/lib/utils', prop: 'mapPathString' },
|
|
204
|
-
]
|
|
205
|
-
for (const { sub, prop } of candidates) {
|
|
125
|
+
for (const [sub, prop] of [['babel-plugin-module-resolver/lib/mapPath', 'default'], ['babel-plugin-module-resolver/lib/utils', 'mapPathString']]) {
|
|
206
126
|
let mod
|
|
207
127
|
try {
|
|
208
128
|
mod = require(require.resolve(sub, { paths }))
|
|
209
129
|
} catch {
|
|
210
|
-
continue
|
|
130
|
+
continue // absent, or a major without this submodule
|
|
211
131
|
}
|
|
212
132
|
const original = mod?.[prop]
|
|
213
133
|
if (typeof original !== 'function' || original[PATCHED]) continue
|
|
@@ -216,21 +136,15 @@ export class StasisWebpack {
|
|
|
216
136
|
const result = original.apply(this, arguments)
|
|
217
137
|
try {
|
|
218
138
|
const after = nodePath?.node?.value
|
|
219
|
-
|
|
220
|
-
// an unchanged node records nothing.
|
|
139
|
+
const importer = state?.file?.opts?.filename
|
|
221
140
|
if (typeof before === 'string' && before.startsWith('#') &&
|
|
222
|
-
typeof after === 'string' && after !== before) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const key = edgeKey(path.resolve(importer), after)
|
|
227
|
-
const originals = subpathOriginals.get(key)
|
|
228
|
-
if (originals === undefined) subpathOriginals.set(key, [before])
|
|
229
|
-
else if (!originals.includes(before)) originals.push(before)
|
|
230
|
-
}
|
|
141
|
+
typeof after === 'string' && after !== before && typeof importer === 'string') {
|
|
142
|
+
const key = edgeKey(path.resolve(importer), after)
|
|
143
|
+
const originals = subpathOriginals.get(key) ?? []
|
|
144
|
+
if (!originals.includes(before)) subpathOriginals.set(key, [...originals, before])
|
|
231
145
|
}
|
|
232
146
|
} catch {
|
|
233
|
-
// A recorder fault must never break the build
|
|
147
|
+
// A recorder fault must never break the build: the rewrite already ran.
|
|
234
148
|
}
|
|
235
149
|
return result
|
|
236
150
|
}
|
|
@@ -241,14 +155,11 @@ export class StasisWebpack {
|
|
|
241
155
|
|
|
242
156
|
#applyCaptureMode(compiler) {
|
|
243
157
|
this.#captureApplyCount += 1
|
|
244
|
-
//
|
|
245
|
-
// compiler.run()), so the recorder is populated before afterResolve looks an edge up.
|
|
158
|
+
// apply() runs at webpack() construction, before compiler.run(), so before any afterResolve.
|
|
246
159
|
this.#patchModuleResolver()
|
|
247
160
|
compiler.hooks.normalModuleFactory.tap('Stasis', (nmf) => {
|
|
248
|
-
// webpack 4
|
|
249
|
-
//
|
|
250
|
-
// tests/fixtures/webpack-stale-issuer-repro/NOTES.md). beforeResolve's contextInfo.issuer is
|
|
251
|
-
// fresh, so capture it there in a WeakMap keyed by dep[0] (the same ref flows through create()).
|
|
161
|
+
// webpack 4 reuses rrd.context.issuer across factory calls, so by afterResolve it can hold an earlier
|
|
162
|
+
// resolution's (tests/fixtures/webpack-stale-issuer-repro/NOTES.md); beforeResolve's is fresh, by dep[0].
|
|
252
163
|
const issuerByDep = new WeakMap()
|
|
253
164
|
nmf.hooks.beforeResolve.tap('Stasis', (data) => {
|
|
254
165
|
const dep = data?.dependencies?.[0]
|
|
@@ -257,61 +168,35 @@ export class StasisWebpack {
|
|
|
257
168
|
})
|
|
258
169
|
|
|
259
170
|
nmf.hooks.afterResolve.tap('Stasis', (data) => {
|
|
260
|
-
//
|
|
261
|
-
// webpack 4 hangs them off data. Fall through to data when createData is absent.
|
|
171
|
+
// webpack 5 nests rawRequest/resourceResolveData under createData.
|
|
262
172
|
const cd = data.createData ?? data
|
|
263
|
-
//
|
|
264
|
-
// skip data: URIs, null-loader stubs, and any resolution with no on-disk file to hash.
|
|
173
|
+
// No on-disk file to hash: data: URIs, null-loader stubs.
|
|
265
174
|
const filePath = cd.resourceResolveData?.path
|
|
266
175
|
if (!filePath || !path.isAbsolute(filePath) || !existsSync(filePath)) return
|
|
267
|
-
|
|
268
|
-
const kind = classifyExtension(filePath, this.#resources)
|
|
269
|
-
if (kind === 'unknown') {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`StasisWebpack: unsupported extension for '${filePath}' -- ` +
|
|
272
|
-
`add its extension or filename to the plugin's resources option or stop importing it`
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
176
|
+
const kind = classifyOrThrow('StasisWebpack', filePath, this.#resources)
|
|
276
177
|
const url = pathToFileURL(filePath).toString()
|
|
277
|
-
//
|
|
278
|
-
// stale-issuer bug), (2) contextInfo.issuer (fresh on webpack 5), (3) rrd.context.issuer
|
|
279
|
-
// (webpack 4 only; enhanced-resolve 5 removed it).
|
|
178
|
+
// rrd.context.issuer is webpack 4's alone, removed in enhanced-resolve 5.
|
|
280
179
|
const dep = data?.dependencies?.[0]
|
|
281
|
-
const issuer = (dep && issuerByDep.get(dep))
|
|
282
|
-
?? data.contextInfo?.issuer
|
|
283
|
-
?? cd.resourceResolveData?.context?.issuer
|
|
180
|
+
const issuer = (dep && issuerByDep.get(dep)) ?? data.contextInfo?.issuer ?? cd.resourceResolveData?.context?.issuer
|
|
284
181
|
const isEntry = !issuer
|
|
285
182
|
const rawRequest = cd.rawRequest ?? data.rawRequest
|
|
286
|
-
|
|
287
183
|
if (kind === 'code' && !isEntry) {
|
|
288
184
|
const issuerAbsolute = path.resolve(issuer)
|
|
289
185
|
const parentURL = pathToFileURL(issuerAbsolute).toString()
|
|
290
|
-
|
|
291
|
-
// by each recorded ORIGINAL (portable, round-trips at load) instead of the relative path;
|
|
292
|
-
// no rewrite -> fall back to rawRequest.
|
|
293
|
-
const originals = subpathOriginals.get(edgeKey(issuerAbsolute, rawRequest))
|
|
294
|
-
for (const specifier of originals ?? [rawRequest]) {
|
|
186
|
+
for (const specifier of subpathOriginals.get(edgeKey(issuerAbsolute, rawRequest)) ?? [rawRequest]) {
|
|
295
187
|
this.#state.addImport(parentURL, specifier, url)
|
|
296
188
|
}
|
|
297
189
|
}
|
|
298
|
-
|
|
299
190
|
if (!this.#seen.has(filePath)) {
|
|
300
191
|
this.#seen.add(filePath)
|
|
301
|
-
// Real reader (state-util.js): plugin bookkeeping, must not be re-recorded under --fs.
|
|
302
192
|
const source = realReadFileSync(filePath)
|
|
303
|
-
|
|
304
|
-
if (kind === 'code') {
|
|
305
|
-
assert.ok(isUtf8(source), `StasisWebpack: code-classified file has non-UTF-8 bytes: ${filePath}`)
|
|
306
|
-
}
|
|
193
|
+
if (kind === 'code') assertCodeUtf8('StasisWebpack', filePath, source)
|
|
307
194
|
this.#state.addFile(url, { source, isEntry, resource: kind === 'resource', reason: 'webpack' })
|
|
308
195
|
}
|
|
309
196
|
})
|
|
310
197
|
})
|
|
311
198
|
|
|
312
|
-
//
|
|
313
|
-
// changed bytes would emit new bytes while the bundle/lockfile keep the OLD ones. watchRun fires
|
|
314
|
-
// only under compiler.watch(), before the first build, so throwing here refuses the whole session.
|
|
199
|
+
// watchRun fires only under compiler.watch(), before the first build, so this refuses the whole session.
|
|
315
200
|
compiler.hooks.watchRun.tap('Stasis', () => {
|
|
316
201
|
throw new Error(
|
|
317
202
|
'StasisWebpack: watch mode is not supported for capture -- ' +
|
|
@@ -319,33 +204,28 @@ export class StasisWebpack {
|
|
|
319
204
|
)
|
|
320
205
|
})
|
|
321
206
|
|
|
322
|
-
// The preload writes itself
|
|
323
|
-
// finish -- only on a clean build (a partial one would overwrite a good lockfile).
|
|
207
|
+
// The preload writes itself; a failed build mustn't overwrite a good lockfile.
|
|
324
208
|
if (this.#state !== State.preload) {
|
|
325
209
|
compiler.hooks.done.tap('Stasis', (stats) => {
|
|
326
210
|
if (stats.hasErrors()) return
|
|
327
|
-
//
|
|
328
|
-
// so writing each time pays N super-linear brotli passes over a growing bundle; coalesce to one.
|
|
211
|
+
// `done` fires per child of a MultiCompiler: one write, not N brotli passes over a growing bundle.
|
|
329
212
|
if (this.#captureApplyCount === 1) this.#state.write()
|
|
330
213
|
else this.#deferWrite()
|
|
331
214
|
})
|
|
332
215
|
}
|
|
333
216
|
}
|
|
334
217
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
// beforeExit/exit, so a signal death (SIGINT/SIGTERM/SIGKILL) bypasses it -- same capture-then-exit
|
|
338
|
-
// assumption as the preload's write (hooks.js).
|
|
218
|
+
// A single flush at beforeExit, with exit as the sync backstop for process.exit(). KNOWN LIMITATION: a signal death
|
|
219
|
+
// bypasses it, as it does the preload's write.
|
|
339
220
|
#deferWrite() {
|
|
340
221
|
this.#deferredWriteDirty = true
|
|
341
222
|
if (this.#deferredWriteArmed) return
|
|
342
223
|
this.#deferredWriteArmed = true
|
|
343
224
|
const flush = () => {
|
|
344
|
-
if (!this.#deferredWriteDirty) return
|
|
345
|
-
this.#state.write()
|
|
346
|
-
this.#deferredWriteDirty = false
|
|
225
|
+
if (!this.#deferredWriteDirty) return
|
|
226
|
+
this.#state.write() // on a throw, dirty stays set so exit retries
|
|
227
|
+
this.#deferredWriteDirty = false
|
|
347
228
|
}
|
|
348
|
-
// beforeExit for the natural drain; exit is the sync backstop for process.exit() (write is sync).
|
|
349
229
|
process.on('beforeExit', flush)
|
|
350
230
|
process.on('exit', flush)
|
|
351
231
|
}
|