@dimina-kit/compiler 0.0.1-dev.20260702173719
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/LICENSE +21 -0
- package/README.md +363 -0
- package/dist/compile-core.browser.js +281945 -0
- package/dist/compile-core.node.js +3702 -0
- package/dist/pool.browser.js +99 -0
- package/dist/pool.node.js +3743 -0
- package/dist/stage-worker.browser.js +291085 -0
- package/dist/stage-worker.node.js +3097 -0
- package/dist/toolchain.browser.js +30 -0
- package/package.json +87 -0
- package/scripts/build-compiler.js +207 -0
- package/scripts/gen-bench-fixture.js +24 -0
- package/scripts/kit-resolve-hook.js +35 -0
- package/scripts/register-kit.js +2 -0
- package/scripts/test-appid-fallback.js +114 -0
- package/scripts/test-decompose.js +90 -0
- package/scripts/test-hardening.js +78 -0
- package/scripts/test-node.js +69 -0
- package/scripts/test-npm-scan.js +164 -0
- package/scripts/test-pool-hardening.js +65 -0
- package/scripts/test-pool-node.js +117 -0
- package/scripts/test-realm-reuse.js +77 -0
- package/src/browser-entry.js +25 -0
- package/src/compile-core.js +428 -0
- package/src/pool-node.js +170 -0
- package/src/pool.js +122 -0
- package/src/shims/esbuild-wasm.js +19 -0
- package/src/shims/fs-promises.js +20 -0
- package/src/shims/fs.js +59 -0
- package/src/shims/less.js +9 -0
- package/src/shims/os.js +11 -0
- package/src/shims/oxc-parser.js +21 -0
- package/src/shims/oxc-walker.js +29 -0
- package/src/shims/postcss-noop-plugin.js +9 -0
- package/src/shims/process.js +20 -0
- package/src/shims/url.js +4 -0
- package/src/shims/worker_threads.js +10 -0
- package/src/stage-worker-node.js +41 -0
- package/src/stage-worker.js +88 -0
- package/src/toolchain.js +49 -0
package/src/pool.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Orchestrated compile pool — the "batteries-included" export. The downstream calls
|
|
2
|
+
// createCompilerPool(...).compile(source) and gets the merged build back; the pool
|
|
3
|
+
// owns ALL the orchestration the downstream used to hand-write:
|
|
4
|
+
// - a resident worker per stage, kept warm so the wasm toolchain loads ONCE
|
|
5
|
+
// - dmcc-consistent stage-level parallelism (logic | view | style each in a realm)
|
|
6
|
+
// - realm reuse across compiles (resetCompilerState inside the worker)
|
|
7
|
+
// - dispatch + disjoint-output merge
|
|
8
|
+
//
|
|
9
|
+
// What stays with the downstream is only what is genuinely host-specific:
|
|
10
|
+
// - createWorker(): how to spawn a module Worker running this package's stage worker
|
|
11
|
+
// (bundler/hosting-specific — one line)
|
|
12
|
+
// - toolchainSetupURL: an ESM the worker imports to install the two wasm hooks
|
|
13
|
+
// (esbuild.wasm / oxc wasm are host-hosted assets)
|
|
14
|
+
// - the source itself (a { relPath: content } map). OPFS is intentionally NOT here:
|
|
15
|
+
// it's an optional zero-copy source-distribution the downstream can layer on.
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STAGES = ['logic', 'view', 'style']
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* createWorker: () => Worker, // required: spawn a module worker running dist/stage-worker.browser.js
|
|
22
|
+
* toolchainSetupURL: string, // required: ESM URL that installs __esbuildTransform/__oxcParseSync in the worker
|
|
23
|
+
* stages?: string[], // default ['logic','view','style']
|
|
24
|
+
* workPath?: string, // default '/work'
|
|
25
|
+
* onLog?: (entry: { level: string, message: string }) => void, // worker console diagnostics
|
|
26
|
+
* }} options
|
|
27
|
+
*/
|
|
28
|
+
export function createCompilerPool(options = {}) {
|
|
29
|
+
const {
|
|
30
|
+
createWorker,
|
|
31
|
+
toolchainSetupURL,
|
|
32
|
+
stages = DEFAULT_STAGES,
|
|
33
|
+
workPath: defaultWorkPath = '/work',
|
|
34
|
+
onLog,
|
|
35
|
+
} = options
|
|
36
|
+
if (typeof createWorker !== 'function') {
|
|
37
|
+
throw new Error('[compiler] createCompilerPool: options.createWorker (() => Worker) is required')
|
|
38
|
+
}
|
|
39
|
+
if (!toolchainSetupURL) {
|
|
40
|
+
throw new Error('[compiler] createCompilerPool: options.toolchainSetupURL is required')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// one resident worker per stage. send() returns a promise resolved by the worker's
|
|
44
|
+
// next message; replies are FIFO so a per-worker queue pairs them up.
|
|
45
|
+
const workers = stages.map((stage) => {
|
|
46
|
+
const w = createWorker()
|
|
47
|
+
const q = []
|
|
48
|
+
w.onmessage = (e) => {
|
|
49
|
+
const d = e.data
|
|
50
|
+
// Diagnostics the compiler logs inside the worker (missing components, unsupported
|
|
51
|
+
// wx APIs, style-preprocessor fallbacks, …) arrive as out-of-band { type:'log' }
|
|
52
|
+
// messages — surface them via onLog instead of pairing them with a send() reply.
|
|
53
|
+
if (d && d.type === 'log') { if (onLog) { try { onLog({ level: d.level, message: d.message, stage }) } catch { /* ignore */ } } return }
|
|
54
|
+
const r = q.shift(); if (r) r(d)
|
|
55
|
+
}
|
|
56
|
+
w.onerror = (ev) => {
|
|
57
|
+
// Worker-script-level failure (module load / uncaught). ErrorEvent.message is often
|
|
58
|
+
// empty for these — surface filename:lineno and a hint so it's not a bare "error".
|
|
59
|
+
const msg = (ev && (ev.message || (ev.error && ev.error.message)))
|
|
60
|
+
|| 'worker failed to load or threw (no message — often a module-load / static-asset / cross-origin failure)'
|
|
61
|
+
const where = ev && ev.filename ? ` (${ev.filename}:${ev.lineno || 0})` : ''
|
|
62
|
+
const r = q.shift(); if (r) r({ type: 'error', error: `[compiler] stage '${stage}' worker error: ${msg}${where}` })
|
|
63
|
+
}
|
|
64
|
+
return { stage, w, send: (m) => new Promise((res) => { q.push(res); w.postMessage(m) }) }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let warmed = null
|
|
68
|
+
async function warmup() {
|
|
69
|
+
if (!warmed) {
|
|
70
|
+
warmed = Promise.all(workers.map((x) => x.send({ type: 'warmup', toolchainSetupURL })))
|
|
71
|
+
.then((rs) => rs.forEach((r, i) => {
|
|
72
|
+
// The worker's own try/catch reports the REAL cause (e.g. a toolchainSetupURL
|
|
73
|
+
// import failure) as r.error — surface it verbatim, tagged with the stage.
|
|
74
|
+
if (r && r.type === 'error') throw new Error(r.error || `[compiler] stage '${workers[i].stage}' warmup failed`)
|
|
75
|
+
}))
|
|
76
|
+
.catch((err) => { warmed = null; throw err })
|
|
77
|
+
}
|
|
78
|
+
return warmed
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compiles share the resident realms, so they must not overlap — serialize them.
|
|
82
|
+
let chain = Promise.resolve()
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single argument, no ambiguity: pass { files, workPath }. A bare { relPath: content }
|
|
86
|
+
* map is also accepted (uses the default workPath).
|
|
87
|
+
* @param {{ files: Record<string,string>, workPath?: string } | Record<string,string>} input
|
|
88
|
+
* @returns {Promise<{ appId: string, name: string, files: Record<string,string> }>}
|
|
89
|
+
*/
|
|
90
|
+
function compile(input = {}) {
|
|
91
|
+
const run = chain.then(async () => {
|
|
92
|
+
await warmup()
|
|
93
|
+
const files = input.files || input
|
|
94
|
+
if (!files || typeof files !== 'object' || !Object.keys(files).length) {
|
|
95
|
+
throw new Error('[compiler] pool.compile expects { files: { relPath: content }, workPath? } (or a non-empty files map)')
|
|
96
|
+
}
|
|
97
|
+
const workPath = input.workPath || defaultWorkPath
|
|
98
|
+
const parts = await Promise.all(workers.map((x) =>
|
|
99
|
+
x.send({ type: 'compile-subset', files, workPath, stages: [x.stage] })))
|
|
100
|
+
const merged = {}
|
|
101
|
+
let appId, name
|
|
102
|
+
for (let i = 0; i < parts.length; i++) {
|
|
103
|
+
const pr = parts[i]
|
|
104
|
+
// pr.error carries the worker's real error string (message + stack) — surface it.
|
|
105
|
+
if (!pr || pr.type === 'error') throw new Error(pr && pr.error ? pr.error : `[compiler] stage '${workers[i].stage}' worker error`)
|
|
106
|
+
appId = pr.result.appId
|
|
107
|
+
name = pr.result.name
|
|
108
|
+
Object.assign(merged, pr.result.files) // stages write disjoint files -> clean union
|
|
109
|
+
}
|
|
110
|
+
return { appId, name, files: merged }
|
|
111
|
+
})
|
|
112
|
+
// keep the chain alive regardless of this compile's outcome
|
|
113
|
+
chain = run.then(() => {}, () => {})
|
|
114
|
+
return run
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function dispose() {
|
|
118
|
+
for (const x of workers) { try { x.w.terminate() } catch { /* ignore */ } }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { warmup, compile, dispose, stages: [...stages] }
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Replacement for `esbuild` (native). esbuild-wasm is NOT bundled here — bundling
|
|
2
|
+
// its embedded Go runtime corrupts it (the bundler renames its internal `globalThis`
|
|
3
|
+
// shadow). Instead the host worker loads esbuild-wasm as a pristine module and
|
|
4
|
+
// installs `globalThis.__esbuildTransform`; we just delegate to it.
|
|
5
|
+
|
|
6
|
+
export function initEsbuild() {
|
|
7
|
+
// no-op: the host worker initializes esbuild-wasm and sets the hook
|
|
8
|
+
return Promise.resolve()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function transform(input, options) {
|
|
12
|
+
const fn = globalThis.__esbuildTransform
|
|
13
|
+
if (!fn) {
|
|
14
|
+
throw new Error('[esbuild] globalThis.__esbuildTransform not installed by host')
|
|
15
|
+
}
|
|
16
|
+
return fn(input, options)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default { transform, initEsbuild }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// node:fs/promises -> forwards to the injected fs's promises API.
|
|
2
|
+
// The compiler uses no async fs; this exists only to satisfy the alias, so the
|
|
3
|
+
// bundle carries no memfs (or any) fs implementation of its own.
|
|
4
|
+
import { promises } from './fs.js'
|
|
5
|
+
|
|
6
|
+
export default promises
|
|
7
|
+
export const readFile = (...a) => promises.readFile(...a)
|
|
8
|
+
export const writeFile = (...a) => promises.writeFile(...a)
|
|
9
|
+
export const mkdir = (...a) => promises.mkdir(...a)
|
|
10
|
+
export const rm = (...a) => promises.rm(...a)
|
|
11
|
+
export const rmdir = (...a) => promises.rmdir(...a)
|
|
12
|
+
export const readdir = (...a) => promises.readdir(...a)
|
|
13
|
+
export const copyFile = (...a) => promises.copyFile(...a)
|
|
14
|
+
export const stat = (...a) => promises.stat(...a)
|
|
15
|
+
export const lstat = (...a) => promises.lstat(...a)
|
|
16
|
+
export const unlink = (...a) => promises.unlink(...a)
|
|
17
|
+
export const rename = (...a) => promises.rename(...a)
|
|
18
|
+
export const appendFile = (...a) => promises.appendFile(...a)
|
|
19
|
+
export const access = (...a) => promises.access(...a)
|
|
20
|
+
export const realpath = (...a) => promises.realpath(...a)
|
package/src/shims/fs.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// node:fs / fs -> whatever the caller injects; there is NO built-in backend.
|
|
2
|
+
//
|
|
3
|
+
// The compiler is all `import fs from 'node:fs'` + `fs.xxx`, aliased to this
|
|
4
|
+
// shim. compileMiniApp({ fs }) sets the active backend via setFs() for the
|
|
5
|
+
// duration of one compile; with nothing injected, every fs.* call throws. This
|
|
6
|
+
// is what lets web-compiler carry no fs implementation of its own — the host
|
|
7
|
+
// brings its own node:fs replacement (memfs, or anything meeting the contract).
|
|
8
|
+
let current = null
|
|
9
|
+
|
|
10
|
+
export function setFs(impl) { current = impl }
|
|
11
|
+
export function resetFs() { current = null }
|
|
12
|
+
export function getFs() { return current }
|
|
13
|
+
|
|
14
|
+
// Forward a method to the injected backend. Every method — including lstat /
|
|
15
|
+
// realpath / access — forwards to the real fs; there are NO synthetic fallbacks.
|
|
16
|
+
// (An identity realpath, a no-op access, or lstat aliased to stat would hand the
|
|
17
|
+
// compiler wrong semantics silently.) A method the backend lacks throws.
|
|
18
|
+
const call = (name) => (...args) => {
|
|
19
|
+
if (!current) throw new Error('[compiler] no fs backend injected — call compileMiniApp({ fs })')
|
|
20
|
+
const fn = current[name]
|
|
21
|
+
if (typeof fn === 'function') return fn.apply(current, args)
|
|
22
|
+
throw new Error(`[compiler] injected fs is missing ${name}()`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const existsSync = call('existsSync')
|
|
26
|
+
export const readFileSync = call('readFileSync')
|
|
27
|
+
export const writeFileSync = call('writeFileSync')
|
|
28
|
+
export const mkdirSync = call('mkdirSync')
|
|
29
|
+
export const rmSync = call('rmSync')
|
|
30
|
+
export const rmdirSync = call('rmdirSync')
|
|
31
|
+
export const readdirSync = call('readdirSync')
|
|
32
|
+
export const copyFileSync = call('copyFileSync')
|
|
33
|
+
export const statSync = call('statSync')
|
|
34
|
+
export const lstatSync = call('lstatSync')
|
|
35
|
+
export const unlinkSync = call('unlinkSync')
|
|
36
|
+
export const renameSync = call('renameSync')
|
|
37
|
+
export const appendFileSync = call('appendFileSync')
|
|
38
|
+
export const realpathSync = call('realpathSync')
|
|
39
|
+
export const accessSync = call('accessSync')
|
|
40
|
+
export const readlinkSync = call('readlinkSync')
|
|
41
|
+
export const watch = call('watch')
|
|
42
|
+
|
|
43
|
+
// Async API — forwarded to the injected fs.promises if present. The compiler
|
|
44
|
+
// uses no async fs; this only satisfies the node:fs/promises alias.
|
|
45
|
+
export const promises = new Proxy({}, {
|
|
46
|
+
get: (_t, prop) => (...args) => {
|
|
47
|
+
if (!current || !current.promises || typeof current.promises[prop] !== 'function') {
|
|
48
|
+
throw new Error(`[compiler] injected fs has no promises.${String(prop)}()`)
|
|
49
|
+
}
|
|
50
|
+
return current.promises[prop](...args)
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const fs = {
|
|
55
|
+
existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, rmdirSync,
|
|
56
|
+
readdirSync, copyFileSync, statSync, lstatSync, unlinkSync, renameSync,
|
|
57
|
+
appendFileSync, realpathSync, accessSync, readlinkSync, watch, promises,
|
|
58
|
+
}
|
|
59
|
+
export default fs
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Minimal `less` stub for the browser build.
|
|
2
|
+
// The base example uses .wxss only; .less compilation is not wired yet.
|
|
3
|
+
// (less ships a browser build; proper interop can replace this stub later.)
|
|
4
|
+
const less = {
|
|
5
|
+
render() {
|
|
6
|
+
return Promise.reject(new Error('[less] .less compilation not supported in this browser build yet'))
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
export default less
|
package/src/shims/os.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// minimal node:os shim for browser
|
|
2
|
+
export const cpus = () => [{}, {}, {}, {}]
|
|
3
|
+
export const totalmem = () => 2 * 1024 * 1024 * 1024
|
|
4
|
+
export const freemem = () => 1 * 1024 * 1024 * 1024
|
|
5
|
+
export const platform = () => 'browser'
|
|
6
|
+
export const tmpdir = () => '/tmp'
|
|
7
|
+
// homedir: lilconfig (via the real cssnano's config lookup) calls os.homedir()
|
|
8
|
+
// while walking up for a cssnano config. We have none, so any stable path works.
|
|
9
|
+
export const homedir = () => '/'
|
|
10
|
+
export const EOL = '\n'
|
|
11
|
+
export default { cpus, totalmem, freemem, platform, tmpdir, homedir, EOL }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// oxc-parser replacement. The browser-capable wasm build is oxc-parser's OWN
|
|
2
|
+
// official wasm32-wasi binding (version-matched, maintained) — loaded by the host
|
|
3
|
+
// worker (it has top-level await + a relative wasm fetch + WASI worker, so it
|
|
4
|
+
// can't be inlined into this bundle). The host installs globalThis.__oxcParseSync;
|
|
5
|
+
// the wasm `parseSync(filename, sourceText, options)` signature matches native, so
|
|
6
|
+
// we forward verbatim — no adaptation, identical AST.
|
|
7
|
+
|
|
8
|
+
export function parseSync(filename, code, opts) {
|
|
9
|
+
const fn = globalThis.__oxcParseSync
|
|
10
|
+
if (!fn) {
|
|
11
|
+
throw new Error('[oxc] globalThis.__oxcParseSync not installed by host')
|
|
12
|
+
}
|
|
13
|
+
return fn(filename, code, opts)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// kept for API compatibility; the host worker initializes the wasm directly.
|
|
17
|
+
export function initOxc() {
|
|
18
|
+
return Promise.resolve()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default { parseSync, initOxc }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Replacement for oxc-walker's `walk` (generic ESTree-ish traversal).
|
|
2
|
+
// The compiler only uses walk(ast, { enter(node, parent) }) — no skip/replace/remove.
|
|
3
|
+
const SKIP_KEYS = new Set([
|
|
4
|
+
'type', 'start', 'end', 'loc', 'range', 'parent',
|
|
5
|
+
'leadingComments', 'trailingComments', 'innerComments',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export function walk(ast, visitor = {}) {
|
|
9
|
+
const { enter, leave } = visitor
|
|
10
|
+
function visit(node, parent) {
|
|
11
|
+
if (!node || typeof node.type !== 'string') return
|
|
12
|
+
if (enter) enter(node, parent)
|
|
13
|
+
for (const key in node) {
|
|
14
|
+
if (SKIP_KEYS.has(key)) continue
|
|
15
|
+
const val = node[key]
|
|
16
|
+
if (Array.isArray(val)) {
|
|
17
|
+
for (const c of val) {
|
|
18
|
+
if (c && typeof c.type === 'string') visit(c, node)
|
|
19
|
+
}
|
|
20
|
+
} else if (val && typeof val.type === 'string') {
|
|
21
|
+
visit(val, node)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (leave) leave(node, parent)
|
|
25
|
+
}
|
|
26
|
+
visit(ast, null)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default { walk }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// No-op postcss plugin standing in for autoprefixer / cssnano in the browser build.
|
|
2
|
+
// Those pull in browserslist/caniuse which need a Node `process` global; the demo
|
|
3
|
+
// skips autoprefixing & minification rather than polluting global process
|
|
4
|
+
// (which would make sass / esbuild-wasm / the Go wasm runtime mis-detect Node).
|
|
5
|
+
function noop() {
|
|
6
|
+
return { postcssPlugin: 'noop-shim', Once() {} }
|
|
7
|
+
}
|
|
8
|
+
noop.postcss = true
|
|
9
|
+
export default noop
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// minimal node:process shim for browser
|
|
2
|
+
const proc = {
|
|
3
|
+
cwd: () => '/',
|
|
4
|
+
chdir: () => {},
|
|
5
|
+
platform: 'browser',
|
|
6
|
+
env: {},
|
|
7
|
+
argv: ['node', 'compiler'],
|
|
8
|
+
version: 'v18.0.0',
|
|
9
|
+
versions: { node: '18.0.0' },
|
|
10
|
+
nextTick: (cb, ...args) => Promise.resolve().then(() => cb(...args)),
|
|
11
|
+
exit: () => {},
|
|
12
|
+
on: () => {},
|
|
13
|
+
hrtime: () => [0, 0],
|
|
14
|
+
}
|
|
15
|
+
export default proc
|
|
16
|
+
export const cwd = proc.cwd
|
|
17
|
+
export const env = proc.env
|
|
18
|
+
export const platform = proc.platform
|
|
19
|
+
export const argv = proc.argv
|
|
20
|
+
export const nextTick = proc.nextTick
|
package/src/shims/url.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// node:worker_threads shim.
|
|
2
|
+
// isMainThread = true so the compiler files SKIP their `if (!isMainThread)` parentPort
|
|
3
|
+
// bootstrap blocks. We drive the exported compile functions directly instead.
|
|
4
|
+
export const isMainThread = true
|
|
5
|
+
export const parentPort = null
|
|
6
|
+
export const workerData = null
|
|
7
|
+
export class Worker {
|
|
8
|
+
constructor() { throw new Error('worker_threads.Worker is not available in browser build') }
|
|
9
|
+
}
|
|
10
|
+
export default { isMainThread, parentPort, workerData, Worker }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Resident Node worker_threads stage worker for the disk pool.
|
|
2
|
+
//
|
|
3
|
+
// One of these runs per stage (logic | view | style). It stays warm across builds
|
|
4
|
+
// (the pool never terminates it between compiles) and writes its stage's product
|
|
5
|
+
// files DIRECTLY to the shared real-disk staging dir (getTargetPath(), carried in
|
|
6
|
+
// the storeInfo the main thread ships). Stages write disjoint file names, so the
|
|
7
|
+
// three workers can write the same staging dir concurrently — exactly like dmcc's
|
|
8
|
+
// own index.js does.
|
|
9
|
+
//
|
|
10
|
+
// fs is NATIVE here (this bundle is built without the fs alias — see build-compiler.js),
|
|
11
|
+
// so dmcc's `import fs from 'node:fs'` hits real disk. worker_threads is SHIMMED for
|
|
12
|
+
// dmcc (isMainThread=true) so its own parentPort bootstrap stays off; we grab the REAL
|
|
13
|
+
// parentPort via createRequire to talk to the pool.
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import { runStage, resetCompilerState } from './compile-core.js'
|
|
16
|
+
import { resetStoreInfo, getAppId, getAppName } from '../../../dimina/fe/packages/compiler/src/env.js'
|
|
17
|
+
|
|
18
|
+
const { parentPort } = createRequire(import.meta.url)('node:worker_threads')
|
|
19
|
+
|
|
20
|
+
if (!parentPort) {
|
|
21
|
+
throw new Error('[compiler] stage-worker-node.js must run inside a worker_threads Worker')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
parentPort.on('message', async (msg) => {
|
|
25
|
+
const { stage, pages, storeInfo, sourcemap } = msg || {}
|
|
26
|
+
try {
|
|
27
|
+
// Warm-realm hygiene: clear this worker's module-level caches so a reused worker
|
|
28
|
+
// does not leak state from the previous build (same contract as the browser pool).
|
|
29
|
+
resetCompilerState()
|
|
30
|
+
// Restore the env singletons (paths/config/targetPath) from the main thread's setup.
|
|
31
|
+
resetStoreInfo(storeInfo)
|
|
32
|
+
await runStage(stage, pages, { sourcemap })
|
|
33
|
+
parentPort.postMessage({ type: 'done', stage, appId: getAppId(), name: getAppName() })
|
|
34
|
+
} catch (error) {
|
|
35
|
+
parentPort.postMessage({
|
|
36
|
+
type: 'error',
|
|
37
|
+
stage,
|
|
38
|
+
error: { message: error && error.message, stack: error && error.stack, name: error && error.name },
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Resident stage worker — shipped BY this package so downstream doesn't hand-write
|
|
2
|
+
// worker glue. One instance runs ONE full compile stage (logic | view | style) in a
|
|
3
|
+
// whole realm; the pool (src/pool.js) keeps three of them warm and unions the
|
|
4
|
+
// disjoint outputs. This is the dmcc-consistent parallel axis (each stage entirely
|
|
5
|
+
// in one realm, so view sees all pages and app-level module dedup still holds).
|
|
6
|
+
//
|
|
7
|
+
// The wasm toolchain (esbuild-wasm + oxc-parser) can't be inlined here (their Go/WASI
|
|
8
|
+
// runtimes break when bundled), and their .wasm assets are host-specific — so the
|
|
9
|
+
// host provides ONE `toolchainSetupURL`: an ESM module that, when imported inside
|
|
10
|
+
// this worker, installs `globalThis.__esbuildTransform` and `globalThis.__oxcParseSync`
|
|
11
|
+
// (see README). That URL is the ONLY wasm-hosting detail the downstream owns; all
|
|
12
|
+
// orchestration, fs seeding, reset-reuse and merge live in this package.
|
|
13
|
+
//
|
|
14
|
+
// Source distribution is deliberately OPFS-free: the pool posts the source map and we
|
|
15
|
+
// seed it into our own memfs. A downstream that wants zero-copy OPFS distribution can
|
|
16
|
+
// layer it on top (hydrate OPFS -> a files map before calling the pool).
|
|
17
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
18
|
+
import { setupCompile, compileStage, collectOutputs, resetCompilerState } from './compile-core.js'
|
|
19
|
+
|
|
20
|
+
// The compiler logs diagnostics (missing components, unsupported wx APIs, style
|
|
21
|
+
// preprocessor fallbacks, asset-copy failures, …) via console.* inside this worker,
|
|
22
|
+
// where a downstream can't see them. Forward them to the pool as { type:'log' } so
|
|
23
|
+
// createCompilerPool({ onLog }) can surface them; still log locally for devtools.
|
|
24
|
+
for (const level of ['log', 'warn', 'error']) {
|
|
25
|
+
const orig = typeof console[level] === 'function' ? console[level].bind(console) : () => {}
|
|
26
|
+
console[level] = (...args) => {
|
|
27
|
+
try { self.postMessage({ type: 'log', level, message: args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ') }) } catch { /* ignore */ }
|
|
28
|
+
orig(...args)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load the host's wasm toolchain exactly once. Memoized on the setup URL; a failed
|
|
33
|
+
// load clears the cache so a later message can retry instead of replaying the reject.
|
|
34
|
+
let toolchainReady = null
|
|
35
|
+
let toolchainURL = null
|
|
36
|
+
function ensureToolchain(url) {
|
|
37
|
+
if (url) toolchainURL = url
|
|
38
|
+
if (!toolchainReady) {
|
|
39
|
+
if (!toolchainURL) return Promise.reject(new Error('[compiler] stage worker not warmed up: no toolchainSetupURL (call pool.warmup first)'))
|
|
40
|
+
toolchainReady = import(/* @vite-ignore */ toolchainURL)
|
|
41
|
+
.catch((err) => { toolchainReady = null; throw new Error(`[compiler] toolchain setup failed importing ${toolchainURL}: ${(err && err.message) || err}`) })
|
|
42
|
+
}
|
|
43
|
+
return toolchainReady
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function freshFs(files, workPath) {
|
|
47
|
+
return createFsFromVolume(Volume.fromJSON(files, workPath))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Compile only the requested stages against a fresh memfs seeded with the source.
|
|
51
|
+
// resetCompilerState() clears the compiler's module-level caches so this warm realm
|
|
52
|
+
// stays correct across compiles. Stages write disjoint products; we return this
|
|
53
|
+
// worker's subset and the pool unions them.
|
|
54
|
+
async function compileSubset(files, workPath, stages) {
|
|
55
|
+
const fs = freshFs(files, workPath)
|
|
56
|
+
resetCompilerState()
|
|
57
|
+
const ctx = await setupCompile({ fs, workPath })
|
|
58
|
+
for (const stage of stages) {
|
|
59
|
+
await compileStage({ stage, pages: ctx.pages, storeInfo: ctx.storeInfo, fs })
|
|
60
|
+
}
|
|
61
|
+
const map = collectOutputs({ fs, targetPath: ctx.targetPath })
|
|
62
|
+
const out = {}
|
|
63
|
+
for (const k of Object.keys(map)) if (map[k] != null) out[k] = map[k]
|
|
64
|
+
return { appId: ctx.appId, name: ctx.name, files: out }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self.onmessage = async (e) => {
|
|
68
|
+
const { type } = e.data || {}
|
|
69
|
+
try {
|
|
70
|
+
if (type === 'warmup') {
|
|
71
|
+
const t0 = performance.now()
|
|
72
|
+
await ensureToolchain(e.data.toolchainSetupURL)
|
|
73
|
+
self.postMessage({ type: 'ready', ms: Math.round(performance.now() - t0) })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (type === 'compile-subset') {
|
|
77
|
+
const { files, workPath = '/work', stages = ['logic', 'view', 'style'], toolchainSetupURL } = e.data
|
|
78
|
+
await ensureToolchain(toolchainSetupURL)
|
|
79
|
+
const warm = !!toolchainReady
|
|
80
|
+
const t = performance.now()
|
|
81
|
+
const result = await compileSubset(files, workPath, stages)
|
|
82
|
+
self.postMessage({ type: 'done', result, ms: Math.round(performance.now() - t), warm })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
self.postMessage({ type: 'error', error: String((err && err.stack) || err) })
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/toolchain.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Optional helpers for writing the host's `toolchainSetupURL` module (the ESM the pool's
|
|
2
|
+
// stage worker imports to install the two wasm hooks). They package the two fiddly bits
|
|
3
|
+
// a downstream would otherwise hand-write:
|
|
4
|
+
//
|
|
5
|
+
// 1. esbuild-wasm's browser build is usually hosted as a STATIC asset (its Go runtime
|
|
6
|
+
// breaks when bundled). Bundlers (Vite/Webpack/Rollup) refuse to `import()` a file
|
|
7
|
+
// that lives in the static/public dir — so we fetch it and import a Blob URL, which
|
|
8
|
+
// side-steps the bundler module graph. `installEsbuildFromURL` does exactly that.
|
|
9
|
+
// 2. Installing the globals the compiler shims delegate to (`__esbuildTransform`,
|
|
10
|
+
// `__oxcParseSync`).
|
|
11
|
+
//
|
|
12
|
+
// oxc-parser stays a HOST import: only your bundler can resolve `oxc-parser` + fetch its
|
|
13
|
+
// wasm, so you pass the already-imported module to `installOxc`.
|
|
14
|
+
//
|
|
15
|
+
// Typical host toolchain-setup.js (imported by the stage worker at warmup):
|
|
16
|
+
// import { installEsbuildFromURL, installOxc } from '@dimina-kit/compiler/toolchain'
|
|
17
|
+
// installOxc(await import('oxc-parser'))
|
|
18
|
+
// await installEsbuildFromURL('/esbuild-browser.mjs', '/esbuild.wasm')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load esbuild-wasm's browser ESM from a static-asset URL (via a Blob URL, so bundlers
|
|
22
|
+
* don't choke) and install `globalThis.__esbuildTransform`.
|
|
23
|
+
* @param {string} moduleURL URL of esbuild-wasm's browser ESM (e.g. '/esbuild-browser.mjs')
|
|
24
|
+
* @param {string} wasmURL URL of esbuild.wasm (e.g. '/esbuild.wasm')
|
|
25
|
+
* @returns {Promise<any>} the initialized esbuild module
|
|
26
|
+
*/
|
|
27
|
+
export async function installEsbuildFromURL(moduleURL, wasmURL) {
|
|
28
|
+
const code = await fetch(moduleURL).then((r) => {
|
|
29
|
+
if (!r.ok) throw new Error(`[compiler] installEsbuildFromURL: failed to fetch ${moduleURL} (${r.status})`)
|
|
30
|
+
return r.text()
|
|
31
|
+
})
|
|
32
|
+
const blobURL = URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))
|
|
33
|
+
const esbuild = await import(/* @vite-ignore */ blobURL)
|
|
34
|
+
await esbuild.initialize({ wasmURL, worker: true })
|
|
35
|
+
globalThis.__esbuildTransform = (input, options) => esbuild.transform(input, options)
|
|
36
|
+
return esbuild
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Install `globalThis.__oxcParseSync` from an already-imported oxc-parser module.
|
|
41
|
+
* @param {{ parseSync?: Function, default?: { parseSync?: Function } }} oxcModule `await import('oxc-parser')`
|
|
42
|
+
*/
|
|
43
|
+
export function installOxc(oxcModule) {
|
|
44
|
+
const parseSync = oxcModule && (oxcModule.parseSync || (oxcModule.default && oxcModule.default.parseSync))
|
|
45
|
+
if (typeof parseSync !== 'function') {
|
|
46
|
+
throw new Error('[compiler] installOxc: expected an oxc-parser module exposing parseSync (pass `await import(\'oxc-parser\')`)')
|
|
47
|
+
}
|
|
48
|
+
globalThis.__oxcParseSync = parseSync
|
|
49
|
+
}
|