space-data-module-sdk 0.8.23 → 0.8.24
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 +1 -1
- package/src/compiler/compileModule.js +378 -385
- package/src/compiler/emceptionNode.js +339 -70
- package/src/index.d.ts +16 -0
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
cp,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { constants as fsConstants, createReadStream, readFileSync } from "node:fs";
|
|
5
16
|
import { createRequire } from "node:module";
|
|
6
17
|
import { pathToFileURL } from "node:url";
|
|
7
18
|
|
|
8
19
|
const require = createRequire(import.meta.url);
|
|
20
|
+
// Bump when the patched tree's layout or meaning changes. The patch transform's
|
|
21
|
+
// own source text is part of the cache key as well (see
|
|
22
|
+
// computeEmceptionIdentity), so editing patchEmceptionModuleSource cannot reuse
|
|
23
|
+
// a tree patched by an older transform even if this string is left alone.
|
|
9
24
|
const PATCH_VERSION = "space-data-module-sdk-emception-node-v1";
|
|
25
|
+
const PATCH_KEY_TAG = "node-v1";
|
|
10
26
|
const PATCH_MARKER_FILENAME = ".space-data-module-sdk-emception-patch";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
// One shared root per (patch, emception build) lives at
|
|
28
|
+
// <tmpdir>/space-data-module-sdk-emception-<tag>-<version>-<fingerprint>-u<uid>.
|
|
29
|
+
// Every process of every consumer run by that user reuses it. Builds happen in a sibling
|
|
30
|
+
// staging dir that is renamed into place only once it is complete.
|
|
31
|
+
const PATCHED_ROOT_PREFIX = "space-data-module-sdk-emception-";
|
|
32
|
+
const STAGING_MARKER = ".staging-";
|
|
33
|
+
const STALE_MARKER = ".stale-";
|
|
34
|
+
const ORPHAN_NAME_PATTERN = new RegExp(
|
|
35
|
+
`^${PATCHED_ROOT_PREFIX.replaceAll("-", "\\-")}.+\\.(?:staging|stale)-(\\d+)-[A-Za-z0-9]+$`,
|
|
14
36
|
);
|
|
37
|
+
const RENAME_COLLISION_CODES = new Set(["ENOTEMPTY", "EEXIST", "EPERM", "EBUSY"]);
|
|
38
|
+
const MAX_PUBLISH_ATTEMPTS = 4;
|
|
15
39
|
const FILE_URL_FETCH_PATCH_FLAG =
|
|
16
40
|
"__spaceDataModuleSdkFileUrlFetchPatched";
|
|
17
41
|
|
|
@@ -84,90 +108,335 @@ function installNodeRuntimeShims() {
|
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
// Pure transform of one emception .mjs file. Its source text is hashed into
|
|
112
|
+
// the cache key, so any edit here yields a fresh patched root.
|
|
113
|
+
function patchEmceptionModuleSource(fileName, source) {
|
|
114
|
+
let patched = source.replaceAll(
|
|
115
|
+
'scriptDirectory=__dirname+"/"',
|
|
116
|
+
'scriptDirectory=(new URL(".", import.meta.url)).pathname',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (fileName === "emception.mjs") {
|
|
120
|
+
patched = patched.replace(
|
|
121
|
+
"this.#fs = await new FileSystem();",
|
|
122
|
+
[
|
|
123
|
+
"this.#fs = await new FileSystem({",
|
|
124
|
+
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
125
|
+
' cache: "/tmp/emception-cache",',
|
|
126
|
+
" });",
|
|
127
|
+
].join("\n"),
|
|
128
|
+
);
|
|
129
|
+
patched = patched.replace(
|
|
130
|
+
"const config = {",
|
|
131
|
+
[
|
|
132
|
+
"const config = {",
|
|
133
|
+
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
134
|
+
].join("\n"),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (fileName === "FileSystem.mjs") {
|
|
139
|
+
patched = patched.replace(
|
|
140
|
+
[
|
|
141
|
+
" if (!this.exists(cache)) {",
|
|
142
|
+
" this.persist(cache);",
|
|
143
|
+
" }",
|
|
144
|
+
" await this.pull();",
|
|
145
|
+
].join("\n"),
|
|
146
|
+
[
|
|
147
|
+
' if (typeof indexedDB !== "undefined") {',
|
|
148
|
+
" if (!this.exists(cache)) {",
|
|
149
|
+
" this.persist(cache);",
|
|
150
|
+
" }",
|
|
151
|
+
" await this.pull();",
|
|
152
|
+
" }",
|
|
153
|
+
].join("\n"),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return patched;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Relative POSIX paths of every regular file under rootDir, sorted.
|
|
161
|
+
async function listTreeFiles(rootDir, relativeDir = "") {
|
|
162
|
+
const entries = await readdir(path.join(rootDir, relativeDir), {
|
|
163
|
+
withFileTypes: true,
|
|
164
|
+
});
|
|
165
|
+
const files = [];
|
|
89
166
|
for (const entry of entries) {
|
|
90
|
-
const
|
|
167
|
+
const relativePath = relativeDir
|
|
168
|
+
? path.posix.join(relativeDir, entry.name)
|
|
169
|
+
: entry.name;
|
|
91
170
|
if (entry.isDirectory()) {
|
|
92
|
-
await
|
|
93
|
-
|
|
171
|
+
files.push(...(await listTreeFiles(rootDir, relativePath)));
|
|
172
|
+
} else if (entry.isFile()) {
|
|
173
|
+
files.push(relativePath);
|
|
94
174
|
}
|
|
95
|
-
|
|
175
|
+
}
|
|
176
|
+
return files.sort();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function patchEmceptionModuleTree(rootDir, files) {
|
|
180
|
+
for (const relativePath of files) {
|
|
181
|
+
if (!relativePath.endsWith(".mjs")) {
|
|
96
182
|
continue;
|
|
97
183
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
184
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
185
|
+
const source = await readFile(fullPath, "utf8");
|
|
186
|
+
const patched = patchEmceptionModuleSource(
|
|
187
|
+
path.posix.basename(relativePath),
|
|
188
|
+
source,
|
|
103
189
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
source = source.replace(
|
|
107
|
-
"this.#fs = await new FileSystem();",
|
|
108
|
-
[
|
|
109
|
-
"this.#fs = await new FileSystem({",
|
|
110
|
-
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
111
|
-
' cache: "/tmp/emception-cache",',
|
|
112
|
-
" });",
|
|
113
|
-
].join("\n"),
|
|
114
|
-
);
|
|
115
|
-
source = source.replace(
|
|
116
|
-
"const config = {",
|
|
117
|
-
[
|
|
118
|
-
"const config = {",
|
|
119
|
-
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
120
|
-
].join("\n"),
|
|
121
|
-
);
|
|
190
|
+
if (patched !== source) {
|
|
191
|
+
await writeFile(fullPath, patched, "utf8");
|
|
122
192
|
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
123
195
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
" await this.pull();",
|
|
131
|
-
].join("\n"),
|
|
132
|
-
[
|
|
133
|
-
' if (typeof indexedDB !== "undefined") {',
|
|
134
|
-
" if (!this.exists(cache)) {",
|
|
135
|
-
" this.persist(cache);",
|
|
136
|
-
" }",
|
|
137
|
-
" await this.pull();",
|
|
138
|
-
" }",
|
|
139
|
-
].join("\n"),
|
|
196
|
+
async function readEmceptionPackageVersion(sourceRoot) {
|
|
197
|
+
let directory = sourceRoot;
|
|
198
|
+
for (;;) {
|
|
199
|
+
try {
|
|
200
|
+
const packageJson = JSON.parse(
|
|
201
|
+
await readFile(path.join(directory, "package.json"), "utf8"),
|
|
140
202
|
);
|
|
203
|
+
if (packageJson?.name === "sdn-emception") {
|
|
204
|
+
return String(packageJson.version ?? "unknown");
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Keep walking up.
|
|
141
208
|
}
|
|
209
|
+
const parent = path.dirname(directory);
|
|
210
|
+
if (parent === directory) {
|
|
211
|
+
return "unknown";
|
|
212
|
+
}
|
|
213
|
+
directory = parent;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
142
216
|
|
|
143
|
-
|
|
217
|
+
// The cache key: the patch version, the patch transform, the emception
|
|
218
|
+
// package version, and a sha256 over every source file's path, size and
|
|
219
|
+
// bytes. Two installs with the same version but different bytes never share
|
|
220
|
+
// a root.
|
|
221
|
+
async function computeEmceptionIdentity(sourceRoot) {
|
|
222
|
+
const version = await readEmceptionPackageVersion(sourceRoot);
|
|
223
|
+
const files = await listTreeFiles(sourceRoot);
|
|
224
|
+
const hash = createHash("sha256");
|
|
225
|
+
hash.update(`${PATCH_VERSION}\0${patchEmceptionModuleSource.toString()}\0`);
|
|
226
|
+
hash.update(`${version}\0`);
|
|
227
|
+
for (const relativePath of files) {
|
|
228
|
+
const fullPath = path.join(sourceRoot, relativePath);
|
|
229
|
+
const { size } = await stat(fullPath);
|
|
230
|
+
hash.update(`${relativePath}\0${size}\0`);
|
|
231
|
+
for await (const chunk of createReadStream(fullPath)) {
|
|
232
|
+
hash.update(chunk);
|
|
233
|
+
}
|
|
234
|
+
hash.update("\0");
|
|
144
235
|
}
|
|
236
|
+
const fingerprint = hash.digest("hex");
|
|
237
|
+
const safeVersion = version.replace(/[^A-Za-z0-9._]/g, "_");
|
|
238
|
+
// Per user: where the tmp dir is shared (Linux /tmp), users never collide.
|
|
239
|
+
const uid = currentUid();
|
|
240
|
+
return {
|
|
241
|
+
version,
|
|
242
|
+
fingerprint,
|
|
243
|
+
files,
|
|
244
|
+
key: `${PATCH_KEY_TAG}-${safeVersion}-${fingerprint.slice(0, 16)}${uid === null ? "" : `-u${uid}`}`,
|
|
245
|
+
};
|
|
145
246
|
}
|
|
146
247
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
248
|
+
// The content check on reuse. The marker is written last inside the staging
|
|
249
|
+
// dir, so a published root always has one; this also catches a root whose
|
|
250
|
+
// files were later deleted or truncated (tmp cleaners purge by age).
|
|
251
|
+
async function isPatchedRootValid(rootDir, identity) {
|
|
252
|
+
// The root is imported as code, so it must be a real directory that this
|
|
253
|
+
// user owns and nobody else can write. Anything else is never trusted.
|
|
254
|
+
try {
|
|
255
|
+
const rootStat = await lstat(rootDir);
|
|
256
|
+
if (!rootStat.isDirectory()) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const uid = currentUid();
|
|
260
|
+
if (uid !== null && (rootStat.uid !== uid || (rootStat.mode & 0o022) !== 0)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
let marker;
|
|
267
|
+
try {
|
|
268
|
+
marker = JSON.parse(
|
|
269
|
+
await readFile(path.join(rootDir, PATCH_MARKER_FILENAME), "utf8"),
|
|
270
|
+
);
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
marker?.patchVersion !== PATCH_VERSION ||
|
|
276
|
+
marker.emceptionVersion !== identity.version ||
|
|
277
|
+
marker.fingerprint !== identity.fingerprint ||
|
|
278
|
+
!Array.isArray(marker.files) ||
|
|
279
|
+
marker.files.length !== identity.files.length
|
|
280
|
+
) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
for (let index = 0; index < marker.files.length; index += 1) {
|
|
284
|
+
const [relativePath, size] = marker.files[index] ?? [];
|
|
285
|
+
if (relativePath !== identity.files[index]) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const entry = await stat(path.join(rootDir, relativePath));
|
|
290
|
+
if (!entry.isFile() || entry.size !== size) {
|
|
291
|
+
return false;
|
|
157
292
|
}
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function currentUid() {
|
|
301
|
+
return typeof process.getuid === "function" ? process.getuid() : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isProcessAlive(pid) {
|
|
305
|
+
try {
|
|
306
|
+
process.kill(pid, 0);
|
|
307
|
+
return true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return error?.code === "EPERM";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
158
312
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
313
|
+
// Staging and stale dirs carry their creator's pid. A process that died
|
|
314
|
+
// mid-build leaves one behind; nothing else would ever remove it.
|
|
315
|
+
async function removeOrphanedBuildDirs(cacheRoot) {
|
|
316
|
+
let names;
|
|
317
|
+
try {
|
|
318
|
+
names = await readdir(cacheRoot);
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const name of names) {
|
|
323
|
+
const match = ORPHAN_NAME_PATTERN.exec(name);
|
|
324
|
+
if (!match) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const pid = Number(match[1]);
|
|
328
|
+
if (pid === process.pid || isProcessAlive(pid)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const uid = currentUid();
|
|
332
|
+
if (uid !== null) {
|
|
333
|
+
const entry = await lstat(path.join(cacheRoot, name)).catch(() => null);
|
|
334
|
+
if (!entry || entry.uid !== uid) {
|
|
335
|
+
continue;
|
|
164
336
|
}
|
|
337
|
+
}
|
|
338
|
+
await rm(path.join(cacheRoot, name), { recursive: true, force: true }).catch(
|
|
339
|
+
() => {},
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
165
343
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
344
|
+
// rename(2) of a directory is atomic: the root either does not exist or is
|
|
345
|
+
// complete. Losing the race to another process is success when its root
|
|
346
|
+
// passes the content check.
|
|
347
|
+
async function publishPatchedRoot(stagingDir, finalDir, identity) {
|
|
348
|
+
let lastError = null;
|
|
349
|
+
for (let attempt = 0; attempt < MAX_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
350
|
+
try {
|
|
351
|
+
await rename(stagingDir, finalDir);
|
|
352
|
+
return;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (!RENAME_COLLISION_CODES.has(error?.code)) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
lastError = error;
|
|
358
|
+
}
|
|
359
|
+
if (await isPatchedRootValid(finalDir, identity)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// What sits at finalDir is incomplete. Move it aside in one rename so no
|
|
363
|
+
// reader ever sees it half-deleted, then retry.
|
|
364
|
+
const staleDir = `${finalDir}${STALE_MARKER}${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
365
|
+
try {
|
|
366
|
+
await rename(finalDir, staleDir);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error?.code === "ENOENT") {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
169
371
|
throw error;
|
|
372
|
+
}
|
|
373
|
+
await rm(staleDir, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Could not publish the patched emception root at ${finalDir}.`,
|
|
377
|
+
{ cause: lastError },
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Returns the shared patched emception root under `cacheRoot` (default
|
|
383
|
+
* `os.tmpdir()`), building it first if it is missing or fails the content
|
|
384
|
+
* check. Safe to call from any number of processes at once.
|
|
385
|
+
*/
|
|
386
|
+
export async function preparePatchedEmceptionRoot(options = {}) {
|
|
387
|
+
const cacheRoot = options.cacheRoot ?? os.tmpdir();
|
|
388
|
+
const sourceRoot =
|
|
389
|
+
options.sourceRoot ?? path.dirname(require.resolve("sdn-emception"));
|
|
390
|
+
const identity = await computeEmceptionIdentity(sourceRoot);
|
|
391
|
+
const finalDir = path.join(cacheRoot, `${PATCHED_ROOT_PREFIX}${identity.key}`);
|
|
392
|
+
if (await isPatchedRootValid(finalDir, identity)) {
|
|
393
|
+
return finalDir;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await removeOrphanedBuildDirs(cacheRoot);
|
|
397
|
+
const stagingDir = await mkdtemp(
|
|
398
|
+
`${finalDir}${STAGING_MARKER}${process.pid}-`,
|
|
399
|
+
);
|
|
400
|
+
try {
|
|
401
|
+
// Copy-on-write clone where the filesystem supports it (APFS, btrfs);
|
|
402
|
+
// a plain copy elsewhere.
|
|
403
|
+
await cp(sourceRoot, stagingDir, {
|
|
404
|
+
recursive: true,
|
|
405
|
+
mode: fsConstants.COPYFILE_FICLONE,
|
|
170
406
|
});
|
|
407
|
+
await patchEmceptionModuleTree(stagingDir, identity.files);
|
|
408
|
+
const files = [];
|
|
409
|
+
for (const relativePath of identity.files) {
|
|
410
|
+
const { size } = await stat(path.join(stagingDir, relativePath));
|
|
411
|
+
files.push([relativePath, size]);
|
|
412
|
+
}
|
|
413
|
+
await writeFile(
|
|
414
|
+
path.join(stagingDir, PATCH_MARKER_FILENAME),
|
|
415
|
+
`${JSON.stringify({
|
|
416
|
+
patchVersion: PATCH_VERSION,
|
|
417
|
+
emceptionVersion: identity.version,
|
|
418
|
+
fingerprint: identity.fingerprint,
|
|
419
|
+
files,
|
|
420
|
+
})}\n`,
|
|
421
|
+
"utf8",
|
|
422
|
+
);
|
|
423
|
+
await publishPatchedRoot(stagingDir, finalDir, identity);
|
|
424
|
+
} finally {
|
|
425
|
+
// A no-op after a successful rename; removes the copy after a lost race
|
|
426
|
+
// or a failure.
|
|
427
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
return finalDir;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function getPatchedEmceptionRoot() {
|
|
433
|
+
if (!patchedEmceptionRootPromise) {
|
|
434
|
+
patchedEmceptionRootPromise = preparePatchedEmceptionRoot().catch(
|
|
435
|
+
(error) => {
|
|
436
|
+
patchedEmceptionRootPromise = null;
|
|
437
|
+
throw error;
|
|
438
|
+
},
|
|
439
|
+
);
|
|
171
440
|
}
|
|
172
441
|
|
|
173
442
|
return patchedEmceptionRootPromise;
|
|
@@ -181,7 +450,7 @@ class EmceptionController {
|
|
|
181
450
|
if (!this.#instancePromise) {
|
|
182
451
|
this.#instancePromise = (async () => {
|
|
183
452
|
installNodeRuntimeShims();
|
|
184
|
-
const patchedRoot = await
|
|
453
|
+
const patchedRoot = await getPatchedEmceptionRoot();
|
|
185
454
|
const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
|
|
186
455
|
const { default: Emception } = await import(moduleUrl);
|
|
187
456
|
const emception = new Emception({
|
package/src/index.d.ts
CHANGED
|
@@ -978,7 +978,16 @@ export interface CompilationResult {
|
|
|
978
978
|
compiler: string;
|
|
979
979
|
language: string;
|
|
980
980
|
threadModel: ModuleThreadModelName;
|
|
981
|
+
/**
|
|
982
|
+
* The explicit `outputPath`, or the artifact inside `tempDir` when the
|
|
983
|
+
* caller passed `keepTempDir: true`. `null` otherwise: the compile's temp
|
|
984
|
+
* dir has already been removed, so use `wasmBytes`.
|
|
985
|
+
*/
|
|
981
986
|
outputPath: string | null;
|
|
987
|
+
/**
|
|
988
|
+
* Non-null only with `keepTempDir: true`. The caller owns it and frees it
|
|
989
|
+
* with `cleanupCompilation(result)`.
|
|
990
|
+
*/
|
|
982
991
|
tempDir: string | null;
|
|
983
992
|
wasmBytes: Uint8Array;
|
|
984
993
|
guestLink: GuestLinkArtifact | null;
|
|
@@ -1054,6 +1063,13 @@ export function compileModuleFromSource(options: {
|
|
|
1054
1063
|
language?: string;
|
|
1055
1064
|
threadModel?: ModuleThreadModelName;
|
|
1056
1065
|
outputPath?: string;
|
|
1066
|
+
/**
|
|
1067
|
+
* Keep the compile's temp dir (toolchain scratch, intermediate objects, and
|
|
1068
|
+
* the artifact when no `outputPath` is given) after a successful compile.
|
|
1069
|
+
* Default `false`: it is removed before the promise settles, on success and
|
|
1070
|
+
* on failure. A failed compile always removes it.
|
|
1071
|
+
*/
|
|
1072
|
+
keepTempDir?: boolean;
|
|
1057
1073
|
importedMemory?: boolean;
|
|
1058
1074
|
sharedMemory?: boolean;
|
|
1059
1075
|
initialMemoryBytes?: number;
|