@kodycodes/cli 1.23.1 → 1.24.0
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/README.md +34 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/dev/dev-bundler.d.ts +25 -0
- package/dist/dev/dev-bundler.js +51 -0
- package/dist/dev/dev-bundler.js.map +1 -0
- package/dist/dev/dev-events.d.ts +50 -0
- package/dist/dev/dev-events.js +81 -0
- package/dist/dev/dev-events.js.map +1 -0
- package/dist/dev/dev-protocol.d.ts +13 -0
- package/dist/dev/dev-protocol.js +14 -0
- package/dist/dev/dev-protocol.js.map +1 -0
- package/dist/dev/dev-watcher.d.ts +20 -0
- package/dist/dev/dev-watcher.js +108 -0
- package/dist/dev/dev-watcher.js.map +1 -0
- package/dist/dev/error-page.d.ts +21 -0
- package/dist/dev/error-page.js +64 -0
- package/dist/dev/error-page.js.map +1 -0
- package/dist/dev/live-reload.d.ts +45 -0
- package/dist/dev/live-reload.js +142 -0
- package/dist/dev/live-reload.js.map +1 -0
- package/dist/dev/process-reaper.d.ts +40 -0
- package/dist/dev/process-reaper.js +144 -0
- package/dist/dev/process-reaper.js.map +1 -0
- package/dist/dev/stack-trace.d.ts +11 -0
- package/dist/dev/stack-trace.js +56 -0
- package/dist/dev/stack-trace.js.map +1 -0
- package/dist/dev/workerd-process.d.ts +28 -0
- package/dist/dev/workerd-process.js +118 -0
- package/dist/dev/workerd-process.js.map +1 -0
- package/dist/dev-command.d.ts +2 -0
- package/dist/dev-command.js +18 -4
- package/dist/dev-command.js.map +1 -1
- package/dist/help.js +8 -1
- package/dist/help.js.map +1 -1
- package/dist/local-bundle.d.ts +43 -3
- package/dist/local-bundle.js +94 -33
- package/dist/local-bundle.js.map +1 -1
- package/dist/package-app-dev.d.ts +21 -3
- package/dist/package-app-dev.js +446 -192
- package/dist/package-app-dev.js.map +1 -1
- package/dist/package-local-runtime-source.d.ts +8 -0
- package/dist/package-local-runtime-source.js +69 -5
- package/dist/package-local-runtime-source.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watching for `kody dev`. chokidar watches the package tree (it re-arms
|
|
3
|
+
* per-directory watchers when editors or `sed -i` / `cp` replace a file with a
|
|
4
|
+
* new inode). A low-frequency stat poll over the files in the current bundle
|
|
5
|
+
* graph backs it up, so a missed event can never leave a stale bundle serving.
|
|
6
|
+
*/
|
|
7
|
+
import { watch } from 'chokidar';
|
|
8
|
+
import { stat } from 'node:fs/promises';
|
|
9
|
+
import { relative, sep } from 'node:path';
|
|
10
|
+
const defaultPollIntervalMs = 1000;
|
|
11
|
+
export function createDevWatcher(input) {
|
|
12
|
+
const ignorePrefixes = (input.ignore ?? []).map(normalizePrefix).filter(Boolean);
|
|
13
|
+
const ignored = (path) => shouldIgnoreWatchPath(relative(input.root, path), ignorePrefixes);
|
|
14
|
+
const watcher = watch(input.root, {
|
|
15
|
+
ignoreInitial: true,
|
|
16
|
+
ignored,
|
|
17
|
+
atomic: true,
|
|
18
|
+
});
|
|
19
|
+
watcher.on('all', (_event, path) => {
|
|
20
|
+
if (!ignored(path))
|
|
21
|
+
input.onChange(path);
|
|
22
|
+
});
|
|
23
|
+
// Init errors reject `ready` (awaited by start). Later errors are swallowed
|
|
24
|
+
// so a transient watch glitch cannot crash the process.
|
|
25
|
+
let readySettled = false;
|
|
26
|
+
const ready = new Promise((resolve, reject) => {
|
|
27
|
+
watcher.once('ready', () => {
|
|
28
|
+
readySettled = true;
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
watcher.on('error', (error) => {
|
|
32
|
+
if (readySettled)
|
|
33
|
+
return;
|
|
34
|
+
readySettled = true;
|
|
35
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
let tracked = new Map();
|
|
39
|
+
let polling = false;
|
|
40
|
+
const poll = async () => {
|
|
41
|
+
if (polling)
|
|
42
|
+
return;
|
|
43
|
+
polling = true;
|
|
44
|
+
try {
|
|
45
|
+
for (const [path, previous] of tracked) {
|
|
46
|
+
const next = await fingerprint(path);
|
|
47
|
+
if (!tracked.has(path))
|
|
48
|
+
continue;
|
|
49
|
+
if (previous !== null && next !== previous) {
|
|
50
|
+
tracked.set(path, next);
|
|
51
|
+
input.onChange(path);
|
|
52
|
+
}
|
|
53
|
+
else if (previous === null) {
|
|
54
|
+
tracked.set(path, next);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
polling = false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const timer = setInterval(() => void poll(), input.pollIntervalMs ?? defaultPollIntervalMs);
|
|
63
|
+
timer.unref();
|
|
64
|
+
return {
|
|
65
|
+
ready,
|
|
66
|
+
track: (paths) => {
|
|
67
|
+
const next = new Map();
|
|
68
|
+
for (const path of paths) {
|
|
69
|
+
if (ignored(path))
|
|
70
|
+
continue;
|
|
71
|
+
next.set(path, tracked.get(path) ?? null);
|
|
72
|
+
}
|
|
73
|
+
tracked = next;
|
|
74
|
+
void poll();
|
|
75
|
+
},
|
|
76
|
+
close: async () => {
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
tracked = new Map();
|
|
79
|
+
await watcher.close();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function fingerprint(path) {
|
|
84
|
+
try {
|
|
85
|
+
const info = await stat(path);
|
|
86
|
+
return `${info.ino}:${info.size}:${info.mtimeMs}`;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return 'missing';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function normalizePrefix(prefix) {
|
|
93
|
+
return prefix.replace(/^\.\//, '').replace(/\/+$/, '').split('/').join(sep);
|
|
94
|
+
}
|
|
95
|
+
export function shouldIgnoreWatchPath(relativePath, ignorePrefixes = []) {
|
|
96
|
+
if (!relativePath || relativePath === '.')
|
|
97
|
+
return false;
|
|
98
|
+
if (relativePath.startsWith('..'))
|
|
99
|
+
return true;
|
|
100
|
+
const segments = relativePath.split(sep);
|
|
101
|
+
if (segments.some((segment) => segment === 'node_modules' || segment.startsWith('.'))) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (relativePath.endsWith('.map'))
|
|
105
|
+
return true;
|
|
106
|
+
return ignorePrefixes.some((prefix) => relativePath === prefix || relativePath.startsWith(`${prefix}${sep}`));
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=dev-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-watcher.js","sourceRoot":"","sources":["../../src/dev/dev-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only error responses served by the `kody dev` proxy for build failures
|
|
3
|
+
* and uncaught handler errors. HTML for browsers, plain text for everyone else
|
|
4
|
+
* (curl, agents, `kody dev fetch`).
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
|
+
export type DevErrorKind = 'build' | 'runtime';
|
|
8
|
+
export type DevErrorView = {
|
|
9
|
+
kind: DevErrorKind;
|
|
10
|
+
title: string;
|
|
11
|
+
/** Preformatted body: diagnostics with code frames, or a mapped stack. */
|
|
12
|
+
detail: string;
|
|
13
|
+
/** `METHOD /path` for runtime errors. */
|
|
14
|
+
request?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function wantsHtml(req: IncomingMessage): boolean;
|
|
17
|
+
export declare function sendDevError(req: IncomingMessage, res: ServerResponse, status: number, view: DevErrorView, options?: {
|
|
18
|
+
htmlTail?: string;
|
|
19
|
+
}): void;
|
|
20
|
+
export declare function renderDevErrorPage(view: DevErrorView): string;
|
|
21
|
+
export declare function escapeHtml(value: string): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only error responses served by the `kody dev` proxy for build failures
|
|
3
|
+
* and uncaught handler errors. HTML for browsers, plain text for everyone else
|
|
4
|
+
* (curl, agents, `kody dev fetch`).
|
|
5
|
+
*/
|
|
6
|
+
export function wantsHtml(req) {
|
|
7
|
+
const accept = req.headers.accept ?? '';
|
|
8
|
+
return /\btext\/html\b/.test(accept);
|
|
9
|
+
}
|
|
10
|
+
export function sendDevError(req, res, status, view, options = {}) {
|
|
11
|
+
if (res.headersSent) {
|
|
12
|
+
res.end();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const headers = {
|
|
16
|
+
'cache-control': 'no-store',
|
|
17
|
+
'x-kody-dev-error': view.kind,
|
|
18
|
+
};
|
|
19
|
+
if (wantsHtml(req)) {
|
|
20
|
+
res.writeHead(status, { ...headers, 'content-type': 'text/html; charset=utf-8' });
|
|
21
|
+
res.end(renderDevErrorPage(view) + (options.htmlTail ?? ''));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
res.writeHead(status, { ...headers, 'content-type': 'text/plain; charset=utf-8' });
|
|
25
|
+
res.end(`kody dev ${view.kind} error${view.request ? ` (${view.request})` : ''}\n\n${view.detail}\n`);
|
|
26
|
+
}
|
|
27
|
+
export function renderDevErrorPage(view) {
|
|
28
|
+
const label = view.kind === 'build' ? 'Build error' : 'Runtime error';
|
|
29
|
+
return `<!doctype html>
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
34
|
+
<title>${escapeHtml(label)} · kody dev</title>
|
|
35
|
+
<style>
|
|
36
|
+
:root { color-scheme: dark; }
|
|
37
|
+
body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; background: #1a1015; color: #f5e9ee; }
|
|
38
|
+
main { max-width: 960px; margin: 0 auto; padding: 32px 24px; }
|
|
39
|
+
.label { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: #ff8fab; }
|
|
40
|
+
h1 { font-size: 20px; margin: 8px 0 20px; word-break: break-word; }
|
|
41
|
+
pre { margin: 0; padding: 16px; overflow: auto; background: #0e080b; border: 1px solid #3a2530; border-radius: 8px; font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
|
|
42
|
+
p { color: #bfa9b3; font-size: 13px; }
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<main>
|
|
47
|
+
<span class="label">${escapeHtml(label)}</span>
|
|
48
|
+
<h1>${escapeHtml(view.title)}</h1>${view.request ? `\n<p>${escapeHtml(view.request)}</p>` : ''}
|
|
49
|
+
<pre>${escapeHtml(view.detail)}</pre>
|
|
50
|
+
<p>Served by <code>kody dev</code> only; published apps never see this page.</p>
|
|
51
|
+
</main>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
export function escapeHtml(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/&/g, '&')
|
|
59
|
+
.replace(/</g, '<')
|
|
60
|
+
.replace(/>/g, '>')
|
|
61
|
+
.replace(/"/g, '"')
|
|
62
|
+
.replace(/'/g, ''');
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=error-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-page.js","sourceRoot":"","sources":["../../src/dev/error-page.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,MAAM,UAAU,SAAS,CAAC,GAAoB;IAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;IACvC,OAAO,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AACrC,CAAC;AAED,MAAM,UAAU,YAAY,CAC3B,GAAoB,EACpB,GAAmB,EACnB,MAAc,EACd,IAAkB,EAClB,UAAiC,EAAE;IAEnC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QACrB,GAAG,CAAC,GAAG,EAAE,CAAA;QACT,OAAM;IACP,CAAC;IACD,MAAM,OAAO,GAAG;QACf,eAAe,EAAE,UAAU;QAC3B,kBAAkB,EAAE,IAAI,CAAC,IAAI;KAC7B,CAAA;IACD,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;QACpB,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAA;QACjF,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAA;QAC5D,OAAM;IACP,CAAC;IACD,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,2BAA2B,EAAE,CAAC,CAAA;IAClF,GAAG,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,CAAA;AACtG,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAkB;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAA;IACrE,OAAO;;;;;SAKC,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;sBAaJ,UAAU,CAAC,KAAK,CAAC;MACjC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;OACvF,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;;;;;CAK7B,CAAA;AACD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,KAAK;SACV,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AACzB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live reload for `kody dev`: an SSE stream at {@link liveReloadEventsPath}, a
|
|
3
|
+
* tiny client injected into HTML responses that reloads after each swap, and
|
|
4
|
+
* an error overlay for build and runtime errors. No HMR: a reload re-renders
|
|
5
|
+
* the page from the new build.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
8
|
+
import type { DevErrorView } from './error-page.js';
|
|
9
|
+
export declare const devRoutePrefix = "/__kody/dev/";
|
|
10
|
+
export declare const liveReloadEventsPath = "/__kody/dev/events";
|
|
11
|
+
export declare const liveReloadClientPath = "/__kody/dev/client.js";
|
|
12
|
+
export declare const liveReloadScriptTag = "<script type=\"module\" src=\"/__kody/dev/client.js\"></script>";
|
|
13
|
+
export type LiveReloadMessage = {
|
|
14
|
+
event: 'reload';
|
|
15
|
+
data: {
|
|
16
|
+
buildId: number;
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
event: 'build_error';
|
|
20
|
+
data: DevErrorView;
|
|
21
|
+
} | {
|
|
22
|
+
event: 'runtime_error';
|
|
23
|
+
data: DevErrorView;
|
|
24
|
+
};
|
|
25
|
+
export type LiveReloadHub = {
|
|
26
|
+
/** Serve the SSE stream; sends `hello` with the current build first. */
|
|
27
|
+
handleEvents: (req: IncomingMessage, res: ServerResponse) => void;
|
|
28
|
+
broadcast: (message: LiveReloadMessage) => void;
|
|
29
|
+
clientCount: () => number;
|
|
30
|
+
close: () => void;
|
|
31
|
+
};
|
|
32
|
+
export declare function createLiveReloadHub(input: {
|
|
33
|
+
state: () => {
|
|
34
|
+
buildId: number | null;
|
|
35
|
+
error: DevErrorView | null;
|
|
36
|
+
};
|
|
37
|
+
}): LiveReloadHub;
|
|
38
|
+
/** Insert the client tag before `</head>`, else `</body>`, else at the end. */
|
|
39
|
+
export declare function injectLiveReloadScript(html: string): string;
|
|
40
|
+
export declare function shouldInjectLiveReload(input: {
|
|
41
|
+
method: string | undefined;
|
|
42
|
+
status: number;
|
|
43
|
+
headers: Record<string, string | Array<string> | number | undefined>;
|
|
44
|
+
}): boolean;
|
|
45
|
+
export declare const liveReloadClientSource: string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live reload for `kody dev`: an SSE stream at {@link liveReloadEventsPath}, a
|
|
3
|
+
* tiny client injected into HTML responses that reloads after each swap, and
|
|
4
|
+
* an error overlay for build and runtime errors. No HMR: a reload re-renders
|
|
5
|
+
* the page from the new build.
|
|
6
|
+
*/
|
|
7
|
+
export const devRoutePrefix = '/__kody/dev/';
|
|
8
|
+
export const liveReloadEventsPath = '/__kody/dev/events';
|
|
9
|
+
export const liveReloadClientPath = '/__kody/dev/client.js';
|
|
10
|
+
export const liveReloadScriptTag = `<script type="module" src="${liveReloadClientPath}"></script>`;
|
|
11
|
+
const keepAliveMs = 15_000;
|
|
12
|
+
export function createLiveReloadHub(input) {
|
|
13
|
+
const clients = new Set();
|
|
14
|
+
const send = (res, event, data) => {
|
|
15
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
16
|
+
};
|
|
17
|
+
const keepAlive = setInterval(() => {
|
|
18
|
+
for (const res of clients)
|
|
19
|
+
res.write(': keep-alive\n\n');
|
|
20
|
+
}, keepAliveMs);
|
|
21
|
+
keepAlive.unref();
|
|
22
|
+
return {
|
|
23
|
+
handleEvents: (req, res) => {
|
|
24
|
+
res.writeHead(200, {
|
|
25
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
26
|
+
'cache-control': 'no-store',
|
|
27
|
+
connection: 'keep-alive',
|
|
28
|
+
});
|
|
29
|
+
res.write('retry: 500\n\n');
|
|
30
|
+
send(res, 'hello', input.state());
|
|
31
|
+
clients.add(res);
|
|
32
|
+
req.on('close', () => clients.delete(res));
|
|
33
|
+
},
|
|
34
|
+
broadcast: (message) => {
|
|
35
|
+
for (const res of clients)
|
|
36
|
+
send(res, message.event, message.data);
|
|
37
|
+
},
|
|
38
|
+
clientCount: () => clients.size,
|
|
39
|
+
close: () => {
|
|
40
|
+
clearInterval(keepAlive);
|
|
41
|
+
for (const res of clients)
|
|
42
|
+
res.end();
|
|
43
|
+
clients.clear();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Insert the client tag before `</head>`, else `</body>`, else at the end. */
|
|
48
|
+
export function injectLiveReloadScript(html) {
|
|
49
|
+
if (html.includes(liveReloadClientPath))
|
|
50
|
+
return html;
|
|
51
|
+
for (const pattern of [/<\/head\s*>/i, /<\/body\s*>/i]) {
|
|
52
|
+
const match = pattern.exec(html);
|
|
53
|
+
if (match) {
|
|
54
|
+
return `${html.slice(0, match.index)}${liveReloadScriptTag}${html.slice(match.index)}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return `${html}${liveReloadScriptTag}`;
|
|
58
|
+
}
|
|
59
|
+
export function shouldInjectLiveReload(input) {
|
|
60
|
+
if (input.method === 'HEAD')
|
|
61
|
+
return false;
|
|
62
|
+
if (input.status === 204 || input.status === 304 || input.status < 200)
|
|
63
|
+
return false;
|
|
64
|
+
const contentType = String(input.headers['content-type'] ?? '');
|
|
65
|
+
if (!/^\s*text\/html\b/i.test(contentType))
|
|
66
|
+
return false;
|
|
67
|
+
const encoding = String(input.headers['content-encoding'] ?? 'identity').trim().toLowerCase();
|
|
68
|
+
return encoding === '' || encoding === 'identity';
|
|
69
|
+
}
|
|
70
|
+
export const liveReloadClientSource = `
|
|
71
|
+
const eventsUrl = ${JSON.stringify(liveReloadEventsPath)};
|
|
72
|
+
let buildId = null;
|
|
73
|
+
let overlay = null;
|
|
74
|
+
|
|
75
|
+
function hideOverlay() {
|
|
76
|
+
overlay?.remove();
|
|
77
|
+
overlay = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function showOverlay(view) {
|
|
81
|
+
if (globalThis.__kodyDevErrorPage) return;
|
|
82
|
+
hideOverlay();
|
|
83
|
+
const host = document.createElement('kody-dev-overlay');
|
|
84
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
85
|
+
const label = view.kind === 'build' ? 'Build error' : 'Runtime error';
|
|
86
|
+
root.innerHTML = \`
|
|
87
|
+
<style>
|
|
88
|
+
.backdrop { position: fixed; inset: 0; z-index: 2147483647; background: rgba(10, 4, 7, .82); display: flex; align-items: flex-start; justify-content: center; overflow: auto; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; }
|
|
89
|
+
.panel { margin: 48px 16px; max-width: 960px; width: 100%; background: #1a1015; color: #f5e9ee; border: 1px solid #3a2530; border-radius: 10px; padding: 20px 24px; box-shadow: 0 20px 60px rgba(0,0,0,.5); }
|
|
90
|
+
.top { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
91
|
+
.label { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: #ff8fab; }
|
|
92
|
+
button { all: unset; cursor: pointer; color: #bfa9b3; font-size: 20px; line-height: 1; padding: 4px 8px; border-radius: 6px; }
|
|
93
|
+
button:hover, button:focus-visible { background: #2a1a22; color: #fff; }
|
|
94
|
+
h1 { font-size: 18px; margin: 8px 0 4px; word-break: break-word; }
|
|
95
|
+
.request { color: #bfa9b3; font-size: 13px; margin: 0 0 12px; }
|
|
96
|
+
pre { margin: 12px 0 0; padding: 14px; overflow: auto; background: #0e080b; border: 1px solid #3a2530; border-radius: 8px; font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
|
|
97
|
+
.hint { color: #bfa9b3; font-size: 12px; margin-top: 12px; }
|
|
98
|
+
</style>
|
|
99
|
+
<div class="backdrop" role="alertdialog" aria-modal="true" aria-labelledby="kody-dev-title">
|
|
100
|
+
<div class="panel">
|
|
101
|
+
<div class="top"><span class="label"></span><button type="button" aria-label="Dismiss">×</button></div>
|
|
102
|
+
<h1 id="kody-dev-title"></h1>
|
|
103
|
+
<p class="request"></p>
|
|
104
|
+
<pre></pre>
|
|
105
|
+
<p class="hint">Fix it and save. The page reloads on the next good build. Esc dismisses.</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>\`;
|
|
108
|
+
root.querySelector('.label').textContent = label;
|
|
109
|
+
root.querySelector('h1').textContent = view.title;
|
|
110
|
+
const request = root.querySelector('.request');
|
|
111
|
+
if (view.request) request.textContent = view.request;
|
|
112
|
+
else request.remove();
|
|
113
|
+
root.querySelector('pre').textContent = view.detail;
|
|
114
|
+
root.querySelector('button').addEventListener('click', hideOverlay);
|
|
115
|
+
document.documentElement.appendChild(host);
|
|
116
|
+
overlay = host;
|
|
117
|
+
root.querySelector('button').focus();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addEventListener('keydown', (event) => {
|
|
121
|
+
if (event.key === 'Escape') hideOverlay();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function connect() {
|
|
125
|
+
const source = new EventSource(eventsUrl);
|
|
126
|
+
source.addEventListener('hello', (event) => {
|
|
127
|
+
const state = JSON.parse(event.data);
|
|
128
|
+
if (buildId !== null && state.buildId !== null && state.buildId !== buildId) {
|
|
129
|
+
location.reload();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
buildId = state.buildId;
|
|
133
|
+
if (state.error) showOverlay(state.error);
|
|
134
|
+
});
|
|
135
|
+
source.addEventListener('reload', () => location.reload());
|
|
136
|
+
source.addEventListener('build_error', (event) => showOverlay(JSON.parse(event.data)));
|
|
137
|
+
source.addEventListener('runtime_error', (event) => showOverlay(JSON.parse(event.data)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
connect();
|
|
141
|
+
`.trimStart();
|
|
142
|
+
//# sourceMappingURL=live-reload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-reload.js","sourceRoot":"","sources":["../../src/dev/live-reload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps `kody dev` from leaking workerd processes and `/tmp/kody-dev-*` dirs.
|
|
3
|
+
*
|
|
4
|
+
* - A tiny detached `sh` reaper holds a pipe from this process. It learns
|
|
5
|
+
* workerd pids over that pipe and, when the pipe closes for any reason
|
|
6
|
+
* (including SIGKILL of the CLI), kills those pids and removes the work dir.
|
|
7
|
+
* - On startup, {@link sweepStaleDevDirs} removes dirs and workerd processes
|
|
8
|
+
* left behind by dead CLI processes (including CLIs that predate the reaper).
|
|
9
|
+
*/
|
|
10
|
+
export declare const devWorkDirPrefix = "kody-dev-";
|
|
11
|
+
export type ProcessReaper = {
|
|
12
|
+
track: (pid: number) => void;
|
|
13
|
+
untrack: (pid: number) => void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
export declare function startProcessReaper(input: {
|
|
17
|
+
workDir: string;
|
|
18
|
+
}): Promise<ProcessReaper>;
|
|
19
|
+
type ListedProcess = {
|
|
20
|
+
pid: number;
|
|
21
|
+
ppid?: number;
|
|
22
|
+
name: string;
|
|
23
|
+
cmd?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Remove `kody-dev-*` work dirs whose owner is gone and kill workerd processes
|
|
27
|
+
* still serving from them. Best effort; never throws.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sweepStaleDevDirs(input?: {
|
|
30
|
+
tmp?: string;
|
|
31
|
+
now?: number;
|
|
32
|
+
listProcesses?: () => Promise<Array<ListedProcess>>;
|
|
33
|
+
isAlive?: (pid: number) => boolean;
|
|
34
|
+
kill?: (pid: number) => void;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
removed: Array<string>;
|
|
37
|
+
killed: Array<number>;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps `kody dev` from leaking workerd processes and `/tmp/kody-dev-*` dirs.
|
|
3
|
+
*
|
|
4
|
+
* - A tiny detached `sh` reaper holds a pipe from this process. It learns
|
|
5
|
+
* workerd pids over that pipe and, when the pipe closes for any reason
|
|
6
|
+
* (including SIGKILL of the CLI), kills those pids and removes the work dir.
|
|
7
|
+
* - On startup, {@link sweepStaleDevDirs} removes dirs and workerd processes
|
|
8
|
+
* left behind by dead CLI processes (including CLIs that predate the reaper).
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import psList from 'ps-list';
|
|
15
|
+
export const devWorkDirPrefix = 'kody-dev-';
|
|
16
|
+
const ownerFileName = 'owner.json';
|
|
17
|
+
/** Legacy dirs without an owner file are only removed once this old. */
|
|
18
|
+
const legacyDirMinAgeMs = 10 * 60_000;
|
|
19
|
+
const reaperScript = `
|
|
20
|
+
pids=" "
|
|
21
|
+
while IFS= read -r line; do
|
|
22
|
+
case "$line" in
|
|
23
|
+
+*) pids="$pids\${line#+} " ;;
|
|
24
|
+
-*) pids="$(printf '%s' "$pids" | sed "s/ \${line#-} / /g")" ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
for pid in $pids; do kill -9 "$pid" 2>/dev/null; done
|
|
28
|
+
rm -rf "$1"
|
|
29
|
+
`;
|
|
30
|
+
export async function startProcessReaper(input) {
|
|
31
|
+
await writeFile(join(input.workDir, ownerFileName), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
32
|
+
let reaper;
|
|
33
|
+
try {
|
|
34
|
+
reaper = spawn('/bin/sh', ['-c', reaperScript, 'kody-dev-reaper', input.workDir], {
|
|
35
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
36
|
+
detached: true,
|
|
37
|
+
});
|
|
38
|
+
reaper.on('error', () => {
|
|
39
|
+
reaper = undefined;
|
|
40
|
+
});
|
|
41
|
+
reaper.stdin?.on('error', () => { });
|
|
42
|
+
reaper.unref();
|
|
43
|
+
reaper.stdin?.unref?.();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
reaper = undefined;
|
|
47
|
+
}
|
|
48
|
+
const send = (line) => {
|
|
49
|
+
if (reaper?.stdin?.writable)
|
|
50
|
+
reaper.stdin.write(`${line}\n`);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
track: (pid) => send(`+${pid}`),
|
|
54
|
+
untrack: (pid) => send(`-${pid}`),
|
|
55
|
+
close: () => {
|
|
56
|
+
reaper?.stdin?.end();
|
|
57
|
+
reaper = undefined;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Remove `kody-dev-*` work dirs whose owner is gone and kill workerd processes
|
|
63
|
+
* still serving from them. Best effort; never throws.
|
|
64
|
+
*/
|
|
65
|
+
export async function sweepStaleDevDirs(input = {}) {
|
|
66
|
+
const tmp = input.tmp ?? tmpdir();
|
|
67
|
+
const now = input.now ?? Date.now();
|
|
68
|
+
const isAlive = input.isAlive ?? isProcessAlive;
|
|
69
|
+
const kill = input.kill ?? ((pid) => process.kill(pid, 'SIGKILL'));
|
|
70
|
+
const removed = [];
|
|
71
|
+
const killed = [];
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = (await readdir(tmp)).filter((name) => name.startsWith(devWorkDirPrefix));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { removed, killed };
|
|
78
|
+
}
|
|
79
|
+
if (entries.length === 0)
|
|
80
|
+
return { removed, killed };
|
|
81
|
+
let processes = [];
|
|
82
|
+
try {
|
|
83
|
+
processes = await (input.listProcesses ?? psList)();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
processes = [];
|
|
87
|
+
}
|
|
88
|
+
const byPid = new Map(processes.map((item) => [item.pid, item]));
|
|
89
|
+
for (const name of entries) {
|
|
90
|
+
const dir = join(tmp, name);
|
|
91
|
+
try {
|
|
92
|
+
const owner = await readOwnerPid(dir);
|
|
93
|
+
if (owner !== null && isAlive(owner))
|
|
94
|
+
continue;
|
|
95
|
+
const workerds = processes.filter((item) => /workerd/.test(item.name) && (item.cmd ?? '').includes(`${dir}/`));
|
|
96
|
+
if (owner === null) {
|
|
97
|
+
const info = await stat(dir);
|
|
98
|
+
if (now - info.mtimeMs < legacyDirMinAgeMs)
|
|
99
|
+
continue;
|
|
100
|
+
// Legacy CLI without an owner file: only reap workerds whose parent died.
|
|
101
|
+
if (workerds.some((item) => !isOrphaned(item, byPid, isAlive)))
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const item of workerds) {
|
|
105
|
+
try {
|
|
106
|
+
kill(item.pid);
|
|
107
|
+
killed.push(item.pid);
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
}
|
|
111
|
+
await rm(dir, { recursive: true, force: true });
|
|
112
|
+
removed.push(dir);
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
}
|
|
116
|
+
return { removed, killed };
|
|
117
|
+
}
|
|
118
|
+
function isOrphaned(item, byPid, isAlive) {
|
|
119
|
+
if (item.ppid === undefined)
|
|
120
|
+
return false;
|
|
121
|
+
if (item.ppid <= 1 || !isAlive(item.ppid))
|
|
122
|
+
return true;
|
|
123
|
+
const parent = byPid.get(item.ppid);
|
|
124
|
+
return parent !== undefined && !/node|kody/i.test(`${parent.name} ${parent.cmd ?? ''}`);
|
|
125
|
+
}
|
|
126
|
+
async function readOwnerPid(dir) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(await readFile(join(dir, ownerFileName), 'utf8'));
|
|
129
|
+
return typeof parsed.pid === 'number' ? parsed.pid : null;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function isProcessAlive(pid) {
|
|
136
|
+
try {
|
|
137
|
+
process.kill(pid, 0);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return error.code === 'EPERM';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=process-reaper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-reaper.js","sourceRoot":"","sources":["../../src/dev/process-reaper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a workerd V8 stack (frames point into the bundled `main.js`) into one
|
|
3
|
+
* that points at the author's `.ts` sources, without runtime wrapper frames.
|
|
4
|
+
*/
|
|
5
|
+
export type StackMapper = (stack: string) => string;
|
|
6
|
+
export declare function createStackMapper(input: {
|
|
7
|
+
/** esbuild source map JSON for the user module, sources relative to the package root. */
|
|
8
|
+
sourceMap: string | null;
|
|
9
|
+
/** Module name of the bundled user code inside workerd. */
|
|
10
|
+
userModule: string;
|
|
11
|
+
}): StackMapper;
|