Skip to content

Commit

Permalink
feat: Add minimal polyfills for node.js modules
Browse files Browse the repository at this point in the history
  • Loading branch information
guilgaly authored and notdryft committed Nov 7, 2024
1 parent 2280087 commit 01028a3
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 3 deletions.
2 changes: 2 additions & 0 deletions js-simulation/package-lock.json

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

1 change: 1 addition & 0 deletions js/cli/.npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*
!/target/**
!/polyfills/**
!/package.json
2 changes: 2 additions & 0 deletions js/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"main": "target/index.js",
"types": "target/index.d.ts",
"dependencies": {
"@jspm/core": "2.1.0",
"archiver": "7.0.1",
"axios": "1.7.7",
"commander": "12.1.0",
"decompress": "4.2.1",
"esbuild": "0.24.0",
"esbuild-plugin-tsc": "0.4.0",
"import-meta-resolve": "4.1.0",
"readline-sync": "1.4.10"
},
"devDependencies": {
Expand Down
87 changes: 87 additions & 0 deletions js/cli/polyfills/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Buffer } from "buffer"

// limit of Crypto.getRandomValues()
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const MAX_BYTES = 65536;

// Node supports requesting up to this number of bytes
// https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48
const MAX_UINT32 = 4294967295;

const JavaCrypto = Java.type("io.gatling.js.polyfills.Crypto");

export const randomBytes = (size, cb) => {
// Node supports requesting up to this number of bytes
// https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48
if (size > MAX_UINT32) {
throw new RangeError('requested too many random bytes');
}
const bytes = Buffer.from(JavaCrypto.randomBytes(size));
if (typeof cb === 'function') {
return process.nextTick(function () {
cb(null, bytes);
})
}
return bytes;
};
export const rng = randomBytes;
export const pseudoRandomBytes = randomBytes;
export const prng = randomBytes;
export const getRandomValues = (values) => {
const byteView = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
const bytes = randomBytes(byteView.length);
for (let i = 0; i < byteView.length; i++) {
// The range of Math.random() is [0, 1) and the ToUint8 abstract operation rounds down
byteView[i] = bytes[i];
}
return values;
};
export const randomUUID = () => JavaCrypto.randomUUID();

// export const Cipher = crypto.Cipher;
// export const Cipheriv = crypto.Cipheriv;
// export const Decipher = crypto.Decipher;
// export const Decipheriv = crypto.Decipheriv;
// export const DiffieHellman = crypto.DiffieHellman;
// export const DiffieHellmanGroup = crypto.DiffieHellmanGroup;
// export const Hash = crypto.Hash;
// export const Hmac = crypto.Hmac;
// export const Sign = crypto.Sign;
// export const Verify = crypto.Verify;
// export const constants = crypto.constants;
// export const createCipher = crypto.createCipher;
// export const createCipheriv = crypto.createCipheriv;
// export const createCredentials = crypto.createCredentials;
// export const createDecipher = crypto.createDecipher;
// export const createDecipheriv = crypto.createDecipheriv;
// export const createDiffieHellman = crypto.createDiffieHellman;
// export const createDiffieHellmanGroup = crypto.createDiffieHellmanGroup;
// export const createECDH = crypto.createECDH;
// export const createHash = crypto.createHash;
// export const createHmac = crypto.createHmac;
// export const createSign = crypto.createSign;
// export const createVerify = crypto.createVerify;
// export const getCiphers = crypto.getCiphers;
// export const getDiffieHellman = crypto.getDiffieHellman;
// export const getHashes = crypto.getHashes;
// export const listCiphers = crypto.listCiphers;
// export const pbkdf2 = crypto.pbkdf2;
// export const pbkdf2Sync = crypto.pbkdf2Sync;
// export const privateDecrypt = crypto.privateDecrypt;
// export const privateEncrypt = crypto.privateEncrypt;
// export const publicDecrypt = crypto.publicDecrypt;
// export const publicEncrypt = crypto.publicEncrypt;
// export const randomFill = crypto.randomFill;
// export const randomFillSync = crypto.randomFillSync;

const crypto = {
randomBytes,
rng,
pseudoRandomBytes,
prng,
getRandomValues,
randomUUID,
};
crypto.webcrypto = crypto;
globalThis.crypto = crypto;
export default crypto;
13 changes: 13 additions & 0 deletions js/cli/polyfills/global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const global = globalThis;
export { global };

export { Buffer } from "buffer";

// These values are used by some of the JSPM polyfills
export const navigator = {
deviceMemory: 8, // Highest allowed value
hardwareConcurrency: 8, // Fairly common default
language: "en-US", // Most common default
};

export * as crypto from "crypto"
8 changes: 6 additions & 2 deletions js/cli/src/bundle.ts → js/cli/src/bundle/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as esbuild from "esbuild";
import esbuildPluginTsc from "esbuild-plugin-tsc";

import { SimulationFile } from "./simulations";
import { logger } from "./log";
import { polyfill } from "./polyfill";
import { SimulationFile } from "../simulations";
import { logger } from "../log";

export interface BundleOptions {
sourcesFolder: string;
Expand All @@ -20,12 +21,15 @@ export const bundle = async (options: BundleOptions): Promise<void> => {
const contents = options.simulations.map((s) => `export { default as "${s.name}" } from "./${s.path}";`).join("\n");

const plugins = options.typescript ? [esbuildPluginTsc({ force: true })] : [];
plugins.push(polyfill());
await esbuild.build({
stdin: {
contents,
resolveDir: options.sourcesFolder
},
outfile: options.bundleFile,
platform: "neutral",
mainFields: ["main", "module"],
bundle: true,
minify: false,
sourcemap: true,
Expand Down
91 changes: 91 additions & 0 deletions js/cli/src/bundle/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Plugin } from "esbuild";
import { fileURLToPath, pathToFileURL } from "url";
import { resolve, dirname } from "path";

// This is largely inspired by https://github.com/cyco130/esbuild-plugin-polyfill-node

export const polyfill = (): Plugin => ({
name: "gatling-js-polyfill",
setup: async (build) => {
// modules
const jspmResolved = await resolveImport(`@jspm/core/nodelibs/fs`);
build.onResolve({ filter: polyfillsFilter }, async ({ path }) => {
const [, , moduleName] = path.match(polyfillsFilter)!;
const resolved = customPolyfills.find((name) => name === moduleName)
? resolve(dirname(__filename), `../../polyfills/${moduleName}.js`)
: resolve(jspmResolved, `../../browser/${moduleName}.js`);
return { path: resolved };
});

// Globals
build.initialOptions.inject = build.initialOptions.inject || [];
const injectGlobal = (name: string) =>
(build.initialOptions.inject as string[]).push(resolve(dirname(__filename), `../../polyfills/${name}.js`));
injectGlobal("global");
}
});

const customPolyfills = ["crypto"];

const jspmPolyfills = ["buffer", "path", "string_decoder"];

// Other available jspm-core modules:
// "_stream_duplex"
// "_stream_passthrough"
// "_stream_readable"
// "_stream_transform"
// "_stream_writable"
// "assert"
// "assert/strict"
// "async_hooks"
// "child_process"
// "cluster"
// "console"
// "constants"
// "crypto"
// "dgram"
// "diagnostics_channel"
// "dns"
// "domain"
// "events"
// "fs"
// "fs/promises"
// "http"
// "http2"
// "https"
// "module"
// "net"
// "os"
// "perf_hooks"
// "process"
// "punycode"
// "querystring"
// "readline"
// "repl"
// "stream"
// "sys"
// "timers"
// "timers/promises"
// "tls"
// "tty"
// "url"
// "util"
// "v8"
// "vm"
// "wasi"
// "worker_threads"
// "zlib"

const polyfillsFilter = new RegExp(`^(node:)?(${jspmPolyfills.concat(customPolyfills).join("|")})$`);

let importMetaResolve: (specifier: string, parent: string) => string;

const importMetaUrl = pathToFileURL(__filename).href;

const resolveImport = async (specifier: string) => {
if (!importMetaResolve) {
importMetaResolve = (await import("import-meta-resolve")).resolve;
}
const resolved = importMetaResolve(specifier, importMetaUrl);
return fileURLToPath(resolved);
};
Loading

0 comments on commit 01028a3

Please sign in to comment.