# @lark-apaas/coding-static-server

> Lightweight static file server for the miaoda-coding sandbox preview. Wraps serve-handler behind a bin and strips the runtime base-path prefix (CLIENT_BASE_PATH) so files under the workspace code dir are served as-is.

Latest version **0.1.6** (published 2026-08-10) · MIT license · 0 weekly downloads

## Install

```sh
npm install @lark-apaas/coding-static-server
pnpm add @lark-apaas/coding-static-server
yarn add @lark-apaas/coding-static-server
bun add @lark-apaas/coding-static-server
```

Provides the command `coding-static-server`.

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; no esm support; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.6 |
| Published | 2026-08-10 |
| First published | 2026-06-30 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | CommonJS |
| Node | >=22.0.0 |
| Dependencies | 2 |
| Unpacked size | 71.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | bytednpm, fengtianran_byted, xiaochuanzhang_byted, byted.lycoiref, duanlikang_byte |
| Keywords | miaoda, coding, static, server, preview |

## Links

- npm: https://www.npmjs.com/package/@lark-apaas/coding-static-server
- npm.io page: https://npm.io/package/@lark-apaas/coding-static-server

## Dependencies (2)

- [ignore](https://npm.io/package/ignore.md) ^7.0.5
- [serve-handler](https://npm.io/package/serve-handler.md) ^6.1.7

## Alternatives

- [flatbuffers](https://npm.io/package/flatbuffers.md) — 6.0M weekly downloads
- [jwt-simple](https://npm.io/package/jwt-simple.md) — 259.5K weekly downloads
- [@exodus/patch-broken-hermes-typed-arrays](https://npm.io/package/@exodus/patch-broken-hermes-typed-arrays.md) — 28.5K weekly downloads
- [@native-to-anchor/buffer-layout](https://npm.io/package/@native-to-anchor/buffer-layout.md) — 12.2K weekly downloads
- [binary-parser-encoder](https://npm.io/package/binary-parser-encoder.md) — 5.3K weekly downloads

## Recent versions

- 0.1.6 (latest) — 2026-08-10
- 0.1.6-beta.0 (beta) — 2026-08-10
- 0.1.6-alpha.20260810091940 (alpha) — 2026-08-10
- 0.1.5 — 2026-08-06
- 0.1.5-beta.0 — 2026-08-06
- 0.1.5-alpha.20260806071912 — 2026-08-06
- 0.1.5-alpha.20260806024743 — 2026-08-06
- 0.1.4 — 2026-08-05
- 0.1.4-beta.0 — 2026-08-05
- 0.1.4-alpha.20260805115411 — 2026-08-05
- 0.1.4-alpha.20260805100653 — 2026-08-05
- 0.1.3 — 2026-07-31
- 0.1.3-beta.0 — 2026-07-31
- 0.1.3-alpha.20260731090740 — 2026-07-31
- 0.1.3-alpha.20260728114408 — 2026-07-28
- … 4 more at https://npm.io/package/@lark-apaas/coding-static-server/versions

## README

# @lark-apaas/coding-static-server

miaoda-coding 沙箱预览用的轻量静态文件 server。把工作区代码目录（如 `/home/gem/workspace/code`）**原样**托管，供 agent / 浏览器经沙箱网关访问。

底层用 [`serve-handler`](https://github.com/vercel/serve-handler)（`serve` CLI 的引擎）兜底静态服务（文件解析 / MIME / 路径穿越防护 / Range / 条件请求），本包只做一层薄封装：**剥 base 前缀 + 目录首页解析 + 关缓存 + 留扩展位**。与 `@lark-apaas/coding-html-devserver` 零代码耦合。

## 用法

```bash
coding-static-server [--root <dir>] [--port <n>] [--host <addr>] [--base <path>]
```

| flag | 默认 | 说明 |
|------|------|------|
| `--root` | `.`（cwd） | 静态根目录，沙箱传 `/home/gem/workspace/code` |
| `--port` | `CLIENT_DEV_PORT` env ‖ `8001` | 监听端口 |
| `--host` | `0.0.0.0` | 绑定 host |
| `--base` | `CLIENT_BASE_PATH` env ‖ 空 | 要剥离的前缀，如 `/af/p/<appId>/` |

## 行为

- **request-target 单点归一**：进入任何处理之前，用 **serve-handler 同款的 legacy `url.parse`** 把 `req.url` 归一成 `pathname + search`，之后全链路只认它。两套 URL 解析器就是绕过面——早期按第一个 `?` 切分，与 serve-handler 在 `#` fragment（`/.env#x`）、反斜杠（`/a\..\.git\config`）、absolute-form request-target（`GET http://h/build/x.js`）三类输入上解析结果不同，而不同就意味着**判定的路径 ≠ 实际读的文件**。这里刻意用同一个已 deprecated 的 API：与 serve-handler 字节级一致，比换成更"现代"的 WHATWG `URL` 更重要。
- **技术栈感知服务根（`.spark/meta.json`）**：每次请求读 `<root>/.spark/meta.json` 的 `stack` 字段，`stack === 'html'` 时实际服务根切到 `<root>/src`（html 栈项目页面按约定在 `src/` 下，对齐 `coding-html-devserver` 默认 `--root src`）；其它 stack / 无 meta / 解析失败维持 `--root` 不变。不固化进启动配置（meta 是绑定应用后才写入的，与 `CLIENT_BASE_PATH` 同理），但带 **mtime 校验缓存**：每请求仅 `stat` 一次 meta，`(mtimeMs, size)` 签名没变就复用上次解析结果，变了才重读——变更（含 html → 全栈升级）仍即时生效。
- **剥网关前缀（正则，不依赖 env）**：网关把完整路径 `/app/<appId>/...`（或旧形态 `/af/p/<appId>/...`）透传过来。本包用正则 `^/(app|af/p)/[^/]+` 识别并剥掉这层前缀，再映射到 `--root` 下的文件。**默认不依赖 `CLIENT_BASE_PATH`**——因为该 env 是绑定时才写入沙箱 env 文件的，进程启动（容器 boot / 池预热）时拿不到。若启动时确有 `--base`/`CLIENT_BASE_PATH` 则优先用它精确匹配。非网关前缀的路径原样透传。
- **每页注入 `<base href>` 替代补尾斜杠**：命中网关前缀的 HTML 一律 head-prepend `<base href="<prefix>/">`，相对引用锚到 code 根目录（对齐 `coding-html-devserver` 的 `baseTag` 口径）。**裸 base 不能 301**——沙箱网关拼后端 URL 走 Go `path.Join`，转发路径的尾斜杠会被 `path.Clean` 吃掉，浏览器跳到 `/app/<id>/` 后经网关又变回 `/app/<id>`，服务端再 301 就是 `ERR_TOO_MANY_REDIRECTS`。`bareBaseRedirect` 已 `@deprecated`，仅为下游兼容保留一个版本。
- **托管面对齐 git 跟踪面（`.gitignore` 过滤）**：见下节。
- **目录首页解析**：pathname 以 `/` 结尾补 `index.html`（`/` → `/index.html`，`/sub/` → `/sub/index.html`）。
- **Clean URL（MPA 无扩展名路由）**：无扩展名、无尾斜杠的 pathname 按两种约定在 root 下查 html 并重写（**fs 判断 + 重写，不发 301**）：`/about` → `about.html`（平铺）或 `about/index.html`（目录），两者都存在时优先平铺 `.html`。
- **命中文件零 3xx**：命中文件不产生任何重定向；尤其关掉 serve-handler 默认 cleanUrls（它会把 `/index.html` 301 到 `/index` 且 Location 丢 base 前缀，网关下死循环）。
- **非 SPA**：文件不存在返回 404，不做 history fallback。
- **关缓存**：响应带 `Cache-Control: no-store`，预览改了即时生效。
- **安全**：不列目录、不跟随软链（含下面 HTML 改写分支，见该节最后几条）。
- **React prod → dev 改写**：见下节。

## `.gitignore` 过滤（托管面 = git 跟踪面）

**不进 git 的文件不该被预览托管。** 每个请求在 URL 归一之后、落到文件之前，拿「相对仓库根的路径」过一遍规则，命中就返回 **404**（`text/plain`，不解释原因——响应做成「这里没有这个文件」的样子，而不是「有但不给你」，后者等于给探测者确认了文件存在）。

- **规则源**：`<root>/.gitignore`（仓库根那一份）。gitignore 语义由 [`ignore`](https://github.com/kaelzhang/node-ignore) 包实现——锚定（`/build` vs `build`）、目录限定（`dist/`）、`**`、`!` 否定及其与父目录排除的交互，全部与 `git check-ignore` 一致，不自己写正则近似。**显式传 `ignorecase: false`**：node-ignore 默认大小写不敏感，那会让 `Build/` 把 `build/app.js` 也拦掉，而沙箱是大小写敏感的 Linux fs，git 在这里是区分大小写的。
- **匹配基准是仓库根，不是服务根**：html 栈下服务根被切到 `<root>/src`，但 `.gitignore` 的规则永远相对仓库根解释。所以请求 `/index.html` 是拿 `src/index.html` 去匹配的——否则项目里写的 `/src/generated/` 一条也拦不住。
- **硬拒清单**（与 `.gitignore` 写没写无关，恒 404）：路径里任意一段等于 `.git` 或 `node_modules`。前者是同源的泄露面——`.gitignore` 从不列 `.git`，但读得到 `.git/config` 就等于拿走带 credential 的 remote URL 和完整源码历史。硬拒是**独立于 `.gitignore` 的逐段字符串比较**，不共用 matcher：一来项目里一条 `!node_modules/foo` 就不能把它翻掉，二来 `ignore` 实例会把每个查过的路径**永久缓存**在实例上（键完全由请求方控制，实测 20 万条不同路径 ≈ 50MB 不可回收），长驻进程不能拿它接不可信输入。语义等价于不带锚定的 `.git` / `node_modules`，故不误伤 `.gitignore` / `.github/` / `node_modules_backup/`。
- **判定时机在 URL 归一之后、下游分叉之前**：判的是**最终会读的那个文件**（`/about` 已解析成 `about.html`、`/priv/` 已成 `priv/index.html`），且早于 html 自接管 / serve-handler 的分叉——否则被忽略的 `.html` 会从改写分支绕过去。无 base 前缀的直连路径、非 GET/HEAD 的 method 同样拦，passthrough 不是后门。
- **按 realpath 再判一次**：软链会让「字符串路径」与「真正打开的文件」分叉，而 `resolveUnderRoot` 的字符串校验、serve-handler 的 `symlinks: false`、`htmlTarget` 的 lstat **都只看最后一跳**——`ln -s dist pub` 后 `/pub/bundle.js`、`ln -s .git gitdir` 后 `/gitdir/config` 本可以把过滤和硬拒一起绕开（root 对 agent 完全可写）。真实路径逃出仓库根时直接拒。
- **fail-open，但保留 last-known-good**：没有 `.gitignore` 时只保留硬拒（没有 `.gitignore` 的项目全拦会把预览整个打死，比漏拦糟得多）；而**读失败不等于没有规则**——重读只发生在「有人刚改完 `.gitignore` 的那一刻」，恰是最容易撞上半写状态 / EMFILE / 权限抖动的时刻，此时退回上一次成功编译的规则，只有 ENOENT 才真的回到 fail-open。
- **缓存**：与 `.spark/meta.json` 同款签名校验——每请求仅 `stat` 一次 `.gitignore`，签名没变复用编译好的 matcher，改 `.gitignore`（含删除）立即生效。签名取 `(mtimeMs, ctimeMs, size, ino)` 四元组而非只看 mtime+size，等长同刻改写（`sed -i` 之类）才不会漏判。matcher 每服务 5 万次查询强制重建一次，清掉 node-ignore 的无界 per-path 缓存。
- **命中时留一行 stderr**：响应体一个字节不提原因（做成「这里没有这个文件」而非「有但不给你」），但服务端打 `blocked (git-ignored): <path>`。误伤不是假想——`logs` / `dist` / `out` 这类**无锚定**规则会连 `src/pages/logs/index.html` 一起命中，没有这行日志时人和 agent 看到的都只是裸 404，无从诊断。
- **不覆盖**：嵌套子目录的 `.gitignore`、`.git/info/exclude`、全局 `core.excludesFile`。agent 生成的应用几乎只有仓库根一份 `.gitignore`，为剩下的场景每请求多 stat 若干文件不划算。
- **没有开关**：这个 server 只托管源码目录，被 git 忽略的东西本来就不该出现在预览里。

## React prod → dev 改写

agent 生成的 HTML 引的是 **production** 资源（线上就该用 prod，这是对的）。但本 server 只服务**沙箱预览 = 开发态**，dev 版 React 才有可读的 warning（key 缺失、hook 误用、水合不一致）和非 minified 的报错栈——agent 读 console 排障、人看预览调试都依赖它。所以渲染 `.html` 时把 prod 换成 dev：

```
react@<ver>/umd/react.production.min.js          →  react@<ver>/umd/react.development.js
react-dom@<ver>/umd/react-dom.production.min.js  →  react-dom@<ver>/umd/react-dom.development.js
```

- **只覆盖 react / react-dom**——它们是 [`coding-unpkg-sdk`](../coding-unpkg-sdk/) 里**唯一镜像了 dev+prod 双版本**的两个库；其余（`@babel/standalone` / `echarts` / `gsap` / `popmotion`）只镜像了 `.min.js`，没有非压缩版可换，换了就 404。这是供给侧的硬边界，不是保守取舍。
- **版本段原样保留**，`react@18.3.1` / `react@17.0.2` 都适用，不随 manifest 升级而失效。
- **改 `src` 的同时摘掉该标签的 `integrity`**：SRI hash 是对 prod 文件字节算的，换 dev 文件后校验必然失败，而 SRI 失败浏览器是**直接拒绝执行**（不是降级）→ 白屏。`crossorigin` 保留（无害）。摘除是**属性级**的：引号内的内容整段跳过，`data-note="see integrity = docs"` 里的那个不会被误删（误删会吃掉右引号让属性不闭合、页面结构崩坏）。
- **只改 CDN 绝对 URL 上的完整 unpkg 形态** `<scheme>//<host>/…/<pkg>@<ver>/umd/<pkg>.production.min.js`，且包名左边界必须是 `/`。因此这些都**不会**被改：agent 下载到本地的 `./vendor/react.production.min.js`、按 unpkg 目录结构 vendor 的 `/vendor/react@18/umd/…`（本地没有 dev 文件 → 改了就 404）、以及 `…/myreact@1.0.0/umd/react.production.min.js` 这类后缀恰为 react 的第三方包。
- **标签匹配是引号感知的**（三分支 alternation：非引号非 `>` 字符 \| 双引号串 \| 单引号串），不用 `[^>]*`：属性值含 `>`（`data-x="a>b"`）时后者会截断，可能切出「含 `src` 但不含 `integrity`」的片段 → 换了 src 却留着 prod 的 SRI hash → 白屏。开标签未闭合（或无引号值里出现引号）时该标签整段不改，不影响其它标签。
- **`.html` 由本包自己读文件并改写**，不经 serve-handler（改写会变响应体长度，从它的输出流里改会踩 Content-Length）。因此 HTML 放弃了 ETag / 条件请求 / Range——本包对所有响应都是 `no-store`，无损。读文件失败（不存在 / 是目录 / 无权限）落回 serve-handler，404 形态与其它资源一致。
- **软链 `.html` 一律拒（404）**，与 serve-handler 的 `symlinks: false` 对齐。`readFile` 会跟随软链，而路径字符串校验看不出「最终目标是指向 root 外的软链」；少了这道判断，agent 只要 `ln -s <任意文件> x.html`（root 对 agent 完全可写）就能经预览 URL 读走 root 外的文件。
- **非 UTF-8 的 `.html` 字节原样送出**：改写为 no-op 时直接回原始 Buffer，不做 decode→encode 往返（否则 GBK 等编码会被换成 U+FFFD，违背「原样托管」）。**命中改写的页面**按 UTF-8 重编码——「非 UTF-8 且真的引了 React CDN」这种组合基本不共存，属有界残留。
- 接管 `.html` / `.htm`（大小写不敏感），只对 **GET / HEAD** 生效。其它 method 交给 serve-handler，而它 **不区分 method**（6.1.7 源码里没有 405 / method 判定），所以 POST/OPTIONS 拿到的是未改写的 prod 内容——浏览器取文档只用 GET，无实际影响。
- **幂等**：已是 dev 版的原样返回。

改写**无条件生效**，没有 flag / env 开关——本 server 只服务沙箱预览（开发态），不存在该给 prod 的场景；线上是另一条链路。

**不覆盖**（已知边界）：`<script type="importmap">` 里的 URL（那是标签 children 不是属性）、`.js` / `.mjs` 文件里的 URL（只改写 html 响应）。无编译 HTML 场景主流是 UMD + `@babel/standalone`，这两种形态少见。

## 编程接口

```ts
import { startServer } from '@lark-apaas/coding-static-server';

const server = await startServer({ root: '/home/gem/workspace/code', port: 8001, base: '/af/p/app_x/' });
```

亦导出 `resolveConfig` / `resolvePrefix` / `stripBase` / `appendIndexHtml` / `bareBaseRedirect` / `prefixBaseHref` / `injectBaseHref` / `cleanUrlResolve` / `resolveServeRoot` / `gitRelPath` / `isBlockedPath` / `createRequestListener` / `normalizeBasePath` / `parseFlags` / `rewriteReactDev` 供组合与测试。

---
_Source: https://npm.io/package/@lark-apaas/coding-static-server · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
