diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..d4b7c23 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,63 @@ +{ + "options": { + "typeAware": true, + "typeCheck": true + }, + "env": { + "browser": true, + "es2026": true, + "es6": true, + "node": true + }, + "plugins": ["import", "jsdoc", "jsx-a11y", "node", "promise"], + "rules": { + "eslint/capitalized-comments": [ + "warn", + "always", + { "ignoreConsecutiveComments": true } + ], + "eslint/eqeqeq": ["off", "smart"], + "eslint/func-style": ["warn", "declaration"], + "eslint/id-length": ["warn", { "exceptionPatterns": ["^_", "^[Trtv]$"] }], + "eslint/init-declarations": "off", + "eslint/max-params": ["warn", { "max": 4 }], + "eslint/max-statements": ["warn", { "max": 20 }], + "eslint/no-console": "warn", + "eslint/no-use-before-define": "off", + "eslint/prefer-destructuring": ["warn", { "object": true, "array": false }], + "eslint/no-continue": "off", + "eslint/no-eq-null": "off", + "eslint/no-magic-numbers": "warn", + "eslint/no-ternary": "off", + "eslint/no-undefined": "off", + "eslint/no-void": "off", + "eslint/sort-keys": "off", + "eslint/sort-imports": [ + "error", + { "allowSeparatedGroups": true, "ignoreDeclarationSort": true } + ], + "import/consistent-type-specifier-style": "off", + "import/exports-last": "off", + "import/group-exports": "off", + "import/no-default-export": "off", + "import/no-named-export": "off", + "import/no-namespace": [ + "error", + { "ignore": ["@fedify/vocab", "./schema.ts"] } + ], + "import/no-nodejs-modules": "off", + "import/prefer-default-export": "off", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns-type": "off", + "promise/avoid-new": "warn" + }, + "categories": { + "correctness": "error", + "suspicious": "error", + "pedantic": "error", + "perf": "error", + "style": "error", + "restriction": "error", + "nursery": "error" + } +} diff --git a/mise.toml b/mise.toml index d7b3656..8c7b3ef 100644 --- a/mise.toml +++ b/mise.toml @@ -1,10 +1,15 @@ min_version = "2026.6.10" +[settings.npm] +package_manager = "pnpm" + [tools] "aqua:dahlia/hongdown" = "0.4.3" "github:nushell/nushell" = "latest" node = "26" "npm:@typescript/native-preview" = "7.0.0-dev.20260620.1" +"npm:oxlint" = "latest" +"npm:oxlint-tsgolint" = "latest" "npm:pglite-cli" = "latest" oxfmt = "0.55.0" pnpm = "11" @@ -23,6 +28,10 @@ depends = ["check:*"] description = "Check TypeScript types" run = "pnpm --recursive exec tsgo --noEmit" +[tasks."check:lint"] +description = "Check linting" +run = "oxlint" + [tasks."check:fmt"] description = "Check formatting" run = "oxfmt --check" diff --git a/packages/drfed/bin/drfed-server.mjs b/packages/drfed/bin/drfed-server.mjs index 4dc320d..7b47cbe 100644 --- a/packages/drfed/bin/drfed-server.mjs +++ b/packages/drfed/bin/drfed-server.mjs @@ -14,6 +14,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +// oxlint-disable-next-line import/no-relative-parent-imports import { main } from "../dist/index.mjs"; await main(); diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts index 5c938f1..60dab32 100644 --- a/packages/drfed/src/index.ts +++ b/packages/drfed/src/index.ts @@ -20,6 +20,7 @@ import { migrate } from "@drfed/models"; import { run } from "@optique/run"; import { serve } from "srvx"; +// oxlint-disable-next-line import/no-relative-parent-imports import metadata from "../package.json" with { type: "json" }; import type { Options } from "./parser.ts"; import program from "./program.ts"; @@ -27,26 +28,27 @@ import program from "./program.ts"; export async function main() { const options: Options = run(program, { help: "option", + showChoices: true, + showDefault: true, version: { - value: metadata.version, option: true, + value: metadata.version, }, - showChoices: true, - showDefault: true, }); if (options.drizzle.migrate) { await migrate({ credentials: options.drizzle.credentials }); } const yogaServer = createYogaServer(options.drizzle.db); const server = serve({ + fetch: yogaServer.fetch.bind(yogaServer), hostname: options.address.host, - port: options.address.port, manual: true, - fetch: yogaServer.fetch.bind(yogaServer), + port: options.address.port, }); - const shutdown = () => { + function shutdown() { + // oxlint-disable-next-line promise/catch-or-return promise/prefer-await-to-then no-magic-numbers server.close().then(() => process.exit(0)); - }; + } process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); await server.serve(); diff --git a/packages/drfed/src/parser.ts b/packages/drfed/src/parser.ts index 0a84674..8c33f46 100644 --- a/packages/drfed/src/parser.ts +++ b/packages/drfed/src/parser.ts @@ -34,16 +34,16 @@ const pgliteParser = map( description: message`The path to the directory where the PGlite database files will be stored. Mutually exclusive with ${optionNames(["--postgres-url", "--database-url", "-D"])}.`, }, ), - (path) => ({ - db: drizzlePglite({ - schema, - relations, - connection: { dataDir: path }, - }), + (dbPath) => ({ credentials: { driver: "pglite" as const, - url: path, + url: dbPath, }, + db: drizzlePglite({ + connection: { dataDir: dbPath }, + relations, + schema, + }), }), ); @@ -57,17 +57,17 @@ const postgresParser = map( description: message`The URL of the PostgreSQL database to connect to. Mutually exclusive with ${optionNames(["--pglite-data-path", "--data-path", "-d"])}.`, }, ), - (url) => ({ + (dbUrl) => ({ + credentials: { + url: dbUrl.href, + }, db: drizzlePostgres({ - schema, - relations, connection: { - connectionString: url.href, + connectionString: dbUrl.href, }, + relations, + schema, }), - credentials: { - url: url.href, - }, }), ); @@ -88,7 +88,7 @@ export const parser = object({ option("--no-migrate", "-M", { description: message`Disable automatic database migrations.`, }), - (m) => !m, + (noMigrate) => !noMigrate, ), }), ), diff --git a/packages/drfed/src/program.ts b/packages/drfed/src/program.ts index 064a9d2..8f6aee5 100644 --- a/packages/drfed/src/program.ts +++ b/packages/drfed/src/program.ts @@ -20,11 +20,11 @@ import type { Program } from "@optique/core/program"; import parser from "./parser.ts"; const program: Program<"sync", InferValue> = { - parser, metadata: { - name: "drfed-server", description: message`Run a DrFed server.`, + name: "drfed-server", }, + parser, }; export default program; diff --git a/packages/drfed/tsconfig.json b/packages/drfed/tsconfig.json index 10cc181..39bce6b 100644 --- a/packages/drfed/tsconfig.json +++ b/packages/drfed/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "types": ["node"], "paths": { "@drfed/graphql": ["../graphql/src/index.ts"], "@drfed/models": ["../models/src/index.ts"] diff --git a/packages/graphql/src/account.ts b/packages/graphql/src/account.ts index 36d4f8a..c6fd670 100644 --- a/packages/graphql/src/account.ts +++ b/packages/graphql/src/account.ts @@ -16,46 +16,46 @@ import builder from "./builder.ts"; export const Account = builder.drizzleNode("accounts", { - name: "Account", description: "Represents an `Account` in the DrFed platform. " + "Note that it differs from the ActivityPub `Actor`s that belong to `Instance`s.", - id: { - column(account) { - return account.id; - }, - description: "The unique identifier of the `Account`.", - }, fields: (t) => ({ - uuid: t.expose("id", { - type: "UUID", - description: "The UUID of the `Account`.", + created: t.expose("created", { + type: "DateTime", + description: "The date/time when the `Account` was created.", }), email: t.expose("email", { type: "Email", description: "The email address of the `Account`.", }), - created: t.expose("created", { - type: "DateTime", - description: "The date/time when the `Account` was created.", + uuid: t.expose("id", { + type: "UUID", + description: "The UUID of the `Account`.", }), }), + id: { + column(account) { + return account.id; + }, + description: "The unique identifier of the `Account`.", + }, + name: "Account", }); builder.queryFields((t) => ({ accountByUuid: t.drizzleField({ - type: Account, - description: "Get an `Account` by its UUID.", args: { uuid: t.arg({ - type: "UUID", - required: true, description: "The UUID of the `Account` to retrieve.", + required: true, + type: "UUID", }), }, + description: "Get an `Account` by its UUID.", nullable: true, resolve(query, _, { uuid }, ctx) { return ctx.db.query.accounts.findFirst(query({ where: { id: uuid } })); }, + type: Account, }), })); diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts index 62df5f6..0c6c77a 100644 --- a/packages/graphql/src/builder.ts +++ b/packages/graphql/src/builder.ts @@ -67,7 +67,6 @@ export interface SchemaTypes { * The GraphQL schema builder. */ export const builder = new SchemaBuilder({ - plugins: [DrizzlePlugin, RelayPlugin], defaultFieldNullability: false, drizzle: { client(ctx) { @@ -76,13 +75,14 @@ export const builder = new SchemaBuilder({ getTableConfig, relations, }, + plugins: [DrizzlePlugin, RelayPlugin], }); builder.addScalarType("DateTime", DateTimeResolver); builder.scalarType("Email", { - serialize: (v) => normalizeEmail(v), parseValue: (v) => normalizeEmail(String(v)), + serialize: (v) => normalizeEmail(v), }); builder.addScalarType("UUID", UUIDResolver); diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 81d87ff..f9c318e 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -14,8 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import type { Database } from "@drfed/models"; -import { createYoga, useExecutionCancellation } from "graphql-yoga"; -import type { YogaServerInstance } from "graphql-yoga"; +import { + type YogaServerInstance, + createYoga, + useExecutionCancellation, +} from "graphql-yoga"; import type { ServerContext, UserContext } from "./builder.ts"; import { schema } from "./schema.ts"; @@ -30,10 +33,11 @@ export function createYogaServer( db: Database, ): YogaServerInstance { return createYoga({ - plugins: [useExecutionCancellation()], - schema, + // oxlint-disable-next-line require-await async context(ctx) { - return { request: ctx.request, db }; + return { db, request: ctx.request }; }, + plugins: [useExecutionCancellation()], + schema, }); } diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts index e73bd5c..2176397 100644 --- a/packages/graphql/src/instance.ts +++ b/packages/graphql/src/instance.ts @@ -16,23 +16,23 @@ import builder from "./builder.ts"; export const Instance = builder.drizzleNode("instances", { - name: "Instance", description: "Represents an `Instance` in the DrFed platform.", - id: { - column(instance) { - return instance.id; - }, - description: "The unique identifier of the `Instance`.", - }, fields: (t) => ({ - slug: t.exposeString("slug"), - expires: t.expose("expires", { - type: "DateTime", - description: "The expiration date/time of the `Instance`.", - }), created: t.expose("created", { type: "DateTime", description: "The creation date/time of the `Instance`.", }), + expires: t.expose("expires", { + type: "DateTime", + description: "The expiration date/time of the `Instance`.", + }), + slug: t.exposeString("slug"), }), + id: { + column(instance) { + return instance.id; + }, + description: "The unique identifier of the `Instance`.", + }, + name: "Instance", }); diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index 7acc9e1..e612e9f 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -13,11 +13,12 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +// oxlint-disable import/no-unassigned-import import "./account.ts"; import "./instance.ts"; import builder from "./builder.ts"; builder.queryType({}); -// builder.mutationType({}); +// Builder.mutationType({}); export const schema = builder.toSchema(); diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json index a4b544d..15b2e87 100644 --- a/packages/graphql/tsconfig.json +++ b/packages/graphql/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "types": ["node"], "paths": { "@drfed/models": ["../models/src/index.ts"] } diff --git a/packages/models/src/email.ts b/packages/models/src/email.ts index 2ab6894..7515479 100644 --- a/packages/models/src/email.ts +++ b/packages/models/src/email.ts @@ -58,8 +58,11 @@ export function normalizeEmail( export function normalizeEmail( email: string | null | undefined, ): string | null | undefined { - if (typeof email === "undefined") return undefined; - else if (email == null) return null; + if (typeof email === "undefined") { + return undefined; + } else if (email == null) { + return null; + } const [local, host, shouldNotExist] = email.trim().split("@"); if ( local == null || diff --git a/packages/models/src/migrate.ts b/packages/models/src/migrate.ts index a1b753b..4d7f93b 100644 --- a/packages/models/src/migrate.ts +++ b/packages/models/src/migrate.ts @@ -86,12 +86,12 @@ export async function migrate(options: MigrateOptions): Promise { const config = { migrationsFolder, + migrationsSchema: + options.migrationsSchema ?? options.migrations?.schema ?? "drizzle", migrationsTable: options.migrationsTable ?? options.migrations?.table ?? "__drizzle_migrations", - migrationsSchema: - options.migrationsSchema ?? options.migrations?.schema ?? "drizzle", }; if (isPGliteMigrateCredentials(options.credentials)) { @@ -101,11 +101,14 @@ export async function migrate(options: MigrateOptions): Promise { } } -function assertV3MigrationsFolder(migrationsFolder: string): void { - if (!existsSync(join(migrationsFolder, "meta", "_journal.json"))) return; +function assertV3MigrationsFolder(migrationsDir: string): void { + // oxlint-disable-next-line node/no-sync + if (!existsSync(join(migrationsDir, "meta", "_journal.json"))) { + return; + } throw new Error( - `The migrations folder format is outdated: ${migrationsFolder}. ` + + `The migrations folder format is outdated: ${migrationsDir}. ` + "Run `drizzle-kit up` before using migrate().", ); } @@ -130,7 +133,9 @@ async function migratePGliteDatabase( await client.waitReady; await migratePglite(drizzlePglite({ client }), config); } finally { - if (shouldCloseClient) await client.close(); + if (shouldCloseClient) { + await client.close(); + } } } @@ -158,7 +163,9 @@ async function migratePostgresDatabase( } function normalizePGliteUrl(url: string): string { - if (url.startsWith("file:")) return url.slice("file:".length); + if (url.startsWith("file:")) { + return url.slice("file:".length); + } return url; } diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts index 73ef89e..da56125 100644 --- a/packages/models/src/relations.ts +++ b/packages/models/src/relations.ts @@ -24,20 +24,20 @@ export const relations = defineRelations(schema, (r) => ({ to: r.instances.id.through(r.instanceMembers.instanceId), }), }, - instances: { - members: r.many.accounts({ - from: r.instances.id.through(r.instanceMembers.instanceId), - to: r.accounts.id.through(r.instanceMembers.accountId), - }), - }, instanceMembers: { + account: r.one.accounts({ + from: r.instanceMembers.accountId, + to: r.accounts.id, + }), instance: r.one.instances({ from: r.instanceMembers.instanceId, to: r.instances.id, }), - account: r.one.accounts({ - from: r.instanceMembers.accountId, - to: r.accounts.id, + }, + instances: { + members: r.many.accounts({ + from: r.instances.id.through(r.instanceMembers.instanceId), + to: r.accounts.id.through(r.instanceMembers.accountId), }), }, })); diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts index 342f6f5..35ef9eb 100644 --- a/packages/models/src/schema.ts +++ b/packages/models/src/schema.ts @@ -15,12 +15,12 @@ // along with this program. If not, see . import { sql } from "drizzle-orm"; import { - uuid, - varchar, - pgTable, check, - timestamp, + pgTable, primaryKey, + timestamp, + uuid, + varchar, } from "drizzle-orm/pg-core"; /** @@ -29,11 +29,11 @@ import { export const accounts = pgTable( "accounts", { - id: uuid().primaryKey(), - email: varchar({ length: 255 }).notNull().unique(), created: timestamp({ withTimezone: true }) .notNull() .default(sql`CURRENT_TIMESTAMP`), + email: varchar({ length: 255 }).notNull().unique(), + id: uuid().primaryKey(), }, (table) => [ check( @@ -52,12 +52,12 @@ export type NewAccount = typeof accounts.$inferInsert; export const instances = pgTable( "instances", { - id: uuid().primaryKey(), - slug: varchar({ length: 100 }).notNull().unique(), - expires: timestamp({ withTimezone: true }).notNull(), created: timestamp({ withTimezone: true }) .notNull() .default(sql`CURRENT_TIMESTAMP`), + expires: timestamp({ withTimezone: true }).notNull(), + id: uuid().primaryKey(), + slug: varchar({ length: 100 }).notNull().unique(), }, (table) => [ check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,100}$'`), @@ -77,15 +77,15 @@ export type NewInstance = typeof instances.$inferInsert; export const instanceMembers = pgTable( "instance_members", { - instanceId: uuid() - .notNull() - .references(() => instances.id), accountId: uuid() .notNull() .references(() => accounts.id), created: timestamp({ withTimezone: true }) .notNull() .default(sql`CURRENT_TIMESTAMP`), + instanceId: uuid() + .notNull() + .references(() => instances.id), }, (table) => [primaryKey({ columns: [table.instanceId, table.accountId] })], ); diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json index 4082f16..2e05131 100644 --- a/packages/models/tsconfig.json +++ b/packages/models/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "../../tsconfig.json" + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } } diff --git a/scripts/bump-versions.mts b/scripts/bump-versions.mts index 13d9fdc..b23a8c6 100644 --- a/scripts/bump-versions.mts +++ b/scripts/bump-versions.mts @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { readdir, readFile, writeFile } from "node:fs/promises"; +// oxlint-disable no-console no-magic-numbers +import { readFile, readdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; @@ -28,6 +29,24 @@ if (version == null) { process.exit(1); } +async function findPackageJsonPaths(): Promise { + const entries = await readdir(packagesDir, { withFileTypes: true }); + const paths: string[] = []; + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + paths.push(join(packagesDir, entry.name, "package.json")); + } + return paths; +} + +function isSemver(verStr: string): boolean { + return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test( + verStr, + ); +} + if (!isSemver(version)) { console.error(`Invalid semver version: ${version}`); process.exit(1); @@ -39,33 +58,19 @@ if (packageJsonPaths.length === 0) { process.exit(1); } -for (const path of packageJsonPaths) { - const content = await readFile(path, "utf8"); - const data = JSON.parse(content) as { name: string; version: string }; - const oldVersion = data.version; - data.version = version; - const updated = `${JSON.stringify(data, null, 2)}\n`; - await writeFile(path, updated, "utf8"); - console.log(`${data.name}: ${oldVersion} -> ${version}`); -} +await Promise.all( + packageJsonPaths.map(async (path) => { + const content = await readFile(path, "utf8"); + const data = JSON.parse(content) as { name: string; version: string }; + const oldVersion = data.version; + data.version = version; + const updated = `${JSON.stringify(data, null, 2)}\n`; + await writeFile(path, updated, "utf8"); + console.log(`${data.name}: ${oldVersion} -> ${version}`); + }), +); const count = packageJsonPaths.length; console.log( `\nBumped ${count} package${count === 1 ? "" : "s"} to ${version}.`, ); - -async function findPackageJsonPaths(): Promise { - const entries = await readdir(packagesDir, { withFileTypes: true }); - const paths: string[] = []; - for (const entry of entries) { - if (!entry.isDirectory()) continue; - paths.push(join(packagesDir, entry.name, "package.json")); - } - return paths; -} - -function isSemver(version: string): boolean { - return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test( - version, - ); -} diff --git a/scripts/check-versions.mts b/scripts/check-versions.mts index dfddbb9..66b831e 100644 --- a/scripts/check-versions.mts +++ b/scripts/check-versions.mts @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { readdir, readFile } from "node:fs/promises"; +// oxlint-disable no-console no-magic-numbers +import { readFile, readdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; @@ -43,7 +44,7 @@ for (const pkg of packages) { } if (versions.size === 1) { - const [version, names] = [...versions.entries()][0]; + const [version, names] = [...versions.entries()][0]!; const count = names.length; console.log( `All ${count} package${count === 1 ? "" : "s"} ${count === 1 ? "is" : "are"} at version ${version}.`, @@ -59,13 +60,16 @@ process.exit(1); async function loadPackages(): Promise { const entries = await readdir(packagesDir, { withFileTypes: true }); - const packages: Package[] = []; + const loadedPackages: Package[] = []; for (const entry of entries) { - if (!entry.isDirectory()) continue; + if (!entry.isDirectory()) { + continue; + } const packageJsonPath = join(packagesDir, entry.name, "package.json"); + // oxlint-disable-next-line no-await-in-loop const content = await readFile(packageJsonPath, "utf8"); const data = JSON.parse(content) as { name: string; version: string }; - packages.push({ name: data.name, version: data.version }); + loadedPackages.push({ name: data.name, version: data.version }); } - return packages; + return loadedPackages; } diff --git a/scripts/dev.mts b/scripts/dev.mts index 29cd906..71ee052 100644 --- a/scripts/dev.mts +++ b/scripts/dev.mts @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { spawn, type ChildProcess } from "node:child_process"; +// oxlint-disable no-console no-magic-numbers +import { type ChildProcess, spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { readdir, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; @@ -35,43 +36,6 @@ const packagesDir = join(root, "packages"); const isWindows = process.platform === "win32"; const pnpm = isWindows ? "pnpm.cmd" : "pnpm"; -let buildProcess: ChildProcess | undefined; -let serverProcess: ChildProcess | undefined; -let shuttingDown = false; - -process.on("SIGINT", () => { - void shutdown(0, "SIGINT"); -}); -process.on("SIGTERM", () => { - void shutdown(143, "SIGTERM"); -}); - -try { - await removeDistDirs(); - - buildProcess = spawnManaged( - pnpm, - ["--parallel", "--recursive", "exec", "tsdown", "--watch", "--no-clean"], - root, - ); - const buildExit = waitForExit(buildProcess); - - await waitForBuilds(buildExit); - - serverProcess = spawnManaged( - process.execPath, - ["--watch", "bin/drfed-server.mjs", "--pglite-data-path", "../../.pgdata"], - join(root, "packages", "drfed"), - ); - const serverExit = await waitForExit(serverProcess); - if (serverExit.error != null) throw serverExit.error; - const exitCode = serverExit.code ?? signalExitCode(serverExit.signal) ?? 1; - await shutdown(exitCode, "SIGTERM", { skipServer: true }); -} catch (error) { - console.error(error instanceof Error ? error.message : error); - await shutdown(1, "SIGTERM"); -} - async function removeDistDirs() { const packages = await readdir(packagesDir, { withFileTypes: true }); await Promise.all( @@ -102,13 +66,16 @@ function spawnManaged( async function waitForBuilds(buildExit: Promise): Promise { let buildExitResult: ExitResult | undefined; + // oxlint-disable-next-line promise/prefer-await-to-then promise/catch-or-return promise/always-return buildExit.then((result) => { buildExitResult = result; }); while (true) { if (buildExitResult != null) { - if (buildExitResult.error != null) throw buildExitResult.error; + if (buildExitResult.error != null) { + throw buildExitResult.error; + } const exitCode = buildExitResult.code ?? signalExitCode(buildExitResult.signal); throw new Error( @@ -116,19 +83,26 @@ async function waitForBuilds(buildExit: Promise): Promise { ); } + // oxlint-disable-next-line no-await-in-loop const packages = await readdir(packagesDir, { withFileTypes: true }); + // oxlint-disable-next-line no-await-in-loop const allGenerated = await Promise.all( packages .filter((entry) => entry.isDirectory()) .map(async (entry) => { const distDir = join(packagesDir, entry.name, "dist"); - if (!existsSync(distDir)) return false; + if (!existsSync(distDir)) { + return false; + } const entries = await readdir(distDir); return entries.length > 0; }), ); - if (allGenerated.every(Boolean)) return; + if (allGenerated.every(Boolean)) { + return; + } + // oxlint-disable-next-line no-await-in-loop await sleep(100); } } @@ -155,9 +129,10 @@ async function shutdown( } function waitForExit(child: ChildProcess): Promise { + // oxlint-disable-next-line promise/avoid-new return new Promise((resolve) => { child.once("exit", (code, signal) => resolve({ code, signal })); - child.once("error", (error) => resolve({ code: 1, signal: null, error })); + child.once("error", (error) => resolve({ code: 1, error, signal: null })); }); } @@ -169,6 +144,7 @@ function terminate( return Promise.resolve(); } + // oxlint-disable-next-line promise/avoid-new return new Promise((resolve) => { const timeout = setTimeout(() => { forceKill(child); @@ -176,6 +152,7 @@ function terminate( child.once("exit", () => { clearTimeout(timeout); + // oxlint-disable-next-line promise/no-multiple-resolved resolve(); }); killTree(child, signal); @@ -186,17 +163,20 @@ function killTree(child: ChildProcess, signal: NodeJS.Signals): void { try { if (isWindows) { child.kill(signal); - } else { - if (child.pid != null) process.kill(-child.pid, signal); + } else if (child.pid != null) { + process.kill(-child.pid, signal); } } catch (error) { - if (!isProcessLookupError(error)) child.kill(signal); + if (!isProcessLookupError(error)) { + child.kill(signal); + } } } function forceKill(child: ChildProcess | undefined): void { - if (child == null || child.exitCode != null || child.signalCode != null) + if (child == null || child.exitCode != null || child.signalCode != null) { return; + } if (isWindows) { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { @@ -207,9 +187,13 @@ function forceKill(child: ChildProcess | undefined): void { } try { - if (child.pid != null) process.kill(-child.pid, "SIGKILL"); + if (child.pid != null) { + process.kill(-child.pid, "SIGKILL"); + } } catch (error) { - if (!isProcessLookupError(error)) child.kill("SIGKILL"); + if (!isProcessLookupError(error)) { + child.kill("SIGKILL"); + } } } @@ -223,13 +207,57 @@ function isProcessLookupError(error: unknown): boolean { } function signalExitCode(signal: NodeJS.Signals | null): number | undefined { - if (signal === "SIGINT") return 130; - if (signal === "SIGTERM") return 143; + if (signal === "SIGINT") { + return 130; + } + if (signal === "SIGTERM") { + return 143; + } return undefined; } function sleep(ms: number): Promise { + // oxlint-disable-next-line promise/avoid-new return new Promise((resolve) => { setTimeout(resolve, ms); }); } + +let buildProcess: ChildProcess | undefined; +let serverProcess: ChildProcess | undefined; +let shuttingDown = false; + +process.on("SIGINT", () => { + void shutdown(0, "SIGINT"); +}); +process.on("SIGTERM", () => { + void shutdown(143, "SIGTERM"); +}); + +try { + await removeDistDirs(); + + buildProcess = spawnManaged( + pnpm, + ["--parallel", "--recursive", "exec", "tsdown", "--watch", "--no-clean"], + root, + ); + const buildExit = waitForExit(buildProcess); + + await waitForBuilds(buildExit); + + serverProcess = spawnManaged( + process.execPath, + ["--watch", "bin/drfed-server.mjs", "--pglite-data-path", "../../.pgdata"], + join(root, "packages", "drfed"), + ); + const serverExit = await waitForExit(serverProcess); + if (serverExit.error != null) { + throw serverExit.error; + } + const exitCode = serverExit.code ?? signalExitCode(serverExit.signal) ?? 1; + await shutdown(exitCode, "SIGTERM", { skipServer: true }); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + await shutdown(1, "SIGTERM"); +} diff --git a/tsconfig.json b/tsconfig.json index 2c0a0d1..fb0d88c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "skipLibCheck": true, "strict": true, "target": "ES2024", - "types": ["node"], + "types": [], "verbatimModuleSyntax": true } }