@volter/browser-substrate 0.1.103 → 0.1.105

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.
Files changed (160) hide show
  1. package/catalog/demand-input.json +133 -0
  2. package/dist/boot-screen.d.ts +2 -0
  3. package/dist/boot-screen.d.ts.map +1 -1
  4. package/dist/boot-screen.js +1 -1
  5. package/dist/boot-screen.js.map +1 -1
  6. package/dist/cli/archive.d.ts +1 -0
  7. package/dist/cli/archive.d.ts.map +1 -1
  8. package/dist/cli/archive.js +4 -4
  9. package/dist/cli/archive.js.map +1 -1
  10. package/dist/cli/build.d.ts +5 -1
  11. package/dist/cli/build.d.ts.map +1 -1
  12. package/dist/cli/build.js +21 -2
  13. package/dist/cli/build.js.map +1 -1
  14. package/dist/cli/cache-root.d.ts +7 -0
  15. package/dist/cli/cache-root.d.ts.map +1 -0
  16. package/dist/cli/cache-root.js +24 -0
  17. package/dist/cli/cache-root.js.map +1 -0
  18. package/dist/cli/companions.d.ts +1 -1
  19. package/dist/cli/companions.d.ts.map +1 -1
  20. package/dist/cli/companions.js +30 -16
  21. package/dist/cli/companions.js.map +1 -1
  22. package/dist/cli/dependency-plan-build.d.ts +4 -0
  23. package/dist/cli/dependency-plan-build.d.ts.map +1 -1
  24. package/dist/cli/dependency-plan-build.js +111 -30
  25. package/dist/cli/dependency-plan-build.js.map +1 -1
  26. package/dist/cli/docker-build.d.ts +1 -0
  27. package/dist/cli/docker-build.d.ts.map +1 -1
  28. package/dist/cli/docker-build.js +4 -4
  29. package/dist/cli/docker-build.js.map +1 -1
  30. package/dist/cli/dry-run.d.ts +4 -4
  31. package/dist/cli/dry-run.d.ts.map +1 -1
  32. package/dist/cli/dry-run.js +10 -9
  33. package/dist/cli/dry-run.js.map +1 -1
  34. package/dist/cli/generated-code.d.ts +2 -1
  35. package/dist/cli/generated-code.d.ts.map +1 -1
  36. package/dist/cli/generated-code.js +46 -16
  37. package/dist/cli/generated-code.js.map +1 -1
  38. package/dist/cli/host-build.d.ts +1 -0
  39. package/dist/cli/host-build.d.ts.map +1 -1
  40. package/dist/cli/host-build.js +10 -9
  41. package/dist/cli/host-build.js.map +1 -1
  42. package/dist/cli/host-managers.d.ts +3 -1
  43. package/dist/cli/host-managers.d.ts.map +1 -1
  44. package/dist/cli/host-managers.js +7 -3
  45. package/dist/cli/host-managers.js.map +1 -1
  46. package/dist/cli/image-layers.d.ts.map +1 -1
  47. package/dist/cli/image-layers.js +5 -2
  48. package/dist/cli/image-layers.js.map +1 -1
  49. package/dist/cli/main.d.ts +5 -1
  50. package/dist/cli/main.d.ts.map +1 -1
  51. package/dist/cli/main.js +97 -34
  52. package/dist/cli/main.js.map +1 -1
  53. package/dist/cli/pack-publication.d.ts +2 -2
  54. package/dist/cli/pack-publication.d.ts.map +1 -1
  55. package/dist/cli/pack-publication.js +3 -3
  56. package/dist/cli/pack-publication.js.map +1 -1
  57. package/dist/cli/package-pack.d.ts +1 -1
  58. package/dist/cli/package-pack.d.ts.map +1 -1
  59. package/dist/cli/package-pack.js +7 -4
  60. package/dist/cli/package-pack.js.map +1 -1
  61. package/dist/cli/package-packs.d.ts +1 -0
  62. package/dist/cli/package-packs.d.ts.map +1 -1
  63. package/dist/cli/package-packs.js +84 -14
  64. package/dist/cli/package-packs.js.map +1 -1
  65. package/dist/cli/plan-tarballs.d.ts +1 -0
  66. package/dist/cli/plan-tarballs.d.ts.map +1 -1
  67. package/dist/cli/plan-tarballs.js +6 -3
  68. package/dist/cli/plan-tarballs.js.map +1 -1
  69. package/dist/cli/preparation-memory.d.ts +17 -0
  70. package/dist/cli/preparation-memory.d.ts.map +1 -0
  71. package/dist/cli/preparation-memory.js +367 -0
  72. package/dist/cli/preparation-memory.js.map +1 -0
  73. package/dist/cli/project-build.d.ts +1 -0
  74. package/dist/cli/project-build.d.ts.map +1 -1
  75. package/dist/cli/project-build.js +100 -71
  76. package/dist/cli/project-build.js.map +1 -1
  77. package/dist/cli/provision-catalog.d.ts +49 -0
  78. package/dist/cli/provision-catalog.d.ts.map +1 -0
  79. package/dist/cli/provision-catalog.js +72 -0
  80. package/dist/cli/provision-catalog.js.map +1 -0
  81. package/dist/cli/provision-demand.d.ts +45 -0
  82. package/dist/cli/provision-demand.d.ts.map +1 -0
  83. package/dist/cli/provision-demand.js +208 -0
  84. package/dist/cli/provision-demand.js.map +1 -0
  85. package/dist/cli/rolldown-binding.d.ts +1 -1
  86. package/dist/cli/rolldown-binding.d.ts.map +1 -1
  87. package/dist/cli/rolldown-binding.js +8 -4
  88. package/dist/cli/rolldown-binding.js.map +1 -1
  89. package/dist/cli/serve.d.ts +3 -0
  90. package/dist/cli/serve.d.ts.map +1 -1
  91. package/dist/cli/serve.js +9 -2
  92. package/dist/cli/serve.js.map +1 -1
  93. package/dist/index.d.ts +2 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +75 -31
  96. package/dist/index.js.map +1 -1
  97. package/dist/origin-reset.d.ts +4 -0
  98. package/dist/origin-reset.d.ts.map +1 -0
  99. package/dist/origin-reset.js +76 -0
  100. package/dist/origin-reset.js.map +1 -0
  101. package/dist/program-dom.d.ts +2 -0
  102. package/dist/program-dom.d.ts.map +1 -1
  103. package/dist/program-dom.js +11 -1
  104. package/dist/program-dom.js.map +1 -1
  105. package/dist/program-types.d.ts +2 -0
  106. package/dist/program-types.d.ts.map +1 -1
  107. package/dist/shell/assets/browser-opfs-filesystem-worker-CmF6UKMW.js +3 -0
  108. package/dist/shell/assets/browser-sandbox-bridge-C8nlSsT2.js +237 -0
  109. package/dist/shell/assets/{filesystem-host-DJbFuokb.js → filesystem-host-BzPlXSri.js} +1 -1
  110. package/dist/shell/assets/filesystem-host-Pq9IHKRx.js +3 -0
  111. package/dist/shell/assets/filesystem-worker-CC3pSXUP.js +5 -0
  112. package/dist/shell/assets/{fork-worker-BXY2kfdX.js → fork-worker-4YKQ4KTo.js} +1 -1
  113. package/dist/shell/assets/{node-execution-worker-7e8e0c5321ca0d10.js → node-execution-worker-3ddf5efbaa427e9d.js} +8 -8
  114. package/dist/shell/assets/{sandbox-bMeKqKBn.js → sandbox-mhFzIWaQ.js} +1 -1
  115. package/dist/shell/assets/{shell-DLIQ6jHu.js → shell-BZbhxnju.js} +763 -757
  116. package/dist/shell/assets/worker-BmHEnCyv.js +816 -0
  117. package/dist/shell/assets/{worker-program-worker-B4OcKVtV.js → worker-program-worker-DueMwljE.js} +10 -10
  118. package/dist/shell/brand/fonts/Geist-Variable.woff2 +0 -0
  119. package/dist/shell/brand/fonts/GeistMono-Variable.woff2 +0 -0
  120. package/dist/shell/brand/fonts/InstrumentSerif-Italic.woff2 +0 -0
  121. package/dist/shell/brand/fonts/InstrumentSerif-Regular.woff2 +0 -0
  122. package/dist/shell/brand/icons/app-window.svg +1 -0
  123. package/dist/shell/brand/icons/at-sign.svg +1 -0
  124. package/dist/shell/brand/icons/box.svg +1 -0
  125. package/dist/shell/brand/icons/chevron-right.svg +1 -0
  126. package/dist/shell/brand/icons/folder.svg +1 -0
  127. package/dist/shell/brand/icons/info.svg +1 -0
  128. package/dist/shell/brand/icons/key-round.svg +1 -0
  129. package/dist/shell/brand/icons/link.svg +1 -0
  130. package/dist/shell/brand/icons/monitor.svg +1 -0
  131. package/dist/shell/brand/icons/plus.svg +1 -0
  132. package/dist/shell/brand/icons/rotate-ccw.svg +1 -0
  133. package/dist/shell/brand/icons/share-2.svg +1 -0
  134. package/dist/shell/brand/icons/square-arrow-out-up-right.svg +1 -0
  135. package/dist/shell/brand/icons/terminal.svg +1 -0
  136. package/dist/shell/brand/icons/x.svg +1 -0
  137. package/dist/shell/brand/substrate.svg +1 -0
  138. package/dist/shell/brand/tokens.css +211 -0
  139. package/dist/shell/brand/wordmark.svg +1 -0
  140. package/dist/shell/index.html +5 -3
  141. package/dist/shell/node-execution-worker.js +16 -13
  142. package/dist/shell/node-host.html +2 -0
  143. package/dist/shell/python-execution-worker.js +15 -12
  144. package/dist/shell/python-host.html +2 -0
  145. package/dist/shell/python-runtime-snapshot.bin +0 -0
  146. package/dist/shell/python-runtime-snapshot.json +1 -1
  147. package/dist/shell/sandbox.html +4 -2
  148. package/dist/toolbar-styles.generated.d.ts +1 -1
  149. package/dist/toolbar-styles.generated.d.ts.map +1 -1
  150. package/dist/toolbar-styles.generated.js +1 -1
  151. package/dist/toolbar-styles.generated.js.map +1 -1
  152. package/dist/toolbar.d.ts.map +1 -1
  153. package/dist/toolbar.js +3 -1
  154. package/dist/toolbar.js.map +1 -1
  155. package/package.json +1 -1
  156. package/dist/shell/assets/browser-opfs-filesystem-worker-k2CPzPBU.js +0 -3
  157. package/dist/shell/assets/browser-sandbox-bridge-BJbQoc4f.js +0 -237
  158. package/dist/shell/assets/filesystem-host-yMcJuvmT.js +0 -3
  159. package/dist/shell/assets/filesystem-worker-BbsBhZ9p.js +0 -5
  160. package/dist/shell/assets/worker-Ce8bBwSJ.js +0 -816
