1tube 0.1.14 → 0.1.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  * braces guarantee.
32
32
  */
33
33
 
34
- import { isAbsolute, join, resolve as resolvePath } from "node:path";
34
+ import { isAbsolute, join, relative, resolve as resolvePath } from "node:path";
35
35
  import { ensureDir } from "jsr:@std/fs@^1/ensure-dir";
36
36
  import { type AuthContext } from "../../registry.ts";
37
37
  import { type FunctionManifest, loadManifest } from "../../manifest.ts";
@@ -47,6 +47,14 @@ import {
47
47
  parsePrebuiltManifest,
48
48
  type PrebuiltManifest,
49
49
  } from "./prebuilt.ts";
50
+ import {
51
+ copyPrebuiltRuntimeFiles,
52
+ defaultRuntimeOverrideRoot,
53
+ RuntimeOverrideStore,
54
+ type EditableSourceResponse,
55
+ type EditableFunctionSummary,
56
+ type SaveEditableSourceInput,
57
+ } from "./runtime-overrides.ts";
50
58
  import { type CapnpRoute, generateCapnp } from "./capnp.ts";
51
59
  import {
52
60
  SHARED_RUNTIME_TOKEN_ENV,
@@ -76,6 +84,8 @@ export interface WorkerdBackendOptions {
76
84
  * `node_modules/` exists, else `.1tube-cache/workerd/`.
77
85
  */
78
86
  cacheDir?: string;
87
+ /** Writable runtime source/bundle overlay used by the admin edge editor. */
88
+ runtimeOverrideDir?: string;
79
89
  /** Loopback address for workerd sockets. Defaults to `"127.0.0.1"`. */
80
90
  bindAddress?: string;
81
91
  /** First port to assign to a function. Defaults to `8800`. */
@@ -84,6 +94,8 @@ export interface WorkerdBackendOptions {
84
94
  compatibilityDate?: string;
85
95
  /** Compatibility flags applied to every service. */
86
96
  compatibilityFlags?: readonly string[];
97
+ /** Pass `--experimental` to the workerd process. */
98
+ experimental?: boolean;
87
99
  /** Sourcemap mode passed to the bundler. */
88
100
  sourcemap?: boolean | "linked" | "inline";
89
101
  /** Sink for workerd's [workerd]-prefixed log lines. Defaults to stderr. */
@@ -135,8 +147,8 @@ export interface WorkerdBackendOptions {
135
147
  *
136
148
  * In this mode `functionsDir` is ignored, no bundler is created (so
137
149
  * the operator does not need esbuild's worker subprocess on the box),
138
- * and `reload()` is unsupported — the artifact is sealed. HMR must
139
- * not be enabled by the gateway when this is set.
150
+ * and runtime reloads compose the sealed artifact with admin
151
+ * overlays. HMR must not be enabled by the gateway when this is set.
140
152
  *
141
153
  * Bundle integrity is verified against the manifest's recorded
142
154
  * sha-256 at boot, so a tampered bundle fails fast with a clear
@@ -329,6 +341,12 @@ export interface WorkerdBackend {
329
341
  * exactly what the e2e tests assert on.
330
342
  */
331
343
  readonly lastReloadDurationMs: number | null;
344
+ listEditableFunctions(): Promise<EditableFunctionSummary[]>;
345
+ readEditableSource(name: string): Promise<EditableSourceResponse>;
346
+ saveEditableSource(name: string, input: SaveEditableSourceInput): Promise<WorkerdReloadResult>;
347
+ createEditableFunction(name: string): Promise<EditableSourceResponse>;
348
+ deleteEditableFunction(name: string): Promise<WorkerdReloadResult>;
349
+ revertEditableFunction(name: string): Promise<WorkerdReloadResult>;
332
350
  /** Tear down workerd and dispose esbuild's worker. Idempotent. */
333
351
  stop(): Promise<void>;
334
352
  }
@@ -639,6 +657,15 @@ export function resolveEnvAllowlist(
639
657
  return { resolved, missing, mode: "restricted" };
640
658
  }
641
659
 
660
+ function runtimeSecretNames(envSource: EnvSource = Deno.env): string[] {
661
+ const raw = envSource.get("1TUBE_SECRET_NAMES") ?? "";
662
+ const names = raw
663
+ .split(",")
664
+ .map((s) => s.trim())
665
+ .filter((s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s));
666
+ return [...new Set(names)].sort();
667
+ }
668
+
642
669
  /**
643
670
  * Compute the env vars exposed to a single function as workerd
644
671
  * `fromEnvironment` bindings.
@@ -815,6 +842,7 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
815
842
  // Mutable state set during start(). Kept in closure scope so dispatch()
816
843
  // can reach it without an extra indirection layer.
817
844
  let bundler: Bundler | null = null;
845
+ let overlayBundler: Bundler | null = null;
818
846
  let process: WorkerdProcess | null = null;
819
847
  let routesByName = new Map<string, CapnpRoute>();
820
848
  let workerdVersion: string | null = null;
@@ -858,8 +886,19 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
858
886
  ? opts.prebuiltDir
859
887
  : resolvePath(cwd, opts.prebuiltDir))
860
888
  : null;
889
+ const runtimeEditorEnabled = prebuiltDir !== null;
861
890
  let prebuiltManifest: PrebuiltManifest | null = null;
862
891
  let sharedRuntime: WorkerdSharedRuntime | null = null;
892
+ const runtimeOverrides = new RuntimeOverrideStore({
893
+ rootDir: opts.runtimeOverrideDir
894
+ ? (isAbsolute(opts.runtimeOverrideDir) ? opts.runtimeOverrideDir : resolvePath(cwd, opts.runtimeOverrideDir))
895
+ : defaultRuntimeOverrideRoot(cwd),
896
+ sourceFunctionsDir: resolvedFunctionsDir,
897
+ prebuiltDir,
898
+ getPrebuiltManifest: () => prebuiltManifest,
899
+ getLiveManifests: () => manifests,
900
+ getBundleBytes: () => bundleBytes,
901
+ });
863
902
 
864
903
  /**
865
904
  * Shared boot pipeline used by both `start()` and `reload()`.
@@ -907,7 +946,11 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
907
946
  // while live mode allocates the usual node_modules/.cache slot.
908
947
  if (cacheDir === null) {
909
948
  if (prebuiltDir !== null) {
910
- cacheDir = prebuiltDir;
949
+ cacheDir = opts.cacheDir
950
+ ? (isAbsolute(opts.cacheDir) ? opts.cacheDir : resolvePath(cwd, opts.cacheDir))
951
+ : join(await defaultCacheDir(cwd), "prebuilt-runtime");
952
+ await ensureDir(cacheDir);
953
+ await writeGitignore(cacheDir);
911
954
  } else {
912
955
  cacheDir = opts.cacheDir
913
956
  ? (isAbsolute(opts.cacheDir) ? opts.cacheDir : resolvePath(cwd, opts.cacheDir))
@@ -924,6 +967,7 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
924
967
  // to the caller for the WorkerdReloadResult; prebuilt mode never
925
968
  // rebuilds anything so this stays empty there.
926
969
  let rebundledNames: string[] = [];
970
+ const overlayManifests = new Map<string, FunctionManifest>();
927
971
  if (prebuiltDir !== null) {
928
972
  // Prebuilt mode — every "bundle" already exists on disk. We
929
973
  // synthesise BundleResults from the manifest so the rest of
@@ -936,7 +980,14 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
936
980
  // with a null manifest. Raising hard makes the bug obvious.
937
981
  throw new Error("prebuilt manifest not loaded");
938
982
  }
939
- let entries = prebuiltManifest.functions;
983
+ await copyPrebuiltRuntimeFiles(prebuiltManifest, prebuiltDir, cacheDir!);
984
+ const baseNames = new Set(prebuiltManifest.functions.map((e) => e.name));
985
+ const overlays = runtimeEditorEnabled
986
+ ? await runtimeOverrides.compose(baseNames)
987
+ : [];
988
+ for (const overlay of overlays) overlayManifests.set(overlay.name, overlay.manifest);
989
+ const patchedNames = new Set(overlays.filter((e) => e.origin === "patched").map((e) => e.name));
990
+ let entries = prebuiltManifest.functions.filter((e) => !patchedNames.has(e.name));
940
991
  if (opts.only && opts.only.length > 0) {
941
992
  const allow = new Set(opts.only);
942
993
  entries = entries.filter((e) => allow.has(e.name));
@@ -958,6 +1009,22 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
958
1009
  byteLength: e.bundleBytes,
959
1010
  durationMs: 0,
960
1011
  }));
1012
+ if (overlays.length > 0) {
1013
+ if (!overlayBundler) {
1014
+ overlayBundler = createBundler({
1015
+ outDir: join(cacheDir!, "runtime-overrides"),
1016
+ configPath,
1017
+ sourcemap: "linked",
1018
+ minify: true,
1019
+ });
1020
+ }
1021
+ const overlayResults = await overlayBundler.bundleAll(
1022
+ overlays.map((entry) => ({ name: entry.name, entrypoint: entry.entrypoint })),
1023
+ { concurrency: opts.bundleConcurrency ?? 4 },
1024
+ );
1025
+ bundleResults.push(...overlayResults);
1026
+ rebundledNames = overlayResults.map((r) => r.name);
1027
+ }
961
1028
  for (const shared of prebuiltManifest.sharedModules) {
962
1029
  sharedModules.push({
963
1030
  id: shared.id,
@@ -976,6 +1043,18 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
976
1043
  );
977
1044
  }
978
1045
  }
1046
+ const baseNames = new Set(inputs.map((i) => i.name));
1047
+ const overlays = runtimeEditorEnabled
1048
+ ? await runtimeOverrides.compose(baseNames)
1049
+ : [];
1050
+ for (const overlay of overlays) overlayManifests.set(overlay.name, overlay.manifest);
1051
+ if (overlays.length > 0) {
1052
+ const overlayNames = new Set(overlays.map((entry) => entry.name));
1053
+ inputs = [
1054
+ ...inputs.filter((input) => !overlayNames.has(input.name)),
1055
+ ...overlays.map((entry) => ({ name: entry.name, entrypoint: entry.entrypoint })),
1056
+ ].sort((a, b) => a.name.localeCompare(b.name));
1057
+ }
979
1058
  if (inputs.length === 0) {
980
1059
  // Empty functions dir is a legitimate state — typically a
981
1060
  // fresh deployment that hasn't received its first firmware
@@ -1121,16 +1200,22 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1121
1200
  }
1122
1201
  }
1123
1202
 
1124
- // Resolve the gateway-wide env allowlist. This is the *maximum*
1125
- // surface any single function can ask for; per-function manifests
1126
- // narrow it further via `permissions.env`. Most-restrictive wins
1127
- // — operator declares what's available, the function declares
1128
- // what it actually uses, and we hand workerd the intersection.
1203
+ // Resolve the gateway-wide env policy. Workerd gets live secrets
1204
+ // plus the operator's explicit allowlist. We deliberately do not
1205
+ // infer source usage here: dynamic access is common, and secrets
1206
+ // are runtime configuration.
1207
+ const hasConfiguredEnvPolicy = (opts.envAllowlist?.length ?? 0) > 0 ||
1208
+ ((Deno.env.get("1TUBE_WORKERD_ENV") ?? "").trim().length > 0);
1209
+ const secretNames = runtimeSecretNames();
1129
1210
  const {
1130
- resolved: envBindings,
1211
+ resolved: configuredEnvBindings,
1131
1212
  missing: missingEnv,
1132
- mode: envMode,
1213
+ mode: configuredEnvMode,
1133
1214
  } = resolveEnvAllowlist(opts.envAllowlist);
1215
+ const gatewayEnvBindings = [...new Set([
1216
+ ...secretNames,
1217
+ ...(hasConfiguredEnvPolicy ? configuredEnvBindings : []),
1218
+ ])].sort();
1134
1219
  if (missingEnv.length > 0) {
1135
1220
  console.warn(
1136
1221
  `[1tube] workerd env allowlist references vars not present in the gateway env: ` +
@@ -1138,17 +1223,20 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1138
1223
  `Set them before starting the gateway, or remove them from the allowlist.`,
1139
1224
  );
1140
1225
  }
1141
- if (envBindings.length > 0) {
1142
- if (envMode === "all") {
1226
+ if (hasConfiguredEnvPolicy && configuredEnvBindings.length > 0) {
1227
+ if (configuredEnvMode === "all") {
1143
1228
  console.log(
1144
- `[1tube] forwarding all ${envBindings.length} env var(s) to workerd ` +
1145
- `(default; pass --workerd-env=A,B,C or set 1TUBE_WORKERD_ENV to restrict)`,
1229
+ `[1tube] forwarding all ${configuredEnvBindings.length} env var(s) to workerd ` +
1230
+ `plus ${secretNames.length} runtime secret name(s) (explicit '*')`,
1146
1231
  );
1147
1232
  } else {
1148
1233
  console.log(
1149
- `[1tube] forwarding ${envBindings.length} env var(s) to workerd: ${envBindings.join(", ")}`,
1234
+ `[1tube] forwarding ${gatewayEnvBindings.length} env var(s) to workerd ` +
1235
+ `(${secretNames.length} secret(s), ${configuredEnvBindings.length} explicit): ${gatewayEnvBindings.join(", ")}`,
1150
1236
  );
1151
1237
  }
1238
+ } else if (secretNames.length > 0) {
1239
+ console.log(`[1tube] forwarding ${secretNames.length} runtime secret env var(s) to workerd: ${secretNames.join(", ")}`);
1152
1240
  }
1153
1241
 
1154
1242
  // Load each function's 1tube.json. Missing files yield
@@ -1165,16 +1253,19 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1165
1253
  const newManifests = new Map<string, FunctionManifest>();
1166
1254
  if (prebuiltDir !== null && prebuiltManifest) {
1167
1255
  for (const e of prebuiltManifest.functions) {
1168
- newManifests.set(e.name, e.manifest);
1256
+ if (!overlayManifests.has(e.name)) newManifests.set(e.name, e.manifest);
1169
1257
  }
1170
1258
  } else {
1171
1259
  await Promise.all(
1172
1260
  bundleResults.map(async (r) => {
1173
- const m = await loadManifest(resolvedFunctionsDir, r.name);
1261
+ const m = overlayManifests.get(r.name) ?? await loadManifest(resolvedFunctionsDir, r.name);
1174
1262
  newManifests.set(r.name, m);
1175
1263
  }),
1176
1264
  );
1177
1265
  }
1266
+ for (const [name, manifest] of overlayManifests) {
1267
+ newManifests.set(name, manifest);
1268
+ }
1178
1269
 
1179
1270
  // Mirror the Deno backend's two-mode env story:
1180
1271
  // - Default: every function sees the full gateway allowlist.
@@ -1200,9 +1291,11 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1200
1291
  : [];
1201
1292
  const capnpInputs = bundleResults.map((r) => {
1202
1293
  const m = newManifests.get(r.name)!;
1203
- const bundleEmbedPath = prebuiltBundleFiles?.get(r.name) ??
1204
- r.bundlePath.split(/[\\/]/).pop()!;
1205
- const fnEnvBindings = intersectEnvForFunction(envBindings, m.permissions.env, enforceManifest);
1294
+ const bundleEmbedPath = overlayManifests.has(r.name)
1295
+ ? relative(cacheDir!, r.bundlePath).replaceAll("\\", "/")
1296
+ : prebuiltBundleFiles?.get(r.name) ??
1297
+ relative(cacheDir!, r.bundlePath).replaceAll("\\", "/");
1298
+ const fnEnvBindings = intersectEnvForFunction(gatewayEnvBindings, m.permissions.env, enforceManifest);
1206
1299
  for (const internalName of internalEnvBindings) {
1207
1300
  if (!fnEnvBindings.includes(internalName)) fnEnvBindings.push(internalName);
1208
1301
  }
@@ -1213,7 +1306,7 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1213
1306
  // and capnp is written at dist/, so use the manifest-relative
1214
1307
  // path ("functions/<fn>.js") or workerd cannot resolve embeds.
1215
1308
  bundleBasename: bundleEmbedPath,
1216
- moduleFiles: prebuiltModuleFiles?.get(r.name),
1309
+ moduleFiles: overlayManifests.has(r.name) ? undefined : prebuiltModuleFiles?.get(r.name),
1217
1310
  envBindings: fnEnvBindings,
1218
1311
  };
1219
1312
  });
@@ -1241,6 +1334,21 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1241
1334
 
1242
1335
  const newRoutesByName = new Map(capnp.routes.map((r) => [r.name, r]));
1243
1336
  const newNames = capnp.routes.map((r) => r.name).sort();
1337
+ if (args.gen === 0) {
1338
+ const ports = capnp.routes.map((r) => r.port);
1339
+ const minPort = Math.min(...ports);
1340
+ const maxPort = Math.max(...ports);
1341
+ const totalEnvBindings = capnpInputs.reduce((sum, input) => sum + (input.envBindings?.length ?? 0), 0);
1342
+ const maxEnvBindings = capnpInputs.reduce((max, input) => Math.max(max, input.envBindings?.length ?? 0), 0);
1343
+ console.log(
1344
+ `[1tube] workerd config: path=${capnpPath} services=${capnp.routes.length} ` +
1345
+ `ports=${minPort}${minPort === maxPort ? "" : `-${maxPort}`} ` +
1346
+ `compat=${effectiveCompatDate ?? "default"} ` +
1347
+ `flags=${(opts.compatibilityFlags ?? ["default"]).join(",")} ` +
1348
+ `envMode=${hasConfiguredEnvPolicy ? configuredEnvMode : "secrets"} ` +
1349
+ `envBindingsTotal=${totalEnvBindings} envBindingsMaxPerFn=${maxEnvBindings}`,
1350
+ );
1351
+ }
1244
1352
 
1245
1353
  // Preflight: refuse to spawn workerd against ports that are
1246
1354
  // already in use. Without this, a leftover workerd from a
@@ -1341,11 +1449,20 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1341
1449
 
1342
1450
  const maxHeapArgs = buildMaxHeapExtraArgs(opts.maxHeapMB);
1343
1451
  const extraArgs = [...inspectorArgs, ...maxHeapArgs];
1452
+ const verboseWorkerd = (Deno.env.get("1TUBE_WORKERD_VERBOSE") ?? "") === "1";
1453
+ if (verboseWorkerd) {
1454
+ console.log(`[1tube] workerd verbose logging enabled (1TUBE_WORKERD_VERBOSE=1)`);
1455
+ }
1456
+ const globalArgs = [
1457
+ ...(verboseWorkerd ? ["-v"] : []),
1458
+ ...(opts.experimental ? ["--experimental"] : []),
1459
+ ];
1344
1460
 
1345
1461
  const newProcess = createWorkerdProcess({
1346
1462
  binary: workerdBin,
1347
1463
  capnpPath,
1348
1464
  routes: capnp.routes,
1465
+ ...(globalArgs.length > 0 ? { globalArgs } : {}),
1349
1466
  extraArgs,
1350
1467
  env: sharedRuntime
1351
1468
  ? {
@@ -1560,6 +1677,15 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1560
1677
  return reloadInFlight;
1561
1678
  }
1562
1679
 
1680
+ function requireRuntimeEditor(): void {
1681
+ if (!runtimeEditorEnabled) {
1682
+ throw new Error(
1683
+ "edge function runtime editor is disabled outside prebuilt workerd mode; " +
1684
+ "use HMR/source files for local development",
1685
+ );
1686
+ }
1687
+ }
1688
+
1563
1689
  return {
1564
1690
  get pid() {
1565
1691
  return process?.pid ?? null;
@@ -1586,6 +1712,7 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1586
1712
  async start() {
1587
1713
  if (started) throw new Error("workerd backend already started");
1588
1714
  started = true;
1715
+ if (runtimeEditorEnabled) await runtimeOverrides.load();
1589
1716
 
1590
1717
  // Load + verify the prebuilt manifest BEFORE probing workerd, so
1591
1718
  // a stale / tampered artifact fails with a clear error before we
@@ -1745,16 +1872,14 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1745
1872
  },
1746
1873
 
1747
1874
  reload(changed) {
1748
- // Prebuilt artifacts are sealed — block every public reload
1749
- // request. Crash-recovery still works because the internal
1750
- // crash handler calls `doReload` directly (re-spawning workerd
1751
- // against the same on-disk bundles is legitimate; only
1752
- // re-bundling is forbidden).
1875
+ // Keep the public API contract: prebuilt firmware is immutable
1876
+ // from HMR/test callers. Runtime editor admin methods below use
1877
+ // doReload() directly after mutating the overlay store.
1753
1878
  if (prebuiltDir !== null) {
1754
1879
  return Promise.reject(
1755
1880
  new Error(
1756
- "workerd backend is in prebuilt mode; reload is not supported. " +
1757
- "Re-run `1tube build` and restart the gateway to pick up changes.",
1881
+ "workerd backend is in prebuilt mode; public reload is not supported. " +
1882
+ "Use the runtime editor APIs for overlay hot-swaps, or promote new firmware.",
1758
1883
  ),
1759
1884
  );
1760
1885
  }
@@ -1802,6 +1927,44 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1802
1927
  }
1803
1928
  },
1804
1929
 
1930
+ async listEditableFunctions() {
1931
+ requireRuntimeEditor();
1932
+ return await runtimeOverrides.list(new Set(names));
1933
+ },
1934
+
1935
+ async readEditableSource(name) {
1936
+ requireRuntimeEditor();
1937
+ return await runtimeOverrides.read(name, new Set(names));
1938
+ },
1939
+
1940
+ async createEditableFunction(name) {
1941
+ requireRuntimeEditor();
1942
+ if (names.includes(name.trim())) {
1943
+ throw new Error(`function "${name}" exists in firmware; open Code and save a patch instead`);
1944
+ }
1945
+ const source = await runtimeOverrides.create(name);
1946
+ await doReload(new Set([name]));
1947
+ return source;
1948
+ },
1949
+
1950
+ async saveEditableSource(name, input) {
1951
+ requireRuntimeEditor();
1952
+ await runtimeOverrides.save(name, input, new Set(names));
1953
+ return await doReload(new Set([name]));
1954
+ },
1955
+
1956
+ async deleteEditableFunction(name) {
1957
+ requireRuntimeEditor();
1958
+ await runtimeOverrides.deleteAdded(name);
1959
+ return await doReload("all");
1960
+ },
1961
+
1962
+ async revertEditableFunction(name) {
1963
+ requireRuntimeEditor();
1964
+ await runtimeOverrides.revert(name);
1965
+ return await doReload(new Set([name]));
1966
+ },
1967
+
1805
1968
  async stop() {
1806
1969
  if (stopping) return;
1807
1970
  stopping = true;
@@ -1814,8 +1977,12 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1814
1977
  if (bundler) {
1815
1978
  await bundler.dispose().catch(() => {});
1816
1979
  }
1980
+ if (overlayBundler && overlayBundler !== bundler) {
1981
+ await overlayBundler.dispose().catch(() => {});
1982
+ }
1817
1983
  sharedRuntime = null;
1818
1984
  bundler = null;
1985
+ overlayBundler = null;
1819
1986
  process = null;
1820
1987
  manifests.clear();
1821
1988
  }
@@ -39,6 +39,8 @@ export interface PrebuiltFunctionEntry {
39
39
  moduleFiles: string[];
40
40
  /** Per-function `1tube.json` parsed at build time. */
41
41
  manifest: FunctionManifest;
42
+ /** Optional inspectable authored source files under `sources/<name>/`. */
43
+ sourceFiles?: string[];
42
44
  }
43
45
 
44
46
  export interface PrebuiltChunkEntry {
@@ -128,6 +130,9 @@ export function parsePrebuiltManifest(raw: unknown): PrebuiltManifest {
128
130
  bundleSha256,
129
131
  moduleFiles: moduleFiles.length > 0 ? moduleFiles : [bundleFile],
130
132
  manifest: parseManifest(e.manifest, /* fromFile */ true),
133
+ sourceFiles: Array.isArray(e.sourceFiles)
134
+ ? e.sourceFiles.filter((n): n is string => typeof n === "string")
135
+ : undefined,
131
136
  };
132
137
  });
133
138
  const chunks: PrebuiltChunkEntry[] = Array.isArray(obj.chunks)
@@ -44,6 +44,8 @@ const DEFAULT_READY_TIMEOUT_MS = 15_000;
44
44
  const DEFAULT_PROBE_INTERVAL_MS = 50;
45
45
  /** Default grace period before escalating from SIGTERM to SIGKILL. */
46
46
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
47
+ /** Bounded child-output tail attached to startup failures. */
48
+ const OUTPUT_TAIL_LINES = 800;
47
49
 
48
50
  export interface WorkerdProcessOptions {
49
51
  /** Absolute or PATH-resolvable path to the `workerd` binary. */
@@ -362,6 +364,46 @@ function bestEffortKill(child: Deno.ChildProcess, signal: Deno.Signal): boolean
362
364
  }
363
365
  }
364
366
 
367
+ function quoteArg(arg: string): string {
368
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : JSON.stringify(arg);
369
+ }
370
+
371
+ function summarizeRoutes(routes: readonly CapnpRoute[]): string {
372
+ if (routes.length === 0) return "routes=0";
373
+ const ports = routes.map((r) => r.port);
374
+ const min = Math.min(...ports);
375
+ const max = Math.max(...ports);
376
+ const hosts = [...new Set(routes.map((r) => r.address))].join(",");
377
+ return `routes=${routes.length} hosts=${hosts} ports=${min}${min === max ? "" : `-${max}`}`;
378
+ }
379
+
380
+ function formatStartupFailure(opts: {
381
+ command: readonly string[];
382
+ cwd: string;
383
+ capnpPath: string;
384
+ routes: readonly CapnpRoute[];
385
+ exitCode: number | null | undefined;
386
+ outputTail: readonly string[];
387
+ cause: unknown;
388
+ }): string {
389
+ const lines = [
390
+ "workerd exited before sockets became ready",
391
+ ` exitCode=${opts.exitCode ?? "<unknown>"}`,
392
+ ` command=${opts.command.map(quoteArg).join(" ")}`,
393
+ ` cwd=${opts.cwd}`,
394
+ ` capnp=${opts.capnpPath}`,
395
+ ` ${summarizeRoutes(opts.routes)}`,
396
+ ];
397
+ if (opts.cause instanceof Error) {
398
+ lines.push(` readiness=${opts.cause.message}`);
399
+ }
400
+ if (opts.outputTail.length > 0) {
401
+ lines.push(` -- last ${opts.outputTail.length} workerd output line(s) --`);
402
+ for (const line of opts.outputTail) lines.push(` | ${line}`);
403
+ }
404
+ return lines.join("\n");
405
+ }
406
+
365
407
  /** Build the manager. Stateful — caller owns lifecycle. */
366
408
  export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProcess {
367
409
  const sink = opts.logLineSink ?? defaultLogLineSink;
@@ -402,14 +444,23 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
402
444
  // a transitive node-path dependency on this module; the inline
403
445
  // separator-aware split is enough for our purposes.
404
446
  const cwd = opts.cwd ?? opts.capnpPath.replace(/[\\/][^\\/]+$/, "");
447
+ const args = [
448
+ ...(opts.globalArgs ?? []),
449
+ "serve",
450
+ opts.capnpPath,
451
+ ...(opts.extraArgs ?? []),
452
+ ];
453
+ const command = [opts.binary, ...args];
454
+ const outputTail: string[] = [];
455
+ let exitCode: number | null | undefined;
456
+ const captureLine = (line: string) => {
457
+ outputTail.push(line);
458
+ while (outputTail.length > OUTPUT_TAIL_LINES) outputTail.shift();
459
+ sink(line);
460
+ };
405
461
 
406
462
  const cmd = new Deno.Command(opts.binary, {
407
- args: [
408
- ...(opts.globalArgs ?? []),
409
- "serve",
410
- opts.capnpPath,
411
- ...(opts.extraArgs ?? []),
412
- ],
463
+ args,
413
464
  cwd,
414
465
  env: opts.env,
415
466
  stdout: "piped",
@@ -421,8 +472,8 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
421
472
  // Pump stdout + stderr into the sink. Both pumps are awaited at
422
473
  // shutdown so the consumer can be sure no lines are lost.
423
474
  pumpsDone = Promise.all([
424
- pumpLines(child.stdout, sink),
425
- pumpLines(child.stderr, sink),
475
+ pumpLines(child.stdout, captureLine),
476
+ pumpLines(child.stderr, captureLine),
426
477
  ]).then(() => {});
427
478
 
428
479
  // Watch for early exit so a workerd that crashes during boot
@@ -430,6 +481,7 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
430
481
  // the readiness wait instead of hanging until the timeout.
431
482
  const exitAbort = new AbortController();
432
483
  const exitWatch = child.status.then((status) => {
484
+ exitCode = status.code;
433
485
  const expected = stopRequested;
434
486
  if (!expected) {
435
487
  // Crash before ready or during runtime. Abort the readiness
@@ -455,11 +507,17 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
455
507
  // (extremely rare with workerd, but defence in depth).
456
508
  if (child) bestEffortKill(child, "SIGKILL");
457
509
  await exitWatch.catch(() => {}); // swallow — already in failure path
510
+ if (pumpsDone) await pumpsDone.catch(() => {});
458
511
  if (exitAbort.signal.aborted) {
459
- throw new Error(
460
- `workerd exited before sockets became ready` +
461
- (err instanceof Error ? `: ${err.message}` : ""),
462
- );
512
+ throw new Error(formatStartupFailure({
513
+ command,
514
+ cwd,
515
+ capnpPath: opts.capnpPath,
516
+ routes: opts.routes,
517
+ exitCode,
518
+ outputTail,
519
+ cause: err,
520
+ }));
463
521
  }
464
522
  throw err;
465
523
  }