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.

### `limit`
bminixhofer marked this conversation as resolved.
Show resolved Hide resolved

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

The file size limit 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 `limit` 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
133 changes: 111 additions & 22 deletions packages/wasm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,83 @@
import { readFile } from 'fs';
import { resolve } from 'path';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { createHash } from 'crypto';

import { assert } from 'console';

import { Plugin } from 'rollup';

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

const makeDir = require('make-dir');

const fsStatPromise = promisify(fs.stat);
const fsReadFilePromise = promisify(fs.readFile);
bminixhofer marked this conversation as resolved.
Show resolved Hide resolved

export function wasm(options: RollupWasmOptions = {}): Plugin {
const syncFiles = (options.sync || []).map((x) => resolve(x));
const { sync = [], limit = 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([fsStatPromise(id), fsReadFilePromise(id)]).then(([stats, buffer]) => {
if ((limit && stats.size > limit) || limit === 0) {
const hash = createHash('sha1')
.update(buffer)
.digest('hex')
.substr(0, 16);

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

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,26 +89,66 @@ 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 filepath = copies[id] ? `'${copies[id]}'` : null;
let src;

if (filepath === null) {
assert(!isSync, 'non-inlined files can not be `sync`.');
bminixhofer marked this conversation as resolved.
Show resolved Hide resolved
src = Buffer.from(code, 'binary').toString('base64');
src = `'${src}'`;
} else {
src = null;
}

return `export default function(imports){return _loadWasmModule(${+isSync}, ${filepath}, ${src}, imports)}`;
}
return null;
},
generateBundle: async function write(outputOptions) {
// can't generate anything if we can't determine the output base
if (!outputOptions.dir && !outputOptions.file) {
return;
}

const base = outputOptions.dir || path.dirname(outputOptions.file);

await makeDir(base);

await Promise.all(
Object.keys(copies).map(async (name) => {
const output = copies[name];
// Create a nested directory if the fileName pattern contains
// a directory structure
const outputDirectory = path.join(base, path.dirname(output));
await makeDir(outputDirectory);
return copy(name, path.join(base, output));
bminixhofer marked this conversation as resolved.
Show resolved Hide resolved
})
);
}
};
}

function copy(src, dest) {
return new Promise((resolve, reject) => {
const read = fs.createReadStream(src);
read.on('error', reject);
const write = fs.createWriteStream(dest);
write.on('error', reject);
write.on('finish', resolve);
read.pipe(write);
});
}

export default wasm;
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({
limit: 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 file size limit 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 `limit` is set to `0` all files will be copied.
* Files specified in `sync` to load synchronously are always inlined, regardless of size.
*/
limit?: Number;
/**
* A string which will be added in front of filenames when they are not inlined but are copied.
*/
publicPath?: string;
}

/**
Expand Down