package/dist/cli/main.js CHANGED
@@ -7,7 +7,8 @@
7
7
  import { catalogProvidedPackages } from "../emscripten-catalog.js";
8
8
  import { shellPrograms } from "./shell-programs.js";
9
9
  import { createHash } from "node:crypto";
10
- import { homedir } from "node:os";
10
+ import { preparationCacheDirectory, selectedCacheRoot } from "./cache-root.js";
11
+ import { preparationBounded, preparationCheckpoint, preparationMemoryLimit, preparationPhase, withPreparationMemory } from "./preparation-memory.js";
11
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, watch, writeFileSync } from "node:fs";
12
13
  import { basename, join, relative, resolve, sep } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
@@ -38,15 +39,18 @@ import { detectSetup, SetupError } from "../setup.js";
38
39
  import { carryRegistryTarballs } from "./plan-tarballs.js";
39
40
  import { builtForPlan } from "./project-build.js";
40
41
  import { TAB_IMAGE_PROGRAMS, TAB_PROVIDED_IMAGES } from "../program-images.js";
42
+ import { formatBrowserProvisionPlan, planBrowserProvisionDemand, readBrowserProvisionDemand } from "./provision-demand.js";
41
43
  const USAGE = `Usage:
42
44
  browser-substrate dev [--port <n>] [--no-open] [--no-packs] [--world <dir>] [--dir <sub>] [--build <command>] [settings] [-- <command>]
43
45
  run this folder in a tab, attached to a World; a command after -- is what serves it,
44
46
  run in --dir, the way a developer types it, in place of what the folder's files say;
45
47
  --build is the project's own build, run on this machine at its root, its output carried
46
- browser-substrate build [--out <dir>] [--world <dir>] [--project <id>] [--build-arg <name=value>] [--program <browser-program.json> --programs-dir <dir>] [settings]
48
+ browser-substrate build [--out <dir>] [--cache-root <dir>] [--world <dir>] [--project <id>] [--build-arg <name=value>] [--program <browser-program.json> --programs-dir <dir>] [settings]
47
49
  write ./dist: the page and the image; a --program the image's command names
48
50
  is carried in its layers, its archives read from <dir><url>; --world images the
49
51
  World the tab runs the image's command under; --build-arg is a Dockerfile's ARG
52
+ browser-substrate plan-provisions [--provision-catalog <json>] [--source-pin <commit>] [--definition <file>] [--dependency-plan <json>]
53
+ read and report demand without preparing an image or starting compute
50
54
  browser-substrate preview [--port <n>] [--out <dir>] [--no-open] serve ./dist locally with its _headers
51
55
  browser-substrate dry-run [--page <url>] [--settle <s>] [--rounds <n>] [--captured <file>] [--steps <file.json>]
52
56
  open the image in Chrome and record every install the tab refuses; what a pack
@@ -59,6 +63,9 @@ Settings, all optional, needed only to serve something yourself:
59
63
  --sandbox-origin <origin> where a playground's sandbox is served (default: sandbox.<host>)
60
64
  --relay <origin> the relay for Git and outbound TCP (default: ours, under its allowance)
61
65
  --artifacts <url> where verified runtime artifacts are fetched from (default: ours)
66
+ --cache-root <dir> preparation cache and scratch; overrides BROWSER_SUBSTRATE_CACHE_ROOT
67
+ --provision-catalog <json> local publication directory, pinned catalog digest and runtime profile
68
+ --preparation-memory-mib <n> build's owned-tree RSS admission; overrides BROWSER_SUBSTRATE_PREPARATION_MEMORY_MIB
62
69
  --name <image> the image name a page opens (default: app)
63
70
  --no-packs skip preparing packs on this machine; the tab never builds a package, so such an
64
71
  image is refused at the first import that needs one
@@ -69,11 +76,18 @@ export async function main(argv) {
69
76
  const cwd = process.cwd();
70
77
  switch (args.command) {
71
78
  case "dev": return dev(cwd, args);
72
- case "build": return build(cwd, args);
79
+ case "build": return withPreparationMemory(args.preparationMemoryMiB, resolve(cwd, args.out), () => build(cwd, args));
73
80
  case "init": return init(cwd);
74
81
  case "preview": return preview(cwd, args);
75
82
  case "dockerfile": return dockerfile(cwd, args);
76
83
  case "dry-run": return dryRunCommand(cwd, args);
84
+ case "plan-provisions": {
85
+ const catalog = args.provisionCatalogFile ? JSON.parse(readFileSync(resolve(cwd, args.provisionCatalogFile), "utf8")) : undefined;
86
+ const demand = readBrowserProvisionDemand(cwd, { abi: catalog?.profile.abi ?? "unselected", ...(args.sourcePin ? { sourcePin: args.sourcePin } : {}), ...(args.definition ? { selectedFile: args.definition } : {}), ...(args.dependencyPlanFile ? { plan: JSON.parse(readFileSync(resolve(cwd, args.dependencyPlanFile), "utf8")) } : {}) });
87
+ const plan = catalog ? await planBrowserProvisionDemand(demand, catalog) : undefined;
88
+ process.stdout.write(formatBrowserProvisionPlan(demand, plan));
89
+ return demand.incomplete.length || demand.items.some((item, index) => item.scope === "selected" && plan?.rows[index]?.status !== "resolved") ? 2 : 0;
90
+ }
77
91
  default:
78
92
  process.stdout.write(USAGE);
79
93
  return args.command === undefined || args.command === "help" ? 0 : 2;
@@ -115,6 +129,18 @@ function parse(argv) {
115
129
  // builds each package itself, which is what it did before it carried any.
116
130
  else if (arg === "--no-packs")
117
131
  args.packs = false;
132
+ else if (arg === "--cache-root")
133
+ args.cacheRoot = value();
134
+ else if (arg === "--provision-catalog")
135
+ args.provisionCatalogFile = value();
136
+ else if (arg === "--source-pin")
137
+ args.sourcePin = value();
138
+ else if (arg === "--definition")
139
+ args.definition = value();
140
+ else if (arg === "--dependency-plan")
141
+ args.dependencyPlanFile = value();
142
+ else if (arg === "--preparation-memory-mib")
143
+ args.preparationMemoryMiB = preparationMemoryLimit(value());
118
144
  else if (arg === "--out")
119
145
  args.out = value();
120
146
  else if (arg === "--name")
@@ -166,6 +192,18 @@ function parse(argv) {
166
192
  throw new Error("--name must be lowercase letters, digits, dots, dashes, or underscores.");
167
193
  if (!Number.isInteger(args.port) || args.port < 1 || args.port > 65_535)
168
194
  throw new Error("--port must be a port number.");
195
+ const cacheRoot = selectedCacheRoot(args.cacheRoot);
196
+ if (cacheRoot)
197
+ args.cacheRoot = cacheRoot;
198
+ if (args.command === "build" && args.preparationMemoryMiB === undefined) {
199
+ const memoryLimit = preparationMemoryLimit();
200
+ if (memoryLimit !== undefined)
201
+ args.preparationMemoryMiB = memoryLimit;
202
+ }
203
+ if (args.command !== "build" && args.preparationMemoryMiB !== undefined)
204
+ throw new Error("--preparation-memory-mib applies to build only.");
205
+ if (args.command !== "plan-provisions" && (args.sourcePin || args.definition || args.dependencyPlanFile))
206
+ throw new Error("--source-pin, --definition and --dependency-plan apply to plan-provisions only.");
169
207
  return args;
170
208
  }
171
209
  /** What a folder carries, for the setup reader. */
@@ -186,6 +224,12 @@ export async function buildImage(folder, name, settings, project) {
186
224
  }
187
225
  export async function buildImageWith(folder, name, settings, project, options) {
188
226
  assertShell();
227
+ // Report all static demands before even an archive copy or setup refusal.
228
+ // Existing families remain on their only current path until their genuine
229
+ // replacement is published and their atomic consumer/removal cutover lands.
230
+ const demand = readBrowserProvisionDemand(folder, { abi: options.provisionCatalog?.profile.abi ?? "unselected", ...(options.buildArgs ? { buildArgs: options.buildArgs } : {}) });
231
+ const provisionPlan = options.provisionCatalog ? await planBrowserProvisionDemand(demand, options.provisionCatalog) : undefined;
232
+ process.stdout.write(formatBrowserProvisionPlan(demand, provisionPlan));
189
233
  let setup;
190
234
  try {
191
235
  setup = await detectSetup(projectFiles(folder), { ...(options.serve ? { serve: options.serve } : {}), ...(options.directory ? { directory: options.directory } : {}) });
@@ -198,7 +242,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
198
242
  if (!setup) {
199
243
  throw new Error(`${folder} carries nothing to run: add a Dockerfile with a CMD, or a package.json with a dev script. An empty folder becomes a playground with \`browser-substrate init\`.`);
