@bytecodealliance/jco 0.8.0 → 0.9.0
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 +220 -0
- package/README.md +38 -14
- package/lib/console.js +7 -0
- package/obj/imports/cli-base-environment.d.ts +3 -0
- package/obj/imports/cli-base-exit.d.ts +4 -0
- package/obj/imports/cli-base-preopens.d.ts +5 -0
- package/obj/imports/cli-base-stderr.d.ts +5 -0
- package/obj/imports/cli-base-stdin.d.ts +5 -0
- package/obj/imports/cli-base-stdout.d.ts +5 -0
- package/obj/imports/clocks-wall-clock.d.ts +6 -0
- package/obj/imports/environment.d.ts +3 -0
- package/obj/imports/exit.d.ts +4 -0
- package/obj/imports/filesystem-filesystem.d.ts +152 -0
- package/obj/imports/filesystem.d.ts +152 -0
- package/obj/imports/io-streams.d.ts +12 -0
- package/obj/imports/preopens.d.ts +5 -0
- package/obj/imports/random-random.d.ts +3 -0
- package/obj/imports/random.d.ts +3 -0
- package/obj/imports/stderr.d.ts +5 -0
- package/obj/imports/stdin.d.ts +5 -0
- package/obj/imports/stdout.d.ts +5 -0
- package/obj/imports/streams.d.ts +12 -0
- package/obj/imports/wall-clock.d.ts +6 -0
- package/obj/imports/wasi:cli-base/environment.d.ts +3 -0
- package/obj/imports/wasi:cli-base/exit.d.ts +4 -0
- package/obj/imports/wasi:cli-base/preopens.d.ts +5 -0
- package/obj/imports/wasi:cli-base/stderr.d.ts +5 -0
- package/obj/imports/wasi:cli-base/stdin.d.ts +5 -0
- package/obj/imports/wasi:cli-base/stdout.d.ts +5 -0
- package/obj/imports/wasi:clocks/wall-clock.d.ts +6 -0
- package/obj/imports/wasi:filesystem/filesystem.d.ts +152 -0
- package/obj/imports/wasi:io/streams.d.ts +12 -0
- package/obj/imports/wasi:random/random.d.ts +3 -0
- package/{js-component-bindgen-component.core.wasm → obj/js-component-bindgen-component.core.wasm} +0 -0
- package/obj/js-component-bindgen-component.d.ts +14 -0
- package/obj/js-component-bindgen-component.js +2036 -0
- package/obj/wasm-tools.d.ts +19 -0
- package/obj/wasm-tools.js +2236 -0
- package/package.json +36 -6
- package/src/api.js +45 -0
- package/src/cmd/componentize.js +21 -0
- package/src/cmd/opt.js +151 -0
- package/src/cmd/transpile.js +366 -0
- package/src/cmd/wasm-tools.js +114 -0
- package/src/common.js +104 -0
- package/src/jco.js +129 -0
- package/api.d.ts +0 -109
- package/api.mjs +0 -46035
- package/cli.mjs +0 -49700
- package/wasm-opt +0 -2
- package/wasm2js +0 -2
- /package/{wasi_snapshot_preview1.command.wasm → lib/wasi_snapshot_preview1.command.wasm} +0 -0
- /package/{wasi_snapshot_preview1.reactor.wasm → lib/wasi_snapshot_preview1.reactor.wasm} +0 -0
- /package/{js-component-bindgen-component.core2.wasm → obj/js-component-bindgen-component.core2.wasm} +0 -0
- /package/{wasm-tools.core.wasm → obj/wasm-tools.core.wasm} +0 -0
- /package/{wasm-tools.core2.wasm → obj/wasm-tools.core2.wasm} +0 -0
package/package.json
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytecodealliance/jco",
|
|
3
|
+
"version": "0.9.0",
|
|
3
4
|
"description": "JavaScript tooling for working with WebAssembly Components",
|
|
4
|
-
"version": "0.8.0",
|
|
5
|
-
"exports": "./api.mjs",
|
|
6
|
-
"types": "api.d.ts",
|
|
7
5
|
"author": "Guy Bedford",
|
|
8
6
|
"bin": {
|
|
9
|
-
"jco": "
|
|
7
|
+
"jco": "src/jco.js"
|
|
10
8
|
},
|
|
9
|
+
"exports": "./src/api.js",
|
|
10
|
+
"type": "module",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bytecodealliance/preview2-shim": "0.0.9"
|
|
12
|
+
"@bytecodealliance/preview2-shim": "0.0.9",
|
|
13
|
+
"binaryen": "^111.0.0",
|
|
14
|
+
"chalk-template": "^0.4.0",
|
|
15
|
+
"commander": "^9.4.1",
|
|
16
|
+
"mkdirp": "^1.0.4",
|
|
17
|
+
"ora": "^6.1.2",
|
|
18
|
+
"terser": "^5.16.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@bytecodealliance/componentize-js": "0.0.7",
|
|
22
|
+
"@types/node": "^18.11.17",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
|
24
|
+
"@typescript-eslint/parser": "^5.41.0",
|
|
25
|
+
"eslint": "^8.30.0",
|
|
26
|
+
"mocha": "^10.2.0",
|
|
27
|
+
"terser": "^5.16.1",
|
|
28
|
+
"typescript": "^4.3.2"
|
|
13
29
|
},
|
|
14
30
|
"repository": {
|
|
15
31
|
"type": "git",
|
|
@@ -24,5 +40,19 @@
|
|
|
24
40
|
"bugs": {
|
|
25
41
|
"url": "https://github.com/bytecodealliance/jco/issues"
|
|
26
42
|
},
|
|
27
|
-
"homepage": "https://github.com/bytecodealliance/jco#readme"
|
|
43
|
+
"homepage": "https://github.com/bytecodealliance/jco#readme",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "cargo build --workspace --target wasm32-wasi --release && cargo run",
|
|
46
|
+
"build:types:preview2-shim": "PREVIEW2_SHIM_TYPES=* cargo build -p jco",
|
|
47
|
+
"lint": "eslint -c eslintrc.cjs lib/**/*.js packages/*/lib/**/*.js",
|
|
48
|
+
"test": "mocha -u tdd test/test.js --timeout 120000"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"lib",
|
|
52
|
+
"src",
|
|
53
|
+
"obj"
|
|
54
|
+
],
|
|
55
|
+
"workspaces": [
|
|
56
|
+
"packages/preview2-shim"
|
|
57
|
+
]
|
|
28
58
|
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { optimizeComponent as opt } from './cmd/opt.js';
|
|
2
|
+
export { transpileComponent as transpile } from './cmd/transpile.js';
|
|
3
|
+
import { $init, print as printFn, parse as parseFn, componentWit as componentWitFn, componentNew as componentNewFn, componentEmbed as componentEmbedFn, metadataAdd as metadataAddFn, metadataShow as metadataShowFn } from "../obj/wasm-tools.js";
|
|
4
|
+
|
|
5
|
+
/** @type {import('../obj/wasm-tools.js').print} */
|
|
6
|
+
export async function print (binary) {
|
|
7
|
+
await $init;
|
|
8
|
+
return printFn(binary);
|
|
9
|
+
}
|
|
10
|
+
/** @type {import('../obj/wasm-tools.js').parse} */
|
|
11
|
+
export async function parse (wat) {
|
|
12
|
+
await $init;
|
|
13
|
+
return parseFn(wat);
|
|
14
|
+
}
|
|
15
|
+
/** @type {import('../obj/wasm-tools.js').componentWit} */
|
|
16
|
+
export async function componentWit (binary) {
|
|
17
|
+
await $init;
|
|
18
|
+
return componentWitFn(binary);
|
|
19
|
+
}
|
|
20
|
+
/** @type {import('../obj/wasm-tools.js').componentNew} */
|
|
21
|
+
export async function componentNew (binary, adapters) {
|
|
22
|
+
await $init;
|
|
23
|
+
return componentNewFn(binary, adapters);
|
|
24
|
+
}
|
|
25
|
+
/** @type {import('../obj/wasm-tools.js').componentEmbed} */
|
|
26
|
+
export async function componentEmbed (embedOpts) {
|
|
27
|
+
await $init;
|
|
28
|
+
return componentEmbedFn(embedOpts);
|
|
29
|
+
}
|
|
30
|
+
/** @type {import('../obj/wasm-tools.js').metadataAdd} */
|
|
31
|
+
export async function metadataAdd () {
|
|
32
|
+
await $init;
|
|
33
|
+
return metadataAddFn(binary, metadata);
|
|
34
|
+
}
|
|
35
|
+
/** @type {import('../obj/wasm-tools.js').metadataShow} */
|
|
36
|
+
export async function metadataShow (binary) {
|
|
37
|
+
await $init;
|
|
38
|
+
return metadataShowFn(binary);
|
|
39
|
+
}
|
|
40
|
+
export function preview1AdapterCommandPath () {
|
|
41
|
+
return new URL('../lib/wasi_snapshot_preview1.command.wasm', import.meta.url);
|
|
42
|
+
}
|
|
43
|
+
export function preview1AdapterReactorPath () {
|
|
44
|
+
return new URL('../lib/wasi_snapshot_preview1.reactor.wasm', import.meta.url);
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import c from 'chalk-template';
|
|
4
|
+
|
|
5
|
+
export async function componentize (jsSource, opts) {
|
|
6
|
+
let componentizeFn;
|
|
7
|
+
try {
|
|
8
|
+
({ componentize: componentizeFn } = await eval('import("@bytecodealliance/componentize-js")'));
|
|
9
|
+
} catch (e) {
|
|
10
|
+
if (e?.code === 'ERR_MODULE_NOT_FOUND' && e?.message?.includes('\'@bytecodealliance/componentize-js\''))
|
|
11
|
+
throw new Error(`componentize-js must first be installed separately via "npm install @bytecodealliance/componentize-js".`);
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
14
|
+
const source = await readFile(jsSource, 'utf8');
|
|
15
|
+
const { component, imports } = await componentizeFn(source, {
|
|
16
|
+
witPath: resolve(opts.wit),
|
|
17
|
+
worldName: opts.worldName
|
|
18
|
+
});
|
|
19
|
+
await writeFile(opts.out, component);
|
|
20
|
+
console.log(c`{green OK} Successfully written {bold ${opts.out}} with imports (${imports.join(', ')}).`);
|
|
21
|
+
}
|
package/src/cmd/opt.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { $init, metadataShow, print } from '../../obj/wasm-tools.js';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import c from 'chalk-template';
|
|
5
|
+
import { readFile, sizeStr, fixedDigitDisplay, table, spawnIOTmp, setShowSpinner, getShowSpinner } from '../common.js';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
let WASM_OPT;
|
|
9
|
+
try {
|
|
10
|
+
WASM_OPT = fileURLToPath(new URL('../../node_modules/binaryen/bin/wasm-opt', import.meta.url));
|
|
11
|
+
} catch (e) {
|
|
12
|
+
WASM_OPT = new URL('../../node_modules/binaryen/bin/wasm-opt', import.meta.url);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function opt (componentPath, opts, program) {
|
|
16
|
+
await $init;
|
|
17
|
+
const varIdx = program.parent.rawArgs.indexOf('--');
|
|
18
|
+
if (varIdx !== -1)
|
|
19
|
+
opts.optArgs = program.parent.rawArgs.slice(varIdx + 1);
|
|
20
|
+
const componentBytes = await readFile(componentPath);
|
|
21
|
+
|
|
22
|
+
if (!opts.quiet) setShowSpinner(true);
|
|
23
|
+
const optPromise = optimizeComponent(componentBytes, opts);
|
|
24
|
+
const { component, compressionInfo } = await optPromise;
|
|
25
|
+
|
|
26
|
+
await writeFile(opts.output, component);
|
|
27
|
+
|
|
28
|
+
let totalBeforeBytes = 0, totalAfterBytes = 0;
|
|
29
|
+
|
|
30
|
+
if (!opts.quiet)
|
|
31
|
+
console.log(c`
|
|
32
|
+
{bold Optimized WebAssembly Component Internal Core Modules:}
|
|
33
|
+
|
|
34
|
+
${table([...compressionInfo.map(({ beforeBytes, afterBytes }, i) => {
|
|
35
|
+
totalBeforeBytes += beforeBytes;
|
|
36
|
+
totalAfterBytes += afterBytes;
|
|
37
|
+
return [
|
|
38
|
+
` - Core Module ${i + 1}: `,
|
|
39
|
+
sizeStr(beforeBytes),
|
|
40
|
+
' -> ',
|
|
41
|
+
c`{cyan ${sizeStr(afterBytes)}} `,
|
|
42
|
+
`(${fixedDigitDisplay(afterBytes / beforeBytes * 100, 2)}%)`
|
|
43
|
+
];
|
|
44
|
+
}), ['', '', '', '', ''], [
|
|
45
|
+
` = Total: `,
|
|
46
|
+
`${sizeStr(totalBeforeBytes)}`,
|
|
47
|
+
` => `,
|
|
48
|
+
c`{cyan ${sizeStr(totalAfterBytes)}} `,
|
|
49
|
+
`(${fixedDigitDisplay(totalAfterBytes / totalBeforeBytes * 100, 2)}%)`
|
|
50
|
+
]], [,,,,'right'])}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @param {Uint8Array} componentBytes
|
|
56
|
+
* @param {{ quiet: boolean, optArgs?: string[] }} options?
|
|
57
|
+
* @returns {Promise<{ component: Uint8Array, compressionInfo: { beforeBytes: number, afterBytes: number }[] >}
|
|
58
|
+
*/
|
|
59
|
+
export async function optimizeComponent (componentBytes, opts) {
|
|
60
|
+
await $init;
|
|
61
|
+
const showSpinner = getShowSpinner();
|
|
62
|
+
let spinner;
|
|
63
|
+
try {
|
|
64
|
+
const coreModules = metadataShow(componentBytes).slice(1, -1).map(({ range }) => range);
|
|
65
|
+
|
|
66
|
+
let completed = 0;
|
|
67
|
+
const spinnerText = () => c`{cyan ${completed} / ${coreModules.length}} Running Binaryen on WebAssembly Component Internal Core Modules \n`;
|
|
68
|
+
if (showSpinner) {
|
|
69
|
+
spinner = ora({
|
|
70
|
+
color: 'cyan',
|
|
71
|
+
spinner: 'bouncingBar'
|
|
72
|
+
}).start();
|
|
73
|
+
spinner.text = spinnerText();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const optimizedCoreModules = await Promise.all(coreModules.map(async ([coreModuleStart, coreModuleEnd]) => {
|
|
77
|
+
const optimized = wasmOpt(componentBytes.subarray(coreModuleStart, coreModuleEnd), opts?.optArgs);
|
|
78
|
+
if (spinner) {
|
|
79
|
+
completed++;
|
|
80
|
+
spinner.text = spinnerText();
|
|
81
|
+
}
|
|
82
|
+
return optimized;
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
let outComponentBytes = new Uint8Array(componentBytes.byteLength);
|
|
86
|
+
let nextReadPos = 0, nextWritePos = 0;
|
|
87
|
+
for (let i = 0; i < coreModules.length; i++) {
|
|
88
|
+
const [coreModuleStart, coreModuleEnd] = coreModules[i];
|
|
89
|
+
const optimizedCoreModule = optimizedCoreModules[i];
|
|
90
|
+
|
|
91
|
+
let lebByteLen = 1;
|
|
92
|
+
while (componentBytes[coreModuleStart - 1 - lebByteLen] & 0x80) lebByteLen++;
|
|
93
|
+
|
|
94
|
+
// Write from the last read to the LEB byte start of the core module
|
|
95
|
+
outComponentBytes.set(componentBytes.subarray(nextReadPos, coreModuleStart - lebByteLen), nextWritePos);
|
|
96
|
+
nextWritePos += coreModuleStart - lebByteLen - nextReadPos;
|
|
97
|
+
|
|
98
|
+
// Write the new LEB bytes
|
|
99
|
+
let val = optimizedCoreModule.byteLength;
|
|
100
|
+
do {
|
|
101
|
+
const byte = val & 0x7F;
|
|
102
|
+
val >>>= 7;
|
|
103
|
+
outComponentBytes[nextWritePos++] = val === 0 ? byte : byte | 0x80;
|
|
104
|
+
} while (val !== 0);
|
|
105
|
+
|
|
106
|
+
// Write the core module
|
|
107
|
+
outComponentBytes.set(optimizedCoreModule, nextWritePos);
|
|
108
|
+
nextWritePos += optimizedCoreModule.byteLength;
|
|
109
|
+
|
|
110
|
+
nextReadPos = coreModuleEnd;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
outComponentBytes.set(componentBytes.subarray(nextReadPos, componentBytes.byteLength), nextWritePos);
|
|
114
|
+
nextWritePos += componentBytes.byteLength - nextReadPos;
|
|
115
|
+
nextReadPos += componentBytes.byteLength - nextReadPos;
|
|
116
|
+
|
|
117
|
+
outComponentBytes = outComponentBytes.subarray(0, outComponentBytes.length + nextWritePos - nextReadPos);
|
|
118
|
+
|
|
119
|
+
// verify it still parses ok
|
|
120
|
+
try {
|
|
121
|
+
await print(outComponentBytes);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
throw new Error(`Internal error performing optimization.\n${e.message}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
component: outComponentBytes,
|
|
128
|
+
compressionInfo: coreModules.map(([s, e], i) => ({ beforeBytes: e - s, afterBytes: optimizedCoreModules[i].byteLength }))
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (spinner)
|
|
133
|
+
spinner.stop();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {Uint8Array} source
|
|
139
|
+
* @returns {Promise<Uint8Array>}
|
|
140
|
+
*/
|
|
141
|
+
async function wasmOpt (source, args = ['-O1', '--low-memory-unused', '--enable-bulk-memory']) {
|
|
142
|
+
try {
|
|
143
|
+
return await spawnIOTmp(WASM_OPT, source, [
|
|
144
|
+
...args, '-o'
|
|
145
|
+
]);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (e.toString().includes('BasicBlock requested'))
|
|
148
|
+
return wasmOpt(source, args);
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { $init, generate } from '../../obj/js-component-bindgen-component.js';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { mkdir } from 'fs/promises';
|
|
4
|
+
import { dirname, extname, basename } from 'path';
|
|
5
|
+
import c from 'chalk-template';
|
|
6
|
+
import { readFile, sizeStr, table, spawnIOTmp, setShowSpinner, getShowSpinner } from '../common.js';
|
|
7
|
+
import { optimizeComponent } from './opt.js';
|
|
8
|
+
import { minify } from 'terser';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
|
|
12
|
+
export async function transpile (componentPath, opts, program) {
|
|
13
|
+
const varIdx = program.parent.rawArgs.indexOf('--');
|
|
14
|
+
if (varIdx !== -1)
|
|
15
|
+
opts.optArgs = program.parent.rawArgs.slice(varIdx + 1);
|
|
16
|
+
const component = await readFile(componentPath);
|
|
17
|
+
|
|
18
|
+
if (!opts.quiet)
|
|
19
|
+
setShowSpinner(true);
|
|
20
|
+
if (!opts.name)
|
|
21
|
+
opts.name = basename(componentPath.slice(0, -extname(componentPath).length || Infinity));
|
|
22
|
+
if (opts.map)
|
|
23
|
+
opts.map = Object.fromEntries(opts.map.map(mapping => mapping.split('=')));
|
|
24
|
+
const { files } = await transpileComponent(component, opts);
|
|
25
|
+
|
|
26
|
+
await Promise.all(Object.entries(files).map(async ([name, file]) => {
|
|
27
|
+
await mkdir(dirname(name), { recursive: true });
|
|
28
|
+
await writeFile(name, file);
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
if (!opts.quiet)
|
|
32
|
+
console.log(c`
|
|
33
|
+
{bold Transpiled JS Component Files:}
|
|
34
|
+
|
|
35
|
+
${table(Object.entries(files).map(([name, source]) => [
|
|
36
|
+
c` - {italic ${name}} `,
|
|
37
|
+
c`{black.italic ${sizeStr(source.length)}}`
|
|
38
|
+
]))}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let WASM_2_JS;
|
|
42
|
+
try {
|
|
43
|
+
WASM_2_JS = fileURLToPath(new URL('../../node_modules/binaryen/bin/wasm2js', import.meta.url));
|
|
44
|
+
} catch {
|
|
45
|
+
WASM_2_JS = new URL('../../node_modules/binaryen/bin/wasm2js', import.meta.url);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {Uint8Array} source
|
|
50
|
+
* @returns {Promise<Uint8Array>}
|
|
51
|
+
*/
|
|
52
|
+
async function wasm2Js (source) {
|
|
53
|
+
try {
|
|
54
|
+
return await spawnIOTmp(WASM_2_JS, source, ['-Oz', '-o']);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e.toString().includes('BasicBlock requested'))
|
|
57
|
+
return wasm2Js(source);
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
* @param {Uint8Array} component
|
|
65
|
+
* @param {{
|
|
66
|
+
* name: string,
|
|
67
|
+
* instantiation?: bool,
|
|
68
|
+
* map?: Record<string, string>,
|
|
69
|
+
* validLiftingOptimization?: bool,
|
|
70
|
+
* noNodejsCompat?: bool,
|
|
71
|
+
* tlaCompat?: bool,
|
|
72
|
+
* base64Cutoff?: bool,
|
|
73
|
+
* js?: bool,
|
|
74
|
+
* minify?: bool,
|
|
75
|
+
* optimize?: bool,
|
|
76
|
+
* optArgs?: string[],
|
|
77
|
+
* }} opts
|
|
78
|
+
* @returns {Promise<{ files: { [filename: string]: Uint8Array }, imports: string[], exports: [string, 'function' | 'instance'][] }>}
|
|
79
|
+
*/
|
|
80
|
+
export async function transpileComponent (component, opts = {}) {
|
|
81
|
+
await $init;
|
|
82
|
+
if (opts.noWasiShim) opts.wasiShim = false;
|
|
83
|
+
|
|
84
|
+
let spinner;
|
|
85
|
+
const showSpinner = getShowSpinner();
|
|
86
|
+
if (opts.optimize) {
|
|
87
|
+
if (showSpinner) setShowSpinner(true);
|
|
88
|
+
({ component } = await optimizeComponent(component, opts));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (opts.wasiShim !== false) {
|
|
92
|
+
opts.map = Object.assign({
|
|
93
|
+
'wasi:cli-base/*': '@bytecodealliance/preview2-shim/cli-base#*',
|
|
94
|
+
'wasi:filesystem/*': '@bytecodealliance/preview2-shim/filesystem#*',
|
|
95
|
+
'wasi:io/*': '@bytecodealliance/preview2-shim/io#*',
|
|
96
|
+
'wasi:random/*': '@bytecodealliance/preview2-shim/random#*',
|
|
97
|
+
}, opts.map || {});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let { files, imports, exports } = generate(component, {
|
|
101
|
+
name: opts.name ?? 'component',
|
|
102
|
+
map: Object.entries(opts.map ?? {}),
|
|
103
|
+
instantiation: opts.instantiation || opts.js,
|
|
104
|
+
validLiftingOptimization: opts.validLiftingOptimization ?? false,
|
|
105
|
+
noNodejsCompat: !(opts.nodejsCompat ?? true),
|
|
106
|
+
tlaCompat: opts.tlaCompat ?? false,
|
|
107
|
+
base64Cutoff: opts.js ? 0 : opts.base64Cutoff ?? 5000
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let outDir = (opts.outDir ?? '').replace(/\\/g, '/');
|
|
111
|
+
if (!outDir.endsWith('/') && outDir !== '')
|
|
112
|
+
outDir += '/';
|
|
113
|
+
files = files.map(([name, source]) => [`${outDir}${name}`, source]);
|
|
114
|
+
|
|
115
|
+
const jsFile = files.find(([name]) => name.endsWith('.js'));
|
|
116
|
+
|
|
117
|
+
if (opts.js) {
|
|
118
|
+
const source = Buffer.from(jsFile[1]).toString('utf8')
|
|
119
|
+
// update imports manging to match emscripten asm
|
|
120
|
+
.replace(/exports(\d+)\['([^']+)']/g, (_, i, s) => `exports${i}['${asmMangle(s)}']`);
|
|
121
|
+
|
|
122
|
+
const wasmFiles = files.filter(([name]) => name.endsWith('.wasm'));
|
|
123
|
+
files = files.filter(([name]) => !name.endsWith('.wasm'));
|
|
124
|
+
|
|
125
|
+
// TODO: imports as specifier list
|
|
126
|
+
|
|
127
|
+
let completed = 0;
|
|
128
|
+
const spinnerText = () => c`{cyan ${completed} / ${wasmFiles.length}} Running Binaryen wasm2js on Wasm core modules (this takes a while)...\n`;
|
|
129
|
+
if (showSpinner) {
|
|
130
|
+
spinner = ora({
|
|
131
|
+
color: 'cyan',
|
|
132
|
+
spinner: 'bouncingBar'
|
|
133
|
+
}).start();
|
|
134
|
+
spinner.text = spinnerText();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const asmFiles = await Promise.all(wasmFiles.map(async ([, source]) => {
|
|
139
|
+
const output = (await wasm2Js(source)).toString('utf8');
|
|
140
|
+
if (spinner) {
|
|
141
|
+
completed++;
|
|
142
|
+
spinner.text = spinnerText();
|
|
143
|
+
}
|
|
144
|
+
return output;
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const asms = asmFiles.map((asm, i) =>`function asm${i}(imports) {
|
|
148
|
+
${
|
|
149
|
+
// strip and replace the asm instantiation wrapper
|
|
150
|
+
asm
|
|
151
|
+
.replace(/import \* as [^ ]+ from '[^']*';/g, '')
|
|
152
|
+
.replace('function asmFunc(imports) {', '')
|
|
153
|
+
.replace(/export var ([^ ]+) = ([^. ]+)\.([^ ]+);/g, '')
|
|
154
|
+
.replace(/var retasmFunc = [\s\S]*$/, '')
|
|
155
|
+
.replace(/var memasmFunc = new ArrayBuffer\(0\);/g, '')
|
|
156
|
+
.replace('memory.grow = __wasm_memory_grow;', '')
|
|
157
|
+
.trim()
|
|
158
|
+
}`).join(',\n');
|
|
159
|
+
|
|
160
|
+
const outSource = `${
|
|
161
|
+
imports.map((impt, i) => `import * as import${i} from '${impt}';`).join('\n')}
|
|
162
|
+
${source.replace('export async function instantiate', 'async function instantiate')}
|
|
163
|
+
|
|
164
|
+
let ${exports.filter(([, ty]) => ty === 'function').map(([name]) => '_' + name).join(', ')};
|
|
165
|
+
|
|
166
|
+
${exports.map(([name, ty]) => ty === 'function' ? `\nfunction ${asmMangle(name)} () {
|
|
167
|
+
return _${name}.apply(this, arguments);
|
|
168
|
+
}` : `\nlet ${asmMangle(name)};`).join('\n')}
|
|
169
|
+
|
|
170
|
+
const asmInit = [${asms}];
|
|
171
|
+
|
|
172
|
+
${opts.tlaCompat ? 'export ' : ''}const $init = (async () => {
|
|
173
|
+
let idx = 0;
|
|
174
|
+
({ ${exports.map(([name, ty]) => asmMangle(name) === name ? name : `'${name}': ${asmMangle(name)}`).join(',\n')} } = await instantiate(n => idx++, {
|
|
175
|
+
${imports.map((impt, i) => ` '${impt}': import${i},`).join('\n')}
|
|
176
|
+
}, (i, imports) => ({ exports: asmInit[i](imports) })));
|
|
177
|
+
})();${exports.length > 0 ? `\nexport { ${exports.map(([name]) => name === asmMangle(name) ? `${name}` : `${asmMangle(name)} as "${name}"`).join(', ')} }` : ''}
|
|
178
|
+
${opts.tlaCompat ? '' : '\nawait $init;\n'}`;
|
|
179
|
+
|
|
180
|
+
jsFile[1] = Buffer.from(outSource);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (spinner)
|
|
184
|
+
spinner.stop();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (opts.minify) {
|
|
189
|
+
({ code: jsFile[1] } = await minify(Buffer.from(jsFile[1]).toString('utf8'), {
|
|
190
|
+
module: true,
|
|
191
|
+
compress: {
|
|
192
|
+
ecma: 9,
|
|
193
|
+
unsafe: true
|
|
194
|
+
},
|
|
195
|
+
mangle: {
|
|
196
|
+
keep_classnames: true
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { files: Object.fromEntries(files), imports, exports };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// emscripten asm mangles specifiers to be valid identifiers
|
|
205
|
+
// for imports to match up we must do the same
|
|
206
|
+
// See https://github.com/WebAssembly/binaryen/blob/main/src/asmjs/asmangle.cpp
|
|
207
|
+
function asmMangle (name) {
|
|
208
|
+
if (name === '')
|
|
209
|
+
return '$';
|
|
210
|
+
|
|
211
|
+
let mightBeKeyword = true;
|
|
212
|
+
let i = 1;
|
|
213
|
+
|
|
214
|
+
// Names must start with a character, $ or _
|
|
215
|
+
switch (name[0]) {
|
|
216
|
+
case '0':
|
|
217
|
+
case '1':
|
|
218
|
+
case '2':
|
|
219
|
+
case '3':
|
|
220
|
+
case '4':
|
|
221
|
+
case '5':
|
|
222
|
+
case '6':
|
|
223
|
+
case '7':
|
|
224
|
+
case '8':
|
|
225
|
+
case '9': {
|
|
226
|
+
name = '$' + name;
|
|
227
|
+
i = 2;
|
|
228
|
+
// fallthrough
|
|
229
|
+
}
|
|
230
|
+
case '$':
|
|
231
|
+
case '_': {
|
|
232
|
+
mightBeKeyword = false;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
default: {
|
|
236
|
+
let chNum = name.charCodeAt(0);
|
|
237
|
+
if (!(chNum >= 97 && chNum <= 122) && !(chNum >= 65 && chNum <= 90)) {
|
|
238
|
+
name = '$' + name.substr(1);
|
|
239
|
+
mightBeKeyword = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Names must contain only characters, digits, $ or _
|
|
245
|
+
let len = name.length;
|
|
246
|
+
for (; i < len; ++i) {
|
|
247
|
+
switch (name[i]) {
|
|
248
|
+
case '0':
|
|
249
|
+
case '1':
|
|
250
|
+
case '2':
|
|
251
|
+
case '3':
|
|
252
|
+
case '4':
|
|
253
|
+
case '5':
|
|
254
|
+
case '6':
|
|
255
|
+
case '7':
|
|
256
|
+
case '8':
|
|
257
|
+
case '9':
|
|
258
|
+
case '$':
|
|
259
|
+
case '_': {
|
|
260
|
+
mightBeKeyword = false;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
default: {
|
|
264
|
+
let chNum = name.charCodeAt(i);
|
|
265
|
+
if (!(chNum >= 97 && chNum <= 122) && !(chNum >= 65 && chNum <= 90)) {
|
|
266
|
+
name = name.substr(0, i) + '_' + name.substr(i + 1);
|
|
267
|
+
mightBeKeyword = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Names must not collide with keywords
|
|
274
|
+
if (mightBeKeyword && len >= 2 && len <= 10) {
|
|
275
|
+
switch (name[0]) {
|
|
276
|
+
case 'a': {
|
|
277
|
+
if (name == "arguments")
|
|
278
|
+
return name + '_';
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'b': {
|
|
282
|
+
if (name == "break")
|
|
283
|
+
return name + '_';
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'c': {
|
|
287
|
+
if (name == "case" || name == "continue" || name == "catch" ||
|
|
288
|
+
name == "const" || name == "class")
|
|
289
|
+
return name + '_';
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'd': {
|
|
293
|
+
if (name == "do" || name == "default" || name == "debugger")
|
|
294
|
+
return name + '_';
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'e': {
|
|
298
|
+
if (name == "else" || name == "enum" || name == "eval" || // to be sure
|
|
299
|
+
name == "export" || name == "extends")
|
|
300
|
+
return name + '_';
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'f': {
|
|
304
|
+
if (name == "for" || name == "false" || name == "finally" ||
|
|
305
|
+
name == "function")
|
|
306
|
+
return name + '_';
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case 'i': {
|
|
310
|
+
if (name == "if" || name == "in" || name == "import" ||
|
|
311
|
+
name == "interface" || name == "implements" ||
|
|
312
|
+
name == "instanceof")
|
|
313
|
+
return name + '_';
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 'l': {
|
|
317
|
+
if (name == "let")
|
|
318
|
+
return name + '_';
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'n': {
|
|
322
|
+
if (name == "new" || name == "null")
|
|
323
|
+
return name + '_';
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'p': {
|
|
327
|
+
if (name == "public" || name == "package" || name == "private" ||
|
|
328
|
+
name == "protected")
|
|
329
|
+
return name + '_';
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'r': {
|
|
333
|
+
if (name == "return")
|
|
334
|
+
return name + '_';
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 's': {
|
|
338
|
+
if (name == "super" || name == "static" || name == "switch")
|
|
339
|
+
return name + '_';
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case 't': {
|
|
343
|
+
if (name == "try" || name == "this" || name == "true" ||
|
|
344
|
+
name == "throw" || name == "typeof")
|
|
345
|
+
return name + '_';
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'v': {
|
|
349
|
+
if (name == "var" || name == "void")
|
|
350
|
+
return name + '_';
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'w': {
|
|
354
|
+
if (name == "with" || name == "while")
|
|
355
|
+
return name + '_';
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 'y': {
|
|
359
|
+
if (name == "yield")
|
|
360
|
+
return name + '_';
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return name;
|
|
366
|
+
}
|