Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions bindings/typescript/__tests__/cjs-consumer.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// CJS package-specifier consumer fixture. Verifies that TypeScript Node16
// resolution against the package `exports` map selects the CommonJS declaration
// (index.d.cts) for require/import-from-CJS consumers.
import {
Peer,
Listener,
SchemaRegistry,
control,
CONTROL,
COMMAND,
DATA,
TELEMETRY,
ERROR,
type JsFrame,
type AuthTokenResult,
} from "@3leaps/ipcprims";

export function surface(): number {
const peer: Peer = Peer.connect("/tmp/ipcprims-cjs.sock", [
CONTROL,
COMMAND,
DATA,
TELEMETRY,
ERROR,
]);
const listener: Listener = Listener.bind("/tmp/ipcprims-cjs.sock");
const reg: SchemaRegistry = SchemaRegistry.fromDirectory("/tmp");
const token: AuthTokenResult = peer.takeClientAuthToken();
const frame: JsFrame = { channel: control(), payload: Buffer.from("x") };
void frame;
void listener;
void reg;
void token;
return CONTROL + COMMAND + DATA + TELEMETRY + ERROR;
}
63 changes: 63 additions & 0 deletions bindings/typescript/__tests__/esm-consumer.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// ESM package-specifier consumer fixture. Verifies that TypeScript Node16
// resolution against the package `exports` map selects the ESM declaration
// (index.d.mts) and that the full named-export surface type-checks, including
// the four type-only interfaces.
import {
AsyncChannelReceiver,
AsyncListener,
AsyncPeer,
Listener,
Peer,
SchemaRegistry,
control,
command,
data,
telemetry,
error,
CONTROL,
COMMAND,
DATA,
TELEMETRY,
ERROR,
type JsFrame,
type RecvAsyncOptions,
type ListenerOptions,
type AuthTokenResult,
} from "@3leaps/ipcprims";

const controller = new AbortController();
const signal: RecvAsyncOptions = { signal: controller.signal };

export async function surface(): Promise<number> {
const listener: AsyncListener = AsyncListener.bind("/tmp/ipcprims-esm.sock", {
channels: [COMMAND],
} satisfies ListenerOptions);
const accepted: Promise<AsyncPeer> = listener.accept();
const client: AsyncPeer = await AsyncPeer.connect("/tmp/ipcprims-esm.sock", [
CONTROL,
COMMAND,
DATA,
TELEMETRY,
ERROR,
]);
const receiver: AsyncChannelReceiver = await client.openChannel(COMMAND);
const frame: JsFrame = { channel: control(), payload: Buffer.from("x") };
await client.send(frame.channel, frame.payload);
const up: number = control() + command() + data() + telemetry() + error();
const syncPeer: Peer = Peer.connect("/tmp/ipcprims-esm.sock", [CONTROL]);
const syncListener: Listener = Listener.bind("/tmp/ipcprims-esm.sock");
const reg: SchemaRegistry = SchemaRegistry.fromDirectory("/tmp");
const token: AuthTokenResult = syncPeer.takeClientAuthToken();
for await (const received of receiver) {
void received;
}
void frame;
void up;
void syncListener;
void receiver;
void signal;
void reg;
void token;
await client.shutdown();
return 0;
}
89 changes: 89 additions & 0 deletions bindings/typescript/index.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* tslint:disable */
/* eslint-disable */

/* auto-generated by NAPI-RS */

