feature: sk-react-toolkit holds the server helpers, web components and Vite font plugin shared by manage-ui and sql-query-webui.
Release / release (push) Failing after 1m18s
Release / release (push) Failing after 1m18s
This commit is contained in:
commit
4505b37abc
30 files changed
+4826
No files matched your search
@@ -0,0 +1,66 @@
|
||||
import type {} from '@fastify/static'
|
||||
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { Log } from './log.ts'
|
||||
|
||||
/**
|
||||
* An error answered with its status code and message as { error }. A refused write also carries the problems by
|
||||
* field and a message about what it would break elsewhere, answered as { error, problems, message }.
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
statusCode: number
|
||||
problems?: Record<string, string>
|
||||
detail?: string
|
||||
|
||||
constructor(statusCode: number, message: string, problems?: Record<string, string>, detail?: string) {
|
||||
super(message)
|
||||
this.statusCode = statusCode
|
||||
this.problems = problems
|
||||
this.detail = detail
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A JSON schema of an object with exactly the properties given, all required. With Fastify's removeAdditional off, a
|
||||
* body with another property is refused with 400.
|
||||
*/
|
||||
export function strict(properties: Record<string, unknown>) {
|
||||
return { type: 'object', required: Object.keys(properties), additionalProperties: false, properties }
|
||||
}
|
||||
|
||||
/**
|
||||
* The error handler of setErrorHandler: the status of the error, 500 when it has none, with { error }, and the
|
||||
* problems and message of an HttpError that has problems. An error of 500 or more that is not an HttpError is logged
|
||||
* with its stack. A request that fails its schema is answered with "The request is not valid".
|
||||
*/
|
||||
export function errorHandler(log: Log) {
|
||||
return (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
|
||||
const status = error.statusCode ?? 500
|
||||
if (status >= 500 && !(error instanceof HttpError)) {
|
||||
log.error(`${request.method} ${request.url}: ${error.stack ?? error.message}`)
|
||||
}
|
||||
const message = error.validation ? `The request is not valid (${error.message}).` : error.message
|
||||
if (error instanceof HttpError && error.problems !== undefined) {
|
||||
return reply.code(status).send({ error: message, problems: error.problems, message: error.detail ?? '' })
|
||||
}
|
||||
return reply.code(status).send({ error: message })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The not-found handler of setNotFoundHandler for an application of webDir, the built frontend served by
|
||||
* @fastify/static: a path under /api is 404 with { error }; any other path answers index.html, so the frontend's
|
||||
* routes work on a reload, or 503 with buildHint while the frontend is not built.
|
||||
*/
|
||||
export function notFoundHandler(webDir: string, buildHint: string) {
|
||||
return (request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (request.url === '/api' || request.url.startsWith('/api/')) {
|
||||
return reply.code(404).send({ error: `'${request.method} ${request.url.split('?')[0]}' is not an endpoint of this tool.` })
|
||||
}
|
||||
if (!existsSync(join(webDir, 'index.html'))) {
|
||||
return reply.code(503).type('text/plain; charset=utf-8').send(`${buildHint}\n`)
|
||||
}
|
||||
return reply.type('text/html; charset=utf-8').sendFile('index.html')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { HttpError, errorHandler, notFoundHandler, strict } from './http.ts'
|
||||
export { QUIET, createLog, fail, timestampsOn, type Log } from './log.ts'
|
||||
export { exitOnSignals, gitVersion, readJsonObject } from './process.ts'
|
||||
@@ -0,0 +1,42 @@
|
||||
/** The log of a server: events to stdout, warnings and errors in colour to stderr, and requests when they are on. */
|
||||
export type Log = {
|
||||
event: (text: string) => void
|
||||
warn: (text: string) => void
|
||||
error: (text: string) => void
|
||||
request: (text: string) => void
|
||||
}
|
||||
|
||||
/** A log that writes nothing, for tests. */
|
||||
export const QUIET: Log = { event() {}, warn() {}, error() {}, request() {} }
|
||||
|
||||
/**
|
||||
* The log of a server. With timestamps each line starts with the local time with milliseconds; requests logs each
|
||||
* request to stderr, otherwise request writes nothing.
|
||||
*/
|
||||
export function createLog(options: { timestamps: boolean; requests: boolean }): Log {
|
||||
const line = (text: string) => {
|
||||
if (!options.timestamps) {
|
||||
return text
|
||||
}
|
||||
const now = new Date()
|
||||
const pad = (value: number, size = 2) => String(value).padStart(size, '0')
|
||||
return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)} ${text}`
|
||||
}
|
||||
return {
|
||||
event: (text) => console.log(line(text)),
|
||||
warn: (text) => console.error(`\x1b[33m${line(`warning: ${text}`)}\x1b[0m`),
|
||||
error: (text) => console.error(`\x1b[31m${line(`error: ${text}`)}\x1b[0m`),
|
||||
request: options.requests ? (text) => console.error(line(text)) : () => {},
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether --timestamps auto|on|off asks for timestamps: auto only when stdout is a terminal. */
|
||||
export function timestampsOn(choice: string) {
|
||||
return choice === 'on' || (choice === 'auto' && process.stdout.isTTY === true)
|
||||
}
|
||||
|
||||
/** Prints the message in red to stderr as an error and ends the process with status 2. */
|
||||
export function fail(message: string): never {
|
||||
console.error(`\x1b[31merror: ${message}\x1b[0m`)
|
||||
process.exit(2)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
/**
|
||||
* The git description of the checkout of dir now, a commit or a tag changing it without a restart; outside a
|
||||
* checkout, or without git, the variable named fallbackVariable, which a container image sets, or "dev".
|
||||
*/
|
||||
export function gitVersion(dir: string, fallbackVariable: string) {
|
||||
const described = spawnSync('git', ['describe', '--tags', '--always', '--dirty'], { cwd: dir, encoding: 'utf8' })
|
||||
return described.status === 0 ? described.stdout.trim() : process.env[fallbackVariable] || 'dev'
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl-C and SIGTERM, which watch mode sends on a restart, call close and end the process when it settles, or after 5
|
||||
* seconds.
|
||||
*/
|
||||
export function exitOnSignals(close: () => Promise<unknown>) {
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||
process.on(signal, () => {
|
||||
setTimeout(() => process.exit(0), 5000).unref()
|
||||
void close().then(() => process.exit(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The JSON object of the file at path, and its version, the SHA-256 of the bytes. With missingAsEmpty a missing file
|
||||
* reads as {}, whose version is that of "{}". A file that cannot be read, invalid JSON or another value than an
|
||||
* object throws with the reason.
|
||||
*/
|
||||
export function readJsonObject(path: string, options: { missingAsEmpty: boolean }): { data: Record<string, unknown>; version: string } {
|
||||
let bytes: Buffer
|
||||
let data: unknown
|
||||
try {
|
||||
bytes = options.missingAsEmpty && !existsSync(path) ? Buffer.from('{}') : readFileSync(path)
|
||||
data = JSON.parse(bytes.toString('utf8'))
|
||||
} catch (error) {
|
||||
throw new Error(`${path} cannot be read: ${(error as Error).message}`)
|
||||
}
|
||||
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
throw new Error(`${path} must hold a JSON object`)
|
||||
}
|
||||
return { data: data as Record<string, unknown>, version: createHash('sha256').update(bytes).digest('hex') }
|
||||
}
|
||||
Reference in new issue
Block a user