Skip to content

Commit

Permalink
chore: add test runner and prepare doc
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianwessel committed Jul 7, 2024
1 parent 8928f4a commit 1b1424d
Show file tree
Hide file tree
Showing 19 changed files with 474 additions and 28 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ dist
.yarn/install-state.gz
.pnp.*

.tshy
.tshy

src/modules/build
16 changes: 16 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.tshy
.vscode
.env
node_modules
docs
examples
src
vendor
bun*
biome*
*.ts
**/*.test.*

!dist
!package.json
!README.md
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This TypeScript package allows you to safely execute JavaScript code within a We
- **File System**: Can mount a virtual file system
- **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`

## Installation

Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"enabled": true
},
"files": {
"ignore": ["dist", "node_modules", ".tshy"]
"ignore": ["dist", "node_modules", ".tshy", "**/build/**"]
},
"linter": {
"enabled": true,
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# QuickJS Sandbox in Javascript & Typescript
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Examples
1 change: 1 addition & 0 deletions example/run-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Running tests in QuickJS
13 changes: 13 additions & 0 deletions example/run-tests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const code = `
import 'test'
describe('mocha', ()=> {
it('should work',()=>{
expect(true).to.be.true
})
})
const testResult = await runTests();
export default testResult
`
1 change: 1 addition & 0 deletions example/server/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class MyThreadWorker extends ThreadWorker<InputData, ResponseData> {
executionTimeout: 10,
allowFs: true,
allowFetch: true,
enableTestUtils: true,
env: {},
})

Expand Down
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
},
"scripts": {
"start": "bun run --watch example/server/server.ts",
"build": "tshy",
"build": "bun run build:vendor && tshy && bun run build:copy",
"build:vendor": "bun vendor.ts",
"build:copy": "cp -r ./src/modules/build/ ./dist/esm/modules/build && cp -r ./src/modules/build/ ./dist/commonjs/modules/build",
"test": "bun test",
"test:dev": "bun test --watch",
"lint": "bunx @biomejs/biome check",
Expand All @@ -49,17 +51,19 @@
"license": "ISC",
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@hono/swagger-ui": "^0.3.0",
"@hono/zod-openapi": "^0.14.7",
"@types/autocannon": "^7.12.5",
"@types/bun": "^1.1.6",
"@types/node": "^20.14.10",
"autocannon": "^7.15.0",
"chai": "^5.1.1",
"hono": "^4.4.12",
"poolifier-web-worker": "^0.4.13",
"quickjs-emscripten": "^0.29.2",
"sinon": "^18.0.0",
"tshy": "^1.17.0",
"typescript": "^5.5.3",
"poolifier-web-worker": "^0.4.13",
"hono": "^4.4.12",
"@hono/swagger-ui": "^0.3.0",
"@hono/zod-openapi": "^0.14.7"
"typescript": "^5.5.3"
},
"dependencies": {
"@jitl/quickjs-ng-wasmfile-release-sync": "^0.29.2",
Expand Down
27 changes: 27 additions & 0 deletions play.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { quickJS } from './src/quickJS.js'

const code = `export default await new Promise((resolve)=> setTimeout(()=>resolve('DONE'), 20_000))
`

let runtime: Awaited<ReturnType<typeof quickJS>> | undefined

while (true) {
if (!runtime) {
runtime = await quickJS()
}

const { evalCode } = await runtime.createRuntime({
executionTimeout: 2,
allowFs: true,
allowFetch: true,
env: {},
})

const result = await evalCode(code)

console.log(result)

if (!result.ok && result.error.name === 'ExecutionTimeout') {
runtime = undefined
}
}
11 changes: 11 additions & 0 deletions src/getModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Volume } from 'memfs'
import type { JSModuleLoader } from 'quickjs-emscripten-core'

import { readFileSync } from 'node:fs'
import { join } from 'node:path'
// import { fileURLToPath } from 'node:url'
import assertModule from './modules/assert.js'
import fsPromisesModule from './modules/fs-promises.js'
import fsModule from './modules/fs.js'
Expand All @@ -9,6 +12,8 @@ import utilModule from './modules/util.js'
import type { RuntimeOptions } from './types/RuntimeOptions.js'

export const getModuleLoader = (options: RuntimeOptions) => {
//const __dirname = dirname(fileURLToPath(import.meta.url))

const customVol = options?.nodeModules ? Volume.fromNestedJSON(options?.nodeModules) : {}

const modules: Record<string, any> = {
Expand All @@ -31,6 +36,12 @@ export const getModuleLoader = (options: RuntimeOptions) => {
modules['/'].node_modules.fs = { 'index.js': fsModule, promises: { 'index.js': fsPromisesModule } }
}

if (options.enableTestUtils) {
modules['/'].node_modules.test = {
'index.js': readFileSync(join(__dirname, 'modules', 'build', 'test-lib.js')),
}
}

const vol = Volume.fromNestedJSON(modules)

const moduleLoader: JSModuleLoader = (name, _context) => {
Expand Down
7 changes: 6 additions & 1 deletion src/quickJS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ export const quickJS = async (wasmVariantName = '@jitl/quickjs-ng-wasmfile-relea
})

const result = await Promise.race([
evalResult,
(async () => {
console.log('result')
const res = await evalResult
console.log('result', res)
return JSON.parse(JSON.stringify(res))
})(),
new Promise((_resolve, reject) => {
const maxTimeout = getMaxTimeout()
if (maxTimeout) {
Expand Down
6 changes: 6 additions & 0 deletions src/types/RuntimeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export type RuntimeOptions = {
* When enabled, the global fetch will be available
*/
allowFetch?: boolean
/**
* Includes test framework
* If enabled, the packages chai and mocha become available
* They are registered global
*/
enableTestUtils?: 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
85 changes: 66 additions & 19 deletions test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,74 @@
import { getQuickJS } from 'quickjs-emscripten'
import fs from 'node:fs'
import path from 'node:path'

