@sleepy-hollow/framework 0.3.3 → 0.3.4
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/CHANGELOG.md +11 -0
- package/dist/{chunk-4PPSJ2LE.js → chunk-2GNHTFCZ.js} +4 -4
- package/dist/{chunk-LZ2HLHDW.js → chunk-AADQFGT4.js} +3 -3
- package/dist/chunk-EHBEUJIZ.js +139 -0
- package/dist/chunk-EHBEUJIZ.js.map +1 -0
- package/dist/{chunk-S3Z6CO7J.js → chunk-LPPASMRR.js} +2 -2
- package/dist/chunk-LPPASMRR.js.map +1 -0
- package/dist/{chunk-JRFHLLLF.js → chunk-QY3TSXM6.js} +2 -2
- package/dist/{chunk-M4D63YC7.js → chunk-RYKRRAPD.js} +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +78 -20
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -5
- package/dist/routing.js +2 -2
- package/dist/security.js +4 -4
- package/dist/server.d.ts +15 -6
- package/dist/server.js +1 -1
- package/dist/testing.js +5 -5
- package/dist/validation.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-LNJDFJGT.js +0 -47
- package/dist/chunk-LNJDFJGT.js.map +0 -1
- package/dist/chunk-S3Z6CO7J.js.map +0 -1
- /package/dist/{chunk-4PPSJ2LE.js.map → chunk-2GNHTFCZ.js.map} +0 -0
- /package/dist/{chunk-LZ2HLHDW.js.map → chunk-AADQFGT4.js.map} +0 -0
- /package/dist/{chunk-JRFHLLLF.js.map → chunk-QY3TSXM6.js.map} +0 -0
- /package/dist/{chunk-M4D63YC7.js.map → chunk-RYKRRAPD.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Sleepy Hollow are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.3.4 - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### Fixed: development server lifecycle
|
|
8
|
+
|
|
9
|
+
The development worker now reports readiness only after the listener is bound,
|
|
10
|
+
keeps the Node 24 server lifecycle under one explicit contract, and preserves
|
|
11
|
+
bounded diagnostics when serving fails after binding. Public `hollow dev`
|
|
12
|
+
supervision now reports unexpected worker exits instead of remaining falsely
|
|
13
|
+
active. SQLite database files and their `-wal`/`-shm` runtime sidecars are
|
|
14
|
+
excluded from reload triggers.
|
|
15
|
+
|
|
5
16
|
## 0.3.3 - 2026-08-21
|
|
6
17
|
|
|
7
18
|
### Changed: Node/Bun documentation and scaffold alignment
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
redactSecurityData
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-AADQFGT4.js";
|
|
4
4
|
import {
|
|
5
5
|
z
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QY3TSXM6.js";
|
|
7
7
|
import {
|
|
8
8
|
platform
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LPPASMRR.js";
|
|
10
10
|
|
|
11
11
|
// core/config/types.ts
|
|
12
12
|
var RUNTIME_MODES = [
|
|
@@ -426,4 +426,4 @@ export {
|
|
|
426
426
|
createJsonLogger,
|
|
427
427
|
createOperationalRoutes
|
|
428
428
|
};
|
|
429
|
-
//# sourceMappingURL=chunk-
|
|
429
|
+
//# sourceMappingURL=chunk-2GNHTFCZ.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createValidatedRouter
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QY3TSXM6.js";
|
|
4
4
|
import {
|
|
5
5
|
platform
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LPPASMRR.js";
|
|
7
7
|
|
|
8
8
|
// core/security/declaration.ts
|
|
9
9
|
import { isAbsolute, resolve, sep } from "path";
|
|
@@ -827,4 +827,4 @@ export {
|
|
|
827
827
|
composeProjectSecurity,
|
|
828
828
|
createMemoryRateLimiter
|
|
829
829
|
};
|
|
830
|
-
//# sourceMappingURL=chunk-
|
|
830
|
+
//# sourceMappingURL=chunk-AADQFGT4.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// runtime/server.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
function nodeRequest(request) {
|
|
6
|
+
const origin = `http://${request.headers.host ?? "localhost"}`;
|
|
7
|
+
return new Request(new URL(request.url ?? "/", origin), {
|
|
8
|
+
method: request.method,
|
|
9
|
+
headers: request.headers,
|
|
10
|
+
body: request.method === "GET" || request.method === "HEAD" ? void 0 : request,
|
|
11
|
+
// Node requires this for streaming request bodies.
|
|
12
|
+
duplex: "half"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function serve(handler, options = {}) {
|
|
16
|
+
const server = createServer(async (incoming, outgoing) => {
|
|
17
|
+
try {
|
|
18
|
+
const response = await handler(nodeRequest(incoming));
|
|
19
|
+
outgoing.statusCode = response.status;
|
|
20
|
+
response.headers.forEach((value, name) => outgoing.setHeader(name, value));
|
|
21
|
+
const body = response.body ? Buffer.from(await response.arrayBuffer()) : void 0;
|
|
22
|
+
outgoing.end(body);
|
|
23
|
+
} catch {
|
|
24
|
+
outgoing.statusCode = 500;
|
|
25
|
+
outgoing.setHeader("content-type", "application/problem+json");
|
|
26
|
+
outgoing.end(JSON.stringify({ type: "about:blank", title: "Internal Server Error", status: 500 }));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
let readySettled = false;
|
|
30
|
+
let finishedSettled = false;
|
|
31
|
+
let closing;
|
|
32
|
+
let closingRequested = false;
|
|
33
|
+
let resolveReady;
|
|
34
|
+
let rejectReady;
|
|
35
|
+
let resolveFinished;
|
|
36
|
+
let rejectFinished;
|
|
37
|
+
const ready = new Promise((resolve2, reject) => {
|
|
38
|
+
resolveReady = resolve2;
|
|
39
|
+
rejectReady = reject;
|
|
40
|
+
});
|
|
41
|
+
const finished = new Promise((resolve2, reject) => {
|
|
42
|
+
resolveFinished = resolve2;
|
|
43
|
+
rejectFinished = reject;
|
|
44
|
+
});
|
|
45
|
+
const closeNow = () => new Promise((resolve2, reject) => {
|
|
46
|
+
server.close((error) => {
|
|
47
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
48
|
+
reject(error);
|
|
49
|
+
} else {
|
|
50
|
+
resolve2();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const shutdown = () => {
|
|
55
|
+
closingRequested = true;
|
|
56
|
+
if (closing !== void 0) return closing;
|
|
57
|
+
if (!server.listening) {
|
|
58
|
+
closing = Promise.resolve();
|
|
59
|
+
return closing;
|
|
60
|
+
}
|
|
61
|
+
closing = closeNow();
|
|
62
|
+
return closing;
|
|
63
|
+
};
|
|
64
|
+
const fail = (error) => {
|
|
65
|
+
if (!readySettled) {
|
|
66
|
+
readySettled = true;
|
|
67
|
+
rejectReady(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!finishedSettled) {
|
|
71
|
+
finishedSettled = true;
|
|
72
|
+
rejectFinished(error);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
server.on("error", fail);
|
|
76
|
+
server.once("close", () => {
|
|
77
|
+
if (!readySettled) {
|
|
78
|
+
readySettled = true;
|
|
79
|
+
rejectReady(new Error("The HTTP server closed before it became ready."));
|
|
80
|
+
}
|
|
81
|
+
if (!finishedSettled) {
|
|
82
|
+
finishedSettled = true;
|
|
83
|
+
resolveFinished();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
server.once("listening", async () => {
|
|
87
|
+
try {
|
|
88
|
+
await options.onListen?.();
|
|
89
|
+
if (closingRequested) {
|
|
90
|
+
await closeNow();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!readySettled) {
|
|
94
|
+
readySettled = true;
|
|
95
|
+
resolveReady();
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
fail(error);
|
|
99
|
+
await shutdown().catch(() => void 0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
if (options.signal) {
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
void shutdown();
|
|
105
|
+
};
|
|
106
|
+
if (options.signal.aborted) onAbort();
|
|
107
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
108
|
+
server.once("close", () => options.signal?.removeEventListener("abort", onAbort));
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
server.listen(
|
|
112
|
+
options.port ?? Number(process.env.PORT ?? 3e3),
|
|
113
|
+
options.hostname ?? "0.0.0.0"
|
|
114
|
+
);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
fail(error);
|
|
117
|
+
}
|
|
118
|
+
void ready.catch(() => void 0);
|
|
119
|
+
void finished.catch(() => void 0);
|
|
120
|
+
return { ready, finished, shutdown };
|
|
121
|
+
}
|
|
122
|
+
async function startConfiguredApplication() {
|
|
123
|
+
const entry = process.env.HOLLOW_APP_MODULE;
|
|
124
|
+
if (!entry) throw new Error("HOLLOW_APP_MODULE must name the compiled application module.");
|
|
125
|
+
const loaded = await import(pathToFileURL(resolve(entry)).href);
|
|
126
|
+
const handler = typeof loaded.fetch === "function" ? loaded.fetch : loaded.default;
|
|
127
|
+
if (typeof handler !== "function") throw new Error("The configured application module must export a fetch handler.");
|
|
128
|
+
const server = serve(handler);
|
|
129
|
+
await server.ready;
|
|
130
|
+
await server.finished;
|
|
131
|
+
}
|
|
132
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
133
|
+
await startConfiguredApplication();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
serve
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=chunk-EHBEUJIZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/server.ts"],"sourcesContent":["import { createServer } from \"http\";\nimport { resolve } from \"path\";\nimport { pathToFileURL } from \"url\";\n\nexport type FetchHandler = (request: Request) => Response | Promise<Response>;\n\nexport interface ServerOptions {\n readonly port?: number;\n readonly hostname?: string;\n readonly signal?: AbortSignal;\n readonly onListen?: () => void | Promise<void>;\n}\n\nexport interface HttpServer {\n /** Resolves only after the underlying Node listener emits `listening`. */\n readonly ready: Promise<void>;\n /** Resolves on a normal close and rejects on a post-bind server failure. */\n readonly finished: Promise<void>;\n /** Stops accepting connections and waits for the listener to close. */\n shutdown(): Promise<void>;\n}\n\nfunction nodeRequest(request: import(\"http\").IncomingMessage): Request {\n const origin = `http://${request.headers.host ?? \"localhost\"}`;\n return new Request(new URL(request.url ?? \"/\", origin), {\n method: request.method,\n headers: request.headers as HeadersInit,\n body: request.method === \"GET\" || request.method === \"HEAD\" ? undefined : request,\n // Node requires this for streaming request bodies.\n duplex: \"half\",\n } as RequestInit);\n}\n\n/** Starts the Node HTTP adapter for a framework fetch handler. */\nexport function serve(handler: FetchHandler, options: ServerOptions = {}): HttpServer {\n const server = createServer(async (incoming, outgoing) => {\n try {\n const response = await handler(nodeRequest(incoming));\n outgoing.statusCode = response.status;\n response.headers.forEach((value, name) => outgoing.setHeader(name, value));\n const body = response.body ? Buffer.from(await response.arrayBuffer()) : undefined;\n outgoing.end(body);\n } catch {\n outgoing.statusCode = 500;\n outgoing.setHeader(\"content-type\", \"application/problem+json\");\n outgoing.end(JSON.stringify({ type: \"about:blank\", title: \"Internal Server Error\", status: 500 }));\n }\n });\n\n let readySettled = false;\n let finishedSettled = false;\n let closing: Promise<void> | undefined;\n let closingRequested = false;\n let resolveReady!: () => void;\n let rejectReady!: (error: unknown) => void;\n let resolveFinished!: () => void;\n let rejectFinished!: (error: unknown) => void;\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve;\n rejectReady = reject;\n });\n const finished = new Promise<void>((resolve, reject) => {\n resolveFinished = resolve;\n rejectFinished = reject;\n });\n\n const closeNow = (): Promise<void> => new Promise((resolve, reject) => {\n server.close((error) => {\n if (error && (error as NodeJS.ErrnoException).code !== \"ERR_SERVER_NOT_RUNNING\") {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n\n const shutdown = (): Promise<void> => {\n closingRequested = true;\n if (closing !== undefined) return closing;\n if (!server.listening) {\n closing = Promise.resolve();\n return closing;\n }\n closing = closeNow();\n return closing;\n };\n\n const fail = (error: unknown): void => {\n if (!readySettled) {\n readySettled = true;\n rejectReady(error);\n return;\n }\n if (!finishedSettled) {\n finishedSettled = true;\n rejectFinished(error);\n }\n };\n\n server.on(\"error\", fail);\n server.once(\"close\", () => {\n if (!readySettled) {\n readySettled = true;\n rejectReady(new Error(\"The HTTP server closed before it became ready.\"));\n }\n if (!finishedSettled) {\n finishedSettled = true;\n resolveFinished();\n }\n });\n server.once(\"listening\", async () => {\n try {\n await options.onListen?.();\n if (closingRequested) {\n await closeNow();\n return;\n }\n if (!readySettled) {\n readySettled = true;\n resolveReady();\n }\n } catch (error) {\n fail(error);\n await shutdown().catch(() => undefined);\n }\n });\n\n if (options.signal) {\n const onAbort = () => {\n void shutdown();\n };\n if (options.signal.aborted) onAbort();\n else options.signal.addEventListener(\"abort\", onAbort, { once: true });\n server.once(\"close\", () => options.signal?.removeEventListener(\"abort\", onAbort));\n }\n\n try {\n server.listen(\n options.port ?? Number(process.env.PORT ?? 3000),\n options.hostname ?? \"0.0.0.0\",\n );\n } catch (error) {\n fail(error);\n }\n\n // A caller may intentionally use only `shutdown`; mark these promises as\n // handled while preserving their rejection for callers that await them.\n void ready.catch(() => undefined);\n void finished.catch(() => undefined);\n return { ready, finished, shutdown };\n}\n\nasync function startConfiguredApplication(): Promise<void> {\n const entry = process.env.HOLLOW_APP_MODULE;\n if (!entry) throw new Error(\"HOLLOW_APP_MODULE must name the compiled application module.\");\n const loaded = await import(pathToFileURL(resolve(entry)).href) as { fetch?: unknown; default?: unknown };\n const handler = typeof loaded.fetch === \"function\" ? loaded.fetch : loaded.default;\n if (typeof handler !== \"function\") throw new Error(\"The configured application module must export a fetch handler.\");\n const server = serve(handler as FetchHandler);\n await server.ready;\n await server.finished;\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n await startConfiguredApplication();\n}\n"],"mappings":"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","names":["resolve"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
serve
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EHBEUJIZ.js";
|
|
4
4
|
|
|
5
5
|
// core/routing/define_route.ts
|
|
6
6
|
function defineRoute(route) {
|
|
@@ -467,4 +467,4 @@ export {
|
|
|
467
467
|
discoverRoutes,
|
|
468
468
|
createRouter
|
|
469
469
|
};
|
|
470
|
-
//# sourceMappingURL=chunk-
|
|
470
|
+
//# sourceMappingURL=chunk-LPPASMRR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../core/routing/define_route.ts","../runtime/platform.ts","../core/routing/discover.ts","../core/routing/types.ts","../core/routing/router.ts"],"sourcesContent":["import type {\n HttpMethod,\n RouteHandlerContext,\n RouteOperation,\n} from \"./types.ts\";\n\ntype MethodMap = Partial<Record<HttpMethod, unknown>>;\n\ntype DefinedRoute<\n Schemas extends MethodMap,\n Security extends { readonly [Method in keyof Schemas]: unknown },\n Contract extends { readonly [Method in keyof Schemas]: unknown },\n> = {\n readonly [Method in keyof Schemas]: RouteOperation<\n Schemas[Method],\n Security[Method],\n Contract[Method]\n >;\n};\n\n/**\n * Declares the operations a route file answers, one per HTTP method.\n *\n * The call is an identity function at runtime; its work is done in the type\n * system, where the schemas you pass become the types of `params`, `query`,\n * `headers`, and `body` inside each handler, and the declared authentication\n * mode determines whether `principal` can be `null`.\n *\n * ```ts\n * import { defineRoute } from \"@sleepy-hollow/framework/routing\";\n * import { z } from \"@sleepy-hollow/framework/validation\";\n *\n * export default defineRoute({\n * GET: {\n * schemas: {\n * params: z.object({ id: z.string() }).strict(),\n * responses: { 200: z.object({ id: z.string() }).strict() },\n * },\n * security: { authentication: { mode: \"none\" } },\n * contract: { summary: \"Return one widget\" },\n * handler: ({ params }) => Response.json({ id: params.id }),\n * },\n * });\n * ```\n *\n * @param route The operations this file answers, keyed by HTTP method.\n * @returns The same declaration, typed so handlers infer their inputs.\n */\nexport function defineRoute<\n const Schemas extends MethodMap,\n const Security extends { readonly [Method in keyof Schemas]: unknown },\n const Contract extends { readonly [Method in keyof Schemas]: unknown },\n>(\n route: {\n readonly [Method in keyof Schemas]: {\n readonly schemas: Schemas[Method];\n readonly security: Security[Method];\n readonly contract: Contract[Method];\n readonly handler: (\n context: RouteHandlerContext<Schemas[Method], Security[Method]>,\n ) => Response | Promise<Response>;\n };\n },\n): DefinedRoute<Schemas, Security, Contract> {\n return route;\n}\n","import { spawn as spawnChild } from \"child_process\";\nimport {\n copyFile,\n lstat,\n mkdir,\n mkdtemp,\n readFile,\n readdir,\n realpath,\n rename,\n rm,\n stat,\n writeFile,\n} from \"fs/promises\";\nimport { readdirSync, watch, type FSWatcher } from \"fs\";\nimport { symlink } from \"fs/promises\";\nimport { Readable } from \"stream\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\nimport {\n serve as nodeServe,\n type FetchHandler,\n type HttpServer,\n type ServerOptions,\n} from \"./server.ts\";\n\nexport type { HttpServer, ServerOptions } from \"./server.ts\";\n\nexport interface PlatformDirEntry {\n readonly name: string;\n readonly isFile: boolean;\n readonly isDirectory: boolean;\n readonly isSymlink: boolean;\n}\n\nexport interface PlatformCommandOutput {\n readonly code: number;\n readonly success: boolean;\n readonly stdout: Uint8Array;\n readonly stderr: Uint8Array;\n}\n\nclass NotFound extends Error {\n constructor(path?: string) {\n super(path ? `Not found: ${path}` : \"Not found\");\n this.name = \"NotFound\";\n }\n}\n\nfunction normalizeFilesystemError(error: unknown, path?: string): never {\n if (typeof error === \"object\" && error !== null && \"code\" in error &&\n (error as { readonly code?: unknown }).code === \"ENOENT\") {\n throw new NotFound(path);\n }\n throw error;\n}\n\nasync function filesystem<T>(operation: Promise<T>, path?: string): Promise<T> {\n try {\n return await operation;\n } catch (error) {\n return normalizeFilesystemError(error, path);\n }\n}\n\ninterface CommandOptions {\n readonly args?: readonly string[];\n readonly cwd?: string;\n readonly env?: Readonly<Record<string, string>>;\n readonly clearEnv?: boolean;\n readonly stdin?: \"null\" | \"piped\";\n readonly stdout?: \"piped\" | \"null\";\n readonly stderr?: \"piped\" | \"null\";\n}\n\nexport class Command {\n readonly #command: string;\n readonly #options: CommandOptions;\n\n constructor(command: string, options: CommandOptions = {}) {\n this.#command = command;\n this.#options = options;\n }\n\n async output(): Promise<PlatformCommandOutput> {\n const child = spawnChild(this.#command, this.#options.args ?? [], {\n cwd: this.#options.cwd,\n env: this.#options.clearEnv ? this.#options.env : { ...process.env, ...this.#options.env },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n child.stdout.on(\"data\", (chunk: Buffer) => stdout.push(chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => stderr.push(chunk));\n const code = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"close\", (status) => resolve(status ?? 1));\n });\n return { code, success: code === 0, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };\n }\n\n spawn() {\n const child = spawnChild(this.#command, this.#options.args ?? [], {\n cwd: this.#options.cwd,\n env: this.#options.clearEnv ? this.#options.env : { ...process.env, ...this.#options.env },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n return {\n stdout: Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,\n stderr: Readable.toWeb(child.stderr) as ReadableStream<Uint8Array>,\n status: new Promise<{ code: number; success: boolean }>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"close\", (code) => {\n const resolved = code ?? 1;\n resolve({ code: resolved, success: resolved === 0 });\n });\n }),\n kill: (signal?: NodeJS.Signals) => child.kill(signal),\n };\n }\n}\n\nclass Watcher implements AsyncIterable<{ readonly paths: readonly string[] }> {\n readonly #watcher: FSWatcher;\n #closed = false;\n #pending: Array<{ readonly paths: readonly string[] }> = [];\n #resolve?: (event: IteratorResult<{ readonly paths: readonly string[] }>) => void;\n #reject?: (reason: unknown) => void;\n #failure?: unknown;\n\n constructor(root: string) {\n this.#watcher = watch(root, { recursive: true }, (_event, filename) => {\n const event = { paths: [join(root, String(filename ?? \"\"))] };\n if (this.#resolve) {\n this.#resolve({ done: false, value: event });\n this.#resolve = undefined;\n } else this.#pending.push(event);\n });\n this.#watcher.on(\"error\", (error) => {\n if (this.#closed) return;\n this.#failure = error;\n this.#closed = true;\n this.#reject?.(error);\n this.#resolve = undefined;\n this.#reject = undefined;\n });\n }\n\n close(): void {\n this.#closed = true;\n this.#watcher.close();\n this.#resolve?.({ done: true, value: undefined });\n this.#resolve = undefined;\n this.#reject = undefined;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<{ readonly paths: readonly string[] }> {\n return {\n next: () => {\n const value = this.#pending.shift();\n if (value) return Promise.resolve({ done: false, value });\n if (this.#failure) return Promise.reject(this.#failure);\n if (this.#closed) return Promise.resolve({ done: true, value: undefined });\n return new Promise((resolve, reject) => {\n this.#resolve = resolve;\n this.#reject = reject;\n });\n },\n };\n }\n}\n\nexport const platform = Object.freeze({\n args: process.argv.slice(2),\n cwd: () => process.cwd(),\n exit: (code?: number) => process.exit(code),\n execPath: () => process.execPath,\n env: Object.freeze({\n get: (name: string) => process.env[name],\n toObject: () => ({ ...process.env }),\n }),\n errors: Object.freeze({\n NotFound,\n AddrInUse: class AddrInUse extends Error {},\n PermissionDenied: class PermissionDenied extends Error {},\n }),\n isNotFound: (error: unknown) =>\n error instanceof NotFound ||\n (typeof error === \"object\" && error !== null &&\n \"code\" in error && (error as { readonly code?: unknown }).code === \"ENOENT\"),\n readTextFile: async (path: string) => filesystem(readFile(path, \"utf8\"), path),\n readFile: async (path: string) => filesystem(readFile(path), path),\n writeTextFile: async (path: string, text: string, options?: { readonly createNew?: boolean }) =>\n writeFile(path, text, options?.createNew ? { flag: \"wx\" } : undefined),\n stat: (path: string) => filesystem(stat(path), path),\n lstat: (path: string) => filesystem(lstat(path), path),\n mkdir,\n rename,\n remove: (path: string, options?: { readonly recursive?: boolean }) => rm(path, { recursive: options?.recursive, force: true }),\n realPath: (path: string) => filesystem(realpath(path), path),\n copyFile: (source: string, target: string) => filesystem(copyFile(source, target), source),\n symlink: (target: string, path: string) => filesystem(symlink(target, path), path),\n readDirSync: (path: string) => readdirSync(path, { withFileTypes: true }).map((entry) => ({ name: entry.name, isFile: entry.isFile(), isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink() })),\n makeTempDir: async (options?: { readonly prefix?: string; readonly dir?: string }) =>\n mkdtemp(join(options?.dir ?? tmpdir(), options?.prefix ?? \"sleepy-hollow-\")),\n async *readDir(path: string): AsyncIterable<PlatformDirEntry> {\n for (const entry of await readdir(path, { withFileTypes: true })) {\n yield { name: entry.name, isFile: entry.isFile(), isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink() };\n }\n },\n watchFs: (root: string, _options?: { readonly recursive?: boolean }) => new Watcher(root),\n addSignalListener: (signal: NodeJS.Signals, listener: () => void) => process.on(signal, listener),\n removeSignalListener: (signal: NodeJS.Signals, listener: () => void) => process.off(signal, listener),\n serve: (options: ServerOptions, handler: FetchHandler): HttpServer => nodeServe(handler, options),\n Command,\n});\n","import { platform, type PlatformDirEntry } from \"#platform\";\nimport { dirname, relative, resolve, sep } from \"path\";\nimport { fileURLToPath, pathToFileURL } from \"url\";\n\nimport {\n HTTP_METHODS,\n type HttpMethod,\n type NormalizedRoute,\n RouteDiscoveryError,\n type RouteModule,\n type RouteOperation,\n type RoutingDiagnostic,\n} from \"./types.ts\";\n\nconst dynamicSegment = /^\\[([^\\]]+)\\]$/;\nconst parameterName = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst methods = new Set<string>(HTTP_METHODS);\n\ninterface RouteFile {\n readonly path: string;\n readonly segments: readonly string[];\n readonly routePath: string;\n readonly conflictPath: string;\n readonly parameterNames: readonly string[];\n}\n\nconst portablePath = (path: string) => path.split(sep).join(\"/\");\n\nasync function collectRouteFiles(directory: string): Promise<string[]> {\n const files: string[] = [];\n const entries: PlatformDirEntry[] = [];\n\n for await (const entry of platform.readDir(directory)) entries.push(entry);\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const path = resolve(directory, entry.name);\n if (entry.isDirectory) files.push(...await collectRouteFiles(path));\n if (entry.isFile && entry.name === \"route.ts\") files.push(path);\n }\n\n return files;\n}\n\nfunction normalizeRouteFile(\n apiRoot: string,\n path: string,\n): RouteFile | RoutingDiagnostic {\n const segments = portablePath(relative(apiRoot, dirname(path))).split(\"/\")\n .filter(Boolean);\n const routeSegments: string[] = [];\n const conflictSegments: string[] = [];\n const parameterNames: string[] = [];\n\n for (const segment of segments) {\n const match = segment.match(dynamicSegment);\n if (!match) {\n if (segment.includes(\"[\") || segment.includes(\"]\")) {\n return invalidSegment(path, segment);\n }\n routeSegments.push(segment);\n conflictSegments.push(segment);\n continue;\n }\n\n const name = match[1];\n if (!parameterName.test(name)) return invalidSegment(path, segment);\n if (parameterNames.includes(name)) {\n return {\n code: \"SH_ROUTE_INVALID_SEGMENT\",\n summary: `Dynamic parameter '${name}' is repeated in one route`,\n files: [portablePath(path)],\n correction: \"Use a unique parameter name for every dynamic segment.\",\n };\n }\n\n parameterNames.push(name);\n routeSegments.push(`:${name}`);\n conflictSegments.push(\":parameter\");\n }\n\n return {\n path: portablePath(path),\n segments,\n routePath: `/${routeSegments.join(\"/\")}`,\n conflictPath: `/${conflictSegments.join(\"/\")}`,\n parameterNames,\n };\n}\n\nfunction invalidSegment(path: string, segment: string): RoutingDiagnostic {\n return {\n code: \"SH_ROUTE_INVALID_SEGMENT\",\n summary: `Invalid dynamic route segment '${segment}'`,\n files: [portablePath(path)],\n correction:\n \"Use [name] with a TypeScript identifier as the parameter name.\",\n };\n}\n\nfunction validateModule(\n value: unknown,\n file: RouteFile,\n): RoutingDiagnostic | RouteModule {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return invalidModule(\n file.path,\n \"The default export must be created with defineRoute\",\n );\n }\n\n const entries = Object.entries(value);\n if (entries.length === 0) {\n return invalidModule(\n file.path,\n \"The route must declare at least one HTTP method\",\n );\n }\n\n for (const [method, operation] of entries) {\n if (!methods.has(method)) {\n return invalidModule(file.path, `Unsupported HTTP method '${method}'`);\n }\n if (!isOperation(operation)) {\n return invalidModule(\n file.path,\n `${method} must declare schemas, security, contract, and a handler`,\n );\n }\n }\n\n return value as RouteModule;\n}\n\nfunction isOperation(value: unknown): value is RouteOperation {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const operation = value as Record<string, unknown>;\n return Object.hasOwn(operation, \"schemas\") &&\n Object.hasOwn(operation, \"security\") &&\n Object.hasOwn(operation, \"contract\") &&\n typeof operation.handler === \"function\";\n}\n\nfunction invalidModule(path: string, summary: string): RoutingDiagnostic {\n return {\n code: \"SH_ROUTE_INVALID_MODULE\",\n summary,\n files: [portablePath(path)],\n correction:\n \"Default-export one defineRoute method map with complete operations.\",\n };\n}\n\nfunction findConflicts(files: readonly RouteFile[]): RoutingDiagnostic[] {\n const groups = Map.groupBy(files, (file) => file.conflictPath);\n const diagnostics: RoutingDiagnostic[] = [];\n\n for (const [route, group] of groups) {\n if (group.length < 2) continue;\n diagnostics.push({\n code: \"SH_ROUTE_CONFLICT\",\n summary: `Ambiguous route definitions normalize to '${route}'`,\n files: group.map((file) => file.path).sort(),\n route,\n correction: \"Keep only one dynamic sibling at each route depth.\",\n });\n }\n\n return diagnostics.sort((left, right) =>\n (left.route ?? \"\").localeCompare(right.route ?? \"\")\n );\n}\n\n/**\n * Walks a directory and derives the route table from the file layout.\n *\n * Each `route.ts` becomes one route whose URL path is its position in the\n * tree, and each method it exports becomes one entry. Faults are collected\n * across the whole tree and thrown together as a\n * {@linkcode RouteDiscoveryError}, so one run reports every correction rather\n * than stopping at the first.\n *\n * @param apiRoot Directory to walk, as a path or a `file:` URL.\n * @returns Every discovered route, one entry per method.\n * @throws {RouteDiscoveryError} When any route in the tree is malformed.\n */\nexport async function discoverRoutes(\n apiRoot: URL | string,\n): Promise<readonly NormalizedRoute[]> {\n const root = resolve(\n apiRoot instanceof URL ? fileURLToPath(apiRoot) : apiRoot,\n );\n const diagnostics: RoutingDiagnostic[] = [];\n const routeFiles: RouteFile[] = [];\n\n for (const path of await collectRouteFiles(root)) {\n const normalized = normalizeRouteFile(root, path);\n if (\"code\" in normalized) diagnostics.push(normalized);\n else routeFiles.push(normalized);\n }\n\n diagnostics.push(...findConflicts(routeFiles));\n\n const routes: NormalizedRoute[] = [];\n for (const file of routeFiles) {\n try {\n const imported = await import(pathToFileURL(file.path).href);\n const routeModule = validateModule(imported.default, file);\n if (\"code\" in routeModule) {\n diagnostics.push(routeModule);\n continue;\n }\n\n for (const [method, operation] of Object.entries(routeModule)) {\n routes.push({\n method: method as HttpMethod,\n path: file.routePath,\n source: file.path,\n parameterNames: file.parameterNames,\n operation,\n });\n }\n } catch (error) {\n diagnostics.push(invalidModule(\n file.path,\n `Route module could not be loaded: ${\n error instanceof Error ? error.message : String(error)\n }`,\n ));\n }\n }\n\n if (diagnostics.length > 0) {\n diagnostics.sort((left, right) =>\n `${left.code}:${left.files.join(\":\")}`.localeCompare(\n `${right.code}:${right.files.join(\":\")}`,\n )\n );\n throw new RouteDiscoveryError(diagnostics);\n }\n\n return routes.sort((left, right) =>\n left.path.localeCompare(right.path) ||\n left.method.localeCompare(right.method)\n );\n}\n","/** The HTTP methods a route module may export an operation for. */\nexport const HTTP_METHODS = [\n \"DELETE\",\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PATCH\",\n \"POST\",\n \"PUT\",\n] as const;\n\n/** One of the {@linkcode HTTP_METHODS} a route operation may answer. */\nexport type HttpMethod = (typeof HTTP_METHODS)[number];\n\ntype SchemaOutput<Schema, Fallback> = Schema extends {\n readonly _zod: { readonly output: infer Output };\n} ? Output\n : Schema extends { readonly _output: infer Output } ? Output\n : Fallback;\n\ntype ReadonlyOutput<Output> = Output extends object ? Readonly<Output> : Output;\n\ntype LocationOutput<\n Schemas,\n Location extends PropertyKey,\n Fallback,\n> = Schemas extends { readonly [Key in Location]: infer Schema }\n ? ReadonlyOutput<SchemaOutput<Schema, Fallback>>\n : Fallback;\n\ntype BodyOutput<Schemas> = Schemas extends {\n readonly body: { readonly schema: infer Schema };\n} ? ReadonlyOutput<SchemaOutput<Schema, unknown>>\n : undefined;\n\n/**\n * The authenticated caller a handler runs on behalf of.\n *\n * Present only on routes whose security declares authentication; a route\n * declaring `\"none\"` receives `null` instead, and the type reflects that so a\n * handler cannot read a principal it was never given.\n */\nexport interface RoutePrincipal {\n /** Stable identifier for the caller, unique within its {@linkcode type}. */\n readonly id: string;\n /** What kind of caller this is, as named by the authentication provider. */\n readonly type: string;\n /** Additional claims the provider asserted about the caller. */\n readonly claims?: Readonly<Record<string, unknown>>;\n}\n\ntype SecurityPrincipal<Security> = Security extends {\n readonly authentication: { readonly mode: \"required\" };\n} ? RoutePrincipal\n : Security extends {\n readonly authentication: { readonly mode: \"none\" };\n } ? null\n : RoutePrincipal | null;\n\n/**\n * What a route handler receives.\n *\n * Each validated location is typed from the route's own schemas, so `params`,\n * `query`, `headers`, and `body` arrive already parsed rather than as raw\n * strings the handler has to re-check.\n */\nexport interface RouteHandlerContext<Schemas = unknown, Security = unknown> {\n /** The incoming request, unmodified. */\n readonly request: Request;\n /** Path parameters, parsed by the route's `params` schema. */\n readonly params: LocationOutput<\n Schemas,\n \"params\",\n Readonly<Record<string, string>>\n >;\n /** Query string values, parsed by the route's `query` schema. */\n readonly query: LocationOutput<\n Schemas,\n \"query\",\n Readonly<Record<string, unknown>>\n >;\n /** Request headers, parsed by the route's `headers` schema. */\n readonly headers: LocationOutput<\n Schemas,\n \"headers\",\n Readonly<Record<string, unknown>>\n >;\n /** The parsed request body, or `undefined` when the route declares none. */\n readonly body: BodyOutput<Schemas>;\n /** Aborts when the client disconnects or the request times out. */\n readonly signal: AbortSignal;\n /** The authenticated caller, or `null` on an unauthenticated route. */\n readonly principal: SecurityPrincipal<Security>;\n /** Correlates this request across logs and captured evidence. */\n readonly requestId: string;\n}\n\n/**\n * One method's implementation within a route module: its schemas, its security,\n * its documented contract, and the handler that answers it.\n */\nexport interface RouteOperation<\n Schemas = unknown,\n Security = unknown,\n Contract = unknown,\n> {\n /** Validation schemas for each request location and each response status. */\n readonly schemas: Schemas;\n /** Authentication and authorization requirements for this operation. */\n readonly security: Security;\n /** Documentation for this operation, such as its summary. */\n readonly contract: Contract;\n /** Answers the request once validation and security have passed. */\n readonly handler: (\n context: RouteHandlerContext<Schemas, Security>,\n ) => Response | Promise<Response>;\n}\n\n/** A route file's default export: one operation per method it answers. */\nexport type RouteModule = Partial<\n Record<HttpMethod, RouteOperation<unknown, unknown, unknown>>\n>;\n\n/**\n * One method of one route after discovery, with its URL path derived from the\n * file's position in the tree. This is what the router dispatches against.\n */\nexport interface NormalizedRoute {\n /** The method this entry answers. */\n readonly method: HttpMethod;\n /** The URL path, with parameters as `[name]` segments. */\n readonly path: string;\n /** Path of the file this route was discovered from. */\n readonly source: string;\n /** Names of the path parameters, in the order they appear. */\n readonly parameterNames: readonly string[];\n /** The operation to invoke for this method. */\n readonly operation: RouteOperation<unknown, unknown, unknown>;\n}\n\n/** One reason discovery refused a route tree. */\nexport interface RoutingDiagnostic {\n /** Stable machine-readable identifier for this kind of fault. */\n readonly code: string;\n /** What is wrong, in one sentence. */\n readonly summary: string;\n /** The files this diagnostic was raised against. */\n readonly files: readonly string[];\n /** The route path concerned, when the fault is specific to one. */\n readonly route?: string;\n /** What to change to resolve it. */\n readonly correction?: string;\n}\n\n/**\n * Thrown when a route tree cannot be discovered.\n *\n * Discovery reports every fault it found rather than the first, so one run\n * surfaces the whole set of corrections.\n */\nexport class RouteDiscoveryError extends Error {\n /**\n * Builds an error whose message lists every diagnostic, one per line.\n *\n * @param diagnostics Every fault discovery found, in the order detected.\n */\n constructor(readonly diagnostics: readonly RoutingDiagnostic[]) {\n super(\n diagnostics.map((diagnostic) =>\n `${diagnostic.code}: ${diagnostic.summary}`\n ).join(\"\\n\"),\n );\n this.name = \"RouteDiscoveryError\";\n }\n}\n","import { platform } from \"#platform\";\nimport type { NormalizedRoute } from \"./types.ts\";\n\ninterface Match {\n readonly route: NormalizedRoute;\n readonly params: Readonly<Record<string, string>>;\n}\n\nfunction splitPath(path: string): readonly string[] | undefined {\n try {\n return path.split(\"/\").filter(Boolean).map(decodeURIComponent);\n } catch {\n return undefined;\n }\n}\n\nfunction matchRoute(\n route: NormalizedRoute,\n requestSegments: readonly string[],\n): Match | undefined {\n const routeSegments = route.path.split(\"/\").filter(Boolean);\n if (routeSegments.length !== requestSegments.length) return undefined;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < routeSegments.length; index += 1) {\n const expected = routeSegments[index];\n const actual = requestSegments[index];\n if (expected.startsWith(\":\")) params[expected.slice(1)] = actual;\n else if (expected !== actual) return undefined;\n }\n\n return { route, params };\n}\n\nfunction compareSpecificity(left: Match, right: Match): number {\n const leftSegments = left.route.path.split(\"/\").filter(Boolean);\n const rightSegments = right.route.path.split(\"/\").filter(Boolean);\n\n for (let index = 0; index < leftSegments.length; index += 1) {\n const leftDynamic = leftSegments[index].startsWith(\":\");\n const rightDynamic = rightSegments[index].startsWith(\":\");\n if (leftDynamic !== rightDynamic) return leftDynamic ? 1 : -1;\n }\n\n return left.route.path.localeCompare(right.route.path);\n}\n\nfunction problem(\n status: number,\n title: string,\n instance: string,\n headers?: HeadersInit,\n): Response {\n return new Response(\n JSON.stringify({ type: \"about:blank\", title, status, instance }),\n {\n status,\n headers: {\n \"content-type\": \"application/problem+json\",\n ...headers,\n },\n },\n );\n}\n\n/**\n * Builds a request handler that dispatches to a discovered route table.\n *\n * The returned object exposes `fetch`, so it can be passed to `platform.serve`\n * directly. An unmatched path answers 404 and an unmatched method answers 405,\n * both as problem-details responses.\n *\n * ```ts\n * import { createRouter, discoverRoutes } from \"@sleepy-hollow/framework\";\n *\n * const router = createRouter(await discoverRoutes(\"./api\"));\n * platform.serve(router.fetch);\n * ```\n *\n * @param routes The route table, normally from {@linkcode discoverRoutes}.\n * @returns A handler suitable for `platform.serve`.\n */\nexport function createRouter(\n routes: readonly NormalizedRoute[],\n): { fetch(request: Request): Promise<Response> } {\n const inventory = [...routes];\n\n return {\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const requestSegments = splitPath(url.pathname);\n if (!requestSegments) return problem(404, \"Not Found\", url.pathname);\n\n const matches = inventory\n .map((route) => matchRoute(route, requestSegments))\n .filter((match): match is Match => match !== undefined)\n .sort(compareSpecificity);\n\n if (matches.length === 0) return problem(404, \"Not Found\", url.pathname);\n\n const selectedPath = matches[0].route.path;\n const pathMatches = matches.filter((match) =>\n match.route.path === selectedPath\n );\n const method = request.method.toUpperCase();\n const selected = pathMatches.find((match) =>\n match.route.method === method\n );\n if (!selected) {\n const allowed = [\n ...new Set(pathMatches.map((match) => match.route.method)),\n ]\n .sort();\n return problem(405, \"Method Not Allowed\", url.pathname, {\n allow: allowed.join(\", \"),\n });\n }\n\n return await selected.route.operation.handler({\n request,\n params: selected.params,\n query: Object.freeze({}),\n headers: Object.freeze({}),\n body: undefined,\n signal: request.signal,\n principal: null,\n requestId: \"\",\n });\n },\n };\n}\n"],"mappings":"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","names":["resolve"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRouter
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LPPASMRR.js";
|
|
4
4
|
|
|
5
5
|
// core/validation/mod.ts
|
|
6
6
|
import { z as z2 } from "zod";
|
|
@@ -595,4 +595,4 @@ export {
|
|
|
595
595
|
createValidatedRouter,
|
|
596
596
|
z2 as z
|
|
597
597
|
};
|
|
598
|
-
//# sourceMappingURL=chunk-
|
|
598
|
+
//# sourceMappingURL=chunk-QY3TSXM6.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-BAKXP7IR.js";
|
|
4
4
|
import {
|
|
5
5
|
composeProjectSecurity
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-AADQFGT4.js";
|
|
7
7
|
import {
|
|
8
8
|
__commonJS,
|
|
9
9
|
__toESM
|
|
@@ -4582,4 +4582,4 @@ export {
|
|
|
4582
4582
|
* LICENSE file in the root directory of this source tree.
|
|
4583
4583
|
*)
|
|
4584
4584
|
*/
|
|
4585
|
-
//# sourceMappingURL=chunk-
|
|
4585
|
+
//# sourceMappingURL=chunk-RYKRRAPD.js.map
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
resolveConfiguration
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2GNHTFCZ.js";
|
|
5
5
|
import {
|
|
6
6
|
createTraceabilityReport,
|
|
7
7
|
selectAffectedTests
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RYKRRAPD.js";
|
|
9
9
|
import "./chunk-BAKXP7IR.js";
|
|
10
10
|
import {
|
|
11
11
|
composeProjectSecurity
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-AADQFGT4.js";
|
|
13
13
|
import {
|
|
14
14
|
normalizeRoutes
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-QY3TSXM6.js";
|
|
16
16
|
import {
|
|
17
17
|
discoverRoutes,
|
|
18
18
|
platform
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
} from "./chunk-LPPASMRR.js";
|
|
20
|
+
import "./chunk-EHBEUJIZ.js";
|
|
21
21
|
import "./chunk-5WRI5ZAA.js";
|
|
22
22
|
|
|
23
23
|
// cli/main.ts
|
|
@@ -1006,7 +1006,7 @@ var CreationError = class extends Error {
|
|
|
1006
1006
|
};
|
|
1007
1007
|
|
|
1008
1008
|
// cli/create/create.ts
|
|
1009
|
-
var VERSION = "0.3.
|
|
1009
|
+
var VERSION = "0.3.4";
|
|
1010
1010
|
var NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1011
1011
|
function files(name) {
|
|
1012
1012
|
return {
|
|
@@ -1173,7 +1173,7 @@ async function pathExists(path) {
|
|
|
1173
1173
|
throw error;
|
|
1174
1174
|
}
|
|
1175
1175
|
}
|
|
1176
|
-
var FRAMEWORK_VERSION = "0.3.
|
|
1176
|
+
var FRAMEWORK_VERSION = "0.3.4";
|
|
1177
1177
|
async function createProject(options) {
|
|
1178
1178
|
if (!NAME.test(options.name) || options.name.length > 64) {
|
|
1179
1179
|
throw creationError(
|
|
@@ -1359,6 +1359,7 @@ async function startWorker(options, expectedRoutes) {
|
|
|
1359
1359
|
const child = workerCommand(options, "serve").spawn();
|
|
1360
1360
|
const stdout = readFirstLine(child.stdout);
|
|
1361
1361
|
const stderr = readBounded(child.stderr);
|
|
1362
|
+
const status = child.status;
|
|
1362
1363
|
let timeout;
|
|
1363
1364
|
const timer = new Promise((_, reject) => {
|
|
1364
1365
|
timeout = setTimeout(
|
|
@@ -1387,9 +1388,34 @@ async function startWorker(options, expectedRoutes) {
|
|
|
1387
1388
|
);
|
|
1388
1389
|
}
|
|
1389
1390
|
let stopped = false;
|
|
1391
|
+
const failure3 = new Promise((_, reject) => {
|
|
1392
|
+
void status.then(async (result) => {
|
|
1393
|
+
if (stopped) return;
|
|
1394
|
+
if (result.success) {
|
|
1395
|
+
reject(new DevCommandError([{
|
|
1396
|
+
code: "SH_DEV_WORKER_EXITED",
|
|
1397
|
+
severity: "error",
|
|
1398
|
+
summary: "The development worker exited after becoming active",
|
|
1399
|
+
correction: "Inspect the worker diagnostics and retry."
|
|
1400
|
+
}]));
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
reject(await workerFailure(
|
|
1404
|
+
await stderr,
|
|
1405
|
+
"The development worker failed after becoming active"
|
|
1406
|
+
));
|
|
1407
|
+
}, async () => {
|
|
1408
|
+
if (stopped) return;
|
|
1409
|
+
reject(await workerFailure(
|
|
1410
|
+
await stderr,
|
|
1411
|
+
"The development worker failed after becoming active"
|
|
1412
|
+
));
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1390
1415
|
return {
|
|
1391
1416
|
url: `http://${options.hostname}:${options.port}/`,
|
|
1392
1417
|
routeCount: expectedRoutes,
|
|
1418
|
+
failure: failure3,
|
|
1393
1419
|
async stop() {
|
|
1394
1420
|
if (stopped) return;
|
|
1395
1421
|
stopped = true;
|
|
@@ -1620,6 +1646,7 @@ function failure2(error, fallback) {
|
|
|
1620
1646
|
}
|
|
1621
1647
|
function changedPaths(projectRoot, paths) {
|
|
1622
1648
|
const normalized = [];
|
|
1649
|
+
const runtimeDatabaseArtifact = /^\.sleepyhollow\/[^/]+\.(?:sqlite|db)(?:-(?:wal|shm))?$/;
|
|
1623
1650
|
for (const path of paths) {
|
|
1624
1651
|
const absolute = isAbsolute(path) ? resolve2(path) : resolve2(projectRoot, path);
|
|
1625
1652
|
const local = relative(projectRoot, absolute).split(sep).join("/");
|
|
@@ -1630,7 +1657,7 @@ function changedPaths(projectRoot, paths) {
|
|
|
1630
1657
|
"Watch only project-contained application inputs."
|
|
1631
1658
|
);
|
|
1632
1659
|
}
|
|
1633
|
-
if (local === ".git" || local.startsWith(".git/") || local === "generated" || local.startsWith("generated/") || local.includes("/node_modules/") || /(?:^|\/)(?:\.DS_Store|.*(?:\.swp|~))$/.test(local)) continue;
|
|
1660
|
+
if (local === ".git" || local.startsWith(".git/") || local === "generated" || local.startsWith("generated/") || local.includes("/node_modules/") || runtimeDatabaseArtifact.test(local) || /(?:^|\/)(?:\.DS_Store|.*(?:\.swp|~))$/.test(local)) continue;
|
|
1634
1661
|
normalized.push(local);
|
|
1635
1662
|
}
|
|
1636
1663
|
return Object.freeze([...new Set(normalized)].sort());
|
|
@@ -1645,6 +1672,21 @@ function abortReason(signal) {
|
|
|
1645
1672
|
}
|
|
1646
1673
|
return "cancelled";
|
|
1647
1674
|
}
|
|
1675
|
+
async function nextChange(iterator, active) {
|
|
1676
|
+
const change = iterator.next();
|
|
1677
|
+
if (!active?.failure) return change;
|
|
1678
|
+
return Promise.race([
|
|
1679
|
+
change,
|
|
1680
|
+
active.failure.then(() => {
|
|
1681
|
+
throw new DevCommandError([{
|
|
1682
|
+
code: "SH_DEV_WORKER_EXITED",
|
|
1683
|
+
severity: "error",
|
|
1684
|
+
summary: "The active development worker exited unexpectedly",
|
|
1685
|
+
correction: "Inspect the worker diagnostics and retry."
|
|
1686
|
+
}]);
|
|
1687
|
+
})
|
|
1688
|
+
]);
|
|
1689
|
+
}
|
|
1648
1690
|
async function runDevCommand(args, io, suppliedDependencies) {
|
|
1649
1691
|
const parsed = parse2(args);
|
|
1650
1692
|
if ("code" in parsed) {
|
|
@@ -1766,7 +1808,11 @@ ${parsed.correction}`
|
|
|
1766
1808
|
return 1;
|
|
1767
1809
|
}
|
|
1768
1810
|
if (signal.aborted) await closeWatcher();
|
|
1769
|
-
|
|
1811
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
1812
|
+
while (true) {
|
|
1813
|
+
const next = await nextChange(iterator, active);
|
|
1814
|
+
if (next.done) break;
|
|
1815
|
+
const paths = next.value;
|
|
1770
1816
|
if (signal.aborted) break;
|
|
1771
1817
|
const changes = changedPaths(projectRoot, paths);
|
|
1772
1818
|
if (!Array.isArray(changes)) {
|
|
@@ -1919,14 +1965,26 @@ function safeDiagnostic(error, intent) {
|
|
|
1919
1965
|
...typeof item.key === "string" ? { configuration: [item.key] } : {}
|
|
1920
1966
|
}));
|
|
1921
1967
|
}
|
|
1922
|
-
|
|
1968
|
+
const nodeError = error;
|
|
1969
|
+
const nodeCode = typeof nodeError?.code === "string" ? nodeError.code : void 0;
|
|
1970
|
+
const isListenFailure = error instanceof platform.errors.AddrInUse || error instanceof platform.errors.PermissionDenied || nodeError?.syscall === "listen" || nodeCode === "EADDRINUSE" || nodeCode === "EACCES" || nodeCode === "EPERM";
|
|
1971
|
+
if (intent === "serve" && isListenFailure) {
|
|
1923
1972
|
return [{
|
|
1924
1973
|
code: "SH_DEV_BIND_FAILED",
|
|
1925
1974
|
severity: "error",
|
|
1926
|
-
summary: "The loopback listener could not bind",
|
|
1975
|
+
summary: nodeCode ? `The loopback listener failed with ${nodeCode}` : "The loopback listener could not bind",
|
|
1927
1976
|
correction: "Choose an available port and confirm local network access."
|
|
1928
1977
|
}];
|
|
1929
1978
|
}
|
|
1979
|
+
if (intent === "serve") {
|
|
1980
|
+
const cause = error instanceof Error ? `${error.name}: ${error.message}`.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/(?:Bearer|token|password|secret|authorization)[=: ]+\S+/gi, "$1=[redacted]").replace(/(?:[A-Za-z]:)?\/[^\s]+/g, "[path]").slice(0, 180) : void 0;
|
|
1981
|
+
return [{
|
|
1982
|
+
code: "SH_DEV_SERVE_FAILED",
|
|
1983
|
+
severity: "error",
|
|
1984
|
+
summary: cause ? `The development server failed: ${cause}` : "The development server failed",
|
|
1985
|
+
correction: "Inspect the bounded server diagnostic and retry after repairing the host boundary."
|
|
1986
|
+
}];
|
|
1987
|
+
}
|
|
1930
1988
|
return [{
|
|
1931
1989
|
code: "SH_DEV_PROJECT_INVALID",
|
|
1932
1990
|
severity: "error",
|
|
@@ -1998,20 +2056,20 @@ async function runDevWorker(args) {
|
|
|
1998
2056
|
const [intent, projectRoot, hostname, rawPort] = args;
|
|
1999
2057
|
const port = Number(rawPort);
|
|
2000
2058
|
if (hostname !== "127.0.0.1" || !Number.isInteger(port) || port < 1 || port > 65535) return 2;
|
|
2059
|
+
let server;
|
|
2001
2060
|
try {
|
|
2002
2061
|
const { runtime: runtime2, routeCount } = await loadRuntime(projectRoot);
|
|
2003
2062
|
if (intent === "validate") {
|
|
2004
2063
|
console.log(JSON.stringify({ ready: true, routeCount }));
|
|
2005
2064
|
return 0;
|
|
2006
2065
|
}
|
|
2007
|
-
|
|
2066
|
+
server = platform.serve({ hostname, port }, (request) => runtime2.fetch(request));
|
|
2067
|
+
await server.ready;
|
|
2008
2068
|
console.log(JSON.stringify({ ready: true, routeCount }));
|
|
2009
|
-
await
|
|
2010
|
-
server.once("close", resolve5);
|
|
2011
|
-
server.once("error", reject);
|
|
2012
|
-
});
|
|
2069
|
+
await server.finished;
|
|
2013
2070
|
return 0;
|
|
2014
2071
|
} catch (error) {
|
|
2072
|
+
await server?.shutdown().catch(() => void 0);
|
|
2015
2073
|
console.error(
|
|
2016
2074
|
JSON.stringify({
|
|
2017
2075
|
ready: false,
|
|
@@ -2911,7 +2969,7 @@ function renderArtifacts(inventory2) {
|
|
|
2911
2969
|
];
|
|
2912
2970
|
const manifestContent = canonicalJson({
|
|
2913
2971
|
schema: "sleepy-hollow-generated-manifest/v1",
|
|
2914
|
-
generatorVersion: "0.3.
|
|
2972
|
+
generatorVersion: "0.3.4",
|
|
2915
2973
|
serviceId: normalized.serviceId,
|
|
2916
2974
|
inputDigest: digest2(input),
|
|
2917
2975
|
artifacts: Object.fromEntries(
|
|
@@ -3185,7 +3243,7 @@ function inventoryFromRoutes(routes2, options) {
|
|
|
3185
3243
|
return {
|
|
3186
3244
|
serviceId: options.serviceId,
|
|
3187
3245
|
title: options.title ?? options.serviceId,
|
|
3188
|
-
version: options.version ?? "0.3.
|
|
3246
|
+
version: options.version ?? "0.3.4",
|
|
3189
3247
|
...options.description ? { description: options.description } : {},
|
|
3190
3248
|
operations,
|
|
3191
3249
|
securitySchemes: options.securitySchemes ?? {}
|
|
@@ -4039,7 +4097,7 @@ var CLI_COMMANDS = [
|
|
|
4039
4097
|
"generate",
|
|
4040
4098
|
"deploy"
|
|
4041
4099
|
];
|
|
4042
|
-
var CLI_VERSION = "0.3.
|
|
4100
|
+
var CLI_VERSION = "0.3.4";
|
|
4043
4101
|
var commandMetadata = {
|
|
4044
4102
|
create: {
|
|
4045
4103
|
description: "Create one deterministic Sleepy Hollow project.",
|