@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 +5 -3
- package/dist/pool.node.js +9 -2
- package/package.json +1 -1
- package/scripts/test-pool-node.js +93 -3
- package/src/pool-node.js +33 -10
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
|
|
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 }
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
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
|
-
|
|
281
|
+
throw e
|
|
259
282
|
}
|
|
260
283
|
}
|
|
261
284
|
|