@darthcav/ts-http-server 0.0.1

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.
Files changed (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +118 -0
  3. package/dist/__tests__/launcher.test.d.ts +2 -0
  4. package/dist/__tests__/launcher.test.d.ts.map +1 -0
  5. package/dist/__tests__/launcher.test.js +116 -0
  6. package/dist/__tests__/launcher.test.js.map +1 -0
  7. package/dist/defaults/defaultErrorHandler.d.ts +14 -0
  8. package/dist/defaults/defaultErrorHandler.d.ts.map +1 -0
  9. package/dist/defaults/defaultErrorHandler.js +58 -0
  10. package/dist/defaults/defaultErrorHandler.js.map +1 -0
  11. package/dist/defaults/defaultFastifyOptions.d.ts +18 -0
  12. package/dist/defaults/defaultFastifyOptions.d.ts.map +1 -0
  13. package/dist/defaults/defaultFastifyOptions.js +25 -0
  14. package/dist/defaults/defaultFastifyOptions.js.map +1 -0
  15. package/dist/defaults/defaultPlugins.d.ts +19 -0
  16. package/dist/defaults/defaultPlugins.d.ts.map +1 -0
  17. package/dist/defaults/defaultPlugins.js +84 -0
  18. package/dist/defaults/defaultPlugins.js.map +1 -0
  19. package/dist/defaults/defaultRoutes.d.ts +10 -0
  20. package/dist/defaults/defaultRoutes.d.ts.map +1 -0
  21. package/dist/defaults/defaultRoutes.js +38 -0
  22. package/dist/defaults/defaultRoutes.js.map +1 -0
  23. package/dist/defaults/getConsoleFastifyLogger.d.ts +15 -0
  24. package/dist/defaults/getConsoleFastifyLogger.d.ts.map +1 -0
  25. package/dist/defaults/getConsoleFastifyLogger.js +20 -0
  26. package/dist/defaults/getConsoleFastifyLogger.js.map +1 -0
  27. package/dist/hooks/onResponse.d.ts +18 -0
  28. package/dist/hooks/onResponse.d.ts.map +1 -0
  29. package/dist/hooks/onResponse.js +26 -0
  30. package/dist/hooks/onResponse.js.map +1 -0
  31. package/dist/hooks/preHandler.d.ts +18 -0
  32. package/dist/hooks/preHandler.d.ts.map +1 -0
  33. package/dist/hooks/preHandler.js +19 -0
  34. package/dist/hooks/preHandler.js.map +1 -0
  35. package/dist/index.d.ts +15 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +14 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/launcher.d.ts +17 -0
  40. package/dist/launcher.d.ts.map +1 -0
  41. package/dist/launcher.js +55 -0
  42. package/dist/launcher.js.map +1 -0
  43. package/dist/start.d.ts +2 -0
  44. package/dist/start.d.ts.map +1 -0
  45. package/dist/start.js +24 -0
  46. package/dist/start.js.map +1 -0
  47. package/dist/types.d.ts +46 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +78 -0
  52. package/src/__tests__/launcher.test.ts +136 -0
  53. package/src/defaults/defaultErrorHandler.ts +69 -0
  54. package/src/defaults/defaultFastifyOptions.ts +29 -0
  55. package/src/defaults/defaultPlugins.ts +90 -0
  56. package/src/defaults/defaultRoutes.ts +42 -0
  57. package/src/defaults/getConsoleFastifyLogger.ts +27 -0
  58. package/src/hooks/onResponse.ts +34 -0
  59. package/src/hooks/preHandler.ts +25 -0
  60. package/src/index.ts +28 -0
  61. package/src/launcher.ts +74 -0
  62. package/src/public/README.md +3 -0
  63. package/src/start.ts +30 -0
  64. package/src/types.ts +55 -0
  65. package/src/views/README.md +3 -0
@@ -0,0 +1,136 @@
1
+ import { equal, ok } from "node:assert/strict"
2
+ import { after, before, describe, it } from "node:test"
3
+ import { setTimeout } from "node:timers/promises"
4
+ import FastifyAccepts from "@fastify/accepts"
5
+ import type { Logger } from "@logtape/logtape"
6
+ import type { RouteOptions } from "fastify"
7
+ import launcher from "../launcher.ts"
8
+ import type { FSTPlugin } from "../types.ts"
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Minimal test logger (no real I/O)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const noop = (): void => {}
15
+ const testLogger = {
16
+ category: ["test"],
17
+ info: noop,
18
+ error: noop,
19
+ warn: noop,
20
+ debug: noop,
21
+ getChild: () => testLogger,
22
+ } as unknown as Logger
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Minimal plugins and routes (no EJS/static required)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const plugins = new Map<string, FSTPlugin>([
29
+ ["@fastify/accepts", { plugin: FastifyAccepts }],
30
+ ])
31
+
32
+ const routes = new Map<string, RouteOptions>([
33
+ [
34
+ "JSON_OK",
35
+ {
36
+ method: "GET",
37
+ url: "/",
38
+ handler: async (_request, reply) => reply.send({ ok: true }),
39
+ },
40
+ ],
41
+ [
42
+ "JSON_405",
43
+ {
44
+ method: ["POST", "PUT", "DELETE", "PATCH"],
45
+ url: "/",
46
+ handler: async (_request, reply) => {
47
+ reply.header("allow", "GET, HEAD")
48
+ return reply.status(405).send({ statusCode: 405 })
49
+ },
50
+ },
51
+ ],
52
+ [
53
+ "ERROR_ROUTE",
54
+ {
55
+ method: "GET",
56
+ url: "/error",
57
+ handler: async () => {
58
+ throw new Error("test server error")
59
+ },
60
+ },
61
+ ],
62
+ ])
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // HTTP server suite
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe("launcher [HTTP]", () => {
69
+ const port = 19001
70
+ const base = `http://localhost:${port}`
71
+ let server: import("fastify").FastifyInstance
72
+
73
+ before(async () => {
74
+ server = launcher({
75
+ logger: testLogger,
76
+ locals: { port },
77
+ plugins,
78
+ routes,
79
+ opts: { disableRequestLogging: true },
80
+ })
81
+ await setTimeout(500)
82
+ })
83
+
84
+ after(async () => {
85
+ await setTimeout(200)
86
+ await server.close()
87
+ })
88
+
89
+ it("GET / → 200 JSON", async () => {
90
+ const res = await fetch(`${base}/`)
91
+ equal(res.status, 200)
92
+ const body = (await res.json()) as { ok: boolean }
93
+ equal(body.ok, true)
94
+ })
95
+
96
+ it("POST / → 405", async () => {
97
+ const res = await fetch(`${base}/`, { method: "POST" })
98
+ equal(res.status, 405)
99
+ equal(res.headers.get("allow"), "GET, HEAD")
100
+ })
101
+
102
+ it("GET /missing → 404 JSON", async () => {
103
+ const res = await fetch(`${base}/missing`, {
104
+ headers: { accept: "application/json" },
105
+ })
106
+ equal(res.status, 404)
107
+ equal(
108
+ res.headers.get("content-type"),
109
+ "application/json; charset=utf-8",
110
+ )
111
+ const body = (await res.json()) as { statusCode: number }
112
+ ok(body.statusCode)
113
+ equal(body.statusCode, 404)
114
+ })
115
+
116
+ it("GET /missing → 404 plain text", async () => {
117
+ const res = await fetch(`${base}/missing`, {
118
+ headers: { accept: "text/plain" },
119
+ })
120
+ equal(res.status, 404)
121
+ })
122
+
123
+ it("GET /error → 500 JSON", async () => {
124
+ const res = await fetch(`${base}/error`, {
125
+ headers: { accept: "application/json" },
126
+ })
127
+ equal(res.status, 500)
128
+ })
129
+
130
+ it("GET /error → 500 plain text", async () => {
131
+ const res = await fetch(`${base}/error`, {
132
+ headers: { accept: "text/plain" },
133
+ })
134
+ equal(res.status, 500)
135
+ })
136
+ })
@@ -0,0 +1,69 @@
1
+ import type { Boom } from "@hapi/boom"
2
+ import type { FastifyError, FastifyReply, FastifyRequest } from "fastify"
3
+
4
+ /**
5
+ * Fastify error handler supporting Boom errors and generic errors.
6
+ *
7
+ * Negotiates the response format via `@fastify/accepts`:
8
+ * - `text/html` — renders `_error.ejs` (requires `@fastify/view`).
9
+ * - `application/json` — JSON payload.
10
+ * - Otherwise — plain-text.
11
+ *
12
+ * For Boom errors the HTTP status code and payload come from `error.output`.
13
+ * For generic errors a 500 is used unless the reply already has a 4xx/5xx status.
14
+ */
15
+ export default async function defaultErrorHandler(
16
+ error: FastifyError,
17
+ request: FastifyRequest,
18
+ reply: FastifyReply,
19
+ ): Promise<void> {
20
+ const accept = request.accepts()
21
+ const boom = error as unknown as Boom
22
+
23
+ if (boom.isBoom) {
24
+ const payload = boom.output.payload
25
+ payload.message = boom.message
26
+
27
+ reply.status(payload.statusCode)
28
+
29
+ switch (accept.type(["html", "json"])) {
30
+ case "html":
31
+ return reply.type("text/html").view("_error", {
32
+ menu_name: "",
33
+ header: `HTTP error ${payload.statusCode} (${payload.message})`,
34
+ uri: request.url,
35
+ status: payload.statusCode,
36
+ message: payload.message,
37
+ })
38
+ case "json":
39
+ return reply.type("application/json").send(payload)
40
+ default:
41
+ return reply
42
+ .type("text/plain")
43
+ .send(`${payload.error} :: ${payload.message}`)
44
+ }
45
+ } else {
46
+ if (
47
+ !reply.statusCode ||
48
+ reply.statusCode < 400 ||
49
+ reply.statusCode > 599
50
+ ) {
51
+ reply.status(500)
52
+ }
53
+
54
+ switch (accept.type(["html", "json"])) {
55
+ case "html":
56
+ return reply.type("text/html").view("_error", {
57
+ menu_name: "",
58
+ header: `HTTP error ${reply.statusCode} (${error.message})`,
59
+ uri: request.url,
60
+ status: reply.statusCode,
61
+ message: error.message,
62
+ })
63
+ case "json":
64
+ return reply.type("application/json").send(error)
65
+ default:
66
+ return reply.type("text/plain").send(JSON.stringify(error))
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,29 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import type { Logger } from "@logtape/logtape"
3
+ import type { FastifyServerOptions } from "fastify"
4
+ import getConsoleFastifyLogger from "./getConsoleFastifyLogger.ts"
5
+
6
+ /**
7
+ * Returns base `FastifyServerOptions` used as defaults in {@link launcher}.
8
+ *
9
+ * Uses `getConsoleFastifyLogger` as `loggerInstance` so that all Fastify
10
+ * internal logs are routed through LogTape under `logger.category`.
11
+ * Built-in per-request logging is disabled in favor of the {@link preHandler}
12
+ * and {@link onResponse} hooks, which emit structured access log lines.
13
+ *
14
+ * Generates request IDs via `crypto.randomUUID()` and enables proxy trust.
15
+ * Callers may override any field by spreading their own options on top.
16
+ *
17
+ * @param logger - The application LogTape logger; its category is used as the
18
+ * base category for Fastify's own logger.
19
+ */
20
+ export default function defaultFastifyOptions(
21
+ logger: Logger,
22
+ ): FastifyServerOptions {
23
+ return {
24
+ genReqId: () => randomUUID(),
25
+ trustProxy: true,
26
+ disableRequestLogging: true,
27
+ loggerInstance: getConsoleFastifyLogger([...logger.category]),
28
+ }
29
+ }
@@ -0,0 +1,90 @@
1
+ import { join } from "node:path"
2
+ import FastifyAccepts from "@fastify/accepts"
3
+ import FastifyCompress from "@fastify/compress"
4
+ import FastifyCors from "@fastify/cors"
5
+ import FastifyEtag from "@fastify/etag"
6
+ import FastifyHelmet from "@fastify/helmet"
7
+ import FastifyStatic from "@fastify/static"
8
+ import FastifyView from "@fastify/view"
9
+ import Ejs from "ejs"
10
+ import type { FSTPlugin, LauncherLocals } from "../types.ts"
11
+
12
+ /**
13
+ * Builds the default plugin map for use with `launcher`.
14
+ *
15
+ * Registers: `@fastify/accepts`, `@fastify/compress`,
16
+ * `@fastify/cors`, `@fastify/etag`, `@fastify/helmet`,
17
+ * `@fastify/view` (EJS), and `@fastify/static`.
18
+ *
19
+ * @param opts.locals - Application locals; `locals.pkg` is exposed as the
20
+ * default context for every EJS view.
21
+ * @param opts.baseDir - Optional base directory for resolving the `src/` folder; defaults to the parent of `import.meta.dirname`.
22
+ * @returns A `Map` of plugin names to plugin entries, suitable for passing as
23
+ * the `plugins` field of `LauncherOptions`.
24
+ */
25
+ export default function defaultPlugins(opts: {
26
+ locals: LauncherLocals
27
+ baseDir?: string | null
28
+ }): Map<string, FSTPlugin> {
29
+ const { locals, baseDir = null } = opts
30
+ const plugins = new Map<string, FSTPlugin>()
31
+ const srcDir = baseDir
32
+ ? join(baseDir, "src")
33
+ : join(import.meta.dirname, "..")
34
+
35
+ plugins.set("@fastify/accepts", {
36
+ plugin: FastifyAccepts,
37
+ })
38
+ plugins.set("@fastify/compress", {
39
+ plugin: FastifyCompress,
40
+ })
41
+ plugins.set("@fastify/cors", {
42
+ plugin: FastifyCors,
43
+ opts: { origin: true },
44
+ })
45
+ plugins.set("@fastify/etag", {
46
+ plugin: FastifyEtag,
47
+ opts: { algorithm: "sha1" },
48
+ })
49
+ plugins.set("@fastify/helmet", {
50
+ plugin: FastifyHelmet,
51
+ opts: {
52
+ global: true,
53
+ contentSecurityPolicy: {
54
+ directives: {
55
+ fontSrc: [
56
+ "'self'",
57
+ "https://fonts.googleapis.com/",
58
+ "https://fonts.gstatic.com/",
59
+ ],
60
+ scriptSrc: [
61
+ "'self'",
62
+ "'unsafe-inline'",
63
+ "https://cdn.jsdelivr.net/",
64
+ ],
65
+ },
66
+ },
67
+ hsts: {
68
+ maxAge: 31536000,
69
+ includeSubDomains: true,
70
+ },
71
+ },
72
+ })
73
+ plugins.set("@fastify/view", {
74
+ plugin: FastifyView,
75
+ opts: {
76
+ engine: { ejs: Ejs },
77
+ root: join(srcDir, "views"),
78
+ defaultContext: { pkg: locals.pkg },
79
+ },
80
+ })
81
+ plugins.set("@fastify/static", {
82
+ plugin: FastifyStatic,
83
+ opts: {
84
+ root: join(srcDir, "public"),
85
+ prefix: "/",
86
+ },
87
+ })
88
+
89
+ return plugins
90
+ }
@@ -0,0 +1,42 @@
1
+ import { methodNotAllowed, notAcceptable } from "@hapi/boom"
2
+ import type { RouteOptions } from "fastify"
3
+
4
+ /**
5
+ * Returns the default route map used in {@link launcher}.
6
+ *
7
+ * Registers:
8
+ * - `GET /` — renders `index.ejs` for `text/html`, throws 406 otherwise.
9
+ * - `DELETE|PATCH|POST|PUT|OPTIONS /` — responds with 405 Method Not Allowed.
10
+ */
11
+ export default function defaultRoutes(): Map<string, RouteOptions> {
12
+ const routes = new Map<string, RouteOptions>()
13
+
14
+ routes.set("INDEX", {
15
+ method: "GET",
16
+ url: "/",
17
+ exposeHeadRoute: true,
18
+ handler: async (request, reply) => {
19
+ const accept = request.accepts()
20
+ switch (accept.type(["html"])) {
21
+ case "html":
22
+ return reply.type("text/html").view("index.ejs", {
23
+ menu_name: "index",
24
+ header: "Welcome page",
25
+ })
26
+ default:
27
+ throw notAcceptable()
28
+ }
29
+ },
30
+ })
31
+
32
+ routes.set("INDEX_405", {
33
+ method: ["DELETE", "PATCH", "POST", "PUT", "OPTIONS"],
34
+ url: "/",
35
+ handler: async (_request, reply) => {
36
+ reply.header("allow", "GET, HEAD")
37
+ throw methodNotAllowed()
38
+ },
39
+ })
40
+
41
+ return routes
42
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ getLogTapeFastifyLogger,
3
+ type PinoLevel,
4
+ type PinoLikeLogger,
5
+ } from "@logtape/fastify"
6
+
7
+ /**
8
+ * Returns a `PinoLikeLogger` for Fastify scoped to the given category.
9
+ *
10
+ * Assumes LogTape has already been configured by the caller (e.g. via
11
+ * `getConsoleLogger`). All records at or above `level` are forwarded
12
+ * by Fastify to the active LogTape sinks.
13
+ *
14
+ * @param name - Logger category array passed to `getLogTapeFastifyLogger`
15
+ * (e.g. `["my-app", "fastify"]`).
16
+ * @param level - Minimum Pino log level to pass through. Defaults to `"info"`.
17
+ * @returns A Pino-compatible logger backed by LogTape.
18
+ */
19
+ export default function getConsoleFastifyLogger(
20
+ name: string[],
21
+ level: PinoLevel = "info",
22
+ ): PinoLikeLogger {
23
+ return getLogTapeFastifyLogger({
24
+ category: name,
25
+ level,
26
+ })
27
+ }
@@ -0,0 +1,34 @@
1
+ import type { FastifyReply, FastifyRequest } from "fastify"
2
+
3
+ /**
4
+ * Fastify `onResponse` hook that logs completed request details:
5
+ *
6
+ * ```
7
+ * {ip} -- {method} {url} HTTP/{httpVersion} {status} {size} {elapsed}ms "{referrer}" "{userAgent}"
8
+ * ```
9
+ *
10
+ * Intended to be registered via:
11
+ * ```ts
12
+ * fastify.addHook("onResponse", onResponse)
13
+ * ```
14
+ *
15
+ * Uses `reply.log.info` so each log record is automatically correlated with
16
+ * the request ID assigned by Fastify.
17
+ */
18
+ export default async function onResponse(
19
+ request: FastifyRequest,
20
+ reply: FastifyReply,
21
+ ): Promise<void> {
22
+ const contentLength = reply.getHeader("content-length")
23
+ const size = contentLength != null ? Number(contentLength) : "-"
24
+
25
+ if (reply.statusCode < 400) {
26
+ reply.log.info(
27
+ `${request.ip} -- ${request.method} ${request.url} HTTP/${request.raw.httpVersion} ${reply.statusCode} ${size} ${Math.round(reply.elapsedTime)}ms "${request.headers["referer"] ?? "-"}" "${request.headers["user-agent"] ?? "-"}"`,
28
+ )
29
+ } else {
30
+ reply.log.error(
31
+ `${request.ip} -- ${request.method} ${request.url} HTTP/${request.raw.httpVersion} ${reply.statusCode} ${size} ${Math.round(reply.elapsedTime)}ms "${request.headers["referer"] ?? "-"}" "${request.headers["user-agent"] ?? "-"}"`,
32
+ )
33
+ }
34
+ }
@@ -0,0 +1,25 @@
1
+ import type { FastifyReply, FastifyRequest } from "fastify"
2
+
3
+ /**
4
+ * Fastify `preHandler` hook that logs incoming request details:
5
+ *
6
+ * ```
7
+ * Incoming request: {method} {url} HTTP/{httpVersion} from {ip}
8
+ * ```
9
+ *
10
+ * Intended to be registered via:
11
+ * ```ts
12
+ * fastify.addHook("preHandler", preHandler)
13
+ * ```
14
+ *
15
+ * Uses `request.log.info` so each log record is automatically correlated with
16
+ * the request ID assigned by Fastify.
17
+ */
18
+ export default async function preHandler(
19
+ request: FastifyRequest,
20
+ _reply: FastifyReply,
21
+ ): Promise<void> {
22
+ request.log.debug(
23
+ `Incoming request [${request.id}]: ${request.method} ${request.url} HTTP/${request.raw.httpVersion} from ${request.ip}`,
24
+ )
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A TypeScript HTTP server library built on Fastify for Node.js >= 25.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ import defaultErrorHandler from "./defaults/defaultErrorHandler.ts"
8
+ import defaultFastifyOptions from "./defaults/defaultFastifyOptions.ts"
9
+ import defaultPlugins from "./defaults/defaultPlugins.ts"
10
+ import defaultRoutes from "./defaults/defaultRoutes.ts"
11
+ import onResponse from "./hooks/onResponse.ts"
12
+ import preHandler from "./hooks/preHandler.ts"
13
+ import launcher from "./launcher.ts"
14
+
15
+ export type {
16
+ FSTPlugin,
17
+ LauncherLocals,
18
+ LauncherOptions,
19
+ } from "./types.ts"
20
+ export {
21
+ defaultErrorHandler,
22
+ defaultFastifyOptions,
23
+ defaultPlugins,
24
+ defaultRoutes,
25
+ launcher,
26
+ preHandler,
27
+ onResponse,
28
+ }
@@ -0,0 +1,74 @@
1
+ import process, { env } from "node:process"
2
+ import { notFound } from "@hapi/boom"
3
+ import Fastify, { type FastifyInstance } from "fastify"
4
+ import defaultErrorHandler from "./defaults/defaultErrorHandler.ts"
5
+ import defaultFastifyOptions from "./defaults/defaultFastifyOptions.ts"
6
+ import { onResponse, preHandler } from "./index.ts"
7
+ import type { LauncherOptions } from "./types.ts"
8
+
9
+ /**
10
+ * Creates and starts a Fastify HTTP server with the given configuration.
11
+ *
12
+ * Steps performed:
13
+ * 1. Creates a `FastifyInstance` merging {@link defaultFastifyOptions} with `opts`.
14
+ * 2. Decorates the instance with `locals` and any extra `decorators`.
15
+ * 3. Registers all `plugins` and `routes`.
16
+ * 4. Sets a `notFound` handler (throws Boom 404) and {@link defaultErrorHandler}.
17
+ * 5. Registers {@link preHandler} and {@link onResponse} hooks.
18
+ * 6. Calls `fastify.listen()` and invokes the optional `done` callback.
19
+ *
20
+ * @returns The `FastifyInstance` (e.g. for use with `fastify.close()`).
21
+ */
22
+ export default function launcher({
23
+ logger,
24
+ locals,
25
+ plugins,
26
+ routes,
27
+ decorators,
28
+ opts,
29
+ done,
30
+ }: LauncherOptions): FastifyInstance {
31
+ const host = locals?.host ?? "localhost"
32
+ const port = locals?.port ?? Number(env["CONTAINER_EXPOSE_PORT"] ?? 8888)
33
+
34
+ const fastify = Fastify({
35
+ ...defaultFastifyOptions(logger),
36
+ ...opts,
37
+ })
38
+
39
+ fastify.decorate("locals", locals)
40
+
41
+ if (decorators instanceof Map) {
42
+ for (const [key, value] of decorators) {
43
+ fastify.decorate(key, value)
44
+ }
45
+ }
46
+
47
+ for (const value of plugins.values()) {
48
+ void fastify.register(value.plugin, value.opts ?? {})
49
+ }
50
+
51
+ for (const value of routes.values()) {
52
+ fastify.route(value)
53
+ }
54
+
55
+ fastify.setNotFoundHandler(async (_request, _reply) => {
56
+ throw notFound()
57
+ })
58
+
59
+ fastify.setErrorHandler(defaultErrorHandler)
60
+
61
+ // TODO: Add hook for `onRequestAbort`
62
+ fastify.addHook("preHandler", preHandler)
63
+ fastify.addHook("onResponse", onResponse)
64
+
65
+ fastify.listen({ host, port }, (error) => {
66
+ if (error) {
67
+ logger.error(`${error.message}`)
68
+ process.exit(1)
69
+ }
70
+ done?.()
71
+ })
72
+
73
+ return fastify
74
+ }
@@ -0,0 +1,3 @@
1
+ # Public directory
2
+
3
+ Placeholder directory for static files.
package/src/start.ts ADDED
@@ -0,0 +1,30 @@
1
+ import process from "node:process"
2
+ import { getConsoleLogger, main } from "@darthcav/ts-utils"
3
+ import pkg from "../package.json" with { type: "json" }
4
+ import { defaultPlugins, defaultRoutes, launcher } from "./index.ts"
5
+
6
+ const logger = await getConsoleLogger(pkg.name, "info")
7
+
8
+ main(pkg.name, logger, false, () => {
9
+ const locals = { pkg }
10
+ const plugins = defaultPlugins({ locals })
11
+ const routes = defaultRoutes()
12
+
13
+ const fastify = launcher({ logger, locals, plugins, routes })
14
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
15
+ process.on(signal, async (signal) =>
16
+ fastify
17
+ .close()
18
+ .then(() => {
19
+ logger.error(
20
+ `Process interrupted and server closed. Received signal: ${signal}`,
21
+ )
22
+ process.exit(0)
23
+ })
24
+ .catch((error) => {
25
+ logger.error(`Server shutdown error: ${error}`)
26
+ process.exit(1)
27
+ }),
28
+ )
29
+ }
30
+ })
package/src/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Logger } from "@logtape/logtape"
2
+ import type {
3
+ FastifyPluginAsync,
4
+ FastifyPluginCallback,
5
+ FastifyPluginOptions,
6
+ FastifyServerOptions,
7
+ RouteOptions,
8
+ } from "fastify"
9
+
10
+ /**
11
+ * A plugin entry combining a Fastify plugin function with its options,
12
+ * stored in the plugins `Map` passed to {@link launcher}.
13
+ */
14
+ export type FSTPlugin = {
15
+ /** The Fastify plugin function to register. */
16
+ // biome-ignore lint/suspicious/noExplicitAny: third-party plugins use varied option types
17
+ plugin: FastifyPluginCallback<any> | FastifyPluginAsync<any>
18
+ /** Optional options forwarded to the plugin on registration. */
19
+ opts?: FastifyPluginOptions
20
+ }
21
+
22
+ /**
23
+ * Application locals decorated onto the Fastify instance and available
24
+ * throughout the request lifecycle.
25
+ */
26
+ export type LauncherLocals = {
27
+ /** Package metadata (e.g. contents of `package.json`). */
28
+ pkg?: object
29
+ /** Hostname the server will bind to. */
30
+ host?: string
31
+ /** Port the server will listen on. */
32
+ port?: number
33
+ /** Any additional application-specific locals. */
34
+ [key: string]: unknown
35
+ }
36
+
37
+ /**
38
+ * Options passed to the {@link launcher} function.
39
+ */
40
+ export type LauncherOptions = {
41
+ /** Logger instance used for error and info output. */
42
+ logger: Logger
43
+ /** Application locals decorated onto the Fastify instance. */
44
+ locals: LauncherLocals
45
+ /** Map of named plugins to register. */
46
+ plugins: Map<string, FSTPlugin>
47
+ /** Map of named routes to register. */
48
+ routes: Map<string, RouteOptions>
49
+ /** Map of named decorators to add to the Fastify instance. */
50
+ decorators?: Map<string, unknown>
51
+ /** Optional Fastify server options (merged over {@link defaultFastifyOptions}). */
52
+ opts?: FastifyServerOptions
53
+ /** Optional callback invoked once the server is listening. */
54
+ done?: () => void
55
+ }
@@ -0,0 +1,3 @@
1
+ # Views directory
2
+
3
+ Placeholder directory for views files.