1tube 0.1.40 → 0.1.41
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/vercel/capture-config.ts +122 -0
- package/src/cli/vercel-build.ts +185 -10
package/package.json
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time capture of each function's declared `serve()` options for the
|
|
3
|
+
* Vercel target.
|
|
4
|
+
*
|
|
5
|
+
* On Vercel each function runs standalone, so `_shared/handler.ts` takes the
|
|
6
|
+
* `getStandaloneServe()` path and the gateway registry is never installed —
|
|
7
|
+
* which means a `serve({ timeoutMs })` value would otherwise be invisible to the
|
|
8
|
+
* static build that bakes `maxDuration` into `.vc-config.json`.
|
|
9
|
+
*
|
|
10
|
+
* Rather than parse source or require a side-car `1tube.json`, we read the value
|
|
11
|
+
* the function ACTUALLY declares, through the same contract the runtime uses:
|
|
12
|
+
* `_shared/handler.ts` calls `globalThis.__edgeFunctionRegistry.register(handler,
|
|
13
|
+
* { public, timeoutMs })` at import time whenever that global exists. We install
|
|
14
|
+
* a capturing stub, import each entrypoint once, and record what it registered.
|
|
15
|
+
*
|
|
16
|
+
* Cost & safety:
|
|
17
|
+
* - Runs ONLY at build time — the deployed function pays nothing.
|
|
18
|
+
* - Imports each entrypoint a single time; Deno caches the shared module graph
|
|
19
|
+
* (`handler.ts`, providers, …) across functions, so the marginal cost per
|
|
20
|
+
* function is just its own top-level `serve(...)` call.
|
|
21
|
+
* - The registry path returns before binding any server, so importing does not
|
|
22
|
+
* start listeners or do per-request work.
|
|
23
|
+
* - Resilient: a function that throws at import (e.g. a stray top-level env
|
|
24
|
+
* read) is skipped with a warning; the caller falls back to its manifest /
|
|
25
|
+
* the global default.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
import type { BundleInput } from "../../bundler/core.ts";
|
|
30
|
+
|
|
31
|
+
/** What a function declared on its `serve({ ... })` call. */
|
|
32
|
+
export interface DeclaredServeConfig {
|
|
33
|
+
public: boolean;
|
|
34
|
+
/** Per-function wall-clock budget (ms). Undefined = no explicit timeout. */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RegistryStub {
|
|
39
|
+
register(
|
|
40
|
+
handler: unknown,
|
|
41
|
+
opts?: { public?: boolean; timeoutMs?: number },
|
|
42
|
+
): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const REGISTRY_KEY = "__edgeFunctionRegistry";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Minimal stand-in for `Deno.serve`'s return value so importing an entrypoint
|
|
49
|
+
* that calls `Deno.serve` directly (instead of the registry `serve()` wrapper)
|
|
50
|
+
* does not bind a real port during the build.
|
|
51
|
+
*/
|
|
52
|
+
function fakeHttpServer(): unknown {
|
|
53
|
+
return {
|
|
54
|
+
finished: Promise.resolve(),
|
|
55
|
+
shutdown: () => Promise.resolve(),
|
|
56
|
+
ref() {},
|
|
57
|
+
unref() {},
|
|
58
|
+
addr: { transport: "tcp", hostname: "0.0.0.0", port: 0 },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Import every entrypoint with a stub registry installed and collect the
|
|
64
|
+
* `serve()` options each one registers. Returns a `name → config` map; functions
|
|
65
|
+
* that don't register (or fail to import) are simply absent.
|
|
66
|
+
*/
|
|
67
|
+
export async function captureDeclaredServeConfigs(
|
|
68
|
+
inputs: readonly BundleInput[],
|
|
69
|
+
onWarn: (message: string) => void = (m) => console.warn(m),
|
|
70
|
+
): Promise<Map<string, DeclaredServeConfig>> {
|
|
71
|
+
const out = new Map<string, DeclaredServeConfig>();
|
|
72
|
+
if (inputs.length === 0) return out;
|
|
73
|
+
|
|
74
|
+
const globalAny = globalThis as Record<string, unknown>;
|
|
75
|
+
const previousRegistry = globalAny[REGISTRY_KEY];
|
|
76
|
+
|
|
77
|
+
let current: DeclaredServeConfig | undefined;
|
|
78
|
+
const stub: RegistryStub = {
|
|
79
|
+
register(_handler, opts) {
|
|
80
|
+
current = { public: !!opts?.public, timeoutMs: opts?.timeoutMs };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
globalAny[REGISTRY_KEY] = stub;
|
|
84
|
+
|
|
85
|
+
// The registry path never calls Deno.serve, but neutralise it anyway so a raw
|
|
86
|
+
// `Deno.serve(...)` entrypoint can't bind a port (or leak a listener) during
|
|
87
|
+
// the sweep. Snapshot/restore around the whole pass.
|
|
88
|
+
const denoNs = (globalThis as { Deno?: { serve?: unknown } }).Deno;
|
|
89
|
+
const hadServe = !!denoNs && "serve" in denoNs;
|
|
90
|
+
const previousServe = denoNs?.serve;
|
|
91
|
+
if (denoNs) denoNs.serve = fakeHttpServer as unknown as typeof denoNs.serve;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
for (const input of inputs) {
|
|
95
|
+
current = undefined;
|
|
96
|
+
try {
|
|
97
|
+
await import(pathToFileURL(input.entrypoint).href);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
onWarn(
|
|
100
|
+
`could not load ${input.name} to read its serve() timeout; ` +
|
|
101
|
+
`falling back to manifest/default (${
|
|
102
|
+
err instanceof Error ? err.message : String(err)
|
|
103
|
+
})`,
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (current) out.set(input.name, current);
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
if (previousRegistry === undefined) delete globalAny[REGISTRY_KEY];
|
|
111
|
+
else globalAny[REGISTRY_KEY] = previousRegistry;
|
|
112
|
+
if (denoNs) {
|
|
113
|
+
if (hadServe) {
|
|
114
|
+
denoNs.serve = previousServe as typeof denoNs.serve;
|
|
115
|
+
} else {
|
|
116
|
+
delete denoNs.serve;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return out;
|
|
122
|
+
}
|
package/src/cli/vercel-build.ts
CHANGED
|
@@ -20,10 +20,19 @@
|
|
|
20
20
|
* required: the Vercel bundle wrapper captures `Deno.serve` and bridges Node
|
|
21
21
|
* req/res to the Web fetch handler (see `../backends/vercel/wrapper.ts`).
|
|
22
22
|
*
|
|
23
|
+
* Vercel's filesystem routing matches a `.func` only on its EXACT path, so
|
|
24
|
+
* `/functions/v1/rooms/<id>` would 404. Supabase's edge-runtime instead routes
|
|
25
|
+
* on the first path segment and forwards the whole request to the function, so
|
|
26
|
+
* path-based routers handle their own sub-paths. To reproduce that, this command
|
|
27
|
+
* also merges a per-function "main"-phase rewrite into `config.json` mapping
|
|
28
|
+
* `/<prefix>/<name>/<rest>` back to the bare function path (see
|
|
29
|
+
* {@link mergeSubpathRoutes}).
|
|
30
|
+
*
|
|
23
31
|
* This command MERGES into an existing `.vercel/output` (e.g. one produced by
|
|
24
32
|
* `vercel build` for the static frontend). It only creates/replaces the
|
|
25
|
-
* specific `.func` directories
|
|
26
|
-
* frontend's
|
|
33
|
+
* specific `.func` directories and additively edits `config.json`'s `routes`
|
|
34
|
+
* (preserving the frontend's existing routes and static assets); it never wipes
|
|
35
|
+
* the output root.
|
|
27
36
|
*
|
|
28
37
|
* Each function is fully standalone — there is no gateway shared runtime on
|
|
29
38
|
* Vercel, so modules like `_shared/profile-cache.ts` are bundled inline rather
|
|
@@ -40,6 +49,7 @@ import {
|
|
|
40
49
|
disposeBundlerResources,
|
|
41
50
|
} from "../bundler/core.ts";
|
|
42
51
|
import { VERCEL_PROFILE } from "../backends/vercel/bundle-profile.ts";
|
|
52
|
+
import { captureDeclaredServeConfigs } from "../backends/vercel/capture-config.ts";
|
|
43
53
|
import { loadManifest } from "../manifest.ts";
|
|
44
54
|
import { resolveDenoConfigPath } from "./deno-config.ts";
|
|
45
55
|
|
|
@@ -48,9 +58,10 @@ const DEFAULT_RUNTIME = "nodejs24.x";
|
|
|
48
58
|
/** Default route prefix; matches sciobot's `/functions/v1/<name>` calls. */
|
|
49
59
|
const DEFAULT_PATH_PREFIX = "functions/v1";
|
|
50
60
|
/**
|
|
51
|
-
* Default function timeout (seconds) when
|
|
52
|
-
*
|
|
53
|
-
*
|
|
61
|
+
* Default function timeout (seconds) when a function declares no timeout via
|
|
62
|
+
* `serve({ timeoutMs })` (or a `1tube.json` manifest). Defaults to the cap so
|
|
63
|
+
* long-running AI pipelines aren't silently truncated unless a function opts
|
|
64
|
+
* into a shorter timeout.
|
|
54
65
|
*/
|
|
55
66
|
const DEFAULT_MAX_DURATION = 800;
|
|
56
67
|
/** Upper bound for maxDuration (seconds). Fluid/Pro allows long durations. */
|
|
@@ -74,7 +85,10 @@ export interface VercelBuildOptions {
|
|
|
74
85
|
pathPrefix?: string;
|
|
75
86
|
/** Vercel runtime identifier. Defaults to "nodejs24.x". */
|
|
76
87
|
runtime?: string;
|
|
77
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Fallback maxDuration (seconds) when a function declares no `timeoutMs`
|
|
90
|
+
* (via `serve({ timeoutMs })` or `1tube.json`).
|
|
91
|
+
*/
|
|
78
92
|
defaultMaxDuration?: number;
|
|
79
93
|
/** Hard cap (seconds) applied to any derived maxDuration. */
|
|
80
94
|
maxDurationCap?: number;
|
|
@@ -96,7 +110,14 @@ export type VercelBuildProgress =
|
|
|
96
110
|
route: string;
|
|
97
111
|
bytes: number;
|
|
98
112
|
maxDuration: number;
|
|
99
|
-
}
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
phase: "config-merge";
|
|
116
|
+
configPath: string;
|
|
117
|
+
routes: number;
|
|
118
|
+
created: boolean;
|
|
119
|
+
}
|
|
120
|
+
| { phase: "capture-warning"; message: string };
|
|
100
121
|
|
|
101
122
|
export interface VercelFunctionEntry {
|
|
102
123
|
/** Logical function name. */
|
|
@@ -126,6 +147,10 @@ export interface VercelBuildResult {
|
|
|
126
147
|
pathPrefix: string;
|
|
127
148
|
/** Emitted functions, sorted by name. */
|
|
128
149
|
functions: VercelFunctionEntry[];
|
|
150
|
+
/** Absolute path to the Build Output API `config.json` that was merged. */
|
|
151
|
+
configPath: string;
|
|
152
|
+
/** Number of per-function sub-path routes written into `config.json`. */
|
|
153
|
+
subpathRoutes: number;
|
|
129
154
|
durationMs: number;
|
|
130
155
|
}
|
|
131
156
|
|
|
@@ -170,6 +195,116 @@ function normalizePrefix(prefix: string): string {
|
|
|
170
195
|
);
|
|
171
196
|
}
|
|
172
197
|
|
|
198
|
+
/** Escape a string for safe literal use inside a regular expression. */
|
|
199
|
+
function escapeRegExp(input: string): string {
|
|
200
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Minimal shape of a Build Output API route entry we read or write. */
|
|
204
|
+
interface VercelRoute {
|
|
205
|
+
handle?: string;
|
|
206
|
+
src?: string;
|
|
207
|
+
dest?: string;
|
|
208
|
+
[key: string]: unknown;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Minimal shape of `.vercel/output/config.json` (Build Output API v3). */
|
|
212
|
+
interface VercelOutputConfig {
|
|
213
|
+
version?: number;
|
|
214
|
+
routes?: VercelRoute[];
|
|
215
|
+
[key: string]: unknown;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Add, per function, a "main"-phase rewrite mapping `/<prefix>/<name>/<rest>`
|
|
220
|
+
* to the bare function path `/<prefix>/<name>` so a single Vercel Node Function
|
|
221
|
+
* also serves all of its sub-paths.
|
|
222
|
+
*
|
|
223
|
+
* Why this is needed: Vercel's filesystem routing matches a `.func` only on its
|
|
224
|
+
* exact path, so `/<prefix>/rooms/<id>` would 404. Supabase's edge-runtime
|
|
225
|
+
* instead routes on the first path segment and forwards the whole request to the
|
|
226
|
+
* function (`worker.fetch(req)`) — the behaviour path-based routers such as
|
|
227
|
+
* sciobot's `rooms` handler rely on. A *regular* serverless function receives
|
|
228
|
+
* the UNMODIFIED original URL through a rewrite (only prerender functions get
|
|
229
|
+
* the rewritten path), so the function still sees the sub-path in
|
|
230
|
+
* `url.pathname`.
|
|
231
|
+
*
|
|
232
|
+
* The routes go in the main phase (before the first `handle` marker) so each
|
|
233
|
+
* rewrite is immediately followed by the filesystem lookup that resolves the
|
|
234
|
+
* `.func`. The exact path (`/<prefix>/<name>`) is intentionally NOT matched — it
|
|
235
|
+
* already resolves via filesystem routing — keeping the change purely additive.
|
|
236
|
+
*
|
|
237
|
+
* Existing routes are preserved; only this builder's own previously-written
|
|
238
|
+
* sub-path routes (for the functions being built now) are replaced, so repeated
|
|
239
|
+
* and `--only` builds stay idempotent.
|
|
240
|
+
*/
|
|
241
|
+
async function mergeSubpathRoutes(
|
|
242
|
+
outDir: string,
|
|
243
|
+
pathPrefix: string,
|
|
244
|
+
functionNames: readonly string[],
|
|
245
|
+
): Promise<{ created: boolean; routeCount: number }> {
|
|
246
|
+
const configPath = join(outDir, "config.json");
|
|
247
|
+
|
|
248
|
+
let config: VercelOutputConfig | null = null;
|
|
249
|
+
try {
|
|
250
|
+
config = JSON.parse(
|
|
251
|
+
await Deno.readTextFile(configPath),
|
|
252
|
+
) as VercelOutputConfig;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (!(err instanceof Deno.errors.NotFound)) throw err;
|
|
255
|
+
}
|
|
256
|
+
const created = config === null;
|
|
257
|
+
if (config === null) config = { version: 3, routes: [] };
|
|
258
|
+
if (typeof config.version !== "number") config.version = 3;
|
|
259
|
+
|
|
260
|
+
const existing = Array.isArray(config.routes) ? config.routes : [];
|
|
261
|
+
|
|
262
|
+
const prefixRe = escapeRegExp(pathPrefix);
|
|
263
|
+
const destPrefix = `/${pathPrefix}/`;
|
|
264
|
+
const builtNames = new Set(functionNames);
|
|
265
|
+
// The exact route a function `name` maps to. Used for BOTH emit and cleanup
|
|
266
|
+
// so detection can never drift from generation (e.g. a prefix with regex
|
|
267
|
+
// metacharacters is escaped identically in both).
|
|
268
|
+
const routeFor = (name: string): Required<Pick<VercelRoute, "src" | "dest">> => ({
|
|
269
|
+
src: `^/${prefixRe}/${escapeRegExp(name)}/.*$`,
|
|
270
|
+
dest: `${destPrefix}${name}`,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Drop our own previously-written routes for the functions being built now: a
|
|
274
|
+
// route is ours iff its dest is the bare function path AND its src is exactly
|
|
275
|
+
// what we would emit for that function. Gating on `builtNames` leaves routes
|
|
276
|
+
// for other functions intact, so `--only` (incremental) builds are safe.
|
|
277
|
+
const isOwnRoute = (r: VercelRoute): boolean => {
|
|
278
|
+
if (r.handle !== undefined || typeof r.dest !== "string") return false;
|
|
279
|
+
if (!r.dest.startsWith(destPrefix)) return false;
|
|
280
|
+
const name = r.dest.slice(destPrefix.length);
|
|
281
|
+
return builtNames.has(name) && r.src === routeFor(name).src;
|
|
282
|
+
};
|
|
283
|
+
const cleaned = existing.filter((r) => !isOwnRoute(r));
|
|
284
|
+
|
|
285
|
+
const subpathRoutes: VercelRoute[] = [...builtNames]
|
|
286
|
+
.sort((a, b) => a.localeCompare(b))
|
|
287
|
+
.map((name) => routeFor(name));
|
|
288
|
+
|
|
289
|
+
// Place the routes in the main phase — before the first `handle` marker — so
|
|
290
|
+
// each rewrite is immediately resolved against the filesystem (the `.func`).
|
|
291
|
+
const markerIdx = cleaned.findIndex((r) => typeof r.handle === "string");
|
|
292
|
+
config.routes = markerIdx === -1
|
|
293
|
+
? [...cleaned, ...subpathRoutes]
|
|
294
|
+
: [
|
|
295
|
+
...cleaned.slice(0, markerIdx),
|
|
296
|
+
...subpathRoutes,
|
|
297
|
+
...cleaned.slice(markerIdx),
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
await ensureDir(outDir);
|
|
301
|
+
await Deno.writeTextFile(
|
|
302
|
+
configPath,
|
|
303
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
304
|
+
);
|
|
305
|
+
return { created, routeCount: subpathRoutes.length };
|
|
306
|
+
}
|
|
307
|
+
|
|
173
308
|
async function buildVercelOnce(
|
|
174
309
|
opts: VercelBuildOptions,
|
|
175
310
|
): Promise<VercelBuildResult> {
|
|
@@ -223,6 +358,15 @@ async function buildVercelOnce(
|
|
|
223
358
|
durationMs: performance.now() - bundleStart,
|
|
224
359
|
});
|
|
225
360
|
|
|
361
|
+
// Read each function's declared `serve({ timeoutMs })` so it can drive
|
|
362
|
+
// `maxDuration` without a side-car manifest. Done after bundling so the
|
|
363
|
+
// entrypoints' module graph is already warm in Deno's cache; the deployed
|
|
364
|
+
// function is unaffected (see capture-config.ts).
|
|
365
|
+
const declaredConfigs = await captureDeclaredServeConfigs(
|
|
366
|
+
inputs,
|
|
367
|
+
(m) => opts.onProgress?.({ phase: "capture-warning", message: m }),
|
|
368
|
+
);
|
|
369
|
+
|
|
226
370
|
const functionsRoot = join(outDir, "functions", ...pathPrefix.split("/"));
|
|
227
371
|
const entries: VercelFunctionEntry[] = [];
|
|
228
372
|
|
|
@@ -247,8 +391,11 @@ async function buildVercelOnce(
|
|
|
247
391
|
|
|
248
392
|
const handler = fn.moduleFiles[0];
|
|
249
393
|
const manifest = await loadManifest(functionsDir, fn.name);
|
|
394
|
+
// A `serve({ timeoutMs })` declaration wins over the manifest, matching
|
|
395
|
+
// the runtime precedence in `_shared/handler.ts`.
|
|
396
|
+
const declaredTimeoutMs = declaredConfigs.get(fn.name)?.timeoutMs;
|
|
250
397
|
const maxDuration = clampMaxDuration(
|
|
251
|
-
manifest.timeoutMs,
|
|
398
|
+
declaredTimeoutMs ?? manifest.timeoutMs,
|
|
252
399
|
defaultMaxDuration,
|
|
253
400
|
maxDurationCap,
|
|
254
401
|
);
|
|
@@ -293,12 +440,31 @@ async function buildVercelOnce(
|
|
|
293
440
|
}
|
|
294
441
|
|
|
295
442
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
443
|
+
|
|
444
|
+
// Make every function also serve its sub-paths (e.g. /<prefix>/rooms/<id>),
|
|
445
|
+
// matching Supabase edge-runtime's first-segment routing. Done after the
|
|
446
|
+
// `.func` dirs exist so the rewrites resolve to real functions.
|
|
447
|
+
const configPath = join(outDir, "config.json");
|
|
448
|
+
const merge = await mergeSubpathRoutes(
|
|
449
|
+
outDir,
|
|
450
|
+
pathPrefix,
|
|
451
|
+
entries.map((e) => e.name),
|
|
452
|
+
);
|
|
453
|
+
opts.onProgress?.({
|
|
454
|
+
phase: "config-merge",
|
|
455
|
+
configPath,
|
|
456
|
+
routes: merge.routeCount,
|
|
457
|
+
created: merge.created,
|
|
458
|
+
});
|
|
459
|
+
|
|
296
460
|
return {
|
|
297
461
|
outDir,
|
|
298
462
|
functionsRoot,
|
|
299
463
|
runtime,
|
|
300
464
|
pathPrefix,
|
|
301
465
|
functions: entries,
|
|
466
|
+
configPath,
|
|
467
|
+
subpathRoutes: merge.routeCount,
|
|
302
468
|
durationMs: performance.now() - startedAt,
|
|
303
469
|
};
|
|
304
470
|
} finally {
|
|
@@ -467,6 +633,14 @@ export async function runVercelBuild(args: string[]): Promise<number> {
|
|
|
467
633
|
fmt(event.bytes)
|
|
468
634
|
} (maxDuration ${event.maxDuration}s)`,
|
|
469
635
|
);
|
|
636
|
+
} else if (event.phase === "config-merge") {
|
|
637
|
+
console.log(
|
|
638
|
+
`[1tube vercel-build] ${
|
|
639
|
+
event.created ? "wrote" : "merged"
|
|
640
|
+
} ${event.routes} sub-path route(s) into ${event.configPath}`,
|
|
641
|
+
);
|
|
642
|
+
} else if (event.phase === "capture-warning") {
|
|
643
|
+
console.warn(`[1tube vercel-build] ${event.message}`);
|
|
470
644
|
}
|
|
471
645
|
},
|
|
472
646
|
});
|
|
@@ -504,8 +678,9 @@ Options:
|
|
|
504
678
|
(default: ${DEFAULT_ESBUILD_TARGET})
|
|
505
679
|
--path-prefix <p> Route prefix under functions/ (default: functions/v1)
|
|
506
680
|
--runtime <id> Vercel runtime (default: nodejs24.x)
|
|
507
|
-
--max-duration <s> Fallback maxDuration seconds when
|
|
508
|
-
timeoutMs
|
|
681
|
+
--max-duration <s> Fallback maxDuration seconds when a function
|
|
682
|
+
declares no timeoutMs via serve()/1tube.json
|
|
683
|
+
(default: 800)
|
|
509
684
|
--max-duration-cap <s> Hard cap for maxDuration seconds (default: 800)
|
|
510
685
|
--config <path> Explicit deno.json path for the import map.
|
|
511
686
|
Default: auto-detect cwd/deno.json[c] then
|