chore: migrate Node.js implementation for js-sandbox to isolated-vm (#3973)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
James George
2024-04-19 08:38:46 -07:00
committed by GitHub
parent a079e0f04b
commit 22c6eabd13
52 changed files with 1028 additions and 285 deletions

View File

@@ -1,10 +0,0 @@
export default {
preset: "ts-jest",
testEnvironment: "jsdom",
collectCoverage: true,
setupFilesAfterEnv: ["./jest.setup.ts"],
moduleNameMapper: {
"~/(.*)": "<rootDir>/src/$1",
"^lodash-es$": "lodash",
},
}

View File

@@ -1 +0,0 @@
require("@relmify/jest-fp-ts")

View File

@@ -1,2 +1,2 @@
export { default } from "./dist/node.d.ts"
export * from "./dist/node.d.ts"
export { default } from "./dist/node/index.d.ts"
export * from "./dist/node/index.d.ts"

View File

@@ -30,7 +30,7 @@
"scripts": {
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
"test": "pnpm exec jest",
"test": "vitest run",
"build": "vite build && tsc --emitDeclarationOnly",
"clean": "pnpm tsc --build --clean",
"postinstall": "pnpm run build",
@@ -69,10 +69,18 @@
"eslint-config-prettier": "8.6.0",
"eslint-plugin-prettier": "4.2.1",
"io-ts": "2.2.16",
"jest": "27.5.1",
"prettier": "2.8.4",
"ts-jest": "27.1.5",
"typescript": "4.9.5",
"vite": "5.0.5"
"vite": "5.0.5",
"vitest": "0.34.6"
},
"peerDependencies": {
"isolated-vm": "4.7.2"
},
"peerDependenciesMeta": {
"isolated-vm": {
"optional": true
}
}
}

View File

@@ -0,0 +1,15 @@
// Vitest doesn't work without globals
// Ref: https://github.com/relmify/jest-fp-ts/issues/11
import decodeMatchers from "@relmify/jest-fp-ts/dist/decodeMatchers"
import eitherMatchers from "@relmify/jest-fp-ts/dist/eitherMatchers"
import optionMatchers from "@relmify/jest-fp-ts/dist/optionMatchers"
import theseMatchers from "@relmify/jest-fp-ts/dist/theseMatchers"
import eitherOrTheseMatchers from "@relmify/jest-fp-ts/dist/eitherOrTheseMatchers"
import { expect } from "vitest"
expect.extend(decodeMatchers.matchers)
expect.extend(eitherMatchers.matchers)
expect.extend(optionMatchers.matchers)
expect.extend(theseMatchers.matchers)
expect.extend(eitherOrTheseMatchers.matchers)

View File

