fix(web,build): consume packages/db + shared via dist; bind web to LAN

Two related fixes:

1. Phone (and any LAN client) couldn't reach the web container because
   the dev compose mapped 127.0.0.1:WEB_PORT instead of binding all
   interfaces. Drop the loopback prefix.

2. Turbopack and NodeNext disagree on extension handling: bot's tsc
   needs `.js` extensions in source imports; Turbopack's transpilePackages
   path can't resolve those `.js` requests back to `.ts` source. Switch
   to consuming the workspace packages via their compiled dist instead:
   - packages/db + packages/shared point `main`/`exports` at ./dist/*
   - drop transpilePackages from next.config.ts; web picks up the
     compiled `.js` files directly
   - dev compose command for web builds shared+db before running
     `next dev` so dist is fresh when Turbopack starts
   - put the `.js` extensions back in packages/db source so NodeNext
     compilers (bot's tsc, packages/db's own tsc) are happy
This commit is contained in:
yiekheng 2026-05-10 00:18:56 +08:00
parent 3d470069d3
commit e45bcb581a
8 changed files with 33 additions and 26 deletions

View File

@ -1,27 +1,24 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import { join } from "node:path"; import { join } from "node:path";
// In a pnpm workspace + Turbopack setup, Next can't always infer the monorepo // Pin Turbopack's workspace root explicitly — pnpm + Turbopack can't always
// root (it walks up looking for next/package.json). Pin it explicitly so both // infer it inside Docker bind mounts.
// dev and production builds resolve files correctly inside the Docker image.
const workspaceRoot = join(import.meta.dirname, "..", ".."); const workspaceRoot = join(import.meta.dirname, "..", "..");
// We consume @cmbot/db and @cmbot/shared via their compiled dist (their
// package.json `main` points at ./dist/index.js). The dist is built at
// container start (see docker-compose.dev.yml) and during the production
// Docker build (see docker/web.Dockerfile). This sidesteps Turbopack's
// inability to resolve NodeNext-style `.js` extensions to `.ts` source.
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: "standalone", output: "standalone",
outputFileTracingRoot: workspaceRoot, outputFileTracingRoot: workspaceRoot,
transpilePackages: ["@cmbot/db", "@cmbot/shared"],
experimental: { experimental: {
typedRoutes: true, typedRoutes: true,
}, },
turbopack: { turbopack: {
root: workspaceRoot, root: workspaceRoot,
resolveExtensions: [".tsx", ".ts", ".jsx", ".js", ".mjs", ".json"],
resolveAlias: {
// Turbopack doesn't strip the `.js` extension alias that NodeNext requires.
// Map the compiled-style paths back to the real TS source files.
"@cmbot/db/schema.js": `${workspaceRoot}/packages/db/src/schema.ts`,
},
}, },
}; };

View File

@ -43,13 +43,16 @@ services:
container_name: cmbot-web container_name: cmbot-web
user: "${HOST_UID:-1000}:${HOST_GID:-1000}" user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
working_dir: /app working_dir: /app
command: ["pnpm", "--filter", "@cmbot/web", "dev"] command:
- sh
- -c
- "pnpm --filter @cmbot/shared build && pnpm --filter @cmbot/db build && pnpm --filter @cmbot/web dev"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- .:/app - .:/app
- ./dev-data:/data - ./dev-data:/data
ports: ports:
- "127.0.0.1:${WEB_PORT}:3000" - "${WEB_PORT}:3000"
environment: environment:
NODE_ENV: development NODE_ENV: development
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: ${DATABASE_URL}

View File

@ -3,11 +3,17 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./dist/index.js",
"types": "./src/index.ts", "types": "./dist/index.d.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": {
"./schema": "./src/schema.ts" "types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./schema": {
"types": "./dist/schema.d.ts",
"default": "./dist/schema.js"
}
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",

View File

@ -1,8 +1,8 @@
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
import { Pool } from "pg"; import { Pool } from "pg";
import * as schema from "./schema"; import * as schema from "./schema.js";
export * from "./schema"; export * from "./schema.js";
export type DB = NodePgDatabase<typeof schema>; export type DB = NodePgDatabase<typeof schema>;

View File

@ -1,5 +1,5 @@
import { migrate } from "drizzle-orm/node-postgres/migrator"; import { migrate } from "drizzle-orm/node-postgres/migrator";
import { createClient } from "./index"; import { createClient } from "./index.js";
const databaseUrl = process.env.DATABASE_URL; const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) { if (!databaseUrl) {

View File

@ -1,4 +1,4 @@
import { createClient, operators } from "./index"; import { createClient, operators } from "./index.js";
const databaseUrl = process.env.DATABASE_URL; const databaseUrl = process.env.DATABASE_URL;
const operatorTelegramId = process.env.SEED_OPERATOR_TELEGRAM_ID; const operatorTelegramId = process.env.SEED_OPERATOR_TELEGRAM_ID;

View File

@ -2,9 +2,7 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src"
"module": "ESNext",
"moduleResolution": "Bundler"
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }

View File

@ -3,10 +3,13 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./dist/index.js",
"types": "./src/index.ts", "types": "./dist/index.d.ts",
"exports": { "exports": {
".": "./src/index.ts" ".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",