@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +363 -0
  3. package/dist/compile-core.browser.js +281945 -0
  4. package/dist/compile-core.node.js +3702 -0
  5. package/dist/pool.browser.js +99 -0
  6. package/dist/pool.node.js +3743 -0
  7. package/dist/stage-worker.browser.js +291085 -0
  8. package/dist/stage-worker.node.js +3097 -0
  9. package/dist/toolchain.browser.js +30 -0
  10. package/package.json +87 -0
  11. package/scripts/build-compiler.js +207 -0
  12. package/scripts/gen-bench-fixture.js +24 -0
  13. package/scripts/kit-resolve-hook.js +35 -0
  14. package/scripts/register-kit.js +2 -0
  15. package/scripts/test-appid-fallback.js +114 -0
  16. package/scripts/test-decompose.js +90 -0
  17. package/scripts/test-hardening.js +78 -0
  18. package/scripts/test-node.js +69 -0
  19. package/scripts/test-npm-scan.js +164 -0
  20. package/scripts/test-pool-hardening.js +65 -0
  21. package/scripts/test-pool-node.js +117 -0
  22. package/scripts/test-realm-reuse.js +77 -0
  23. package/src/browser-entry.js +25 -0
  24. package/src/compile-core.js +428 -0
  25. package/src/pool-node.js +170 -0
  26. package/src/pool.js +122 -0
  27. package/src/shims/esbuild-wasm.js +19 -0
  28. package/src/shims/fs-promises.js +20 -0
  29. package/src/shims/fs.js +59 -0
  30. package/src/shims/less.js +9 -0
  31. package/src/shims/os.js +11 -0
  32. package/src/shims/oxc-parser.js +21 -0
  33. package/src/shims/oxc-walker.js +29 -0
  34. package/src/shims/postcss-noop-plugin.js +9 -0
  35. package/src/shims/process.js +20 -0
  36. package/src/shims/url.js +4 -0
  37. package/src/shims/worker_threads.js +10 -0
  38. package/src/stage-worker-node.js +41 -0
  39. package/src/stage-worker.js +88 -0
  40. 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)
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ // minimal node:url shim
2
+ export const fileURLToPath = (u) => String(u).replace(/^file:\/\//, '')
3
+ export const pathToFileURL = (p) => ({ href: `file://${p}`, toString: () => `file://${p}` })
4
+ export default { fileURLToPath, pathToFileURL }
@@ -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
+ }
@@ -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
+ }