@dimina-kit/compiler 0.0.1-dev.20260706064107 → 0.0.1-dev.20260706131625

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -160,17 +160,19 @@ Node 宿主(Electron devtools、CLI watch 服务等)用这个导出替代直
160
160
 
161
161
  dmcc 每次 `build()` 都新建 3 个 worker_threads、编完销毁,每次重编都要重新加载 sass/postcss/esbuild/oxc 并重启 esbuild 服务进程;本 pool 只在第一次付这笔钱。实测 watch 热重编(`dimina/fe/example` 全部 5 个 demo)比 dmcc 快 1.6–4×(base 826→207ms、weui 2716→763ms);冷启动略慢于 dmcc(3 个 worker 首次加载 bundle),"打开一次、保存无数次"的 devtools 场景下净赚。数据与方法见 [`docs/compile-fs-experiments.md`](./docs/compile-fs-experiments.md)。
162
162
 
163
- **dmcc drop-in(默认导出)**——签名、返回值、日志面貌与 dmcc `build()` 一致,宿主原有的日志抓取(`✔ 输出编译产物` / `✖ <stage>` / `<workPath> 编译出错:`)不用改:
163
+ **dmcc drop-in(默认导出)**——签名、成功返回值、日志面貌与 dmcc `build()` 一致,宿主原有的日志抓取(`✔ 输出编译产物` / `✖ <stage>` / `<workPath> 编译出错:`)不用改。**错误路径与 dmcc 刻意分歧:编译失败会 reject**(dmcc 吞错 resolve undefined,失败编译与"无 app 信息"从此不可区分,宿主会带着兜底 appId 启动一个只能 404 的会话)——失败时先照旧把 `✖ <stage>` + `编译出错:` 打到 stderr,再把错误抛给调用方:
164
164
 
165
165
  ```js
166
166
  import build from '@dimina-kit/compiler/pool-node'
167
167
 
168
168
  // 首次调用起常驻 worker,后续调用(watch 重编)复用
169
169
  const appInfo = await build(outputDir, workPath, true, { sourcemap: true, fileTypes })
170
- // 成功 → { appId, name, path };失败undefined(错误已打到 stderr,同 dmcc)
170
+ // 成功 → { appId, name, path };编译失败reject(错误同时已打到 stderr,日志面貌同 dmcc)
171
171
  ```
172
172
 
173
- 配套导出:`warmDefaultPool()` 提前创建这个 singleton 池并拉起 stage worker(不 build、无输出,项目无关——热备胎宿主在没有项目打开时调用,让第一次真实 build 从热池起步);`disposeDefaultPool()` 显式回收。
173
+ 配套导出:`warmDefaultPool()` 提前创建这个 singleton 池并拉起 stage worker(不 build、无输出,项目无关——热备胎宿主在没有项目打开时调用,让第一次真实 build 从热池起步);`disposeDefaultPool()` 显式回收;`oxcNativeBindingHint(message)` 把 oxc-parser 的 "Cannot find native binding" 报错映射成打包提示(其他消息返回 null)——stage 失败的 reject message 命中时会自动附带这条提示。
174
+
175
+ > **Electron 打包分发注意(oxc-parser 运行时绑定)**:Node 编译路径依赖 oxc-parser,其运行时需要 `@oxc-parser/binding-<platform>-<arch>`(平台原生 `.node`)或 `@oxc-parser/binding-wasm32-wasi`(wasm 兜底)**二者之一实际存在于分发包内**。它们都不是宿主的直接依赖,electron-builder 等工具收集依赖时容易丢——丢了之后每次编译都在 logic stage 报 `Cannot find native binding`。宿主打包时请把其中一个显式声明为自己的 dependency(或在打包配置里强制收集)。
174
176
 
175
177
  **结构化用法(`createNodeCompilerPool`)**——要抛错误对象、显式回收 worker 时用:
176
178
 
