Skip to content

Commit 2795b0a

Browse files
authored
refactor: make rslint compatible with browser (#303)
1 parent 52cefe0 commit 2795b0a

File tree

10 files changed

+559
-197
lines changed

10 files changed

+559
-197
lines changed

packages/rslint-wasm/rslint.wasm

30.9 MB
Binary file not shown.

packages/rslint/.npmignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# don't publish local rslint bin
2-
./bin/rslint
2+
./bin/rslint
3+
dist/tsconfig.build.tsbuildinfo

packages/rslint/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"test:update": "rstest run -u"
2222
},
2323
"files": [
24-
"bin/rslint.cjs"
24+
"bin/rslint.cjs",
25+
"dist/"
2526
],
2627
"bin": {
2728
"rslint": "./bin/rslint.cjs"
@@ -30,13 +31,15 @@
3031
"access": "public"
3132
},
3233
"license": "MIT",
33-
"description": "rslint cli",
34+
"description": "rslint core library",
3435
"publisher": "rslint",
3536
"keywords": [
3637
"rslint",
3738
"linter",
3839
"typescript",
39-
"go"
40+
"go",
41+
"browser",
42+
"webworker"
4043
],
4144
"devDependencies": {
4245
"@types/node": "24.0.14",

packages/rslint/src/browser.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {
2+
RslintServiceInterface,
3+
LintOptions,
4+
LintResponse,
5+
ApplyFixesRequest,
6+
ApplyFixesResponse,
7+
RSlintOptions,
8+
PendingMessage,
9+
IpcMessage,
10+
} from './types.js';
11+
12+
/**
13+
* Browser implementation of RslintService using web workers
14+
*/
15+
export class BrowserRslintService implements RslintServiceInterface {
16+
private nextMessageId: number;
17+
private pendingMessages: Map<number, PendingMessage>;
18+
private worker: Worker | null;
19+
private workerUrl: string;
20+
21+
constructor(options: RSlintOptions = {}) {
22+
this.nextMessageId = 1;
23+
this.pendingMessages = new Map();
24+
this.worker = null;
25+
26+
// In browser, we need to use a web worker that can run the rslint binary
27+
// This would typically be a WASM version or a worker that can spawn processes
28+
this.workerUrl =
29+
options.rslintPath || new URL('./worker.js', import.meta.url).href;
30+
}
31+
32+
/**
33+
* Initialize the web worker
34+
*/
35+
private async ensureWorker(): Promise<Worker> {
36+
if (!this.worker) {
37+
this.worker = new Worker(this.workerUrl);
38+
39+
this.worker.onmessage = event => {
40+
this.handleWorkerMessage(event.data);
41+
};
42+
43+
this.worker.onerror = error => {
44+
console.error('Worker error:', error);
45+
// Reject all pending messages
46+
for (const [id, pending] of this.pendingMessages) {
47+
pending.reject(new Error(`Worker error: ${error.message}`));
48+
}
49+
this.pendingMessages.clear();
50+
};
51+
}
52+
return this.worker;
53+
}
54+
55+
/**
56+
* Send a message to the worker
57+
*/
58+
async sendMessage(kind: string, data: any): Promise<any> {
59+
const worker = await this.ensureWorker();
60+
61+
return new Promise((resolve, reject) => {
62+
const id = this.nextMessageId++;
63+
const message: IpcMessage = { id, kind, data };
64+
65+
// Register promise callbacks
66+
this.pendingMessages.set(id, { resolve, reject });
67+
68+
// Send message to worker
69+
worker.postMessage(message);
70+
});
71+
}
72+
73+
/**
74+
* Handle messages from the worker
75+
*/
76+
private handleWorkerMessage(message: IpcMessage): void {
77+
const { id, kind, data } = message;
78+
const pending = this.pendingMessages.get(id);
79+
if (!pending) return;
80+
81+
this.pendingMessages.delete(id);
82+
83+
if (kind === 'error') {
84+
pending.reject(new Error(data.message));
85+
} else {
86+
pending.resolve(data);
87+
}
88+
}
89+
90+
/**
91+
* Terminate the worker
92+
*/
93+
terminate(): void {
94+
if (this.worker) {
95+
// Reject all pending messages
96+
for (const [id, pending] of this.pendingMessages) {
97+
pending.reject(new Error('Service terminated'));
98+
}
99+
this.pendingMessages.clear();
100+
101+
this.worker.terminate();
102+
this.worker = null;
103+
}
104+
}
105+
}

packages/rslint/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import {
22
LintOptions,
33
LintResponse,
44
RSLintService,
5+
createRslintService,
56
ApplyFixesRequest,
67
ApplyFixesResponse,
7-
} from './service.ts';
8+
} from './service.js';
89

9-
// Export the RSLintService class for direct usage
10-
export { RSLintService } from './service.ts';
10+
// Export the main RSLintService class for direct usage
11+
export { RSLintService, createRslintService } from './service.js';
12+
13+
// Export specific implementations for advanced usage
14+
export { NodeRslintService } from './node.js';
15+
export { BrowserRslintService } from './browser.js';
1116

