Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to not inline wasm #543

Merged
merged 9 commits into from
Sep 6, 2020
Merged
22 changes: 19 additions & 3 deletions packages/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export default {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
format: 'cjs',
},
plugins: [wasm()]
plugins: [wasm()],
};
```

Expand All @@ -53,6 +53,22 @@ Default: `null`

Specifies an array of strings that each represent a WebAssembly file to load synchronously. See [Synchronous Modules](#synchronous-modules) for a functional example.

### `maxFileSize`

Type: `Number`<br>
Default: `14336` (14kb)

The maximum file size for inline files. If a file exceeds this limit, it will be copied to the destination folder and loaded from a separate file at runtime. If `maxFileSize` is set to `0` all files will be copied.

Files specified in `sync` to load synchronously are always inlined, regardless of size.

### `publicPath`

Type: `String`<br>
Default: (empty string)

A string which will be added in front of filenames when they are not inlined but are copied.

## WebAssembly Example

Given the following simple C file:
Expand Down Expand Up @@ -83,7 +99,7 @@ Small modules (< 4KB) can be compiled synchronously by specifying them in the co

```js
wasm({
sync: ['web/sample.wasm', 'web/foobar.wasm']
sync: ['web/sample.wasm', 'web/foobar.wasm'],
});
```

Expand Down
118 changes: 96 additions & 22 deletions packages/wasm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,84 @@
import { readFile } from 'fs';
import { resolve } from 'path';
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';

import { Plugin } from 'rollup';

import { RollupWasmOptions } from '../types';

export function wasm(options: RollupWasmOptions = {}): Plugin {
const syncFiles = (options.sync || []).map((x) => resolve(x));
const { sync = [], maxFileSize = 14 * 1024, publicPath = '' } = options;

const syncFiles = sync.map((x) => path.resolve(x));
const copies = Object.create(null);

return {
name: 'wasm',

load(id) {
if (/\.wasm$/.test(id)) {
return new Promise((res, reject) => {
readFile(id, (error, buffer) => {
if (error != null) {
reject(error);
}
res(buffer.toString('binary'));
});
});
if (!/\.wasm$/.test(id)) {
return null;
}
return null;

return Promise.all([fs.promises.stat(id), fs.promises.readFile(id)]).then(
([stats, buffer]) => {
if ((maxFileSize && stats.size > maxFileSize) || maxFileSize === 0) {
const hash = createHash('sha1')
.update(buffer)
.digest('hex')
.substr(0, 16);

const filename = `${hash}.wasm`;
const publicFilepath = `${publicPath}${filename}`;

// only copy if the file is not marked `sync`, `sync` files are always inlined
if (syncFiles.indexOf(id) === -1) {
copies[id] = {
filename,
publicFilepath,
buffer
};
}
}

return buffer.toString('binary');
}
);
},

banner: `
function _loadWasmModule (sync, src, imports) {
function _loadWasmModule (sync, filepath, src, imports) {
function _instantiateOrCompile(source, imports, stream) {
var instantiateFunc = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate;
var compileFunc = stream ? WebAssembly.compileStreaming : WebAssembly.compile;

if (imports) {
return instantiateFunc(source, imports)
} else {
return compileFunc(source)
}
}

var buf = null
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null

if (filepath && isNode) {
var fs = eval('require("fs")')
var path = eval('require("path")')

return new Promise((resolve, reject) => {
fs.readFile(path.resolve(__dirname, filepath), (error, buffer) => {
if (error != null) {
reject(error)
}

resolve(_instantiateOrCompile(buffer, imports, false))
});
});
} else if (filepath) {
return _instantiateOrCompile(fetch(filepath), imports, true)
}

if (isNode) {
buf = Buffer.from(src, 'base64')
} else {
Expand All @@ -40,24 +90,48 @@ export function wasm(options: RollupWasmOptions = {}): Plugin {
}
}

if (imports && !sync) {
return WebAssembly.instantiate(buf, imports)
} else if (!imports && !sync) {
return WebAssembly.compile(buf)
} else {
if(sync) {
var mod = new WebAssembly.Module(buf)
return imports ? new WebAssembly.Instance(mod, imports) : mod
} else {
return _instantiateOrCompile(buf, imports, false)
}
}
`.trim(),

transform(code, id) {
if (code && /\.wasm$/.test(id)) {
const src = Buffer.from(code, 'binary').toString('base64');
const sync = syncFiles.indexOf(id) !== -1;
return `export default function(imports){return _loadWasmModule(${+sync}, '${src}', imports)}`;
const isSync = syncFiles.indexOf(id) !== -1;
const publicFilepath = copies[id] ? `'${copies[id].publicFilepath}'` : null;
let src;

if (publicFilepath === null) {
src = Buffer.from(code, 'binary').toString('base64');
src = `'${src}'`;
} else {
if (isSync) {
this.error('non-inlined files can not be `sync`.');
}
src = null;
}

return `export default function(imports){return _loadWasmModule(${+isSync}, ${publicFilepath}, ${src}, imports)}`;
}
return null;
},
generateBundle: async function write() {
await Promise.all(
Object.keys(copies).map(async (name) => {
const copy = copies[name];

this.emitFile({
type: 'asset',
source: copy.buffer,
name: 'Rollup WASM Asset',
fileName: copy.filename
});
})
);
}
};
}
Expand Down
13 changes: 13 additions & 0 deletions packages/wasm/test/snapshots/test.js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Snapshot report for `test/test.js`

The actual snapshot is saved in `test.js.snap`.

Generated by [AVA](https://avajs.dev).

## fetching WASM from separate file

> Snapshot 1

[
'output/85cebae0fa1ae813.wasm',
]
Binary file added packages/wasm/test/snapshots/test.js.snap
Binary file not shown.
49 changes: 42 additions & 7 deletions packages/wasm/test/test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { sep, posix, join } from 'path';

import { rollup } from 'rollup';
import globby from 'globby';
import test from 'ava';
import del from 'del';

import { getCode } from '../../../util/test';

import wasm from '../';

const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;

process.chdir(__dirname);

const outputFile = './output/bundle.js';
const outputDir = './output/';

const testBundle = async (t, bundle) => {
const code = await getCode(bundle);
const func = new AsyncFunction('t', `let result;\n\n${code}\n\nreturn result;`);
Expand All @@ -17,17 +26,43 @@ test('async compiling', async (t) => {
t.plan(2);

const bundle = await rollup({
input: 'test/fixtures/async.js',
input: 'fixtures/async.js',
plugins: [wasm()]
});
await testBundle(t, bundle);
});

test('fetching WASM from separate file', async (t) => {
t.plan(3);

const bundle = await rollup({
input: 'fixtures/complex.js',
plugins: [
wasm({
maxFileSize: 0
})
]
});

await bundle.write({ format: 'cjs', file: outputFile });
const glob = join(outputDir, `**/*.wasm`)
.split(sep)
.join(posix.sep);

global.result = null;
global.t = t;
require(outputFile);

await global.result;
t.snapshot(await globby(glob));
await del(outputDir);
});

test('complex module decoding', async (t) => {
t.plan(2);

const bundle = await rollup({
input: 'test/fixtures/complex.js',
input: 'fixtures/complex.js',
plugins: [wasm()]
});
await testBundle(t, bundle);
Expand All @@ -37,10 +72,10 @@ test('sync compiling', async (t) => {
t.plan(2);

const bundle = await rollup({
input: 'test/fixtures/sync.js',
input: 'fixtures/sync.js',
plugins: [
wasm({
sync: ['test/fixtures/sample.wasm']
sync: ['fixtures/sample.wasm']
})
]
});
Expand All @@ -51,10 +86,10 @@ test('imports', async (t) => {
t.plan(1);

const bundle = await rollup({
input: 'test/fixtures/imports.js',
input: 'fixtures/imports.js',
plugins: [
wasm({
sync: ['test/fixtures/imports.wasm']
sync: ['fixtures/imports.wasm']
})
]
});
Expand All @@ -67,7 +102,7 @@ try {
t.plan(2);

const bundle = await rollup({
input: 'test/fixtures/worker.js',
input: 'fixtures/worker.js',
plugins: [wasm()]
});
const code = await getCode(bundle);
Expand Down
10 changes: 10 additions & 0 deletions packages/wasm/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export interface RollupWasmOptions {
* Specifies an array of strings that each represent a WebAssembly file to load synchronously.
*/
sync?: readonly string[];
/**
* The maximum file size for inline files. If a file exceeds this limit, it will be copied to the destination folder and loaded from a separate file at runtime.
* If `maxFileSize` is set to `0` all files will be copied.
* Files specified in `sync` to load synchronously are always inlined, regardless of size.
*/
maxFileSize?: Number;
/**
* A string which will be added in front of filenames when they are not inlined but are copied.
*/
publicPath?: string;
}

/**
Expand Down