@rxflex/rom 0.0.1 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "rom-node-native"
3
3
  description = "Native napi-rs bridge for the ROM Node.js bindings"
4
- version = "0.0.1"
4
+ version = "0.0.6"
5
5
  edition = "2024"
6
6
  homepage = "https://github.com/Rxflex/rom"
7
7
  license = "MIT"
@@ -16,6 +16,8 @@ crate-type = ["cdylib"]
16
16
  napi = "3"
17
17
  napi-derive = "3"
18
18
  rom-runtime = { path = "../../crates/rom-runtime" }
19
+ serde = { version = "1.0", features = ["derive"] }
20
+ serde_json = "1.0"
19
21
 
20
22
  [build-dependencies]
21
23
  napi-build = "2"
package/README.md CHANGED
@@ -27,6 +27,7 @@ import { RomRuntime, hasNativeBinding } from "@rxflex/rom";
27
27
 
28
28
  const runtime = new RomRuntime({
29
29
  href: "https://example.test/",
30
+ referrer: "https://referrer.example/",
30
31
  cors_enabled: false,
31
32
  proxy_url: process.env.ROM_PROXY_URL ?? null,
32
33
  });
@@ -37,10 +38,14 @@ const snapshot = await runtime.surfaceSnapshot();
37
38
  console.log("native:", hasNativeBinding());
38
39
  console.log(href);
39
40
  console.log(snapshot.fetch);
41
+
42
+ await runtime.evalAsync("(async () => { globalThis.__romValue = 42; return 'ok'; })()");
43
+ console.log(await runtime.evalAsync("(async () => String(globalThis.__romValue))()"));
40
44
  ```
41
45
 
42
46
  Config keys use the Rust runtime field names, so use snake_case such as `cors_enabled` and `proxy_url`.
43
47
  `cors_enabled` is `false` by default.
48
+ When the native addon is loaded, one `RomRuntime` instance keeps JS globals alive across multiple `eval()` and `evalAsync()` calls.
44
49
 
45
50
  ## Optional native build
46
51
 
@@ -48,7 +53,15 @@ Config keys use the Rust runtime field names, so use snake_case such as `cors_en
48
53
  npm run build:native
49
54
  ```
50
55
 
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.
56
+ Local `npm pack` and `npm publish` still build the native addon for the current platform via `prepack`.
57
+ Tagged GitHub releases assemble multi-platform prebuilds and publish a single npm package that includes:
58
+
59
+ - `linux-x64-gnu`
60
+ - `win32-x64-msvc`
61
+ - `darwin-x64`
62
+ - `darwin-arm64`
63
+
64
+ At runtime the loader picks the matching binary from `prebuilds/<platform>/rom_node_native.node`.
52
65
 
53
66
  ## Common methods
54
67
 
package/native/lib.rs CHANGED
@@ -1,7 +1,93 @@
1
- use napi::Result;
1
+ use napi::{Error, Result};
2
2
  use napi_derive::napi;
3
+ use rom_runtime::{RomRuntime, RuntimeConfig};
4
+ use serde::Serialize;
5
+ use serde_json::Value;
6
+
7
+ fn parse_config(config_json: String) -> Result<RuntimeConfig> {
8
+ serde_json::from_str(&config_json)
9
+ .map_err(|error| Error::from_reason(format!("Invalid ROM config JSON: {error}")))
10
+ }
11
+
12
+ fn to_json<T>(value: &T) -> Result<String>
13
+ where
14
+ T: Serialize,
15
+ {
16
+ serde_json::to_string(value)
17
+ .map_err(|error| Error::from_reason(format!("Failed to serialize ROM value: {error}")))
18
+ }
19
+
20
+ fn map_runtime_error<T>(result: rom_runtime::Result<T>) -> Result<T> {
21
+ result.map_err(|error| Error::from_reason(error.to_string()))
22
+ }
3
23
 
4
24
  #[napi]
5
25
  pub fn execute_bridge(request_json: String) -> Result<String> {
6
26
  Ok(rom_runtime::execute_bridge_request_json(&request_json))
7
27
  }
28
+
29
+ #[napi]
30
+ pub struct NativeRomRuntime {
31
+ runtime: RomRuntime,
32
+ }
33
+
34
+ #[napi]
35
+ impl NativeRomRuntime {
36
+ #[napi(constructor)]
37
+ pub fn new(config_json: String) -> Result<Self> {
38
+ Ok(Self {
39
+ runtime: map_runtime_error(RomRuntime::new(parse_config(config_json)?))?,
40
+ })
41
+ }
42
+
43
+ #[napi(js_name = "eval")]
44
+ pub fn eval(&self, script: String) -> Result<String> {
45
+ map_runtime_error(self.runtime.eval_as_string(&script))
46
+ .map(|value| value.to_owned())
47
+ }
48
+
49
+ #[napi(js_name = "evalAsync")]
50
+ pub fn eval_async(&self, script: String) -> Result<String> {
51
+ map_runtime_error(self.runtime.eval_async_as_string(&script))
52
+ .map(|value| value.to_owned())
53
+ }
54
+
55
+ #[napi(js_name = "surfaceSnapshotJson")]
56
+ pub fn surface_snapshot_json(&self) -> Result<String> {
57
+ to_json(&map_runtime_error(self.runtime.surface_snapshot())?)
58
+ }
59
+
60
+ #[napi(js_name = "fingerprintProbeJson")]
61
+ pub fn fingerprint_probe_json(&self) -> Result<String> {
62
+ to_json(&map_runtime_error(self.runtime.fingerprint_probe())?)
63
+ }
64
+
65
+ #[napi(js_name = "fingerprintJsHarnessJson")]
66
+ pub fn fingerprint_js_harness_json(&self) -> Result<String> {
67
+ to_json(&map_runtime_error(self.runtime.run_fingerprintjs_harness())?)
68
+ }
69
+
70
+ #[napi(js_name = "fingerprintJsVersion")]
71
+ pub fn fingerprint_js_version(&self) -> Result<String> {
72
+ Ok(self.runtime.fingerprintjs_version().to_owned())
73
+ }
74
+
75
+ #[napi(js_name = "exportCookieStore")]
76
+ pub fn export_cookie_store(&self) -> Result<String> {
77
+ map_runtime_error(self.runtime.export_cookie_store())
78
+ .map(|value| value.to_owned())
79
+ }
80
+
81
+ #[napi(js_name = "evalJson")]
82
+ pub fn eval_json(&self, script: String, asynchronous: bool) -> Result<String> {
83
+ let value = if asynchronous {
84
+ map_runtime_error(self.runtime.eval_async_as_string(&script))?
85
+ } else {
86
+ map_runtime_error(self.runtime.eval_as_string(&script))?
87
+ };
88
+
89
+ let parsed: Value = serde_json::from_str(&value)
90
+ .map_err(|error| Error::from_reason(format!("ROM eval did not return JSON: {error}")))?;
91
+ to_json(&parsed)
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxflex/rom",
3
- "version": "0.0.1",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "description": "Node.js wrapper for the ROM browser-like runtime",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "prepack": "node ./scripts/build-native.mjs --release",
28
28
  "build:native": "node ./scripts/build-native.mjs --release",
29
29
  "build:native:debug": "node ./scripts/build-native.mjs",
30
+ "stage:prebuilds": "node ./scripts/stage-prebuilds.mjs",
30
31
  "smoke": "node ./scripts/smoke.mjs",
31
32
  "pack:check": "npm pack --dry-run"
32
33
  },
@@ -36,7 +37,7 @@
36
37
  "types": "./src/index.d.ts",
37
38
  "files": [
38
39
  "README.md",
39
- "rom_node_native.node",
40
+ "prebuilds",
40
41
  "src",
41
42
  "scripts",
42
43
  "native",
@@ -2,14 +2,18 @@ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { spawnSync } from "node:child_process";
5
+ import { detectNativePrebuildId } from "../src/platform.js";
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  const packageRoot = path.resolve(__dirname, "..");
9
- const profile = process.argv.includes("--release") ? "release" : "debug";
10
+ const args = process.argv.slice(2);
11
+ const profile = args.includes("--release") ? "release" : "debug";
10
12
  const targetDir = process.env.CARGO_TARGET_DIR || path.join(packageRoot, "target");
11
13
  const artifactDir = path.join(targetDir, profile);
12
- const outputPath = path.join(packageRoot, "rom_node_native.node");
14
+ const outputPath =
15
+ readOptionValue(args, "--output") ||
16
+ defaultOutputPath();
13
17
 
14
18
  const cargoArgs = ["build", "--manifest-path", "Cargo.toml"];
15
19
  if (profile === "release") {
@@ -42,3 +46,27 @@ if (!existsSync(artifactPath)) {
42
46
  mkdirSync(path.dirname(outputPath), { recursive: true });
43
47
  copyFileSync(artifactPath, outputPath);
44
48
  console.log(`Built ${outputPath}`);
49
+
50
+ function readOptionValue(argv, optionName) {
51
+ const index = argv.indexOf(optionName);
52
+ if (index === -1) {
53
+ return null;
54
+ }
55
+
56
+ const value = argv[index + 1];
57
+ if (!value || value.startsWith("--")) {
58
+ console.error(`Missing value for ${optionName}`);
59
+ process.exit(1);
60
+ }
61
+
62
+ return path.resolve(packageRoot, value);
63
+ }
64
+
65
+ function defaultOutputPath() {
66
+ const prebuildId = detectNativePrebuildId();
67
+ if (prebuildId === null) {
68
+ return path.join(packageRoot, "rom_node_native.node");
69
+ }
70
+
71
+ return path.join(packageRoot, "prebuilds", prebuildId, "rom_node_native.node");
72
+ }
package/scripts/smoke.mjs CHANGED
@@ -3,6 +3,8 @@ import { RomRuntime, hasNativeBinding } from "../src/index.js";
3
3
  async function main() {
4
4
  const runtime = new RomRuntime({ href: "https://example.test/" });
5
5
  const href = await runtime.evalAsync("(async () => location.href)()");
6
+ await runtime.evalAsync("(async () => { globalThis.__romSmokeValue = 42; return 'ok'; })()");
7
+ const persisted = await runtime.evalAsync("(async () => String(globalThis.__romSmokeValue))()");
6
8
  const snapshot = await runtime.surfaceSnapshot();
7
9
 
8
10
  if (!hasNativeBinding()) {
@@ -17,7 +19,11 @@ async function main() {
17
19
  throw new Error("Surface snapshot did not expose window.");
18
20
  }
19
21
 
20
- console.log(JSON.stringify({ native: true, href }));
22
+ if (persisted !== "42") {
23
+ throw new Error(`Expected persisted global state, got: ${persisted}`);
24
+ }
25
+
26
+ console.log(JSON.stringify({ native: true, href, persisted }));
21
27
  }
22
28
 
23
29
  main().catch((error) => {
@@ -0,0 +1,47 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const packageRoot = path.resolve(__dirname, "..");
8
+ const args = process.argv.slice(2);
9
+ const sourceRoot =
10
+ readOptionValue(args, "--source") || path.join(packageRoot, "release-inputs", "prebuilds");
11
+ const outputRoot =
12
+ readOptionValue(args, "--output") || path.join(packageRoot, "prebuilds");
13
+
14
+ if (!existsSync(sourceRoot)) {
15
+ console.error(`Prebuild source directory not found: ${sourceRoot}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ rmSync(outputRoot, { recursive: true, force: true });
20
+ mkdirSync(outputRoot, { recursive: true });
21
+
22
+ for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
23
+ if (!entry.isDirectory()) {
24
+ continue;
25
+ }
26
+
27
+ const sourceDir = path.join(sourceRoot, entry.name);
28
+ const outputDir = path.join(outputRoot, entry.name);
29
+ cpSync(sourceDir, outputDir, { recursive: true });
30
+ }
31
+
32
+ console.log(`Staged prebuilds from ${sourceRoot} to ${outputRoot}`);
33
+
34
+ function readOptionValue(argv, optionName) {
35
+ const index = argv.indexOf(optionName);
36
+ if (index === -1) {
37
+ return null;
38
+ }
39
+
40
+ const value = argv[index + 1];
41
+ if (!value || value.startsWith("--")) {
42
+ console.error(`Missing value for ${optionName}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ return path.resolve(packageRoot, value);
47
+ }
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export interface RuntimeConfig {
2
2
  href?: string;
3
+ referrer?: string;
3
4
  user_agent?: string;
4
5
  app_name?: string;
5
6
  platform?: string;
@@ -8,6 +9,9 @@ export interface RuntimeConfig {
8
9
  hardware_concurrency?: number;
9
10
  device_memory?: number;
10
11
  webdriver?: boolean;
12
+ cors_enabled?: boolean;
13
+ proxy_url?: string | null;
14
+ cookie_store?: string | null;
11
15
  }
12
16
 
13
17
  export declare class RomRuntime {
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
  const repoRoot = path.resolve(__dirname, "..", "..", "..");
9
9
  const nativeBridge = loadNativeBridge();
10
+ const NativeRomRuntime = nativeBridge?.NativeRomRuntime ?? null;
10
11
 
11
12
  function resolveBridgeCommand() {
12
13
  if (process.env.ROM_BRIDGE_BIN) {
@@ -44,7 +45,10 @@ function parseBridgeResponse(stdout, stderr, error) {
44
45
  throw new Error(response.error || error?.message || "ROM bridge command failed.");
45
46
  }
46
47
 
47
- return response.result;
48
+ return {
49
+ result: response.result,
50
+ state: response.state ?? null,
51
+ };
48
52
  }
49
53
 
50
54
  function runNativeBridge(command, payload) {
@@ -85,17 +89,92 @@ function runBridge(command, payload) {
85
89
  return runNativeBridge(command, payload) ?? runCliBridge(command, payload);
86
90
  }
87
91
 
92
+ function applyBridgeState(targetConfig, state) {
93
+ if (!state || typeof state.cookie_store !== "string") {
94
+ return targetConfig;
95
+ }
96
+
97
+ return {
98
+ ...targetConfig,
99
+ cookie_store: state.cookie_store,
100
+ };
101
+ }
102
+
88
103
  export class RomRuntime {
104
+ #nativeRuntime = null;
105
+
89
106
  constructor(config = {}) {
90
107
  this.config = config;
108
+ if (typeof NativeRomRuntime === "function") {
109
+ this.#nativeRuntime = new NativeRomRuntime(JSON.stringify(this.config));
110
+ }
111
+ }
112
+
113
+ #applyCookieStore(cookieStore) {
114
+ if (typeof cookieStore !== "string") {
115
+ return;
116
+ }
117
+
118
+ this.config = {
119
+ ...this.config,
120
+ cookie_store: cookieStore,
121
+ };
122
+ }
123
+
124
+ #syncNativeState() {
125
+ if (!this.#nativeRuntime || typeof this.#nativeRuntime.exportCookieStore !== "function") {
126
+ return;
127
+ }
128
+
129
+ this.#applyCookieStore(this.#nativeRuntime.exportCookieStore());
130
+ }
131
+
132
+ async #runNative(method, ...args) {
133
+ if (!this.#nativeRuntime || typeof this.#nativeRuntime[method] !== "function") {
134
+ return null;
135
+ }
136
+
137
+ const result = await Promise.resolve(this.#nativeRuntime[method](...args));
138
+ this.#syncNativeState();
139
+ return result;
140
+ }
141
+
142
+ async #run(command, payload = {}) {
143
+ if (this.#nativeRuntime) {
144
+ if (command === "eval") {
145
+ return this.#runNative("eval", payload.script);
146
+ }
147
+ if (command === "eval-async") {
148
+ return this.#runNative("evalAsync", payload.script);
149
+ }
150
+ if (command === "surface-snapshot") {
151
+ return JSON.parse(await this.#runNative("surfaceSnapshotJson"));
152
+ }
153
+ if (command === "fingerprint-probe") {
154
+ return JSON.parse(await this.#runNative("fingerprintProbeJson"));
155
+ }
156
+ if (command === "fingerprint-js-harness") {
157
+ return JSON.parse(await this.#runNative("fingerprintJsHarnessJson"));
158
+ }
159
+ if (command === "fingerprint-js-version") {
160
+ return this.#runNative("fingerprintJsVersion");
161
+ }
162
+ }
163
+
164
+ const response = await runBridge(command, {
165
+ config: this.config,
166
+ ...payload,
167
+ });
168
+ this.config = applyBridgeState(this.config, response.state);
169
+ return response.result;
91
170
  }
92
171
 
93
172
  eval(script) {
94
- return runBridge("eval", { config: this.config, script });
173
+ return this.#run("eval", { script });
95
174
  }
96
175
 
97
176
  evalAsync(script) {
98
- return runBridge("eval-async", { config: this.config, script });
177
+ return this.#run("eval-async", { script });
99
178
  }
100
179
 
101
180
  async evalJson(script, { async = true } = {}) {
@@ -104,19 +183,19 @@ export class RomRuntime {
104
183
  }
105
184
 
106
185
  surfaceSnapshot() {
107
- return runBridge("surface-snapshot", { config: this.config });
186
+ return this.#run("surface-snapshot");
108
187
  }
109
188
 
110
189
  fingerprintProbe() {
111
- return runBridge("fingerprint-probe", { config: this.config });
190
+ return this.#run("fingerprint-probe");
112
191
  }
113
192
 
114
193
  runFingerprintJsHarness() {
115
- return runBridge("fingerprint-js-harness", { config: this.config });
194
+ return this.#run("fingerprint-js-harness");
116
195
  }
117
196
 
118
197
  fingerprintJsVersion() {
119
- return runBridge("fingerprint-js-version", { config: this.config });
198
+ return this.#run("fingerprint-js-version");
120
199
  }
121
200
  }
122
201
 
package/src/native.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { detectNativePrebuildId } from "./platform.js";
5
6
 
6
7
  const require = createRequire(import.meta.url);
7
8
  const __filename = fileURLToPath(import.meta.url);
@@ -15,7 +16,13 @@ function candidatePaths() {
15
16
  candidates.push(process.env.ROM_NATIVE_NODE_BINDING);
16
17
  }
17
18
 
19
+ const prebuildId = detectNativePrebuildId();
20
+ if (prebuildId !== null) {
21
+ candidates.push(path.join(packageRoot, "prebuilds", prebuildId, "rom_node_native.node"));
22
+ }
23
+
18
24
  candidates.push(path.join(packageRoot, "rom_node_native.node"));
25
+
19
26
  return candidates;
20
27
  }
21
28
 
@@ -0,0 +1,31 @@
1
+ import process from "node:process";
2
+
3
+ export function detectNativePrebuildId() {
4
+ if (process.platform === "linux" && process.arch === "x64") {
5
+ if (detectLinuxLibcFlavor() !== "gnu") {
6
+ return null;
7
+ }
8
+ return "linux-x64-gnu";
9
+ }
10
+
11
+ if (process.platform === "win32" && process.arch === "x64") {
12
+ return "win32-x64-msvc";
13
+ }
14
+
15
+ if (process.platform === "darwin" && process.arch === "x64") {
16
+ return "darwin-x64";
17
+ }
18
+
19
+ if (process.platform === "darwin" && process.arch === "arm64") {
20
+ return "darwin-arm64";
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ function detectLinuxLibcFlavor() {
27
+ const report = process.report?.getReport?.();
28
+ const glibcVersion =
29
+ report?.header?.glibcVersionRuntime ?? report?.header?.glibcVersionCompiler ?? null;
30
+ return glibcVersion ? "gnu" : "musl";
31
+ }