@dimina-kit/compiler 0.0.2-dev.20260711141929 → 0.0.2-dev.20260716163758

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.
@@ -3,12 +3,12 @@ import {
3
3
  preloadStage,
4
4
  resetCompilerState,
5
5
  runStage
6
- } from "./pool.node-chunks/chunk-PDHO4Y56.js";
6
+ } from "./pool.node-chunks/chunk-LVUKEB4F.js";
7
7
  import {
8
8
  getAppId,
9
9
  getAppName,
10
10
  resetStoreInfo
11
- } from "./pool.node-chunks/chunk-7FGOYOXU.js";
11
+ } from "./pool.node-chunks/chunk-QLBB5HQB.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.2-dev.20260711141929",
3
+ "version": "0.0.2-dev.20260716163758",
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",
@@ -82,6 +82,7 @@
82
82
  "test:realm-reuse": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-realm-reuse.js",
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
+ "test:crypto-shim": "node scripts/test-crypto-shim.js",
85
86
  "test:pool-hardening": "node scripts/build-compiler.js node && node scripts/test-pool-hardening.js",
86
87
  "test:pool-toolchain-death": "node scripts/build-compiler.js node && node scripts/test-pool-toolchain-death.js",
87
88
  "test:pool-worker-hardening": "node scripts/test-pool-worker-hardening.js",
