@exodus/stasis-plugins 1.0.0-beta.15 → 1.0.0-beta.17
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 +2 -2
- package/src/esbuild.js +70 -160
- package/src/metro-babel-transformer.js +20 -41
- package/src/metro-native.js +44 -80
- package/src/metro-transformer.js +18 -46
- package/src/metro.js +102 -293
- package/src/plugins.js +82 -88
- package/src/rollup.js +58 -169
- package/src/webpack.js +56 -160
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 { attestedImport,
|
|
6
|
+
import { assertCodeUtf8, attestedImport, classifyOrThrow, resolvePluginState } from './plugins.js'
|
|
9
7
|
import { State } from '@exodus/stasis-core/state'
|
|
10
8
|
import { realReadFileSync, servedByBundle } from '@exodus/stasis-core/state-util'
|
|
11
|
-
import { classifyExtension } from '@exodus/stasis-core/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,95 +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 served from the bundle under load (servedByBundle).
|
|
99
59
|
const served = (p) => typeof p === 'string' && p !== '' && servedByBundle(state, p)
|
|
100
|
-
|
|
101
|
-
// Family = the sidecar's own app-graph bundle first, then its parent (loaders + resolution-path
|
|
102
|
-
// dirs the ambient preload captured). Both share one lockfile; no parent -> just [state].
|
|
103
60
|
const family = state.parent ? [state, state.parent] : [state]
|
|
104
|
-
// First family State that attests `url` (file or dir), else undefined. Pure existence check.
|
|
105
61
|
const ownerOf = (url) => family.find((s) => s.getFsStat(url) !== undefined)
|
|
106
|
-
|
|
107
|
-
// Serve from whichever family bundle carries the path; getFile re-verifies the hash and throws
|
|
108
|
-
// on a miss (the fail-closed gate -- never a silent disk fallback).
|
|
62
|
+
// Throws on a miss: the fail-closed gate.
|
|
109
63
|
const serveFromBundle = (p) => {
|
|
110
64
|
const url = pathToFileURL(p).toString()
|
|
111
65
|
return (ownerOf(url) ?? state).getFile(url).source
|
|
112
66
|
}
|
|
113
67
|
|
|
114
|
-
// Proxy (not Object.assign): CachedInputFileSystem stores state on `this`, so forwarded calls
|
|
115
|
-
// must keep `this` = orig; we override only readFile and stat.
|
|
116
68
|
const wrappedReadFile = (p, options, callback) => {
|
|
117
69
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
118
70
|
if (served(p)) {
|
|
119
71
|
try {
|
|
120
72
|
const src = serveFromBundle(p)
|
|
121
|
-
|
|
122
|
-
return callback(null, buf)
|
|
73
|
+
return callback(null, Buffer.isBuffer(src) ? src : Buffer.from(src))
|
|
123
74
|
} catch (err) {
|
|
124
75
|
return callback(err)
|
|
125
76
|
}
|
|
126
77
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
if (options === undefined) return orig.readFile(p, callback)
|
|
130
|
-
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)
|
|
131
80
|
}
|
|
132
81
|
const wrappedStat = (p, callback) => {
|
|
133
82
|
if (served(p)) {
|
|
134
|
-
//
|
|
135
|
-
// existence record (getFsStat) -- webpack's resolver stat()s intermediate dirs while walking.
|
|
83
|
+
// The resolver stat()s the dirs it walks, which have no bundle content but an existence record.
|
|
136
84
|
const url = pathToFileURL(p).toString()
|
|
137
|
-
if (ownerOf(url)?.getFsStat(url) === 'directory')
|
|
138
|
-
return callback(null, bundleStat(0, true))
|
|
139
|
-
}
|
|
85
|
+
if (ownerOf(url)?.getFsStat(url) === 'directory') return callback(null, bundleStat(0, true))
|
|
140
86
|
try {
|
|
141
87
|
const src = serveFromBundle(p)
|
|
142
|
-
|
|
143
|
-
const size = typeof src === 'string' ? Buffer.byteLength(src) : src.length
|
|
144
|
-
return callback(null, bundleStat(size))
|
|
88
|
+
return callback(null, bundleStat(typeof src === 'string' ? Buffer.byteLength(src) : src.length))
|
|
145
89
|
} catch (err) {
|
|
146
90
|
return callback(err)
|
|
147
91
|
}
|
|
148
92
|
}
|
|
149
93
|
return orig.stat(p, callback)
|
|
150
94
|
}
|
|
95
|
+
// A Proxy, not Object.assign: CachedInputFileSystem keeps its state on `this`, so forwarded calls keep orig.
|
|
151
96
|
compiler.inputFileSystem = new Proxy(orig, {
|
|
152
97
|
get(target, prop) {
|
|
153
98
|
if (prop === 'readFile') return wrappedReadFile
|
|
@@ -160,41 +105,29 @@ export class StasisWebpack {
|
|
|
160
105
|
compiler.hooks.normalModuleFactory.tap('Stasis', (nmf) => {
|
|
161
106
|
nmf.hooks.beforeResolve.tap('Stasis', (data) => {
|
|
162
107
|
const issuer = data.contextInfo?.issuer
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Built-ins are never bundled or recorded as edges; skip up front. SECURITY: never consult
|
|
167
|
-
// 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`.
|
|
168
111
|
if (isBuiltin(data.request)) return
|
|
169
|
-
const
|
|
170
|
-
// No attested edge: defer to webpack. File BYTES still fail closed at the inputFileSystem wrapper.
|
|
171
|
-
const resolved = attestedImport(state, parentURL, data.request)
|
|
112
|
+
const resolved = attestedImport(state, pathToFileURL(issuer).toString(), data.request)
|
|
172
113
|
if (resolved === undefined) return
|
|
173
|
-
//
|
|
114
|
+
// The stat wrapper then reports it exists.
|
|
174
115
|
data.request = fileURLToPath(resolved.url)
|
|
175
116
|
})
|
|
176
117
|
})
|
|
177
118
|
}
|
|
178
119
|
|
|
179
|
-
// webpack 4's enhanced-resolve has no imports
|
|
180
|
-
//
|
|
181
|
-
// '
|
|
182
|
-
// v3/v4 lib/utils.mapPathString) to record (importer, rewritten) -> original, so afterResolve can
|
|
183
|
-
// key the edge by the portable original. Best-effort, side-effect-only passthrough; absent package
|
|
184
|
-
// 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.
|
|
185
123
|
#patchModuleResolver() {
|
|
186
124
|
const paths = [this.#state.root, process.cwd()]
|
|
187
|
-
|
|
188
|
-
const candidates = [
|
|
189
|
-
{ sub: 'babel-plugin-module-resolver/lib/mapPath', prop: 'default' },
|
|
190
|
-
{ sub: 'babel-plugin-module-resolver/lib/utils', prop: 'mapPathString' },
|
|
191
|
-
]
|
|
192
|
-
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']]) {
|
|
193
126
|
let mod
|
|
194
127
|
try {
|
|
195
128
|
mod = require(require.resolve(sub, { paths }))
|
|
196
129
|
} catch {
|
|
197
|
-
continue
|
|
130
|
+
continue // absent, or a major without this submodule
|
|
198
131
|
}
|
|
199
132
|
const original = mod?.[prop]
|
|
200
133
|
if (typeof original !== 'function' || original[PATCHED]) continue
|
|
@@ -203,21 +136,15 @@ export class StasisWebpack {
|
|
|
203
136
|
const result = original.apply(this, arguments)
|
|
204
137
|
try {
|
|
205
138
|
const after = nodePath?.node?.value
|
|
206
|
-
|
|
207
|
-
// an unchanged node records nothing.
|
|
139
|
+
const importer = state?.file?.opts?.filename
|
|
208
140
|
if (typeof before === 'string' && before.startsWith('#') &&
|
|
209
|
-
typeof after === 'string' && after !== before) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const key = edgeKey(path.resolve(importer), after)
|
|
214
|
-
const originals = subpathOriginals.get(key)
|
|
215
|
-
if (originals === undefined) subpathOriginals.set(key, [before])
|
|
216
|
-
else if (!originals.includes(before)) originals.push(before)
|
|
217
|
-
}
|
|
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])
|
|
218
145
|
}
|
|
219
146
|
} catch {
|
|
220
|
-
// A recorder fault must never break the build
|
|
147
|
+
// A recorder fault must never break the build: the rewrite already ran.
|
|
221
148
|
}
|
|
222
149
|
return result
|
|
223
150
|
}
|
|
@@ -228,14 +155,11 @@ export class StasisWebpack {
|
|
|
228
155
|
|
|
229
156
|
#applyCaptureMode(compiler) {
|
|
230
157
|
this.#captureApplyCount += 1
|
|
231
|
-
//
|
|
232
|
-
// 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.
|
|
233
159
|
this.#patchModuleResolver()
|
|
234
160
|
compiler.hooks.normalModuleFactory.tap('Stasis', (nmf) => {
|
|
235
|
-
// webpack 4
|
|
236
|
-
//
|
|
237
|
-
// tests/fixtures/webpack-stale-issuer-repro/NOTES.md). beforeResolve's contextInfo.issuer is
|
|
238
|
-
// 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].
|
|
239
163
|
const issuerByDep = new WeakMap()
|
|
240
164
|
nmf.hooks.beforeResolve.tap('Stasis', (data) => {
|
|
241
165
|
const dep = data?.dependencies?.[0]
|
|
@@ -244,58 +168,35 @@ export class StasisWebpack {
|
|
|
244
168
|
})
|
|
245
169
|
|
|
246
170
|
nmf.hooks.afterResolve.tap('Stasis', (data) => {
|
|
247
|
-
//
|
|
248
|
-
// webpack 4 hangs them off data. Fall through to data when createData is absent.
|
|
171
|
+
// webpack 5 nests rawRequest/resourceResolveData under createData.
|
|
249
172
|
const cd = data.createData ?? data
|
|
250
|
-
//
|
|
251
|
-
// 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.
|
|
252
174
|
const filePath = cd.resourceResolveData?.path
|
|
253
175
|
if (!filePath || !path.isAbsolute(filePath) || !existsSync(filePath)) return
|
|
254
|
-
|
|
255
|
-
const kind = classifyExtension(filePath, this.#resources)
|
|
256
|
-
if (kind === 'unknown') {
|
|
257
|
-
throw new Error(unsupportedExtension('StasisWebpack', `'${filePath}'`))
|
|
258
|
-
}
|
|
259
|
-
|
|
176
|
+
const kind = classifyOrThrow('StasisWebpack', filePath, this.#resources)
|
|
260
177
|
const url = pathToFileURL(filePath).toString()
|
|
261
|
-
//
|
|
262
|
-
// stale-issuer bug), (2) contextInfo.issuer (fresh on webpack 5), (3) rrd.context.issuer
|
|
263
|
-
// (webpack 4 only; enhanced-resolve 5 removed it).
|
|
178
|
+
// rrd.context.issuer is webpack 4's alone, removed in enhanced-resolve 5.
|
|
264
179
|
const dep = data?.dependencies?.[0]
|
|
265
|
-
const issuer = (dep && issuerByDep.get(dep))
|
|
266
|
-
?? data.contextInfo?.issuer
|
|
267
|
-
?? cd.resourceResolveData?.context?.issuer
|
|
180
|
+
const issuer = (dep && issuerByDep.get(dep)) ?? data.contextInfo?.issuer ?? cd.resourceResolveData?.context?.issuer
|
|
268
181
|
const isEntry = !issuer
|
|
269
182
|
const rawRequest = cd.rawRequest ?? data.rawRequest
|
|
270
|
-
|
|
271
183
|
if (kind === 'code' && !isEntry) {
|
|
272
184
|
const issuerAbsolute = path.resolve(issuer)
|
|
273
185
|
const parentURL = pathToFileURL(issuerAbsolute).toString()
|
|
274
|
-
|
|
275
|
-
// by each recorded ORIGINAL (portable, round-trips at load) instead of the relative path;
|
|
276
|
-
// no rewrite -> fall back to rawRequest.
|
|
277
|
-
const originals = subpathOriginals.get(edgeKey(issuerAbsolute, rawRequest))
|
|
278
|
-
for (const specifier of originals ?? [rawRequest]) {
|
|
186
|
+
for (const specifier of subpathOriginals.get(edgeKey(issuerAbsolute, rawRequest)) ?? [rawRequest]) {
|
|
279
187
|
this.#state.addImport(parentURL, specifier, url)
|
|
280
188
|
}
|
|
281
189
|
}
|
|
282
|
-
|
|
283
190
|
if (!this.#seen.has(filePath)) {
|
|
284
191
|
this.#seen.add(filePath)
|
|
285
|
-
// Real reader (state-util.js): plugin bookkeeping, must not be re-recorded under --fs.
|
|
286
192
|
const source = realReadFileSync(filePath)
|
|
287
|
-
|
|
288
|
-
if (kind === 'code') {
|
|
289
|
-
assert.ok(isUtf8(source), `StasisWebpack: code-classified file has non-UTF-8 bytes: ${filePath}`)
|
|
290
|
-
}
|
|
193
|
+
if (kind === 'code') assertCodeUtf8('StasisWebpack', filePath, source)
|
|
291
194
|
this.#state.addFile(url, { source, isEntry, resource: kind === 'resource', reason: 'webpack' })
|
|
292
195
|
}
|
|
293
196
|
})
|
|
294
197
|
})
|
|
295
198
|
|
|
296
|
-
//
|
|
297
|
-
// changed bytes would emit new bytes while the bundle/lockfile keep the OLD ones. watchRun fires
|
|
298
|
-
// 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.
|
|
299
200
|
compiler.hooks.watchRun.tap('Stasis', () => {
|
|
300
201
|
throw new Error(
|
|
301
202
|
'StasisWebpack: watch mode is not supported for capture -- ' +
|
|
@@ -303,33 +204,28 @@ export class StasisWebpack {
|
|
|
303
204
|
)
|
|
304
205
|
})
|
|
305
206
|
|
|
306
|
-
// The preload writes itself
|
|
307
|
-
// 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.
|
|
308
208
|
if (this.#state !== State.preload) {
|
|
309
209
|
compiler.hooks.done.tap('Stasis', (stats) => {
|
|
310
210
|
if (stats.hasErrors()) return
|
|
311
|
-
//
|
|
312
|
-
// 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.
|
|
313
212
|
if (this.#captureApplyCount === 1) this.#state.write()
|
|
314
213
|
else this.#deferWrite()
|
|
315
214
|
})
|
|
316
215
|
}
|
|
317
216
|
}
|
|
318
217
|
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// beforeExit/exit, so a signal death (SIGINT/SIGTERM/SIGKILL) bypasses it -- same capture-then-exit
|
|
322
|
-
// 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.
|
|
323
220
|
#deferWrite() {
|
|
324
221
|
this.#deferredWriteDirty = true
|
|
325
222
|
if (this.#deferredWriteArmed) return
|
|
326
223
|
this.#deferredWriteArmed = true
|
|
327
224
|
const flush = () => {
|
|
328
|
-
if (!this.#deferredWriteDirty) return
|
|
329
|
-
this.#state.write()
|
|
330
|
-
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
|
|
331
228
|
}
|
|
332
|
-
// beforeExit for the natural drain; exit is the sync backstop for process.exit() (write is sync).
|
|
333
229
|
process.on('beforeExit', flush)
|
|
334
230
|
process.on('exit', flush)
|
|
335
231
|
}
|