1tube 0.1.48 → 0.1.50
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/env-hot-reloader.ts +170 -0
- package/src/env-file.ts +254 -0
- package/src/launch.ts +8 -0
- package/src/server.ts +51 -0
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches `--env-file` / `.env` paths and hot-applies secret changes.
|
|
3
|
+
*
|
|
4
|
+
* Deno only loads `--env-file` once at process start. With `--hmr`, a
|
|
5
|
+
* save to `.env` must:
|
|
6
|
+
* 1. Re-parse the file(s) into `Deno.env`
|
|
7
|
+
* 2. Respawn function workers / workerd so top-level env captures
|
|
8
|
+
* and workerd `fromEnvironment` bindings see the new values
|
|
9
|
+
*
|
|
10
|
+
* The functions-dir HMR loop never sees these paths (they live at the
|
|
11
|
+
* project root), so this watcher runs alongside it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
applyEnvFiles,
|
|
16
|
+
type EnvApplyDiff,
|
|
17
|
+
type EnvSource,
|
|
18
|
+
envFileWatchDirs,
|
|
19
|
+
isEnvFileEvent,
|
|
20
|
+
} from "../env-file.ts";
|
|
21
|
+
import type { FsEventStream } from "./deno/hot-reloader.ts";
|
|
22
|
+
|
|
23
|
+
export interface EnvHotReloaderOptions {
|
|
24
|
+
/** Absolute paths of env files to watch (order = precedence). */
|
|
25
|
+
envFiles: readonly string[];
|
|
26
|
+
/** Called after a real env diff is applied. Should reload workers. */
|
|
27
|
+
onChanged: (diff: EnvApplyDiff) => void | Promise<void>;
|
|
28
|
+
debounceMs?: number;
|
|
29
|
+
leadingMs?: number;
|
|
30
|
+
/** Test seam: inject a fake watcher. */
|
|
31
|
+
watch?: (dirs: readonly string[]) => FsEventStream;
|
|
32
|
+
setTimer?: (cb: () => void, ms: number) => number;
|
|
33
|
+
clearTimer?: (id: number) => void;
|
|
34
|
+
log?: (line: string) => void;
|
|
35
|
+
/** Test seam: alternate env target (defaults to Deno.env). */
|
|
36
|
+
env?: EnvSource;
|
|
37
|
+
/** Test seam: override apply. */
|
|
38
|
+
apply?: (files: readonly string[], env: EnvSource) => EnvApplyDiff;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EnvHotReloader {
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
stop(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createEnvHotReloader(
|
|
47
|
+
opts: EnvHotReloaderOptions,
|
|
48
|
+
): EnvHotReloader {
|
|
49
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
50
|
+
const envFiles = [...opts.envFiles];
|
|
51
|
+
const env = opts.env ?? Deno.env;
|
|
52
|
+
const apply = opts.apply ?? applyEnvFiles;
|
|
53
|
+
const setTimer = opts.setTimer ??
|
|
54
|
+
((cb, ms) => setTimeout(cb, ms) as unknown as number);
|
|
55
|
+
const clearTimer = opts.clearTimer ?? ((id) => clearTimeout(id));
|
|
56
|
+
const debounceMs = opts.debounceMs ?? 200;
|
|
57
|
+
const leadingMs = opts.leadingMs !== undefined && opts.leadingMs > 0
|
|
58
|
+
? opts.leadingMs
|
|
59
|
+
: 40;
|
|
60
|
+
|
|
61
|
+
const watcherFactory = opts.watch ??
|
|
62
|
+
((dirs: readonly string[]) =>
|
|
63
|
+
Deno.watchFs([...dirs], { recursive: false }) as unknown as FsEventStream);
|
|
64
|
+
|
|
65
|
+
let stream: FsEventStream | null = null;
|
|
66
|
+
let stopped = false;
|
|
67
|
+
let consumeLoop: Promise<void> | null = null;
|
|
68
|
+
let timerId: number | null = null;
|
|
69
|
+
let flushing = false;
|
|
70
|
+
let pending = false;
|
|
71
|
+
|
|
72
|
+
const flush = async () => {
|
|
73
|
+
timerId = null;
|
|
74
|
+
if (flushing) return;
|
|
75
|
+
if (!pending) return;
|
|
76
|
+
pending = false;
|
|
77
|
+
flushing = true;
|
|
78
|
+
try {
|
|
79
|
+
const diff = apply(envFiles, env);
|
|
80
|
+
if (!diff.changed) return;
|
|
81
|
+
const parts: string[] = [];
|
|
82
|
+
if (diff.updated.length > 0) {
|
|
83
|
+
parts.push(`updated=${diff.updated.join(",")}`);
|
|
84
|
+
}
|
|
85
|
+
if (diff.added.length > 0) parts.push(`added=${diff.added.join(",")}`);
|
|
86
|
+
if (diff.removed.length > 0) {
|
|
87
|
+
parts.push(`removed=${diff.removed.join(",")}`);
|
|
88
|
+
}
|
|
89
|
+
log(
|
|
90
|
+
`[1tube] HMR env file changed (${parts.join("; ") || "secrets"}) � reloading functions`,
|
|
91
|
+
);
|
|
92
|
+
await opts.onChanged(diff);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
log(`[1tube] HMR env reload FAILED: ${msg}`);
|
|
96
|
+
} finally {
|
|
97
|
+
flushing = false;
|
|
98
|
+
if (pending) {
|
|
99
|
+
timerId = setTimer(flush, debounceMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const schedule = () => {
|
|
105
|
+
pending = true;
|
|
106
|
+
if (flushing) return;
|
|
107
|
+
if (timerId !== null) {
|
|
108
|
+
// Leading-edge: first event of a quiet period already armed �
|
|
109
|
+
// don't keep pushing the flush out.
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
timerId = setTimer(flush, leadingMs);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
start() {
|
|
117
|
+
if (stream || envFiles.length === 0) return Promise.resolve();
|
|
118
|
+
const dirs = envFileWatchDirs(envFiles).filter((d) => {
|
|
119
|
+
try {
|
|
120
|
+
return Deno.statSync(d).isDirectory;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (dirs.length === 0) return Promise.resolve();
|
|
126
|
+
|
|
127
|
+
// Seed ownership from the current files so a subsequent delete of
|
|
128
|
+
// a key from `.env` can be removed from Deno.env on the next save.
|
|
129
|
+
apply(envFiles, env);
|
|
130
|
+
|
|
131
|
+
stream = watcherFactory(dirs);
|
|
132
|
+
log(
|
|
133
|
+
`[1tube] HMR watching env: ${envFiles.join(", ")}`,
|
|
134
|
+
);
|
|
135
|
+
consumeLoop = (async () => {
|
|
136
|
+
try {
|
|
137
|
+
for await (const event of stream!) {
|
|
138
|
+
if (stopped) break;
|
|
139
|
+
if (event.paths.length === 0) continue;
|
|
140
|
+
if (!isEnvFileEvent(event.paths, envFiles)) continue;
|
|
141
|
+
schedule();
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (!stopped) log(`[1tube] HMR env watcher disabled: ${err}`);
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
return Promise.resolve();
|
|
148
|
+
},
|
|
149
|
+
async stop() {
|
|
150
|
+
if (stopped) return;
|
|
151
|
+
stopped = true;
|
|
152
|
+
if (timerId !== null) {
|
|
153
|
+
clearTimer(timerId);
|
|
154
|
+
timerId = null;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
stream?.close();
|
|
158
|
+
} catch { /* */ }
|
|
159
|
+
stream = null;
|
|
160
|
+
if (consumeLoop) {
|
|
161
|
+
let timeoutId: number | undefined;
|
|
162
|
+
const bound = new Promise<void>((resolve) => {
|
|
163
|
+
timeoutId = setTimeout(resolve, 250) as unknown as number;
|
|
164
|
+
});
|
|
165
|
+
await Promise.race([consumeLoop, bound]);
|
|
166
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/env-file.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dotenv parse + apply helpers for HMR secret reloads.
|
|
3
|
+
*
|
|
4
|
+
* Deno's `--env-file` only loads once at process start. When `--hmr` is
|
|
5
|
+
* on we re-read the same files on change, push values into `Deno.env`,
|
|
6
|
+
* and ask the hot-reloader to respawn workers / workerd so top-level
|
|
7
|
+
* `const KEY = Deno.env.get(...)` captures pick up the new secrets.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
export type EnvSource = {
|
|
13
|
+
get(key: string): string | undefined;
|
|
14
|
+
set(key: string, value: string): void;
|
|
15
|
+
delete(key: string): void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface EnvApplyDiff {
|
|
19
|
+
/** Absolute paths that were successfully read. */
|
|
20
|
+
files: string[];
|
|
21
|
+
added: string[];
|
|
22
|
+
updated: string[];
|
|
23
|
+
removed: string[];
|
|
24
|
+
/** True when at least one key was added, updated, or removed. */
|
|
25
|
+
changed: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keys currently owned by watched env files (union across all files). */
|
|
29
|
+
const ownedKeys = new Set<string>();
|
|
30
|
+
|
|
31
|
+
/** Test seam: reset owned-key tracking between tests. */
|
|
32
|
+
export function resetEnvFileOwnership(): void {
|
|
33
|
+
ownedKeys.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a dotenv-format document into an ordered map.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors Deno's `--env-file` rules closely enough for HMR:
|
|
40
|
+
* - `#` comments and blank lines are ignored
|
|
41
|
+
* - optional `export ` prefix
|
|
42
|
+
* - first occurrence of a key wins within a file
|
|
43
|
+
* - unquoted / single-quoted / double-quoted values
|
|
44
|
+
* - `\n` / `\r` / `\t` / `\\` / `\"` escapes inside double quotes
|
|
45
|
+
*/
|
|
46
|
+
export function parseEnvFile(content: string): Map<string, string> {
|
|
47
|
+
const out = new Map<string, string>();
|
|
48
|
+
const lines = content.split(/\r?\n/);
|
|
49
|
+
for (const raw of lines) {
|
|
50
|
+
const line = raw.trim();
|
|
51
|
+
if (!line || line.startsWith("#")) continue;
|
|
52
|
+
const body = line.startsWith("export ")
|
|
53
|
+
? line.slice("export ".length).trim()
|
|
54
|
+
: line;
|
|
55
|
+
const eq = body.indexOf("=");
|
|
56
|
+
if (eq <= 0) continue;
|
|
57
|
+
const key = body.slice(0, eq).trim();
|
|
58
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
59
|
+
if (out.has(key)) continue; // first wins
|
|
60
|
+
let value = body.slice(eq + 1).trim();
|
|
61
|
+
if (
|
|
62
|
+
(value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
63
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)
|
|
64
|
+
) {
|
|
65
|
+
const quote = value[0];
|
|
66
|
+
value = value.slice(1, -1);
|
|
67
|
+
if (quote === '"') {
|
|
68
|
+
value = value
|
|
69
|
+
.replace(/\\n/g, "\n")
|
|
70
|
+
.replace(/\\r/g, "\r")
|
|
71
|
+
.replace(/\\t/g, "\t")
|
|
72
|
+
.replace(/\\"/g, '"')
|
|
73
|
+
.replace(/\\\\/g, "\\");
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Unquoted: strip inline comments (`KEY=val # comment`).
|
|
77
|
+
const hash = value.indexOf(" #");
|
|
78
|
+
if (hash !== -1) value = value.slice(0, hash).trimEnd();
|
|
79
|
+
}
|
|
80
|
+
out.set(key, value);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve which env files HMR should watch.
|
|
87
|
+
*
|
|
88
|
+
* Priority:
|
|
89
|
+
* 1. Explicit list (tests / future CLI)
|
|
90
|
+
* 2. `1TUBE_ENV_FILES` (comma-separated) � set by the launcher when
|
|
91
|
+
* the operator passes `--env-file=�`
|
|
92
|
+
* 3. Convention: `.env`, `.env.local`, `.env.development` under cwd
|
|
93
|
+
* when those files already exist
|
|
94
|
+
*/
|
|
95
|
+
export function resolveWatchedEnvFiles(opts: {
|
|
96
|
+
cwd?: string;
|
|
97
|
+
explicit?: readonly string[];
|
|
98
|
+
envFilesEnv?: string | null;
|
|
99
|
+
} = {}): string[] {
|
|
100
|
+
const cwd = opts.cwd ?? Deno.cwd();
|
|
101
|
+
const collected: string[] = [];
|
|
102
|
+
if (opts.explicit) {
|
|
103
|
+
for (const p of opts.explicit) collected.push(p);
|
|
104
|
+
}
|
|
105
|
+
const fromEnv = opts.envFilesEnv === null
|
|
106
|
+
? ""
|
|
107
|
+
: (opts.envFilesEnv ?? Deno.env.get("1TUBE_ENV_FILES") ?? "");
|
|
108
|
+
for (const part of fromEnv.split(",")) {
|
|
109
|
+
const t = part.trim();
|
|
110
|
+
if (t) collected.push(t);
|
|
111
|
+
}
|
|
112
|
+
if (collected.length === 0) {
|
|
113
|
+
for (const name of [".env", ".env.local", ".env.development"]) {
|
|
114
|
+
const candidate = resolve(cwd, name);
|
|
115
|
+
try {
|
|
116
|
+
if (Deno.statSync(candidate).isFile) collected.push(candidate);
|
|
117
|
+
} catch {
|
|
118
|
+
// absent � skip
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const seen = new Set<string>();
|
|
123
|
+
const out: string[] = [];
|
|
124
|
+
for (const p of collected) {
|
|
125
|
+
const abs = isAbsolute(p) ? resolve(p) : resolve(cwd, p);
|
|
126
|
+
if (seen.has(abs)) continue;
|
|
127
|
+
seen.add(abs);
|
|
128
|
+
out.push(abs);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Re-read `files` (in order; later files override earlier) and sync
|
|
135
|
+
* into `env`. Keys that disappeared from every watched file since the
|
|
136
|
+
* last apply are deleted so secret rotations/removals take effect.
|
|
137
|
+
*/
|
|
138
|
+
export function applyEnvFiles(
|
|
139
|
+
files: readonly string[],
|
|
140
|
+
env: EnvSource = Deno.env,
|
|
141
|
+
): EnvApplyDiff {
|
|
142
|
+
const merged = new Map<string, string>();
|
|
143
|
+
const readFiles: string[] = [];
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
let text: string;
|
|
146
|
+
try {
|
|
147
|
+
text = Deno.readTextFileSync(file);
|
|
148
|
+
} catch {
|
|
149
|
+
// Missing mid-edit (atomic save) � skip this file for now; the
|
|
150
|
+
// next fs event will retry. Don't wipe owned keys on a transient
|
|
151
|
+
// read failure.
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
readFiles.push(file);
|
|
155
|
+
const parsed = parseEnvFile(text);
|
|
156
|
+
// Later files win (matches Deno's last `--env-file` precedence).
|
|
157
|
+
for (const [k, v] of parsed) merged.set(k, v);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If every file failed to read, leave state alone � likely a
|
|
161
|
+
// transient atomic-rename race.
|
|
162
|
+
if (files.length > 0 && readFiles.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
files: [],
|
|
165
|
+
added: [],
|
|
166
|
+
updated: [],
|
|
167
|
+
removed: [],
|
|
168
|
+
changed: false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const added: string[] = [];
|
|
173
|
+
const updated: string[] = [];
|
|
174
|
+
const removed: string[] = [];
|
|
175
|
+
|
|
176
|
+
for (const [key, value] of merged) {
|
|
177
|
+
const prev = env.get(key);
|
|
178
|
+
if (prev === undefined) {
|
|
179
|
+
env.set(key, value);
|
|
180
|
+
added.push(key);
|
|
181
|
+
} else if (prev !== value) {
|
|
182
|
+
env.set(key, value);
|
|
183
|
+
updated.push(key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const key of ownedKeys) {
|
|
188
|
+
if (!merged.has(key)) {
|
|
189
|
+
env.delete(key);
|
|
190
|
+
removed.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ownedKeys.clear();
|
|
195
|
+
for (const key of merged.keys()) ownedKeys.add(key);
|
|
196
|
+
|
|
197
|
+
added.sort();
|
|
198
|
+
updated.sort();
|
|
199
|
+
removed.sort();
|
|
200
|
+
return {
|
|
201
|
+
files: readFiles,
|
|
202
|
+
added,
|
|
203
|
+
updated,
|
|
204
|
+
removed,
|
|
205
|
+
changed: added.length + updated.length + removed.length > 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Parent directories to feed `Deno.watchFs` (atomic renames land here). */
|
|
210
|
+
export function envFileWatchDirs(files: readonly string[]): string[] {
|
|
211
|
+
const dirs = new Set<string>();
|
|
212
|
+
for (const f of files) {
|
|
213
|
+
dirs.add(dirname(f));
|
|
214
|
+
}
|
|
215
|
+
return [...dirs];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** True when an fs event path is (or renames onto) one of our env files. */
|
|
219
|
+
export function isEnvFileEvent(
|
|
220
|
+
eventPaths: readonly string[],
|
|
221
|
+
envFiles: readonly string[],
|
|
222
|
+
): boolean {
|
|
223
|
+
if (envFiles.length === 0) return false;
|
|
224
|
+
const targets = new Set(envFiles.map((p) => resolve(p)));
|
|
225
|
+
const byDir = new Map<string, Set<string>>();
|
|
226
|
+
for (const t of targets) {
|
|
227
|
+
const dir = dirname(t);
|
|
228
|
+
const name = basename(t);
|
|
229
|
+
let set = byDir.get(dir);
|
|
230
|
+
if (!set) {
|
|
231
|
+
set = new Set();
|
|
232
|
+
byDir.set(dir, set);
|
|
233
|
+
}
|
|
234
|
+
set.add(name);
|
|
235
|
+
}
|
|
236
|
+
for (const raw of eventPaths) {
|
|
237
|
+
const p = resolve(raw);
|
|
238
|
+
if (targets.has(p)) return true;
|
|
239
|
+
// Atomic save: event may report the temp sibling or just the dir.
|
|
240
|
+
const dir = dirname(p);
|
|
241
|
+
const names = byDir.get(dir);
|
|
242
|
+
if (!names) continue;
|
|
243
|
+
if (names.has(basename(p))) return true;
|
|
244
|
+
// Directory-level events (Windows) � treat as "maybe our file".
|
|
245
|
+
try {
|
|
246
|
+
if (Deno.statSync(p).isDirectory && byDir.has(p)) return true;
|
|
247
|
+
} catch {
|
|
248
|
+
// deleted path � if its basename matched we'd already returned;
|
|
249
|
+
// a missing temp file still warrants a re-read of that dir's targets.
|
|
250
|
+
if (byDir.has(dir)) return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
package/src/launch.ts
CHANGED
|
@@ -468,6 +468,14 @@ async function runServe(args: ServeArgs): Promise<never> {
|
|
|
468
468
|
// Child env additions (merged onto the inherited environment).
|
|
469
469
|
const childEnv: Record<string, string> = {};
|
|
470
470
|
if (lockAbs) childEnv.ONETUBE_LOCK = lockAbs;
|
|
471
|
+
// Tell the gateway which `--env-file` paths to HMR-watch. Deno itself
|
|
472
|
+
// only loads them once at process start; the gateway re-applies on
|
|
473
|
+
// change so secret edits land without a full restart.
|
|
474
|
+
if (args.envFiles.length > 0) {
|
|
475
|
+
childEnv["1TUBE_ENV_FILES"] = args.envFiles
|
|
476
|
+
.map((p) => resolveToAbs(p))
|
|
477
|
+
.join(",");
|
|
478
|
+
}
|
|
471
479
|
// Deno 2.9's 24h minimum-dependency-age default makes freshly published
|
|
472
480
|
// pins fail to resolve; it must be set before the gateway's Deno starts.
|
|
473
481
|
if (
|
package/src/server.ts
CHANGED
|
@@ -74,6 +74,11 @@ import {
|
|
|
74
74
|
createRewriteCache,
|
|
75
75
|
type RewriteCache,
|
|
76
76
|
} from "./backends/deno/source-rewriter.ts";
|
|
77
|
+
import {
|
|
78
|
+
createEnvHotReloader,
|
|
79
|
+
type EnvHotReloader,
|
|
80
|
+
} from "./backends/env-hot-reloader.ts";
|
|
81
|
+
import { resolveWatchedEnvFiles } from "./env-file.ts";
|
|
77
82
|
import { createBootProgress, createScanProgress } from "./boot-progress.ts";
|
|
78
83
|
import {
|
|
79
84
|
createWorkerdWatchdog,
|
|
@@ -1227,6 +1232,7 @@ let workerdWatchdog: WorkerdWatchdog | null = null;
|
|
|
1227
1232
|
const workerdNames = new Set<string>();
|
|
1228
1233
|
let denoWorkerHost: DenoWorkerHost | null = null;
|
|
1229
1234
|
let denoHotReloader: DenoHotReloader | null = null;
|
|
1235
|
+
let envHotReloader: EnvHotReloader | null = null;
|
|
1230
1236
|
let denoSharedRuntimeRef: DenoSharedRuntime | null = null;
|
|
1231
1237
|
let denoRewriteCacheRef: RewriteCache | null = null;
|
|
1232
1238
|
|
|
@@ -1705,6 +1711,46 @@ if (opts.backend === "workerd") {
|
|
|
1705
1711
|
}
|
|
1706
1712
|
}
|
|
1707
1713
|
|
|
1714
|
+
// Env-file HMR: Deno's `--env-file` only loads at process start, and
|
|
1715
|
+
// the functions-dir watcher never sees project-root `.env` saves.
|
|
1716
|
+
// Watch the same files the launcher forwarded (via 1TUBE_ENV_FILES)
|
|
1717
|
+
// or the conventional `.env*` in cwd, re-apply into Deno.env, and
|
|
1718
|
+
// respawn every function so secret rotations land immediately.
|
|
1719
|
+
if (opts.hmr && !opts.prebuiltDir) {
|
|
1720
|
+
const envFiles = resolveWatchedEnvFiles();
|
|
1721
|
+
if (envFiles.length > 0) {
|
|
1722
|
+
envHotReloader = createEnvHotReloader({
|
|
1723
|
+
envFiles,
|
|
1724
|
+
onChanged: async () => {
|
|
1725
|
+
if (workerdBackend) {
|
|
1726
|
+
const result = await workerdBackend.reload("all");
|
|
1727
|
+
console.log(
|
|
1728
|
+
`[1tube] HMR env reload ok in ${result.durationMs.toFixed(0)}ms ` +
|
|
1729
|
+
`(gen=${result.generation}; rebundled=all)`,
|
|
1730
|
+
);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (denoWorkerHost) {
|
|
1734
|
+
const summary = await denoWorkerHost.reload(
|
|
1735
|
+
"all",
|
|
1736
|
+
"env file changed",
|
|
1737
|
+
);
|
|
1738
|
+
console.log(
|
|
1739
|
+
`[1tube] HMR env reload ok in ${summary.durationMs.toFixed(0)}ms` +
|
|
1740
|
+
(summary.reloaded.length > 0
|
|
1741
|
+
? ` (reloaded=${summary.reloaded.join(",")})`
|
|
1742
|
+
: ""),
|
|
1743
|
+
);
|
|
1744
|
+
for (const e of summary.errors) {
|
|
1745
|
+
console.log(`[1tube] HMR ${e.name}: ${e.error}`);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
await envHotReloader.start();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1708
1754
|
// ---------------------------------------------------------------------------
|
|
1709
1755
|
// HTTP Server
|
|
1710
1756
|
// ---------------------------------------------------------------------------
|
|
@@ -2480,6 +2526,11 @@ async function shutdown(reason: string) {
|
|
|
2480
2526
|
console.warn("[1tube] deno hot reloader stop() error:", err);
|
|
2481
2527
|
});
|
|
2482
2528
|
}
|
|
2529
|
+
if (envHotReloader) {
|
|
2530
|
+
envHotReloader.stop().catch((err) => {
|
|
2531
|
+
console.warn("[1tube] env hot reloader stop() error:", err);
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2483
2534
|
|
|
2484
2535
|
// Phase 1: stop accepting NEW connections. Already-accepted
|
|
2485
2536
|
// requests keep running against the still-live workerd. This is
|