@@ -1,8 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runPreRequestScript } from "~/pre-request/node-vm"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runPreRequestScript, runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
describe("Base64 helper functions", () => {

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
@@ -23,7 +24,7 @@ describe("toBe", () => {
return expect(
func(
`
pw.expect(2).toBe(2)
pw.expect(2).toBe(2)
`,
fakeResponse
)()

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,6 +1,6 @@
import "@relmify/jest-fp-ts"
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/pre-request/node-vm"
import { runPreRequestScript } from "~/node"
describe("runPreRequestScript", () => {
test("returns the updated environment properly", () => {

View File

@@ -1,4 +1,6 @@
import { preventCyclicObjects } from "~/utils"
import { preventCyclicObjects } from "~/shared-utils"
import { describe, expect, test } from "vitest"
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {

View File

@@ -1,7 +1,9 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {

View File

@@ -1,2 +0,0 @@
export * from "./pre-request/node-vm"
export * from "./test-runner/node-vm"

View File

@@ -0,0 +1,2 @@
export { runPreRequestScript } from "./pre-request"
export { runTestScript } from "./test-runner"

View File

@@ -0,0 +1,91 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import { createRequire } from "module"
import type ivmT from "isolated-vm"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/shared-utils"
import { getSerializedAPIMethods } from "./utils"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> =>
pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () => {
const jail = context.global
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
const serializedAPIMethods = getSerializedAPIMethods(pw)
jail.setSync("serializedAPIMethods", serializedAPIMethods, {
copy: true,
})
jail.setSync("atob", atob)
jail.setSync("btoa", btoa)
// Methods in the isolate context can't be invoked straightaway
const finalScript = `
const pw = new Proxy(serializedAPIMethods, {
get: (pwObjTarget, pwObjProp) => {
const topLevelEntry = pwObjTarget[pwObjProp]
// "pw.env" set of API methods
if (topLevelEntry && typeof topLevelEntry === "object") {
return new Proxy(topLevelEntry, {
get: (subTarget, subProp) => {
const subLevelProperty = subTarget[subProp]
if (subLevelProperty && subLevelProperty.typeof === "function") {
return (...args) => subLevelProperty.applySync(null, args)
}
},
})
}
}
})
${preRequestScript}
`
// Create a script and compile it
const script = await isolate.compileScript(finalScript)
// Run the pre-request script in the provided context
await script.run(context)
return updatedEnvs
},
(reason) => reason
),
TE.fold(
(error) => TE.left(`Script execution failed: ${error}`),
(result) =>
pipe(
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeError) => `Isolate disposal failed: ${disposeError}`
)
)
)
)
)
)

View File

@@ -0,0 +1,217 @@
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { createRequire } from "module"
import type ivmT from "isolated-vm"
import { TestResponse, TestResult } from "~/types"
import {
getTestRunnerScriptMethods,
preventCyclicObjects,
} from "~/shared-utils"
import { getSerializedAPIMethods } from "./utils"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
): TE.TaskEither<string, TestResult> =>
pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () =>
executeScriptInContext(
testScript,
envs,
response,
isolate,
context
),
(reason) => `Script execution failed: ${reason}`
),
TE.chain((result) =>
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeReason) => `Isolate disposal failed: ${disposeReason}`
)
)
)
)
)
const executeScriptInContext = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
isolate: ivmT.Isolate,
context: ivmT.Context
): Promise<TestResult> => {
return new Promise((resolve, reject) => {
// Parse response object
const responseObjHandle = preventCyclicObjects(response)
if (E.isLeft(responseObjHandle)) {
return reject(`Response parsing failed: ${responseObjHandle.left}`)
}
const jail = context.global
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
const serializedAPIMethods = getSerializedAPIMethods({
...pw,
response: responseObjHandle.right,
})
jail.setSync("serializedAPIMethods", serializedAPIMethods, { copy: true })
jail.setSync("atob", atob)
jail.setSync("btoa", btoa)
jail.setSync("ivm", ivm)
// Methods in the isolate context can't be invoked straightaway
const finalScript = `
const pw = new Proxy(serializedAPIMethods, {
get: (pwObj, pwObjProp) => {
// pw.expect(), pw.env, etc.
const topLevelEntry = pwObj[pwObjProp]
// If the entry exists and is a function
// pw.expect(), pw.test(), etc.
if (topLevelEntry && topLevelEntry.typeof === "function") {
// pw.test() just involves invoking the function via "applySync()"
if (pwObjProp === "test") {
return (...args) => topLevelEntry.applySync(null, args)
}
// pw.expect() returns an object with matcher methods
return (...args) => {
// Invoke "pw.expect()" and get access to the object with matcher methods
const expectFnResult = topLevelEntry.applySync(
null,
args.map((expectVal) => {
if (typeof expectVal === "object") {
if (expectVal === null) {
return null
}
// Only arrays and objects stringified here should be parsed from the "pw.expect()" method definition
// The usecase is that any JSON string supplied should be preserved
// An extra "isStringifiedWithinIsolate" prop is added to indicate it has to be parsed
if (Array.isArray(expectVal)) {
return JSON.stringify({
arr: expectVal,
isStringifiedWithinIsolate: true,
})
}
return JSON.stringify({
...expectVal,
isStringifiedWithinIsolate: true,
})
}
return expectVal
})
)
// Matcher methods that can be chained with "pw.expect()"
// pw.expect().toBe(), etc
if (expectFnResult.typeof === "object") {
// Access the getter that points to the negated matcher methods via "{ accessors: true }"
const matcherMethods = {
not: expectFnResult.getSync("not", { accessors: true }),
}
// Serialize matcher methods for use in the isolate context
const matcherMethodNames = [
"toBe",
"toBeLevel2xx",
"toBeLevel3xx",
"toBeLevel4xx",
"toBeLevel5xx",
"toBeType",
"toHaveLength",
"toInclude",
]
matcherMethodNames.forEach((methodName) => {
matcherMethods[methodName] = expectFnResult.getSync(methodName)
})
return new Proxy(matcherMethods, {
get: (matcherMethodTarget, matcherMethodProp) => {
// pw.expect().not.toBe(), etc
const matcherMethodEntry = matcherMethodTarget[matcherMethodProp]
if (matcherMethodProp === "not") {
return new Proxy(matcherMethodEntry, {
get: (negatedObjTarget, negatedObjprop) => {
// Return the negated matcher method defn that is invoked from the test script
const negatedMatcherMethodDefn = negatedObjTarget.getSync(negatedObjprop)
return negatedMatcherMethodDefn
},
})
}
// Return the matcher method defn that is invoked from the test script
return matcherMethodEntry
},
})
}
}
}
// "pw.env" set of API methods
if (typeof topLevelEntry === "object" && pwObjProp !== "response") {
return new Proxy(topLevelEntry, {
get: (subTarget, subProp) => {
const subLevelProperty = subTarget[subProp]
if (
subLevelProperty &&
subLevelProperty.typeof === "function"
) {
return (...args) => subLevelProperty.applySync(null, args)
}
},
})
}
return topLevelEntry
},
})
${testScript}
`
// Create a script and compile it
const script = isolate.compileScript(finalScript)
// Run the test script in the provided context
script
.then((script) => script.run(context))
.then(() => {
resolve({
tests: testRunStack,
envs: updatedEnvs,
})
})
.catch((error: Error) => {
reject(error)
})
})
}

