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

vm2 making browser build fail #152

Closed
jfbeats opened this issue May 28, 2022 · 8 comments
Closed

vm2 making browser build fail #152

jfbeats opened this issue May 28, 2022 · 8 comments

Comments

@jfbeats
Copy link

jfbeats commented May 28, 2022

Building using vite and using import { SmartWeave, Contract } from 'redstone-smartweave'

things from vm2 aren't affected by tree shaking so they are getting imported by rollup which fails to compile

 ERROR  [vite-plugin-pwa] 'VMScript' is not exported by __vite-browser-external, imported by node_modules/redstone-smartweave/lib/
esm/core/modules/impl/HandlerExecutorFactory.js
file: C:/Dev/ArweaveWebWallet/node_modules/redstone-smartweave/lib/esm/core/modules/impl/HandlerExecutorFactory.js:9:17
 7: import { Go } from './wasm/go-wasm-imports';
 8: import BigNumber from 'bignumber.js';
 9: import { NodeVM, VMScript } from 'vm2';
                     ^
10: class ContractError extends Error {
11:     constructor(message) {


 ERROR  error during build:                                                                                             10:13:18  
Error: 'VMScript' is not exported by __vite-browser-external, imported by node_modules/redstone-smartweave/lib/esm/core/modules/im
pl/HandlerExecutorFactory.js
    at error (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:198:30)
    at Module.error (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:12708:16)
    at Module.traceVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:13067:29)
    at ModuleScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:11711:39)
    at ChildScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)
    at ClassBodyScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)
    at ChildScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)
    at FunctionScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)
    at ChildScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)
    at TrackingScope.findVariable (C:\Dev\ArweaveWebWallet\node_modules\rollup\dist\shared\rollup.js:6538:38)

Looks like it's always part of the runtime since it's being enabled through a toggle option. I want to put together a custom client like in the example but it seems that you still have to import SmartWeave which will in turn always import HandlerExecutorFactory, so no import path to avoid node specific things.

@twilson63
Copy link
Contributor

FWIW,

when I comment out the following code:

esm/core/modules/impl/HandlerExecutionFactory.js - it works, I don't think there is a shim for NodeVM to run in the browser and for some reason it appears vite/esbuild is unable to remove it.

import { Benchmark, LoggerFactory, MemCache, normalizeContractSource, SmartWeaveGlobal } from '../../..';
import { ContractHandlerApi } from './ContractHandlerApi';
import loader from '@assemblyscript/loader';
import { WasmContractHandlerApi } from './WasmContractHandlerApi';
import { asWasmImports } from './wasm/as-wasm-imports';
import { rustWasmImports } from './wasm/rust-wasm-imports';
import { Go } from './wasm/go-wasm-imports';
import BigNumber from 'bignumber.js';
//import { NodeVM, VMScript } from 'vm2';
class ContractError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ContractError';
    }
}
/**
 * A factory that produces handlers that are compatible with the "current" style of
 * writing SW contracts (i.e. using "handle" function).
 */