export interface RecvAsyncOptions {
signal?: AbortSignal;
}
export interface JsFrame {
channel: number;
payload: Buffer;
}
export interface ListenerOptions {
channels?: Array<number>;
schemaDir?: string;
}
export interface AuthTokenResult {
present: boolean;
token?: Buffer;
}
export declare function control(): number;
export declare function command(): number;
export declare function data(): number;
export declare function telemetry(): number;
export declare function error(): number;
export declare const CONTROL: number;
export declare const COMMAND: number;
export declare const DATA: number;
export declare const TELEMETRY: number;
export declare const ERROR: number;
export declare class AsyncPeer {
static connect(path: string, channels: Array<number>): Promise<AsyncPeer>;
static connectWithAuth(
path: string,
channels: Array<number>,
authToken: Buffer,
): Promise<AsyncPeer>;
send(channel: number, data: Buffer): Promise<void>;
recvAsync(options?: RecvAsyncOptions | undefined | null): Promise<JsFrame>;
recvOnAsync(
channel: number,
options?: RecvAsyncOptions | undefined | null,
): Promise<JsFrame>;
openChannel(channel: number): Promise<AsyncChannelReceiver>;
ping(): Promise<number>;
takeClientAuthToken(): AuthTokenResult;
shutdown(): Promise<void>;
close(): void;
}
export declare class AsyncChannelReceiver implements AsyncIterable<JsFrame> {
recvAsync(options?: RecvAsyncOptions | undefined | null): Promise<JsFrame>;
[Symbol.asyncIterator](): AsyncIterator<JsFrame>;
}
export declare class AsyncListener {
static bind(
path: string,
options?: ListenerOptions | undefined | null,
): AsyncListener;
accept(): Promise<AsyncPeer>;
close(): Promise<void>;
}
export declare class Listener {
static bind(
path: string,
options?: ListenerOptions | undefined | null,
): Listener;
accept(): Peer;
close(): void;
}
export declare class Peer {
static connect(path: string, channels: Array<number>): Peer;
static connectWithAuth(
path: string,
channels: Array<number>,
authToken: Buffer,
): Peer;
send(channel: number, data: Buffer): void;
recv(): JsFrame;
recvOn(channel: number): JsFrame;
ping(): number;
takeClientAuthToken(): AuthTokenResult;
shutdown(): void;
close(): void;
}
export declare class SchemaRegistry {
static fromDirectory(path: string): SchemaRegistry;
validate(channel: number, data: Buffer): void;
close(): void;
}
89 changes: 89 additions & 0 deletions bindings/typescript/index.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* tslint:disable */
/* eslint-disable */

/* auto-generated by NAPI-RS */

