1tube 0.1.35 → 0.1.37
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/wrapper.ts +307 -0
- package/src/backends/workerd/bundler.ts +98 -9
- package/src/cli/build.ts +31 -0
- package/src/cli/deno-config.ts +83 -0
- package/src/cli/vercel-build.ts +497 -0
- package/src/cli.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel (Node / Fluid) bundle wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the workerd wrapper (which installs a `__edgeFunctionRegistry` and
|
|
5
|
+
* exports a workerd `default { fetch(request, env, ctx) }`), the Vercel target
|
|
6
|
+
* runs each function as a standalone Vercel Node Function. The goals:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Stay on the shared handler's standalone path.** The host project's
|
|
9
|
+
* `_shared/handler.ts` checks for `globalThis.__edgeFunctionRegistry`. We
|
|
10
|
+
* deliberately do NOT install it, so `serve()` falls through to
|
|
11
|
+
* `getStandaloneServe()` -> `globalThis.Deno.serve(...)`. The banner
|
|
12
|
+
* installs a `Deno.serve` shim that simply captures the fetch handler.
|
|
13
|
+
* This keeps auth (JWT verification, service-role), CORS preflight,
|
|
14
|
+
* claims checks, Supabase context construction, and route dispatch exactly
|
|
15
|
+
* where they already live — no per-function changes required.
|
|
16
|
+
*
|
|
17
|
+
* 2. **`Deno.env` -> `process.env`.** User code reads env via `Deno.env.get`.
|
|
18
|
+
* On Vercel Node there is no `Deno`, so the banner installs a `Deno.env`
|
|
19
|
+
* shim backed by `process.env`.
|
|
20
|
+
*
|
|
21
|
+
* 3. **`EdgeRuntime.waitUntil`.** Supabase Edge Runtime exposes
|
|
22
|
+
* `EdgeRuntime.waitUntil()` for background work after the response is sent.
|
|
23
|
+
* On Vercel the equivalent is provided through the `@vercel/request-context`
|
|
24
|
+
* global (which is precisely what `@vercel/functions`' `waitUntil()` reads).
|
|
25
|
+
* The banner shims `globalThis.EdgeRuntime.waitUntil` onto it, and falls
|
|
26
|
+
* back to locally draining pending promises before the handler resolves
|
|
27
|
+
* when no platform context is present (e.g. local execution / tests).
|
|
28
|
+
*
|
|
29
|
+
* 4. **Node request/response bridge.** The footer exports a Vercel Node
|
|
30
|
+
* handler `(req, res)` that converts the Node request into a Web `Request`
|
|
31
|
+
* (preserving the raw body stream for uploads), invokes the captured fetch
|
|
32
|
+
* handler, and streams the Web `Response` back to the Node `res` (so SSE /
|
|
33
|
+
* NDJSON / AI token streaming keep flowing incrementally).
|
|
34
|
+
*
|
|
35
|
+
* The banner intentionally avoids the workerd console-capture marker: on Vercel
|
|
36
|
+
* `console.*` is already routed to the function's logs.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Node builtin module names. Imports of these (bare or `node:`-prefixed) are
|
|
41
|
+
* marked external for the Vercel target so the real Node runtime provides them
|
|
42
|
+
* instead of bundling Deno's polyfills. Subpath imports (e.g. `stream/web`,
|
|
43
|
+
* `fs/promises`) are matched by their head segment.
|
|
44
|
+
*/
|
|
45
|
+
export const NODE_BUILTIN_MODULES: readonly string[] = [
|
|
46
|
+
"assert",
|
|
47
|
+
"async_hooks",
|
|
48
|
+
"buffer",
|
|
49
|
+
"child_process",
|
|
50
|
+
"cluster",
|
|
51
|
+
"console",
|
|
52
|
+
"constants",
|
|
53
|
+
"crypto",
|
|
54
|
+
"dgram",
|
|
55
|
+
"diagnostics_channel",
|
|
56
|
+
"dns",
|
|
57
|
+
"domain",
|
|
58
|
+
"events",
|
|
59
|
+
"fs",
|
|
60
|
+
"http",
|
|
61
|
+
"http2",
|
|
62
|
+
"https",
|
|
63
|
+
"inspector",
|
|
64
|
+
"module",
|
|
65
|
+
"net",
|
|
66
|
+
"os",
|
|
67
|
+
"path",
|
|
68
|
+
"perf_hooks",
|
|
69
|
+
"process",
|
|
70
|
+
"punycode",
|
|
71
|
+
"querystring",
|
|
72
|
+
"readline",
|
|
73
|
+
"repl",
|
|
74
|
+
"stream",
|
|
75
|
+
"string_decoder",
|
|
76
|
+
"sys",
|
|
77
|
+
"timers",
|
|
78
|
+
"tls",
|
|
79
|
+
"trace_events",
|
|
80
|
+
"tty",
|
|
81
|
+
"url",
|
|
82
|
+
"util",
|
|
83
|
+
"v8",
|
|
84
|
+
"vm",
|
|
85
|
+
"wasi",
|
|
86
|
+
"worker_threads",
|
|
87
|
+
"zlib",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Banner injected at the top of every Vercel bundle. Runs before the user
|
|
92
|
+
* entrypoint import so top-level `Deno.env.get(...)` / `serve(...)` calls find
|
|
93
|
+
* the shims installed. The captured handler lives in module scope; the footer
|
|
94
|
+
* exports the Vercel Node handler that dispatches to it.
|
|
95
|
+
*/
|
|
96
|
+
export const VERCEL_BANNER = `// AUTO-GENERATED by 1tube vercel bundler — do not edit
|
|
97
|
+
import { Readable as __1tubeReadable } from "node:stream";
|
|
98
|
+
|
|
99
|
+
let __1tubeCapturedHandler;
|
|
100
|
+
let __1tubeCapturedOpts = { public: false };
|
|
101
|
+
const __1tubePending = [];
|
|
102
|
+
|
|
103
|
+
// --- Deno.env / Deno.serve shim ------------------------------------------
|
|
104
|
+
const __1tubeProcessEnv = () => (globalThis.process && globalThis.process.env) || {};
|
|
105
|
+
const __1tubeDenoEnv = {
|
|
106
|
+
get(key) {
|
|
107
|
+
const v = __1tubeProcessEnv()[key];
|
|
108
|
+
return typeof v === "string" ? v : undefined;
|
|
109
|
+
},
|
|
110
|
+
set(key, value) {
|
|
111
|
+
__1tubeProcessEnv()[key] = String(value);
|
|
112
|
+
},
|
|
113
|
+
delete(key) {
|
|
114
|
+
delete __1tubeProcessEnv()[key];
|
|
115
|
+
},
|
|
116
|
+
has(key) {
|
|
117
|
+
return typeof __1tubeProcessEnv()[key] === "string";
|
|
118
|
+
},
|
|
119
|
+
toObject() {
|
|
120
|
+
const out = {};
|
|
121
|
+
const env = __1tubeProcessEnv();
|
|
122
|
+
for (const k of Object.keys(env)) {
|
|
123
|
+
if (typeof env[k] === "string") out[k] = env[k];
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Captures the fetch handler passed to Deno.serve(). Deno.serve supports
|
|
130
|
+
// serve(handler), serve(options, handler) and serve({ handler }); we accept
|
|
131
|
+
// all three shapes. Returns a Deno.HttpServer-like object so callers that
|
|
132
|
+
// inspect the return value don't crash.
|
|
133
|
+
function __1tubeCaptureServe(arg1, arg2) {
|
|
134
|
+
const handler = typeof arg1 === "function"
|
|
135
|
+
? arg1
|
|
136
|
+
: (typeof arg2 === "function"
|
|
137
|
+
? arg2
|
|
138
|
+
: (arg1 && typeof arg1.handler === "function" ? arg1.handler : undefined));
|
|
139
|
+
if (typeof handler === "function") __1tubeCapturedHandler = handler;
|
|
140
|
+
return {
|
|
141
|
+
finished: Promise.resolve(),
|
|
142
|
+
shutdown() { return Promise.resolve(); },
|
|
143
|
+
ref() {},
|
|
144
|
+
unref() {},
|
|
145
|
+
addr: { transport: "tcp", hostname: "0.0.0.0", port: 0 },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof globalThis.Deno === "undefined") {
|
|
150
|
+
// Pure Node (Vercel): install the full shim.
|
|
151
|
+
Object.defineProperty(globalThis, "Deno", {
|
|
152
|
+
value: { env: __1tubeDenoEnv, serve: __1tubeCaptureServe },
|
|
153
|
+
writable: true,
|
|
154
|
+
configurable: true,
|
|
155
|
+
enumerable: false,
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
// Host Deno (1tube's own tests): override only serve so the handler is
|
|
159
|
+
// captured instead of binding a real port; leave the rest of Deno intact.
|
|
160
|
+
try {
|
|
161
|
+
globalThis.Deno.serve = __1tubeCaptureServe;
|
|
162
|
+
} catch (_e) { /* serve not writable — capture unavailable in this realm */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- EdgeRuntime.waitUntil shim ------------------------------------------
|
|
166
|
+
function __1tubeRequestContext() {
|
|
167
|
+
try {
|
|
168
|
+
const rc = globalThis[Symbol.for("@vercel/request-context")];
|
|
169
|
+
return rc && typeof rc.get === "function" ? rc.get() : undefined;
|
|
170
|
+
} catch (_e) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function __1tubeHasPlatformWaitUntil() {
|
|
175
|
+
const ctx = __1tubeRequestContext();
|
|
176
|
+
return !!(ctx && typeof ctx.waitUntil === "function");
|
|
177
|
+
}
|
|
178
|
+
function __1tubePlatformWaitUntil(promise) {
|
|
179
|
+
const ctx = __1tubeRequestContext();
|
|
180
|
+
if (ctx && typeof ctx.waitUntil === "function") {
|
|
181
|
+
try {
|
|
182
|
+
ctx.waitUntil(promise);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (_e) { /* fall through to local draining */ }
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!globalThis.EdgeRuntime || typeof globalThis.EdgeRuntime.waitUntil !== "function") {
|
|
189
|
+
const edgeRuntime = (globalThis.EdgeRuntime && typeof globalThis.EdgeRuntime === "object")
|
|
190
|
+
? globalThis.EdgeRuntime
|
|
191
|
+
: {};
|
|
192
|
+
edgeRuntime.waitUntil = (promise) => {
|
|
193
|
+
const p = Promise.resolve(promise);
|
|
194
|
+
__1tubePending.push(p.catch(() => {}));
|
|
195
|
+
__1tubePlatformWaitUntil(p);
|
|
196
|
+
return p;
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
globalThis.EdgeRuntime = edgeRuntime;
|
|
200
|
+
} catch (_e) { /* read-only EdgeRuntime — waitUntil shim unavailable */ }
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Footer injected at the bottom of every Vercel bundle. Exports the Vercel
|
|
206
|
+
* Node handler. By the time Vercel invokes it the entrypoint import has run, so
|
|
207
|
+
* `__1tubeCapturedHandler` is populated.
|
|
208
|
+
*/
|
|
209
|
+
export const VERCEL_FOOTER = `
|
|
210
|
+
function __1tubeHeadersFromNode(req) {
|
|
211
|
+
const headers = new Headers();
|
|
212
|
+
const raw = req.headers || {};
|
|
213
|
+
for (const key of Object.keys(raw)) {
|
|
214
|
+
const value = raw[key];
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
for (const v of value) headers.append(key, String(v));
|
|
217
|
+
} else if (value !== undefined && value !== null) {
|
|
218
|
+
headers.set(key, String(value));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return headers;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function __1tubeToWebRequest(req) {
|
|
225
|
+
const headers = __1tubeHeadersFromNode(req);
|
|
226
|
+
const xfproto = headers.get("x-forwarded-proto");
|
|
227
|
+
const proto = (xfproto ? xfproto.split(",")[0] : "https").trim() || "https";
|
|
228
|
+
const host = headers.get("x-forwarded-host") || headers.get("host") || "localhost";
|
|
229
|
+
const path = req.url || "/";
|
|
230
|
+
const url = /^https?:\\/\\//.test(path) ? path : (proto + "://" + host + path);
|
|
231
|
+
const method = (req.method || "GET").toUpperCase();
|
|
232
|
+
const init = { method, headers };
|
|
233
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
234
|
+
// Stream the raw request body through untouched — multipart uploads and
|
|
235
|
+
// proxied request streams must not be buffered/parsed here.
|
|
236
|
+
init.body = __1tubeReadable.toWeb(req);
|
|
237
|
+
init.duplex = "half";
|
|
238
|
+
}
|
|
239
|
+
return new Request(url, init);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function __1tubeWriteWebResponse(res, response) {
|
|
243
|
+
res.statusCode = response.status;
|
|
244
|
+
const setCookie = typeof response.headers.getSetCookie === "function"
|
|
245
|
+
? response.headers.getSetCookie()
|
|
246
|
+
: [];
|
|
247
|
+
response.headers.forEach((value, key) => {
|
|
248
|
+
if (key.toLowerCase() === "set-cookie") return;
|
|
249
|
+
try { res.setHeader(key, value); } catch (_e) { /* ignore invalid header */ }
|
|
250
|
+
});
|
|
251
|
+
if (setCookie.length > 0) {
|
|
252
|
+
try { res.setHeader("set-cookie", setCookie); } catch (_e) { /* ignore */ }
|
|
253
|
+
}
|
|
254
|
+
if (!response.body) {
|
|
255
|
+
res.end();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const reader = response.body.getReader();
|
|
259
|
+
try {
|
|
260
|
+
while (true) {
|
|
261
|
+
const { done, value } = await reader.read();
|
|
262
|
+
if (done) break;
|
|
263
|
+
if (value && value.length > 0) {
|
|
264
|
+
const ok = res.write(value);
|
|
265
|
+
if (ok === false) {
|
|
266
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
res.end();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default async function handler(req, res) {
|
|
276
|
+
try {
|
|
277
|
+
if (typeof __1tubeCapturedHandler !== "function") {
|
|
278
|
+
res.statusCode = 500;
|
|
279
|
+
res.setHeader("content-type", "application/json");
|
|
280
|
+
res.end(JSON.stringify({ error: "function did not register a handler" }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const request = __1tubeToWebRequest(req);
|
|
284
|
+
const response = await __1tubeCapturedHandler(request);
|
|
285
|
+
await __1tubeWriteWebResponse(res, response);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
+
try {
|
|
289
|
+
console.error(err instanceof Error ? (err.stack || message) : message);
|
|
290
|
+
} catch (_e) { /* logging must never throw */ }
|
|
291
|
+
if (!res.headersSent) {
|
|
292
|
+
res.statusCode = 500;
|
|
293
|
+
res.setHeader("content-type", "application/json");
|
|
294
|
+
res.end(JSON.stringify({ error: message }));
|
|
295
|
+
} else {
|
|
296
|
+
try { res.end(); } catch (_e) { /* already ended */ }
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
// When no platform waitUntil is available (local execution / tests), drain
|
|
300
|
+
// background work before resolving so it isn't dropped.
|
|
301
|
+
if (!__1tubeHasPlatformWaitUntil() && __1tubePending.length > 0) {
|
|
302
|
+
const pending = __1tubePending.splice(0, __1tubePending.length);
|
|
303
|
+
await Promise.allSettled(pending);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
@@ -44,6 +44,20 @@ import {
|
|
|
44
44
|
SHARED_RUNTIME_URL_ENV,
|
|
45
45
|
type SharedRuntimeModule,
|
|
46
46
|
} from "./shared-runtime.ts";
|
|
47
|
+
import {
|
|
48
|
+
NODE_BUILTIN_MODULES,
|
|
49
|
+
VERCEL_BANNER,
|
|
50
|
+
VERCEL_FOOTER,
|
|
51
|
+
} from "../vercel/wrapper.ts";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which runtime the chunked bundler emits for. `workerd` (default) wraps each
|
|
55
|
+
* function as a workerd `default { fetch }` registered through
|
|
56
|
+
* `__edgeFunctionRegistry`; `vercel` wraps it as a Vercel Node handler that
|
|
57
|
+
* captures `Deno.serve` and bridges Node req/res to Web fetch. See
|
|
58
|
+
* `../vercel/wrapper.ts` for the Vercel banner/footer.
|
|
59
|
+
*/
|
|
60
|
+
export type BundleTarget = "workerd" | "vercel";
|
|
47
61
|
|
|
48
62
|
/**
|
|
49
63
|
* Banner injected at the top of every bundle. Runs before any user code so
|
|
@@ -485,14 +499,19 @@ function toPosix(path: string): string {
|
|
|
485
499
|
return path.replace(/\\/g, "/");
|
|
486
500
|
}
|
|
487
501
|
|
|
488
|
-
function entryProxySource(entrypoint: string): string {
|
|
489
|
-
|
|
502
|
+
function entryProxySource(entrypoint: string, target: BundleTarget): string {
|
|
503
|
+
const banner = target === "vercel" ? VERCEL_BANNER : BANNER;
|
|
504
|
+
const footer = target === "vercel" ? VERCEL_FOOTER : FOOTER;
|
|
505
|
+
return `${banner}
|
|
490
506
|
await import(${JSON.stringify(pathToFileURL(entrypoint).href)});
|
|
491
|
-
${
|
|
507
|
+
${footer}
|
|
492
508
|
`;
|
|
493
509
|
}
|
|
494
510
|
|
|
495
|
-
function entryProxyPlugin(
|
|
511
|
+
function entryProxyPlugin(
|
|
512
|
+
inputs: readonly BundleInput[],
|
|
513
|
+
target: BundleTarget,
|
|
514
|
+
): esbuild.Plugin {
|
|
496
515
|
const byName = new Map(inputs.map((input) => [input.name, input]));
|
|
497
516
|
return {
|
|
498
517
|
name: "1tube-entry-proxy",
|
|
@@ -513,7 +532,7 @@ function entryProxyPlugin(inputs: readonly BundleInput[]): esbuild.Plugin {
|
|
|
513
532
|
);
|
|
514
533
|
}
|
|
515
534
|
return {
|
|
516
|
-
contents: entryProxySource(input.entrypoint),
|
|
535
|
+
contents: entryProxySource(input.entrypoint, target),
|
|
517
536
|
loader: "js",
|
|
518
537
|
resolveDir: dirname(input.entrypoint),
|
|
519
538
|
};
|
|
@@ -523,6 +542,59 @@ function entryProxyPlugin(inputs: readonly BundleInput[]): esbuild.Plugin {
|
|
|
523
542
|
};
|
|
524
543
|
}
|
|
525
544
|
|
|
545
|
+
const NODE_BUILTIN_SET = new Set<string>(NODE_BUILTIN_MODULES);
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Optional native/peer packages that popular Node libraries `require()` from
|
|
549
|
+
* inside a `try/catch` as an opt-in speedup, degrading to a pure-JS path when
|
|
550
|
+
* absent. They are `optionalDependencies` that are not installed, so esbuild's
|
|
551
|
+
* deno loader fails to resolve them when the Vercel (Node) target selects the
|
|
552
|
+
* libraries' `node` build. Marking them external makes esbuild emit its
|
|
553
|
+
* `__require` shim (which throws at the call site); the library catches that
|
|
554
|
+
* and uses its fallback. The workerd target never hits this because its
|
|
555
|
+
* `browser`/`worker` conditions resolve the dependency-free browser builds.
|
|
556
|
+
*
|
|
557
|
+
* - `bufferutil`, `utf-8-validate` → `ws` (native frame masking / UTF-8 check)
|
|
558
|
+
* - `supports-color` → `debug` (TTY colour detection)
|
|
559
|
+
*/
|
|
560
|
+
const VERCEL_OPTIONAL_EXTERNALS: readonly string[] = [
|
|
561
|
+
"bufferutil",
|
|
562
|
+
"utf-8-validate",
|
|
563
|
+
"supports-color",
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
function isNodeBuiltinBare(spec: string): boolean {
|
|
567
|
+
const slash = spec.indexOf("/");
|
|
568
|
+
const head = slash === -1 ? spec : spec.slice(0, slash);
|
|
569
|
+
return NODE_BUILTIN_SET.has(head);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* For the Vercel (Node) target, leave Node builtins to the host runtime
|
|
574
|
+
* instead of bundling Deno's polyfills for them. `node:`-prefixed specifiers
|
|
575
|
+
* are always external; bare builtins (`crypto`, `stream`, `stream/web`, ...)
|
|
576
|
+
* are rewritten to their `node:` form and externalized. Non-builtins fall
|
|
577
|
+
* through to the deno loader for npm/jsr resolution. Registered before the
|
|
578
|
+
* deno plugin so it wins for these specifiers.
|
|
579
|
+
*/
|
|
580
|
+
function nodeBuiltinsExternalPlugin(): esbuild.Plugin {
|
|
581
|
+
return {
|
|
582
|
+
name: "1tube-node-builtins-external",
|
|
583
|
+
setup(build) {
|
|
584
|
+
build.onResolve({ filter: /^node:/ }, (args) => ({
|
|
585
|
+
path: args.path,
|
|
586
|
+
external: true,
|
|
587
|
+
}));
|
|
588
|
+
build.onResolve({ filter: /^[A-Za-z][A-Za-z0-9._/-]*$/ }, (args) => {
|
|
589
|
+
if (isNodeBuiltinBare(args.path)) {
|
|
590
|
+
return { path: `node:${args.path}`, external: true };
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
526
598
|
async function patchOutputFile(path: string): Promise<void> {
|
|
527
599
|
const emitted = await Deno.readTextFile(path);
|
|
528
600
|
const patched = patchCjsDefaultInterop(emitted);
|
|
@@ -692,14 +764,21 @@ export function createBundler(opts: BundlerOptions): Bundler {
|
|
|
692
764
|
export async function bundleAllChunked(
|
|
693
765
|
opts: BundlerOptions & {
|
|
694
766
|
inputs: BundleInput[];
|
|
767
|
+
/** Runtime to emit for. Defaults to `workerd`. */
|
|
768
|
+
target?: BundleTarget;
|
|
695
769
|
},
|
|
696
770
|
): Promise<ChunkedBundleAllResult> {
|
|
697
771
|
await ensureDir(opts.outDir);
|
|
698
772
|
const start = performance.now();
|
|
773
|
+
const target: BundleTarget = opts.target ?? "workerd";
|
|
774
|
+
const isVercel = target === "vercel";
|
|
699
775
|
const sourcemap = opts.sourcemap ?? "linked";
|
|
700
776
|
const sharedModules = opts.sharedModules ?? [];
|
|
701
777
|
const plugins = [
|
|
702
|
-
|
|
778
|
+
// Node builtins must be externalized before the deno loader gets a
|
|
779
|
+
// chance to substitute its polyfills for the Vercel/Node target.
|
|
780
|
+
...(isVercel ? [nodeBuiltinsExternalPlugin()] : []),
|
|
781
|
+
entryProxyPlugin(opts.inputs, target),
|
|
703
782
|
...(sharedModules.length > 0
|
|
704
783
|
? [sharedModulesExternalPlugin(sharedModules)]
|
|
705
784
|
: []),
|
|
@@ -722,8 +801,18 @@ export async function bundleAllChunked(
|
|
|
722
801
|
splitting: true,
|
|
723
802
|
format: "esm",
|
|
724
803
|
target: "es2022",
|
|
725
|
-
|
|
726
|
-
|
|
804
|
+
// Vercel runs on Node, so resolve the node conditions and let the
|
|
805
|
+
// host runtime provide builtins; workerd uses the leaner
|
|
806
|
+
// worker/browser conditions with nodejs_compat polyfills.
|
|
807
|
+
platform: isVercel ? "node" : "neutral",
|
|
808
|
+
conditions: isVercel
|
|
809
|
+
? ["node", "import", "default"]
|
|
810
|
+
: ["worker", "browser", "import", "default"],
|
|
811
|
+
// Optional native deps that `ws`/`debug` require() behind a try/catch.
|
|
812
|
+
// The deno loader honors esbuild's `external`, short-circuiting them to
|
|
813
|
+
// external so the Node build resolves without their (uninstalled)
|
|
814
|
+
// optionalDependencies. Workerd resolves the browser build and needs none.
|
|
815
|
+
external: isVercel ? [...VERCEL_OPTIONAL_EXTERNALS] : undefined,
|
|
727
816
|
sourcemap,
|
|
728
817
|
minify: opts.minify ?? false,
|
|
729
818
|
treeShaking: true,
|
|
@@ -737,7 +826,7 @@ export async function bundleAllChunked(
|
|
|
737
826
|
});
|
|
738
827
|
} catch (err) {
|
|
739
828
|
const cause = err instanceof Error ? err.message : String(err);
|
|
740
|
-
throw new Error(
|
|
829
|
+
throw new Error(`${target} chunked bundle failed\n${cause}`, {
|
|
741
830
|
cause: err instanceof Error ? err : undefined,
|
|
742
831
|
});
|
|
743
832
|
}
|
package/src/cli/build.ts
CHANGED
|
@@ -419,6 +419,35 @@ export async function build(opts: BuildOptions): Promise<BuildResult> {
|
|
|
419
419
|
* --workerd-shared path repeatable; module to run in gateway shared runtime
|
|
420
420
|
*/
|
|
421
421
|
export async function runBuild(args: string[]): Promise<number> {
|
|
422
|
+
// --target selects the backend artifact. Default `workerd` (dist/ +
|
|
423
|
+
// manifest.json); `--target vercel` delegates to the Vercel Build Output
|
|
424
|
+
// API builder, which owns its own flags (--path-prefix, --runtime,
|
|
425
|
+
// --max-duration, …). Pre-scanned here so we route before parsing any
|
|
426
|
+
// workerd-specific flags.
|
|
427
|
+
let target = "workerd";
|
|
428
|
+
const rest: string[] = [];
|
|
429
|
+
for (let i = 0; i < args.length; i++) {
|
|
430
|
+
const a = args[i];
|
|
431
|
+
if ((a === "--target" || a === "-t") && args[i + 1]) {
|
|
432
|
+
target = args[++i];
|
|
433
|
+
} else if (a.startsWith("--target=")) {
|
|
434
|
+
target = a.slice("--target=".length);
|
|
435
|
+
} else {
|
|
436
|
+
rest.push(a);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (target === "vercel") {
|
|
440
|
+
const { runVercelBuild } = await import("./vercel-build.ts");
|
|
441
|
+
return await runVercelBuild(rest);
|
|
442
|
+
}
|
|
443
|
+
if (target !== "workerd") {
|
|
444
|
+
console.error(
|
|
445
|
+
`[1tube build] unknown --target: ${target} (expected "workerd" or "vercel")`,
|
|
446
|
+
);
|
|
447
|
+
return 2;
|
|
448
|
+
}
|
|
449
|
+
args = rest;
|
|
450
|
+
|
|
422
451
|
let functionsDir = Deno.env.get("FUNCTIONS_PATH") || "./supabase/functions";
|
|
423
452
|
let outDir = "./dist";
|
|
424
453
|
let only: string[] | undefined;
|
|
@@ -713,6 +742,8 @@ export async function runBuild(args: string[]): Promise<number> {
|
|
|
713
742
|
const BUILD_USAGE = `Usage: 1tube build [options]
|
|
714
743
|
|
|
715
744
|
Options:
|
|
745
|
+
-t, --target NAME Build target: workerd (default) | vercel
|
|
746
|
+
For vercel options: 1tube build --target vercel --help
|
|
716
747
|
-f, --functions <path> Function source dir (default: ./supabase/functions)
|
|
717
748
|
-o, --out <path> Output directory (default: ./dist)
|
|
718
749
|
--only A,B,C Build only the named subset
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Deno-config resolution for the build commands.
|
|
3
|
+
*
|
|
4
|
+
* esbuild's Deno loader needs the host project's `deno.json[c]` to apply the
|
|
5
|
+
* import map. Both `1tube build` (workerd) and `1tube vercel-build` resolve it
|
|
6
|
+
* the same way, in priority order:
|
|
7
|
+
*
|
|
8
|
+
* 1. explicit `--config <path>` → must exist (hard error otherwise)
|
|
9
|
+
* 2. explicit `--no-config` → skip the import map entirely
|
|
10
|
+
* 3. auto-detect → cwd/deno.json[c] then
|
|
11
|
+
* <functions-dir>/deno.json[c]
|
|
12
|
+
*
|
|
13
|
+
* The functions-dir fallback is what makes this work for hosts like
|
|
14
|
+
* sciobot-next: a Vite/Bun project whose only `deno.json` lives next to the
|
|
15
|
+
* edge functions (Supabase layout), not at repo root.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
19
|
+
|
|
20
|
+
export interface ResolveDenoConfigOptions {
|
|
21
|
+
/** Function source dir (absolute or relative to cwd). */
|
|
22
|
+
functionsDir: string;
|
|
23
|
+
/**
|
|
24
|
+
* Explicit `--config` value. `undefined` = auto-detect; `""` = `--no-config`
|
|
25
|
+
* (skip the import map); otherwise an explicit path that must exist.
|
|
26
|
+
*/
|
|
27
|
+
configArg?: string | undefined;
|
|
28
|
+
/** Sink for human-facing detection messages (no prefix). */
|
|
29
|
+
onMessage?: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResolveDenoConfigResult {
|
|
33
|
+
/** Resolved absolute config path, or undefined when none applies. */
|
|
34
|
+
configPath?: string;
|
|
35
|
+
/** Set when an explicit `--config` path was unreadable. */
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveDenoConfigPath(
|
|
40
|
+
opts: ResolveDenoConfigOptions,
|
|
41
|
+
): Promise<ResolveDenoConfigResult> {
|
|
42
|
+
const { functionsDir, configArg, onMessage } = opts;
|
|
43
|
+
|
|
44
|
+
if (configArg !== undefined) {
|
|
45
|
+
if (configArg === "") return {};
|
|
46
|
+
const abs = isAbsolute(configArg)
|
|
47
|
+
? configArg
|
|
48
|
+
: resolvePath(Deno.cwd(), configArg);
|
|
49
|
+
try {
|
|
50
|
+
const stat = await Deno.stat(abs);
|
|
51
|
+
if (!stat.isFile) throw new Error("not a regular file");
|
|
52
|
+
return { configPath: abs };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
error: `--config path not readable: ${abs} (${(err as Error).message})`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fnDirAbs = isAbsolute(functionsDir)
|
|
61
|
+
? functionsDir
|
|
62
|
+
: resolvePath(Deno.cwd(), functionsDir);
|
|
63
|
+
const candidates = [
|
|
64
|
+
`${Deno.cwd()}/deno.json`,
|
|
65
|
+
`${Deno.cwd()}/deno.jsonc`,
|
|
66
|
+
`${fnDirAbs}/deno.json`,
|
|
67
|
+
`${fnDirAbs}/deno.jsonc`,
|
|
68
|
+
];
|
|
69
|
+
for (const abs of candidates) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await Deno.stat(abs);
|
|
72
|
+
if (stat.isFile) {
|
|
73
|
+
onMessage?.(`using deno config: ${abs}`);
|
|
74
|
+
return { configPath: abs };
|
|
75
|
+
}
|
|
76
|
+
} catch { /* not found, try next */ }
|
|
77
|
+
}
|
|
78
|
+
onMessage?.(
|
|
79
|
+
`no deno.json / deno.jsonc at ${Deno.cwd()} or ${fnDirAbs} — bundling ` +
|
|
80
|
+
`without an import map. Pass --config <path> if you want one.`,
|
|
81
|
+
);
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `1tube vercel-build` — compile edge functions into Vercel Build Output API
|
|
3
|
+
* function artifacts.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `1tube build` (which produces a workerd `dist/` + `manifest.json`),
|
|
6
|
+
* this command emits one Vercel Node Function per edge function, laid out for
|
|
7
|
+
* the Build Output API so `vercel deploy --prebuilt` ships them as-is:
|
|
8
|
+
*
|
|
9
|
+
* .vercel/output/
|
|
10
|
+
* functions/
|
|
11
|
+
* functions/v1/<name>.func/
|
|
12
|
+
* .vc-config.json — runtime + handler + streaming/maxDuration
|
|
13
|
+
* package.json — { "type": "module" } so .js is treated ESM
|
|
14
|
+
* functions/<name>.js — esbuild entry (the Vercel Node handler)
|
|
15
|
+
* chunks/<hash>.js — shared ESM chunks (copied per function)
|
|
16
|
+
*
|
|
17
|
+
* A function placed at `functions/functions/v1/<name>.func` is served by Vercel
|
|
18
|
+
* at the route `/functions/v1/<name>` — matching the path shape the frontend
|
|
19
|
+
* already calls (see sciobot's `API_CONFIG`). No per-function source changes are
|
|
20
|
+
* required: the Vercel bundle wrapper captures `Deno.serve` and bridges Node
|
|
21
|
+
* req/res to the Web fetch handler (see `../backends/vercel/wrapper.ts`).
|
|
22
|
+
*
|
|
23
|
+
* This command MERGES into an existing `.vercel/output` (e.g. one produced by
|
|
24
|
+
* `vercel build` for the static frontend). It only creates/replaces the
|
|
25
|
+
* specific `.func` directories; it never wipes the output root, so the
|
|
26
|
+
* frontend's `config.json` and static assets are left untouched.
|
|
27
|
+
*
|
|
28
|
+
* Each function is fully standalone — there is no gateway shared runtime on
|
|
29
|
+
* Vercel, so modules like `_shared/profile-cache.ts` are bundled inline rather
|
|
30
|
+
* than turned into RPC stubs.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { ensureDir } from "jsr:@std/fs@^1/ensure-dir";
|
|
34
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
35
|
+
import {
|
|
36
|
+
bundleAllChunked,
|
|
37
|
+
type ChunkedBundleAllResult,
|
|
38
|
+
discoverEntrypoints,
|
|
39
|
+
disposeBundlerResources,
|
|
40
|
+
} from "../backends/workerd/bundler.ts";
|
|
41
|
+
import { loadManifest } from "../manifest.ts";
|
|
42
|
+
import { resolveDenoConfigPath } from "./deno-config.ts";
|
|
43
|
+
|
|
44
|
+
/** Default Vercel managed Node runtime for emitted functions. */
|
|
45
|
+
const DEFAULT_RUNTIME = "nodejs24.x";
|
|
46
|
+
/** Default route prefix; matches sciobot's `/functions/v1/<name>` calls. */
|
|
47
|
+
const DEFAULT_PATH_PREFIX = "functions/v1";
|
|
48
|
+
/** Default function timeout (seconds) when no `1tube.json` timeoutMs is set. */
|
|
49
|
+
const DEFAULT_MAX_DURATION = 300;
|
|
50
|
+
/** Upper bound for maxDuration (seconds). Fluid/Pro allows long durations. */
|
|
51
|
+
const DEFAULT_MAX_DURATION_CAP = 800;
|
|
52
|
+
|
|
53
|
+
export interface VercelBuildOptions {
|
|
54
|
+
functionsDir: string;
|
|
55
|
+
/** Build Output API root, typically `.vercel/output`. */
|
|
56
|
+
outDir: string;
|
|
57
|
+
/** Path to host project's deno.json (for esbuild's Deno loader import map). */
|
|
58
|
+
configPath?: string;
|
|
59
|
+
/** Restrict the build to a subset of function names. */
|
|
60
|
+
only?: readonly string[];
|
|
61
|
+
/** Sourcemap mode. Defaults to "linked". */
|
|
62
|
+
sourcemap?: boolean | "linked" | "inline";
|
|
63
|
+
/** Minify bundles. Defaults to false. */
|
|
64
|
+
minify?: boolean;
|
|
65
|
+
/** Route prefix under `functions/`. Defaults to "functions/v1". */
|
|
66
|
+
pathPrefix?: string;
|
|
67
|
+
/** Vercel runtime identifier. Defaults to "nodejs24.x". */
|
|
68
|
+
runtime?: string;
|
|
69
|
+
/** Fallback maxDuration (seconds) when a function has no timeoutMs. */
|
|
70
|
+
defaultMaxDuration?: number;
|
|
71
|
+
/** Hard cap (seconds) applied to any derived maxDuration. */
|
|
72
|
+
maxDurationCap?: number;
|
|
73
|
+
/** Progress callback for CLI/status reporting. */
|
|
74
|
+
onProgress?: (event: VercelBuildProgress) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type VercelBuildProgress =
|
|
78
|
+
| { phase: "bundle-start"; functions: number }
|
|
79
|
+
| {
|
|
80
|
+
phase: "bundle-complete";
|
|
81
|
+
functions: number;
|
|
82
|
+
chunks: number;
|
|
83
|
+
durationMs: number;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
phase: "emit-function";
|
|
87
|
+
name: string;
|
|
88
|
+
route: string;
|
|
89
|
+
bytes: number;
|
|
90
|
+
maxDuration: number;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface VercelFunctionEntry {
|
|
94
|
+
/** Logical function name. */
|
|
95
|
+
name: string;
|
|
96
|
+
/** Public route, e.g. `functions/v1/hello`. */
|
|
97
|
+
route: string;
|
|
98
|
+
/** Absolute path to the emitted `<name>.func` directory. */
|
|
99
|
+
funcDir: string;
|
|
100
|
+
/** Handler path relative to `funcDir` (POSIX), e.g. `functions/hello.js`. */
|
|
101
|
+
handler: string;
|
|
102
|
+
/** All module files copied into the func dir (POSIX, entry first). */
|
|
103
|
+
moduleFiles: string[];
|
|
104
|
+
/** Entry bundle byte size. */
|
|
105
|
+
byteLength: number;
|
|
106
|
+
/** Derived Vercel maxDuration (seconds). */
|
|
107
|
+
maxDuration: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface VercelBuildResult {
|
|
111
|
+
/** Build Output API root. */
|
|
112
|
+
outDir: string;
|
|
113
|
+
/** Absolute path to `<outDir>/functions/<pathPrefix>`. */
|
|
114
|
+
functionsRoot: string;
|
|
115
|
+
/** Runtime baked into each `.vc-config.json`. */
|
|
116
|
+
runtime: string;
|
|
117
|
+
/** Effective route prefix. */
|
|
118
|
+
pathPrefix: string;
|
|
119
|
+
/** Emitted functions, sorted by name. */
|
|
120
|
+
functions: VercelFunctionEntry[];
|
|
121
|
+
durationMs: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `.vc-config.json` shape for a Build Output API Node function. */
|
|
125
|
+
interface VcConfig {
|
|
126
|
+
runtime: string;
|
|
127
|
+
handler: string;
|
|
128
|
+
launcherType: "Nodejs";
|
|
129
|
+
shouldAddHelpers: boolean;
|
|
130
|
+
shouldAddSourcemapSupport: boolean;
|
|
131
|
+
supportsResponseStreaming: boolean;
|
|
132
|
+
maxDuration: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function removeDirIfExists(path: string): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
await Deno.remove(path, { recursive: true });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (!(err instanceof Deno.errors.NotFound)) throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convert a per-function `timeoutMs` into a Vercel maxDuration (seconds),
|
|
145
|
+
* clamped to `[1, cap]`. Falls back to `def` when no timeout is configured.
|
|
146
|
+
*/
|
|
147
|
+
function clampMaxDuration(
|
|
148
|
+
timeoutMs: number | undefined,
|
|
149
|
+
def: number,
|
|
150
|
+
cap: number,
|
|
151
|
+
): number {
|
|
152
|
+
const base = typeof timeoutMs === "number" && timeoutMs > 0
|
|
153
|
+
? Math.ceil(timeoutMs / 1000)
|
|
154
|
+
: def;
|
|
155
|
+
return Math.max(1, Math.min(cap, base));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizePrefix(prefix: string): string {
|
|
159
|
+
return prefix.replace(/^[/\\]+/, "").replace(/[/\\]+$/, "").replace(
|
|
160
|
+
/\\/g,
|
|
161
|
+
"/",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function buildVercelOnce(
|
|
166
|
+
opts: VercelBuildOptions,
|
|
167
|
+
): Promise<VercelBuildResult> {
|
|
168
|
+
const startedAt = performance.now();
|
|
169
|
+
const cwd = Deno.cwd();
|
|
170
|
+
const functionsDir = isAbsolute(opts.functionsDir)
|
|
171
|
+
? opts.functionsDir
|
|
172
|
+
: resolvePath(cwd, opts.functionsDir);
|
|
173
|
+
const outDir = isAbsolute(opts.outDir)
|
|
174
|
+
? opts.outDir
|
|
175
|
+
: resolvePath(cwd, opts.outDir);
|
|
176
|
+
const pathPrefix = normalizePrefix(opts.pathPrefix ?? DEFAULT_PATH_PREFIX);
|
|
177
|
+
const runtime = opts.runtime ?? DEFAULT_RUNTIME;
|
|
178
|
+
const sourcemap = opts.sourcemap ?? "linked";
|
|
179
|
+
const defaultMaxDuration = opts.defaultMaxDuration ?? DEFAULT_MAX_DURATION;
|
|
180
|
+
const maxDurationCap = opts.maxDurationCap ?? DEFAULT_MAX_DURATION_CAP;
|
|
181
|
+
|
|
182
|
+
let inputs = await discoverEntrypoints(functionsDir);
|
|
183
|
+
if (opts.only && opts.only.length > 0) {
|
|
184
|
+
const allow = new Set(opts.only);
|
|
185
|
+
inputs = inputs.filter((i) => allow.has(i.name));
|
|
186
|
+
}
|
|
187
|
+
if (inputs.length === 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`1tube vercel-build: no functions matched under ${functionsDir}` +
|
|
190
|
+
(opts.only ? ` (only=${JSON.stringify(opts.only)})` : ""),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Bundle into a throwaway staging dir, then distribute each function's
|
|
195
|
+
// module files into its own `.func` directory. Staging keeps the chunk
|
|
196
|
+
// graph intact (shared chunks are emitted once) while letting us copy the
|
|
197
|
+
// reachable set per function so each `.func` is self-contained.
|
|
198
|
+
const stageDir = await Deno.makeTempDir({ prefix: "1tube-vercel-" });
|
|
199
|
+
try {
|
|
200
|
+
opts.onProgress?.({ phase: "bundle-start", functions: inputs.length });
|
|
201
|
+
const bundleStart = performance.now();
|
|
202
|
+
const chunked: ChunkedBundleAllResult = await bundleAllChunked({
|
|
203
|
+
inputs,
|
|
204
|
+
outDir: stageDir,
|
|
205
|
+
target: "vercel",
|
|
206
|
+
...(opts.configPath ? { configPath: opts.configPath } : {}),
|
|
207
|
+
sourcemap,
|
|
208
|
+
minify: opts.minify ?? false,
|
|
209
|
+
});
|
|
210
|
+
opts.onProgress?.({
|
|
211
|
+
phase: "bundle-complete",
|
|
212
|
+
functions: chunked.functions.length,
|
|
213
|
+
chunks: chunked.chunks.length,
|
|
214
|
+
durationMs: performance.now() - bundleStart,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const functionsRoot = join(outDir, "functions", ...pathPrefix.split("/"));
|
|
218
|
+
const entries: VercelFunctionEntry[] = [];
|
|
219
|
+
|
|
220
|
+
for (const fn of chunked.functions) {
|
|
221
|
+
const funcDir = join(functionsRoot, `${fn.name}.func`);
|
|
222
|
+
// Replace any stale artifact so removed chunks don't linger.
|
|
223
|
+
await removeDirIfExists(funcDir);
|
|
224
|
+
|
|
225
|
+
for (const moduleFile of fn.moduleFiles) {
|
|
226
|
+
const src = join(stageDir, moduleFile);
|
|
227
|
+
const dest = join(funcDir, moduleFile);
|
|
228
|
+
await ensureDir(dirname(dest));
|
|
229
|
+
await Deno.copyFile(src, dest);
|
|
230
|
+
if (sourcemap === "linked") {
|
|
231
|
+
try {
|
|
232
|
+
await Deno.copyFile(`${src}.map`, `${dest}.map`);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!(err instanceof Deno.errors.NotFound)) throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handler = fn.moduleFiles[0];
|
|
240
|
+
const manifest = await loadManifest(functionsDir, fn.name);
|
|
241
|
+
const maxDuration = clampMaxDuration(
|
|
242
|
+
manifest.timeoutMs,
|
|
243
|
+
defaultMaxDuration,
|
|
244
|
+
maxDurationCap,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const vcConfig: VcConfig = {
|
|
248
|
+
runtime,
|
|
249
|
+
handler,
|
|
250
|
+
launcherType: "Nodejs",
|
|
251
|
+
shouldAddHelpers: false,
|
|
252
|
+
shouldAddSourcemapSupport: sourcemap === "linked",
|
|
253
|
+
supportsResponseStreaming: true,
|
|
254
|
+
maxDuration,
|
|
255
|
+
};
|
|
256
|
+
await Deno.writeTextFile(
|
|
257
|
+
join(funcDir, ".vc-config.json"),
|
|
258
|
+
JSON.stringify(vcConfig, null, 2) + "\n",
|
|
259
|
+
);
|
|
260
|
+
// package.json with type:module so Vercel's Node launcher loads the
|
|
261
|
+
// emitted `.js` files as ESM (esbuild output uses import/export).
|
|
262
|
+
await Deno.writeTextFile(
|
|
263
|
+
join(funcDir, "package.json"),
|
|
264
|
+
JSON.stringify({ type: "module" }, null, 2) + "\n",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const route = `${pathPrefix}/${fn.name}`;
|
|
268
|
+
entries.push({
|
|
269
|
+
name: fn.name,
|
|
270
|
+
route,
|
|
271
|
+
funcDir,
|
|
272
|
+
handler,
|
|
273
|
+
moduleFiles: [...fn.moduleFiles],
|
|
274
|
+
byteLength: fn.byteLength,
|
|
275
|
+
maxDuration,
|
|
276
|
+
});
|
|
277
|
+
opts.onProgress?.({
|
|
278
|
+
phase: "emit-function",
|
|
279
|
+
name: fn.name,
|
|
280
|
+
route,
|
|
281
|
+
bytes: fn.byteLength,
|
|
282
|
+
maxDuration,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
287
|
+
return {
|
|
288
|
+
outDir,
|
|
289
|
+
functionsRoot,
|
|
290
|
+
runtime,
|
|
291
|
+
pathPrefix,
|
|
292
|
+
functions: entries,
|
|
293
|
+
durationMs: performance.now() - startedAt,
|
|
294
|
+
};
|
|
295
|
+
} finally {
|
|
296
|
+
await removeDirIfExists(stageDir);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function buildVercel(
|
|
301
|
+
opts: VercelBuildOptions,
|
|
302
|
+
): Promise<VercelBuildResult> {
|
|
303
|
+
try {
|
|
304
|
+
return await buildVercelOnce(opts);
|
|
305
|
+
} finally {
|
|
306
|
+
// Always release esbuild's worker subprocess — on Windows it otherwise
|
|
307
|
+
// lingers and blocks deletion of the staging dir / output.
|
|
308
|
+
await disposeBundlerResources().catch(() => {});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* CLI entry point. Returns a process exit code (does not call `Deno.exit`).
|
|
314
|
+
*
|
|
315
|
+
* Recognised flags (all optional):
|
|
316
|
+
* --functions <path> function source dirs (default ./supabase/functions)
|
|
317
|
+
* --out <path> Build Output API root (default ./.vercel/output)
|
|
318
|
+
* --only A,B,C build a subset of functions
|
|
319
|
+
* --sourcemap none|linked|inline (default: linked)
|
|
320
|
+
* --minify minify bundles
|
|
321
|
+
* --path-prefix <p> route prefix under functions/ (default functions/v1)
|
|
322
|
+
* --runtime <id> Vercel runtime (default nodejs24.x)
|
|
323
|
+
* --max-duration <s> fallback maxDuration seconds (default 300)
|
|
324
|
+
* --max-duration-cap <s> hard cap for maxDuration (default 800)
|
|
325
|
+
* --config <path> explicit deno.json path
|
|
326
|
+
* --no-config skip the import map
|
|
327
|
+
*/
|
|
328
|
+
export async function runVercelBuild(args: string[]): Promise<number> {
|
|
329
|
+
let functionsDir = Deno.env.get("FUNCTIONS_PATH") || "./supabase/functions";
|
|
330
|
+
let outDir = "./.vercel/output";
|
|
331
|
+
let only: string[] | undefined;
|
|
332
|
+
let sourcemap: boolean | "linked" | "inline" = "linked";
|
|
333
|
+
let minify = false;
|
|
334
|
+
let pathPrefix = DEFAULT_PATH_PREFIX;
|
|
335
|
+
let runtime = DEFAULT_RUNTIME;
|
|
336
|
+
let defaultMaxDuration = DEFAULT_MAX_DURATION;
|
|
337
|
+
let maxDurationCap = DEFAULT_MAX_DURATION_CAP;
|
|
338
|
+
let configPathArg: string | undefined;
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < args.length; i++) {
|
|
341
|
+
const a = args[i];
|
|
342
|
+
if ((a === "--functions" || a === "-f") && args[i + 1]) {
|
|
343
|
+
functionsDir = args[++i];
|
|
344
|
+
} else if ((a === "--out" || a === "-o") && args[i + 1]) {
|
|
345
|
+
outDir = args[++i];
|
|
346
|
+
} else if (a === "--only" && args[i + 1]) {
|
|
347
|
+
only = args[++i].split(",").map((s) => s.trim()).filter((s) =>
|
|
348
|
+
s.length > 0
|
|
349
|
+
);
|
|
350
|
+
} else if (a === "--sourcemap" && args[i + 1]) {
|
|
351
|
+
const v = args[++i];
|
|
352
|
+
if (v === "none" || v === "false" || v === "off") sourcemap = false;
|
|
353
|
+
else if (v === "inline") sourcemap = "inline";
|
|
354
|
+
else if (v === "linked" || v === "true" || v === "on") {
|
|
355
|
+
sourcemap = "linked";
|
|
356
|
+
} else {
|
|
357
|
+
console.error(`[1tube vercel-build] unknown --sourcemap value: ${v}`);
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
} else if (a === "--minify") {
|
|
361
|
+
minify = true;
|
|
362
|
+
} else if (a === "--path-prefix" && args[i + 1]) {
|
|
363
|
+
pathPrefix = args[++i];
|
|
364
|
+
} else if (a.startsWith("--path-prefix=")) {
|
|
365
|
+
pathPrefix = a.slice("--path-prefix=".length);
|
|
366
|
+
} else if (a === "--runtime" && args[i + 1]) {
|
|
367
|
+
runtime = args[++i];
|
|
368
|
+
} else if (a.startsWith("--runtime=")) {
|
|
369
|
+
runtime = a.slice("--runtime=".length);
|
|
370
|
+
} else if (a === "--max-duration" && args[i + 1]) {
|
|
371
|
+
const n = parseInt(args[++i], 10);
|
|
372
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
373
|
+
console.error(
|
|
374
|
+
`[1tube vercel-build] --max-duration must be a positive integer`,
|
|
375
|
+
);
|
|
376
|
+
return 2;
|
|
377
|
+
}
|
|
378
|
+
defaultMaxDuration = n;
|
|
379
|
+
} else if (a === "--max-duration-cap" && args[i + 1]) {
|
|
380
|
+
const n = parseInt(args[++i], 10);
|
|
381
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
382
|
+
console.error(
|
|
383
|
+
`[1tube vercel-build] --max-duration-cap must be a positive integer`,
|
|
384
|
+
);
|
|
385
|
+
return 2;
|
|
386
|
+
}
|
|
387
|
+
maxDurationCap = n;
|
|
388
|
+
} else if (a === "--config" && args[i + 1]) {
|
|
389
|
+
configPathArg = args[++i];
|
|
390
|
+
} else if (a.startsWith("--config=")) {
|
|
391
|
+
configPathArg = a.slice("--config=".length);
|
|
392
|
+
} else if (a === "--no-config") {
|
|
393
|
+
configPathArg = "";
|
|
394
|
+
} else if (a === "--help" || a === "-h") {
|
|
395
|
+
console.log(VERCEL_BUILD_USAGE);
|
|
396
|
+
return 0;
|
|
397
|
+
} else {
|
|
398
|
+
console.error(
|
|
399
|
+
`[1tube vercel-build] unknown argument: ${a}\n${VERCEL_BUILD_USAGE}`,
|
|
400
|
+
);
|
|
401
|
+
return 2;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const configResult = await resolveDenoConfigPath({
|
|
406
|
+
functionsDir,
|
|
407
|
+
configArg: configPathArg,
|
|
408
|
+
onMessage: (m) => console.log(`[1tube vercel-build] ${m}`),
|
|
409
|
+
});
|
|
410
|
+
if (configResult.error) {
|
|
411
|
+
console.error(`[1tube vercel-build] ${configResult.error}`);
|
|
412
|
+
return 2;
|
|
413
|
+
}
|
|
414
|
+
const configPath = configResult.configPath;
|
|
415
|
+
|
|
416
|
+
console.log(
|
|
417
|
+
`[1tube vercel-build] bundling functions from ${functionsDir} → ${outDir} ` +
|
|
418
|
+
`(runtime ${runtime}, route /${normalizePrefix(pathPrefix)}/<name>)`,
|
|
419
|
+
);
|
|
420
|
+
const fmt = (n: number) => {
|
|
421
|
+
if (n < 1024) return `${n}B`;
|
|
422
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
423
|
+
return `${(n / (1024 * 1024)).toFixed(2)}MB`;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const result = await buildVercel({
|
|
428
|
+
functionsDir,
|
|
429
|
+
outDir,
|
|
430
|
+
...(configPath ? { configPath } : {}),
|
|
431
|
+
...(only ? { only } : {}),
|
|
432
|
+
sourcemap,
|
|
433
|
+
minify,
|
|
434
|
+
pathPrefix,
|
|
435
|
+
runtime,
|
|
436
|
+
defaultMaxDuration,
|
|
437
|
+
maxDurationCap,
|
|
438
|
+
onProgress(event) {
|
|
439
|
+
if (event.phase === "bundle-start") {
|
|
440
|
+
console.log(
|
|
441
|
+
`[1tube vercel-build] bundling ${event.functions} function(s) for Vercel Node…`,
|
|
442
|
+
);
|
|
443
|
+
} else if (event.phase === "bundle-complete") {
|
|
444
|
+
console.log(
|
|
445
|
+
`[1tube vercel-build] bundled ${event.functions} function(s) and ${event.chunks} chunk(s) in ${
|
|
446
|
+
event.durationMs.toFixed(0)
|
|
447
|
+
}ms`,
|
|
448
|
+
);
|
|
449
|
+
} else if (event.phase === "emit-function") {
|
|
450
|
+
console.log(
|
|
451
|
+
`[1tube vercel-build] /${event.route} → ${
|
|
452
|
+
fmt(event.bytes)
|
|
453
|
+
} (maxDuration ${event.maxDuration}s)`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const totalBytes = result.functions.reduce((acc, f) => acc + f.byteLength, 0);
|
|
460
|
+
console.log(
|
|
461
|
+
`[1tube vercel-build] emitted ${result.functions.length} function(s) ` +
|
|
462
|
+
`(${fmt(totalBytes)}) into ${result.functionsRoot} in ${
|
|
463
|
+
result.durationMs.toFixed(0)
|
|
464
|
+
}ms`,
|
|
465
|
+
);
|
|
466
|
+
console.log(
|
|
467
|
+
`[1tube vercel-build] deploy with: vercel deploy --prebuilt`,
|
|
468
|
+
);
|
|
469
|
+
return 0;
|
|
470
|
+
} catch (err) {
|
|
471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
+
console.error(`[1tube vercel-build] FAILED: ${msg}`);
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const VERCEL_BUILD_USAGE = `Usage: 1tube build --target vercel [options]
|
|
478
|
+
|
|
479
|
+
Emits Vercel Build Output API function artifacts under <out>/functions/<prefix>/.
|
|
480
|
+
Merges into an existing .vercel/output (e.g. from \`vercel build\`).
|
|
481
|
+
|
|
482
|
+
Options:
|
|
483
|
+
-f, --functions <path> Function source dir (default: ./supabase/functions)
|
|
484
|
+
-o, --out <path> Build Output root (default: ./.vercel/output)
|
|
485
|
+
--only A,B,C Build only the named subset
|
|
486
|
+
--sourcemap MODE none | linked (default) | inline
|
|
487
|
+
--minify Minify bundles
|
|
488
|
+
--path-prefix <p> Route prefix under functions/ (default: functions/v1)
|
|
489
|
+
--runtime <id> Vercel runtime (default: nodejs24.x)
|
|
490
|
+
--max-duration <s> Fallback maxDuration seconds when no 1tube.json
|
|
491
|
+
timeoutMs is set (default: 300)
|
|
492
|
+
--max-duration-cap <s> Hard cap for maxDuration seconds (default: 800)
|
|
493
|
+
--config <path> Explicit deno.json path for the import map.
|
|
494
|
+
Default: auto-detect cwd/deno.json[c] then
|
|
495
|
+
<functions-dir>/deno.json[c] (Supabase layout).
|
|
496
|
+
--no-config Skip the import map entirely.
|
|
497
|
+
-h, --help Show this help`;
|
package/src/cli.ts
CHANGED
|
@@ -36,7 +36,7 @@ if (arg0 === "build") {
|
|
|
36
36
|
|
|
37
37
|
Commands:
|
|
38
38
|
serve Run the gateway (default when no command given)
|
|
39
|
-
build Bundle functions
|
|
39
|
+
build Bundle functions for a target (--target workerd|vercel)
|
|
40
40
|
package Build and/or wrap dist/ into a signed .1tube firmware payload
|
|
41
41
|
--version Print version and exit
|
|
42
42
|
--help Print this help
|