npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lark-apaas/coding-static-server

v0.1.6

Published

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.

Readme

@lark-apaas/coding-static-server

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

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

用法

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.parsereq.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.jsonstack 字段,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-devserverbaseTag 口径)。裸 base 不能 301——沙箱网关拼后端 URL 走 Go path.Join,转发路径的尾斜杠会被 path.Clean 吃掉,浏览器跳到 /app/<id>/ 后经网关又变回 /app/<id>,服务端再 301 就是 ERR_TOO_MANY_REDIRECTSbareBaseRedirect@deprecated,仅为下游兼容保留一个版本。
  • 托管面对齐 git 跟踪面(.gitignore 过滤):见下节。
  • 目录首页解析:pathname 以 / 结尾补 index.html//index.html/sub//sub/index.html)。
  • Clean URL(MPA 无扩展名路由):无扩展名、无尾斜杠的 pathname 按两种约定在 root 下查 html 并重写(fs 判断 + 重写,不发 301):/aboutabout.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 归一之后、落到文件之前,拿「相对仓库根的路径」过一遍规则,命中就返回 404text/plain,不解释原因——响应做成「这里没有这个文件」的样子,而不是「有但不给你」,后者等于给探测者确认了文件存在)。

  • 规则源<root>/.gitignore(仓库根那一份)。gitignore 语义由 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):路径里任意一段等于 .gitnode_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: falsehtmlTarget 的 lstat 都只看最后一跳——ln -s dist pub/pub/bundle.jsln -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唯一镜像了 dev+prod 双版本的两个库;其余(@babel/standalone / echarts / gsap / popmotion)只镜像了 .min.js,没有非压缩版可换,换了就 404。这是供给侧的硬边界,不是保守取舍。
  • 版本段原样保留[email protected] / [email protected] 都适用,不随 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)、以及 …/[email protected]/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,这两种形态少见。

编程接口

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 供组合与测试。