1217
// For backward compatibility and convenience
1318
export async function lint(options: LintOptions): Promise<LintResponse> {
@@ -29,6 +34,7 @@ export async function applyFixes(
2934
return result;
3035
}
3136

37+
// Export all types
3238
export {
3339
type Diagnostic,
3440
type LintOptions,
@@ -37,4 +43,6 @@ export {
3743
type ApplyFixesResponse,
3844
type LanguageOptions,
3945
type ParserOptions,
40-
} from './service.ts';
46+
type RSlintOptions,
47+
type RslintServiceInterface,
48+
} from './types.js';

packages/rslint/src/node.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { spawn, ChildProcess } from 'child_process';
2+
import path from 'path';
3+
import type {
4+
RslintServiceInterface,
5+
LintOptions,
6+
LintResponse,
7+
ApplyFixesRequest,
8+
ApplyFixesResponse,
9+
RSlintOptions,
10+
PendingMessage,
11+
IpcMessage,
12+
} from './types.js';
13+
14+
/**
15+
* Node.js implementation of RslintService using child processes
16+
*/
17+
export class NodeRslintService implements RslintServiceInterface {
18+
private nextMessageId: number;
19+
private pendingMessages: Map<number, PendingMessage>;
20+
private rslintPath: string;
21+
private process: ChildProcess;
22+
private chunks: Buffer[];
23+
private chunkSize: number;
24+
private expectedSize: number | null;
25+
26+
constructor(options: RSlintOptions = {}) {
27+
this.nextMessageId = 1;
28+
this.pendingMessages = new Map();
29+
this.rslintPath =
30+
options.rslintPath || path.join(import.meta.dirname, '../bin/rslint');
31+
32+
this.process = spawn(this.rslintPath, ['--api'], {
33+
stdio: ['pipe', 'pipe', 'inherit'],
34+
cwd: options.workingDirectory || process.cwd(),
35+
env: {
36+
...process.env,
37+
},
38+
});
39+
40+
// Set up binary message reading
41+
this.process.stdout!.on('data', data => {
42+
this.handleChunk(data);
43+
});
44+
this.chunks = [];
45+
this.chunkSize = 0;
46+
this.expectedSize = null;
47+
}
48+
49+
/**
50+
* Send a message to the rslint process
51+
*/
52+
async sendMessage(kind: string, data: any): Promise<any> {
53+
return new Promise((resolve, reject) => {
54+
const id = this.nextMessageId++;
55+
const message: IpcMessage = { id, kind, data };
56+
57+
// Register promise callbacks
58+
this.pendingMessages.set(id, { resolve, reject });
59+
60+
// Write message length as 4 bytes in little endian
61+
const json = JSON.stringify(message);
62+
const length = Buffer.alloc(4);
63+
length.writeUInt32LE(json.length, 0);
64+
65+
// Send message
66+
this.process.stdin!.write(
67+
Buffer.concat([length, Buffer.from(json, 'utf8')]),
68+
);
69+
});
70+
}
71+
72+
/**
73+
* Handle incoming binary data chunks
74+
*/
75+
private handleChunk(chunk: Buffer): void {
76+
this.chunks.push(chunk);
77+
this.chunkSize += chunk.length;
78+
79+
// Process complete messages
80+
while (true) {
81+
// Read message length if we don't have it yet
82+
if (this.expectedSize === null) {
83+
if (this.chunkSize < 4) return;
84+
85+
// Combine chunks to read the message length
86+
const combined = Buffer.concat(this.chunks);
87+
this.expectedSize = combined.readUInt32LE(0);
88+
89+
// Remove length bytes from buffer
90+
this.chunks = [combined.slice(4)];
91+
this.chunkSize -= 4;
92+
}
93+
94+
// Check if we have the full message
95+
if (this.chunkSize < this.expectedSize) return;
96+
97+
// Read the message content
98+
const combined = Buffer.concat(this.chunks);
99+
const message = combined.slice(0, this.expectedSize).toString('utf8');
100+
101+
// Handle the message
102+
try {
103+
const parsed: IpcMessage = JSON.parse(message);
104+
this.handleMessage(parsed);
105+
} catch (err) {
106+
console.error('Error parsing message:', err);
107+
}
108+
109+
// Reset for next message
110+
this.chunks = [combined.slice(this.expectedSize)];
111+
this.chunkSize = this.chunks[0].length;
112+
this.expectedSize = null;
113+
}
114+
}
115+
116+
/**
117+
* Handle a complete message from rslint
118+
*/
119+
private handleMessage(message: IpcMessage): void {
120+
const { id, kind, data } = message;
121+
const pending = this.pendingMessages.get(id);
122+
if (!pending) return;
123+
124+
this.pendingMessages.delete(id);
125+
126+
if (kind === 'error') {
127+
pending.reject(new Error(data.message));
128+
} else {
129+
pending.resolve(data);
130+
}
131+
}
132+
133+
/**
134+
* Terminate the rslint process
135+
*/
136+
terminate(): void {
137+
if (this.process && !this.process.killed) {
138+
this.process.stdin!.end();
139+
this.process.kill();
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)