@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
|
@@ -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,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.')
|