@rxflex/rom 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +23 -0
- package/README.md +74 -0
- package/build.rs +3 -0
- package/native/lib.rs +7 -0
- package/package.json +49 -0
- package/rom_node_native.node +0 -0
- package/scripts/build-native.mjs +44 -0
- package/scripts/smoke.mjs +26 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +129 -0
- package/src/native.js +40 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rom-node-native"
|
|
3
|
+
description = "Native napi-rs bridge for the ROM Node.js bindings"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
edition = "2024"
|
|
6
|
+
homepage = "https://github.com/Rxflex/rom"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
repository = "https://github.com/Rxflex/rom"
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "rom_node_native"
|
|
12
|
+
path = "native/lib.rs"
|
|
13
|
+
crate-type = ["cdylib"]
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
napi = "3"
|
|
17
|
+
napi-derive = "3"
|
|
18
|
+
rom-runtime = { path = "../../crates/rom-runtime" }
|
|
19
|
+
|
|
20
|
+
[build-dependencies]
|
|
21
|
+
napi-build = "2"
|
|
22
|
+
|
|
23
|
+
[workspace]
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# `@rxflex/rom`
|
|
2
|
+
|
|
3
|
+
Node.js bindings for the ROM browser-like runtime.
|
|
4
|
+
|
|
5
|
+
This package exposes a small JavaScript API on top of ROM:
|
|
6
|
+
|
|
7
|
+
- `eval()`
|
|
8
|
+
- `evalAsync()`
|
|
9
|
+
- `evalJson()`
|
|
10
|
+
- `surfaceSnapshot()`
|
|
11
|
+
- `fingerprintProbe()`
|
|
12
|
+
- `runFingerprintJsHarness()`
|
|
13
|
+
- `fingerprintJsVersion()`
|
|
14
|
+
|
|
15
|
+
It prefers a native `napi-rs` bridge when available and falls back to the ROM CLI bridge otherwise.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @rxflex/rom
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { RomRuntime, hasNativeBinding } from "@rxflex/rom";
|
|
27
|
+
|
|
28
|
+
const runtime = new RomRuntime({
|
|
29
|
+
href: "https://example.test/",
|
|
30
|
+
cors_enabled: false,
|
|
31
|
+
proxy_url: process.env.ROM_PROXY_URL ?? null,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const href = await runtime.evalAsync("(async () => location.href)()");
|
|
35
|
+
const snapshot = await runtime.surfaceSnapshot();
|
|
36
|
+
|
|
37
|
+
console.log("native:", hasNativeBinding());
|
|
38
|
+
console.log(href);
|
|
39
|
+
console.log(snapshot.fetch);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Config keys use the Rust runtime field names, so use snake_case such as `cors_enabled` and `proxy_url`.
|
|
43
|
+
`cors_enabled` is `false` by default.
|
|
44
|
+
|
|
45
|
+
## Optional native build
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run build:native
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`npm pack` and `npm publish` now run the native release build automatically via `prepack`, and the produced `rom_node_native.node` is included in the published tarball for the platform that performed the publish.
|
|
52
|
+
|
|
53
|
+
## Common methods
|
|
54
|
+
|
|
55
|
+
- `eval(script)`
|
|
56
|
+
- `evalAsync(script)`
|
|
57
|
+
- `evalJson(script, { async })`
|
|
58
|
+
- `surfaceSnapshot()`
|
|
59
|
+
- `fingerprintProbe()`
|
|
60
|
+
- `runFingerprintJsHarness()`
|
|
61
|
+
- `fingerprintJsVersion()`
|
|
62
|
+
|
|
63
|
+
## Environment
|
|
64
|
+
|
|
65
|
+
- `ROM_NATIVE_NODE_BINDING`: explicit path to a compiled `.node` addon
|
|
66
|
+
- `ROM_FORCE_CLI_BRIDGE=1`: disable the native path and force CLI fallback
|
|
67
|
+
- `ROM_BRIDGE_BIN`: explicit path to the `rom_bridge` executable
|
|
68
|
+
- `ROM_BRIDGE_CWD`: working directory used by the CLI fallback
|
|
69
|
+
- `ROM_PROXY_URL`: convenience env var you can forward into `proxy_url`
|
|
70
|
+
|
|
71
|
+
## More docs
|
|
72
|
+
|
|
73
|
+
- Root guide: [../../README.md](../../README.md)
|
|
74
|
+
- LLM guide: [../../LLMS.md](../../LLMS.md)
|
package/build.rs
ADDED
package/native/lib.rs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rxflex/rom",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Node.js wrapper for the ROM browser-like runtime",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Rxflex",
|
|
8
|
+
"homepage": "https://github.com/Rxflex/rom",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Rxflex/rom.git",
|
|
12
|
+
"directory": "bindings/gom-node"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Rxflex/rom/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"rom",
|
|
19
|
+
"runtime",
|
|
20
|
+
"browser",
|
|
21
|
+
"web-api",
|
|
22
|
+
"fingerprint"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./src/index.js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepack": "node ./scripts/build-native.mjs --release",
|
|
28
|
+
"build:native": "node ./scripts/build-native.mjs --release",
|
|
29
|
+
"build:native:debug": "node ./scripts/build-native.mjs",
|
|
30
|
+
"smoke": "node ./scripts/smoke.mjs",
|
|
31
|
+
"pack:check": "npm pack --dry-run"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.js"
|
|
35
|
+
},
|
|
36
|
+
"types": "./src/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"README.md",
|
|
39
|
+
"rom_node_native.node",
|
|
40
|
+
"src",
|
|
41
|
+
"scripts",
|
|
42
|
+
"native",
|
|
43
|
+
"Cargo.toml",
|
|
44
|
+
"build.rs"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const profile = process.argv.includes("--release") ? "release" : "debug";
|
|
10
|
+
const targetDir = process.env.CARGO_TARGET_DIR || path.join(packageRoot, "target");
|
|
11
|
+
const artifactDir = path.join(targetDir, profile);
|
|
12
|
+
const outputPath = path.join(packageRoot, "rom_node_native.node");
|
|
13
|
+
|
|
14
|
+
const cargoArgs = ["build", "--manifest-path", "Cargo.toml"];
|
|
15
|
+
if (profile === "release") {
|
|
16
|
+
cargoArgs.push("--release");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const build = spawnSync("cargo", cargoArgs, {
|
|
20
|
+
cwd: packageRoot,
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
env: process.env,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (build.status !== 0) {
|
|
26
|
+
process.exit(build.status ?? 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const artifactName =
|
|
30
|
+
process.platform === "win32"
|
|
31
|
+
? "rom_node_native.dll"
|
|
32
|
+
: process.platform === "darwin"
|
|
33
|
+
? "librom_node_native.dylib"
|
|
34
|
+
: "librom_node_native.so";
|
|
35
|
+
const artifactPath = path.join(artifactDir, artifactName);
|
|
36
|
+
|
|
37
|
+
if (!existsSync(artifactPath)) {
|
|
38
|
+
console.error(`Native addon artifact not found: ${artifactPath}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
43
|
+
copyFileSync(artifactPath, outputPath);
|
|
44
|
+
console.log(`Built ${outputPath}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RomRuntime, hasNativeBinding } from "../src/index.js";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const runtime = new RomRuntime({ href: "https://example.test/" });
|
|
5
|
+
const href = await runtime.evalAsync("(async () => location.href)()");
|
|
6
|
+
const snapshot = await runtime.surfaceSnapshot();
|
|
7
|
+
|
|
8
|
+
if (!hasNativeBinding()) {
|
|
9
|
+
throw new Error("Expected Node native binding to be loaded.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (href !== "https://example.test/") {
|
|
13
|
+
throw new Error(`Unexpected href: ${href}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (snapshot?.globals?.window !== true) {
|
|
17
|
+
throw new Error("Surface snapshot did not expose window.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(JSON.stringify({ native: true, href }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((error) => {
|
|
24
|
+
console.error(error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RuntimeConfig {
|
|
2
|
+
href?: string;
|
|
3
|
+
user_agent?: string;
|
|
4
|
+
app_name?: string;
|
|
5
|
+
platform?: string;
|
|
6
|
+
language?: string;
|
|
7
|
+
languages?: string[];
|
|
8
|
+
hardware_concurrency?: number;
|
|
9
|
+
device_memory?: number;
|
|
10
|
+
webdriver?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export declare class RomRuntime {
|
|
14
|
+
constructor(config?: RuntimeConfig);
|
|
15
|
+
eval(script: string): Promise<string>;
|
|
16
|
+
evalAsync(script: string): Promise<string>;
|
|
17
|
+
evalJson<T = unknown>(script: string, options?: { async?: boolean }): Promise<T>;
|
|
18
|
+
surfaceSnapshot(): Promise<unknown>;
|
|
19
|
+
fingerprintProbe(): Promise<unknown>;
|
|
20
|
+
runFingerprintJsHarness(): Promise<unknown>;
|
|
21
|
+
fingerprintJsVersion(): Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare function createRuntime(config?: RuntimeConfig): RomRuntime;
|
|
25
|
+
export declare function hasNativeBinding(): boolean;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { loadNativeBridge } from "./native.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const repoRoot = path.resolve(__dirname, "..", "..", "..");
|
|
9
|
+
const nativeBridge = loadNativeBridge();
|
|
10
|
+
|
|
11
|
+
function resolveBridgeCommand() {
|
|
12
|
+
if (process.env.ROM_BRIDGE_BIN) {
|
|
13
|
+
return {
|
|
14
|
+
file: process.env.ROM_BRIDGE_BIN,
|
|
15
|
+
args: [],
|
|
16
|
+
cwd: process.env.ROM_BRIDGE_CWD || repoRoot,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
file: "cargo",
|
|
22
|
+
args: ["run", "--quiet", "-p", "rom-runtime", "--bin", "rom_bridge"],
|
|
23
|
+
cwd: process.env.ROM_BRIDGE_CWD || repoRoot,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseBridgeResponse(stdout, stderr, error) {
|
|
28
|
+
const trimmed = stdout.trim();
|
|
29
|
+
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
throw error ?? new Error(`ROM bridge produced no output.\n${stderr}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = JSON.parse(trimmed);
|
|
37
|
+
} catch (parseError) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`ROM bridge returned invalid JSON: ${parseError.message}\n${stdout}\n${stderr}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error || !response.ok) {
|
|
44
|
+
throw new Error(response.error || error?.message || "ROM bridge command failed.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return response.result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runNativeBridge(command, payload) {
|
|
51
|
+
if (!nativeBridge || typeof nativeBridge.executeBridge !== "function") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const responseText = nativeBridge.executeBridge(JSON.stringify({ command, ...payload }));
|
|
56
|
+
return Promise.resolve(parseBridgeResponse(responseText, "", null));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runCliBridge(command, payload) {
|
|
60
|
+
const bridge = resolveBridgeCommand();
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const child = execFile(
|
|
64
|
+
bridge.file,
|
|
65
|
+
bridge.args,
|
|
66
|
+
{
|
|
67
|
+
cwd: bridge.cwd,
|
|
68
|
+
env: process.env,
|
|
69
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
70
|
+
},
|
|
71
|
+
(error, stdout, stderr) => {
|
|
72
|
+
try {
|
|
73
|
+
resolve(parseBridgeResponse(stdout, stderr, error));
|
|
74
|
+
} catch (bridgeError) {
|
|
75
|
+
reject(bridgeError);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
child.stdin.end(JSON.stringify({ command, ...payload }));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runBridge(command, payload) {
|
|
85
|
+
return runNativeBridge(command, payload) ?? runCliBridge(command, payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class RomRuntime {
|
|
89
|
+
constructor(config = {}) {
|
|
90
|
+
this.config = config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
eval(script) {
|
|
94
|
+
return runBridge("eval", { config: this.config, script });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
evalAsync(script) {
|
|
98
|
+
return runBridge("eval-async", { config: this.config, script });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async evalJson(script, { async = true } = {}) {
|
|
102
|
+
const result = async ? await this.evalAsync(script) : await this.eval(script);
|
|
103
|
+
return JSON.parse(result);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
surfaceSnapshot() {
|
|
107
|
+
return runBridge("surface-snapshot", { config: this.config });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fingerprintProbe() {
|
|
111
|
+
return runBridge("fingerprint-probe", { config: this.config });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
runFingerprintJsHarness() {
|
|
115
|
+
return runBridge("fingerprint-js-harness", { config: this.config });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fingerprintJsVersion() {
|
|
119
|
+
return runBridge("fingerprint-js-version", { config: this.config });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createRuntime(config = {}) {
|
|
124
|
+
return new RomRuntime(config);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function hasNativeBinding() {
|
|
128
|
+
return !!nativeBridge;
|
|
129
|
+
}
|
package/src/native.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
function candidatePaths() {
|
|
12
|
+
const candidates = [];
|
|
13
|
+
|
|
14
|
+
if (process.env.ROM_NATIVE_NODE_BINDING) {
|
|
15
|
+
candidates.push(process.env.ROM_NATIVE_NODE_BINDING);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
candidates.push(path.join(packageRoot, "rom_node_native.node"));
|
|
19
|
+
return candidates;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadNativeBridge() {
|
|
23
|
+
if (process.env.ROM_FORCE_CLI_BRIDGE === "1") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const candidate of candidatePaths()) {
|
|
28
|
+
if (!existsSync(candidate)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return require(candidate);
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|