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
}
}