Skip to content

feat(compiler): 发布浏览器静态资源清单,构建期挡住产物漂移 - #190

Closed
lbb00 wants to merge 2 commits into
feat/compiler-typesfrom
feat/compiler-browser-assets
Closed

feat(compiler): 发布浏览器静态资源清单,构建期挡住产物漂移#190
lbb00 wants to merge 2 commits into
feat/compiler-typesfrom
feat/compiler-browser-assets

Conversation

@lbb00

@lbb00 lbb00 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

问题

dist/ 里的三个 .browser.js 必须由宿主原样拷贝、原样托管:stage worker 只被 new Worker(url) 引用,pool.browser.jscompile-core.browser.js 只被 fetch 下来从 Blob URL import。没有一处是打包器能看见的静态 import。

后果是这个契约完全靠口头维持:产物改个名、被 esbuild 拆出一个新 chunk、或者哪天某个 bundle 不再自包含(开始 import 别的文件),本仓构建照样绿,宿主那边也不会有编译期报错——只在运行时 404 或行为异常。宿主目前只能把这三个文件名硬编码在自己的拷贝脚本里。

改动

@dimina-kit/compiler/browser-assets(新导出)——清单本身是可被程序读取的:

const { resolveBrowserAssets } = require('@dimina-kit/compiler/browser-assets')
const { files } = resolveBrowserAssets(require.resolve('@dimina-kit/compiler/browser'))
for (const file of files) fs.copyFileSync(file, path.join(publicDir, path.basename(file)))

宿主只需要一个它本来就能拿到的路径(./browser 入口的解析结果),不用知道 dist 布局,也不用手抄文件名。模块只做字符串拼接,不依赖 node:path;ESM 和 CJS 都发(require 条件),带类型声明。

构建期自检——browser 构建开 metafile,用 esbuild 自己的产物记录核对三件事,任一不满足直接 exit 1:

  • 清单里列的产物都被生成了;
  • 没有生成清单外的产物(新产物必须显式归类:COMPILER_BROWSER_ASSETS 宿主要托管,BUNDLER_ONLY_BROWSER_OUTPUTS 宿主用自己的打包器 import);
  • 每个静态资源都没有静态 import(动态 import 不算——stage worker 运行时 import(toolchainSetupURL) 正是约定的用法)。

这个检查随 pnpm --filter @dimina-kit/compiler build:browser 跑,而 devkit 的 check-types 依赖 ^build,所以 CI 里必经。

验证

scripts/test-browser-assets.js(13 条断言,已接进 package.jsontestturbo run test 会跑到)把检查存在的意义——那几种失败模式——直接驱动一遍:产物改名同时报「少了」和「多了未分类的」、拆出新 chunk 被拦、静态资源出现静态 import 被拦、metafile 整形(丢 sourcemap、动态 import 不计入)、POSIX 与 Windows 路径解析、传裸文件名报错。

端到端也真的验过一次:把 build-compiler.jspool.browser 的输出名临时改成 pool-renamed.browser,构建报出

✗ browser-assets.js lists dist/pool.browser.js, but the browser build did not emit it
✗ dist/pool-renamed.browser.js is a new browser output: add it to COMPILER_BROWSER_ASSETS …

改回来后构建恢复 ✅ browser static-asset contract holds (3 assets, no static imports)

./scripts/gate.sh 全绿(lint / typecheck / test / pawl:check)。

说明

基于 #189,先合那个。这里只多了 types-fixture/consumer.ts 里新导出的那几行类型断言。

