diff --git a/packages/wasm/README.md b/packages/wasm/README.md index ce9ff1360..7a6c6393b 100755 --- a/packages/wasm/README.md +++ b/packages/wasm/README.md @@ -36,9 +36,9 @@ export default { input: 'src/index.js', output: { dir: 'output', - format: 'cjs' + format: 'cjs', }, - plugins: [wasm()] + plugins: [wasm()], }; ``` @@ -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`
+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`
+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: @@ -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'], }); ``` diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index 4e0ad4061..a15267bc9 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -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 { @@ -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 + }); + }) + ); } }; } diff --git a/packages/wasm/test/snapshots/test.js.md b/packages/wasm/test/snapshots/test.js.md new file mode 100644 index 000000000..b6a228c2a --- /dev/null +++ b/packages/wasm/test/snapshots/test.js.md @@ -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', + ] diff --git a/packages/wasm/test/snapshots/test.js.snap b/packages/wasm/test/snapshots/test.js.snap new file mode 100644 index 000000000..2c8a3b47a Binary files /dev/null and b/packages/wasm/test/snapshots/test.js.snap differ diff --git a/packages/wasm/test/test.js b/packages/wasm/test/test.js index 849c1c971..6e794df40 100755 --- a/packages/wasm/test/test.js +++ b/packages/wasm/test/test.js @@ -1,5 +1,9 @@ +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'; @@ -7,6 +11,11 @@ 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;`); @@ -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); @@ -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'] }) ] }); @@ -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'] }) ] }); @@ -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); diff --git a/packages/wasm/types/index.d.ts b/packages/wasm/types/index.d.ts index 54a8055d5..a80a9d8f4 100644 --- a/packages/wasm/types/index.d.ts +++ b/packages/wasm/types/index.d.ts @@ -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; } /**