@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
@@ -0,0 +1,30 @@
1
+ globalThis.global ||= globalThis;
2
+ globalThis.process ||= { env: {}, cwd: () => "/" };
3
+ globalThis.process.cwd ||= () => "/";
4
+
5
+ // src/toolchain.js
6
+ async function installEsbuildFromURL(moduleURL, wasmURL) {
7
+ const code = await fetch(moduleURL).then((r) => {
8
+ if (!r.ok) throw new Error(`[compiler] installEsbuildFromURL: failed to fetch ${moduleURL} (${r.status})`);
9
+ return r.text();
10
+ });
11
+ const blobURL = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
12
+ const esbuild = await import(
13
+ /* @vite-ignore */
14
+ blobURL
15
+ );
16
+ await esbuild.initialize({ wasmURL, worker: true });
17
+ globalThis.__esbuildTransform = (input, options) => esbuild.transform(input, options);
18
+ return esbuild;
19
+ }
20
+ function installOxc(oxcModule) {
21
+ const parseSync = oxcModule && (oxcModule.parseSync || oxcModule.default && oxcModule.default.parseSync);
22
+ if (typeof parseSync !== "function") {
23
+ throw new Error("[compiler] installOxc: expected an oxc-parser module exposing parseSync (pass `await import('oxc-parser')`)");
24
+ }
25
+ globalThis.__oxcParseSync = parseSync;
26
+ }
27
+ export {
28
+ installEsbuildFromURL,
29
+ installOxc
30
+ };
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@dimina-kit/compiler",
3
+ "version": "0.0.1-dev.20260702173719",
4
+ "description": "dmcc compiler bundles (browser + node) that drive @dimina/compiler against a caller-injected node:fs replacement (no bundled fs)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/EchoTechFE/dimina-kit.git",
10
+ "directory": "packages/compiler"
11
+ },
12
+ "homepage": "https://github.com/EchoTechFE/dimina-kit/tree/main/packages/compiler#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/EchoTechFE/dimina-kit/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "main": "./dist/compile-core.node.js",
20
+ "exports": {
21
+ ".": "./dist/compile-core.node.js",
22
+ "./browser": "./dist/compile-core.browser.js",
23
+ "./pool-node": "./dist/pool.node.js",
24
+ "./pool": "./dist/pool.browser.js",
25
+ "./stage-worker": "./dist/stage-worker.browser.js",
26
+ "./toolchain": "./dist/toolchain.browser.js",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "scripts"
33
+ ],
34
+ "dependencies": {
35
+ "@babel/parser": "^7.29.7",
36
+ "@vue/compiler-sfc": "^3.5.39",
37
+ "assert": "^2.1.0",
38
+ "autoprefixer": "^10.5.2",
39
+ "buffer": "^6.0.3",
40
+ "cheerio": "^1.2.0",
41
+ "cssnano": "^8.0.2",
42
+ "esbuild": "^0.28.1",
43
+ "estree-walker": "^3.0.3",
44
+ "events": "^3.3.0",
45
+ "htmlparser2": "^12.0.0",
46
+ "less": "^4.6.7",
47
+ "magic-string": "^0.30.21",
48
+ "memfs": "^4.57.8",
49
+ "oxc-parser": "^0.137.0",
50
+ "oxc-walker": "^1.0.0",
51
+ "path-browserify": "^1.0.1",
52
+ "postcss": "^8.5.15",
53
+ "postcss-selector-parser": "^7.1.4",
54
+ "sass": "^1.101.0",
55
+ "source-map-js": "^1.2.1",
56
+ "stream-browserify": "^3.0.0",
57
+ "util": "^0.12.5"
58
+ },
59
+ "peerDependencies": {
60
+ "@oxc-parser/binding-wasm32-wasi": "^0.137.0",
61
+ "esbuild-wasm": "^0.28.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@oxc-parser/binding-wasm32-wasi": {
65
+ "optional": true
66
+ },
67
+ "esbuild-wasm": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "esbuild-wasm": "^0.28.0"
73
+ },
74
+ "scripts": {
75
+ "build:node": "node scripts/build-compiler.js node",
76
+ "build:browser": "node scripts/build-compiler.js browser",
77
+ "build": "pnpm run build:node && pnpm run build:browser",
78
+ "test:node": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-node.js",
79
+ "test:appid": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-appid-fallback.js",
80
+ "test:hardening": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-hardening.js",
81
+ "test:decompose": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-decompose.js",
82
+ "test:realm-reuse": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-realm-reuse.js",
83
+ "test:pool-node": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-pool-node.js",
84
+ "test:pool-hardening": "node scripts/build-compiler.js node && node scripts/test-pool-hardening.js",
85
+ "test:npm-scan": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-npm-scan.js"
86
+ }
87
+ }
@@ -0,0 +1,207 @@
1
+ import esbuild from 'esbuild'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { createRequire } from 'node:module'
5
+ import path from 'node:path'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const root = path.resolve(__dirname, '..')
9
+ const shim = (f) => path.join(root, 'src/shims', f)
10
+
11
+ // The dart-sass package lives in dimina-kit's `dimina` submodule fe workspace.
12
+ // Resolve it from there (instead of a machine-specific absolute path) and force
13
+ // its pure-JS browser entry (`sass.default.js`) — NOT the node launcher.
14
+ const kitFeRequire = createRequire(path.resolve(root, '../../dimina/fe/package.json'))
15
+ const sassBrowserEntry = path.join(path.dirname(kitFeRequire.resolve('sass')), 'sass.default.js')
16
+
17
+ // Pin the browser CSS pipeline to the SAME autoprefixer the node build / real dmcc
18
+ // resolve at runtime. esbuild's bundler resolution would otherwise pick a different
19
+ // autoprefixer island (10.5.2, from the dimina fe pnpm store) than node require does
20
+ // (10.5.0, worktree-root store); the 10.5.2 one adds stray -ms- prefixes for ie 11,
21
+ // diverging from dmcc. Anchor the resolve at the node bundle location so browser and
22
+ // node reference use byte-identical autoprefixer + its browserslist/caniuse island.
23
+ const nodeRuntimeRequire = createRequire(path.join(root, 'dist/compile-core.node.js'))
24
+ const autoprefixerEntry = nodeRuntimeRequire.resolve('autoprefixer')
25
+
26
+ // mode "node" -> Layer1: validate orchestration in Node, native esbuild/oxc kept external
27
+ // mode "browser" -> bundle everything for the browser (pure-JS + wasm toolchain)
28
+ // Passed as argv (cross-platform; `MODE=x node ...` breaks on Windows cmd), env MODE kept as fallback.
29
+ const MODE = process.argv[2] || process.env.MODE || 'node'
30
+ const USE_WASM = process.env.USE_WASM === '1' || MODE === 'browser'
31
+
32
+ // Append exports for functions/reset-hooks the compiler defines but does not
33
+ // export, without touching the submodule source on disk. The reset hooks clear
34
+ // the compiler's module-level caches so a pooled worker realm can compile more
35
+ // than once without cross-compile contamination (see resetCompilerState).
36
+ const exportAppend = {
37
+ name: 'export-append',
38
+ setup(build) {
39
+ const appends = {
40
+ 'logic-compiler.js': '\nexport { writeCompileRes }\nexport function __resetLogicState() { processedModules.clear() }\nexport function __setEnableSourcemap(v) { enableSourcemap = !!v }\n',
41
+ 'style-compiler.js': '\nexport function __resetStyleState() { compileRes.clear() }\n',
42
+ 'view-compiler.js': '\nexport function __resetViewState() { compileResCache.clear(); wxsModuleRegistry.clear(); wxsFilePathMap.clear() }\n',
43
+ 'utils.js': '\nexport function __resetAssets() { for (const k of Object.keys(assetsMap)) delete assetsMap[k] }\n',
44
+ }
45
+ build.onLoad({ filter: /(core[\\/](logic|style|view)-compiler|common[\\/]utils)\.js$/ }, async (args) => {
46
+ const base = path.basename(args.path)
47
+ const src = await readFile(args.path, 'utf8')
48
+ return { contents: src + (appends[base] || ''), loader: 'js' }
49
+ })
50
+ },
51
+ }
52
+
53
+ // In node mode, keep heavy/native deps external (resolved at runtime via NODE_PATH).
54
+ // Isolate the oxc swap: in node mode, optionally replace native oxc with wasm
55
+ // while keeping native esbuild (so any failure is attributable to oxc only).
56
+ const USE_OXC_WASM = process.env.USE_OXC_WASM === '1'
57
+
58
+ // Keep the CSS pipeline external in the node build: inlining browserslist entangles
59
+ // its config lookup with the compiler's INJECTED fs (browserslist would walk the
60
+ // memfs project tree instead of the real disk). External keeps browserslist on real
61
+ // node fs. NOTE: this makes the node build resolve autoprefixer from the worktree
62
+ // root store; to compare against true dmcc, generate the reference with
63
+ // NODE_PATH pointed at dimina/fe/node_modules (dmcc's own 10.5.2). See dump-node-ref.
64
+ const NODE_EXTERNAL = [
65
+ 'esbuild', 'sass', 'less', 'postcss',
66
+ 'autoprefixer', 'cssnano', 'cheerio', 'htmlparser2', '@vue/compiler-sfc',
67
+ 'magic-string', 'source-map-js', 'postcss-selector-parser',
68
+ ...(USE_OXC_WASM ? ['@oxc-parser/wasm'] : ['oxc-parser', 'oxc-walker']),
69
+ ]
70
+
71
+ const common = {
72
+ entryPoints: [path.join(root, 'src/compile-core.js')],
73
+ bundle: true,
74
+ format: 'esm',
75
+ target: ['es2022'],
76
+ plugins: [exportAppend],
77
+ logLevel: 'info',
78
+ }
79
+
80
+ const oxcWasmAlias = USE_OXC_WASM
81
+ ? { 'oxc-parser': shim('oxc-parser.js'), 'oxc-walker': shim('oxc-walker.js') }
82
+ : {}
83
+
84
+ // compile-core.node.js: the injectable in-memory seam — fs is SHIMMED so the caller
85
+ // passes a node:fs replacement (memfs). worker_threads is shimmed (isMainThread=true)
86
+ // to skip dmcc's parentPort bootstrap since we drive the exports inline.
87
+ const nodeShimAlias = {
88
+ 'node:fs': shim('fs.js'),
89
+ 'fs': shim('fs.js'),
90
+ 'node:worker_threads': shim('worker_threads.js'),
91
+ ...oxcWasmAlias,
92
+ }
93
+
94
+ // pool.node.js / stage-worker.node.js: the resident Node disk pool. fs is NATIVE
95
+ // (dmcc writes real disk staging, then publishToDist copies to outputDir) — do NOT
96
+ // alias it. worker_threads is STILL shimmed so dmcc's own `if(!isMainThread)` parentPort
97
+ // bootstrap stays OFF (otherwise its handler would race ours on the same port); the pool
98
+ // and stage worker reach the REAL Worker/parentPort via createRequire (bypasses this alias).
99
+ const nodeNativeAlias = {
100
+ 'node:worker_threads': shim('worker_threads.js'),
101
+ ...oxcWasmAlias,
102
+ }
103
+
104
+ let opts
105
+ if (MODE === 'node') {
106
+ opts = {
107
+ ...common,
108
+ platform: 'node',
109
+ external: NODE_EXTERNAL,
110
+ }
111
+ } else {
112
+ const alias = {
113
+ 'node:fs': shim('fs.js'),
114
+ 'fs': shim('fs.js'),
115
+ 'node:fs/promises': shim('fs-promises.js'),
116
+ 'fs/promises': shim('fs-promises.js'),
117
+ 'node:os': shim('os.js'),
118
+ 'os': shim('os.js'),
119
+ 'node:process': shim('process.js'),
120
+ 'process': shim('process.js'),
121
+ 'node:url': shim('url.js'),
122
+ 'url': shim('url.js'),
123
+ 'node:worker_threads': shim('worker_threads.js'),
124
+ 'node:path': 'path-browserify',
125
+ 'path': 'path-browserify',
126
+ 'node:events': 'events',
127
+ 'node:buffer': 'buffer',
128
+ 'node:stream': 'stream-browserify',
129
+ 'stream': 'stream-browserify',
130
+ 'node:util': 'util',
131
+ 'node:assert': 'assert',
132
+ 'less': shim('less.js'),
133
+ // force the pure-JS (browser) sass entry, not the node launcher
134
+ 'sass': sassBrowserEntry,
135
+ // force esbuild-wasm's browser ESM build (not the node build)
136
+ 'esbuild-wasm': path.join(root, 'node_modules/esbuild-wasm/esm/browser.js'),
137
+ }
138
+ // CSS pipeline: by default bundle the REAL autoprefixer + cssnano (same versions
139
+ // as the node/dmcc build) so the browser CSS output is byte-identical to dmcc.
140
+ // They pull browserslist + caniuse-lite, which only need process.env (already in
141
+ // the banner) since the compiler passes overrideBrowserslist and skips config
142
+ // lookup. Set REAL_CSS=0 to fall back to the old no-op shims (CSS left un-minified).
143
+ if (process.env.REAL_CSS === '0') {
144
+ alias['autoprefixer'] = shim('postcss-noop-plugin.js')
145
+ alias['cssnano'] = shim('postcss-noop-plugin.js')
146
+ } else {
147
+ // pin autoprefixer to the node/dmcc-resolved copy (see above)
148
+ alias['autoprefixer'] = autoprefixerEntry
149
+ }
150
+ if (USE_WASM) {
151
+ alias['esbuild'] = shim('esbuild-wasm.js')
152
+ alias['oxc-parser'] = shim('oxc-parser.js')
153
+ alias['oxc-walker'] = shim('oxc-walker.js')
154
+ }
155
+ opts = {
156
+ ...common,
157
+ platform: 'browser',
158
+ format: 'esm',
159
+ alias,
160
+ define: {
161
+ 'process.env.NODE_ENV': '"production"',
162
+ // some postcss plugins reference __filename/__dirname for source locations;
163
+ // esbuild's browser platform leaves them undefined, so provide stable stubs.
164
+ '__filename': '"/index.js"',
165
+ '__dirname': '"/"',
166
+ },
167
+ // Tiny process shim — env + cwd only. NO process.versions.node, so dart-sass,
168
+ // esbuild-wasm and the Go wasm runtime still detect a browser env; cwd is needed
169
+ // by browserslist (real autoprefixer/cssnano) and is safe to expose.
170
+ banner: {
171
+ js: [
172
+ 'globalThis.global ||= globalThis;',
173
+ 'globalThis.process ||= { env: {}, cwd: () => "/" };',
174
+ 'globalThis.process.cwd ||= () => "/";',
175
+ ].join('\n'),
176
+ },
177
+ }
178
+ }
179
+
180
+ // Browser mode ships three bundles from the same config: the core seams
181
+ // (compile-core.browser.js), the package's resident stage worker
182
+ // (stage-worker.browser.js, bundles the compiler + memfs), and the light-weight
183
+ // orchestrated pool (pool.browser.js). Node mode ships only the core.
184
+ const outputs = MODE === 'node'
185
+ ? [
186
+ { in: 'src/compile-core.js', out: 'dist/compile-core.node.js', alias: nodeShimAlias },
187
+ // Resident Node worker_threads disk pool (real fs, dmcc-parity output + sourcemap).
188
+ { in: 'src/pool-node.js', out: 'dist/pool.node.js', alias: nodeNativeAlias },
189
+ { in: 'src/stage-worker-node.js', out: 'dist/stage-worker.node.js', alias: nodeNativeAlias },
190
+ ]
191
+ : [
192
+ { in: 'src/browser-entry.js', out: 'dist/compile-core.browser.js' },
193
+ { in: 'src/stage-worker.js', out: 'dist/stage-worker.browser.js' },
194
+ { in: 'src/pool.js', out: 'dist/pool.browser.js' },
195
+ { in: 'src/toolchain.js', out: 'dist/toolchain.browser.js' },
196
+ ]
197
+
198
+ for (const o of outputs) {
199
+ const built = {
200
+ ...opts,
201
+ entryPoints: [path.join(root, o.in)],
202
+ outfile: path.join(root, o.out),
203
+ ...(o.alias ? { alias: o.alias } : {}),
204
+ }
205
+ await esbuild.build(built)
206
+ console.log(`✅ built MODE=${MODE} USE_WASM=${USE_WASM ? 1 : 0} -> ${o.out}`)
207
+ }
@@ -0,0 +1,24 @@
1
+ // Emit an example's text source as a flat {relPath: content} JSON so the browser
2
+ // benchmark can seed each fs backend without a bundler.
3
+ // usage: node gen-bench-fixture.js <dest.json> [example=base]
4
+ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
5
+ import { fileURLToPath } from 'node:url'
6
+ import path from 'node:path'
7
+
8
+ const EXAMPLE = process.argv[3] || 'base'
9
+ const APP = fileURLToPath(new URL(`../../../dimina/fe/example/${EXAMPLE}`, import.meta.url))
10
+ const TEXT = new Set(['.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', '.scss', '.sass', '.wxs', '.dds', '.css'])
11
+ const out = {}
12
+ ;(function rd(d, b) {
13
+ for (const n of readdirSync(d)) {
14
+ if (n === 'node_modules' || n === '.git') continue
15
+ const f = path.join(d, n)
16
+ if (statSync(f).isDirectory()) rd(f, b)
17
+ else if (TEXT.has(path.extname(n).toLowerCase())) out[path.relative(b, f).split(path.sep).join('/')] = readFileSync(f, 'utf8')
18
+ }
19
+ })(APP, APP)
20
+
21
+ const dest = process.argv[2]
22
+ if (!dest) throw new Error('usage: node gen-bench-fixture.js <dest.json>')
23
+ writeFileSync(dest, JSON.stringify(out))
24
+ console.log(`fixture: ${Object.keys(out).length} files -> ${dest}`)
@@ -0,0 +1,35 @@
1
+ // ESM resolve hook: resolve bare external specifiers from dimina-kit's node_modules
2
+ // (Layer 1 node test only; the browser bundle inlines everything).
3
+ import { createRequire, isBuiltin } from 'node:module'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ // Resolve bare deps from the dimina-kit workspace root node_modules relative to
7
+ // this file, not a machine-specific path.
8
+ const kitRequire = createRequire(new URL('../../../package.json', import.meta.url))
9
+
10
+ function isBare(spec) {
11
+ return !spec.startsWith('.') && !spec.startsWith('/')
12
+ && !spec.startsWith('node:') && !spec.includes('://')
13
+ && !isBuiltin(spec)
14
+ }
15
+
16
+ export async function resolve(specifier, context, next) {
17
+ if (isBare(specifier)) {
18
+ // Try normal resolution FIRST so packages reachable from the importer (e.g. memfs
19
+ // and its CJS deps) load with their own correct versions and CJS/ESM interop. Only
20
+ // when the default can't find a bare dep — the compiler's externals
21
+ // (sass/less/oxc/esbuild…) that live in the kit workspace root, not next to this
22
+ // bundle — fall back to kit-root resolution.
23
+ try {
24
+ return await next(specifier, context)
25
+ } catch {
26
+ try {
27
+ const p = kitRequire.resolve(specifier)
28
+ return { url: pathToFileURL(p).href, shortCircuit: true }
29
+ } catch {
30
+ // fall through to the default's own error below
31
+ }
32
+ }
33
+ }
34
+ return next(specifier, context)
35
+ }
@@ -0,0 +1,2 @@
1
+ import { register } from 'node:module'
2
+ register('./kit-resolve-hook.js', import.meta.url)
@@ -0,0 +1,114 @@
1
+ // Guards that compileMiniApp always returns a usable appId, even when the
2
+ // project does not include a project.config.json with an explicit appid field.
3
+ // A missing/undefined appId would silently corrupt downstream resource paths
4
+ // (container ?appId= query string, file path prefixes) with no error thrown.
5
+
6
+ import assert from 'node:assert/strict'
7
+ import { Volume, createFsFromVolume } from 'memfs'
8
+ import { compileMiniApp } from '../dist/compile-core.node.js'
9
+
10
+ // web-compiler carries no fs; the caller injects one. memfs stands in as the
11
+ // downstream fs here — seed a files map at workPath and hand over its fs.
12
+ const WP = '/work'
13
+ const fsOf = (files) => createFsFromVolume(Volume.fromJSON(files, WP))
14
+
15
+ // Minimal valid mini-program with no project.config.json.
16
+ const MINIMAL_FILES = {
17
+ 'app.js': 'App({})',
18
+ 'app.json': JSON.stringify({ pages: ['pages/index'] }),
19
+ 'pages/index.js': 'Page({ data: {} })',
20
+ 'pages/index.wxml': '<view>hello</view>',
21
+ }
22
+
23
+ // A project that carries an explicit appid in project.config.json.
24
+ const EXPLICIT_APPID = 'wxTESTappid123456'
25
+ const FILES_WITH_CONFIG = {
26
+ ...MINIMAL_FILES,
27
+ 'project.config.json': JSON.stringify({
28
+ appid: EXPLICIT_APPID,
29
+ compileType: 'miniprogram',
30
+ setting: { es6: true },
31
+ }),
32
+ }
33
+
34
+ let passed = 0
35
+ let failed = 0
36
+
37
+ function ok(label, fn) {
38
+ try {
39
+ fn()
40
+ console.log(` ✅ PASS ${label}`)
41
+ passed++
42
+ } catch (err) {
43
+ console.error(` ❌ FAIL ${label}`)
44
+ console.error(` ${err.message}`)
45
+ failed++
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Group 1: No project.config.json — appId must be a non-empty string and must
51
+ // not appear as the literal text "undefined" in any output file path.
52
+ // ---------------------------------------------------------------------------
53
+ console.log('\nGroup 1: no project.config.json → appId must be a usable non-empty string')
54
+
55
+ const r1 = await compileMiniApp({ fs: fsOf(MINIMAL_FILES), workPath: WP })
56
+ console.log(` actual appId = ${JSON.stringify(r1.appId)}`)
57
+
58
+ ok('appId is a string', () => {
59
+ assert.equal(typeof r1.appId, 'string', `expected string, got ${typeof r1.appId}`)
60
+ })
61
+
62
+ ok('appId is non-empty', () => {
63
+ assert.ok(r1.appId.length > 0, 'appId must not be an empty string')
64
+ })
65
+
66
+ ok('appId is not the literal "undefined"', () => {
67
+ assert.notEqual(r1.appId, 'undefined', 'appId must not be the string "undefined"')
68
+ })
69
+
70
+ const keysWithUndefined = Object.keys(r1.files).filter((k) => k.includes('undefined'))
71
+ ok('output file paths contain no "undefined" segment', () => {
72
+ assert.deepEqual(
73
+ keysWithUndefined,
74
+ [],
75
+ `These output paths contain "undefined": ${JSON.stringify(keysWithUndefined)}`,
76
+ )
77
+ })
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Group 2: Determinism — same input twice must yield identical appId values.
81
+ // ---------------------------------------------------------------------------
82
+ console.log('\nGroup 2: determinism — same input produces the same appId on repeated calls')
83
+
84
+ const r2a = await compileMiniApp({ fs: fsOf(MINIMAL_FILES), workPath: WP })
85
+ const r2b = await compileMiniApp({ fs: fsOf(MINIMAL_FILES), workPath: WP })
86
+ console.log(` call-1 appId = ${JSON.stringify(r2a.appId)}`)
87
+ console.log(` call-2 appId = ${JSON.stringify(r2b.appId)}`)
88
+
89
+ ok('appId is stable across two calls with the same input', () => {
90
+ assert.equal(r2a.appId, r2b.appId, 'appId must be deterministic for the same input')
91
+ })
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Group 3: Explicit appid — project.config.json.appid takes precedence and
95
+ // must be forwarded verbatim. Existing behaviour must not regress.
96
+ // ---------------------------------------------------------------------------
97
+ console.log('\nGroup 3: explicit appid in project.config.json is returned verbatim')
98
+
99
+ const r3 = await compileMiniApp({ fs: fsOf(FILES_WITH_CONFIG), workPath: WP })
100
+ console.log(` actual appId = ${JSON.stringify(r3.appId)}`)
101
+
102
+ ok(`appId equals the declared "${EXPLICIT_APPID}"`, () => {
103
+ assert.equal(r3.appId, EXPLICIT_APPID)
104
+ })
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Summary
108
+ // ---------------------------------------------------------------------------
109
+ console.log(`\n${'─'.repeat(60)}`)
110
+ console.log(`Results: ${passed} passed, ${failed} failed`)
111
+ if (failed > 0) {
112
+ process.exit(1)
113
+ }
114
+ console.log('All assertions passed.')
@@ -0,0 +1,90 @@
1
+ // Decomposed-path test: drive the parallel seams directly — setupCompile once,
2
+ // then each stage (logic/view/style) on its own, then collectOutputs — the exact
3
+ // call shape the parallel worker pipeline uses (minus the multi-realm split, which
4
+ // Phase 3's real workers add). Proves two things Phase 3 relies on:
5
+ // 1. the seams are callable directly (not only through compileMiniApp), and
6
+ // 2. each stage writes ONLY its own products (disjoint outputs) — the invariant
7
+ // that lets stages run concurrently over one shared fs.
8
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
9
+ import { fileURLToPath } from 'node:url'
10
+ import path from 'node:path'
11
+ import { Volume, createFsFromVolume } from 'memfs'
12
+
13
+ const APP = process.env.APP_DIR
14
+ || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url))
15
+
16
+ const TEXT_EXT = new Set([
17
+ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less',
18
+ '.scss', '.sass', '.wxs', '.dds', '.css',
19
+ ])
20
+
21
+ function readDir(dir, baseDir, out) {
22
+ for (const name of readdirSync(dir)) {
23
+ if (name === 'node_modules' || name === '.git') continue
24
+ const full = path.join(dir, name)
25
+ if (statSync(full).isDirectory()) {
26
+ readDir(full, baseDir, out)
27
+ } else {
28
+ const rel = path.relative(baseDir, full).split(path.sep).join('/')
29
+ if (TEXT_EXT.has(path.extname(name).toLowerCase())) out[rel] = readFileSync(full, 'utf8')
30
+ }
31
+ }
32
+ }
33
+
34
+ let failed = 0
35
+ function check(cond, msg) {
36
+ if (cond) console.log(` ✅ ${msg}`)
37
+ else { console.log(` ❌ ${msg}`); failed++ }
38
+ }
39
+
40
+ const files = {}
41
+ readDir(APP, APP, files)
42
+ console.log(`[seed] ${Object.keys(files).length} text files from ${APP}`)
43
+
44
+ const {
45
+ setupCompile, compileStage, collectOutputs, STAGE_NAMES,
46
+ } = await import('../dist/compile-core.node.js')
47
+
48
+ const workPath = '/work'
49
+ const fs = createFsFromVolume(Volume.fromJSON(files, workPath))
50
+
51
+ // --- setup once ---
52
+ const ctx = await setupCompile({ fs, workPath })
53
+ console.log(`\n[setup] appId=${ctx.appId} name=${ctx.name} target=${ctx.targetPath}`)
54
+ check(STAGE_NAMES.join(',') === 'logic,view,style', `STAGE_NAMES = [${STAGE_NAMES}]`)
55
+ check(typeof ctx.storeInfo === 'object' && !!ctx.pages, 'setupCompile returns { storeInfo, pages }')
56
+
57
+ const cssCount = (m) => Object.keys(m).filter((k) => k.endsWith('.css')).length
58
+ const jsCount = (m) => Object.keys(m).filter((k) => k.endsWith('.js')).length
59
+
60
+ const stageArgs = { pages: ctx.pages, storeInfo: ctx.storeInfo, fs }
61
+
62
+ // --- logic only: main/logic.js appears; no styles yet ---
63
+ await compileStage({ stage: 'logic', ...stageArgs })
64
+ const afterLogic = collectOutputs({ fs, targetPath: ctx.targetPath })
65
+ console.log(`\n[after logic] ${Object.keys(afterLogic).length} files, js=${jsCount(afterLogic)} css=${cssCount(afterLogic)}`)
66
+ check('main/logic.js' in afterLogic, 'logic stage wrote main/logic.js')
67
+ check(cssCount(afterLogic) === 0, 'logic stage wrote NO .css (style stage untouched)')
68
+
69
+ // --- view: page view scripts appear; still no styles ---
70
+ await compileStage({ stage: 'view', ...stageArgs })
71
+ const afterView = collectOutputs({ fs, targetPath: ctx.targetPath })
72
+ console.log(`[after view] ${Object.keys(afterView).length} files, js=${jsCount(afterView)} css=${cssCount(afterView)}`)
73
+ check(jsCount(afterView) > jsCount(afterLogic), 'view stage added page view .js files')
74
+ check(cssCount(afterView) === 0, 'view stage wrote NO .css (style stage untouched)')
75
+
76
+ // --- style: .css appears now ---
77
+ await compileStage({ stage: 'style', ...stageArgs })
78
+ const out = collectOutputs({ fs, targetPath: ctx.targetPath })
79
+ console.log(`[after style] ${Object.keys(out).length} files, js=${jsCount(out)} css=${cssCount(out)}`)
80
+ check(cssCount(out) > 0, 'style stage wrote .css files')
81
+
82
+ // --- full artifact set (same expectations as test-node) ---
83
+ for (const n of ['main/logic.js', 'main/app-config.json']) {
84
+ check(n in out && out[n].length > 0, `output has non-empty ${n}`)
85
+ }
86
+ check(out['main/logic.js'].startsWith('modDefine('), 'main/logic.js starts with modDefine(')
87
+
88
+ console.log(`\n────────────────────────────────────────────────────────`)
89
+ if (failed) { console.error(`❌ ${failed} decompose assertion(s) failed.`); process.exit(1) }
90
+ console.log('✅ Decomposed seams work independently; stages write disjoint products.')