diff --git a/.gitignore b/.gitignore index fe525d1..4ff1317 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ AGENTS.md PLAN.md CODING_STYLE.md RESEARCH.md +apps/web/.next/ diff --git a/apps/web/package.json b/apps/web/package.json index 25c42dc..edab5bd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev -p 3000", + "dev": "bun run scripts/dev.ts", "build": "next build", "start": "next start -p 3000" }, @@ -12,5 +12,10 @@ "next": "^14.2.4", "react": "^18.3.1", "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "typescript": "^5.5.4" } } diff --git a/apps/web/scripts/dev.ts b/apps/web/scripts/dev.ts new file mode 100644 index 0000000..b871370 --- /dev/null +++ b/apps/web/scripts/dev.ts @@ -0,0 +1,85 @@ +type PortCheck = { + port: number; + available: boolean; +}; + +const DEFAULT_PORTS = [3001, 3002, 3003, 3004, 3005]; + +const isAvailable = (port: number): PortCheck => { + try { + const probe = Bun.serve({ + port, + fetch: () => new Response("ok") + }); + probe.stop(); + return { port, available: true }; + } catch { + return { port, available: false }; + } +}; + +const parsePort = (value: string | undefined): number | null => { + if (!value) { + return null; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + + return parsed; +}; + +const selectPort = (): number => { + const requested = parsePort(Bun.env.PORT); + + if (requested !== null) { + const check = isAvailable(requested); + if (!check.available) { + throw new Error(`Port ${requested} is already in use. Set PORT to another value.`); + } + return requested; + } + + for (const port of DEFAULT_PORTS) { + if (isAvailable(port).available) { + return port; + } + } + + throw new Error("No available port found for Next dev server."); +}; + +const run = async () => { + const port = selectPort(); + console.log(`[web] starting Next.js dev server on port ${port}`); + + const path = Bun.env.PATH ?? ""; + const cwd = `${import.meta.dir}/..`; + + const child = Bun.spawn(["next", "dev", "-p", String(port)], { + cwd, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + env: { + ...Bun.env, + PATH: `${cwd}/node_modules/.bin:${path}`, + PORT: String(port) + } + }); + + const shutdown = () => { + child.kill(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + const code = await child.exited; + process.exit(code ?? 0); +}; + +await run(); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index f4c4210..c99989f 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -2,10 +2,28 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "jsx": "preserve", - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], "incremental": true, - "noEmit": true + "noEmit": true, + "allowJs": true, + "esModuleInterop": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/bun.lock b/bun.lock index e5e75b5..bc0ef55 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,11 @@ "react": "^18.3.1", "react-dom": "^18.3.1", }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "typescript": "^5.5.4", + }, }, "packages/bus": { "name": "@islandflow/bus", @@ -162,12 +167,20 @@ "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], + "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -202,6 +215,10 @@ "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], } }