@dimina-kit/compiler 0.0.1 → 0.0.2-dev.20260711062001
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 +8 -3
- package/dist/compile-core.browser.js +1331 -1252
- package/dist/{pool.node-chunks/chunk-FVCSERCO.js → compile-core.node-chunks/chunk-LUD2P6RM.js} +5 -1
- package/dist/compile-core.node-chunks/{chunk-5ND5V2SC.js → chunk-QJ34C5KK.js} +77 -2
- package/dist/compile-core.node-chunks/{logic-compiler-RX63Y2FZ.js → logic-compiler-2SQFOFEO.js} +2 -2
- package/dist/compile-core.node-chunks/{style-compiler-JUY5MZSO.js → style-compiler-ZEJ3XLTS.js} +3 -3
- package/dist/compile-core.node-chunks/{view-compiler-KA2OZG44.js → view-compiler-WQNV7H5G.js} +25 -23
- package/dist/compile-core.node.js +10 -7
- package/dist/pool.browser.js +6 -5
- package/dist/pool.node-chunks/{chunk-UYQ7D5I5.js → chunk-7FGOYOXU.js} +77 -2
- package/dist/{compile-core.node-chunks/chunk-CCZB2BJG.js → pool.node-chunks/chunk-C7GEIDCP.js} +5 -1
- package/dist/pool.node-chunks/{chunk-MHOA4NGC.js → chunk-PDHO4Y56.js} +8 -5
- package/dist/pool.node-chunks/{logic-compiler-3VVLN3YN.js → logic-compiler-BODAINZQ.js} +2 -2
- package/dist/pool.node-chunks/{style-compiler-QOUWS72U.js → style-compiler-TUNDVSR5.js} +3 -3
- package/dist/pool.node-chunks/{view-compiler-KAYOMOXO.js → view-compiler-HOFFL63K.js} +25 -23
- package/dist/pool.node.js +34 -11
- package/dist/stage-worker.browser.js +1337 -1258
- package/dist/stage-worker.node.js +2 -2
- package/package.json +4 -2
- package/scripts/test-pool-filetypes.js +159 -0
- package/scripts/test-pool-node.js +93 -3
- package/scripts/test-pool-toolchain-death.js +135 -0
- package/scripts/toolchain-setup-node-native.js +13 -0
- package/src/compile-core.js +8 -5
- package/src/pool-node.js +84 -17
- package/src/pool.js +23 -10
- package/src/stage-worker.js +12 -8
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
preloadStage,
|
|
4
4
|
resetCompilerState,
|
|
5
5
|
runStage
|
|
6
|
-
} from "./pool.node-chunks/chunk-
|
|
6
|
+
} from "./pool.node-chunks/chunk-PDHO4Y56.js";
|
|
7
7
|
import {
|
|
8
8
|
getAppId,
|
|
9
9
|
getAppName,
|
|
10
10
|
resetStoreInfo
|
|
11
|
-
} from "./pool.node-chunks/chunk-
|
|
11
|
+
} from "./pool.node-chunks/chunk-7FGOYOXU.js";
|
|
12
12
|
|
|
13
13
|
// src/stage-worker-node.js
|
|
14
14
|
import { createRequire } from "node:module";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dimina-kit/compiler",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-dev.20260711062001",
|
|
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",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"test:pool-node": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-pool-node.js",
|
|
84
84
|
"test:pool-scopehash": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-pool-scopehash.js",
|
|
85
85
|
"test:pool-hardening": "node scripts/build-compiler.js node && node scripts/test-pool-hardening.js",
|
|
86
|
+
"test:pool-toolchain-death": "node scripts/build-compiler.js node && node scripts/test-pool-toolchain-death.js",
|
|
86
87
|
"test:pool-worker-hardening": "node scripts/test-pool-worker-hardening.js",
|
|
87
88
|
"test:worker-slot": "node scripts/test-worker-slot.js",
|
|
88
89
|
"test:pool-watchdog": "node scripts/test-pool-watchdog.js",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"test:npm-scan": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-npm-scan.js",
|
|
93
94
|
"test:stage-toolchain": "node scripts/build-compiler.js browser && node scripts/test-stage-toolchain.js",
|
|
94
95
|
"test:stage-worker-message-order": "node scripts/build-compiler.js node && node scripts/test-stage-worker-message-order.js",
|
|
95
|
-
"test:stage-load-retry": "node scripts/build-compiler.js node && node scripts/test-stage-load-retry.js"
|
|
96
|
+
"test:stage-load-retry": "node scripts/build-compiler.js node && node scripts/test-stage-load-retry.js",
|
|
97
|
+
"test:pool-filetypes": "node scripts/build-compiler.js node && node scripts/build-compiler.js browser && node scripts/test-pool-filetypes.js"
|
|
96
98
|
}
|
|
97
99
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Red/green coverage for `options.fileTypes` (custom template/style/view-script
|
|
2
|
+
// dialect, e.g. .qdml/.qdss/.qds) reaching the browser pool's compile protocol.
|
|
3
|
+
//
|
|
4
|
+
// dmcc's env.js already normalizes options.fileTypes into a `compilerOptions`
|
|
5
|
+
// bundle inside storeInfo (dimina/fe/packages/compiler/src/env.js:91-120), and
|
|
6
|
+
// that bundle rides inside bundle.storeInfo through the setup->compile-subset
|
|
7
|
+
// broadcast (compile-core.js's compileStage -> resetStoreInfo, env.js:122-132) —
|
|
8
|
+
// see src/pool.js's runAttempt and src/stage-worker.js's runSetup/compileSubset.
|
|
9
|
+
// This test drives the REAL browser bundles (dist/pool.browser.js +
|
|
10
|
+
// dist/stage-worker.browser.js) in Node, so it fails (red) without the
|
|
11
|
+
// options-threading fix and passes (green) with it — no reimplementation of the
|
|
12
|
+
// protocol handlers under test.
|
|
13
|
+
//
|
|
14
|
+
// Fixture mirrors qdmp's e2e "qd-app" (see
|
|
15
|
+
// ~/code/qdmp/main/packages/qdmp-devtools/e2e/qd-app): a page using .qdml/.qdss/.qds
|
|
16
|
+
// instead of .wxml/.wxss/.wxs, with a <wxs src="./index.qds" module="m" /> view
|
|
17
|
+
// script and a `{{ m.shout(title) }}` mustache expression — so a correct compile
|
|
18
|
+
// must recognize the custom template AND the custom view-script extension.
|
|
19
|
+
//
|
|
20
|
+
// Also covers compile-core.js's compileMiniApp (the one-shot worker entry point)
|
|
21
|
+
// at the bottom, since it shares the exact same options-threading gap.
|
|
22
|
+
|
|
23
|
+
// Warm real esbuild/oxc-parser module eval AND esbuild's long-lived service child
|
|
24
|
+
// process with the REAL process object BEFORE masking (below) — the service, once
|
|
25
|
+
// spawned, is reused for later transforms even under a masked process, so this
|
|
26
|
+
// must run first.
|
|
27
|
+
import { transform } from 'esbuild'
|
|
28
|
+
import 'oxc-parser'
|
|
29
|
+
|
|
30
|
+
await transform('const __warm = 1', {})
|
|
31
|
+
|
|
32
|
+
// dart-sass's bundled browser shim checks process.versions.node at module-eval
|
|
33
|
+
// time (only reached once the lazily-loaded style-compiler chunk imports it) —
|
|
34
|
+
// masking to a browser-shaped stub makes it take the browser branch instead of
|
|
35
|
+
// crashing on `Dynamic require of "url" is not supported` (see
|
|
36
|
+
// scripts/test-stage-toolchain.js for the same technique). Saved so PART B below
|
|
37
|
+
// (a Node-target bundle, which needs the real process — its transitive deps
|
|
38
|
+
// module-eval-check process.versions.node too) can restore it.
|
|
39
|
+
const realProcess = globalThis.process
|
|
40
|
+
globalThis.process = { env: {}, cwd: () => '/' }
|
|
41
|
+
|
|
42
|
+
const WORKER_URL = new URL('../dist/stage-worker.browser.js', import.meta.url).href
|
|
43
|
+
const TOOLCHAIN_URL = new URL('./toolchain-setup-node-native.js', import.meta.url).href
|
|
44
|
+
const WORK_PATH = '/work'
|
|
45
|
+
|
|
46
|
+
const FIXTURE_FILES = {
|
|
47
|
+
'app.json': JSON.stringify({ pages: ['pages/index/index'], window: { navigationBarTitleText: 'QD Ext Demo' } }),
|
|
48
|
+
'project.config.json': JSON.stringify({ appid: 'qd_ext_demo_001', projectname: 'qd-ext-demo' }),
|
|
49
|
+
'app.js': "App({\n onLaunch() {\n console.log('[QDExt] App launched')\n },\n})\n",
|
|
50
|
+
'pages/index/index.js': "Page({\n data: {\n title: 'QD Extensions',\n count: 3,\n },\n})\n",
|
|
51
|
+
'pages/index/index.qdss': '.qd-box {\n padding: 20rpx;\n}\n\n.qd-title {\n font-size: 36rpx;\n color: #4A90D9;\n}\n\n.qd-item {\n font-size: 28rpx;\n color: #333;\n}\n',
|
|
52
|
+
'pages/index/index.qdml': '<wxs src="./index.qds" module="m" />\n<view class="qd-box">\n <view class="qd-title" id="qd-title">{{ m.shout(title) }}</view>\n <view class="qd-item" wx:for="{{count}}" wx:key="*this">item-{{item}}</view>\n</view>\n',
|
|
53
|
+
'pages/index/index.json': JSON.stringify({ navigationBarTitleText: 'QD Ext Index' }),
|
|
54
|
+
'pages/index/index.qds': "function shout(text) {\n return text + '!'\n}\n\nmodule.exports = {\n shout: shout,\n}\n",
|
|
55
|
+
}
|
|
56
|
+
const FILE_TYPES_OPTIONS = { fileTypes: { template: ['qdml'], style: ['qdss'], viewScript: ['qds'] } }
|
|
57
|
+
|
|
58
|
+
let failed = false
|
|
59
|
+
const chk = (cond, msg) => { if (!cond) { failed = true; console.error(`❌ ${msg}`) } else console.log(`✅ ${msg}`) }
|
|
60
|
+
|
|
61
|
+
// Real page module: exists, non-empty, and carries the compiled .qds wxs module
|
|
62
|
+
// (`shout`) plus the compiled .qdml view (`qd-title`) — the assertion this whole
|
|
63
|
+
// test hinges on. Without options.fileTypes this is guaranteed to fail (the
|
|
64
|
+
// custom extensions are unrecognized, so the page template/view-script are never
|
|
65
|
+
// found), which is exactly the RED this test protects against regressing to.
|
|
66
|
+
function pageModuleIsCompiled(files) {
|
|
67
|
+
const js = files && files['main/pages_index_index.js']
|
|
68
|
+
return typeof js === 'string' && js.length > 0 && js.includes('shout') && js.includes('qd-title')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- PART A: real browser pool (dist/pool.browser.js + dist/stage-worker.browser.js) ---
|
|
72
|
+
//
|
|
73
|
+
// Each stage worker is its own dynamically-imported module instance (own closure
|
|
74
|
+
// state, mirroring a real Web Worker realm); ALL of them funnel through one shared
|
|
75
|
+
// `chain` so only one is ever "in flight" at a time. This matters because the
|
|
76
|
+
// worker module resolves the bare `self` identifier against whatever
|
|
77
|
+
// `globalThis.self` currently is (see test-stage-toolchain.js's note on the same
|
|
78
|
+
// mechanism) — serializing every send means no two realms' async continuations
|
|
79
|
+
// can ever race over that shared global.
|
|
80
|
+
let chain = Promise.resolve()
|
|
81
|
+
let instanceCounter = 0
|
|
82
|
+
async function makeStageWorker() {
|
|
83
|
+
const worker = { onmessage: null, onerror: null, terminate() {} }
|
|
84
|
+
const fakeSelf = {
|
|
85
|
+
onmessage: null,
|
|
86
|
+
postMessage(msg) { if (worker.onmessage) worker.onmessage({ data: msg }) },
|
|
87
|
+
}
|
|
88
|
+
globalThis.self = fakeSelf
|
|
89
|
+
instanceCounter += 1
|
|
90
|
+
await import(`${WORKER_URL}?n=${instanceCounter}`)
|
|
91
|
+
// stage-worker.js does `self.onmessage = async (e) => {...}` at module top level
|
|
92
|
+
// against whatever `globalThis.self` was AT IMPORT TIME — capture it now.
|
|
93
|
+
const boundOnMessage = fakeSelf.onmessage
|
|
94
|
+
worker.postMessage = (msg) => {
|
|
95
|
+
chain = chain.then(async () => {
|
|
96
|
+
globalThis.self = fakeSelf
|
|
97
|
+
try {
|
|
98
|
+
await boundOnMessage({ data: msg })
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// The real handler already catches internally and posts { type:'error' };
|
|
101
|
+
// an escaping exception would mean it threw before its own try, so surface
|
|
102
|
+
// it the same way rather than hanging the pool's pending request forever.
|
|
103
|
+
fakeSelf.postMessage({ type: 'error', error: String((err && err.stack) || err) })
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return worker
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { createCompilerPool } = await import('../dist/pool.browser.js')
|
|
111
|
+
|
|
112
|
+
// One pool, pre-spawned with exactly the 3 default-stage workers this test needs
|
|
113
|
+
// (no crash/respawn expected) — createWorker must be synchronous (pool.js calls it
|
|
114
|
+
// inline), so every worker instance is built up front and handed out from a queue.
|
|
115
|
+
const stageWorkers = []
|
|
116
|
+
for (const stage of ['logic', 'view', 'style']) stageWorkers.push(await makeStageWorker())
|
|
117
|
+
let nextWorker = 0
|
|
118
|
+
const pool = createCompilerPool({
|
|
119
|
+
createWorker: () => stageWorkers[nextWorker++],
|
|
120
|
+
toolchainSetupURL: TOOLCHAIN_URL,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const withoutOptions = await pool.compile({ files: FIXTURE_FILES, workPath: WORK_PATH })
|
|
124
|
+
chk(!pageModuleIsCompiled(withoutOptions.files),
|
|
125
|
+
'PART A (pool.js + stage-worker.js): WITHOUT options.fileTypes, the .qdml/.qdss/.qds page module is NOT compiled '
|
|
126
|
+
+ `(demonstrates the gap this feature closes; got main/pages_index_index.js=${JSON.stringify(withoutOptions.files['main/pages_index_index.js'])})`)
|
|
127
|
+
|
|
128
|
+
const withOptions = await pool.compile({ files: FIXTURE_FILES, workPath: WORK_PATH, options: FILE_TYPES_OPTIONS })
|
|
129
|
+
chk(pageModuleIsCompiled(withOptions.files),
|
|
130
|
+
'PART A (pool.js + stage-worker.js): WITH options.fileTypes, the .qdml/.qdss/.qds page module IS compiled and non-empty '
|
|
131
|
+
+ `(main/pages_index_index.js length=${(withOptions.files['main/pages_index_index.js'] || '').length})`)
|
|
132
|
+
chk(withOptions.appId === 'qd_ext_demo_001', `PART A: compiled appId matches project.config.json (got ${withOptions.appId})`)
|
|
133
|
+
const compiledCss = withOptions.files['main/pages_index_index.css'] || ''
|
|
134
|
+
chk(compiledCss.includes('qd-title'), `PART A: .qdss style file compiled into page CSS (got ${JSON.stringify(compiledCss)})`)
|
|
135
|
+
|
|
136
|
+
await pool.dispose()
|
|
137
|
+
|
|
138
|
+
// --- PART B: compile-core.js's compileMiniApp (one-shot worker entry point) ---
|
|
139
|
+
// Same options-threading gap, covered directly at the compile-core level (no
|
|
140
|
+
// pool/worker-protocol involved) so a future pool refactor still has this pinned
|
|
141
|
+
// independently of the browser-pool wiring above. This is the NODE-target bundle
|
|
142
|
+
// (dist/compile-core.node.js), whose deps expect a real `process` — restore it
|
|
143
|
+
// before importing.
|
|
144
|
+
globalThis.process = realProcess
|
|
145
|
+
const { compileMiniApp } = await import('../dist/compile-core.node.js')
|
|
146
|
+
const { Volume, createFsFromVolume } = await import('memfs')
|
|
147
|
+
const freshFs = () => createFsFromVolume(Volume.fromJSON(FIXTURE_FILES, WORK_PATH))
|
|
148
|
+
|
|
149
|
+
const coreWithout = await compileMiniApp({ fs: freshFs(), workPath: WORK_PATH })
|
|
150
|
+
chk(!pageModuleIsCompiled(coreWithout.files),
|
|
151
|
+
'PART B (compile-core.js compileMiniApp): WITHOUT options.fileTypes, page module NOT compiled')
|
|
152
|
+
|
|
153
|
+
const coreWith = await compileMiniApp({ fs: freshFs(), workPath: WORK_PATH, options: FILE_TYPES_OPTIONS })
|
|
154
|
+
chk(pageModuleIsCompiled(coreWith.files),
|
|
155
|
+
'PART B (compile-core.js compileMiniApp): WITH options.fileTypes, page module IS compiled and non-empty '
|
|
156
|
+
+ `(length=${(coreWith.files['main/pages_index_index.js'] || '').length})`)
|
|
157
|
+
|
|
158
|
+
console.log(failed ? '\n❌ FAIL' : '\n✅ PASS: options.fileTypes reaches the browser pool protocol (pool.js/stage-worker.js) and compile-core.js\'s compileMiniApp')
|
|
159
|
+
process.exit(failed ? 1 : 0)
|
|
@@ -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)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Regression coverage for the resident Node disk pool's handling of a DEAD esbuild
|
|
2
|
+
// toolchain service — the Windows-packaged-app crash pattern: esbuild's JS lib spawns
|
|
3
|
+
// a long-lived binary child process (the "service"), and in a packaged Electron app
|
|
4
|
+
// that spawn can fail (e.g. the binary sits inside app.asar, which fs.existsSync()
|
|
5
|
+
// sees but child_process.spawn() cannot exec). Once that service is dead inside a
|
|
6
|
+
// warm worker realm, EVERY later esbuild call in that same realm fails with
|
|
7
|
+
// "The service is no longer running: write EPIPE" — a message shape that looks
|
|
8
|
+
// nothing like the real cause. Today the pool classifies this as
|
|
9
|
+
// `code: 'compiler-stage-error'` ("deterministic, never retried") and keeps the
|
|
10
|
+
// broken worker, so the pool never recovers even after the environment is healed.
|
|
11
|
+
//
|
|
12
|
+
// Fault injection needs no source changes: esbuild's node lib honors
|
|
13
|
+
// ESBUILD_BINARY_PATH, and each worker_threads realm captures a COPY of
|
|
14
|
+
// process.env at spawn. Pointing that var (before the pool spawns its workers) at
|
|
15
|
+
// a real, executable file whose shebang interpreter does not exist reproduces the
|
|
16
|
+
// exact "fs.existsSync() true, spawn() ENOENT" split a binary trapped inside
|
|
17
|
+
// app.asar produces — child_process.spawn() reports ENOENT because the kernel's
|
|
18
|
+
// own shebang-interpreter lookup fails, not because the file is missing.
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const APP = process.env.APP_DIR
|
|
24
|
+
|| fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url))
|
|
25
|
+
const TMP = fileURLToPath(new URL('../.tmp-pool-toolchain-death/', import.meta.url))
|
|
26
|
+
fs.rmSync(TMP, { recursive: true, force: true })
|
|
27
|
+
fs.mkdirSync(TMP, { recursive: true })
|
|
28
|
+
const dir = (n) => path.join(TMP, n)
|
|
29
|
+
|
|
30
|
+
const FAKE_ESBUILD = path.join(TMP, 'fake-esbuild-service')
|
|
31
|
+
fs.writeFileSync(FAKE_ESBUILD, '#!/nonexistent/esbuild-interpreter\necho unreachable\n')
|
|
32
|
+
fs.chmodSync(FAKE_ESBUILD, 0o755)
|
|
33
|
+
|
|
34
|
+
let failed = false
|
|
35
|
+
const chk = (cond, msg) => { if (!cond) { failed = true; console.error(`❌ ${msg}`) } else console.log(`✅ ${msg}`) }
|
|
36
|
+
const withTimeout = (p, ms, label) => Promise.race([
|
|
37
|
+
p,
|
|
38
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error(`TIMEOUT: ${label} did not settle in ${ms}ms`)), ms).unref()),
|
|
39
|
+
])
|
|
40
|
+
const settle = (p) => p.then((v) => ({ ok: true, v }), (e) => ({ ok: false, e }))
|
|
41
|
+
const treeOk = (d, i) => fs.existsSync(path.join(d, i.appId, 'main', 'logic.js'))
|
|
42
|
+
|
|
43
|
+
// Property access (not a named import) so a dist build that has not yet added
|
|
44
|
+
// esbuildAsarSpawnHint fails this script's own assertion instead of a module-link
|
|
45
|
+
// SyntaxError that would abort before any other scenario runs.
|
|
46
|
+
const poolModule = await import('../dist/pool.node.js')
|
|
47
|
+
const { createNodeCompilerPool } = poolModule
|
|
48
|
+
const esbuildAsarSpawnHint = poolModule.esbuildAsarSpawnHint
|
|
49
|
+
|
|
50
|
+
// --- classification + recycle ------------------------------------------------------
|
|
51
|
+
// A dead esbuild service must be coded distinctly from a real compile error, and the
|
|
52
|
+
// worker(s) that hit it must be recycled so the NEXT build — once the environment is
|
|
53
|
+
// healed — runs on a fresh realm instead of replaying the same dead service forever.
|
|
54
|
+
process.env.ESBUILD_BINARY_PATH = FAKE_ESBUILD
|
|
55
|
+
const pool = createNodeCompilerPool({ retryOnWorkerDeath: false })
|
|
56
|
+
// Read immediately after creation, before the pool's own eager ensureAlive() settles —
|
|
57
|
+
// the pristine pre-spawn baseline every later generation count is compared against.
|
|
58
|
+
const gensPristine = pool._slots.map((x) => x.slot.generation)
|
|
59
|
+
|
|
60
|
+
const first = await withTimeout(
|
|
61
|
+
settle(pool.build(dir('a1'), APP, true, {})), 60000, 'first build against a dead esbuild service',
|
|
62
|
+
)
|
|
63
|
+
chk(!first.ok, 'first build against a dead esbuild service rejects')
|
|
64
|
+
const firstErr = first.ok ? null : first.e
|
|
65
|
+
chk(!!firstErr && firstErr.code === 'compiler-toolchain-dead',
|
|
66
|
+
`dead-esbuild-service failure is coded 'compiler-toolchain-dead' (got ${firstErr && firstErr.code})`)
|
|
67
|
+
chk(!!firstErr && typeof firstErr.stage === 'string' && firstErr.stage.length > 0,
|
|
68
|
+
`error carries .stage (got ${firstErr && firstErr.stage})`)
|
|
69
|
+
chk(!!firstErr && /^\[compiler\] stage "[^"]+" failed: .*The service (was stopped|is no longer running)/.test(firstErr.message || ''),
|
|
70
|
+
`error message keeps the stage-shaped prefix around the dead-service cause: ${firstErr && firstErr.message}`)
|
|
71
|
+
|
|
72
|
+
// Heal the environment the way a real fix would (repackage / relaunch): only NEWLY
|
|
73
|
+
// spawned worker realms see this — the pool must actually recycle, not get lucky that
|
|
74
|
+
// the already-spawned realm somehow un-breaks itself.
|
|
75
|
+
delete process.env.ESBUILD_BINARY_PATH
|
|
76
|
+
|
|
77
|
+
const second = await withTimeout(
|
|
78
|
+
settle(pool.build(dir('a2'), APP, true, {})), 60000, 'build after healing the toolchain env',
|
|
79
|
+
)
|
|
80
|
+
chk(second.ok && treeOk(dir('a2'), second.v),
|
|
81
|
+
`build after healing the env succeeds via recycled workers, not a permanent write EPIPE: ${second.ok ? 'ok' : second.e && second.e.message}`)
|
|
82
|
+
|
|
83
|
+
const gensAfterHeal = pool._slots.map((x) => x.slot.generation)
|
|
84
|
+
chk(gensAfterHeal.some((g, i) => g - gensPristine[i] > 1),
|
|
85
|
+
`pool recycled at least one worker slot between the dead-service failure and the next build (pristine ${gensPristine.join(',')} -> after-heal ${gensAfterHeal.join(',')})`)
|
|
86
|
+
|
|
87
|
+
await pool.dispose()
|
|
88
|
+
|
|
89
|
+
// --- transparent retry on a persistently broken toolchain --------------------------
|
|
90
|
+
// retryOnWorkerDeath defaults to true and already gives worker crashes one whole-build
|
|
91
|
+
// retry; a dead-toolchain failure must get the same treatment. With the environment
|
|
92
|
+
// STILL broken the retry cannot succeed either, so the build must still reject (no
|
|
93
|
+
// hang) — but by then the affected slot(s) should have been recycled on both the
|
|
94
|
+
// initial attempt and its retry.
|
|
95
|
+
process.env.ESBUILD_BINARY_PATH = FAKE_ESBUILD
|
|
96
|
+
const poolR = createNodeCompilerPool() // retryOnWorkerDeath defaults to true
|
|
97
|
+
const gensPristineR = poolR._slots.map((x) => x.slot.generation)
|
|
98
|
+
|
|
99
|
+
const retried = await withTimeout(
|
|
100
|
+
settle(poolR.build(dir('r1'), APP, true, {})), 120000, 'build against a persistently dead esbuild service with retry enabled',
|
|
101
|
+
)
|
|
102
|
+
chk(!retried.ok, 'build against a persistently dead toolchain still rejects rather than hanging')
|
|
103
|
+
chk(!retried.ok && retried.e && retried.e.code === 'compiler-toolchain-dead',
|
|
104
|
+
`the final rejection after the transparent retry is still coded 'compiler-toolchain-dead' (got ${retried.ok ? undefined : retried.e && retried.e.code})`)
|
|
105
|
+
|
|
106
|
+
const gensAfterRetry = poolR._slots.map((x) => x.slot.generation)
|
|
107
|
+
chk(gensAfterRetry.some((g, i) => g - gensPristineR[i] > 1),
|
|
108
|
+
`a persistently dead toolchain is recycled on both the initial attempt and its retry (pristine ${gensPristineR.join(',')} -> after-retry ${gensAfterRetry.join(',')})`)
|
|
109
|
+
|
|
110
|
+
await poolR.dispose()
|
|
111
|
+
delete process.env.ESBUILD_BINARY_PATH
|
|
112
|
+
|
|
113
|
+
// --- pure helper: actionable asar-unpack hint for a dead esbuild service ------------
|
|
114
|
+
chk(typeof esbuildAsarSpawnHint === 'function', 'esbuildAsarSpawnHint is exported from the pool module')
|
|
115
|
+
if (typeof esbuildAsarSpawnHint === 'function') {
|
|
116
|
+
const asarMsg = 'The service was stopped: spawn C:\\Users\\x\\AppData\\Local\\Programs\\myapp\\resources\\app.asar\\node_modules\\@esbuild\\win32-x64\\esbuild.exe ENOENT'
|
|
117
|
+
const hint = esbuildAsarSpawnHint(asarMsg)
|
|
118
|
+
chk(typeof hint === 'string' && /asarUnpack/.test(hint),
|
|
119
|
+
`an asar-packaged esbuild ENOENT message gets an asarUnpack hint (got ${JSON.stringify(hint)})`)
|
|
120
|
+
|
|
121
|
+
const syntaxMsg = 'Transform failed with 1 error:\nfile.js:1:1: ERROR: Unexpected "}"'
|
|
122
|
+
chk(esbuildAsarSpawnHint(syntaxMsg) === null,
|
|
123
|
+
`an unrelated esbuild syntax error gets no hint (got ${JSON.stringify(esbuildAsarSpawnHint(syntaxMsg))})`)
|
|
124
|
+
|
|
125
|
+
const oxcMsg = 'Cannot find native binding. Napi module could not be resolved.'
|
|
126
|
+
chk(esbuildAsarSpawnHint(oxcMsg) === null,
|
|
127
|
+
`an oxc native-binding message (oxcNativeBindingHint's own territory) gets no hint here (got ${JSON.stringify(esbuildAsarSpawnHint(oxcMsg))})`)
|
|
128
|
+
} else {
|
|
129
|
+
failed = true
|
|
130
|
+
console.error('❌ skipped esbuildAsarSpawnHint behavior checks (not exported)')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.rmSync(TMP, { recursive: true, force: true })
|
|
134
|
+
console.log(failed ? '\n❌ FAIL' : '\n✅ PASS: dead-esbuild-service classification, recycle-on-failure, retry-still-rejects, asar hint')
|
|
135
|
+
process.exit(failed ? 1 : 0)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Test-only toolchainSetupURL: installs globalThis.__esbuildTransform /
|
|
2
|
+
// __oxcParseSync from the NATIVE esbuild/oxc-parser already in this package's own
|
|
3
|
+
// dependencies (see package.json), instead of the real esbuild-wasm/oxc wasm32-wasi
|
|
4
|
+
// bindings a real browser host would fetch. Both native functions have the exact
|
|
5
|
+
// signature the browser shims (src/shims/esbuild-wasm.js / oxc-parser.js) forward
|
|
6
|
+
// to verbatim (transform(input,options) / parseSync(filename,code,opts)), so this
|
|
7
|
+
// exercises the REAL dist/stage-worker.browser.js code path end-to-end in Node
|
|
8
|
+
// without paying for a wasm toolchain load.
|
|
9
|
+
import { transform } from 'esbuild'
|
|
10
|
+
import { parseSync } from 'oxc-parser'
|
|
11
|
+
|
|
12
|
+
globalThis.__esbuildTransform = (input, options) => transform(input, options)
|
|
13
|
+
globalThis.__oxcParseSync = parseSync
|
package/src/compile-core.js
CHANGED
|
@@ -455,13 +455,16 @@ let compileChain = Promise.resolve()
|
|
|
455
455
|
* Compile a mini-program against a caller-injected fs. Calls are serialized per
|
|
456
456
|
* realm (see the singleton note above). Convenience wrapper that runs
|
|
457
457
|
* `setupCompile` + all stages + `collectOutputs` in one realm.
|
|
458
|
-
* @param {{ fs: object, workPath?: string }} opts
|
|
458
|
+
* @param {{ fs: object, workPath?: string, options?: { fileTypes?: { template?: string[], style?: string[], viewScript?: string[] } } }} opts
|
|
459
459
|
* fs: a node:fs replacement (sync subset: existsSync/readFileSync/
|
|
460
460
|
* readdirSync{withFileTypes}/statSync/writeFileSync/mkdirSync{recursive}/
|
|
461
461
|
* copyFileSync/rmSync), already seeded with the project source under
|
|
462
462
|
* `workPath`. The compiler also writes products back into it, and a
|
|
463
463
|
* missing project.config.json appid is written into it.
|
|
464
464
|
* workPath: project root inside the fs, default '/work'.
|
|
465
|
+
* options: forwarded to `setupCompile` -> dmcc's `storeInfo` (custom file-type
|
|
466
|
+
* dialect, e.g. { fileTypes: { template: ['qdml'], style: ['qdss'],
|
|
467
|
+
* viewScript: ['qds'] } }).
|
|
465
468
|
* @returns {Promise<{ appId: string, name: string, files: Record<string,string> }>}
|
|
466
469
|
*/
|
|
467
470
|
export function compileMiniApp(opts = {}) {
|
|
@@ -472,12 +475,12 @@ export function compileMiniApp(opts = {}) {
|
|
|
472
475
|
return result
|
|
473
476
|
}
|
|
474
477
|
|
|
475
|
-
async function runCompile({ fs, workPath = '/work' } = {}) {
|
|
476
|
-
const ctx = await setupCompile({ fs, workPath })
|
|
478
|
+
async function runCompile({ fs, workPath = '/work', options = {} } = {}) {
|
|
479
|
+
const ctx = await setupCompile({ fs, workPath, options })
|
|
477
480
|
const { storeInfo: bundle, pages, appId, name, targetPath } = ctx
|
|
478
481
|
// Same order as the original single pass. Stages are independent (no product
|
|
479
|
-
// read-back), so the order is not load-bearing —
|
|
480
|
-
// in separate worker realms over a shared fs.
|
|
482
|
+
// read-back), so the order is not load-bearing — the worker pool (pool.js) runs
|
|
483
|
+
// them concurrently in separate worker realms over a shared fs.
|
|
481
484
|
await compileStage({ stage: 'logic', pages, storeInfo: bundle, fs })
|
|
482
485
|
await compileStage({ stage: 'view', pages, storeInfo: bundle, fs })
|
|
483
486
|
await compileStage({ stage: 'style', pages, storeInfo: bundle, fs })
|
package/src/pool-node.js
CHANGED
|
@@ -38,7 +38,18 @@ const { Worker } = createRequire(import.meta.url)('node:worker_threads')
|
|
|
38
38
|
// generous than the browser default: disk builds run whole real projects.
|
|
39
39
|
const DEFAULT_SEND_TIMEOUT_MS = 120000
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// 'compiler-toolchain-dead' belongs here even though the worker THREAD is alive: the
|
|
42
|
+
// realm's esbuild service child process is gone, so the realm is just as unusable as a
|
|
43
|
+
// crashed worker — the same one-retry-on-fresh-workers policy applies.
|
|
44
|
+
const WORKER_DEATH_CODES = new Set(['compiler-worker-timeout', 'compiler-worker-crashed', 'compiler-worker-dead', 'compiler-toolchain-dead'])
|
|
45
|
+
|
|
46
|
+
// esbuild's node lib drives a spawned long-lived binary child (its "service"). When that
|
|
47
|
+
// child dies (spawn ENOENT in a packaged app, OOM kill, AV kill), esbuild reports every
|
|
48
|
+
// call with one of these two phrases — and the service NEVER restarts inside that realm,
|
|
49
|
+
// so the warm worker is permanently broken and must be recycled, not kept.
|
|
50
|
+
function isDeadToolchainServiceError(message) {
|
|
51
|
+
return /The service (was stopped|is no longer running)/.test(String(message))
|
|
52
|
+
}
|
|
42
53
|
|
|
43
54
|
// Default idle window before the pool shrinks (terminates its resident stage workers to
|
|
44
55
|
// release their memory — a warm worker set holds hundreds of MB of toolchain + compile
|
|
@@ -164,16 +175,33 @@ export function createNodeCompilerPool({
|
|
|
164
175
|
if (err && err.code && !err.stage) err.stage = x.stage
|
|
165
176
|
throw err
|
|
166
177
|
})))
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
178
|
+
// Walk EVERY result before throwing: when more than one stage hit a dead toolchain
|
|
179
|
+
// service (logic and view both drive esbuild), each broken realm must be recycled
|
|
180
|
+
// now — throwing on the first would leave the sibling's dead service warm, and the
|
|
181
|
+
// next build would fail on it all over again.
|
|
182
|
+
let firstErr = null
|
|
183
|
+
for (let i = 0; i < results.length; i++) {
|
|
184
|
+
const r = results[i]
|
|
185
|
+
if (r && r.type !== 'error') continue
|
|
186
|
+
const info = r && r.error
|
|
187
|
+
const cause = (info && info.message) || 'unknown error'
|
|
188
|
+
const hint = oxcNativeBindingHint(cause) || esbuildAsarSpawnHint(cause)
|
|
189
|
+
const err = new Error(`[compiler] stage "${r && r.stage}" failed: ${cause}${hint ? ` — ${hint}` : ''}`)
|
|
190
|
+
if (info && info.stack) err.stack = info.stack
|
|
191
|
+
err.stage = r && r.stage
|
|
192
|
+
if (isDeadToolchainServiceError(cause)) {
|
|
193
|
+
// The realm's toolchain service child is dead and never comes back — terminate
|
|
194
|
+
// the worker so the next attempt (the transparent retry, or the next build once
|
|
195
|
+
// the environment is healed) respawns a fresh realm with a fresh service.
|
|
196
|
+
// shrink() is safe here: settleAll above guarantees no request is in flight.
|
|
197
|
+
err.code = 'compiler-toolchain-dead'
|
|
198
|
+
workers[i].slot.shrink()
|
|
199
|
+
} else {
|
|
173
200
|
err.code = 'compiler-stage-error' // worker-reported compile error — never retried
|
|
174
|
-
throw err
|
|
175
201
|
}
|
|
202
|
+
if (!firstErr) firstErr = err
|
|
176
203
|
}
|
|
204
|
+
if (firstErr) throw firstErr
|
|
177
205
|
|
|
178
206
|
// 3) Publish the staging dir to the caller's outputDir (dmcc-identical layout).
|
|
179
207
|
publishToDist(outputDir, useAppIdDir)
|
|
@@ -232,19 +260,58 @@ export function createNodeCompilerPool({
|
|
|
232
260
|
// user-facing compile-log lines a host (e.g. devkit/devtools' log panel) already scrapes.
|
|
233
261
|
const STAGE_TITLES = { logic: '编译页面逻辑', view: '编译页面文件', style: '编译样式文件' }
|
|
234
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Map a failure message to an actionable packaging hint when it is esbuild failing to
|
|
265
|
+
* spawn its native binary from inside an Electron app.asar archive. Electron patches
|
|
266
|
+
* child_process.execFile for asar paths but NOT child_process.spawn (which esbuild
|
|
267
|
+
* uses), so an in-archive binary path always ENOENTs at spawn even though fs sees the
|
|
268
|
+
* file — the raw message points at a path that plainly exists, which is why it needs
|
|
269
|
+
* a hint. Returns null for every other message.
|
|
270
|
+
* @param {string} message
|
|
271
|
+
* @returns {string | null}
|
|
272
|
+
*/
|
|
273
|
+
export function esbuildAsarSpawnHint(message) {
|
|
274
|
+
const msg = String(message)
|
|
275
|
+
if (!/app\.asar/.test(msg) || !/esbuild/i.test(msg) || !/ENOENT/.test(msg)) return null
|
|
276
|
+
return 'esbuild 的原生二进制无法从 app.asar 内 spawn(Electron 只为 execFile 打 asar 补丁):'
|
|
277
|
+
+ "打包配置需 asarUnpack '**/node_modules/esbuild/**' 与 '**/node_modules/@esbuild/**',"
|
|
278
|
+
+ '并确保 ESBUILD_BINARY_PATH 指向 app.asar.unpacked 下的真实二进制(@dimina-kit/devkit 在 asar 内运行时会自动设置)'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Map a failure message to an actionable packaging hint when it is oxc-parser's
|
|
283
|
+
* "missing runtime binding" error (thrown when NEITHER the platform-native
|
|
284
|
+
* `@oxc-parser/binding-<platform>` package NOR the `@oxc-parser/binding-wasm32-wasi`
|
|
285
|
+
* fallback resolves at runtime). Neither package is a direct dependency of a
|
|
286
|
+
* typical host, so app bundlers (e.g. electron-builder's dependency collection)
|
|
287
|
+
* silently drop them — and the raw oxc message says nothing about packaging.
|
|
288
|
+
* Returns null for every other message.
|
|
289
|
+
* @param {string} message
|
|
290
|
+
* @returns {string | null}
|
|
291
|
+
*/
|
|
292
|
+
export function oxcNativeBindingHint(message) {
|
|
293
|
+
if (!/Cannot find native binding/i.test(String(message))) return null
|
|
294
|
+
return 'oxc-parser 的运行时绑定没有被打进宿主应用:@dimina-kit/compiler 的 Node 编译路径需要 '
|
|
295
|
+
+ `@oxc-parser/binding-${process.platform}-${process.arch}(平台原生绑定)或 `
|
|
296
|
+
+ '@oxc-parser/binding-wasm32-wasi(wasm 兜底)二者之一实际存在于包内。'
|
|
297
|
+
+ '打包分发(如 electron-builder)时请把其中一个显式声明为宿主依赖,避免依赖收集时被丢弃'
|
|
298
|
+
}
|
|
299
|
+
|
|
235
300
|
// Lazy singleton pool — a DROP-IN replacement for dmcc's `build(targetPath, workPath,
|
|
236
|
-
// useAppIdDir, options)
|
|
237
|
-
// host that consumed dmcc keeps working unchanged:
|
|
301
|
+
// useAppIdDir, options)` with ONE deliberate divergence on the error path:
|
|
238
302
|
// • the first call spins up the resident workers; every later call (a watch rebuild)
|
|
239
303
|
// reuses them warm;
|
|
240
304
|
// • on success it emits `✔ 输出编译产物` on stdout (dmcc's listr2 completion line — the
|
|
241
305
|
// 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
|
-
//
|
|
306
|
+
// • on failure it reports the failing stage + summary on stderr (same lines a dmcc
|
|
307
|
+
// host already scrapes: `✖ <stage>` + `<workPath> 编译出错: …`) and then REJECTS
|
|
308
|
+
// with the error. dmcc's own build() swallows the error and resolves undefined,
|
|
309
|
+
// which makes a failed compile indistinguishable from benign "no app info" — a
|
|
310
|
+
// host would start a session that can only 404. Rejecting keeps the log surface
|
|
311
|
+
// identical while giving callers a real failure signal; a resolved null/undefined
|
|
312
|
+
// no longer means "compile failed", only "no app info to report".
|
|
313
|
+
// Callers that want structured errors (`.stage`/`.code`) + explicit teardown should
|
|
314
|
+
// use createNodeCompilerPool() directly instead.
|
|
248
315
|
let singleton = null
|
|
249
316
|
export default async function build(outputDir, workPath, useAppIdDir = true, options = {}) {
|
|
250
317
|
if (!singleton) singleton = createNodeCompilerPool()
|
|
@@ -255,7 +322,7 @@ export default async function build(outputDir, workPath, useAppIdDir = true, opt
|
|
|
255
322
|
} catch (e) {
|
|
256
323
|
if (e && e.stage) console.error(`✖ ${STAGE_TITLES[e.stage] || e.stage}`)
|
|
257
324
|
console.error(`${workPath} 编译出错: ${e && e.message}`)
|
|
258
|
-
|
|
325
|
+
throw e
|
|
259
326
|
}
|
|
260
327
|
}
|
|
261
328
|
|