Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Lint
run: bun run lint

- name: Build the test utilities
- name: Generate the vendored modules
run: bun run build:vendor

- name: Typecheck
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ src/modules/build

website/.vitepress/cache
docs

src/modules/generated
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This TypeScript package allows you to safely execute **JavaScript AND TypeScript
- **Custom Node Modules**: Custom node modules are mountable.
- **Fetch Client**: Can provide a fetch client to make http(s) calls.
- **Test-Runner**: Includes a test runner and chai based `expect`.
- **Decimal numbers**: Ships decimal.js. The guest imports it, or gets the class `Decimal` as a global.
- **Performance**: Benefit from the lightweight and efficient QuickJS engine.
- **Versatility**: Easily integrate with existing TypeScript projects.
- **Simplicity**: User-friendly API for executing and managing JavaScript and TypeScript code in the sandbox.
Expand Down Expand Up @@ -74,6 +75,7 @@ This lib is based on:
- [quickjs-emscripten-sync](https://github.com/reearth/quickjs-emscripten-sync)
- [memfs](https://github.com/streamich/memfs)
- [Chai](https://www.chaijs.com)
- [decimal.js](https://mikemcl.github.io/decimal.js/)

Tools used:

Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"!**/node_modules",
"!**/.tshy",
"!**/build",
"!**/src/modules/generated",
"!**/cookieconsent2.js",
"!**/cache",
"!**/cookieconsent-init2.js",
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@loop-payments/quickjs",
"version": "3.1.0",
"version": "3.2.0",
"description": "A typescript package to execute JavaScript and TypeScript code in a WebAssembly QuickJS sandbox",
"engines": {
"node": ">=18.0.0"
Expand Down Expand Up @@ -41,7 +41,8 @@
],
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
".": "./src/index.ts",
"./decimal-source": "./src/modules/generated/decimal.js"
}
},
"type": "module",
Expand Down Expand Up @@ -93,6 +94,7 @@
"@types/node": "^25.9.1",
"autocannon": "^8.0.0",
"chai": "^6.2.2",
"decimal.js": "^10.6.0",
"git-cliff": "^2.13.1",
"hono": "^4.12.23",
"jsr": "^0.14.3",
Expand Down Expand Up @@ -132,6 +134,16 @@
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
},
"./decimal-source": {
"import": {
"types": "./dist/esm/modules/generated/decimal.d.ts",
"default": "./dist/esm/modules/generated/decimal.js"
},
"require": {
"types": "./dist/commonjs/modules/generated/decimal.d.ts",
"default": "./dist/commonjs/modules/generated/decimal.js"
}
}
},
"main": "./dist/commonjs/index.js",
Expand Down
7 changes: 6 additions & 1 deletion src/createVirtualFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import bufferModule from './modules/buffer.js'
import eventModule from './modules/events.js'
import fsModule from './modules/fs.js'
import fsPromisesModule from './modules/fs_promises.js'
import decimalModule from './modules/generated/decimal.js'
import moduleModule from './modules/module.js'
import compatibilityEventTarget from './modules/nodeCompatibility/eventTarget.js'
import compatibilityHeaders from './modules/nodeCompatibility/headers.js'
Expand Down Expand Up @@ -68,7 +69,11 @@ export const createVirtualFileSystem = (runtimeOptions: RuntimeOptions = {}) =>
assert: {
'index.js': assertModule,
},

// The package decimal.js is not a node module. The sandbox mounts it
// always, because the host code works with decimal strings.
'decimal.js': {
'index.js': decimalModule,
},
async_hooks: {
'index.js': "throw new Error('module async_hooks not implemented')",
},
Expand Down
1 change: 1 addition & 0 deletions src/sandbox/asyncVersion/prepareAsyncNodeCompatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const prepareAsyncNodeCompatibility = async (vm: QuickJSAsyncContext, san
import '@node_compatibility/request';
import '@node_compatibility/response';
${sandboxOptions.enableTestUtils ? "import 'test'" : ''}
${sandboxOptions.enableDecimalGlobal ? "import Decimal from 'decimal.js'; globalThis.Decimal = Decimal;" : ''}
`,
undefined,
{ type: 'module' },
Expand Down
1 change: 1 addition & 0 deletions src/sandbox/syncVersion/prepareNodeCompatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const prepareNodeCompatibility = (vm: QuickJSContext, sandboxOptions: San
import '@node_compatibility/request';
import '@node_compatibility/response';
${sandboxOptions.enableTestUtils ? "import 'test'" : ''}
${sandboxOptions.enableDecimalGlobal ? "import Decimal from 'decimal.js'; globalThis.Decimal = Decimal;" : ''}
`,
undefined,
{ type: 'module' },
Expand Down
64 changes: 64 additions & 0 deletions src/test/async/decimal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import variant from '@jitl/quickjs-ng-wasmfile-release-asyncify'
import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js'
import type { OkResponse } from '../../types/OkResponse.js'

describe('async - decimal.js', () => {
let runtime: Awaited<ReturnType<typeof loadAsyncQuickJs>>

beforeAll(async () => {
runtime = await loadAsyncQuickJs(variant)
})

const runCode = async (code: string, options: object = {}) => {
return await runtime.runSandboxed(async ({ evalCode }) => {
return await evalCode(code)
}, options)
}

it('can import decimal.js without an option', async () => {
const code = `
import Decimal from 'decimal.js'
export default new Decimal('0.1').plus('0.2').toString()
`

const result = (await runCode(code)) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('0.3')
})

it('does not register the global Decimal by default', async () => {
const code = `
export default typeof Decimal
`

const result = (await runCode(code)) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('undefined')
})

it('registers the global Decimal when enableDecimalGlobal is true', async () => {
const code = `
export default new Decimal('1').dividedBy('3').toFixed(10)
`

const result = (await runCode(code, { enableDecimalGlobal: true })) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('0.3333333333')
})

it('keeps the global Decimal and the imported Decimal identical', async () => {
const code = `
import Decimal from 'decimal.js'
export default Decimal === globalThis.Decimal
`

const result = (await runCode(code, { enableDecimalGlobal: true })) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBeTrue()
})
})
64 changes: 64 additions & 0 deletions src/test/sync/decimal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import variant from '@jitl/quickjs-ng-wasmfile-release-sync'
import { loadQuickJs } from '../../loadQuickJs.js'
import type { OkResponse } from '../../types/OkResponse.js'

describe('sync - decimal.js', () => {
let runtime: Awaited<ReturnType<typeof loadQuickJs>>

beforeAll(async () => {
runtime = await loadQuickJs(variant)
})

const runCode = async (code: string, options: object = {}) => {
return await runtime.runSandboxed(async ({ evalCode }) => {
return await evalCode(code)
}, options)
}

it('can import decimal.js without an option', async () => {
const code = `
import Decimal from 'decimal.js'
export default new Decimal('0.1').plus('0.2').toString()
`

const result = (await runCode(code)) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('0.3')
})

it('does not register the global Decimal by default', async () => {
const code = `
export default typeof Decimal
`

const result = (await runCode(code)) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('undefined')
})

it('registers the global Decimal when enableDecimalGlobal is true', async () => {
const code = `
export default new Decimal('1').dividedBy('3').toFixed(10)
`

const result = (await runCode(code, { enableDecimalGlobal: true })) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBe('0.3333333333')
})

it('keeps the global Decimal and the imported Decimal identical', async () => {
const code = `
import Decimal from 'decimal.js'
export default Decimal === globalThis.Decimal
`

const result = (await runCode(code, { enableDecimalGlobal: true })) as OkResponse

expect(result.ok).toBeTrue()
expect(result.data).toBeTrue()
})
})
8 changes: 8 additions & 0 deletions src/types/RuntimeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export type RuntimeOptions = {
* The custom fetch adapter provided as host function in the QuickJS runtime
*/
fetchAdapter?: typeof fetch
/**
* Register the class Decimal from the package decimal.js as a global.
* The module decimal.js is always available for import. This option only
* makes the import unnecessary.
* The sandbox compiles the source of decimal.js for every execution when
* this option is enabled.
*/
enableDecimalGlobal?: boolean
/**
* Includes test framework
* If enabled, the packages chai and mocha become available
Expand Down
8 changes: 8 additions & 0 deletions src/types/SandboxOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export type SandboxBaseOptions = {
* They are registered global
*/
enableTestUtils?: boolean
/**
* Register the class Decimal from the package decimal.js as a global.
* The module decimal.js is always available for import. This option only
* makes the import unnecessary.
* The sandbox compiles the source of decimal.js for every execution when
* this option is enabled.
*/
enableDecimalGlobal?: boolean
/**
* Per default, the console log inside of QuickJS is passed to the host console log.
* Here, you can customize the handling and provide your own logging methods.
Expand Down
21 changes: 21 additions & 0 deletions vendor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createRequire } from 'node:module'
import { join } from 'node:path'

const testRunnerResult = await Bun.build({
Expand All @@ -13,3 +14,23 @@ for (const res of testRunnerResult.outputs) {

console.info('test lib generated')
}

// The sandbox mounts the decimal.js source into the virtual file system, so the
// source must be a string in the package instead of a file on disk. A file on
// disk is not readable in a worker thread bundle or in the Temporal workflow vm.
const require = createRequire(import.meta.url)
const decimalSource = await Bun.file(require.resolve('decimal.js/decimal.mjs')).text()
const decimalVersion = require('decimal.js/package.json').version

Bun.write(
join('src', 'modules', 'generated', 'decimal.js'),
[
`// Generated by vendor.ts from decimal.js ${decimalVersion}. Do not edit.`,
'/** @type {string} */',
`const decimalJsSource = ${JSON.stringify(decimalSource)}`,
'export default decimalJsSource',
'',
].join('\n'),
)

console.info(`decimal.js ${decimalVersion} module generated`)
16 changes: 16 additions & 0 deletions website/docs/runtime-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ These options apply to both synchronous and asynchronous sandbox instances.
| ----------------- | --------- | ------------------------------------------- |
| `enableTestUtils` | `boolean` | Enables test frameworks (`chai` & `mocha`). |

### 🔢 Decimal Numbers

The sandbox always mounts the package [decimal.js](https://mikemcl.github.io/decimal.js/). The guest code imports it with `import Decimal from 'decimal.js'`. The sandbox only compiles the source when the guest code imports the module.

| Option | Type | Description |
| --------------------- | --------- | ---------------------------------------------------------------------- |
| `enableDecimalGlobal` | `boolean` | Registers the class `Decimal` as a global. The guest needs no import. |

The package also exports the source of decimal.js as a string:

```ts
import decimalJsSource from '@loop-payments/quickjs/decimal-source'
```

Use this export when you embed the QuickJS engine yourself and you must give the same `Decimal` class to the guest. The subpath holds one string constant and imports nothing else, so a bundler does not pull the sandbox and the module memfs into the output. The host also does not read a file at runtime. A bundler, a worker thread, and the Temporal workflow vm all support this.

### 📢 Console Customization

You can override console methods for custom logging behavior.
Expand Down
Loading