esbuild-wasm、oxc 那几个 wasm 静态资源没有一起放进清单:它们是 peer 依赖的产物,不由本包构建生成,写进来就成了构建无法自检的第二份真相,只会重复宿主已经在做的 require.resolve

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T11:35:33.050360Z c0d2564 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0d256419d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/compiler/src/browser-assets.js Outdated
.map(([file, output]) => {
const cut = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\'))
return {
name: cut < 0 ? file : file.slice(cut + 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve output directories in the asset check

When an entry is moved into a subdirectory—for example, esbuild emits dist/assets/stage-worker.browser.js—discarding the directory makes that output indistinguishable from the required sibling dist/stage-worker.browser.js. The contract therefore passes even though resolveBrowserAssets() still returns a nonexistent root-level file; retain and validate the path relative to dist so directory drift is rejected.

AGENTS.md reference: AGENTS.md:L23-L24

Useful? React with 👍 / 👎.

Comment thread packages/compiler/src/browser-assets.js Outdated
return {
name: cut < 0 ? file : file.slice(cut + 1),
staticImports: (output.imports || [])
.filter((entry) => entry.kind === 'import-statement')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-dynamic metafile import kinds

When a dependency is externalized from CommonJS code, esbuild records the remaining dependency as kind: 'require-call' rather than import-statement. This filter drops it, so the contract reports the bundle as self-contained even though the raw browser asset still attempts a runtime require() and fails. Treat every metafile import except the deliberately supported dynamic setup import as a violation.

AGENTS.md reference: AGENTS.md:L23-L24

Useful? React with 👍 / 👎.

/** The browser bundles a host has to host as static assets. */
export const COMPILER_BROWSER_ASSETS = /** @type {readonly CompilerBrowserAsset[]} */ ([
{
name: 'stage-worker.browser.js',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate manifest names against package exports

When an asset is renamed and both the esbuild entry and this manifest are updated, the contract passes while package.json can still map ./browser, ./pool, or ./stage-worker to the old filename. That leaves normal imports—and the documented require.resolve('@dimina-kit/compiler/browser') bootstrap—pointing to a missing file, so the supposedly authoritative names should also generate or be checked against those export targets.

AGENTS.md reference: AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Comment thread packages/compiler/README.md Outdated

### 浏览器静态资源:哪三个文件必须原样托管

上表里带 `.browser.js` 的三个产物,宿主要**原样拷贝、原样托管**,不能再过一遍自己的打包器。stage worker 只被 `new Worker(url)` 引用,另外两个只被 fetch 下来从 Blob URL import——没有一处是打包器能看见的静态 import,所以打包器要么整个漏掉这些文件,要么把它们改写坏,两种情况都不报错,只在运行时 404 或行为异常。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the raw-hosting rule to static loading

This states that all three browser bundles must bypass the host bundler and that the pool/core are only fetched and Blob-imported, but the recommended path later statically imports @dimina-kit/compiler/pool (README.md:89-96) and explicitly supports a bundler-resolved stage worker (README.md:135-137). As written, users following the new rule must add unnecessary manual copying and are told that the documented primary integration is invalid; limit this requirement to hosts that choose runtime URL loading.

AGENTS.md reference: AGENTS.md:L23-L24

Useful? React with 👍 / 👎.

dist 里三个 .browser.js 必须由宿主原样托管:stage worker 只被
new Worker(url) 引用,另外两个只被 fetch 后从 Blob URL import。没有
一处是打包器看得见的静态 import,所以文件名一旦变了,宿主那边不会
有任何编译期报错,只在运行时 404。

新增 @dimina-kit/compiler/browser-assets:清单 COMPILER_BROWSER_ASSETS、
从 ./browser 入口推出资源路径的 resolveBrowserAssets,宿主不用再手抄
文件名。同时发 ESM 与 CJS,只做字符串拼接,不依赖 node:path。

browser 构建现在开 metafile,拿 esbuild 自己的产物记录对着清单自检:
清单里的产物没被生成、生成了清单外的新产物(比如拆出 chunk)、或某个
静态资源开始带静态 import(不再自包含),构建直接失败。

scripts/test-browser-assets.js 把上述失败模式直接驱动一遍,并接进
package.json 的 test(turbo run test 会跑到)。
评审指出三处会静默放行的漏洞:产物被挪进 dist 子目录后,只比对文件名的检查看不出
差别;externalize 的 CJS 依赖在 metafile 里记成 require-call,只筛 import-statement
的检查漏掉它;清单里的文件名和 package.json 的 exports 各写各的,改名只落一边不会
报错。

现在产物名保留 dist 以下的目录,任何 import 类型都算破坏自包含(stage worker 的
import(toolchainSetupURL) 不受影响:specifier 是运行时变量,esbuild 记不下来,真实
浏览器构建实测三个资源零 import),并新增 checkAssetsAgainstExports 双向对账。

README 里"必须原样托管"的说法收窄到自己按 URL 托管的宿主——通过包名 import pool、
用 new URL 解析 stage worker 的宿主,打包器看得见引用,本来就该由它处理。
@lbb00

lbb00 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

合并进 #195,内容一致,关闭这个单独的 PR。

@lbb00 lbb00 closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant