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:
- Modules do not have access to native functions.
- Modules must be plain JavaScript.
- Relative imports within one module are not supported (though they can still import other modules).
- Module resolution and finding the root file via a package.json is not supported.
- Only a small subset of Node.js core modules is available, so not every module will work out of the box.
- Module dependencies are not automatically handled (no package manager).
- Modules with multiple exports must be handled manually.
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' }