200
244
  }
201
- let archive = archiveFolder(folder);
245
+ let archive = archiveFolder(folder, options.cacheRoot ? { cacheRoot: options.cacheRoot } : {});
202
246
  let preparation = { location: "none" };
203
247
  if (setup.kind === "dockerfile") {
204
248
  // A Dockerfile has two halves. The tab builds and runs a file it can take
@@ -224,6 +268,8 @@ export async function buildImageWith(folder, name, settings, project, options) {
224
268
  // the tab receives what it made.
225
269
  const runSteps = parseContainerFile(dockerfile).stages.flatMap((stage) => stage.instructions.filter((instruction) => instruction.kind === "run").map((instruction) => instruction.line));
226
270
  if (report.phases.build.verdict !== "native" || runSteps.length > 0) {
271
+ if (preparationBounded())
272
+ throw new Error("Bounded preparation refuses container RUN backends: their external/runtime tasks lack complete owned native-process cancellation/RSS coverage. No fallback was started.");
227
273
  process.stdout.write(report.phases.build.verdict !== "native"
228
274
  ? ` the build half needs more than the tab has (${report.phases.build.blocking.map((b) => `line ${b.line}`).join(", ")}); building it here\n`
229
275
  : ` the build half runs ${runSteps.length} RUN step${runSteps.length === 1 ? "" : "s"} (line${runSteps.length === 1 ? "" : "s"} ${runSteps.join(", ")}); building it here, where setup runs\n`);
@@ -237,6 +283,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
237
283
  if (options.buildArgs)
238
284
  throw new Error(`Build arguments (${Object.keys(options.buildArgs).join(", ")}) are read by Docker`);
239
285
  built = await buildOnHost(folder, dockerfile, (text) => process.stdout.write(text), {
286
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
240
287
  ...(setup.target ? { target: setup.target } : {}),
241
288
  ...(setup.context ? { context: setup.context } : {}),
242
289
  });
@@ -254,6 +301,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
254
301
  ...(externalBase(report) ? { base: externalBase(report) } : {}),
255
302
  programs: tabLaneTable().programs,
256
303
  ...(options.buildArgs ? { buildArgs: options.buildArgs } : {}),
304
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
257
305
  ...(setup.target ? { target: setup.target } : {}),
258
306
  ...(setup.context ? { context: setup.context } : {}),
259
307
  });
@@ -274,7 +322,9 @@ export async function buildImageWith(folder, name, settings, project, options) {
274
322
  let rolldown;
275
323
  const tarballs = new Map();
276
324
  if (options.dependencies !== false && setup.kind === "manifests" && setup.install) {
325
+ preparationPhase("plan");
277
326
  const resolved = await resolveDependencyPlan(folder, setup.install, {
327
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
278
328
  ...(options.packagesDirectory ? { packagesDirectory: options.packagesDirectory } : {}),
279
329
  onOutput: (text) => process.stdout.write(text),
280
330
  // What the tab's copy of the project holds is the archive, not the
@@ -300,7 +350,8 @@ export async function buildImageWith(folder, name, settings, project, options) {
300
350
  if (options.packagesDirectory)
301
351
  servePlanTarballsFromOrigin(plan, options.packagesDirectory);
302
352
  // What the project's toolchain needs in the tab beyond its lockfile.
303
- const companions = await addCompanionPackages(plan, (text) => process.stdout.write(text));
353
+ preparationPhase("generate");
354
+ const companions = await addCompanionPackages(plan, (text) => process.stdout.write(text), options.cacheRoot);
304
355
  // The companions are part of what the tab links, so they are part of the
305
356
  // plan's key: a workspace linked from an image without them, whose key
306
357
  // was the same, kept its tree and never received them.
@@ -316,6 +367,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
316
367
  // was the same, would keep its tree and never receive it.
317
368
  const generated = await generatedForPlan(folder, plan, setup.install, {
318
369
  imageName: name,
370
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
319
371
  ...(options.packagesDirectory ? { packagesDirectory: options.packagesDirectory } : {}),
320
372
  onOutput: (text) => process.stdout.write(text),
321
373
  });
@@ -332,12 +384,14 @@ export async function buildImageWith(folder, name, settings, project, options) {
332
384
  if (captured.packs.length > 0)
333
385
  process.stdout.write(` captured: ${captured.packs.length} packs an earlier dry run asked for\n`);
334
386
  if (options.build) {
387
+ preparationPhase("product build");
335
388
  const built = await builtForPlan(folder, plan, setup.install, options.build, {
336
389
  imageName: name,
337
390
  sourcesDigest: archive.integrity,
338
391
  holds: (path) => archive.files.has(path),
339
392
  ...(options.buildEnv ? { env: options.buildEnv } : {}),
340
393
  ...(options.buildWorld ? { world: options.buildWorld } : {}),
394
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
341
395
  ...(options.packagesDirectory ? { packagesDirectory: options.packagesDirectory } : {}),
342
396
  onOutput: (text) => process.stdout.write(text),
343
397
  });
@@ -357,7 +411,8 @@ export async function buildImageWith(folder, name, settings, project, options) {
357
411
  if (plan.entries.length > 0)
358
412
  plan.key = `${plan.key}-${createHash("sha256").update(plan.entries.map((entry) => `${entry.path}\0${entry.content}`).join("\n")).digest("hex").slice(0, 12)}`;
359
413
  // The project's own Rolldown binding, where its version is not the shell's.
360
- const binding = await rolldownBindingForPlan(plan, imageRolldownPath(name, plan.packages.find((entry) => entry.name === "rolldown")?.version ?? ""), (text) => process.stdout.write(text));
414
+ preparationPhase("generate");
415
+ const binding = await rolldownBindingForPlan(plan, imageRolldownPath(name, plan.packages.find((entry) => entry.name === "rolldown")?.version ?? ""), (text) => process.stdout.write(text), options.cacheRoot);
361
416
  if (binding) {
362
417
  for (const [served, source] of binding.files)
363
418
  tarballs.set(served, source);
@@ -367,11 +422,12 @@ export async function buildImageWith(folder, name, settings, project, options) {
367
422
  // Every registry package the plan names travels with the image, the
368
423
  // companions included: the tab never reaches a registry, and refuses a
369
424
  // plan that names one.
370
- const carried = await carryRegistryTarballs(plan, name);
425
+ preparationPhase("package archives");
426
+ const carried = await carryRegistryTarballs(plan, name, options.cacheRoot ? { cacheRoot: options.cacheRoot } : {});
371
427
  process.stdout.write(` carried: ${carried.files.size} registry packages, ${formatBytes(carried.bytes)}, ${carried.fetched} fetched here, ${carried.cached} from the cache\n`);
372
428
  // What travels is each package's pack, its tree laid out here; the tab
373
429
  // never reads a tarball (Article 1), so the tarballs stay on this machine.
374
- const planPacks = await packPlan(plan, name, carried.files, tarballs, options.packagesDirectory ?? join(folder, PACKAGES_DIRECTORY));
430
+ const planPacks = await packPlan(plan, name, carried.files, tarballs, options.packagesDirectory ?? join(folder, PACKAGES_DIRECTORY), options.cacheRoot);
375
431
  for (const [served, source] of planPacks.files)
376
432
  tarballs.set(served, source);
377
433
  // The packs are what a warm open links; another set of them is another plan.
@@ -379,9 +435,10 @@ export async function buildImageWith(folder, name, settings, project, options) {
379
435
  process.stdout.write(` packages as packs: ${planPacks.files.size} archives, ${formatBytes(planPacks.bytes)}\n`);
380
436
  // Each package's browser build, made here under the key the tab
381
437
  // computes for it. The tab links what is carried and builds the rest.
382
- const moduleWorld = options.moduleWorld ? await buildWorldImage(options.moduleWorld, name) : undefined;
438
+ const moduleWorld = options.moduleWorld ? await buildWorldImage(options.moduleWorld, name, options.cacheRoot) : undefined;
383
439
  let builtForLayers;
384
440
  if (options.packs !== false) {
441
+ preparationPhase("bundles");
385
442
  const built = await packsForPlan(folder, plan, setup.install, {
386
443
  moduleSourcesKey: `${archive.integrity}|${moduleWorld?.world.archive.integrity ?? ""}`,
387
444
  ...(options.moduleWorld ? { moduleSourceTrees: [options.moduleWorld] } : {}),
@@ -392,6 +449,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
392
449
  throw new Error(`No carried module pack for ${entry.name}@${entry.version}`);
393
450
  return { file, artifact: pack.artifact };
394
451
  }),
452
+ ...(options.cacheRoot ? { cacheRoot: options.cacheRoot } : {}),
395
453
  ...(options.packagesDirectory ? { packagesDirectory: options.packagesDirectory } : {}),
396
454
  // What the image's dry run captured last time is prepared too (ADR-0043).
397
455
  ...(prebundled.length > 0 || captured.packs.length > 0 ? { extra: [...prebundled, ...captured.packs] } : {}),
@@ -404,7 +462,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
404
462
  const have = new Set(built.modules.map((body) => body.key));
405
463
  let carried = 0;
406
464
  for (const key of captured.prepared) {
407
- const file = join(preparedBodiesDirectory(captured.cache), key);
465
+ const file = join(preparedBodiesDirectory(captured.cache, options.cacheRoot), key);
408
466
  if (have.has(key) || !existsSync(file))
409
467
  continue;
410
468
  built.modules.push({ key, file, bytes: statSync(file).size });
@@ -439,6 +497,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
439
497
  // What all of that puts in the origin's shared store, laid out as the
440
498
  // store keeps it, in parts the tab appends whole (ADR-0045).
441
499
  if (options.layers !== false) {
500
+ preparationPhase("layers");
442
501
  const startedAt = Date.now();
443
502
  // World packages enter the same content-addressed store before its links
444
503
  // are placed. Leaving them out makes every cold tab expand them again.
@@ -450,7 +509,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
450
509
  { root: "/workspace/app", tree: treeUnit(archive.bytes, archive.blobs) },
451
510
  ...(moduleWorld ? [{ root: `/workspace/worlds/${moduleWorld.world.name}`, tree: treeUnit(moduleWorld.archive, moduleWorld.blobs) }] : []),
452
511
  ].flatMap(({ root, tree }) => tree ? [{ root, ...tree }] : []);
453
- layers = await buildImageLayers(name, withTrees(layerUnits(layerPlan, layerFiles, builtForLayers, options.programs ?? [], options.programsDirectory), trees.map(({ unit }) => unit)), 0, undefined, {
512
+ layers = await buildImageLayers(name, withTrees(layerUnits(layerPlan, layerFiles, builtForLayers, options.programs ?? [], options.programsDirectory), trees.map(({ unit }) => unit)), 0, preparationCacheDirectory("layer-parts", options.cacheRoot), {
454
513
  plans: [{ root: "/workspace/app", plan }, ...(moduleWorld?.plan ? [{ root: `/workspace/worlds/${moduleWorld.world.name}`, plan: moduleWorld.plan }] : [])],
455
514
  trees: trees.map(({ root, unit, integrity, mtimes }) => ({ root, unit: unit.root, integrity, mtimes })),
456
515
  });
@@ -465,8 +524,8 @@ export async function buildImageWith(folder, name, settings, project, options) {
465
524
  // packs is another pre-bundle, which the tab would serve as this one.
466
525
  const captureKey = readCaptured(options.capturedFile ?? join(folder, PACKAGES_DIRECTORY, "..", CAPTURED_FILE)).cache;
467
526
  const packsDigest = packs ? createHash("sha256").update(JSON.stringify(packs.document)).digest("hex").slice(0, 16) : undefined;
468
- const warmRecord = captureKey ? readWarmRecord(warmDirectory(captureKey)) : undefined;
469
- const warm = captureKey && warmRecord && packsDigest && warmRecord.packs?.digest === packsDigest ? warmFiles(join(warmDirectory(captureKey), "files")) : undefined;
527
+ const warmRecord = captureKey ? readWarmRecord(warmDirectory(captureKey, options.cacheRoot)) : undefined;
528
+ const warm = captureKey && warmRecord && packsDigest && warmRecord.packs?.digest === packsDigest ? warmFiles(join(warmDirectory(captureKey, options.cacheRoot), "files")) : undefined;
470
529
  for (const file of warm?.files ?? [])
471
530
  archive.blobs.set(file.integrity, file.source);
472
531
  if (warm)
@@ -501,7 +560,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
501
560
  // The origin's Cache Storage the dry run left (an engine's artifacts, the
502
561
  // service worker's copies of the project's servers' cacheable answers):
503
562
  // a first open puts each back before anything asks for it.
504
- const caches = captureKey ? capturedCaches(name, cachesDirectory(captureKey)) : undefined;
563
+ const caches = captureKey ? capturedCaches(name, cachesDirectory(captureKey, options.cacheRoot)) : undefined;
505
564
  if (caches) {
506
565
  const bytes = caches.entries.reduce((total, entry) => total + entry.bytes, 0);
507
566
  manifest.caches = { url: `/substrate/${name}.caches.json`, entries: caches.entries.length, bytes };
@@ -510,7 +569,7 @@ export async function buildImageWith(folder, name, settings, project, options) {
510
569
  const startup = readStartupRecord(folder);
511
570
  let startupStreams;
512
571
  if (startup) {
513
- const streams = layers ? writeStartupStreams(name, startup.digests, layers.files) : undefined;
572
+ const streams = layers ? writeStartupStreams(name, startup.digests, layers.files, options.cacheRoot) : undefined;
514
573
  startupStreams = streams?.files;
515
574
  manifest.startup = { url: imageStartupPath(name), ranges: startup.ranges, ...(streams ? { streams: streams.streams } : {}) };
516
575
  process.stdout.write(` startup: ${startup.ranges} image ranges a first boot read, from ${STARTUP_RECORD}${streams ? `, as ${streams.streams.length} streams of ${formatBytes(streams.streams.reduce((total, stream) => total + stream.bytes, 0))}` : ""}\n`);
@@ -754,7 +813,7 @@ function decodeBase64Content(value) {
754
813
  * files, or the project's own in its packages directory, which the plan names
755
814
  * at the origin's registry path.
756
815
  */
757
- function packPlan(plan, imageName, carried, local, packagesDirectory) {
816
+ function packPlan(plan, imageName, carried, local, packagesDirectory, cacheRoot) {
758
817
  const own = packagesDirectory ? readOriginRegistry(packagesDirectory) : undefined;
759
818
  return preparePlanPacks(plan, imageName, (entry) => {
760
819
  // A registry package carried here; a tarball the image names under its
@@ -764,7 +823,7 @@ function packPlan(plan, imageName, carried, local, packagesDirectory) {
764
823
  return fromRegistry;
765
824
  const file = entry.tarball.startsWith(`${REGISTRY_PATH}/`) ? own?.get(entry.name)?.get(entry.version)?.file : undefined;
766
825
  return file && packagesDirectory ? join(packagesDirectory, file) : undefined;
767
- });
826
+ }, cacheRoot);
768
827
  }
769
828
  /** Every file of the image and its World by the path it is served at, and where the folder holds it. */
770
829
  export function imageBlobs(image) {
@@ -802,11 +861,11 @@ const STARTUP_STREAMS = 2;
802
861
  * its served length and its served bytes. One request per helper instead of
803
862
  * one per range: 830 requests took 0.85 s on localhost to deliver 43 MB.
804
863
  */
805
- function writeStartupStreams(name, digests, layerFiles) {
864
+ function writeStartupStreams(name, digests, layerFiles, cacheRoot) {
806
865
  const held = [...new Set(digests)].filter((digest) => layerFiles.has(`/substrate/${name}.layers/${digest}.bin`));
807
866
  if (held.length === 0)
808
867
  return undefined;
809
- const directory = join(homedir(), ".cache", "browser-substrate", "startup-streams");
868
+ const directory = preparationCacheDirectory("startup-streams", cacheRoot);
810
869
  mkdirSync(directory, { recursive: true });
811
870
  const streams = [];
812
871
  const files = new Map();
@@ -872,7 +931,7 @@ export function imageFiles(image) {
872
931
  * directory is the World's, outside any project; the World's own runtime
873
932
  * runs from it in the tab.
874
933
  */
875
- export async function buildWorldImage(directory, imageName) {
934
+ export async function buildWorldImage(directory, imageName, cacheRoot) {
876
935
  const folder = resolve(directory);
877
936
  const config = ["world.json"].find((name) => existsSync(join(folder, name)));
878
937
  if (!config)
@@ -883,18 +942,18 @@ export async function buildWorldImage(directory, imageName) {
883
942
  // What travels is the World's state, its data; a runtime's bookkeeping of
884
943
  // one instance, its pids, logs, receipts, session keys and proxy, is that
885
944
  // runtime's and stays where it ran.
886
- const archive = archiveFolder(folder, { exclude: new Set([...EXCLUDED_NAMES, ...WORLD_RUNTIME_NAMES]) });
945
+ const archive = archiveFolder(folder, { ...(cacheRoot ? { cacheRoot } : {}), exclude: new Set([...EXCLUDED_NAMES, ...WORLD_RUNTIME_NAMES]) });
887
946
  const packagesDirectory = join(folder, PACKAGES_DIRECTORY);
888
947
  const tarballs = new Map();
889
948
  let plan;
890
949
  if (existsSync(join(folder, "package.json"))) {
891
- const resolved = await resolveDependencyPlan(folder, installCommandOf(folder), { packagesDirectory, onOutput: (text) => process.stdout.write(text), holds: (path) => archive.files.has(path) });
950
+ const resolved = await resolveDependencyPlan(folder, installCommandOf(folder), { ...(cacheRoot ? { cacheRoot } : {}), packagesDirectory, onOutput: (text) => process.stdout.write(text), holds: (path) => archive.files.has(path) });
892
951
  if (resolved) {
893
952
  plan = resolved.plan;
894
953
  servePlanTarballsFromOrigin(plan, packagesDirectory);
895
954
  process.stdout.write(` world ${name}: ${plan.packages.length} packages at ${plan.links.length} paths, resolved with ${plan.resolver}${resolved.cached ? ", from the cache" : ` in ${resolved.durationMs} ms`}\n`);
896
- const carried = await carryRegistryTarballs(plan, imageName);
897
- for (const [served, source] of (await packPlan(plan, imageName, carried.files, tarballs, packagesDirectory)).files)
955
+ const carried = await carryRegistryTarballs(plan, imageName, cacheRoot ? { cacheRoot } : {});
956
+ for (const [served, source] of (await packPlan(plan, imageName, carried.files, tarballs, packagesDirectory, cacheRoot)).files)
898
957
  tarballs.set(served, source);
899
958
  plan.key = `${plan.key}-p${createHash("sha256").update(plan.packages.map((entry) => `${entry.pack?.artifact.digest ?? ""}:${entry.linuxX64}`).join("\n")).digest("hex").slice(0, 12)}`;
900
959
  if (carried.files.size > 0)
@@ -924,7 +983,9 @@ export async function buildWorldImage(directory, imageName) {
924
983
  /** What a command's arguments ask of an image. */
925
984
  function imageOptions(args, packagesDirectory) {
926
985
  return {
986
+ ...(args.provisionCatalogFile ? { provisionCatalog: JSON.parse(readFileSync(resolve(args.provisionCatalogFile), "utf8")) } : {}),
927
987
  ...(packagesDirectory ? { packagesDirectory } : {}),
988
+ ...(args.cacheRoot ? { cacheRoot: args.cacheRoot } : {}),
928
989
  packs: args.packs,
929
990
  ...(args.world ? { moduleWorld: args.world } : {}),
930
991
  ...(Object.keys(args.buildArgs).length > 0 ? { buildArgs: args.buildArgs } : {}),
@@ -974,7 +1035,7 @@ async function dev(cwd, args) {
974
1035
  // The app's image first: its build may run attached to the World and leave
975
1036
  // the data it read in the World's directory, which the World's image carries.
976
1037
  let image = await buildImageWith(cwd, args.name, args.settings, project, imageOptions(args, packages));
977
- const world = args.world ? await buildWorldImage(args.world, args.name) : undefined;
1038
+ const world = args.world ? await buildWorldImage(args.world, args.name, args.cacheRoot) : undefined;
978
1039
  const attach = (image) => {
979
1040
  if (!world)
980
1041
  return;
@@ -991,7 +1052,7 @@ async function dev(cwd, args) {
991
1052
  // is written beside its packages and carried by every image built from it,
992
1053
  // and a rebuilt image whose ranges did not change keeps it (ADR-0078).
993
1054
  const startup = { path: imageStartupPath(image.manifest.name), file: join(cwd, STARTUP_RECORD) };
994
- const server = await serve({ shell: SHELL_DIRECTORY, files, blobs: (pathname) => blobs.get(pathname), port: args.port, registry: world ? [packages, world.packagesDirectory] : packages, startup });
1055
+ const server = await serve({ shell: SHELL_DIRECTORY, manifest: image.manifest, files, blobs: (pathname) => blobs.get(pathname), port: args.port, registry: world ? [packages, world.packagesDirectory] : packages, startup });
995
1056
  const summary = `${image.manifest.setup.kind === "dockerfile" ? "Dockerfile" : image.manifest.setup.source}, ${image.manifest.archive.entries} entries, ${formatBytes(image.manifest.archive.fileBytes)}${world ? `; World ${world.world.name} from ${args.world}, ${world.world.archive.entries} entries` : ""}`;
996
1057
  const carried = image.manifest.registry
997
1058
  ? `\n ${PACKAGES_DIRECTORY}: this origin is the registry for ${[...image.manifest.registry.scopes, ...image.manifest.registry.names].join(", ")}`
@@ -1117,14 +1178,16 @@ async function build(cwd, args) {
1117
1178
  throw new Error(`--build-arg ${Object.keys(args.buildArgs).join(", ")} applies to a Dockerfile whose build half runs here (a RUN step); this one's setup is ${image.manifest.setup.kind}, built in the tab with its ARG defaults`);
1118
1179
  }
1119
1180
  if (args.world) {
1120
- const world = await buildWorldImage(args.world, args.name);
1181
+ const world = await buildWorldImage(args.world, args.name, args.cacheRoot);
1121
1182
  image.manifest.world = world.world;
1122
1183
  image.world = { archive: world.archive, blobs: world.blobs, ...(world.plan ? { plan: world.plan } : {}) };
1123
1184
  for (const [served, source] of world.tarballs)
1124
1185
  image.tarballs.set(served, source);
1125
1186
  }
1126
1187
  const out = resolve(cwd, args.out);
1188
+ preparationPhase("final copy");
1127
1189
  const written = writeBuild({ shell: SHELL_DIRECTORY, files: imageFiles(image), blobs: imageBlobs(image), out, manifest: image.manifest });
1190
+ preparationCheckpoint();
1128
1191
  process.stdout.write(`\n browser-substrate build\n\n ${out}\n ${written.files} files, ${formatBytes(written.bytes)}\n ${image.manifest.setup.playground
1129
1192
  ? `a playground: deploy on your page's origin and on ${image.manifest.settings.sandboxOrigin ?? "a sandbox. subdomain of it"}`
1130
1193
  : "one origin, static; _headers carries the isolation headers"}\n\n`);
@@ -1157,34 +1220,34 @@ async function dryRunCommand(cwd, args) {
1157
1220
  const index = originRegistryIndex(packages);
1158
1221
  if (index)
1159
1222
  image.manifest.registry = index;
1160
- server = await serve({ shell: SHELL_DIRECTORY, files: new Map(imageFiles(image)), blobs: ((blobs) => (pathname) => blobs.get(pathname))(imageBlobs(image)), port: 0, registry: packages });
1223
+ server = await serve({ shell: SHELL_DIRECTORY, manifest: image.manifest, files: new Map(imageFiles(image)), blobs: ((blobs) => (pathname) => blobs.get(pathname))(imageBlobs(image)), port: 0, registry: packages });
1161
1224
  url = server.url;
1162
1225
  }
1163
1226
  try {
1164
1227
  say(`\n browser-substrate dry-run, round ${round}\n`);
1165
1228
  const captureKey = captureKeyFor(file);
1166
- const warmBefore = warmSignature(warmDirectory(captureKey));
1167
- const cachesBefore = warmSignature(join(cachesDirectory(captureKey), "files"));
1229
+ const warmBefore = warmSignature(warmDirectory(captureKey, args.cacheRoot));
1230
+ const cachesBefore = warmSignature(join(cachesDirectory(captureKey, args.cacheRoot), "files"));
1168
1231
  const steps = args.steps ? JSON.parse(readFileSync(args.steps, "utf8")) : undefined;
1169
- const result = await dryRun(url, { settleMs: settle * 1000, onOutput: say, ...(steps ? { steps } : {}), preparedInto: preparedBodiesDirectory(captureKey), warmInto: warmDirectory(captureKey), cachesInto: cachesDirectory(captureKey) });
1232
+ const result = await dryRun(url, { settleMs: settle * 1000, onOutput: say, ...(steps ? { steps } : {}), preparedInto: preparedBodiesDirectory(captureKey, args.cacheRoot), warmInto: warmDirectory(captureKey, args.cacheRoot), cachesInto: cachesDirectory(captureKey, args.cacheRoot) });
1170
1233
  const captured = readCaptured(file);
1171
1234
  const { added: packsAdded, uncapturable } = capture(result.records, captured);
1172
1235
  // The bodies the engine prepared in the tab are the image's next time:
1173
1236
  // a first open compiles nothing the dry run saw it compile.
1174
- const bodiesAdded = capturePrepared(result.prepared, captured, preparedBodiesDirectory(captureKey));
1237
+ const bodiesAdded = capturePrepared(result.prepared, captured, preparedBodiesDirectory(captureKey, args.cacheRoot));
1175
1238
  // Caches captured for the first time are built into the next image. Later
1176
1239
  // captures replace them on disk for whatever build comes next, without a
1177
1240
  // round of their own: a cache may differ run to run and never settle.
1178
1241
  // And caches the image did not carry (never captured, or assembled from
1179
1242
  // packs an earlier round's capture changed): the next build carries them.
1180
- const warmChanged = result.warm > 0 && (!result.warmCarried || warmBefore === EMPTY_WARM || captured.cache !== captureKey) && warmSignature(warmDirectory(captureKey)) !== EMPTY_WARM ? 1 : 0;
1243
+ const warmChanged = result.warm > 0 && (!result.warmCarried || warmBefore === EMPTY_WARM || captured.cache !== captureKey) && warmSignature(warmDirectory(captureKey, args.cacheRoot)) !== EMPTY_WARM ? 1 : 0;
1181
1244
  if (result.warm > 0 || bodiesAdded > 0)
1182
1245
  captured.cache = captureKey;
1183
1246
  if (warmChanged)
1184
1247
  say(` captured the project's caches anew (${result.warm} files)\n`);
1185
1248
  // Cache Storage whose bodies are new to this machine is built into the
1186
1249
  // next image; the same bodies again (their headers carry a time) are not.
1187
- const cachesChanged = result.caches > 0 && warmSignature(join(cachesDirectory(captureKey), "files")) !== cachesBefore ? 1 : 0;
1250
+ const cachesChanged = result.caches > 0 && warmSignature(join(cachesDirectory(captureKey, args.cacheRoot), "files")) !== cachesBefore ? 1 : 0;
1188
1251
  if (cachesChanged)
1189
1252
  say(` captured the origin's Cache Storage anew (${result.caches} entries)\n`);
1190
1253
  if (result.caches > 0)