export interface RecvAsyncOptions {
signal?: AbortSignal;
}
export interface JsFrame {
channel: number;
payload: Buffer;
}
export interface ListenerOptions {
channels?: Array<number>;
schemaDir?: string;
}
export interface AuthTokenResult {
present: boolean;
token?: Buffer;
}
export declare function control(): number;
export declare function command(): number;
export declare function data(): number;
export declare function telemetry(): number;
export declare function error(): number;
export declare const CONTROL: number;
export declare const COMMAND: number;
export declare const DATA: number;
export declare const TELEMETRY: number;
export declare const ERROR: number;
export declare class AsyncPeer {
static connect(path: string, channels: Array<number>): Promise<AsyncPeer>;
static connectWithAuth(
path: string,
channels: Array<number>,
authToken: Buffer,
): Promise<AsyncPeer>;
send(channel: number, data: Buffer): Promise<void>;
recvAsync(options?: RecvAsyncOptions | undefined | null): Promise<JsFrame>;
recvOnAsync(
channel: number,
options?: RecvAsyncOptions | undefined | null,
): Promise<JsFrame>;
openChannel(channel: number): Promise<AsyncChannelReceiver>;
ping(): Promise<number>;
takeClientAuthToken(): AuthTokenResult;
shutdown(): Promise<void>;
close(): void;
}
export declare class AsyncChannelReceiver implements AsyncIterable<JsFrame> {
recvAsync(options?: RecvAsyncOptions | undefined | null): Promise<JsFrame>;
[Symbol.asyncIterator](): AsyncIterator<JsFrame>;
}
export declare class AsyncListener {
static bind(
path: string,
options?: ListenerOptions | undefined | null,
): AsyncListener;
accept(): Promise<AsyncPeer>;
close(): Promise<void>;
}
export declare class Listener {
static bind(
path: string,
options?: ListenerOptions | undefined | null,
): Listener;
accept(): Peer;
close(): void;
}
export declare class Peer {
static connect(path: string, channels: Array<number>): Peer;
static connectWithAuth(
path: string,
channels: Array<number>,
authToken: Buffer,
): Peer;
send(channel: number, data: Buffer): void;
recv(): JsFrame;
recvOn(channel: number): JsFrame;
ping(): number;
takeClientAuthToken(): AuthTokenResult;
shutdown(): void;
close(): void;
}
export declare class SchemaRegistry {
static fromDirectory(path: string): SchemaRegistry;
validate(channel: number, data: Buffer): void;
close(): void;
}
5 changes: 5 additions & 0 deletions bindings/typescript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ module.exports = {
Listener: native.Listener,
Peer: native.Peer,
SchemaRegistry: native.SchemaRegistry,
control: native.control,
command: native.command,
data: native.data,
telemetry: native.telemetry,
error: native.error,
CONTROL: native.control(),
COMMAND: native.command(),
DATA: native.data(),
Expand Down
27 changes: 27 additions & 0 deletions bindings/typescript/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// ESM façade over the CommonJS binding wrapper.
//
// The wrapper (index.js) owns the single native-addon instance and
// loadBinding()/platform-selection logic. This file imports it exactly once
// and re-exports its values as explicit bindings, so CJS and ESM consumers in
// one process share one native instance and identical class/prototype
// identities. It must not call loadBinding() or load a native addon itself.
import cjs from "./index.js";

export const {
AsyncChannelReceiver,
AsyncListener,
AsyncPeer,
Listener,
Peer,
SchemaRegistry,
control,
command,
data,
telemetry,
error,
CONTROL,
COMMAND,
DATA,
TELEMETRY,
ERROR,
} = cjs;
40 changes: 38 additions & 2 deletions bindings/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,45 @@
},
"main": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"import": {
"types": "./index.d.mts",
"default": "./index.mjs"
},
"require": {
"types": "./index.d.cts",
"default": "./index.js"
}
},
"./index": {
"import": {
"types": "./index.d.mts",
"default": "./index.mjs"
},
"require": {
"types": "./index.d.cts",
"default": "./index.js"
}
},
"./index.js": {
"import": {
"types": "./index.d.mts",
"default": "./index.mjs"
},
"require": {
"types": "./index.d.cts",
"default": "./index.js"
}
},
"./package.json": "./package.json"
},
"files": [
"index.js",
"index.mjs",
"index.d.ts",
"index.d.cts",
"index.d.mts",
"npm/",
"README.md",
"LICENSE-MIT",
Expand All @@ -37,8 +73,8 @@
"scripts": {
"build:native": "napi build --cargo-cwd . --release --platform --js false --strip",
"postbuild:native": "node scripts/augment-types.mjs",
"test": "npm run build:native && tsc -p tsconfig.test.json && node --test .tmp-test/ipcprims.test.js",
"typecheck": "tsc --noEmit"
"test": "npm run build:native && tsc -p tsconfig.test.json && node --test .tmp-test/ipcprims.test.js && node scripts/check-export-drift.mjs && node scripts/check-pack.mjs",
"typecheck": "tsc --noEmit && tsc -p tsconfig.consumer.json"
},
"optionalDependencies": {
"@3leaps/ipcprims-linux-x64-gnu": "0.2.4",
Expand Down
7 changes: 7 additions & 0 deletions bindings/typescript/scripts/augment-types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ if (format.error && format.error.code !== "ENOENT") {
if (!format.error && format.status !== 0) {
process.exit(format.status ?? 1);
}

// Synchronize the loader-specific declaration variants (.cts for require, .mts
// for import) from the finalized index.d.ts, so the three declaration files
// never drift independently after a NAPI-RS regeneration + augmentation.
const synced = readFileSync(dtsPath, "utf8");
writeFileSync(path.join(dir, "..", "index.d.cts"), synced);
writeFileSync(path.join(dir, "..", "index.d.mts"), synced);
Loading
Loading