package/dist/pool.node.js CHANGED
@@ -248,7 +248,9 @@ function createNodeCompilerPool({
248
248
  for (const r of results) {
249
249
  if (!r || r.type === "error") {
250
250
  const info = r && r.error;
251
- const err = new Error(`[compiler] stage "${r && r.stage}" failed: ${info && info.message || "unknown error"}`);
251
+ const cause = info && info.message || "unknown error";
252
+ const hint = oxcNativeBindingHint(cause);
253
+ const err = new Error(`[compiler] stage "${r && r.stage}" failed: ${cause}${hint ? ` \u2014 ${hint}` : ""}`);
252
254
  if (info && info.stack) err.stack = info.stack;
253
255
  err.stage = r && r.stage;
254
256
  err.code = "compiler-stage-error";
@@ -297,6 +299,10 @@ function createNodeCompilerPool({
297
299
  return { build: build2, dispose, stages, _slots: workers };
298
300
  }
299
301
  var STAGE_TITLES = { logic: "\u7F16\u8BD1\u9875\u9762\u903B\u8F91", view: "\u7F16\u8BD1\u9875\u9762\u6587\u4EF6", style: "\u7F16\u8BD1\u6837\u5F0F\u6587\u4EF6" };
302
+ function oxcNativeBindingHint(message) {
303
+ if (!/Cannot find native binding/i.test(String(message))) return null;
304
+ return `oxc-parser \u7684\u8FD0\u884C\u65F6\u7ED1\u5B9A\u6CA1\u6709\u88AB\u6253\u8FDB\u5BBF\u4E3B\u5E94\u7528\uFF1A@dimina-kit/compiler \u7684 Node \u7F16\u8BD1\u8DEF\u5F84\u9700\u8981 @oxc-parser/binding-${process.platform}-${process.arch}\uFF08\u5E73\u53F0\u539F\u751F\u7ED1\u5B9A\uFF09\u6216 @oxc-parser/binding-wasm32-wasi\uFF08wasm \u515C\u5E95\uFF09\u4E8C\u8005\u4E4B\u4E00\u5B9E\u9645\u5B58\u5728\u4E8E\u5305\u5185\u3002\u6253\u5305\u5206\u53D1\uFF08\u5982 electron-builder\uFF09\u65F6\u8BF7\u628A\u5176\u4E2D\u4E00\u4E2A\u663E\u5F0F\u58F0\u660E\u4E3A\u5BBF\u4E3B\u4F9D\u8D56\uFF0C\u907F\u514D\u4F9D\u8D56\u6536\u96C6\u65F6\u88AB\u4E22\u5F03`;
305
+ }
300
306
  var singleton = null;
301
307
  async function build(outputDir, workPath, useAppIdDir = true, options = {}) {
302
308
  if (!singleton) singleton = createNodeCompilerPool();
@@ -307,7 +313,7 @@ async function build(outputDir, workPath, useAppIdDir = true, options = {}) {
307
313
  } catch (e) {
308
314
  if (e && e.stage) console.error(`\u2716 ${STAGE_TITLES[e.stage] || e.stage}`);
309
315
  console.error(`${workPath} \u7F16\u8BD1\u51FA\u9519: ${e && e.message}`);
310
- return void 0;
316
+ throw e;
311
317
  }
312
318
  }
313
319
  async function warmDefaultPool() {
@@ -325,5 +331,6 @@ export {
325
331
  createNodeCompilerPool,
326
332
  build as default,
327
333
  disposeDefaultPool,
334
+ oxcNativeBindingHint,
328
335
  warmDefaultPool
329
336
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimina-kit/compiler",
3
- "version": "0.0.1-dev.20260706064107",
3
+ "version": "0.0.1-dev.20260706131625",
4
4
  "description": "dmcc compiler bundles (browser + node) that drive @dimina/compiler against a caller-injected node:fs replacement (no bundled fs)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  // and require our pool to match dmcc only where dmcc matches itself. Assets (uuid-named)
9
9
  // are matched by CONTENT hash multiset instead of name. Sourcemap is asserted against
10
10
  // dmcc's own map (parity), since that is the format devtools already consumes.
11
- import { readdirSync, readFileSync, statSync, rmSync, mkdirSync } from 'node:fs'
11
+ import { readdirSync, readFileSync, statSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
  import { createHash } from 'node:crypto'
14
14
  import path from 'node:path'
@@ -42,7 +42,12 @@ function readTree(root) {
42
42
  }
43
43
 
44
44
  const dmccBuild = (await import('../../../dimina/fe/packages/compiler/src/index.js')).default
45
- const { createNodeCompilerPool } = await import('../dist/pool.node.js')
45
+ const poolNodeModule = await import('../dist/pool.node.js')
46
+ const { createNodeCompilerPool } = poolNodeModule
47
+ // Destructured lazily below (may be absent pre-fix): the default drop-in build(),
48
+ // the pure oxc-native-binding-hint mapper, and the lazy-singleton disposer.
49
+ const defaultBuild = poolNodeModule.default
50
+ const { oxcNativeBindingHint, disposeDefaultPool } = poolNodeModule
46
51
 
47
52
  // --- reference: dmcc twice ------------------------------------------------
48
53
  console.log(`[ref] dmcc build ×2 (sourcemap) from ${APP}`)
@@ -113,5 +118,90 @@ checkAgainstDmcc(readTree(path.join(dir('ours1'), appInfo1.appId)), 'cold')
113
118
  checkAgainstDmcc(readTree(path.join(dir('ours2'), appInfo2.appId)), 'warm')
114
119
 
115
120
  console.log(`[sourcemap] logic.js.map: ${refMap.sources.length} sources, sourcesContent present, matches dmcc ✅`)
116
- console.log(failed ? '\n❌ FAIL' : '\n✅ PASS: resident Node pool is dmcc-equivalent (incl. sourcemap) on cold+warm builds')
121
+
122
+ // --- failure contract: the default drop-in build() must REJECT a broken compile ------------------
123
+ // (never resolve undefined and let the caller mistake a failed compile for success), and the pool
124
+ // must expose a pure helper that turns a missing-oxc-native-binding message into an actionable
125
+ // packaging hint (checked as a string mapping only — a real environment can't be made to lack the
126
+ // binding on demand).
127
+ console.log("[fail] broken fixture: default build() must reject, not resolve undefined")
128
+ function writeBrokenFixture(root) {
129
+ const write = (rel, content) => {
130
+ const target = path.join(root, rel)
131
+ mkdirSync(path.dirname(target), { recursive: true })
132
+ writeFileSync(target, content)
133
+ }
134
+ write('project.config.json', JSON.stringify({ appid: 'fixture_app_broken', projectname: 'fixture-broken' }))
135
+ write('app.json', JSON.stringify({ pages: ['pages/index/index'] }))
136
+ write('app.js', 'App({})\n')
137
+ write('app.wxss', 'page { font-size: 14px; }\n')
138
+ write('pages/index/index.json', '{}\n')
139
+ // Unbalanced brace + bad tokens — same corruption shape used by devkit's
140
+ // open-project-compile-log fixture, chosen because it fails the logic stage
141
+ // rather than being silently absorbed by esbuild's error-tolerant fallback.
142
+ write('pages/index/index.js', 'Page({ data: { msg: "hi" } })\nconst broken = {{{ ;;; <<<\n')
143
+ write('pages/index/index.wxml', '<view>{{msg}}</view>\n')
144
+ write('pages/index/index.wxss', '.x { color: red; }\n')
145
+ }
146
+ const brokenRoot = dir('broken-fixture')
147
+ mkdirSync(brokenRoot, { recursive: true })
148
+ writeBrokenFixture(brokenRoot)
149
+
150
+ if (typeof defaultBuild !== 'function') {
151
+ fail('pool.node.js must have a default export `build(outputDir, workPath, useAppIdDir?, options?)` — default export is missing/not a function')
152
+ }
153
+ else {
154
+ let brokenResolvedValue
155
+ let brokenRejectedError
156
+ try {
157
+ brokenResolvedValue = await defaultBuild(dir('broken-out'), brokenRoot, true, {})
158
+ }
159
+ catch (e) {
160
+ brokenRejectedError = e
161
+ }
162
+ if (brokenRejectedError === undefined) {
163
+ fail(`default build() must REJECT on a broken compile — instead it resolved to ${JSON.stringify(brokenResolvedValue)} (a swallowed error looks like success to every caller)`)
164
+ }
165
+ else if (!(brokenRejectedError instanceof Error) || !brokenRejectedError.message) {
166
+ fail(`default build() rejected but without a usable Error message: ${brokenRejectedError}`)
167
+ }
168
+ else if (!/stage "logic" failed/.test(brokenRejectedError.message)) {
169
+ fail(`default build() rejection message must name the failing stage, got: ${brokenRejectedError.message.split('\n')[0]}`)
170
+ }
171
+ else if (!/Transform failed/.test(brokenRejectedError.message)) {
172
+ fail(`default build() rejection message must carry the underlying compile reason (esbuild transform failure), got: ${brokenRejectedError.message.split('\n')[0]}`)
173
+ }
174
+ else {
175
+ console.log(`[fail] ✅ default build() rejected with: ${brokenRejectedError.message.split('\n')[0]}`)
176
+ }
177
+ }
178
+
179
+ // --- oxcNativeBindingHint: pure message -> actionable-packaging-hint mapper -----------------------
180
+ if (typeof oxcNativeBindingHint !== 'function') {
181
+ fail('pool.node.js must export a named `oxcNativeBindingHint(message)` helper — export is missing')
182
+ }
183
+ else {
184
+ const hint = oxcNativeBindingHint('Cannot find native binding. It means oxc-parser was not installed correctly for this platform.')
185
+ if (typeof hint !== 'string' || !hint.includes('@oxc-parser/binding-') || !hint.includes('wasm32-wasi')) {
186
+ fail(`oxcNativeBindingHint must mention both the native platform binding package (@oxc-parser/binding-*) and the wasm32-wasi fallback, got: ${JSON.stringify(hint)}`)
187
+ }
188
+ else {
189
+ console.log('[fail] ✅ oxcNativeBindingHint surfaces a packaging fix for a missing native binding')
190
+ }
191
+ const noHint = oxcNativeBindingHint('some unrelated compile error')
192
+ if (noHint !== null) {
193
+ fail(`oxcNativeBindingHint must return null for a message that is not the native-binding failure, got: ${JSON.stringify(noHint)}`)
194
+ }
195
+ }
196
+
197
+ // The default export is a lazy module-level singleton pool (resident worker threads) — the
198
+ // broken-fixture build above spun it up, so the process cannot exit cleanly without disposing it.
199
+ if (typeof disposeDefaultPool !== 'function') {
200
+ fail('pool.node.js must export `disposeDefaultPool()` to tear down the lazy default-export singleton pool')
201
+ }
202
+ else {
203
+ await disposeDefaultPool()
204
+ }
205
+
206
+ console.log(failed ? '\n❌ FAIL' : '\n✅ PASS: resident Node pool is dmcc-equivalent (incl. sourcemap) on cold+warm builds, and rejects broken compiles')
117
207
  process.exit(failed ? 1 : 0)
package/src/pool-node.js CHANGED
@@ -167,7 +167,9 @@ export function createNodeCompilerPool({
167
167
  for (const r of results) {
168
168
  if (!r || r.type === 'error') {
169
169
  const info = r && r.error
170
- const err = new Error(`[compiler] stage "${r && r.stage}" failed: ${(info && info.message) || 'unknown error'}`)
170
+ const cause = (info && info.message) || 'unknown error'
171
+ const hint = oxcNativeBindingHint(cause)
172
+ const err = new Error(`[compiler] stage "${r && r.stage}" failed: ${cause}${hint ? ` — ${hint}` : ''}`)
171
173
  if (info && info.stack) err.stack = info.stack
172
174
  err.stage = r && r.stage
173
175
  err.code = 'compiler-stage-error' // worker-reported compile error — never retried
@@ -232,19 +234,40 @@ export function createNodeCompilerPool({
232
234
  // user-facing compile-log lines a host (e.g. devkit/devtools' log panel) already scrapes.
233
235
  const STAGE_TITLES = { logic: '编译页面逻辑', view: '编译页面文件', style: '编译样式文件' }
234
236
 
237
+ /**
238
+ * Map a failure message to an actionable packaging hint when it is oxc-parser's
239
+ * "missing runtime binding" error (thrown when NEITHER the platform-native
240
+ * `@oxc-parser/binding-<platform>` package NOR the `@oxc-parser/binding-wasm32-wasi`
241
+ * fallback resolves at runtime). Neither package is a direct dependency of a
242
+ * typical host, so app bundlers (e.g. electron-builder's dependency collection)
243
+ * silently drop them — and the raw oxc message says nothing about packaging.
244
+ * Returns null for every other message.
245
+ * @param {string} message
246
+ * @returns {string | null}
247
+ */
248
+ export function oxcNativeBindingHint(message) {
249
+ if (!/Cannot find native binding/i.test(String(message))) return null
250
+ return 'oxc-parser 的运行时绑定没有被打进宿主应用:@dimina-kit/compiler 的 Node 编译路径需要 '
251
+ + `@oxc-parser/binding-${process.platform}-${process.arch}(平台原生绑定)或 `
252
+ + '@oxc-parser/binding-wasm32-wasi(wasm 兜底)二者之一实际存在于包内。'
253
+ + '打包分发(如 electron-builder)时请把其中一个显式声明为宿主依赖,避免依赖收集时被丢弃'
254
+ }
255
+
235
256
  // Lazy singleton pool — a DROP-IN replacement for dmcc's `build(targetPath, workPath,
236
- // useAppIdDir, options)`, matching its behavior on BOTH the happy and error paths so a
237
- // host that consumed dmcc keeps working unchanged:
257
+ // useAppIdDir, options)` with ONE deliberate divergence on the error path:
238
258
  // • the first call spins up the resident workers; every later call (a watch rebuild)
239
259
  // reuses them warm;
240
260
  // • on success it emits `✔ 输出编译产物` on stdout (dmcc's listr2 completion line — the
241
261
  // pool has no listr2, so the equivalent user-facing line is surfaced here);
242
- // • on failure it reports the failing stage + summary on stderr and RESOLVES undefined
243
- // (never rethrows), exactly like dmcc's build() catch (index.js) the host normalizes
244
- // undefined to a null appInfo and the error detail (incl. dmcc's own
245
- // `[logic] esbuild 转换失败 …`) still reaches the log channel.
246
- // Callers that want structured throwing errors + explicit teardown should use
247
- // createNodeCompilerPool() directly instead.
262
+ // • on failure it reports the failing stage + summary on stderr (same lines a dmcc
263
+ // host already scrapes: `✖ <stage>` + `<workPath> 编译出错: …`) and then REJECTS
264
+ // with the error. dmcc's own build() swallows the error and resolves undefined,
265
+ // which makes a failed compile indistinguishable from benign "no app info" — a
266
+ // host would start a session that can only 404. Rejecting keeps the log surface
267
+ // identical while giving callers a real failure signal; a resolved null/undefined
268
+ // no longer means "compile failed", only "no app info to report".
269
+ // Callers that want structured errors (`.stage`/`.code`) + explicit teardown should
270
+ // use createNodeCompilerPool() directly instead.
248
271
  let singleton = null
249
272
  export default async function build(outputDir, workPath, useAppIdDir = true, options = {}) {
250
273
  if (!singleton) singleton = createNodeCompilerPool()
@@ -255,7 +278,7 @@ export default async function build(outputDir, workPath, useAppIdDir = true, opt
255
278
  } catch (e) {
256
279
  if (e && e.stage) console.error(`✖ ${STAGE_TITLES[e.stage] || e.stage}`)
257
280
  console.error(`${workPath} 编译出错: ${e && e.message}`)
258
- return undefined
281
+ throw e
259
282
  }
260
283
  }
261
284