@elliots/typical-compiler 0.2.0-beta.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/dist/client.d.ts +32 -0
- package/dist/client.js +161 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +21 -0
- package/dist/protocol.js +113 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProjectHandle, TransformResult } from './types.js';
|
|
2
|
+
export interface TypicalCompilerOptions {
|
|
3
|
+
/** Path to the typical binary. If not provided, uses the bundled binary. */
|
|
4
|
+
binaryPath?: string;
|
|
5
|
+
/** Current working directory for the compiler. */
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class TypicalCompiler {
|
|
9
|
+
private process;
|
|
10
|
+
private pendingRequests;
|
|
11
|
+
private buffer;
|
|
12
|
+
private binaryPath;
|
|
13
|
+
private cwd;
|
|
14
|
+
private nextRequestId;
|
|
15
|
+
constructor(options?: TypicalCompilerOptions);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
loadProject(configFileName: string): Promise<ProjectHandle>;
|
|
19
|
+
transformFile(project: ProjectHandle | string, fileName: string): Promise<TransformResult>;
|
|
20
|
+
release(handle: ProjectHandle | string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Transform a standalone TypeScript source string.
|
|
23
|
+
* Creates a temporary project to enable type checking.
|
|
24
|
+
*
|
|
25
|
+
* @param fileName - Virtual filename for error messages (e.g., "test.ts")
|
|
26
|
+
* @param source - TypeScript source code
|
|
27
|
+
* @returns Transformed code with validation
|
|
28
|
+
*/
|
|
29
|
+
transformSource(fileName: string, source: string): Promise<TransformResult>;
|
|
30
|
+
private request;
|
|
31
|
+
private handleData;
|
|
32
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { encodeRequest, decodeResponse } from './protocol.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const debug = process.env.DEBUG === '1';
|
|
9
|
+
function debugLog(...args) {
|
|
10
|
+
if (debug) {
|
|
11
|
+
console.error(...args);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function getBinaryPath() {
|
|
15
|
+
// Try platform-specific package first
|
|
16
|
+
const platform = process.platform; // darwin, linux, win32
|
|
17
|
+
const arch = process.arch; // arm64, x64
|
|
18
|
+
const pkgName = `@elliots/typical-compiler-${platform}-${arch}`;
|
|
19
|
+
try {
|
|
20
|
+
const pkg = require(pkgName);
|
|
21
|
+
debugLog(`[CLIENT] Using platform binary from ${pkgName}`);
|
|
22
|
+
return pkg.binaryPath;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Fallback to local bin (for development)
|
|
26
|
+
debugLog(`[CLIENT] Platform package ${pkgName} not found, falling back to local bin`);
|
|
27
|
+
return join(__dirname, '..', 'bin', 'typical');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class TypicalCompiler {
|
|
31
|
+
process = null;
|
|
32
|
+
pendingRequests = new Map();
|
|
33
|
+
buffer = Buffer.alloc(0);
|
|
34
|
+
binaryPath;
|
|
35
|
+
cwd;
|
|
36
|
+
nextRequestId = 0;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.binaryPath = options.binaryPath ?? getBinaryPath();
|
|
39
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
40
|
+
}
|
|
41
|
+
async start() {
|
|
42
|
+
if (this.process) {
|
|
43
|
+
throw new Error('Compiler already started');
|
|
44
|
+
}
|
|
45
|
+
this.process = spawn(this.binaryPath, ['--cwd', this.cwd], {
|
|
46
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
47
|
+
});
|
|
48
|
+
// Don't let the child process keep the Node process alive
|
|
49
|
+
this.process.unref();
|
|
50
|
+
this.process.stdout.on('data', (data) => {
|
|
51
|
+
this.handleData(data);
|
|
52
|
+
});
|
|
53
|
+
this.process.on('error', err => {
|
|
54
|
+
console.error('Compiler process error:', err);
|
|
55
|
+
});
|
|
56
|
+
this.process.on('exit', code => {
|
|
57
|
+
this.process = null;
|
|
58
|
+
// Reject any pending requests
|
|
59
|
+
for (const [, { reject }] of this.pendingRequests) {
|
|
60
|
+
reject(new Error(`Compiler process exited with code ${code}`));
|
|
61
|
+
}
|
|
62
|
+
this.pendingRequests.clear();
|
|
63
|
+
});
|
|
64
|
+
// Test the connection with echo
|
|
65
|
+
const result = await this.request('echo', 'ping');
|
|
66
|
+
if (result !== 'ping') {
|
|
67
|
+
throw new Error(`Echo test failed: expected "ping", got "${result}"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async close() {
|
|
71
|
+
if (this.process) {
|
|
72
|
+
const proc = this.process;
|
|
73
|
+
this.process = null;
|
|
74
|
+
// Clear pending requests to avoid errors after close
|
|
75
|
+
this.pendingRequests.clear();
|
|
76
|
+
proc.stdin?.end();
|
|
77
|
+
proc.kill();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async loadProject(configFileName) {
|
|
81
|
+
return this.request('loadProject', { configFileName });
|
|
82
|
+
}
|
|
83
|
+
async transformFile(project, fileName) {
|
|
84
|
+
const projectId = typeof project === 'string' ? project : project.id;
|
|
85
|
+
return this.request('transformFile', {
|
|
86
|
+
project: projectId,
|
|
87
|
+
fileName,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async release(handle) {
|
|
91
|
+
const id = typeof handle === 'string' ? handle : handle.id;
|
|
92
|
+
await this.request('release', id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Transform a standalone TypeScript source string.
|
|
96
|
+
* Creates a temporary project to enable type checking.
|
|
97
|
+
*
|
|
98
|
+
* @param fileName - Virtual filename for error messages (e.g., "test.ts")
|
|
99
|
+
* @param source - TypeScript source code
|
|
100
|
+
* @returns Transformed code with validation
|
|
101
|
+
*/
|
|
102
|
+
async transformSource(fileName, source) {
|
|
103
|
+
return this.request('transformSource', {
|
|
104
|
+
fileName,
|
|
105
|
+
source,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async request(method, payload) {
|
|
109
|
+
if (!this.process) {
|
|
110
|
+
throw new Error('Compiler not started');
|
|
111
|
+
}
|
|
112
|
+
// Use unique request ID to correlate request/response
|
|
113
|
+
const requestId = `${method}:${this.nextRequestId++}`;
|
|
114
|
+
const requestData = encodeRequest(requestId, payload);
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
this.pendingRequests.set(requestId, {
|
|
117
|
+
resolve: resolve,
|
|
118
|
+
reject,
|
|
119
|
+
});
|
|
120
|
+
this.process.stdin.write(requestData);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
handleData(data) {
|
|
124
|
+
// Append new data to buffer
|
|
125
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
126
|
+
debugLog(`[CLIENT DEBUG] handleData: received ${data.length} bytes, buffer now ${this.buffer.length} bytes`);
|
|
127
|
+
// Try to decode messages
|
|
128
|
+
while (this.buffer.length > 0) {
|
|
129
|
+
try {
|
|
130
|
+
debugLog(`[CLIENT DEBUG] Attempting to decode ${this.buffer.length} bytes...`);
|
|
131
|
+
const { messageType, method, payload, bytesConsumed } = decodeResponse(this.buffer);
|
|
132
|
+
debugLog(`[CLIENT DEBUG] Decoded: type=${messageType} method=${method} payload=${payload.length} bytes, consumed=${bytesConsumed}`);
|
|
133
|
+
// Find the pending request
|
|
134
|
+
const pending = this.pendingRequests.get(method);
|
|
135
|
+
if (!pending) {
|
|
136
|
+
const pendingKeys = [...this.pendingRequests.keys()].join(', ') || '(none)';
|
|
137
|
+
throw new Error(`No pending request for method: ${method}. Pending requests: ${pendingKeys}. ` + `This indicates a protocol bug - received response for a request that wasn't made or was already resolved.`);
|
|
138
|
+
}
|
|
139
|
+
this.pendingRequests.delete(method);
|
|
140
|
+
if (messageType === 4 /* MessageType.Response */) {
|
|
141
|
+
// Parse JSON payload
|
|
142
|
+
const result = payload.length > 0 ? JSON.parse(payload.toString('utf8')) : null;
|
|
143
|
+
pending.resolve(result);
|
|
144
|
+
}
|
|
145
|
+
else if (messageType === 5 /* MessageType.Error */) {
|
|
146
|
+
pending.reject(new Error(payload.toString('utf8')));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
pending.reject(new Error(`Unexpected message type: ${messageType}`));
|
|
150
|
+
}
|
|
151
|
+
// Remove only the processed bytes from buffer
|
|
152
|
+
this.buffer = this.buffer.subarray(bytesConsumed);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// Not enough data yet, wait for more
|
|
156
|
+
debugLog(`[CLIENT DEBUG] Decode failed (waiting for more data): ${e}`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TypicalCompiler } from './client.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagePack-like binary protocol for communicating with the typical compiler.
|
|
3
|
+
* This matches the protocol used by tsgo's --api mode.
|
|
4
|
+
*/
|
|
5
|
+
export declare const enum MessageType {
|
|
6
|
+
Unknown = 0,
|
|
7
|
+
Request = 1,
|
|
8
|
+
CallResponse = 2,
|
|
9
|
+
CallError = 3,
|
|
10
|
+
Response = 4,
|
|
11
|
+
Error = 5,
|
|
12
|
+
Call = 6
|
|
13
|
+
}
|
|
14
|
+
export declare function encodeRequest(method: string, payload: unknown): Buffer;
|
|
15
|
+
export interface DecodedMessage {
|
|
16
|
+
messageType: MessageType;
|
|
17
|
+
method: string;
|
|
18
|
+
payload: Buffer;
|
|
19
|
+
bytesConsumed: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function decodeResponse(data: Buffer): DecodedMessage;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagePack-like binary protocol for communicating with the typical compiler.
|
|
3
|
+
* This matches the protocol used by tsgo's --api mode.
|
|
4
|
+
*/
|
|
5
|
+
const MessagePackTypeFixedArray3 = 0x93;
|
|
6
|
+
const MessagePackTypeBin8 = 0xc4;
|
|
7
|
+
const MessagePackTypeBin16 = 0xc5;
|
|
8
|
+
const MessagePackTypeBin32 = 0xc6;
|
|
9
|
+
const MessagePackTypeU8 = 0xcc;
|
|
10
|
+
export function encodeRequest(method, payload) {
|
|
11
|
+
const methodBuf = Buffer.from(method, 'utf8');
|
|
12
|
+
const payloadStr = JSON.stringify(payload);
|
|
13
|
+
const payloadBuf = Buffer.from(payloadStr, 'utf8');
|
|
14
|
+
// Calculate total size
|
|
15
|
+
// 1 byte for array marker (0x93)
|
|
16
|
+
// 2 bytes for message type (0xCC + type)
|
|
17
|
+
// variable for method bin
|
|
18
|
+
// variable for payload bin
|
|
19
|
+
const methodBinSize = getBinEncodingSize(methodBuf.length);
|
|
20
|
+
const payloadBinSize = getBinEncodingSize(payloadBuf.length);
|
|
21
|
+
const totalSize = 1 + 2 + methodBinSize + methodBuf.length + payloadBinSize + payloadBuf.length;
|
|
22
|
+
const buf = Buffer.alloc(totalSize);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
// Fixed array of 3 elements
|
|
25
|
+
buf[offset++] = MessagePackTypeFixedArray3;
|
|
26
|
+
// Message type (u8)
|
|
27
|
+
buf[offset++] = MessagePackTypeU8;
|
|
28
|
+
buf[offset++] = 1 /* MessageType.Request */;
|
|
29
|
+
// Method (bin)
|
|
30
|
+
offset = writeBin(buf, offset, methodBuf);
|
|
31
|
+
// Payload (bin)
|
|
32
|
+
offset = writeBin(buf, offset, payloadBuf);
|
|
33
|
+
return buf;
|
|
34
|
+
}
|
|
35
|
+
export function decodeResponse(data) {
|
|
36
|
+
let offset = 0;
|
|
37
|
+
// Check array marker
|
|
38
|
+
if (data[offset++] !== MessagePackTypeFixedArray3) {
|
|
39
|
+
throw new Error(`Expected 0x93, got 0x${data[0].toString(16)}`);
|
|
40
|
+
}
|
|
41
|
+
// Read message type
|
|
42
|
+
if (data[offset++] !== MessagePackTypeU8) {
|
|
43
|
+
throw new Error(`Expected 0xCC for message type`);
|
|
44
|
+
}
|
|
45
|
+
const messageType = data[offset++];
|
|
46
|
+
// Read method
|
|
47
|
+
const { value: methodBuf, newOffset: offset2 } = readBin(data, offset);
|
|
48
|
+
offset = offset2;
|
|
49
|
+
const method = methodBuf.toString('utf8');
|
|
50
|
+
// Read payload
|
|
51
|
+
const { value: payload, newOffset: offset3 } = readBin(data, offset);
|
|
52
|
+
return { messageType, method, payload, bytesConsumed: offset3 };
|
|
53
|
+
}
|
|
54
|
+
function getBinEncodingSize(length) {
|
|
55
|
+
if (length < 256)
|
|
56
|
+
return 2; // 1 byte type + 1 byte length
|
|
57
|
+
if (length < 65536)
|
|
58
|
+
return 3; // 1 byte type + 2 bytes length
|
|
59
|
+
return 5; // 1 byte type + 4 bytes length
|
|
60
|
+
}
|
|
61
|
+
function writeBin(buf, offset, data) {
|
|
62
|
+
const length = data.length;
|
|
63
|
+
if (length < 256) {
|
|
64
|
+
buf[offset++] = MessagePackTypeBin8;
|
|
65
|
+
buf[offset++] = length;
|
|
66
|
+
}
|
|
67
|
+
else if (length < 65536) {
|
|
68
|
+
buf[offset++] = MessagePackTypeBin16;
|
|
69
|
+
buf.writeUInt16BE(length, offset);
|
|
70
|
+
offset += 2;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
buf[offset++] = MessagePackTypeBin32;
|
|
74
|
+
buf.writeUInt32BE(length, offset);
|
|
75
|
+
offset += 4;
|
|
76
|
+
}
|
|
77
|
+
data.copy(buf, offset);
|
|
78
|
+
return offset + length;
|
|
79
|
+
}
|
|
80
|
+
function readBin(buf, offset) {
|
|
81
|
+
if (offset >= buf.length) {
|
|
82
|
+
throw new Error('Not enough data: need type byte');
|
|
83
|
+
}
|
|
84
|
+
const type = buf[offset++];
|
|
85
|
+
let length;
|
|
86
|
+
switch (type) {
|
|
87
|
+
case MessagePackTypeBin8:
|
|
88
|
+
if (offset >= buf.length)
|
|
89
|
+
throw new Error('Not enough data: need length byte');
|
|
90
|
+
length = buf[offset++];
|
|
91
|
+
break;
|
|
92
|
+
case MessagePackTypeBin16:
|
|
93
|
+
if (offset + 2 > buf.length)
|
|
94
|
+
throw new Error('Not enough data: need 2 length bytes');
|
|
95
|
+
length = buf.readUInt16BE(offset);
|
|
96
|
+
offset += 2;
|
|
97
|
+
break;
|
|
98
|
+
case MessagePackTypeBin32:
|
|
99
|
+
if (offset + 4 > buf.length)
|
|
100
|
+
throw new Error('Not enough data: need 4 length bytes');
|
|
101
|
+
length = buf.readUInt32BE(offset);
|
|
102
|
+
offset += 4;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Expected bin type (0xC4-0xC6), got 0x${type.toString(16)}`);
|
|
106
|
+
}
|
|
107
|
+
// Check if we have enough data for the full payload
|
|
108
|
+
if (offset + length > buf.length) {
|
|
109
|
+
throw new Error(`Not enough data: need ${length} bytes, have ${buf.length - offset}`);
|
|
110
|
+
}
|
|
111
|
+
const value = buf.subarray(offset, offset + length);
|
|
112
|
+
return { value, newOffset: offset + length };
|
|
113
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ProjectHandle {
|
|
2
|
+
id: string;
|
|
3
|
+
configFile: string;
|
|
4
|
+
rootFiles: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface RawSourceMap {
|
|
7
|
+
version: number;
|
|
8
|
+
file: string;
|
|
9
|
+
sourceRoot?: string;
|
|
10
|
+
sources: string[];
|
|
11
|
+
names: string[];
|
|
12
|
+
mappings: string;
|
|
13
|
+
sourcesContent?: (string | null)[];
|
|
14
|
+
}
|
|
15
|
+
export interface TransformResult {
|
|
16
|
+
code: string;
|
|
17
|
+
sourceMap?: RawSourceMap;
|
|
18
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elliots/typical-compiler",
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"description": "TypeScript compiler powered by tsgo for typical validation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"compiler",
|
|
7
|
+
"runtime",
|
|
8
|
+
"typescript",
|
|
9
|
+
"validation"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "elliots",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.7.0"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"@elliots/typical-compiler-darwin-arm64": "0.2.0-beta.1",
|
|
30
|
+
"@elliots/typical-compiler-darwin-x64": "0.2.0-beta.1",
|
|
31
|
+
"@elliots/typical-compiler-linux-arm64": "0.2.0-beta.1",
|
|
32
|
+
"@elliots/typical-compiler-linux-x64": "0.2.0-beta.1",
|
|
33
|
+
"@elliots/typical-compiler-win32-arm64": "0.2.0-beta.1",
|
|
34
|
+
"@elliots/typical-compiler-win32-x64": "0.2.0-beta.1"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"build:go": "./go/scripts/sync-shims.sh && cd go && go build -o ../bin/typical ./cmd/typical",
|
|
39
|
+
"test": "npx tsx test/e2e.ts"
|
|
40
|
+
}
|
|
41
|
+
}
|