tywrap 0.6.0 → 0.6.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/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +19 -14
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts +17 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +59 -49
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/validation.d.ts +23 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +52 -48
- package/dist/core/validation.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +175 -133
- package/dist/dev.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +15 -0
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +45 -48
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +18 -0
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +30 -24
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +16 -0
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +54 -35
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/tywrap.d.ts +0 -5
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +0 -20
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +50 -58
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js +28 -17
- package/dist/utils/python.js.map +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +22 -16
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/src/core/annotation-parser.ts +23 -18
- package/src/core/discovery.ts +70 -54
- package/src/core/validation.ts +84 -48
- package/src/dev.ts +237 -153
- package/src/runtime/bridge-codec.ts +58 -70
- package/src/runtime/rpc-client.ts +39 -32
- package/src/runtime/subprocess-transport.ts +69 -39
- package/src/tywrap.ts +0 -24
- package/src/utils/cache.ts +59 -61
- package/src/utils/python.ts +33 -20
- package/src/utils/runtime.ts +24 -13
- package/src/version.ts +1 -1
|
@@ -281,26 +281,7 @@ export class RpcClient extends DisposableBase {
|
|
|
281
281
|
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
282
282
|
options?: ExecuteOptions<T>
|
|
283
283
|
): Promise<T> {
|
|
284
|
-
|
|
285
|
-
...message,
|
|
286
|
-
id: this.generateId(),
|
|
287
|
-
protocol: PROTOCOL_ID,
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
return this.execute(async () => {
|
|
291
|
-
// 1. Encode request (validates args)
|
|
292
|
-
const encoded = this.codec.encodeRequest(fullMessage);
|
|
293
|
-
|
|
294
|
-
// 2. Send via transport
|
|
295
|
-
const responseStr = await this.transport.send(
|
|
296
|
-
encoded,
|
|
297
|
-
options?.timeoutMs ?? this.defaultTimeoutMs,
|
|
298
|
-
options?.signal
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
// 3. Decode response (validates result)
|
|
302
|
-
return this.codec.decodeResponse<T>(responseStr);
|
|
303
|
-
}, options);
|
|
284
|
+
return this.sendVia(message, options, responseStr => this.codec.decodeResponse<T>(responseStr));
|
|
304
285
|
}
|
|
305
286
|
|
|
306
287
|
/**
|
|
@@ -321,11 +302,27 @@ export class RpcClient extends DisposableBase {
|
|
|
321
302
|
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
322
303
|
options?: ExecuteOptions<T>
|
|
323
304
|
): Promise<T> {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
305
|
+
return this.sendVia(message, options, responseStr =>
|
|
306
|
+
this.codec.decodeResponseAsync<T>(responseStr)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Shared body for sendMessage/sendMessageAsync: stamp the frame, run the
|
|
312
|
+
* encode -> transport.send -> decode pipeline inside this.execute() (which
|
|
313
|
+
* supplies auto-init, timeout/retry/abort), where the only difference between
|
|
314
|
+
* the sync and Arrow-aware paths is the supplied `decode` step.
|
|
315
|
+
*
|
|
316
|
+
* Behavior-preserving extraction of the two twins; ordering, the
|
|
317
|
+
* `options?.timeoutMs ?? this.defaultTimeoutMs` fallback, and the
|
|
318
|
+
* `this.execute(..., options)` wrapping are unchanged.
|
|
319
|
+
*/
|
|
320
|
+
private async sendVia<T>(
|
|
321
|
+
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
322
|
+
options: ExecuteOptions<T> | undefined,
|
|
323
|
+
decode: (responseStr: string) => T | Promise<T>
|
|
324
|
+
): Promise<T> {
|
|
325
|
+
const fullMessage = this.stampMessage(message);
|
|
329
326
|
|
|
330
327
|
return this.execute(async () => {
|
|
331
328
|
// 1. Encode request (validates args)
|
|
@@ -338,8 +335,8 @@ export class RpcClient extends DisposableBase {
|
|
|
338
335
|
options?.signal
|
|
339
336
|
);
|
|
340
337
|
|
|
341
|
-
// 3. Decode response
|
|
342
|
-
return
|
|
338
|
+
// 3. Decode response (sync or Arrow-aware, per caller)
|
|
339
|
+
return decode(responseStr);
|
|
343
340
|
}, options);
|
|
344
341
|
}
|
|
345
342
|
|
|
@@ -364,11 +361,7 @@ export class RpcClient extends DisposableBase {
|
|
|
364
361
|
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
365
362
|
opts?: { timeoutMs?: number; signal?: AbortSignal }
|
|
366
363
|
): Promise<T> {
|
|
367
|
-
const fullMessage
|
|
368
|
-
...message,
|
|
369
|
-
id: this.generateId(),
|
|
370
|
-
protocol: PROTOCOL_ID,
|
|
371
|
-
};
|
|
364
|
+
const fullMessage = this.stampMessage(message);
|
|
372
365
|
const encoded = this.codec.encodeRequest(fullMessage);
|
|
373
366
|
const responseStr = await transport.send(
|
|
374
367
|
encoded,
|
|
@@ -378,6 +371,20 @@ export class RpcClient extends DisposableBase {
|
|
|
378
371
|
return this.codec.decodeResponseAsync<T>(responseStr);
|
|
379
372
|
}
|
|
380
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Stamp a partial message into a full wire frame: assign the next request id
|
|
376
|
+
* and the protocol marker. The single id counter + protocol stamping live
|
|
377
|
+
* here so every send path (sendMessage/sendMessageAsync/sendOn) is correlated
|
|
378
|
+
* identically.
|
|
379
|
+
*/
|
|
380
|
+
private stampMessage(message: Omit<ProtocolMessage, 'id' | 'protocol'>): ProtocolMessage {
|
|
381
|
+
return {
|
|
382
|
+
...message,
|
|
383
|
+
id: this.generateId(),
|
|
384
|
+
protocol: PROTOCOL_ID,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
381
388
|
/**
|
|
382
389
|
* Generate a unique request ID.
|
|
383
390
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { spawn, type ChildProcess } from 'child_process';
|
|
17
|
+
import type { Writable } from 'stream';
|
|
17
18
|
import { DisposableBase } from './bounded-context.js';
|
|
18
19
|
import { BridgeDisposedError, BridgeProtocolError, BridgeTimeoutError } from './errors.js';
|
|
19
20
|
import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
|
|
@@ -764,6 +765,67 @@ export class SubprocessTransport extends DisposableBase implements Transport {
|
|
|
764
765
|
});
|
|
765
766
|
}
|
|
766
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Reject and clear every entry currently in the write queue.
|
|
770
|
+
* Clears each entry's timeout before rejecting so no late timer fires.
|
|
771
|
+
*/
|
|
772
|
+
private rejectAllQueuedWrites(error: Error): void {
|
|
773
|
+
for (const q of this.writeQueue) {
|
|
774
|
+
this.clearQueuedWriteTimeout(q);
|
|
775
|
+
q.reject(error);
|
|
776
|
+
}
|
|
777
|
+
this.writeQueue.length = 0;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Process a single dequeued write entry: enforce the fallback queue timeout,
|
|
782
|
+
* then attempt the stdin write. The returned status tells {@link flushWriteQueue}
|
|
783
|
+
* how to proceed:
|
|
784
|
+
* - `'continue'`: entry settled (written or timed out); process the next entry.
|
|
785
|
+
* - `'backpressure'`: write accepted but stream is full; pause until the next drain.
|
|
786
|
+
* - `'error'`: synchronous write failure; the caller must reject the remaining queue.
|
|
787
|
+
*
|
|
788
|
+
* The entry's own timeout must already be cleared by the caller before this runs.
|
|
789
|
+
*/
|
|
790
|
+
private processQueuedWrite(
|
|
791
|
+
queued: QueuedWrite,
|
|
792
|
+
stdin: Writable,
|
|
793
|
+
now: number
|
|
794
|
+
): 'continue' | 'backpressure' | 'error' {
|
|
795
|
+
// Check for write queue timeout (fallback check, timer should have handled this)
|
|
796
|
+
if (now - queued.queuedAt > this.writeQueueTimeoutMs) {
|
|
797
|
+
queued.reject(
|
|
798
|
+
new BridgeTimeoutError(
|
|
799
|
+
`Write queue timeout: entry waited ${now - queued.queuedAt}ms (limit: ${this.writeQueueTimeoutMs}ms)`
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
return 'continue';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const canWrite = stdin.write(queued.data);
|
|
807
|
+
|
|
808
|
+
if (canWrite) {
|
|
809
|
+
queued.resolve();
|
|
810
|
+
return 'continue';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Backpressure - this write has been accepted by stream buffer.
|
|
814
|
+
// Pause further queued writes until the next "drain" event.
|
|
815
|
+
queued.resolve();
|
|
816
|
+
this.draining = true;
|
|
817
|
+
return 'backpressure';
|
|
818
|
+
} catch (err) {
|
|
819
|
+
// Synchronous write error (e.g., EPIPE) - reject this entry; caller rejects the rest.
|
|
820
|
+
const errorMessage = err instanceof Error ? err.message : 'unknown';
|
|
821
|
+
const error = new BridgeProtocolError(this.withStderrTail(`Write error: ${errorMessage}`));
|
|
822
|
+
queued.reject(error);
|
|
823
|
+
this.rejectAllQueuedWrites(error);
|
|
824
|
+
this.markForRestart();
|
|
825
|
+
return 'error';
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
767
829
|
/**
|
|
768
830
|
* Flush queued writes when backpressure clears.
|
|
769
831
|
*/
|
|
@@ -771,13 +833,12 @@ export class SubprocessTransport extends DisposableBase implements Transport {
|
|
|
771
833
|
const now = Date.now();
|
|
772
834
|
|
|
773
835
|
while (this.writeQueue.length > 0 && !this.draining) {
|
|
774
|
-
|
|
836
|
+
const stdin = this.process?.stdin;
|
|
837
|
+
if (!stdin || this.processExited) {
|
|
775
838
|
// Process died - reject all queued writes
|
|
776
|
-
|
|
777
|
-
this.
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
this.writeQueue.length = 0;
|
|
839
|
+
this.rejectAllQueuedWrites(
|
|
840
|
+
new BridgeProtocolError(this.withStderrTail('Process stdin not available'))
|
|
841
|
+
);
|
|
781
842
|
this.markForRestart();
|
|
782
843
|
return;
|
|
783
844
|
}
|
|
@@ -790,39 +851,8 @@ export class SubprocessTransport extends DisposableBase implements Transport {
|
|
|
790
851
|
// Clear the timeout since we're processing this entry now
|
|
791
852
|
this.clearQueuedWriteTimeout(queued);
|
|
792
853
|
|
|
793
|
-
|
|
794
|
-
if (
|
|
795
|
-
queued.reject(
|
|
796
|
-
new BridgeTimeoutError(
|
|
797
|
-
`Write queue timeout: entry waited ${now - queued.queuedAt}ms (limit: ${this.writeQueueTimeoutMs}ms)`
|
|
798
|
-
)
|
|
799
|
-
);
|
|
800
|
-
continue; // Process next entry
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
try {
|
|
804
|
-
const canWrite = this.process.stdin.write(queued.data);
|
|
805
|
-
|
|
806
|
-
if (canWrite) {
|
|
807
|
-
queued.resolve();
|
|
808
|
-
} else {
|
|
809
|
-
// Backpressure - this write has been accepted by stream buffer.
|
|
810
|
-
// Pause further queued writes until the next "drain" event.
|
|
811
|
-
queued.resolve();
|
|
812
|
-
this.draining = true;
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
} catch (err) {
|
|
816
|
-
// Synchronous write error (e.g., EPIPE) - reject this and all remaining writes
|
|
817
|
-
const errorMessage = err instanceof Error ? err.message : 'unknown';
|
|
818
|
-
const error = new BridgeProtocolError(this.withStderrTail(`Write error: ${errorMessage}`));
|
|
819
|
-
queued.reject(error);
|
|
820
|
-
for (const q of this.writeQueue) {
|
|
821
|
-
this.clearQueuedWriteTimeout(q);
|
|
822
|
-
q.reject(error);
|
|
823
|
-
}
|
|
824
|
-
this.writeQueue.length = 0;
|
|
825
|
-
this.markForRestart();
|
|
854
|
+
const status = this.processQueuedWrite(queued, stdin, now);
|
|
855
|
+
if (status === 'backpressure' || status === 'error') {
|
|
826
856
|
return;
|
|
827
857
|
}
|
|
828
858
|
}
|
package/src/tywrap.ts
CHANGED
|
@@ -52,30 +52,6 @@ export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<Tywr
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export default tywrap;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Append minimal tsd tests for a generated module (optional dev aid)
|
|
59
|
-
*/
|
|
60
|
-
export async function emitTypeTestsForModule(
|
|
61
|
-
moduleName: string,
|
|
62
|
-
outDir = 'test-d/generated'
|
|
63
|
-
): Promise<void> {
|
|
64
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
65
|
-
const { join } = await import('path');
|
|
66
|
-
try {
|
|
67
|
-
await mkdir(outDir, { recursive: true });
|
|
68
|
-
} catch {}
|
|
69
|
-
const filePath = join(outDir, `${moduleName}.test-d.ts`);
|
|
70
|
-
const content = `import { expectType } from 'tsd';
|
|
71
|
-
import * as mod from '../../generated/${moduleName}.generated.ts';
|
|
72
|
-
|
|
73
|
-
// Opportunistic: ensure module namespace is an object
|
|
74
|
-
expectType<Record<string, unknown>>(mod);
|
|
75
|
-
`;
|
|
76
|
-
await writeFile(filePath, content, 'utf-8');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
55
|
export interface GenerateRunOptions {
|
|
80
56
|
/**
|
|
81
57
|
* If true, do not write files; instead compare generated output to what's on disk.
|
package/src/utils/cache.ts
CHANGED
|
@@ -149,75 +149,73 @@ export class ArtifactCache {
|
|
|
149
149
|
hash.update(prefix);
|
|
150
150
|
hash.update('\0');
|
|
151
151
|
|
|
152
|
-
// Add inputs
|
|
152
|
+
// Add inputs. Each input contributes a type tag, an optional payload, and a
|
|
153
|
+
// boundary separator so that "1" vs 1 and ["a","bc"] vs ["ab","c"] never collide.
|
|
153
154
|
for (const input of inputs) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
hash.update(
|
|
158
|
-
hash.update('\0');
|
|
159
|
-
continue;
|
|
155
|
+
const { tag, payload } = ArtifactCache.encodeKeyInput(input);
|
|
156
|
+
hash.update(tag);
|
|
157
|
+
if (payload !== undefined) {
|
|
158
|
+
hash.update(payload);
|
|
160
159
|
}
|
|
161
|
-
if (input === null) {
|
|
162
|
-
hash.update('null:');
|
|
163
|
-
hash.update('\0');
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (typeof input === 'string') {
|
|
167
|
-
hash.update('str:');
|
|
168
|
-
hash.update(input);
|
|
169
|
-
hash.update('\0');
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (typeof input === 'number') {
|
|
173
|
-
hash.update('num:');
|
|
174
|
-
hash.update(String(input));
|
|
175
|
-
hash.update('\0');
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (typeof input === 'boolean') {
|
|
179
|
-
hash.update('bool:');
|
|
180
|
-
hash.update(input ? '1' : '0');
|
|
181
|
-
hash.update('\0');
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (typeof input === 'bigint') {
|
|
185
|
-
hash.update('bigint:');
|
|
186
|
-
hash.update(input.toString());
|
|
187
|
-
hash.update('\0');
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (Buffer.isBuffer(input)) {
|
|
191
|
-
hash.update('buf:');
|
|
192
|
-
hash.update(input);
|
|
193
|
-
hash.update('\0');
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (typeof input === 'symbol') {
|
|
198
|
-
hash.update('sym:');
|
|
199
|
-
hash.update(input.toString());
|
|
200
|
-
hash.update('\0');
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
hash.update('json:');
|
|
205
|
-
let json: string;
|
|
206
|
-
try {
|
|
207
|
-
json =
|
|
208
|
-
JSON.stringify(input, (_key, value) =>
|
|
209
|
-
typeof value === 'symbol' ? value.toString() : value
|
|
210
|
-
) ?? 'undefined';
|
|
211
|
-
} catch {
|
|
212
|
-
json = String(input);
|
|
213
|
-
}
|
|
214
|
-
hash.update(json);
|
|
215
160
|
hash.update('\0');
|
|
216
161
|
}
|
|
217
162
|
|
|
218
163
|
return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
|
|
219
164
|
}
|
|
220
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Encode a single key input as a type tag plus optional payload.
|
|
168
|
+
* Why: disambiguate types so "1" and 1 don't collide, and keep input boundaries so
|
|
169
|
+
* ["a", "bc"] doesn't collide with ["ab", "c"].
|
|
170
|
+
*/
|
|
171
|
+
private static encodeKeyInput(input: unknown): {
|
|
172
|
+
tag: string;
|
|
173
|
+
payload?: string | Buffer;
|
|
174
|
+
} {
|
|
175
|
+
if (input === undefined) {
|
|
176
|
+
return { tag: 'undef:' };
|
|
177
|
+
}
|
|
178
|
+
if (input === null) {
|
|
179
|
+
return { tag: 'null:' };
|
|
180
|
+
}
|
|
181
|
+
if (typeof input === 'string') {
|
|
182
|
+
return { tag: 'str:', payload: input };
|
|
183
|
+
}
|
|
184
|
+
if (typeof input === 'number') {
|
|
185
|
+
return { tag: 'num:', payload: String(input) };
|
|
186
|
+
}
|
|
187
|
+
if (typeof input === 'boolean') {
|
|
188
|
+
return { tag: 'bool:', payload: input ? '1' : '0' };
|
|
189
|
+
}
|
|
190
|
+
if (typeof input === 'bigint') {
|
|
191
|
+
return { tag: 'bigint:', payload: input.toString() };
|
|
192
|
+
}
|
|
193
|
+
if (Buffer.isBuffer(input)) {
|
|
194
|
+
return { tag: 'buf:', payload: input };
|
|
195
|
+
}
|
|
196
|
+
if (typeof input === 'symbol') {
|
|
197
|
+
return { tag: 'sym:', payload: input.toString() };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { tag: 'json:', payload: ArtifactCache.stringifyKeyInput(input) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Stringify an arbitrary key input for hashing, normalizing symbols and
|
|
205
|
+
* falling back to String() when JSON serialization fails.
|
|
206
|
+
*/
|
|
207
|
+
private static stringifyKeyInput(input: unknown): string {
|
|
208
|
+
try {
|
|
209
|
+
return (
|
|
210
|
+
JSON.stringify(input, (_key, value) =>
|
|
211
|
+
typeof value === 'symbol' ? value.toString() : value
|
|
212
|
+
) ?? 'undefined'
|
|
213
|
+
);
|
|
214
|
+
} catch {
|
|
215
|
+
return String(input);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
221
219
|
/**
|
|
222
220
|
* Get entry from cache with performance tracking
|
|
223
221
|
*/
|
package/src/utils/python.ts
CHANGED
|
@@ -44,30 +44,43 @@ export function getDefaultPythonPath(): string {
|
|
|
44
44
|
return getPythonExecutableName();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Whether the configured pythonPath is the default interpreter (or unset),
|
|
49
|
+
* meaning a virtual environment lookup should take precedence.
|
|
50
|
+
*/
|
|
51
|
+
function usesDefaultPython(pythonPath: string | undefined): boolean {
|
|
52
|
+
return !pythonPath || pythonPath === 'python3' || pythonPath === 'python';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the Python executable inside a virtual environment, preferring the
|
|
57
|
+
* Node path module when available and falling back to cross-runtime pathUtils.
|
|
58
|
+
*/
|
|
59
|
+
async function resolveVenvPython(virtualEnv: string, cwd: string): Promise<string> {
|
|
60
|
+
const binDir = getVenvBinDir();
|
|
61
|
+
const exe = getVenvPythonExe();
|
|
62
|
+
const pathMod = await loadNodePathModule();
|
|
63
|
+
|
|
64
|
+
if (pathMod) {
|
|
65
|
+
const venvRoot = pathMod.resolve(cwd, virtualEnv);
|
|
66
|
+
return pathMod.join(venvRoot, binDir, exe);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const venvRoot = isAbsolutePath(virtualEnv)
|
|
70
|
+
? pathUtils.join(virtualEnv)
|
|
71
|
+
: pathUtils.join(cwd, virtualEnv);
|
|
72
|
+
return pathUtils.join(venvRoot, binDir, exe);
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
export async function resolvePythonExecutable(options: PythonResolveOptions = {}): Promise<string> {
|
|
48
76
|
const pythonPath = options.pythonPath?.trim();
|
|
49
77
|
const virtualEnv = options.virtualEnv?.trim();
|
|
50
78
|
|
|
51
|
-
if (virtualEnv) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
(typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '.');
|
|
57
|
-
const binDir = getVenvBinDir();
|
|
58
|
-
const exe = getVenvPythonExe();
|
|
59
|
-
const pathMod = await loadNodePathModule();
|
|
60
|
-
|
|
61
|
-
if (pathMod) {
|
|
62
|
-
const venvRoot = pathMod.resolve(cwd, virtualEnv);
|
|
63
|
-
return pathMod.join(venvRoot, binDir, exe);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const venvRoot = isAbsolutePath(virtualEnv)
|
|
67
|
-
? pathUtils.join(virtualEnv)
|
|
68
|
-
: pathUtils.join(cwd, virtualEnv);
|
|
69
|
-
return pathUtils.join(venvRoot, binDir, exe);
|
|
70
|
-
}
|
|
79
|
+
if (virtualEnv && usesDefaultPython(pythonPath)) {
|
|
80
|
+
const cwd =
|
|
81
|
+
options.cwd ??
|
|
82
|
+
(typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '.');
|
|
83
|
+
return resolveVenvPython(virtualEnv, cwd);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
if (pythonPath) {
|
package/src/utils/runtime.ts
CHANGED
|
@@ -229,26 +229,37 @@ async function loadPathModule(): Promise<PathModule | null> {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Apply a single path segment to the accumulated normalized segments,
|
|
234
|
+
* resolving '..' and dropping '.'/empty segments in place.
|
|
235
|
+
*/
|
|
236
|
+
function applyPathSegment(normalized: string[], segment: string, isAbsolute: boolean): void {
|
|
237
|
+
if (segment === '.' || segment === '') {
|
|
238
|
+
return; // Skip current directory and empty segments
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (segment !== '..') {
|
|
242
|
+
normalized.push(segment);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// '..' segment: go up one directory when possible.
|
|
247
|
+
if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') {
|
|
248
|
+
normalized.pop();
|
|
249
|
+
} else if (!isAbsolute) {
|
|
250
|
+
normalized.push(segment); // Keep '..' if not absolute and at root
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
/**
|
|
233
255
|
* Normalize path by stripping '.' and resolving '..' components
|
|
234
256
|
*/
|
|
235
257
|
function normalizePath(path: string): string {
|
|
236
258
|
const isAbsolute = path.startsWith('/');
|
|
237
|
-
const segments = path.split('/');
|
|
238
259
|
const normalized: string[] = [];
|
|
239
260
|
|
|
240
|
-
for (const segment of
|
|
241
|
-
|
|
242
|
-
continue; // Skip current directory and empty segments
|
|
243
|
-
} else if (segment === '..') {
|
|
244
|
-
if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') {
|
|
245
|
-
normalized.pop(); // Go up one directory
|
|
246
|
-
} else if (!isAbsolute) {
|
|
247
|
-
normalized.push(segment); // Keep '..' if not absolute and at root
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
normalized.push(segment);
|
|
251
|
-
}
|
|
261
|
+
for (const segment of path.split('/')) {
|
|
262
|
+
applyPathSegment(normalized, segment, isAbsolute);
|
|
252
263
|
}
|
|
253
264
|
|
|
254
265
|
const result = normalized.join('/');
|
package/src/version.ts
CHANGED