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 +1 -1
- package/src/backends/workerd/backend.ts +197 -30
- package/src/backends/workerd/prebuilt.ts +5 -0
- package/src/backends/workerd/process.ts +70 -12
- package/src/backends/workerd/runtime-overrides.ts +600 -0
- package/src/cli/build.ts +35 -1
- package/src/cli/package.ts +15 -0
- package/src/server.ts +116 -1
package/package.json
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
|
|
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:
|
|
1211
|
+
resolved: configuredEnvBindings,
|
|
1131
1212
|
missing: missingEnv,
|
|
1132
|
-
mode:
|
|
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 (
|
|
1142
|
-
if (
|
|
1226
|
+
if (hasConfiguredEnvPolicy && configuredEnvBindings.length > 0) {
|
|
1227
|
+
if (configuredEnvMode === "all") {
|
|
1143
1228
|
console.log(
|
|
1144
|
-
`[1tube] forwarding all ${
|
|
1145
|
-
`
|
|
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 ${
|
|
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 =
|
|
1204
|
-
r.bundlePath.
|
|
1205
|
-
|
|
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
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
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
|
-
"
|
|
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,
|
|
425
|
-
pumpLines(child.stderr,
|
|
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
|
-
|
|
461
|
-
|
|
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
|
}
|