export class HandlerExecutorFactory {
    constructor(arweave) {
        this.arweave = arweave;
        this.logger = LoggerFactory.INST.create('HandlerExecutorFactory');
        // TODO: cache compiled wasm binaries here.
        this.cache = new MemCache();
    }
    async create(contractDefinition, evaluationOptions) {
        const swGlobal = new SmartWeaveGlobal(this.arweave, {
            id: contractDefinition.txId,
            owner: contractDefinition.owner
        }, evaluationOptions);
        if (contractDefinition.contractType == 'wasm') {
            this.logger.info('Creating handler for wasm contract', contractDefinition.txId);
            const benchmark = Benchmark.measure();
            let wasmInstance;
            let jsExports = null;
            const wasmResponse = generateResponse(contractDefinition.srcBinary);
            switch (contractDefinition.srcWasmLang) {
                case 'assemblyscript': {
                    const wasmInstanceExports = {
                        exports: null
                    };
                    wasmInstance = await loader.instantiateStreaming(wasmResponse, asWasmImports(swGlobal, wasmInstanceExports));
                    // note: well, exports are required by some imports
                    // - e.g. those that use wasmModule.exports.__newString underneath (like Block.indep_hash)
                    wasmInstanceExports.exports = wasmInstance.exports;
                    break;
                }
                case 'rust': {
                    const wasmInstanceExports = {
                        exports: null,
                        modifiedExports: {
                            wasm_bindgen__convert__closures__invoke2_mut__: null,
                            _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__: null
                        }
                    };
                    /**
                     * wasm-bindgen mangles import function names (adds some random number after "base name")
                     * - that's why we cannot statically build the imports in the SDK.
                     * Instead - we need to first compile the module and check the generated
                     * import function names (all imports from the "__wbindgen_placeholder__" import module).
                     * Having those generated function names - we need to then map them to import functions -
                     * see {@link rustWasmImports}
                     *
                     * That's probably a temporary solution - it would be the best to force the wasm-bindgen
                     * to NOT mangle the import function names - unfortunately that is not currently possible
                     * - https://github.com/rustwasm/wasm-bindgen/issues/1128
                     */
                    const wasmModule = await getWasmModule(wasmResponse, contractDefinition.srcBinary);
                    const moduleImports = WebAssembly.Module.imports(wasmModule);
                    const wbindgenImports = moduleImports
                        .filter((imp) => {
                            return imp.module === '__wbindgen_placeholder__';
                        })
                        .map((imp) => imp.name);
                    const { imports, exports } = rustWasmImports(swGlobal, wbindgenImports, wasmInstanceExports, contractDefinition.metadata.dtor);
                    jsExports = exports;
                    wasmInstance = await WebAssembly.instantiate(wasmModule, imports);
                    wasmInstanceExports.exports = wasmInstance.exports;
                    const moduleExports = Object.keys(wasmInstance.exports);
                    // ... no comments ...
                    moduleExports.forEach((moduleExport) => {
                        if (moduleExport.startsWith('wasm_bindgen__convert__closures__invoke2_mut__')) {
                            wasmInstanceExports.modifiedExports.wasm_bindgen__convert__closures__invoke2_mut__ =
                                wasmInstance.exports[moduleExport];
                        }
                        if (moduleExport.startsWith('_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__')) {
                            wasmInstanceExports.modifiedExports._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ =
                                wasmInstance.exports[moduleExport];
                        }
                    });
                    break;
                }
                case 'go': {
                    const go = new Go(swGlobal);
                    go.importObject.metering = {
                        usegas: function (value) {
                            swGlobal.useGas(value);
                        }
                    };
                    const wasmModule = await getWasmModule(wasmResponse, contractDefinition.srcBinary);
                    wasmInstance = await WebAssembly.instantiate(wasmModule, go.importObject);
                    // nope - DO NOT await here!
                    go.run(wasmInstance);
                    jsExports = go.exports;
                    break;
                }
                default: {
                    throw new Error(`Support for ${contractDefinition.srcWasmLang} not implemented yet.`);
                }
            }
            this.logger.info(`WASM ${contractDefinition.srcWasmLang} handler created in ${benchmark.elapsed()}`);
            return new WasmContractHandlerApi(swGlobal, contractDefinition, jsExports || wasmInstance.exports);
        }
        else {
            this.logger.info('Creating handler for js contract', contractDefinition.txId);
            const normalizedSource = normalizeContractSource(contractDefinition.src, evaluationOptions.useVM2);
            if (!evaluationOptions.allowUnsafeClient) {
                if (normalizedSource.includes('SmartWeave.unsafeClient')) {
                    throw new Error('Using unsafeClient is not allowed by default. Use EvaluationOptions.allowUnsafeClient flag.');
                }
            }
            // if (evaluationOptions.useVM2) {
            //     const vmScript = new VMScript(normalizedSource);
            //     const vm = new NodeVM({
            //         console: 'off',
            //         sandbox: {
            //             SmartWeave: swGlobal,
            //             BigNumber: BigNumber,
            //             logger: this.logger,
            //             ContractError: ContractError,
            //             ContractAssert: function (cond, message) {
            //                 if (!cond)
            //                     throw new ContractError(message);
            //             }
            //         },
            //         compiler: 'javascript',
            //         eval: false,
            //         wasm: false,
            //         allowAsync: true,
            //         wrapper: 'commonjs'
            //     });
            //     return new ContractHandlerApi(swGlobal, vm.run(vmScript), contractDefinition);
            // }
            // else {
            const contractFunction = new Function(normalizedSource);
            const handler = contractFunction(swGlobal, BigNumber, LoggerFactory.INST.create(swGlobal.contract.id));
            return new ContractHandlerApi(swGlobal, handler, contractDefinition);
            //}
        }
    }
}
function generateResponse(wasmBinary) {
    const init = { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/wasm' } };
    return new Response(wasmBinary, init);
}
async function getWasmModule(wasmResponse, binary) {
    if (WebAssembly.compileStreaming) {
        return await WebAssembly.compileStreaming(wasmResponse);
    }
    else {
        return await WebAssembly.compile(binary);
    }
}
//# sourceMappingURL=HandlerExecutorFactory.js.map

@asiaziola
Copy link
Contributor

That's kind of weird as we explicitely tell browser environment to not include vm2 https://github.com/redstone-finance/redstone-smartcontracts/blob/main/package.json#L108

And it works fine in all our apps... eg. here https://github.com/redstone-finance/redstone-smartcontracts-app (tested in webpack-based apps but also in our web bundle-based apps)

Tbh I'm not familiar with vite, so maybe there's an issue with this one? I believe even if you initialize smartweave like this

const smartweave = SmartWeaveWebFactory.memCachedBased(arweave).build();

the issue persists, right? Anyway, I'll try to debug it after the weekend and test it with the custom smartweave client.

@ppedziwiatr
Copy link
Contributor

ppedziwiatr commented May 30, 2022

Gosh, why all those 9827346 different JS build/bundle tools can't work consistently...

Anyway, after googling for ~30seconds, it seems that rollup (which seems to be used by the Vite underneath) does not take the package.json's browser field into account - rollup/rollup#185 (comment)
But the suggested solution (by Rich Harris himself ;-)) should work?