const QuickJS = await getQuickJS()
interface ExportsField {
import?: string | ExportsField
require?: string | ExportsField
[key: string]: any
}

const vm = QuickJS.newContext()
interface PackageJson {
name: string
main?: string
module?: string
exports?: string | ExportsField
[key: string]: any
}

setInterval(() => vm.runtime.executePendingJobs(), 1)
function resolveExportsField(exportsField: ExportsField, subPath: string, isEsm: boolean): string | undefined {
if (typeof exportsField === 'string') {
return subPath ? undefined : exportsField
}
if (isEsm && exportsField.import) {
return resolveExportsField(exportsField.import as ExportsField, subPath, isEsm)
}
if (!isEsm && exportsField.require) {
return resolveExportsField(exportsField.require as ExportsField, subPath, isEsm)
}
if (exportsField[subPath]) {
return isEsm ? exportsField[subPath].import : exportsField[subPath].require
}
return undefined
}

// Evaluate code that uses `readFile`, which returns a promise
const result = vm.evalCode(`(async () => {
function getEntryPointPath(importPath: string): string | undefined {
const [packageName, subPath] = importPath.split(/\/(.+)/)

const x = ()=> new Promise( (resolve, reject) => {
resolve('resolve')
})
const packageJsonPath = path.resolve('node_modules', packageName, 'package.json')
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Cannot find package.json for package: ${packageName}`)
}

const y = ()=>'hello'
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8')
const packageJson: PackageJson = JSON.parse(packageJsonContent)

const content = await y()
return content.toUpperCase()
})()`)
const promiseHandle = vm.unwrapResult(result)
let entryPoint: string | undefined
if (packageJson.exports) {
if (typeof packageJson.exports === 'string') {
entryPoint = subPath ? undefined : packageJson.exports
} else if (typeof packageJson.exports === 'object') {
// Try to resolve ESM entry point first
entryPoint = resolveExportsField(packageJson.exports as ExportsField, subPath, true)
if (!entryPoint) {
// Fallback to CommonJS entry point
entryPoint = resolveExportsField(packageJson.exports as ExportsField, subPath, false)
}
}
}
if (!entryPoint && packageJson.module) {
entryPoint = subPath ? undefined : packageJson.module
}
if (!entryPoint && packageJson.main) {
entryPoint = subPath ? undefined : packageJson.main
}
if (!entryPoint) {
return undefined
}

const resolvedResult = await vm.resolvePromise(promiseHandle)
promiseHandle.dispose()
const resolvedHandle = vm.unwrapResult(resolvedResult)
console.log('Result:', vm.getString(resolvedHandle))
resolvedHandle.dispose()
const finalPath = subPath ? path.join(path.dirname(entryPoint), subPath) : entryPoint
return path.resolve('/', 'node_modules', packageJson.name, finalPath)
}

// Example usage:
console.log(getEntryPointPath('my-package')) // should return /node_modules/my-package/dist/index.mjs
console.log(getEntryPointPath('my-package/feature')) // should return /node_modules/my-package/dist/feature.mjs
15 changes: 15 additions & 0 deletions vendor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { join } from 'node:path'

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

for (const res of testRunnerResult.outputs) {
const content = await res.text()

Bun.write(join('src', 'modules', 'build', 'test-lib.js'), content)

console.info('test lib generated')
}
Loading

0 comments on commit 1b1424d

Please sign in to comment.