View File

@@ -0,0 +1,23 @@
import { createRequire } from "module"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
// Helper function to recursively wrap methods in `ivm.Reference`
export const getSerializedAPIMethods = (
namespaceObj: Record<string, unknown>
): Record<string, unknown> => {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(namespaceObj)) {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
result[key] = getSerializedAPIMethods(value as Record<string, unknown>)
} else if (typeof value === "function") {
result[key] = new ivm.Reference(value)
} else {
result[key] = value
}
}
return result
}

View File

@@ -1,38 +0,0 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import { createContext, runInContext } from "vm"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/utils"
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> =>
pipe(
TE.tryCatch(
async () => {
return createContext()
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain((context) =>
TE.tryCatch(
() =>
new Promise((resolve) => {
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
// Expose pw to the context
context.pw = pw
context.atob = atob
context.btoa = btoa
// Run the pre-request script in the provided context
runInContext(preRequestScript, context)
resolve(updatedEnvs)
}),
(reason) => `Script execution failed: ${reason}`
)
)
)

View File

@@ -182,6 +182,36 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
}
}
const getResolvedExpectValue = (expectVal: any) => {
if (typeof expectVal !== "string") {
return expectVal
}
try {
const parsedExpectVal = JSON.parse(expectVal)
// Supplying non-primitive values is not permitted in the `isStringifiedWithinIsolate` property indicates that the object was stringified before executing the script from the isolate context
// This is done to ensure a JSON string supplied as the "expectVal" is not parsed and preserved as is
if (typeof parsedExpectVal === "object") {
if (parsedExpectVal.isStringifiedWithinIsolate !== true) {
return expectVal
}
// For an array, the contents are stored in the `arr` property
if (Array.isArray(parsedExpectVal.arr)) {
return parsedExpectVal.arr
}
delete parsedExpectVal.isStringifiedWithinIsolate
return parsedExpectVal
}
return expectVal
} catch (_) {
return expectVal
}
}
export function preventCyclicObjects(
obj: Record<string, any>
): E.Left<string> | E.Right<Record<string, any>> {
@@ -215,15 +245,18 @@ export const createExpectation = (
) => {
const result: Record<string, unknown> = {}
// Non-primitive values supplied are stringified in the isolate context
const resolvedExpectVal = getResolvedExpectValue(expectVal)
const toBeFn = (expectedVal: any) => {
let assertion = expectVal === expectedVal
let assertion = resolvedExpectVal === expectedVal
if (negated) {
assertion = !assertion
}
const status = assertion ? "pass" : "fail"
const message = `Expected '${expectVal}' to${
const message = `Expected '${resolvedExpectVal}' to${
negated ? " not" : ""
} be '${expectedVal}'`
@@ -240,7 +273,7 @@ export const createExpectation = (
rangeStart: number,
rangeEnd: number
) => {
const parsedExpectVal = parseInt(expectVal)
const parsedExpectVal = parseInt(resolvedExpectVal)
if (!Number.isNaN(parsedExpectVal)) {
let assertion =
@@ -260,7 +293,7 @@ export const createExpectation = (
message,
})
} else {
const message = `Expected ${level}-level status but could not parse value '${expectVal}'`
const message = `Expected ${level}-level status but could not parse value '${resolvedExpectVal}'`
currTestStack[currTestStack.length - 1].expectResults.push({
status: "error",
message,
@@ -288,14 +321,14 @@ export const createExpectation = (
"function",
].includes(expectedType)
) {
let assertion = typeof expectVal === expectedType
let assertion = typeof resolvedExpectVal === expectedType
if (negated) {
assertion = !assertion
}
const status = assertion ? "pass" : "fail"
const message = `Expected '${expectVal}' to${
const message = `Expected '${resolvedExpectVal}' to${
negated ? " not" : ""
} be type '${expectedType}'`
@@ -316,7 +349,12 @@ export const createExpectation = (
}
const toHaveLengthFn = (expectedLength: any) => {
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
if (
!(
Array.isArray(resolvedExpectVal) ||
typeof resolvedExpectVal === "string"
)
) {
const message =
"Expected toHaveLength to be called for an array or string"
currTestStack[currTestStack.length - 1].expectResults.push({
@@ -328,7 +366,7 @@ export const createExpectation = (
}
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
let assertion = expectVal.length === expectedLength
let assertion = resolvedExpectVal.length === expectedLength
if (negated) {
assertion = !assertion
@@ -355,7 +393,12 @@ export const createExpectation = (
}
const toIncludeFn = (needle: any) => {
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
if (
!(
Array.isArray(resolvedExpectVal) ||
typeof resolvedExpectVal === "string"
)
) {
const message = "Expected toInclude to be called for an array or string"
currTestStack[currTestStack.length - 1].expectResults.push({
status: "error",
@@ -382,13 +425,13 @@ export const createExpectation = (
return undefined
}
let assertion = expectVal.includes(needle)
let assertion = resolvedExpectVal.includes(needle)
if (negated) {
assertion = !assertion
}
const expectValPretty = JSON.stringify(expectVal)
const expectValPretty = JSON.stringify(resolvedExpectVal)
const needlePretty = JSON.stringify(needle)
const status = assertion ? "pass" : "fail"
const message = `Expected ${expectValPretty} to${

View File

@@ -1,57 +0,0 @@
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { createContext, runInContext } from "vm"
import { TestResponse, TestResult } from "~/types"
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
): TE.TaskEither<string, TestResult> =>
pipe(
TE.tryCatch(
async () => {
return createContext()
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain((context) =>
TE.tryCatch(
() => executeScriptInContext(testScript, envs, response, context),
(reason) => `Script execution failed: ${reason}`
)
)
)
const executeScriptInContext = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
context: any
): Promise<TestResult> => {
return new Promise((resolve, reject) => {
// Parse response object
const responseObjHandle = preventCyclicObjects(response)
if (E.isLeft(responseObjHandle)) {
return reject(`Response parsing failed: ${responseObjHandle.left}`)
}
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
// Expose pw to the context
context.pw = { ...pw, response: responseObjHandle.right }
context.atob = atob
context.btoa = btoa
// Run the test script in the provided context
runInContext(testScript, context)
resolve({
tests: testRunStack,
envs: updatedEnvs,
})
})
}

View File

@@ -1,2 +0,0 @@
export * from "./pre-request/web-worker"
export * from "./test-runner/web-worker"

View File

@@ -0,0 +1,2 @@
export { runPreRequestScript } from "./pre-request"
export { runTestScript } from "./test-runner"

View File

@@ -1,7 +1,7 @@
import * as TE from "fp-ts/TaskEither"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/utils"
import { getPreRequestScriptMethods } from "~/shared-utils"
const executeScriptInContext = (
preRequestScript: string,

View File

@@ -2,7 +2,10 @@ import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
import {
getTestRunnerScriptMethods,
preventCyclicObjects,
} from "~/shared-utils"
const executeScriptInContext = (
testScript: string,

View File

@@ -7,16 +7,20 @@ export default defineConfig({
emptyOutDir: true,
lib: {
entry: {
web: "./src/web.ts",
node: "./src/node.ts",
web: "./src/web/index.ts",
node: "./src/node/index.ts",
},
name: "js-sandbox",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["vm"],
external: ["module"],
},
},
test: {
environment: "node",
setupFiles: ["./setupFiles.ts"],
},
resolve: {
alias: {
"~": resolve(__dirname, "./src"),

View File

@@ -1,2 +1,2 @@
export { default } from "./dist/web.d.ts"
export * from "./dist/web.d.ts"
export { default } from "./dist/web/index.d.ts"
export * from "./dist/web/index.d.ts"