| author | |
| committer | |
| log | 78996015dfc479cea728824a842a22cd9f1021cf |
| tree | 251816cc3c35f2c6fec71d4d12a7ff23acb584a7 |
| parent | a0b0784cd1344ffe9d30881135834a71122fb843 |
| signature | Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs |
this implements easy web server without a third party module. since this
is a very simple function, clover library spices it up with
`@clo/lib/progress` support. the library is feeling extremely unified,
we even got a `@clo/lib/subprocess` API.
implements #8515 files changed, 263 insertions(+), 69 deletions(-)
framework/backend/entry-node.ts+8-25| ... | ... | @@ -1,34 +1,19 @@ |
| 1 | 1 | const app = require(globalThis.CLOVER_SERVER_ENTRY ?? process.argv[2]); |
| 2 | 2 | |
| 3 | const protocol = "http"; | |
| 4 | ||
| 5 | const server = serve({ | |
| 6 | fetch: app.default.fetch, | |
| 3 | const server = await http.serve({ | |
| 4 | respond: app.default.fetch, | |
| 7 | 5 | port: Number(process.env.PORT ?? 3000), |
| 8 | }, ({ address, port }) => { | |
| 9 | if (address === "::" || address === "127.0.0.1") { | |
| 10 | address = os.hostname().replace(/\.local$/, ""); | |
| 11 | } | |
| 12 | console.info( | |
| 13 | "live at " + url.format({ | |
| 14 | protocol, | |
| 15 | hostname: address, | |
| 16 | port, | |
| 17 | }), | |
| 18 | ); | |
| 19 | 6 | }); |
| 7 | console.info(`live at ${server.url}`); | |
| 20 | 8 | |
| 21 | 9 | process.on("SIGINT", () => { |
| 22 | server.close(); | |
| 23 | process.exit(0); | |
| 10 | server.close().then(() => { | |
| 11 | process.exit(0); | |
| 12 | }); | |
| 24 | 13 | }); |
| 25 | 14 | |
| 26 | 15 | process.on("SIGTERM", () => { |
| 27 | server.close((err) => { | |
| 28 | if (err) { | |
| 29 | console.error(err); | |
| 30 | process.exit(1); | |
| 31 | } | |
| 16 | server.close().then(() => { | |
| 32 | 17 | process.exit(0); |
| 33 | 18 | }); |
| 34 | 19 | }); |
| ... | ... | @@ -38,8 +23,6 @@ declare global { |
| 38 | 23 | var CLOVER_SERVER_ENTRY: string; |
| 39 | 24 | } |
| 40 | 25 | |
| 41 | import * as os from "os"; | |
| 42 | import * as url from "node:url"; | |
| 43 | import { serve } from "@hono/node-server"; | |
| 26 | import * as http from "@clo/lib/http"; | |
| 44 | 27 | import process from "node:process"; |
| 45 | 28 | import * as console from "@clo/lib/log"; |
framework/lib/view.ts+1-1| ... | ... | @@ -118,7 +118,7 @@ export function regenerate(tag: RegenKey) { |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | 120 | import * as meta from "@clo/lib/render/meta"; |
| 121 | import type * as hono from "#hono"; | |
| 121 | import type * as hono from "hono"; | |
| 122 | 122 | import * as render from "@clo/lib/render"; |
| 123 | 123 | import * as sg from "./sitegen.ts"; |
| 124 | 124 | import * as asset from "./assets.ts"; |
lib/examples/server.ts created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | export async function main() { | |
| 2 | const server = await http.serve({ | |
| 3 | port: 3000, | |
| 4 | ||
| 5 | async respond(request, { node }) { | |
| 6 | node.total = 50; | |
| 7 | for(let i = 0; i < 50; i += 1) { | |
| 8 | await async.delay(5); | |
| 9 | node.value += 1; | |
| 10 | } | |
| 11 | return new Response("hello!"); | |
| 12 | }, | |
| 13 | ||
| 14 | progress: progress.start('my web server'), | |
| 15 | }); | |
| 16 | log.info(`up: ${server.url}`); | |
| 17 | ||
| 18 | } | |
| 19 | ||
| 20 | import * as async from "@clo/lib/async"; | |
| 21 | import * as http from "@clo/lib/http"; | |
| 22 | import * as log from "@clo/lib/log"; | |
| 23 | import * as progress from "@clo/lib/progress"; |
lib/http.ts+135-2| ... | ... | @@ -1,8 +1,137 @@ |
| 1 | export interface Server { | |
| 2 | port: number; | |
| 3 | url: string; | |
| 4 | close(): Promise<void>; | |
| 5 | [Symbol.asyncDispose](): Promise<void>; | |
| 6 | } | |
| 7 | ||
| 8 | export interface ServeOptions { | |
| 9 | /** respond to a server request */ | |
| 10 | respond: (request: Request, opts: ServeContext) => Promise<Response>; | |
| 11 | /** port 0 hosts on a random port */ | |
| 12 | port: number; | |
| 13 | ||
| 14 | progress?: progress.Ref; | |
| 15 | } | |
| 16 | ||
| 17 | export interface ServeContext { | |
| 18 | node: progress.Node; | |
| 19 | } | |
| 20 | ||
| 21 | /** | |
| 22 | * this server is not optimized performance and likely includes bugs. | |
| 23 | * | |
| 24 | * more production ready alternatives: `srvx` and `@hono/node-server`. | |
| 25 | */ | |
| 26 | export async function serve( | |
| 27 | { respond, port, progress: rootNode = progress.nullNode }: ServeOptions, | |
| 28 | ): Promise<Server> { | |
| 29 | const nodeHttp = node.builtin( | |
| 30 | "http", | |
| 31 | ) as unknown as typeof import("node:http"); | |
| 32 | const stream = node.builtin("stream"); | |
| 33 | ASSERT( | |
| 34 | nodeHttp && stream, | |
| 35 | "http.serve must be called in a node.js compatible runtime", | |
| 36 | ); | |
| 37 | ||
| 38 | const server = nodeHttp.createServer((req, res) => { | |
| 39 | const headers = new Headers(); | |
| 40 | for (const key in req.headers) { | |
| 41 | const value = req.headers[key]; | |
| 42 | if (value == null) continue; | |
| 43 | if (Array.isArray(value)) { | |
| 44 | for (const item of value) headers.append(key, item); | |
| 45 | } else { | |
| 46 | headers.append(key, value); | |
| 47 | } | |
| 48 | } | |
| 49 | const method = UNWRAP(req.method).toUpperCase(); | |
| 50 | const hasNoBody = method === "GET" || method === "HEAD"; | |
| 51 | const request = new Request( | |
| 52 | "http://" + (req.headers.host ?? "localhost") + UNWRAP(req.url), | |
| 53 | { | |
| 54 | method, | |
| 55 | headers, | |
| 56 | body: hasNoBody | |
| 57 | ? undefined | |
| 58 | : stream.Readable.toWeb(req), | |
| 59 | duplex: hasNoBody ? undefined : 'half', | |
| 60 | }, | |
| 61 | ); | |
| 62 | const requestNode = rootNode.start(`${method} ${req.url}`); | |
| 63 | respond(request, { node: requestNode }) | |
| 64 | .then((response) => { | |
| 65 | res.writeHead( | |
| 66 | response.status, | |
| 67 | response.statusText, | |
| 68 | Array.from(response.headers.entries()), | |
| 69 | ); | |
| 70 | if (response.body) { | |
| 71 | stream.Readable.fromWeb(response.body).pipe(res); | |
| 72 | } else { | |
| 73 | res.end(); | |
| 74 | } | |
| 75 | }) | |
| 76 | .catch((err) => { | |
| 77 | console.error(err); | |
| 78 | }) | |
| 79 | .finally(() => { | |
| 80 | requestNode.end(); | |
| 81 | }); | |
| 82 | }); | |
| 83 | ||
| 84 | const actualPort = await new Promise<number>((resolve, reject) => { | |
| 85 | async function onError(err: unknown) { | |
| 86 | if (error.code(err) !== "EADDRINUSE") { | |
| 87 | reject(err); | |
| 88 | return; | |
| 89 | } | |
| 90 | const info = await subprocess.lookupProcessOnPort(port).catch(() => null); | |
| 91 | if (!info) return void reject(err); | |
| 92 | ||
| 93 | reject( | |
| 94 | new Error( | |
| 95 | `Port ${port} is in use by ${ | |
| 96 | JSON.stringify(info.command) | |
| 97 | } (pid ${info.pid})`, | |
| 98 | ), | |
| 99 | ); | |
| 100 | } | |
| 101 | server.on("error", onError); | |
| 102 | server.listen(port, () => { | |
| 103 | server.off("error", onError); | |
| 104 | const addr = server.address(); | |
| 105 | ASSERT(addr && typeof addr === "object"); | |
| 106 | resolve(addr.port); | |
| 107 | }); | |
| 108 | }); | |
| 109 | ||
| 110 | function close() { | |
| 111 | return new Promise<void>((resolve, reject) => { | |
| 112 | server.close((err) => { | |
| 113 | if (err) reject(err); | |
| 114 | else resolve(); | |
| 115 | }); | |
| 116 | }); | |
| 117 | } | |
| 118 | ||
| 119 | return { | |
| 120 | port: actualPort, | |
| 121 | url: `http://localhost${actualPort === 80 ? "" : ":" + actualPort}/`, | |
| 122 | close, | |
| 123 | [Symbol.asyncDispose]: close, | |
| 124 | }; | |
| 125 | } | |
| 126 | ||
| 1 | 127 | /** |
| 2 | 128 | * parses the [`Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests) |
| 3 | 129 | * header, returning `null` if it is invalid. the `fileSize` |
| 4 | 130 | */ |
| 5 | export function parseRangeHeader(rangeHeader: string, fileSize: number): Range[] | null { | |
| 131 | export function parseRangeHeader( | |
| 132 | rangeHeader: string, | |
| 133 | fileSize: number, | |
| 134 | ): Range[] | null { | |
| 6 | 135 | const [unit, ranges] = rangeHeader.split("="); |
| 7 | 136 | if (!ranges) return null; |
| 8 | 137 | if (unit !== "bytes") return null; |
| ... | ... | @@ -39,4 +168,8 @@ export interface Range { |
| 39 | 168 | end: number; |
| 40 | 169 | } |
| 41 | 170 | |
| 42 | import { UNWRAP } from "./assert.ts"; | |
| 171 | import { ASSERT, UNWRAP } from "./assert.ts"; | |
| 172 | import * as node from "./node.ts"; | |
| 173 | import * as subprocess from "./subprocess.ts"; | |
| 174 | import * as error from "./error.ts"; | |
| 175 | import * as progress from "./progress.ts"; |
lib/node.ts+4| ... | ... | @@ -62,6 +62,10 @@ interface Builtins { |
| 62 | 62 | ): string; |
| 63 | 63 | }; |
| 64 | 64 | "stream": { |
| 65 | Readable: { | |
| 66 | toWeb(nodeStream: unknown): ReadableStream; | |
| 67 | fromWeb(webStream: ReadableStream): { pipe: (other: unknown) => void}; | |
| 68 | }; | |
| 65 | 69 | Writable: { |
| 66 | 70 | prototype: { |
| 67 | 71 | write: (value: string) => boolean; |
lib/subprocess.ts+75| ... | ... | @@ -86,8 +86,83 @@ export async function spawnAndWait( |
| 86 | 86 | // } |
| 87 | 87 | // }); |
| 88 | 88 | |
| 89 | ||
| 90 | async function findInodeForPort(port) { | |
| 91 | const hexPort = port.toString(16).toUpperCase().padStart(4, '0'); | |
| 92 | for (const f of ['/proc/net/tcp', '/proc/net/tcp6']) { | |
| 93 | try { | |
| 94 | const lines = (await fs.readFile(f, 'utf8')).split('\n').slice(1); | |
| 95 | for (const line of lines) { | |
| 96 | const parts = line.trim().split(/\s+/); | |
| 97 | if (parts[1]?.split(':')[1] === hexPort) return parts[9]; | |
| 98 | } | |
| 99 | } catch {} | |
| 100 | } | |
| 101 | return null; | |
| 102 | } | |
| 103 | ||
| 104 | async function findPidForInode(inode) { | |
| 105 | const target = `socket:[${inode}]`; | |
| 106 | for (const pid of await fs.readdir('/proc')) { | |
| 107 | if (!/^\d+$/.test(pid)) continue; | |
| 108 | try { | |
| 109 | for (const fd of await fs.readdir(`/proc/${pid}/fd`)) { | |
| 110 | try { | |
| 111 | if (await fs.readlink(`/proc/${pid}/fd/${fd}`) === target) return pid; | |
| 112 | } catch {} | |
| 113 | } | |
| 114 | } catch {} | |
| 115 | } | |
| 116 | return null; | |
| 117 | } | |
| 118 | ||
| 119 | async function commandFromPortLinux(port: number) { | |
| 120 | const inode = await findInodeForPort(port); | |
| 121 | if (!inode) throw new Error(`no process on port ${port}`); | |
| 122 | const pid = await findPidForInode(inode); | |
| 123 | if (!pid) throw new Error(`no pid for inode ${inode}`); | |
| 124 | const raw = await fs.readFile(`/proc/${pid}/cmdline`, 'utf8'); | |
| 125 | return { pid: Number(pid), command: raw.split('\0').filter(Boolean).join(' ') }; | |
| 126 | } | |
| 127 | ||
| 128 | async function commandFromPortMac(port: number) { | |
| 129 | const { stdout: lsofOut } = await exec('lsof', ['-ti', `tcp:${port}`]); | |
| 130 | const pid = lsofOut.trim().split('\n')[0]; | |
| 131 | if (!pid) throw new Error(`no process on port ${port}`); | |
| 132 | const { stdout: psOut } = await exec('ps', ['-p', pid, '-o', 'args=']); | |
| 133 | return { pid: Number(pid), command: psOut.trim() }; | |
| 134 | } | |
| 135 | ||
| 136 | async function commandFromPortWindows(port: number) { | |
| 137 | const { stdout } = await exec('netstat', ['-ano']); | |
| 138 | const line = stdout.split('\n').find(l => { | |
| 139 | const m = l.match(/\s+[\d.:]+:(\d+)\s+/); | |
| 140 | return m && parseInt(m[1]!) === port && l.includes('LISTENING'); | |
| 141 | }); | |
| 142 | if (!line) throw new Error(`no process on port ${port}`); | |
| 143 | const pid = line.trim().split(/\s+/).at(-1); | |
| 144 | const { stdout: psOut } = await exec('powershell', [ | |
| 145 | '-NoProfile', '-Command', | |
| 146 | `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine` | |
| 147 | ]); | |
| 148 | const cmdline = psOut.trim(); | |
| 149 | if (!cmdline) throw new Error(`no cmdline for pid ${pid}`); | |
| 150 | return { pid: Number(pid), command: cmdline }; | |
| 151 | } | |
| 152 | ||
| 153 | export async function lookupProcessOnPort(port: number) { | |
| 154 | switch (os.platform()) { | |
| 155 | case 'linux': return commandFromPortLinux(port); | |
| 156 | case 'darwin': return commandFromPortMac(port); | |
| 157 | case 'win32': return commandFromPortWindows(port); | |
| 158 | default: throw new Error(`unsupported platform: ${os.platform()}`); | |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 89 | 162 | // import * as process from "node:process"; |
| 90 | 163 | import * as events from "node:events"; |
| 91 | 164 | import * as util from "node:util"; |
| 92 | 165 | import * as child_process from "node:child_process"; |
| 166 | import * as fs from 'node:fs/promises'; | |
| 167 | import os from 'os'; | |
| 93 | 168 | import { Buffer } from "node:buffer"; |
package-lock.json+6-20| ... | ... | @@ -6,10 +6,9 @@ |
| 6 | 6 | "": { |
| 7 | 7 | "dependencies": { |
| 8 | 8 | "@clo/lib": "file:lib", |
| 9 | "@clo/markodown": "npm:@paperclover/markodown", | |
| 10 | "@hono/node-server": "1.19.11", | |
| 9 | "@clo/markodown": "npm:@paperclover/markodown@1.0.0", | |
| 11 | 10 | "@marko/compiler": "5.39.62", |
| 12 | "@types/node": "^24.12.0", | |
| 11 | "@types/node": "24.x", | |
| 13 | 12 | "dashjs": "5.0.3", |
| 14 | 13 | "esbuild": "0.25.5", |
| 15 | 14 | "harfbuzzjs": "0.10.1", |
| ... | ... | @@ -19,7 +18,7 @@ |
| 19 | 18 | "msgpackr": "1.11.5", |
| 20 | 19 | "playwright": "1.58.2", |
| 21 | 20 | "source-map-support": "0.5.21", |
| 22 | "typescript": "^5.9.3", | |
| 21 | "typescript": "6.0.2", | |
| 23 | 22 | "unique-names-generator": "4.7.1", |
| 24 | 23 | "vitest": "4.1.2", |
| 25 | 24 | "vscode-oniguruma": "2.0.1", |
| ... | ... | @@ -507,18 +506,6 @@ |
| 507 | 506 | "node": ">=18" |
| 508 | 507 | } |
| 509 | 508 | }, |
| 510 | "node_modules/@hono/node-server": { | |
| 511 | "version": "1.19.11", | |
| 512 | "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", | |
| 513 | "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", | |
| 514 | "license": "MIT", | |
| 515 | "engines": { | |
| 516 | "node": ">=18.14.1" | |
| 517 | }, | |
| 518 | "peerDependencies": { | |
| 519 | "hono": "^4" | |
| 520 | } | |
| 521 | }, | |
| 522 | 509 | "node_modules/@jridgewell/sourcemap-codec": { |
| 523 | 510 | "version": "1.5.5", |
| 524 | 511 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", |
| ... | ... | @@ -1411,7 +1398,6 @@ |
| 1411 | 1398 | "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", |
| 1412 | 1399 | "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", |
| 1413 | 1400 | "license": "MIT", |
| 1414 | "peer": true, | |
| 1415 | 1401 | "engines": { |
| 1416 | 1402 | "node": ">=16.9.0" |
| 1417 | 1403 | } |
| ... | ... | @@ -2205,9 +2191,9 @@ |
| 2205 | 2191 | } |
| 2206 | 2192 | }, |
| 2207 | 2193 | "node_modules/typescript": { |
| 2208 | "version": "5.9.3", | |
| 2209 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | |
| 2210 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | |
| 2194 | "version": "6.0.2", | |
| 2195 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", | |
| 2196 | "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", | |
| 2211 | 2197 | "license": "Apache-2.0", |
| 2212 | 2198 | "bin": { |
| 2213 | 2199 | "tsc": "bin/tsc", |
package.json+4-12| ... | ... | @@ -2,10 +2,9 @@ |
| 2 | 2 | "type": "module", |
| 3 | 3 | "dependencies": { |
| 4 | 4 | "@clo/lib": "file:lib", |
| 5 | "@clo/markodown": "npm:@paperclover/markodown", | |
| 6 | "@hono/node-server": "1.19.11", | |
| 5 | "@clo/markodown": "npm:@paperclover/markodown@1.0.0", | |
| 7 | 6 | "@marko/compiler": "5.39.62", |
| 8 | "@types/node": "^24.12.0", | |
| 7 | "@types/node": "24.x", | |
| 9 | 8 | "dashjs": "5.0.3", |
| 10 | 9 | "esbuild": "0.25.5", |
| 11 | 10 | "harfbuzzjs": "0.10.1", |
| ... | ... | @@ -15,7 +14,7 @@ |
| 15 | 14 | "msgpackr": "1.11.5", |
| 16 | 15 | "playwright": "1.58.2", |
| 17 | 16 | "source-map-support": "0.5.21", |
| 18 | "typescript": "^5.9.3", | |
| 17 | "typescript": "6.0.2", | |
| 19 | 18 | "unique-names-generator": "4.7.1", |
| 20 | 19 | "vitest": "4.1.2", |
| 21 | 20 | "vscode-oniguruma": "2.0.1", |
| ... | ... | @@ -27,13 +26,6 @@ |
| 27 | 26 | "#sitegen": "./framework/lib/sitegen.ts", |
| 28 | 27 | "#sitegen/*": "./framework/lib/*.ts", |
| 29 | 28 | "#jsx/jsx-runtime": "./lib/render/jsx-runtime.ts", |
| 30 | "#jsx/jsx-dev-runtime": "./lib/render/jsx-runtime.ts", | |
| 31 | "#hono": "hono", | |
| 32 | "#hono/conninfo": { | |
| 33 | "bun": "hono/bun", | |
| 34 | "deno": "hono/deno", | |
| 35 | "node": "@hono/node-server/conninfo", | |
| 36 | "worker": "hono/cloudflare-workers" | |
| 37 | } | |
| 29 | "#jsx/jsx-dev-runtime": "./lib/render/jsx-runtime.ts" | |
| 38 | 30 | } |
| 39 | 31 | } |
src/backend.ts+1-1| ... | ... | @@ -59,7 +59,7 @@ async function removeDuplicateSlashes(c: Context, next: Next) { |
| 59 | 59 | await next(); |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | import { type Context, Hono, type Next } from "#hono"; | |
| 62 | import { type Context, Hono, type Next } from "hono"; | |
| 63 | 63 | import { logger } from "hono/logger"; |
| 64 | 64 | import { trimTrailingSlash } from "hono/trailing-slash"; |
| 65 | 65 | import * as assets from "#sitegen/assets"; |
src/blog/backend.ts+1-1| ... | ... | @@ -16,5 +16,5 @@ app.get("/blog/community-translations", async (c) => { |
| 16 | 16 | |
| 17 | 17 | app.get("/blog/*", assets.notFound); |
| 18 | 18 | |
| 19 | import { Hono } from "#hono"; | |
| 19 | import { Hono } from "hono"; | |
| 20 | 20 | import * as assets from "#sitegen/assets"; |
src/blog/drafts/webdev/deranged-typescript.md+1-1| ... | ... | @@ -66,7 +66,7 @@ export default app; |
| 66 | 66 | |
| 67 | 67 | ... |
| 68 | 68 | |
| 69 | import { type Context, Hono, type Next } from "#hono"; | |
| 69 | import { type Context, Hono, type Next } from "hono"; | |
| 70 | 70 | import { logger } from "hono/logger"; |
| 71 | 71 | import { trimTrailingSlash } from "hono/trailing-slash"; |
| 72 | 72 | import * as assets from "#sitegen/assets"; |
src/friend-auth.ts+1-3| ... | ... | @@ -66,8 +66,7 @@ function friendPage(route: assets.Key) { |
| 66 | 66 | }); |
| 67 | 67 | |
| 68 | 68 | app.post(route, async (c) => { |
| 69 | const ip = c.header("X-Forwarded-For") ?? getConnInfo(c).remote.address ?? | |
| 70 | "unknown"; | |
| 69 | const ip = c.header("X-Forwarded-For") ?? "unknown"; | |
| 71 | 70 | if (incorrectMap[ip]) { |
| 72 | 71 | return serveAsset(c, "/friends/auth/fail", 403); |
| 73 | 72 | } |
| ... | ... | @@ -93,5 +92,4 @@ friendPage("/friends/oct25"); |
| 93 | 92 | import { type Context, Hono } from "hono"; |
| 94 | 93 | import { serveAsset } from "#sitegen/assets"; |
| 95 | 94 | import { setTimeout } from "node:timers/promises"; |
| 96 | import { getConnInfo } from "#hono/conninfo"; | |
| 97 | 95 | import * as assets from "#sitegen/assets"; |
src/q+a/backend.ts+1-1| ... | ... | @@ -214,7 +214,7 @@ async function questionFailure( |
| 214 | 214 | }); |
| 215 | 215 | } |
| 216 | 216 | |
| 217 | import { type Context, Hono } from "#hono"; | |
| 217 | import { type Context, Hono } from "hono"; | |
| 218 | 218 | import type { ContentfulStatusCode } from "hono/utils/http-status"; |
| 219 | 219 | import { |
| 220 | 220 | adjectives, |
src/source-of-truth.ts+1-1| ... | ... | @@ -103,7 +103,7 @@ const openFile = util.promisify(fsCallbacks.open); |
| 103 | 103 | const closeFile = util.promisify(fsCallbacks.close); |
| 104 | 104 | const fstat = util.promisify(fsCallbacks.fstat); |
| 105 | 105 | |
| 106 | import { Hono } from "#hono"; | |
| 106 | import { Hono } from "hono"; | |
| 107 | 107 | import { MediaFile, MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; |
| 108 | 108 | import { FilePermissions } from "#src/file-viewer/models/FilePermissions.ts"; |
| 109 | 109 | import * as path from "node:path"; |
tsconfig.json+1-1| ... | ... | @@ -16,7 +16,7 @@ |
| 16 | 16 | "target": "esnext", |
| 17 | 17 | "noUncheckedIndexedAccess": true, |
| 18 | 18 | "resolveJsonModule": true, |
| 19 | "types": ["marko"] | |
| 19 | "types": ["marko", "node"] | |
| 20 | 20 | }, |
| 21 | 21 | "include": ["./lib/**/*", "src/**/*", "framework/**/*"] |
| 22 | 22 | } |