Skip to the content.

This library allows the use of custom Node.js packages within the QuickJS runtime. Node modules are loaded into a virtual file system, which is then exposed to the client system. This ensures complete isolation from the underlying host system.

Limitations

While custom Node.js modules can be used like regular modules within the QuickJS runtime, there are some limitations:

Preparing a Custom Module

To work around some of these limitations, the custom Node.js module should be a single ESM file, including all dependencies (except Node.js core modules).

Tools like Bun and esbuild make this process easy for many modules.

Bun

Bun is used in the development of this library. Working examples are available in the repository.

To create a bundled module as a single file with dependencies, you need an entry file:

// entry.ts

// Import the module or functionality to bundle
import { expect } from 'chai';

// Optional custom code

export { expect };

Here, only the expect part gets bundled, but you can also use export * from 'chai' to include everything.

A configuration file is recommended to repeat the build step easily:

// build.ts
const testRunnerResult = await Bun.build({
  entrypoints: ['./entry.ts'],
  format: 'esm',
  minify: true,
  outdir: './vendor'
});

Build the custom module file with bun ./build.ts. The generated file should be in ./vendor.

For more information, visit the official Bun website.

Esbuild

Using esbuild works similarly to Bun. An entry file is required, and a config file is recommended.

The entry file will be the same as for Bun. The config file needs to be adapted (with a .mjs extension):

// build.mjs
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['./entry.ts'],
  bundle: true,
  outfile: './vendor/my-package.js',
});

Build the custom module file with node ./build.mjs. The generated file should be in ./vendor.

For more information, visit the official esbuild website.

Using a Custom Module

A virtual file system is used to provide Node.js modules to the client system. To provide custom modules, a nested structure is used.

The root key is the package name, and the child key represents the index file, which should be index.js by default. The custom module itself is provided as a raw string.

Example:

import { join } from 'node:path';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { quickJS } from '@sebastianwessel/quickjs';

// General setup, such as loading and initializing the QuickJS WASM
// This is a resource-intensive job and should be done only once if possible
const { createRuntime } = await quickJS();

const __dirname = dirname(fileURLToPath(import.meta.url));
const customModuleHostLocation = join(__dirname, './custom.js');

// Create a runtime instance each time JS code should be executed
const { evalCode } = await createRuntime({
  nodeModules: {
    // Module name
    'custom-module': {
      // Key must be index.js, value is the file content of the module
      'index.js': await Bun.file(customModuleHostLocation).text(),
    },
  },
});

const result = await evalCode(`
import { customFn } from 'custom-module';

const result = customFn();

console.log(result);

export default result;
`);

console.log(result); // { ok: true, data: 'Hello from the custom module' }