@@ -121,6 +121,8 @@ if (MODE === 'node') {
121
121
  'node:url': shim('url.js'),
122
122
  'url': shim('url.js'),
123
123
  'node:worker_threads': shim('worker_threads.js'),
124
+ 'node:crypto': shim('crypto.js'),
125
+ 'crypto': shim('crypto.js'),
124
126
  'node:path': 'path-browserify',
125
127
  'path': 'path-browserify',
126
128
  'node:events': 'events',
@@ -0,0 +1,58 @@
1
+ // The browser build has no node:crypto, so src/shims/crypto.js provides a synchronous
2
+ // SHA-256 to back @dimina/compiler's scope-id `createHash('sha256').update(path)
3
+ // .digest().readBigUInt64BE(0)`. This asserts the shim is byte-identical to node:crypto
4
+ // on every input the compiler can feed it — if they ever diverge, browser-compiled scope
5
+ // ids would silently disagree with node-compiled ones.
6
+ import nodeCrypto from 'node:crypto'
7
+ import { createHash as shimCreateHash } from '../src/shims/crypto.js'
8
+
9
+ const SAMPLES = [
10
+ 'pages/index/index',
11
+ 'components/foo/foo',
12
+ '/components/c16wtkpz/index',
13
+ '/components/c1v8pxjl/index',
14
+ '',
15
+ 'a',
16
+ 'app.json',
17
+ '中文/页面/path',
18
+ 'pages/very/deeply/nested/component/index',
19
+ 'x'.repeat(1),
20
+ 'x'.repeat(55),
21
+ 'x'.repeat(56),
22
+ 'x'.repeat(64),
23
+ 'x'.repeat(120),
24
+ 'x'.repeat(1000),
25
+ // Varied bytes across block boundaries — a constant byte exercises the sha256
26
+ // message schedule (w[t] recurrence) weakly; these would catch a w[] indexing typo
27
+ // that repeated-byte inputs mask. Lengths deliberately straddle 55/56/64/65.
28
+ Array.from({ length: 65 }, (_, i) => String.fromCharCode(33 + (i * 7) % 90)).join(''),
29
+ Array.from({ length: 200 }, (_, i) => `seg${(i * 31) % 97}/`).join(''),
30
+ 'pages/index/index'.split('').reverse().join('') + '/深/x'.repeat(20),
31
+ ]
32
+
33
+ let failed = 0
34
+ const fail = (m) => { failed++; console.error(`❌ ${m}`) }
35
+
36
+ for (const p of SAMPLES) {
37
+ const shimHex = shimCreateHash('sha256').update(p).digest().toString('hex')
38
+ const nodeHex = nodeCrypto.createHash('sha256').update(p).digest('hex')
39
+ const shimId = shimCreateHash('sha256').update(p).digest().readBigUInt64BE(0).toString(36)
40
+ const nodeId = nodeCrypto.createHash('sha256').update(p).digest().readBigUInt64BE(0).toString(36)
41
+ if (shimHex !== nodeHex) fail(`hex mismatch for ${JSON.stringify(p.slice(0, 24))}: shim ${shimHex} != node ${nodeHex}`)
42
+ else if (shimId !== nodeId) fail(`scope-id mismatch for ${JSON.stringify(p.slice(0, 24))}: shim ${shimId} != node ${nodeId}`)
43
+ }
44
+
45
+ // Chained updates must equal a single concatenated update (the compiler only calls
46
+ // update once today, but the shim promises standard incremental semantics).
47
+ const chained = shimCreateHash('sha256').update('pages/').update('index/').update('index').digest().toString('hex')
48
+ const oneShot = nodeCrypto.createHash('sha256').update('pages/index/index').digest('hex')
49
+ if (chained !== oneShot) fail(`chained update() != single update(): ${chained} != ${oneShot}`)
50
+
51
+ // Only sha256 is implemented — anything else must fail loudly, not silently mis-hash.
52
+ try {
53
+ shimCreateHash('md5')
54
+ fail('createHash("md5") should throw (shim implements sha256 only)')
55
+ } catch { /* expected */ }
56
+
57
+ console.log(failed ? `\n❌ FAIL (${failed})` : `\n✅ PASS: crypto shim === node:crypto on ${SAMPLES.length} samples + chained + guard`)
58
+ process.exit(failed ? 1 : 0)
@@ -1,18 +1,19 @@
1
1
  // Content-level scope-hash consistency check for the stage-parallel pool.
2
2
  //
3
3
  // The pool's structural-equivalence tests (test:pool-node) only compare file
4
- // NAME/length multisets, so they cannot see whether the `data-v-XXXXX` scope
5
- // hashes baked into the CSS (`[data-v-X]` selectors) actually match the ones
6
- // baked into the compiled render templates (`.js`, `scopeId: data-v-X`). If the
7
- // view stage and the style stage allocate ids in DIFFERENT realms, their hashes
8
- // diverge and every WXSS rule targets a selector that never appears in the DOM
9
- // styles silently stop working while the file lists still match.
4
+ // NAME/length multisets, so they cannot see whether the `data-v-<id>` scope hashes
5
+ // baked into the CSS (`[data-v-<id>]` selectors) actually match the ones baked into
6
+ // the compiled render templates (`id:"<id>"`). Scope ids are a deterministic
7
+ // hash(path) (dimina utils.js), so stages compiling in separate realms derive
8
+ // identical ids; were the id ever per-realm state (a random uuid), the view and style
9
+ // stages would diverge and every WXSS rule would target a selector that never renders.
10
10
  //
11
- // This script models the browser pool (src/pool.js + src/stage-worker.js) in
12
- // Node: each stage compiles in its OWN fresh memfs, exactly as a separate Web
13
- // Worker realm would. It runs the OLD per-worker-setup path and the FIXED
14
- // shared-setup path and asserts, at the content level, that every scope hash
15
- // used in CSS also appears in the render output.
11
+ // This script models the browser pool (src/pool.js + src/stage-worker.js) in Node:
12
+ // each stage compiles in its OWN fresh memfs, exactly as a separate Web Worker realm
13
+ // would. It asserts, at the content level, that every scope hash used in CSS also
14
+ // appears in the render output for per-stage independent setup (MODEL A, which
15
+ // deterministic ids keep consistent with no broadcast), the shared-setup broadcast
16
+ // de-dup path (MODEL B), and the real pool orchestration (MODEL C).
16
17
  import { readdirSync, readFileSync, statSync } from 'node:fs'
17
18
  import { fileURLToPath } from 'node:url'
18
19
  import path from 'node:path'
@@ -58,10 +59,10 @@ const STAGES = ['logic', 'view', 'style']
58
59
  const freshFs = () => createFsFromVolume(Volume.fromJSON(seed, workPath))
59
60
  const clean = (m) => { for (const k of Object.keys(m)) if (m[k] == null) delete m[k]; return m }
60
61
 
61
- // CSS carries the scope hash as `[data-v-XXXXX]`; the compiled render carries
62
- // the SAME 5-char id bare in each `Module({ …, id:"XXXXX", … })` (the runtime
63
- // prepends `data-v-`). Extract each with its own pattern so we compare the
64
- // cross-file linkage the runtime actually relies on.
62
+ // CSS carries the scope hash as `[data-v-<id>]`; the compiled render carries the SAME
63
+ // id bare in each `Module({ …, id:"<id>", … })` (the runtime prepends `data-v-`).
64
+ // Extract each with its own pattern so we compare the cross-file linkage the runtime
65
+ // actually relies on.
65
66
  function hashesIn(files, pred, re) {
66
67
  const set = new Set()
67
68
  for (const k of Object.keys(files)) {
@@ -74,8 +75,11 @@ function hashesIn(files, pred, re) {
74
75
  }
75
76
  return set
76
77
  }
77
- const CSS_RE = /data-v-([a-z0-9]{5})/g
78
- const RENDER_RE = /\bid:\s*["']([a-z0-9]{5})["']/g
78
+ // `{5,}` (not a fixed width): the scope id is a base36 hash(path) (~13 chars today).
79
+ // Keeping an open lower bound of 5 still matches a regressed random 5-char id, so a
80
+ // revert to per-realm random ids makes MODEL A orphan again instead of matching nothing.
81
+ const CSS_RE = /data-v-([a-z0-9]{5,})/g
82
+ const RENDER_RE = /\bid:\s*["']([a-z0-9]{5,})["']/g
79
83
  const isCss = (k) => k.endsWith('.css')
80
84
  const isRender = (k) => k.endsWith('.js') && k !== 'main/logic.js' && !k.endsWith('/logic.js')
81
85
 
@@ -93,7 +97,9 @@ function report(label, files) {
93
97
  return { css, js, orphanCss, matched }
94
98
  }
95
99
 
96
- // --- MODEL A: OLD browser pool — each stage runs setupCompile in its own realm ---
100
+ // --- MODEL A: each stage runs setupCompile in its OWN realm, no broadcast ---
101
+ // The direct regression guard: deterministic hash(path) ids must keep independent
102
+ // realms consistent. A revert to per-realm random ids surfaces here as orphan CSS.
97
103
  async function buildPerStageSetup() {
98
104
  const merged = {}
99
105
  for (const stage of STAGES) {
@@ -106,11 +112,11 @@ async function buildPerStageSetup() {
106
112
  return merged
107
113
  }
108
114
 
109
- // --- MODEL B: FIXED pool — setup ONCE, broadcast the bundle across stage realms ---
110
- // This models the fixed browser pool exactly (mirrors the working Node disk pool):
111
- // one realm runs setupCompile (id allocation + npm/app-config scaffold); the shared
112
- // `{ pages, storeInfo }` bundle is broadcast to every stage worker, which only runs
113
- // compileStage against it. Setup scaffold (app-config.json + npm) is merged in.
115
+ // --- MODEL B: setup ONCE, broadcast the bundle across stage realms (de-dup path) ---
116
+ // Models the pool's broadcast path (mirrors the Node disk pool): one realm runs
117
+ // setupCompile (npm/app-config scaffold + { pages, storeInfo }); that bundle is
118
+ // broadcast to every stage worker, which only runs compileStage against it — so the
119
+ // heavy setup runs once. Setup scaffold (app-config.json + npm) is merged in.
114
120
  async function buildSharedSetup() {
115
121
  realm()
116
122
  const setupFs = freshFs()
@@ -186,10 +192,10 @@ const inline = clean((await compileMiniApp({ fs: freshFs(), workPath })).files)
186
192
  const gt = report('inline (single realm, ground truth)', inline)
187
193
 
188
194
  const modelA = await buildPerStageSetup()
189
- const rA = report('MODEL A — per-stage independent setup (current browser pool)', modelA)
195
+ const rA = report('MODEL A — per-stage independent setup (no broadcast)', modelA)
190
196
 
191
197
  const modelB = await buildSharedSetup()
192
- const rB = report('MODEL B — shared setup bundle (fix)', modelB)
198
+ const rB = report('MODEL B — shared setup broadcast (de-dup path)', modelB)
193
199
 
194
200
  const poolRes = await buildViaPool()
195
201
  const modelC = clean(poolRes.files)
@@ -203,25 +209,26 @@ const pass = (m) => console.log(`✅ ${m}`)
203
209
  if (gt.orphanCss.length) fail(`ground truth has ${gt.orphanCss.length} orphan CSS hashes — test harness bug`)
204
210
  else pass('ground truth: every CSS scope hash appears in the render output')
205
211
 
206
- // Model A is EXPECTED to be broken assert it reproduces the bug.
207
- if (rA.orphanCss.length > 0 && rA.matched.length === 0) {
208
- pass(`REPRODUCED: per-stage setup yields ${rA.orphanCss.length} orphan CSS hashes, 0 matched WXSS fully broken`)
209
- } else if (rA.orphanCss.length > 0) {
210
- pass(`REPRODUCED (partial): per-stage setup yields ${rA.orphanCss.length} orphan CSS hashes`)
212
+ // With deterministic hash(path) scope ids, per-stage INDEPENDENT setup (MODEL A) must
213
+ // stay scope-consistent too: separate realms derive the SAME id from the same path with
214
+ // no broadcast. This is the guarantee stable ids buy, and the direct regression guard
215
+ // a revert to per-realm random ids would make MODEL A orphan again.
216
+ if (rA.orphanCss.length === 0 && rA.css.size === gt.css.size) {
217
+ pass(`STABLE ID: per-stage independent setup yields 0 orphan CSS hashes, ${rA.matched.length} matched (== ground truth ${gt.css.size}) — deterministic hash(path) keeps realms consistent without a broadcast`)
211
218
  } else {
212
- fail('per-stage setup did NOT reproduce the mismatch (bug model invalid)')
219
+ fail(`per-stage independent setup has ${rA.orphanCss.length} orphan CSS hashes (css=${rA.css.size} gt=${gt.css.size}) — scope id is not a deterministic function of path across realms`)
213
220
  }
214
221
 
215
222
  // Model B (fix) must be fully consistent AND structurally complete.
216
223
  if (rB.orphanCss.length === 0 && rB.css.size === gt.css.size) {
217
- pass(`FIX VERIFIED: shared setup yields 0 orphan CSS hashes, ${rB.matched.length} matched (== ground truth ${gt.css.size})`)
224
+ pass(`BROADCAST PATH: shared setup yields 0 orphan CSS hashes, ${rB.matched.length} matched (== ground truth ${gt.css.size})`)
218
225
  } else {
219
226
  fail(`shared setup still has ${rB.orphanCss.length} orphan CSS hashes (css=${rB.css.size} gt=${gt.css.size})`)
220
227
  }
221
228
  // Both models must emit the SAME file set as the single-realm ground truth — this is
222
229
  // what the structural test already checks, and why it stays green while WXSS is broken.
223
230
  if (fileSet(modelA) !== fileSet(inline)) fail('MODEL A file set differs from ground truth (unexpected)')
224
- else pass('MODEL A file set == ground truth (this is exactly why the name/length structural test passes while WXSS is broken)')
231
+ else pass('MODEL A file set == ground truth (the name/length structural test can only see this, not the scope-hash linkage this content check verifies)')
225
232
  if (fileSet(modelB) !== fileSet(inline)) fail('MODEL B (fix) file set differs from ground truth')
226
233
  else pass('MODEL B (fix) file set == ground truth')
227
234
 
package/src/pool.js CHANGED
@@ -162,14 +162,15 @@ export function createCompilerPool(options = {}) {
162
162
  async function runAttempt(files, workPath, options) {
163
163
  await settleAll(workers.map(ensureWarm))
164
164
 
165
- // Setup step — one worker runs setup ONCE: it allocates the scope-hash ids
166
- // (page + component data-v-XXXXX) and builds miniprogram_npm/app-config.json.
167
- // Broadcasting this single bundle to every stage is REQUIRED for correctness:
168
- // each stage runs in its own realm, and if each ran its own setup it would roll
169
- // independent random uuids, so the CSS `[data-v-X]` selectors would never match
170
- // the render `Module id` and every WXSS rule would target nothing (regression
171
- // guarded by scripts/test-pool-scopehash.js). This mirrors the Node disk pool,
172
- // which likewise sets up once and fans the same { pages, storeInfo } out.
165
+ // Setup step — one worker runs setup ONCE: it parses config, builds
166
+ // miniprogram_npm + app-config.json (the npm build can invoke the wasm toolchain)
167
+ // and produces the scaffold. The resulting { pages, storeInfo } bundle is
168
+ // broadcast to every stage so this heavy work runs once instead of per stage.
169
+ // Scope ids are a deterministic hash(path) (dimina utils.js), so each stage would
170
+ // derive identical `data-v-<id>` even from its own setup the broadcast is a
171
+ // de-dup optimization, NOT a scope-correctness requirement (scripts/test-pool-scopehash.js
172
+ // asserts per-stage independent setup stays scope-consistent). Mirrors the Node
173
+ // disk pool, which likewise sets up once and fans the same { pages, storeInfo } out.
173
174
  // `options` (e.g. { fileTypes }) only needs to reach THIS setup call: dmcc's
174
175
  // storeInfo() bakes the normalized dialect into the returned storeInfo object
175
176
  // (dimina/fe/packages/compiler/src/env.js:106-120), which rides inside `bundle`
@@ -0,0 +1,101 @@
1
+ // node:crypto shim for the browser build. @dimina/compiler derives its CSS scope id
2
+ // via `createHash('sha256').update(path).digest().readBigUInt64BE(0)` (utils.js). The
3
+ // browser has no node:crypto, and Web Crypto's `subtle.digest` is async — it cannot
4
+ // back a synchronous `uuid()`. So this provides a self-contained synchronous SHA-256.
5
+ // Only the sha256 + digest()->Buffer path the compiler actually uses is implemented.
6
+ import { Buffer } from 'buffer'
7
+
8
+ const K = new Uint32Array([
9
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
10
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
11
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
12
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
13
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
14
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
15
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
16
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
17
+ ])
18
+
19
+ function rotr(x, n) {
20
+ return (x >>> n) | (x << (32 - n))
21
+ }
22
+
23
+ // SHA-256 over a Uint8Array, returning the 32-byte digest as a Uint8Array.
24
+ function sha256(msg) {
25
+ let h0 = 0x6A09E667, h1 = 0xBB67AE85, h2 = 0x3C6EF372, h3 = 0xA54FF53A
26
+ let h4 = 0x510E527F, h5 = 0x9B05688C, h6 = 0x1F83D9AB, h7 = 0x5BE0CD19
27
+
28
+ const l = msg.length
29
+ const bitLen = l * 8
30
+ // pad: 0x80, then zeros so length ≡ 56 (mod 64), then 64-bit big-endian bit length.
31
+ const total = (((l + 8) >> 6) + 1) << 6
32
+ const buf = new Uint8Array(total)
33
+ buf.set(msg)
34
+ buf[l] = 0x80
35
+ const dv = new DataView(buf.buffer)
36
+ dv.setUint32(total - 8, Math.floor(bitLen / 0x100000000), false)
37
+ dv.setUint32(total - 4, bitLen >>> 0, false)
38
+
39
+ const w = new Uint32Array(64)
40
+ for (let i = 0; i < total; i += 64) {
41
+ for (let t = 0; t < 16; t++) w[t] = dv.getUint32(i + t * 4, false)
42
+ for (let t = 16; t < 64; t++) {
43
+ const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ (w[t - 15] >>> 3)
44
+ const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ (w[t - 2] >>> 10)
45
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
46
+ }
47
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7
48
+ for (let t = 0; t < 64; t++) {
49
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
50
+ const ch = (e & f) ^ (~e & g)
51
+ const t1 = (h + S1 + ch + K[t] + w[t]) >>> 0
52
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
53
+ const maj = (a & b) ^ (a & c) ^ (b & c)
54
+ const t2 = (S0 + maj) >>> 0
55
+ h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0
56
+ }
57
+ h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0
58
+ h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0
59
+ }
60
+
61
+ const out = new Uint8Array(32)
62
+ const odv = new DataView(out.buffer)
63
+ const hs = [h0, h1, h2, h3, h4, h5, h6, h7]
64
+ for (let i = 0; i < 8; i++) odv.setUint32(i * 4, hs[i], false)
65
+ return out
66
+ }
67
+
68
+ class Hash {
69
+ constructor() {
70
+ this._chunks = []
71
+ }
72
+
73
+ // Accepts a string (UTF-8 encoded, matching node's default) or raw bytes
74
+ // (Uint8Array/Buffer, copied). No inputEncoding form (`update(x, 'hex')`) — the
75
+ // compiler only ever hashes a path string, and silently accepting an encoding we
76
+ // ignore would diverge from node:crypto.
77
+ update(data) {
78
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data)
79
+ this._chunks.push(bytes)
80
+ return this
81
+ }
82
+
83
+ digest() {
84
+ let len = 0
85
+ for (const c of this._chunks) len += c.length
86
+ const all = new Uint8Array(len)
87
+ let off = 0
88
+ for (const c of this._chunks) { all.set(c, off); off += c.length }
89
+ // Return a Buffer so callers can use Buffer methods (readBigUInt64BE) on the digest.
90
+ return Buffer.from(sha256(all))
91
+ }
92
+ }
93
+
94
+ export function createHash(algorithm) {
95
+ if (algorithm !== 'sha256') {
96
+ throw new Error(`[compiler] browser crypto shim only implements sha256 (got '${algorithm}')`)
97
+ }
98
+ return new Hash()
99
+ }
100
+
101
+ export default { createHash }
@@ -58,13 +58,13 @@ function freshFs(files, workPath) {
58
58
  return createFsFromVolume(Volume.fromJSON(files, workPath))
59
59
  }
60
60
 
61
- // Run setupCompile ONCE for a compile: parse config, allocate the scope-hash ids
62
- // (page + component `data-v-XXXXX`), scaffold app-config.json + miniprogram_npm.
63
- // Returns the SERIALIZABLE id bundle every stage worker must share, plus the
64
- // non-stage scaffold files. Sharing this one bundle across the per-stage realms is
65
- // what keeps the CSS `[data-v-X]` selectors and the render `Module id` in agreement:
66
- // if each stage ran its own setupCompile it would roll its own random uuids and every
67
- // WXSS rule would target a selector that never renders (see scripts/test-pool-scopehash.js).
61
+ // Run setupCompile ONCE for a compile: parse config, build the scaffold
62
+ // (app-config.json + miniprogram_npm), and collect the { pages, storeInfo } bundle.
63
+ // Sharing this one bundle across the per-stage realms lets the heavy setup work (npm
64
+ // build, config parse) run once instead of per stage. Scope ids are a deterministic
65
+ // hash(path) (dimina utils.js), so the CSS `[data-v-<id>]` selectors and the render
66
+ // `Module id` agree across stages no matter who runs setup the shared bundle is a
67
+ // de-dup optimization, not a scope-correctness requirement (see scripts/test-pool-scopehash.js).
68
68
  async function runSetup(files, workPath, options) {
69
69
  const fs = freshFs(files, workPath)
70
70
  resetCompilerState()
@@ -87,11 +87,12 @@ async function runSetup(files, workPath, options) {
87
87
  // stays correct across compiles. Stages write disjoint products; we return this
88
88
  // worker's subset and the pool unions them.
89
89
  //
90
- // With a `bundle` (from runSetup), the stage reuses the coordinator's shared ids
91
- // (mirroring the Node disk pool, which broadcasts the same { pages, storeInfo } to
92
- // every stage worker) instead of running its own setupCompile the fix for the
93
- // cross-stage scope-hash mismatch. Stages read source from `workPath` and write
94
- // disjoint products; they never read the setup scaffold, so it is not seeded here.
90
+ // With a `bundle` (from runSetup), the stage reuses the coordinator's { pages, storeInfo }
91
+ // instead of re-running setupCompile so the npm build / config parse happens once, not
92
+ // per stage (mirroring the Node disk pool). Scope ids are a deterministic hash(path), so
93
+ // reusing the bundle vs re-deriving would yield the same `data-v-<id>` either way. Stages
94
+ // read source from `workPath` and write disjoint products; they never read the setup
95
+ // scaffold, so it is not seeded here.
95
96
  // Without a bundle the worker stays self-contained (single-worker / legacy callers).
96
97
  // `options` (e.g. { fileTypes }) is only used on the no-bundle fallback path: with a
97
98
  // bundle, its storeInfo already carries the normalized dialect from the coordinator's