Skip to content

Commit 43754b5

Browse files
committed
Compile the server into dist/, so 'jevloop serve' works from the package
serve was the one subcommand that did not work from an installed package: it ran the TypeScript at the package root, and Node refuses to strip types under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING -- the trap docs/CODE-STYLE.md 8.8 already records). It printed a clone command instead, which is honest but is not a harness. server.ts moves into src/ so tsc emits dist/server.js beside dist/cli.js: - ROOT is no longer 'the directory this file is in'. web/ and DECISION.md live at the package root and belong to neither src/ nor dist/, so ROOT is now the nearest directory above us that has a package.json. Both locations resolve to the package root; before, a compiled copy would have looked for dist/web. - Two imports (backends, env) were direct L6 -> L6, which section 11 forbids. They go through the facade now, the way cli.ts already does. - serve() prefers dist/server.js and falls back to src/server.ts, so the installed package and a clone both work. Verified by packing and installing into an empty directory: npx jevloop serve starts from node_modules, GET / and GET /app.js return 200, and GET /api/spec returns the English headline. run and spec were already working from the package. server.ts is over the 300-line rule and says why it cannot be split in its module JSDoc -- the route table is one thing and the halves that would grow (sessions, workspaces, directory browsing) are already their own files. That is the shape the rule asks for; the baseline stays empty. Also adds an npm run serve script for working from a clone, drops server.ts from package.json files (dist carries it now) and from ROOTS in check.ts (src/** covers it), and registers server at L6.
1 parent c9f33f2 commit 43754b5

5 files changed

Lines changed: 90 additions & 51 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"build": "tsc -p tsconfig.build.json",
1212
"prepare": "npm run build",
1313
"demo": "node --experimental-strip-types examples/demo.ts",
14+
"serve": "node --experimental-strip-types src/server.ts",
1415
"typecheck": "tsc --noEmit",
1516
"test": "node --experimental-strip-types --test \"tests/*.test.ts\"",
1617
"check": "node --experimental-strip-types scripts/check.ts",
@@ -40,7 +41,6 @@
4041
"files": [
4142
"dist",
4243
"src",
43-
"server.ts",
4444
"web",
4545
"README.md",
4646
"LICENSE",

‎scripts/check.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ interface Violation {
4747
detail: string
4848
}
4949

50-
const ROOTS = ['src/**/*.ts', 'examples/**/*.ts', 'tests/**/*.ts', 'scripts/**/*.ts', 'bench/**/*.ts', 'server.ts']
50+
const ROOTS = ['src/**/*.ts', 'examples/**/*.ts', 'tests/**/*.ts', 'scripts/**/*.ts', 'bench/**/*.ts']
5151

5252
/**
5353
* `dir` 下的全部 TS 文件,**含子目录**。
@@ -299,8 +299,12 @@ const LAYER: Record<string, number> = {
299299
backends: 6,
300300
env: 6,
301301
// 命令行入口。它组合判定、生成、工具三条缝并把它们接到 argv 上,
302-
// 所以和 `backends` 同层。`server.ts` 是同一类东西,但它在包根、不在 `src/`。
302+
// 所以和 `backends` 同层。
303303
cli: 6,
304+
// 开发服务器。它组合的和 `cli` 是同一批东西,只是接到 HTTP 上而不是 argv 上。
305+
// 放在 `src/` 里(而不是包根)是为了让它进 `dist/` —— 见 `cli.ts` 的 `serve`:
306+
// Node 拒绝给 `node_modules` 下的文件剥离类型,所以包根那份 TS 装出来跑不了。
307+
server: 6,
304308
}
305309

306310
/** 层号 → 一句话,报错时要说清两边各是什么 */

‎src/cli.ts‎

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import { spawn } from 'node:child_process'
2424
import { existsSync, readFileSync } from 'node:fs'
25-
import { dirname, join, resolve, sep } from 'node:path'
25+
import { dirname, join, resolve } from 'node:path'
2626
import { fileURLToPath } from 'node:url'
2727

2828
import { USAGE, parseArgv } from './cli-args.ts'
@@ -127,36 +127,35 @@ async function runTask(task: string, options: Map<string, string>): Promise<numb
127127
/**
128128
* 起界面。
129129
*
130-
* `server.ts` 留在包根、不编译进 `dist/`:它用 `import.meta.url` 定位 `web/`
131-
* 和 `DECISION.md`,编译到 `dist/server.js` 之后这两个路径会一起指错。
130+
* 两条路,取决于这个 `cli.js` 是从哪跑起来的:
132131
*
133-
* ⚠️ **代价:从 npm 装出来时它跑不了。** Node 拒绝给 `node_modules` 下的文件
134-
* 剥离类型(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,§8.8 记的就是这条)。
135-
* 所以这里检测到包在 `node_modules` 下就直接给出指引 —— 把 Node 的栈丢给用户
136-
* 等于让人自己猜,而这一条猜不出来。
132+
* - **装出来的包**:`dist/cli.js` 旁边就是 `dist/server.js`(`tsc` 一起编的),
133+
* 直接跑它。这条是 npm 路径,也是 `serve` 从包装出来能跑的原因。
134+
* - **clone**:`src/cli.ts` 旁边是 `src/server.ts`,按类型剥离跑。
135+
*
136+
* 两者都能跑,是因为 `server.ts` 的 `ROOT` 是**往上找 `package.json`**,
137+
* 不是「本文件所在目录」—— 从 `src/` 跑和从 `dist/` 跑都指回包根,
138+
* 而 `web/` 和 `DECISION.md` 都在那儿。
137139
*/
138140
function serve(options: Map<string, string>): Promise<number> {
139-
const serverPath = join(PKG_ROOT, 'server.ts')
140-
if (!existsSync(serverPath)) {
141-
console.error(`✗ ${serverPath} is missing — this package was published without server.ts`)
141+
const here = dirname(fileURLToPath(import.meta.url))
142+
const compiled = join(here, 'server.js')
143+
const source = join(here, 'server.ts')
144+
145+
let script: string
146+
let stripTypes = false
147+
if (existsSync(compiled)) {
148+
script = compiled
149+
} else if (existsSync(source)) {
150+
script = source
151+
// 类型剥离 v22.6 引入、v22.18 才默认开启。低版本必须显式带这个标志。
152+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number)
153+
stripTypes = major < 22 || (major === 22 && minor < 18)
154+
} else {
155+
console.error(`✗ neither ${compiled} nor ${source} exists — this package is incomplete`)
142156
return Promise.resolve(1)
143157
}
144158

145-
if (PKG_ROOT.includes(`${sep}node_modules${sep}`)) {
146-
console.error('✗ `jevloop serve` needs a clone, not an installed package.')
147-
console.error(' Node cannot strip types for files under node_modules, and server.ts is TypeScript.')
148-
console.error('')
149-
console.error(' git clone https://github.com/zjunlp/JevLoop && cd JevLoop')
150-
console.error(' node --experimental-strip-types server.ts')
151-
console.error('')
152-
console.error(' `jevloop run` and `jevloop spec` work from the installed package.')
153-
return Promise.resolve(1)
154-
}
155-
156-
// 类型剥离 v22.6 引入、v22.18 才默认开启。低版本必须显式带这个标志。
157-
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number)
158-
const needsFlag = major < 22 || (major === 22 && minor < 18)
159-
160159
const env = { ...process.env }
161160
const cwd = options.get('cwd')
162161
if (cwd) env.CWD_ROOT = resolve(cwd)
@@ -165,14 +164,14 @@ function serve(options: Map<string, string>): Promise<number> {
165164
const host = options.get('host')
166165
if (host) env.HOST = host
167166

168-
const child = spawn(process.execPath, [...(needsFlag ? ['--experimental-strip-types'] : []), serverPath], {
167+
const child = spawn(process.execPath, [...(stripTypes ? ['--experimental-strip-types'] : []), script], {
169168
stdio: 'inherit',
170169
env,
171170
})
172171
return new Promise((done) => {
173172
child.on('exit', (code) => done(code ?? 1))
174173
child.on('error', (err) => {
175-
console.error(`✗ could not start server.ts: ${err.message}`)
174+
console.error(`✗ could not start the server: ${err.message}`)
176175
done(1)
177176
})
178177
})

server.ts renamed to src/server.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
/**
33
* JevLoop · 开发服务器
44
*
5-
* node --experimental-strip-types server.ts
6-
* CWD_ROOT=/path/to/project node --experimental-strip-types server.ts
5+
* npx jevloop serve # 从包装出来的版本
6+
* node --experimental-strip-types src/server.ts # 从 clone
7+
* CWD_ROOT=/path/to/project node --experimental-strip-types src/server.ts
78
*
89
* 把 agent loop 的事件流通过 SSE 推给浏览器,同时托管 web/ 下的界面。
910
* 只用 Node 标准库 —— 没有 express,没有构建步骤。
@@ -17,34 +18,69 @@
1718
* 双向通信在这个界面里没有任何用途。
1819
*
1920
* 会话落盘在 `JEVLOOP_HOME/sessions/`(默认 `~/.jevloop/`),规则见
20-
* `src/session-store.ts`。**盘上全留,只在读给 agent 用时截到 `MAX_TURNS` 轮** ——
21+
* `session-store.ts`。**盘上全留,只在读给 agent 用时截到 `MAX_TURNS` 轮** ——
2122
* 以前是内存里的一个 Map,重启就没了,而且界面上看不出来。
2223
*
24+
* ── 为什么不能再拆 ────────────────────────────────────────────
25+
*
26+
* `docs/CODE-STYLE.md` §12 问的是能不能用一句话说完它负责什么。这里能:
27+
* **这个文件是开发服务器** —— 收请求、把事件流推给浏览器、托管 `web/`。
28+
* 路由按端点分节,每节都只是「解析请求 → 调内核 → 写响应」,而它们共享同一份
29+
* 状态(会话、工作区、正在跑的 run);拆成 `server-routes.ts` 之类只会让
30+
* 「一共有哪些端点」从一眼可见变成要翻三个文件。
31+
*
32+
* 真的会长的那半已经在别处:会话存储、工作区登记、目录浏览各自成文件
33+
* (`session-store.ts` / `workspace.ts` / `dir-browse.ts`),本文件只做接线。
34+
*
35+
* ⚠️ **本文件在 `src/` 里,但它读的文件在包根。** `web/` 和 `DECISION.md`
36+
* 既不编译也不属于 `src/`,所以 `ROOT` 不是「本文件所在目录」,而是
37+
* **往上找到的第一个有 `package.json` 的目录** —— 从 `src/` 跑和从 `dist/` 跑
38+
* 都指回包根。写成前者的话,编译之后 `web/` 会指到 `dist/web`。
39+
*
2340
* @module JevLoop/server
2441
*/
2542

2643
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
2744
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
28-
import { realpathSync } from 'node:fs'
29-
import { basename, extname, isAbsolute, join, normalize, relative } from 'node:path'
45+
import { realpathSync, existsSync } from 'node:fs'
46+
import { basename, dirname, extname, isAbsolute, join, normalize, relative } from 'node:path'
3047
import { fileURLToPath } from 'node:url'
3148
import { homedir } from 'node:os'
3249

33-
import { Decider } from './src/decide.ts'
34-
import { Meter } from './src/meter.ts'
35-
import { runAgent } from './src/agent.ts'
36-
import { resolveProvider, resolveGenerator } from './src/backends.ts'
37-
import { loadEnv } from './src/env.ts'
38-
import { parseDecisionDoc, summarize, headline, isGate } from './src/decisiondoc.ts'
39-
import { compilePredicate } from './src/decision-compile.ts'
40-
import type { AgentEvent } from './src/events.ts'
41-
import { SessionStore, assertSessionId } from './src/session-store.ts'
42-
import { migrateLegacyLayout } from './src/session-migrate.ts'
43-
import { createDir, listDirs } from './src/dir-browse.ts'
44-
import { WorkspaceStore } from './src/workspace.ts'
45-
import { WorkspaceError, type WorkspaceErrorCode } from './src/vocab-workspace.ts'
46-
47-
const ROOT = fileURLToPath(new URL('.', import.meta.url))
50+
import { Decider } from './decide.ts'
51+
import { Meter } from './meter.ts'
52+
import { runAgent } from './agent.ts'
53+
import { parseDecisionDoc, summarize, headline, isGate } from './decisiondoc.ts'
54+
import { compilePredicate } from './decision-compile.ts'
55+
import type { AgentEvent } from './events.ts'
56+
import { SessionStore, assertSessionId } from './session-store.ts'
57+
import { migrateLegacyLayout } from './session-migrate.ts'
58+
import { createDir, listDirs } from './dir-browse.ts'
59+
import { WorkspaceStore } from './workspace.ts'
60+
import { WorkspaceError, type WorkspaceErrorCode } from './vocab-workspace.ts'
61+
// `backends` 和 `env` 都是 L6,和本文件同层 —— §11 只允许 L0 内部互相指涉、
62+
// 以及 L2 指向定义角,同层直接 import 是违规的。走门面(`index.ts` 不受层约束),
63+
// 这也是 `cli.ts` 的写法。
64+
import { resolveGenerator, resolveProvider, loadEnv } from './index.ts'
65+
66+
/**
67+
* 包根目录:从本文件往上找到的第一个带 `package.json` 的目录。
68+
*
69+
* **不能写成「本文件所在目录」** —— `web/` 和 `DECISION.md` 在包根,
70+
* 而本文件从 `src/server.ts` 跑时所在的是 `src/`、编译后是 `dist/`,
71+
* 两者都指不到包根。往上找 `package.json` 对两种位置都成立。
72+
*/
73+
function findRoot(from: string): string {
74+
let dir = from
75+
for (;;) {
76+
if (existsSync(join(dir, 'package.json'))) return dir
77+
const up = dirname(dir)
78+
if (up === dir) throw new Error(`no package.json above ${from} — is this file installed correctly?`)
79+
dir = up
80+
}
81+
}
82+
83+
const ROOT = findRoot(dirname(fileURLToPath(import.meta.url)))
4884
const WEB_DIR = join(ROOT, 'web')
4985
const DECISION_MD = join(ROOT, 'DECISION.md')
5086
const PORT = Number(process.env.PORT ?? 7799)

‎tsconfig.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@
1919
"skipLibCheck": true,
2020
"forceConsistentCasingInFileNames": true
2121
},
22-
"include": ["src/**/*.ts", "examples/**/*.ts", "bin/**/*.ts", "tests/**/*.ts", "bench/**/*.ts", "scripts/**/*.ts", "server.ts"]
22+
"include": ["src/**/*.ts", "examples/**/*.ts", "bin/**/*.ts", "tests/**/*.ts", "bench/**/*.ts", "scripts/**/*.ts"]
2323
}

0 commit comments

Comments
 (0)