I.e. use this rollup plugin - https://github.com/rollup/plugins/tree/master/packages/node-resolve with probably this setting https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser set to true...

@jfbeats
Copy link
Author

jfbeats commented May 31, 2022

Just checked the source and the exact option you pointed out implement the correct spec to handle the browser field. But I didn't manage to make it work 😅 adding the plugin to either the vue or rollup config still lead to the same error

There is a note in their doc:

This option does not work when a package is using package entrypoints

Maybe the browser field has conflicts with the entrypoints spec

@asiaziola
Copy link
Contributor

Have you tried using an alternative option of adding browser to mainFields and exportConditions arrays?

Setting browser to true should work the same, but maybe...

If it won't work maybe you could use our web bundle in your app (https://github.com/redstone-finance/redstone-smartcontracts#using-web-bundles) and additionally maybe you could share your config and we could open an issue on rollup repo cause it seems a bit odd.

@twilson63
Copy link
Contributor

I got it to work modifying the esbuild bundle.js script

build({
    entryPoints: ['./src/index.ts'],
    minify: false,
    bundle: true,
    outfile: './bundles/web.bundle.js',
    platform: 'browser',
    target: ['es2020', 'chrome58', 'firefox57', 'safari11'],
    format: 'esm',
    globalName: 'rsdk'
  }).catch((e) => {
    console.log(e);
    process.exit(1);
  });

And importing that script via an exports

"exports": {
    "./esm": "bundles/web.bundle.js"
}

Then this works great

import * as SW from "redstone-smartweave/esm";
import { Jumper } from "svelte-loading-spinners";

  const CONTRACT_SRC = "Hljxh8rYyXCb45BYULHb6KhUDnRkxc4ZUaUDCUkOP_w";

  let output = "";
  let submitting = false;

  const arweave = Arweave.init({
    host: "arweave.net",
    protocol: "https",
    port: 443,
  });

  const smartweave = SW.SmartWeaveWebFactory.memCachedBased(arweave).build();

Would you consider a PR that modifies the bundle.js to generate an additional bundle for the browser, one iife and one esm.

Both compile targets will go to the "bundles" folder and then we can add exports to the package.json

"exports": {
  "./esm": "bundles/esm.bundle.js",
  "./web": "bundles/web.bundle.js"
}

The reason I ask, is I am putting together workshops for smart weave contracts using modern ESM tooling with "VITE" and would love to not have to use the umd package.

Thanks for your consideration, I am happy to submit a PR if this is an option.

@asiaziola
Copy link
Contributor

That's great! Although I'm not sure we should mark it as a final solution for vite users, I guess we should find a way to not use a dedicated bundle... But for now, a PR would be a blessing as we are quite busy these days :)

@ppedziwiatr
Copy link
Contributor

closing for now, as we're switching to isolate-vm, which will probably a new set of issues 🤡

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants