Skip to content

Commit ac8e84b

Browse files
esimkowitzclaude
andauthored
feat: add setNativeModulePath for custom native module loading (#272) (#281)
* feat: add setNativeModulePath for custom native module loading (#272) Add `setNativeModulePath()` and `PRINTERS_JS_NATIVE_MODULE_PATH` env var so bundled apps (Electron, pkg) and sandboxed runtimes can point the loader at a specific `.node` binary instead of relying on platform npm packages. Also attach the per-platform binaries as GitHub release assets so they're downloadable without npm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * missed one * fix bad code coverage reports * use fileUrlToPath to fix potential issues with dirs with spaces --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 33c3de3 commit ac8e84b

9 files changed

Lines changed: 379 additions & 39 deletions

File tree

.github/workflows/release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,19 @@ jobs:
247247
ls -la npm/ 2>/dev/null || dir npm
248248
- name: Compile TypeScript for publishing
249249
run: task compile
250+
- name: Collect native binaries for release
251+
run: |
252+
mkdir -p release-assets
253+
for binary in npm/*/printers.*.node; do
254+
if [ -f "$binary" ]; then
255+
cp "$binary" release-assets/
256+
fi
257+
done
258+
ls -la release-assets/
259+
- name: Publish GitHub release with native binaries
260+
uses: softprops/action-gh-release@v2
261+
with:
262+
files: release-assets/*.node
263+
fail_on_unmatched_files: true
250264
- name: Publish to npm
251265
run: pnpm publish --no-git-checks

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ Check if a printer exists on the system.
123123

124124
Get the default system printer.
125125

126+
#### `setNativeModulePath(path: string): void`
127+
128+
Override the path used to load the native N-API binary. Useful when shipping
129+
the library inside a bundler (Electron, pkg, esbuild single-file builds) or in
130+
sandboxed environments where the platform-specific npm package
131+
(`@printers/printers-<platform>`) is not present.
132+
133+
```ts
134+
import { setNativeModulePath, getAllPrinters } from "@printers/printers";
135+
136+
// MUST be called before any other @printers/printers API.
137+
setNativeModulePath("/absolute/path/to/printers.darwin-arm64.node");
138+
139+
const printers = await getAllPrinters();
140+
```
141+
142+
The same override is also available via the `PRINTERS_JS_NATIVE_MODULE_PATH`
143+
environment variable, which is convenient for CI, Docker, and deployment
144+
scenarios. An explicit `setNativeModulePath()` call takes precedence over the
145+
environment variable.
146+
147+
Per-platform `.node` binaries are attached as assets to each GitHub release in
148+
addition to being published via the platform npm packages, so they can be
149+
downloaded and bundled directly.
150+
126151
### Printer Class
127152

128153
#### Properties

Taskfile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ tasks:
4848

4949
test:direct:deno:
5050
desc: Run Deno tests directly
51-
cmd: "{{.SIMULATE}} deno test --allow-read --allow-write --allow-env --allow-ffi src/tests/shared.test.ts"
51+
cmd: "{{.SIMULATE}} deno test --allow-read --allow-write --allow-env --allow-ffi --allow-run src/tests/shared.test.ts"
5252

5353
test:direct:node:
5454
desc: Run Node.js tests directly

deno.lock

Lines changed: 35 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eslint.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export default [
2020
"examples/**/*",
2121
"examples/**",
2222

23+
// Git worktrees (each worktree has its own checkout; let it lint itself)
24+
".worktrees/**/*",
25+
".worktrees/**",
26+
2327
// Build artifacts and dependencies
2428
"dist/**/*",
2529
"dist/**",

scripts/test-runtimes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ async function runTests(runtimesToTest = ["rust", "deno", "node", "bun"]) {
196196
"--allow-env",
197197
"--allow-read",
198198
"--allow-ffi",
199+
"--allow-run",
199200
"--no-check",
200201
"--coverage=test-results/coverage/deno-temp",
201202
"src/tests/shared.test.ts",

src/index.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,27 @@ export function createCustomPageSize(
738738
let nativeModulePromise: Promise<NativeModule> | null = null;
739739
let nativeModuleCache: NativeModule | null = null;
740740
let simulationModeLogged = false;
741+
let customNativeModulePath: string | null = null;
742+
743+
/**
744+
* Override the path used to load the native N-API binary.
745+
*
746+
* Must be called before any other `@printers/printers` API. Useful for
747+
* bundlers (Electron, pkg) and sandboxed runtimes where the standard
748+
* platform-package resolution cannot find the binary.
749+
*
750+
* @param path Absolute path to the `.node` binary file.
751+
* @throws If the native module has already been loaded.
752+
*/
753+
export function setNativeModulePath(path: string): void {
754+
if (nativeModuleCache !== null || nativeModulePromise !== null) {
755+
throw new Error(
756+
"Native module already loaded. setNativeModulePath() must be called " +
757+
"before any other @printers/printers API."
758+
);
759+
}
760+
customNativeModulePath = path;
761+
}
741762

742763
/**
743764
* Get the platform string for the current runtime environment.
@@ -781,6 +802,31 @@ async function loadNativeModule(): Promise<NativeModule> {
781802
);
782803
}
783804

805+
// Custom override: explicit setNativeModulePath() call takes precedence over the env var.
806+
const envOverride = isDeno
807+
? g.Deno?.env?.get("PRINTERS_JS_NATIVE_MODULE_PATH")
808+
: g.process?.env?.PRINTERS_JS_NATIVE_MODULE_PATH;
809+
const overridePath = customNativeModulePath ?? envOverride;
810+
811+
if (overridePath) {
812+
try {
813+
// .node binaries aren't portably loadable via dynamic import() across
814+
// Node, Deno, and Bun, so use createRequire — the same shape that the
815+
// generated per-platform loaders in npm/<platform>/index.js use.
816+
const { createRequire } = await import("node:module");
817+
const require = createRequire(import.meta.url);
818+
return require(overridePath) as NativeModule;
819+
} catch (overrideError) {
820+
throw new Error(
821+
`Failed to load native module from custom path "${overridePath}": ${
822+
overrideError instanceof Error
823+
? overrideError.message
824+
: String(overrideError)
825+
}`
826+
);
827+
}
828+
}
829+
784830
const platformString = getPlatformString();
785831

786832
// Try to load the platform-specific N-API module using dynamic imports

0 commit comments

Comments
 (0)