1tube 0.1.25 → 0.1.27
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/deno.lock +5 -5
- package/package.json +2 -2
- package/src/backends/deno/dep-graph.ts +127 -17
- package/src/backends/deno/hot-reloader.ts +55 -20
- package/src/backends/deno/source-rewriter.ts +190 -44
- package/src/backends/deno/worker-host.ts +126 -27
- package/src/server.ts +485 -141
package/deno.lock
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"npm:es-module-lexer@2": "2.1.0",
|
|
15
15
|
"npm:esbuild@0.28": "0.28.0",
|
|
16
16
|
"npm:esbuild@~0.25.5": "0.25.12",
|
|
17
|
-
"npm:hono@4": "4.12.
|
|
18
|
-
"npm:hono@^4.12.
|
|
17
|
+
"npm:hono@4": "4.12.18",
|
|
18
|
+
"npm:hono@^4.12.18": "4.12.18"
|
|
19
19
|
},
|
|
20
20
|
"jsr": {
|
|
21
21
|
"@deno/esbuild-plugin@1.2.1": {
|
|
@@ -387,8 +387,8 @@
|
|
|
387
387
|
"scripts": true,
|
|
388
388
|
"bin": true
|
|
389
389
|
},
|
|
390
|
-
"hono@4.12.
|
|
391
|
-
"integrity": "sha512-
|
|
390
|
+
"hono@4.12.18": {
|
|
391
|
+
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="
|
|
392
392
|
}
|
|
393
393
|
},
|
|
394
394
|
"workspace": {
|
|
@@ -404,7 +404,7 @@
|
|
|
404
404
|
],
|
|
405
405
|
"packageJson": {
|
|
406
406
|
"dependencies": [
|
|
407
|
-
"npm:hono@^4.12.
|
|
407
|
+
"npm:hono@^4.12.18"
|
|
408
408
|
]
|
|
409
409
|
}
|
|
410
410
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "1tube",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"prepublishOnly": "bun run typecheck && bun run check && bun run smoke:bin"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"hono": "^4.12.
|
|
45
|
+
"hono": "^4.12.18"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -40,6 +40,8 @@ export interface DepGraphOptions {
|
|
|
40
40
|
* only relative imports are tracked precisely.
|
|
41
41
|
*/
|
|
42
42
|
importMap?: Readonly<Record<string, string>>;
|
|
43
|
+
/** Parsed `scopes` field from the host project's deno.json import map. */
|
|
44
|
+
importMapScopes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
43
45
|
/**
|
|
44
46
|
* Absolute path of the deno.json the import map came from. Import
|
|
45
47
|
* map values are resolved relative to this. Required when
|
|
@@ -56,6 +58,16 @@ export interface DepGraph {
|
|
|
56
58
|
* burst with a half-written file doesn't crash the watcher.
|
|
57
59
|
*/
|
|
58
60
|
refresh(name: string, entryFileUrl: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Build one module graph for many function entries and populate each
|
|
63
|
+
* function's reachable file set from that shared graph. Initial eager boot
|
|
64
|
+
* uses this when shared-module rewriting needs every function's file list:
|
|
65
|
+
* it avoids N overlapping `deno_graph` walks over the same `_shared`, npm,
|
|
66
|
+
* and JSR dependencies.
|
|
67
|
+
*/
|
|
68
|
+
refreshMany(
|
|
69
|
+
entries: Iterable<{ name: string; entryFileUrl: string }>,
|
|
70
|
+
): Promise<void>;
|
|
59
71
|
/** Drop a function's graph. Used when an `index.ts` is deleted. */
|
|
60
72
|
forget(name: string): void;
|
|
61
73
|
/**
|
|
@@ -113,6 +125,36 @@ function applyImportMap(
|
|
|
113
125
|
return resolveAgainst(target, base);
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
function resolveImportMap(
|
|
129
|
+
specifier: string,
|
|
130
|
+
referrer: string,
|
|
131
|
+
importMap: Readonly<Record<string, string>> | undefined,
|
|
132
|
+
scopes:
|
|
133
|
+
| Readonly<Record<string, Readonly<Record<string, string>>>>
|
|
134
|
+
| undefined,
|
|
135
|
+
base: string,
|
|
136
|
+
): string | null {
|
|
137
|
+
let bestScope: string | null = null;
|
|
138
|
+
if (scopes) {
|
|
139
|
+
for (const rawScope of Object.keys(scopes)) {
|
|
140
|
+
const scopeUrl = resolveAgainst(rawScope, base);
|
|
141
|
+
if (!referrer.startsWith(scopeUrl)) continue;
|
|
142
|
+
if (bestScope === null || scopeUrl.length > bestScope.length) {
|
|
143
|
+
bestScope = scopeUrl;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (bestScope !== null && scopes) {
|
|
148
|
+
for (const rawScope of Object.keys(scopes)) {
|
|
149
|
+
if (resolveAgainst(rawScope, base) !== bestScope) continue;
|
|
150
|
+
const scoped = applyImportMap(specifier, scopes[rawScope], base);
|
|
151
|
+
if (scoped !== null) return scoped;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return importMap ? applyImportMap(specifier, importMap, base) : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
116
158
|
function resolveAgainst(target: string, base: string): string {
|
|
117
159
|
// Bare URLs (npm:/jsr:/https:/file:) pass through unchanged. Relative
|
|
118
160
|
// paths get resolved against the import map's deno.json directory so
|
|
@@ -127,11 +169,18 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
127
169
|
const reverse = new Map<string, Set<string>>();
|
|
128
170
|
|
|
129
171
|
const importMap = options?.importMap;
|
|
172
|
+
const importMapScopes = options?.importMapScopes;
|
|
130
173
|
const importMapBase = options?.importMapBase;
|
|
131
174
|
|
|
132
|
-
const resolve =
|
|
175
|
+
const resolve = importMapBase && (importMap || importMapScopes)
|
|
133
176
|
? (specifier: string, referrer: string): string => {
|
|
134
|
-
const rewritten =
|
|
177
|
+
const rewritten = resolveImportMap(
|
|
178
|
+
specifier,
|
|
179
|
+
referrer,
|
|
180
|
+
importMap,
|
|
181
|
+
importMapScopes,
|
|
182
|
+
importMapBase,
|
|
183
|
+
);
|
|
135
184
|
if (rewritten !== null) return rewritten;
|
|
136
185
|
// Fall through to standard resolution against the referrer.
|
|
137
186
|
try {
|
|
@@ -158,6 +207,41 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
158
207
|
if (set.size === 0) reverse.delete(fileUrl);
|
|
159
208
|
}
|
|
160
209
|
|
|
210
|
+
function replaceGraph(name: string, fresh: Set<string>): void {
|
|
211
|
+
const old = perName.get(name);
|
|
212
|
+
if (old) {
|
|
213
|
+
for (const f of old) {
|
|
214
|
+
if (!fresh.has(f)) indexRemove(name, f);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const f of fresh) {
|
|
218
|
+
if (!old || !old.has(f)) indexAdd(name, f);
|
|
219
|
+
}
|
|
220
|
+
perName.set(name, fresh);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function minimalGraph(entry: string): Set<string> {
|
|
224
|
+
return new Set<string>([entry]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fileDepsOf(mod: unknown): string[] {
|
|
228
|
+
const deps = (mod as { dependencies?: unknown[] })?.dependencies;
|
|
229
|
+
if (!Array.isArray(deps)) return [];
|
|
230
|
+
const out: string[] = [];
|
|
231
|
+
for (const dep of deps) {
|
|
232
|
+
const d = dep as {
|
|
233
|
+
code?: { specifier?: unknown };
|
|
234
|
+
type?: { specifier?: unknown };
|
|
235
|
+
};
|
|
236
|
+
for (const target of [d.code?.specifier, d.type?.specifier]) {
|
|
237
|
+
if (typeof target === "string" && target.startsWith("file://")) {
|
|
238
|
+
out.push(target);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
161
245
|
return {
|
|
162
246
|
async refresh(name, entryFileUrl) {
|
|
163
247
|
const entry = normalize(entryFileUrl);
|
|
@@ -169,13 +253,7 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
169
253
|
// disk; deno_graph throws. Drop the graph for `name` so the
|
|
170
254
|
// affected-set reverts to "this entry only" and let the next
|
|
171
255
|
// (clean) save rebuild it. Better than dying.
|
|
172
|
-
|
|
173
|
-
if (old) {
|
|
174
|
-
for (const f of old) indexRemove(name, f);
|
|
175
|
-
}
|
|
176
|
-
const minimal = new Set<string>([entry]);
|
|
177
|
-
perName.set(name, minimal);
|
|
178
|
-
indexAdd(name, entry);
|
|
256
|
+
replaceGraph(name, minimalGraph(entry));
|
|
179
257
|
return;
|
|
180
258
|
}
|
|
181
259
|
|
|
@@ -189,17 +267,49 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
189
267
|
// Always include the entry, even if deno_graph short-circuited
|
|
190
268
|
// due to a load error on a child.
|
|
191
269
|
fresh.add(entry);
|
|
270
|
+
replaceGraph(name, fresh);
|
|
271
|
+
},
|
|
192
272
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
273
|
+
async refreshMany(entries) {
|
|
274
|
+
const normalized = [...entries].map((e) => ({
|
|
275
|
+
name: e.name,
|
|
276
|
+
entry: normalize(e.entryFileUrl),
|
|
277
|
+
}));
|
|
278
|
+
if (normalized.length === 0) return;
|
|
279
|
+
|
|
280
|
+
let graph;
|
|
281
|
+
try {
|
|
282
|
+
graph = await createGraph(
|
|
283
|
+
normalized.map((e) => e.entry),
|
|
284
|
+
resolve ? { resolve } : undefined,
|
|
285
|
+
);
|
|
286
|
+
} catch {
|
|
287
|
+
for (const e of normalized) replaceGraph(e.name, minimalGraph(e.entry));
|
|
288
|
+
return;
|
|
198
289
|
}
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
|
|
291
|
+
const modules = new Map<string, unknown>();
|
|
292
|
+
const adjacency = new Map<string, string[]>();
|
|
293
|
+
for (const mod of graph.modules ?? []) {
|
|
294
|
+
const spec = (mod as { specifier?: unknown })?.specifier;
|
|
295
|
+
if (typeof spec !== "string" || !spec.startsWith("file://")) continue;
|
|
296
|
+
modules.set(spec, mod);
|
|
297
|
+
adjacency.set(spec, fileDepsOf(mod));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const e of normalized) {
|
|
301
|
+
const fresh = new Set<string>([e.entry]);
|
|
302
|
+
const stack = [e.entry];
|
|
303
|
+
while (stack.length > 0) {
|
|
304
|
+
const current = stack.pop()!;
|
|
305
|
+
for (const dep of adjacency.get(current) ?? []) {
|
|
306
|
+
if (fresh.has(dep)) continue;
|
|
307
|
+
fresh.add(dep);
|
|
308
|
+
if (modules.has(dep)) stack.push(dep);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
replaceGraph(e.name, fresh);
|
|
201
312
|
}
|
|
202
|
-
perName.set(name, fresh);
|
|
203
313
|
},
|
|
204
314
|
|
|
205
315
|
filesFor(name) {
|
|
@@ -32,6 +32,7 @@ import type { DenoWorkerHost, ReloadSummary } from "./worker-host.ts";
|
|
|
32
32
|
import type { DepGraph } from "./dep-graph.ts";
|
|
33
33
|
import type { DenoSharedRuntime } from "./shared-runtime.ts";
|
|
34
34
|
import type { RewriteCache } from "./source-rewriter.ts";
|
|
35
|
+
import { resolve } from "node:path";
|
|
35
36
|
|
|
36
37
|
export type FsEventStream = AsyncIterable<{ paths: readonly string[] }> & {
|
|
37
38
|
close(): void;
|
|
@@ -159,31 +160,30 @@ export function createDenoReloadDebouncer(
|
|
|
159
160
|
* changed file.
|
|
160
161
|
* 2. Files matching `<functionsDir>/<name>/index.ts`: include `<name>`.
|
|
161
162
|
* Catches the entry's own edits.
|
|
162
|
-
* 3.
|
|
163
|
-
*
|
|
164
|
-
* other rules miss:
|
|
163
|
+
* 3. Optionally, path is the directory `<functionsDir>/<name>` itself. This is
|
|
164
|
+
* necessary for two cases the other rules miss:
|
|
165
165
|
* - **Linux**: `Deno.watchFs` is recursive only over the dirs
|
|
166
166
|
* that existed at start. A *new* function dir reports a single
|
|
167
167
|
* `<functionsDir>/<name>` create event — child file events
|
|
168
168
|
* never fire because inotify doesn't auto-watch the new subdir.
|
|
169
169
|
* - **Windows**: deleting a function dir tree emits dir-path
|
|
170
170
|
* events instead of `index.ts` events.
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
171
|
+
* Callers that can stat the candidate directory should disable this
|
|
172
|
+
* fallback and decide new/removed/existing precisely. A plain parent-dir
|
|
173
|
+
* event can also be emitted for a child sidecar edit, and reloading that
|
|
174
|
+
* function would be noisy when the dep graph has no edge to the file.
|
|
175
175
|
*/
|
|
176
176
|
export function computeAffected(
|
|
177
177
|
paths: readonly string[],
|
|
178
178
|
depGraph: DepGraph,
|
|
179
|
-
options: { functionsDir?: string } = {},
|
|
179
|
+
options: { functionsDir?: string; includeDirectoryEvents?: boolean } = {},
|
|
180
180
|
): Set<string> {
|
|
181
181
|
const out = depGraph.affected(paths);
|
|
182
182
|
for (const p of paths) {
|
|
183
183
|
const m = /[\\/]([^\\/]+)[\\/]index\.ts$/.exec(p);
|
|
184
184
|
if (m && m[1]) out.add(m[1]);
|
|
185
185
|
}
|
|
186
|
-
if (options.functionsDir) {
|
|
186
|
+
if (options.functionsDir && options.includeDirectoryEvents !== false) {
|
|
187
187
|
const fnDir = options.functionsDir.replace(/[\\/]$/, "");
|
|
188
188
|
for (const p of paths) {
|
|
189
189
|
// Strip the prefix; anything left should look like
|
|
@@ -194,7 +194,8 @@ export function computeAffected(
|
|
|
194
194
|
else if (p.startsWith(fnDir + "\\")) rest = p.slice(fnDir.length + 1);
|
|
195
195
|
else if (p === fnDir) continue;
|
|
196
196
|
if (rest === null) continue;
|
|
197
|
-
|
|
197
|
+
if (/[\\/]/.test(rest)) continue;
|
|
198
|
+
const seg = rest;
|
|
198
199
|
// Skip private/shared dirs the host already filters out — keeps
|
|
199
200
|
// the affected set tight even before `reload()` runs.
|
|
200
201
|
if (!seg || seg.startsWith("_") || seg.endsWith("_shared")) continue;
|
|
@@ -204,13 +205,32 @@ export function computeAffected(
|
|
|
204
205
|
return out;
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
function directoryEventFunctionNames(
|
|
209
|
+
paths: readonly string[],
|
|
210
|
+
functionsDir: string,
|
|
211
|
+
): Set<string> {
|
|
212
|
+
const out = new Set<string>();
|
|
213
|
+
const fnDir = functionsDir.replace(/[\\/]$/, "");
|
|
214
|
+
for (const p of paths) {
|
|
215
|
+
let rest: string | null = null;
|
|
216
|
+
if (p.startsWith(fnDir + "/")) rest = p.slice(fnDir.length + 1);
|
|
217
|
+
else if (p.startsWith(fnDir + "\\")) rest = p.slice(fnDir.length + 1);
|
|
218
|
+
else continue;
|
|
219
|
+
if (!rest || /[\\/]/.test(rest)) continue;
|
|
220
|
+
if (rest.startsWith("_") || rest.endsWith("_shared")) continue;
|
|
221
|
+
out.add(rest);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
207
226
|
export function createDenoHotReloader(
|
|
208
227
|
opts: DenoHotReloaderOptions,
|
|
209
228
|
): DenoHotReloader {
|
|
210
229
|
const log = opts.log ?? ((line) => console.log(line));
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
const functionsDir = resolve(opts.functionsDir);
|
|
231
|
+
const watcherFactory = opts.watch ??
|
|
232
|
+
((dir: string) =>
|
|
233
|
+
Deno.watchFs(dir, { recursive: true }) as unknown as FsEventStream);
|
|
214
234
|
|
|
215
235
|
let stream: FsEventStream | null = null;
|
|
216
236
|
let stopped = false;
|
|
@@ -252,7 +272,8 @@ export function createDenoHotReloader(
|
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
const raw = computeAffected(paths, opts.host.depGraph, {
|
|
255
|
-
functionsDir
|
|
275
|
+
functionsDir,
|
|
276
|
+
includeDirectoryEvents: false,
|
|
256
277
|
});
|
|
257
278
|
// Files that aren't owned by any single function (shared
|
|
258
279
|
// modules, anything imported by ≥2 functions) need every
|
|
@@ -265,6 +286,17 @@ export function createDenoHotReloader(
|
|
|
265
286
|
// `functionsDir` (README.md, .gitignore, …) so we don't spend
|
|
266
287
|
// a reload pass producing an empty summary.
|
|
267
288
|
const known = new Set(opts.host.list());
|
|
289
|
+
for (
|
|
290
|
+
const name of directoryEventFunctionNames(paths, functionsDir)
|
|
291
|
+
) {
|
|
292
|
+
const indexPath = `${functionsDir}/${name}/index.ts`;
|
|
293
|
+
try {
|
|
294
|
+
const stat = await Deno.stat(indexPath);
|
|
295
|
+
if (stat.isFile && !known.has(name)) raw.add(name);
|
|
296
|
+
} catch {
|
|
297
|
+
if (known.has(name)) raw.add(name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
268
300
|
const affected = new Set<string>();
|
|
269
301
|
for (const name of raw) {
|
|
270
302
|
if (known.has(name)) {
|
|
@@ -272,7 +304,7 @@ export function createDenoHotReloader(
|
|
|
272
304
|
continue;
|
|
273
305
|
}
|
|
274
306
|
try {
|
|
275
|
-
const indexPath = `${
|
|
307
|
+
const indexPath = `${functionsDir}/${name}/index.ts`;
|
|
276
308
|
const stat = await Deno.stat(indexPath);
|
|
277
309
|
if (stat.isFile) affected.add(name);
|
|
278
310
|
} catch {
|
|
@@ -307,16 +339,18 @@ export function createDenoHotReloader(
|
|
|
307
339
|
opts.onReloaded?.(summary);
|
|
308
340
|
} catch (err) {
|
|
309
341
|
const msg = err instanceof Error ? err.message : String(err);
|
|
310
|
-
log(
|
|
342
|
+
log(
|
|
343
|
+
`[1tube] HMR reload FAILED — keeping previous workers alive. Error: ${msg}`,
|
|
344
|
+
);
|
|
311
345
|
}
|
|
312
346
|
},
|
|
313
347
|
});
|
|
314
348
|
|
|
315
349
|
return {
|
|
316
|
-
|
|
317
|
-
if (stream) return;
|
|
318
|
-
stream = watcherFactory(
|
|
319
|
-
log(`[1tube] HMR watching: ${
|
|
350
|
+
start() {
|
|
351
|
+
if (stream) return Promise.resolve();
|
|
352
|
+
stream = watcherFactory(functionsDir);
|
|
353
|
+
log(`[1tube] HMR watching: ${functionsDir}`);
|
|
320
354
|
consumeLoop = (async () => {
|
|
321
355
|
try {
|
|
322
356
|
for await (const event of stream!) {
|
|
@@ -328,6 +362,7 @@ export function createDenoHotReloader(
|
|
|
328
362
|
if (!stopped) log(`[1tube] HMR watcher disabled: ${err}`);
|
|
329
363
|
}
|
|
330
364
|
})();
|
|
365
|
+
return Promise.resolve();
|
|
331
366
|
},
|
|
332
367
|
async stop() {
|
|
333
368
|
if (stopped) return;
|