Skip to content

Commit

Permalink
feat(wasm): Add target environment configuration (#1132)
Browse files Browse the repository at this point in the history
Currently the Wasm rollup plugin emits code that includes calls to
require builtin node modules (`fs` and `path`) as well as builtin
`fetch` in browser environments.

Even though there is an check before the environment specific code is
executed, the mere presence of `require` for builtin node modules will
cause downstream bundlers like `esbuild` and `webpack` to emit errors
when they statically analyze the output.

This commit adds in a `targetEnv` that configures what code is emitted
to instantiate the Wasm (both inline and separate)

 - `"auto"` will determine the environment at runtime and invoke the correct methods accordingly
 - `"auto-inline"` always inlines the Wasm and will decode it according to the environment
 - `"browser"` omits emitting code that requires node.js builtin modules that may play havoc on downstream bundlers
 - `"node"` omits emitting code that requires `fetch`

"auto" is the default behavior and preserves backwards compatibility.
  • Loading branch information
nickbabcock authored and shellscape committed Apr 29, 2022
1 parent e5db668 commit 50b618b
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 33 deletions.
12 changes: 12 additions & 0 deletions packages/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ Default: (empty string)

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

### `targetEnv`

Type: `"auto" | "browser" | "node"`<br>
Default: `"auto"`

Configures what code is emitted to instantiate the Wasm (both inline and separate):

- `"auto"` will determine the environment at runtime and invoke the correct methods accordingly
- `"auto-inline"` always inlines the Wasm and will decode it according to the environment
- `"browser"` omits emitting code that requires node.js builtin modules that may play havoc on downstream bundlers
- `"node"` omits emitting code that requires `fetch`

## WebAssembly Example

Given the following simple C file:
Expand Down
127 changes: 96 additions & 31 deletions packages/wasm/src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,100 @@
import { TargetEnv } from '../types';

export const HELPERS_ID = '\0wasmHelpers.js';

export const getHelpersModule = () => `
const nodeFilePath = `
var fs = require("fs")
var path = 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))
});
});
`;

const nodeDecode = `
buf = Buffer.from(src, 'base64')
`;

const browserFilePath = `
return _instantiateOrCompile(fetch(filepath), imports, true);
`;

const browserDecode = `
var raw = globalThis.atob(src)
var rawLength = raw.length
buf = new Uint8Array(new ArrayBuffer(rawLength))
for(var i = 0; i < rawLength; i++) {
buf[i] = raw.charCodeAt(i)
}
`;

const autoModule = `
var buf = null
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null
if (filepath && isNode) {
${nodeFilePath}
} else if (filepath) {
${browserFilePath}
}
if (isNode) {
${nodeDecode}
} else {
${browserDecode}
}
`;

const nodeModule = `
var buf = null
if (filepath) {
${nodeFilePath}
}
${nodeDecode}
`;

const browserModule = `
var buf = null
if (filepath) {
${browserFilePath}
}
${browserDecode}
`;

const autoInlineModule = `
var buf = null
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null
if (isNode) {
${nodeDecode}
} else {
${browserDecode}
}
`;

const envModule = (env: TargetEnv) => {
switch (env) {
case 'auto':
return autoModule;
case 'auto-inline':
return autoInlineModule;
case 'browser':
return browserModule;
case 'node':
return nodeModule;
default:
return null;
}
};

export const getHelpersModule = (env: TargetEnv) => `
function _loadWasmModule (sync, filepath, src, imports) {
function _instantiateOrCompile(source, imports, stream) {
var instantiateFunc = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate;
Expand All @@ -13,36 +107,7 @@ function _loadWasmModule (sync, filepath, src, imports) {
}
}
var buf = null
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null
if (filepath && isNode) {
var fs = require("fs")
var path = 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 {
var raw = globalThis.atob(src)
var rawLength = raw.length
buf = new Uint8Array(new ArrayBuffer(rawLength))
for(var i = 0; i < rawLength; i++) {
buf[i] = raw.charCodeAt(i)
}
}
${envModule(env)}
if(sync) {
var mod = new WebAssembly.Module(buf)
Expand Down
8 changes: 6 additions & 2 deletions packages/wasm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { RollupWasmOptions } from '../types';
import { getHelpersModule, HELPERS_ID } from './helper';

export function wasm(options: RollupWasmOptions = {}): Plugin {
const { sync = [], maxFileSize = 14 * 1024, publicPath = '' } = options;
const { sync = [], maxFileSize = 14 * 1024, publicPath = '', targetEnv = 'auto' } = options;

const syncFiles = sync.map((x) => path.resolve(x));
const copies = Object.create(null);
Expand All @@ -27,7 +27,7 @@ export function wasm(options: RollupWasmOptions = {}): Plugin {

load(id) {
if (id === HELPERS_ID) {
return getHelpersModule();
return getHelpersModule(targetEnv);
}

if (!/\.wasm$/.test(id)) {
Expand All @@ -36,6 +36,10 @@ export function wasm(options: RollupWasmOptions = {}): Plugin {

return Promise.all([fs.promises.stat(id), fs.promises.readFile(id)]).then(
([stats, buffer]) => {
if (targetEnv === 'auto-inline') {
return buffer.toString('binary');
}

if ((maxFileSize && stats.size > maxFileSize) || maxFileSize === 0) {
const hash = createHash('sha1').update(buffer).digest('hex').substr(0, 16);

Expand Down
55 changes: 55 additions & 0 deletions packages/wasm/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,58 @@ test('injectHelper', async (t) => {
});
await testBundle(t, bundle);
});

test('target environment auto', async (t) => {
t.plan(5);

const bundle = await rollup({
input: 'fixtures/async.js',
plugins: [wasm({ targetEnv: 'auto' })]
});
const code = await getCode(bundle);
await testBundle(t, bundle);
t.true(code.includes(`require("fs")`));
t.true(code.includes(`require("path")`));
t.true(code.includes(`fetch`));
});

test('target environment auto-inline', async (t) => {
t.plan(6);

const bundle = await rollup({
input: 'fixtures/async.js',
plugins: [wasm({ targetEnv: 'auto-inline' })]
});
const code = await getCode(bundle);
await testBundle(t, bundle);
t.true(!code.includes(`require("fs")`));
t.true(!code.includes(`require("path")`));
t.true(!code.includes(`fetch`));
t.true(code.includes(`if (isNode)`));
});

test('target environment browser', async (t) => {
t.plan(4);

const bundle = await rollup({
input: 'fixtures/async.js',
plugins: [wasm({ targetEnv: 'browser' })]
});
const code = await getCode(bundle);
await testBundle(t, bundle);
t.true(!code.includes(`require("`));
t.true(code.includes(`fetch`));
});

test('target environment node', async (t) => {
t.plan(4);

const bundle = await rollup({
input: 'fixtures/async.js',
plugins: [wasm({ targetEnv: 'node' })]
});
const code = await getCode(bundle);
await testBundle(t, bundle);
t.true(code.includes(`require("`));
t.true(!code.includes(`fetch`));
});
12 changes: 12 additions & 0 deletions packages/wasm/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Plugin } from 'rollup';

/**
* - `"auto"` will determine the environment at runtime and invoke the correct methods accordingly
* - `"auto-inline"` always inlines the Wasm and will decode it according to the environment
* - `"browser"` omits emitting code that requires node.js builtin modules that may play havoc on downstream bundlers
* - `"node"` omits emitting code that requires `fetch`
*/
export type TargetEnv = 'auto' | 'auto-inline' | 'browser' | 'node';

export interface RollupWasmOptions {
/**
* Specifies an array of strings that each represent a WebAssembly file to load synchronously.
Expand All @@ -15,6 +23,10 @@ export interface RollupWasmOptions {
* A string which will be added in front of filenames when they are not inlined but are copied.
*/
publicPath?: string;
/**
* Configures what code is emitted to instantiate the Wasm (both inline and separate)
*/
targetEnv?: TargetEnv;
}

/**
Expand Down

0 comments on commit 50b618b

Please sign in to comment.