From 66afc3994e19ed05fbfbf64b053781eb2830cc82 Mon Sep 17 00:00:00 2001 From: vijay11149 Date: Tue, 18 Aug 2026 20:09:10 +0000 Subject: [PATCH] fix(build): align Next.js, Docusaurus, and Node TS builds across the monorepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate Docusaurus onBrokenMarkdownLinks to markdown.hooks (v3 deprecation) - Fix dashboard monorepo-package transpilation by removing the @guildpass/env source path mapping and adding @guildpass/metrics to transpilePackages - Emit apps/access-api dist/index.js (ES2022, rootDir src) and document its Node http + ESM setup - Add the previousState column and FailedEvent model referenced by the indexer so access-api type-checks and builds - Document Node/pnpm/TypeScript/app versions in VERSIONS.md and add a Node 18/20 build matrix to CI ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/ci.yml | 58 +++++++++++++ CONTRIBUTING.md | 7 +- README.md | 7 +- VERSIONS.md | 85 +++++++++++++++++++ apps/access-api/README.md | 20 ++++- apps/access-api/prisma/schema.prisma | 18 ++++ apps/access-api/src/utils/backfill-lock.ts | 2 +- apps/access-api/src/workers/indexer.ts | 2 +- apps/access-api/tsconfig.json | 6 +- apps/dashboard/next.config.mjs | 1 + .../test/activity-hash-chain-durable.test.ts | 4 +- .../test/activity-hash-chain.test.ts | 2 +- apps/dashboard/tsconfig.json | 5 +- apps/discord-bot/src/bot.ts | 2 +- apps/discord-bot/test/member-lock.test.ts | 2 +- apps/discord-bot/test/queue.test.ts | 6 -- apps/docs/docusaurus.config.js | 6 +- eslint.config.js | 18 +++- .../src/contracts/contract.types.ts | 2 +- 19 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 VERSIONS.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1f7b346 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + name: Build (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # pnpm 10 is required for the workspace config (`allowBuilds`, + # `minimumReleaseAgeExclude`). See VERSIONS.md for the supported range. + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Install dependencies + run: pnpm install + + # The access-api types reference the generated Prisma client. + - name: Generate Prisma client + run: pnpm --filter @guildpass/access-api prisma:generate + + # Shared packages must be compiled before typechecking/building the apps + # that consume them (@guildpass/env has no `prepare` script, and the + # apps resolve `@guildpass/*` packages from their `dist` output). + - name: Build workspace packages + run: pnpm -r --filter "./packages/*" build + + - name: Type check + run: pnpm typecheck + + # Build each app with its own build system (Next.js, Docusaurus, tsc). + - name: Build apps + run: pnpm -r --filter "./apps/*" build + env: + NEXT_TELEMETRY_DISABLED: "1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e17b727..1d02939 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,8 +63,11 @@ By participating you agree to our [Code of Conduct](./CODE_OF_CONDUCT.md). Pleas ### Prerequisites -- **Node.js** 18.17 or higher -- **pnpm** 9+ (install via `npm install -g pnpm`) +- **Node.js** 18.17 or higher (recommended: 20.x) +- **pnpm** 9+ (install via `npm install -g pnpm`; recommended: 10.x) + +> See [`VERSIONS.md`](./VERSIONS.md) for the authoritative Node.js, pnpm, +> TypeScript, and framework version matrix. - A **Discord application** (bot token + `applications.commands` scope) โ€” only needed if working on the Discord bot ### Quick Start (Dashboard) diff --git a/README.md b/README.md index 4047441..8cc9613 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,11 @@ GuildPass is a web dashboard for managing access, passes, guilds/communities, me ## Prerequisites -- **Node.js** 18.17 or later -- **pnpm** (install via `npm install -g pnpm`) +- **Node.js** 18.17 or later (recommended: 20.x) +- **pnpm** 9 or later (recommended: 10.x) + +See [VERSIONS.md](./VERSIONS.md) for the full, up-to-date dependency matrix +(Node.js, pnpm, TypeScript, and each app's framework versions). --- diff --git a/VERSIONS.md b/VERSIONS.md new file mode 100644 index 0000000..4350747 --- /dev/null +++ b/VERSIONS.md @@ -0,0 +1,85 @@ +# Dependency Versions + +This document records the toolchain and framework versions that the GuildPass +monorepo is built and tested against. It exists to keep the three app build +systems (Next.js, Docusaurus, and the Node.js TypeScript services) compatible +with the shared workspace packages. + +> ๐Ÿ’ก The monorepo's `engines` fields and CI matrix are the source of truth for +> what is *supported*. The "recommended" column reflects what the production +> Docker images and CI builds actually use. + +--- + +## Node.js + +| | Version | +| --- | --- | +| Minimum | `18.17.0` (18.x) | +| Recommended | `20.x` | + +All apps and packages declare `"engines": { "node": ">=18.17.0" }`. The +production Dockerfiles (`apps/*/Dockerfile`) build and run on `node:20-alpine`. + +--- + +## pnpm + +| | Version | +| --- | --- | +| Minimum | `9.x` | +| Recommended | `10.x` | + +- `apps/*/Dockerfile` enable and pin `pnpm@9` via Corepack. +- `pnpm-workspace.yaml` uses pnpm 10 options (`allowBuilds`, + `minimumReleaseAgeExclude`), so **pnpm 10 is recommended** for local + development. +- The lockfile is intentionally gitignored (see `verify-lockfile=false` in + `.pnpmrc`). + +--- + +## TypeScript + +| | Version | +| --- | --- | +| Range | `^5.4.0` (workspace packages) ยท `^5.6.3` (dashboard, access-api, env) | + +The workspace base config is [`tsconfig.base.json`](./tsconfig.base.json) +(`strict`, `NodeNext` module resolution, `ES2020` target). Apps override the +target where needed: + +- `apps/dashboard` โ€” `moduleResolution: "bundler"`, JSX `preserve` (Next.js). +- `apps/access-api` โ€” `ES2022` target, NodeNext ESM. +- `apps/discord-bot` โ€” NodeNext ESM, `rootDir: src`. + +--- + +## App-specific versions + +| App | Framework / runtime | Version | +| --- | --- | --- | +| `apps/dashboard` | Next.js | `14.2.21` (14.x) | +| `apps/dashboard` | React / React DOM | `18.3.1` | +| `apps/docs` | Docusaurus | `^3.2.1` (3.x) | +| `apps/docs` | React / React DOM | `^18.2.0` | +| `apps/access-api` | Node.js built-in `http` server | โ€” (no web framework) | +| `apps/access-api` | Prisma | `^5.15.0` | +| `apps/access-api` | viem | `^2.13.0` | +| `apps/discord-bot` | discord.js | `^14.15.3` (14.x) | +| `apps/discord-bot` | @discordjs/rest | `^2.6.1` | + +> **Note:** despite historical references to "Fastify", `apps/access-api` does +> not use Fastify. It uses Node's built-in `http` module for its health +> endpoint. See [`apps/access-api/README.md`](./apps/access-api/README.md). + +--- + +## Update Policy + +- **Major version updates** (e.g. Next.js 14 โ†’ 15, Docusaurus 3 โ†’ 4, Node 20 โ†’ + 22) require approval from the core team and must be accompanied by: + - a passing CI run on the full Node version matrix, and + - updated documentation in this file and the relevant `README.md`. +- Minor and patch updates may be applied independently, but must keep every app + and package building with `pnpm typecheck`, `pnpm lint`, and `pnpm test`. diff --git a/apps/access-api/README.md b/apps/access-api/README.md index aeeefd2..26e61ef 100644 --- a/apps/access-api/README.md +++ b/apps/access-api/README.md @@ -9,9 +9,10 @@ The Access API contains the on-chain event indexer for GuildPass membership stat 1. [Architecture Overview](#architecture-overview) 2. [Environment Setup](#environment-setup) 3. [Running the Indexer](#running-the-indexer) -4. [Database Schema](#database-schema) -5. [Backfill / Replay Runbook](#backfill--replay-runbook) -6. [Development & Testing](#development--testing) +4. [TypeScript, ESM & Node.js](#typescript-esm--nodejs) +5. [Database Schema](#database-schema) +6. [Backfill / Replay Runbook](#backfill--replay-runbook) +7. [Development & Testing](#development--testing) --- @@ -80,6 +81,19 @@ The indexer polls every 10 seconds. Logs are written to stdout. --- +## TypeScript, ESM & Node.js + +The Access API is a plain **Node.js `http` server** (no web framework such as Fastify/Express) compiled to **ECMAScript modules** targeting **Node.js 18+**. + +- **Module system:** `"type": "module"` in `package.json`, combined with `module`/`moduleResolution: "NodeNext"` (inherited from the workspace root [`tsconfig.base.json`](../../tsconfig.base.json)). Relative imports therefore use explicit `.js` extensions, as required by NodeNext ESM. +- **Compile target:** `ES2022` โ€” the language level fully supported by Node.js 18+. +- **Build output:** `tsc` compiles `src/**/*.ts` โ†’ `dist/**/*.js`, with `src/index.ts` emitting `dist/index.js` (the entry point used by both `pnpm start` and the production Docker image). +- **Dev / scripts / tests:** Run directly from TypeScript via `tsx` (`pnpm dev`, `pnpm backfill`, `pnpm test`) โ€” no build step is required for local development. + +Required runtimes are documented in [`VERSIONS.md`](../../VERSIONS.md). + +--- + ## Database Schema | Table | Purpose | diff --git a/apps/access-api/prisma/schema.prisma b/apps/access-api/prisma/schema.prisma index 15be37e..77535bd 100644 --- a/apps/access-api/prisma/schema.prisma +++ b/apps/access-api/prisma/schema.prisma @@ -17,6 +17,7 @@ model ProcessedEvent { status String @default("processed") // e.g., processed, reverted eventType String data Json + previousState Json? // membership state before this event was applied fencingToken Int @default(0) // Monotonically increasing leader generation createdAt DateTime @default(now()) @@ -54,6 +55,23 @@ model BackfillLock { updatedAt DateTime @updatedAt } +/// Dead-letter queue for events that could not be processed (for retry / triage). +model FailedEvent { + id String @id @default(uuid()) + contractAddress String + blockHash String + blockNumber BigInt + transactionHash String + logIndex Int + eventType String + error String + data Json + retryCount Int @default(0) + createdAt DateTime @default(now()) + + @@index([contractAddress]) +} + /// Singleton row (id = "singleton") used for distributed leader election /// across horizontally-scaled indexer instances. model LeaderElection { diff --git a/apps/access-api/src/utils/backfill-lock.ts b/apps/access-api/src/utils/backfill-lock.ts index 3a48300..1b56dd9 100644 --- a/apps/access-api/src/utils/backfill-lock.ts +++ b/apps/access-api/src/utils/backfill-lock.ts @@ -171,7 +171,7 @@ export class BackfillLock { /** Returns all currently-held (possibly stale) locks. */ async listLocks(): Promise { const rows = await this.prisma.backfillLock.findMany(); - return rows.map((r: { holder: string; acquiredAt: Date; liveHead: Date | null }) => ({ + return rows.map((r) => ({ holder: r.holder as LockHolder, acquiredAt: r.acquiredAt, liveHead: r.liveHead ?? undefined, diff --git a/apps/access-api/src/workers/indexer.ts b/apps/access-api/src/workers/indexer.ts index 306cba8..f65be19 100644 --- a/apps/access-api/src/workers/indexer.ts +++ b/apps/access-api/src/workers/indexer.ts @@ -279,7 +279,7 @@ export class IndexerCore { } } - private async applyEventApplication(decoded: any, tx: any) { + private async applyEventApplication(decoded: any, tx: any): Promise { const { eventName, args } = decoded; if (eventName === MEMBERSHIP_EVENTS.MembershipCreated) { diff --git a/apps/access-api/tsconfig.json b/apps/access-api/tsconfig.json index 85b7beb..b20c4b9 100644 --- a/apps/access-api/tsconfig.json +++ b/apps/access-api/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": ".", + "target": "ES2022", + "lib": ["ES2022"], + "rootDir": "src", "outDir": "dist", "composite": false, "baseUrl": ".", @@ -9,5 +11,5 @@ "@/*": ["./src/*"] } }, - "include": ["src", "scripts", "test"] + "include": ["src"] } diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 2bad752..bc5b3f7 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -4,6 +4,7 @@ const nextConfig = { transpilePackages: [ "@guildpass/env", "@guildpass/integration-client", + "@guildpass/metrics", "@guildpass/webhook-utils", ], diff --git a/apps/dashboard/test/activity-hash-chain-durable.test.ts b/apps/dashboard/test/activity-hash-chain-durable.test.ts index 4956353..f72b321 100644 --- a/apps/dashboard/test/activity-hash-chain-durable.test.ts +++ b/apps/dashboard/test/activity-hash-chain-durable.test.ts @@ -70,7 +70,7 @@ if (!connectionString) { const repository = new DurableActivityRepository(connectionString); const storage = new DurableActivityStorage({ ttlSeconds: 3600 }); - await repository.append({ + await repository.append("test-guild", { type: "member.joined", source: "dashboard", severity: "info", @@ -86,7 +86,7 @@ if (!connectionString) { ), "recorded", ); - await repository.append({ + await repository.append("test-guild", { type: "pass.created", source: "dashboard", severity: "info", diff --git a/apps/dashboard/test/activity-hash-chain.test.ts b/apps/dashboard/test/activity-hash-chain.test.ts index 4a84069..a9d3f33 100644 --- a/apps/dashboard/test/activity-hash-chain.test.ts +++ b/apps/dashboard/test/activity-hash-chain.test.ts @@ -74,7 +74,7 @@ describe("activity hash-chain canonical format", () => { test("mock repository events remain unhashed application events", async () => { const repository = new DurableActivityRepository("mock://activity-chain"); - const event = await repository.append({ + const event = await repository.append("test-guild", { type: "member.joined", source: "dashboard", severity: "info", diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index 3522473..78ac9cb 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -23,10 +23,7 @@ "baseUrl": ".", "ignoreDeprecations": "5.0", "paths": { - "@/*": ["./*"], - "@guildpass/env": ["../../packages/env/src/index.ts"], - "@guildpass/integration-client/*": ["../../packages/integration-client/*"], - "@guildpass/mock-repositories": ["../../packages/integration-client/mock/mockRepositories"] + "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/apps/discord-bot/src/bot.ts b/apps/discord-bot/src/bot.ts index c34785f..1f0adcf 100644 --- a/apps/discord-bot/src/bot.ts +++ b/apps/discord-bot/src/bot.ts @@ -1,5 +1,5 @@ import { Client, GatewayIntentBits, Events } from "discord.js"; -import { config, validateConfig } from "./config.js"; +import { config } from "./config.js"; import { RoleReconciliationQueue } from "./queue.js"; import { reconcileMemberRoles, resolveDesiredRoles, type RoleMap } from "./roles.js"; import { handleGuildStats } from "./commands/guild-stats.js"; diff --git a/apps/discord-bot/test/member-lock.test.ts b/apps/discord-bot/test/member-lock.test.ts index 71dfc43..e36fcad 100644 --- a/apps/discord-bot/test/member-lock.test.ts +++ b/apps/discord-bot/test/member-lock.test.ts @@ -351,7 +351,7 @@ describe("MemberReconciliationLock", () => { it("never interleaves read-modify-write cycles for the same member", async () => { const operationLog: { phase: string; time: number }[] = []; - let currentRoles = new Set(); + const currentRoles = new Set(); const state: MockRoleState = { currentRoles: currentRoles, diff --git a/apps/discord-bot/test/queue.test.ts b/apps/discord-bot/test/queue.test.ts index 51a6784..ee9e98e 100644 --- a/apps/discord-bot/test/queue.test.ts +++ b/apps/discord-bot/test/queue.test.ts @@ -27,12 +27,6 @@ function delayedTask(ms: number, value: T): () => Promise { return () => new Promise((r) => setTimeout(() => r(value), ms)); } -/** Create a task that rejects after `ms`. */ -function failingTask(ms: number, error: unknown): () => Promise { - return () => - new Promise((_, reject) => setTimeout(() => reject(error), ms)); -} - /** Collect all metrics events into an array. */ function collectMetrics(): { events: QueueMetrics[]; handler: MetricsHandler } { const events: QueueMetrics[] = []; diff --git a/apps/docs/docusaurus.config.js b/apps/docs/docusaurus.config.js index 2ff2614..b23415e 100644 --- a/apps/docs/docusaurus.config.js +++ b/apps/docs/docusaurus.config.js @@ -7,7 +7,11 @@ const config = { organizationName: "GuildPass", projectName: "guildpass-integrations", onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", + markdown: { + hooks: { + onBrokenMarkdownLinks: "warn", + }, + }, i18n: { defaultLocale: "en", locales: ["en"] }, presets: [ [ diff --git a/eslint.config.js b/eslint.config.js index 0c1cf83..7c9dbda 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,7 +45,23 @@ export default tseslint.config( module: "readonly", process: "readonly", __dirname: "readonly", - console: "readonly" + console: "readonly", + // Web/fetch API globals available in Node 18+ (used by tests and scripts) + fetch: "readonly", + Response: "readonly", + Request: "readonly", + Headers: "readonly", + URL: "readonly", + URLSearchParams: "readonly", + AbortController: "readonly", + AbortSignal: "readonly", + TextEncoder: "readonly", + TextDecoder: "readonly", + structuredClone: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly" } } } diff --git a/packages/integration-client/src/contracts/contract.types.ts b/packages/integration-client/src/contracts/contract.types.ts index fdc2d29..1a03db7 100644 --- a/packages/integration-client/src/contracts/contract.types.ts +++ b/packages/integration-client/src/contracts/contract.types.ts @@ -18,4 +18,4 @@ export interface JsonRpcResponse { id: number | string; } -export interface ContractCallOptions extends HttpRequestOptions {} +export type ContractCallOptions = HttpRequestOptions;