tywrap 0.1.0

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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +263 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/index.d.ts +41 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/config/index.js +234 -0
  10. package/dist/config/index.js.map +1 -0
  11. package/dist/core/analyzer.d.ts +63 -0
  12. package/dist/core/analyzer.d.ts.map +1 -0
  13. package/dist/core/analyzer.js +676 -0
  14. package/dist/core/analyzer.js.map +1 -0
  15. package/dist/core/discovery.d.ts +85 -0
  16. package/dist/core/discovery.d.ts.map +1 -0
  17. package/dist/core/discovery.js +344 -0
  18. package/dist/core/discovery.js.map +1 -0
  19. package/dist/core/generator.d.ts +37 -0
  20. package/dist/core/generator.d.ts.map +1 -0
  21. package/dist/core/generator.js +368 -0
  22. package/dist/core/generator.js.map +1 -0
  23. package/dist/core/mapper.d.ts +40 -0
  24. package/dist/core/mapper.d.ts.map +1 -0
  25. package/dist/core/mapper.js +401 -0
  26. package/dist/core/mapper.js.map +1 -0
  27. package/dist/core/validation.d.ts +79 -0
  28. package/dist/core/validation.d.ts.map +1 -0
  29. package/dist/core/validation.js +486 -0
  30. package/dist/core/validation.js.map +1 -0
  31. package/dist/index.d.ts +26 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/runtime/base.d.ts +12 -0
  36. package/dist/runtime/base.d.ts.map +1 -0
  37. package/dist/runtime/base.js +6 -0
  38. package/dist/runtime/base.js.map +1 -0
  39. package/dist/runtime/errors.d.ts +17 -0
  40. package/dist/runtime/errors.d.ts.map +1 -0
  41. package/dist/runtime/errors.js +21 -0
  42. package/dist/runtime/errors.js.map +1 -0
  43. package/dist/runtime/http.d.ts +22 -0
  44. package/dist/runtime/http.d.ts.map +1 -0
  45. package/dist/runtime/http.js +79 -0
  46. package/dist/runtime/http.js.map +1 -0
  47. package/dist/runtime/index.d.ts +8 -0
  48. package/dist/runtime/index.d.ts.map +1 -0
  49. package/dist/runtime/index.js +17 -0
  50. package/dist/runtime/index.js.map +1 -0
  51. package/dist/runtime/node.d.ts +48 -0
  52. package/dist/runtime/node.d.ts.map +1 -0
  53. package/dist/runtime/node.js +328 -0
  54. package/dist/runtime/node.js.map +1 -0
  55. package/dist/runtime/optimized-node.d.ts +131 -0
  56. package/dist/runtime/optimized-node.d.ts.map +1 -0
  57. package/dist/runtime/optimized-node.js +622 -0
  58. package/dist/runtime/optimized-node.js.map +1 -0
  59. package/dist/runtime/protocol.d.ts +3 -0
  60. package/dist/runtime/protocol.d.ts.map +1 -0
  61. package/dist/runtime/protocol.js +3 -0
  62. package/dist/runtime/protocol.js.map +1 -0
  63. package/dist/runtime/pyodide.d.ts +27 -0
  64. package/dist/runtime/pyodide.d.ts.map +1 -0
  65. package/dist/runtime/pyodide.js +191 -0
  66. package/dist/runtime/pyodide.js.map +1 -0
  67. package/dist/tools/python_suite.js +173 -0
  68. package/dist/tools/python_suite.js.map +1 -0
  69. package/dist/types/index.d.ts +299 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +5 -0
  72. package/dist/types/index.js.map +1 -0
  73. package/dist/tywrap.d.ts +29 -0
  74. package/dist/tywrap.d.ts.map +1 -0
  75. package/dist/tywrap.js +589 -0
  76. package/dist/tywrap.js.map +1 -0
  77. package/dist/utils/bundle-optimizer.d.ts +182 -0
  78. package/dist/utils/bundle-optimizer.d.ts.map +1 -0
  79. package/dist/utils/bundle-optimizer.js +680 -0
  80. package/dist/utils/bundle-optimizer.js.map +1 -0
  81. package/dist/utils/cache.d.ts +149 -0
  82. package/dist/utils/cache.d.ts.map +1 -0
  83. package/dist/utils/cache.js +492 -0
  84. package/dist/utils/cache.js.map +1 -0
  85. package/dist/utils/codec.d.ts +94 -0
  86. package/dist/utils/codec.d.ts.map +1 -0
  87. package/dist/utils/codec.js +150 -0
  88. package/dist/utils/codec.js.map +1 -0
  89. package/dist/utils/logger.d.ts +42 -0
  90. package/dist/utils/logger.d.ts.map +1 -0
  91. package/dist/utils/logger.js +140 -0
  92. package/dist/utils/logger.js.map +1 -0
  93. package/dist/utils/memory-profiler.d.ts +123 -0
  94. package/dist/utils/memory-profiler.d.ts.map +1 -0
  95. package/dist/utils/memory-profiler.js +426 -0
  96. package/dist/utils/memory-profiler.js.map +1 -0
  97. package/dist/utils/parallel-processor.d.ts +146 -0
  98. package/dist/utils/parallel-processor.d.ts.map +1 -0
  99. package/dist/utils/parallel-processor.js +636 -0
  100. package/dist/utils/parallel-processor.js.map +1 -0
  101. package/dist/utils/python.d.ts +8 -0
  102. package/dist/utils/python.d.ts.map +1 -0
  103. package/dist/utils/python.js +56 -0
  104. package/dist/utils/python.js.map +1 -0
  105. package/dist/utils/runtime.d.ts +153 -0
  106. package/dist/utils/runtime.d.ts.map +1 -0
  107. package/dist/utils/runtime.js +519 -0
  108. package/dist/utils/runtime.js.map +1 -0
  109. package/package.json +136 -0
  110. package/runtime/python_bridge.py +476 -0
  111. package/src/cli.ts +331 -0
  112. package/src/config/index.ts +289 -0
  113. package/src/core/analyzer.ts +788 -0
  114. package/src/core/discovery.ts +415 -0
  115. package/src/core/generator.ts +447 -0
  116. package/src/core/mapper.ts +504 -0
  117. package/src/core/validation.ts +693 -0
  118. package/src/index.ts +83 -0
  119. package/src/runtime/base.ts +32 -0
  120. package/src/runtime/errors.ts +19 -0
  121. package/src/runtime/http.ts +132 -0
  122. package/src/runtime/index.ts +24 -0
  123. package/src/runtime/node.ts +431 -0
  124. package/src/runtime/optimized-node.ts +838 -0
  125. package/src/runtime/protocol.ts +2 -0
  126. package/src/runtime/pyodide.ts +228 -0
  127. package/src/types/global.d.ts +60 -0
  128. package/src/types/index.ts +410 -0
  129. package/src/tywrap.ts +669 -0
  130. package/src/utils/bundle-optimizer.ts +876 -0
  131. package/src/utils/cache.ts +634 -0
  132. package/src/utils/codec.ts +275 -0
  133. package/src/utils/logger.ts +201 -0
  134. package/src/utils/memory-profiler.ts +582 -0
  135. package/src/utils/parallel-processor.ts +879 -0
  136. package/src/utils/python.ts +79 -0
  137. package/src/utils/runtime.ts +616 -0
package/src/tywrap.ts ADDED
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Main tywrap API entry point
3
+ */
4
+
5
+ import { CodeGenerator } from './core/generator.js';
6
+ import { TypeMapper } from './core/mapper.js';
7
+ import { createConfig } from './config/index.js';
8
+ import type {
9
+ TywrapOptions,
10
+ PythonFunction,
11
+ PythonModule as TSPythonModule,
12
+ PythonClass,
13
+ Parameter,
14
+ PythonType,
15
+ } from './types/index.js';
16
+ import { fsUtils, pathUtils, processUtils, hashUtils } from './utils/runtime.js';
17
+ import { globalCache } from './utils/cache.js';
18
+ import { globalParallelProcessor } from './utils/parallel-processor.js';
19
+ import { resolvePythonExecutable } from './utils/python.js';
20
+
21
+ // Collect unknown typing constructs encountered during annotation parsing (per-generate run)
22
+ let unknownTypeNamesCollector: Map<string, number> = new Map();
23
+ function recordUnknown(name: string): void {
24
+ const prev = unknownTypeNamesCollector.get(name) ?? 0;
25
+ unknownTypeNamesCollector.set(name, prev + 1);
26
+ }
27
+
28
+ /**
29
+ * Main tywrap function
30
+ */
31
+ export interface TywrapInstance {
32
+ mapper: TypeMapper;
33
+ generator: CodeGenerator;
34
+ options: Partial<TywrapOptions>;
35
+ }
36
+
37
+ export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<TywrapInstance> {
38
+ const mapper = new TypeMapper({ presets: options.types?.presets });
39
+ const generator = new CodeGenerator(mapper);
40
+
41
+ globalCache.setDebug(options.debug ?? false);
42
+ globalParallelProcessor.setDebug(options.debug ?? false);
43
+
44
+ return {
45
+ mapper,
46
+ generator,
47
+ options,
48
+ };
49
+ }
50
+
51
+ export default tywrap;
52
+
53
+ /**
54
+ * Append minimal tsd tests for a generated module (optional dev aid)
55
+ */
56
+ export async function emitTypeTestsForModule(
57
+ moduleName: string,
58
+ outDir = 'test-d/generated'
59
+ ): Promise<void> {
60
+ const { writeFile, mkdir } = await import('fs/promises');
61
+ const { join } = await import('path');
62
+ try {
63
+ await mkdir(outDir, { recursive: true });
64
+ } catch {}
65
+ const filePath = join(outDir, `${moduleName}.test-d.ts`);
66
+ const content = `import { expectType } from 'tsd';
67
+ import * as mod from '../../generated/${moduleName}.generated.ts';
68
+
69
+ // Opportunistic: ensure module namespace is an object
70
+ expectType<Record<string, unknown>>(mod);
71
+ `;
72
+ await writeFile(filePath, content, 'utf-8');
73
+ }
74
+
75
+ /**
76
+ * Generate TypeScript wrappers for configured Python modules.
77
+ * Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
78
+ */
79
+ export async function generate(
80
+ options: Partial<TywrapOptions>
81
+ ): Promise<{ written: string[]; warnings: string[] }> {
82
+ const resolvedOptions = createConfig(options);
83
+ const instance = await tywrap(resolvedOptions);
84
+ const written: string[] = [];
85
+ const warnings: string[] = [];
86
+ const outputDir = resolvedOptions.output.dir;
87
+ const caching = resolvedOptions.performance.caching;
88
+ const cacheDir = '.tywrap/cache';
89
+ const pythonPath = await resolvePythonExecutable({
90
+ pythonPath: resolvedOptions.runtime?.node?.pythonPath,
91
+ virtualEnv: resolvedOptions.runtime?.node?.virtualEnv,
92
+ });
93
+
94
+ // Ensure directory exists (Node-only best-effort)
95
+ try {
96
+ const modFs = await import('fs/promises');
97
+ await modFs.mkdir(outputDir, { recursive: true });
98
+ } catch {
99
+ // ignore in non-node or if already exists
100
+ }
101
+
102
+ const modules = resolvedOptions.pythonModules ?? {};
103
+ for (const entry of Object.entries(modules)) {
104
+ const moduleKey = entry[0];
105
+ // reset collector for each module
106
+ unknownTypeNamesCollector = new Map();
107
+ // Prefer Python IR extractor over TS analyzer with optional cache
108
+ const cacheKey = await computeCacheKey(moduleKey, options);
109
+ let ir: unknown | null = null;
110
+ let irError: string | undefined;
111
+ if (caching && fsUtils.isAvailable()) {
112
+ try {
113
+ const modFs = await import('fs/promises');
114
+ await modFs.mkdir(cacheDir, { recursive: true });
115
+ } catch {}
116
+ try {
117
+ const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
118
+ ir = JSON.parse(cached);
119
+ } catch {
120
+ ir = null;
121
+ }
122
+ }
123
+ if (!ir) {
124
+ const fetchResult = await fetchPythonIr(moduleKey, pythonPath);
125
+ ir = fetchResult.ir;
126
+ irError = fetchResult.error;
127
+ if (ir && caching && fsUtils.isAvailable()) {
128
+ try {
129
+ await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
130
+ } catch {}
131
+ }
132
+ }
133
+ if (!ir) {
134
+ warnings.push(`No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`);
135
+ continue;
136
+ }
137
+
138
+ const moduleModel = transformIrToTsModel(ir);
139
+
140
+ // Generate module code
141
+ const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
142
+ const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
143
+
144
+ // Write file
145
+ const baseName = moduleModel.name || 'module';
146
+ const outPath = pathUtils.join(outputDir, `${baseName}.generated.ts`);
147
+ await fsUtils.writeFile(outPath, gen.typescript);
148
+ written.push(outPath);
149
+
150
+ // Optional .d.ts emission (header-only declarations mirroring exports)
151
+ if (resolvedOptions.output?.declaration) {
152
+ const dtsPath = pathUtils.join(outputDir, `${baseName}.generated.d.ts`);
153
+ const dts = renderDts(gen.typescript);
154
+ await fsUtils.writeFile(dtsPath, dts);
155
+ written.push(dtsPath);
156
+ }
157
+
158
+ // Optional source map emission (placeholder mapping for now)
159
+ if (resolvedOptions.output?.sourceMap) {
160
+ const mapPath = pathUtils.join(outputDir, `${baseName}.generated.ts.map`);
161
+ const map = renderSourceMapPlaceholder(moduleModel.name);
162
+ await fsUtils.writeFile(mapPath, map);
163
+ written.push(mapPath);
164
+ }
165
+
166
+ // Emit warning summary of unknown typing constructs (best-effort)
167
+ if (unknownTypeNamesCollector.size > 0) {
168
+ const entries = Array.from(unknownTypeNamesCollector.entries()).sort((a, b) => b[1] - a[1]);
169
+ const unkList = entries
170
+ .slice(0, 25)
171
+ .map(([n, c]) => `${n}:${c}`)
172
+ .join(', ');
173
+ warnings.push(
174
+ `Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
175
+ );
176
+ // Write JSON report
177
+ try {
178
+ const reportsDir = pathUtils.join('.tywrap', 'reports');
179
+ await fsUtils.writeFile(
180
+ pathUtils.join(reportsDir, `${baseName}.json`),
181
+ JSON.stringify({
182
+ module: baseName,
183
+ unknowns: Object.fromEntries(entries),
184
+ generatedAt: new Date().toISOString(),
185
+ })
186
+ );
187
+ } catch {
188
+ // ignore
189
+ }
190
+ }
191
+ }
192
+
193
+ return { written, warnings };
194
+ }
195
+
196
+ /**
197
+ * Invoke the Python IR CLI to get JSON IR for a module.
198
+ */
199
+ async function fetchPythonIr(
200
+ moduleName: string,
201
+ pythonPath: string
202
+ ): Promise<{ ir: unknown | null; error?: string }> {
203
+ if (!processUtils.isAvailable()) {
204
+ return { ir: null, error: 'Subprocess operations not available in this runtime' };
205
+ }
206
+ try {
207
+ const result = await processUtils.exec(pythonPath, [
208
+ '-m',
209
+ 'tywrap_ir',
210
+ '--module',
211
+ moduleName,
212
+ '--no-pretty',
213
+ ]);
214
+ if (result.code === 0) {
215
+ try {
216
+ return { ir: JSON.parse(result.stdout) };
217
+ } catch {
218
+ return {
219
+ ir: null,
220
+ error: `Failed to parse tywrap_ir output. stderr: ${result.stderr.trim() || 'empty'}`,
221
+ };
222
+ }
223
+ }
224
+
225
+ // Fallback to invoking local __main__.py
226
+ const localMain = pathUtils.join(process.cwd(), 'tywrap_ir', 'tywrap_ir', '__main__.py');
227
+ const fallback = await processUtils.exec(pythonPath, [
228
+ localMain,
229
+ '--module',
230
+ moduleName,
231
+ '--no-pretty',
232
+ ]);
233
+ if (fallback.code !== 0) {
234
+ return {
235
+ ir: null,
236
+ error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || result.stderr.trim() || 'empty'}`,
237
+ };
238
+ }
239
+ try {
240
+ return { ir: JSON.parse(fallback.stdout) };
241
+ } catch {
242
+ return {
243
+ ir: null,
244
+ error: `Failed to parse tywrap_ir fallback output. stderr: ${fallback.stderr.trim() || 'empty'}`,
245
+ };
246
+ }
247
+ } catch (err) {
248
+ return { ir: null, error: err instanceof Error ? err.message : String(err) };
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Convert JSON IR from Python into the internal TypeScript model used by the generator.
254
+ */
255
+ function transformIrToTsModel(ir: unknown): TSPythonModule {
256
+ const obj: Record<string, unknown> =
257
+ typeof ir === 'object' && ir !== null ? (ir as Record<string, unknown>) : {};
258
+ const functions = (obj.functions as unknown[]) ?? [];
259
+ const classes = (obj.classes as unknown[]) ?? [];
260
+ const mapParam = (p: Record<string, unknown>): Parameter => ({
261
+ name: String(p.name ?? ''),
262
+ type: parseAnnotationToPythonType(p.annotation),
263
+ optional: Boolean(p.default),
264
+ varArgs: p.kind === 'VAR_POSITIONAL',
265
+ kwArgs: p.kind === 'VAR_KEYWORD',
266
+ });
267
+
268
+ const mapFunc = (f: Record<string, unknown>): PythonFunction => ({
269
+ name: String(f.name ?? ''),
270
+ signature: {
271
+ parameters: Array.isArray(f.parameters)
272
+ ? (f.parameters as unknown[]).map(v => mapParam((v ?? {}) as Record<string, unknown>))
273
+ : [],
274
+ returnType: parseAnnotationToPythonType(f.returns),
275
+ isAsync: Boolean(f.is_async),
276
+ isGenerator: Boolean(f.is_generator),
277
+ },
278
+ docstring: (f.docstring as string | undefined) ?? undefined,
279
+ decorators: [],
280
+ isAsync: Boolean(f.is_async),
281
+ isGenerator: Boolean(f.is_generator),
282
+ returnType: parseAnnotationToPythonType(f.returns),
283
+ parameters: Array.isArray(f.parameters)
284
+ ? (f.parameters as unknown[]).map(v => mapParam((v ?? {}) as Record<string, unknown>))
285
+ : [],
286
+ });
287
+
288
+ const mapClass = (c: Record<string, unknown>): PythonClass => ({
289
+ name: String(c.name ?? ''),
290
+ bases: Array.isArray(c.bases) ? (c.bases as string[]) : [],
291
+ methods: Array.isArray(c.methods)
292
+ ? (c.methods as unknown[]).map(v => mapFunc((v ?? {}) as Record<string, unknown>))
293
+ : [],
294
+ properties: Array.isArray(c.fields)
295
+ ? ((c.fields as unknown[]).map(v => {
296
+ const p = (v ?? {}) as Record<string, unknown>;
297
+ const optional = Boolean(p.default);
298
+ return {
299
+ name: String(p.name ?? ''),
300
+ type: parseAnnotationToPythonType(p.annotation),
301
+ readonly: false,
302
+ setter: false,
303
+ getter: true,
304
+ optional,
305
+ } as unknown as never;
306
+ }) as unknown as PythonClass['properties'])
307
+ : [],
308
+ docstring: (c.docstring as string | undefined) ?? undefined,
309
+ decorators: (c.typed_dict as boolean) ? ['__typed_dict__'] : [],
310
+ kind: (c.typed_dict as boolean)
311
+ ? 'typed_dict'
312
+ : (c.is_protocol as boolean)
313
+ ? 'protocol'
314
+ : (c.is_namedtuple as boolean)
315
+ ? 'namedtuple'
316
+ : (c.is_dataclass as boolean)
317
+ ? 'dataclass'
318
+ : (c.is_pydantic as boolean)
319
+ ? 'pydantic'
320
+ : 'class',
321
+ });
322
+
323
+ const moduleModel: TSPythonModule = {
324
+ name: (obj.module as string) ?? 'module',
325
+ path: undefined,
326
+ version:
327
+ typeof (obj.metadata as Record<string, unknown> | undefined)?.package_version === 'string'
328
+ ? ((obj.metadata as Record<string, unknown> | undefined)?.package_version as string)
329
+ : undefined,
330
+ functions: functions.map(v => mapFunc((v ?? {}) as Record<string, unknown>)),
331
+ classes: classes.map(v => mapClass((v ?? {}) as Record<string, unknown>)),
332
+ imports: [],
333
+ exports: [],
334
+ };
335
+ return moduleModel;
336
+ }
337
+
338
+ function unknownType(): PythonType {
339
+ return { kind: 'custom', name: 'Any', module: 'typing' };
340
+ }
341
+
342
+ // Parse Python IR annotation string -> PythonType
343
+ function parseAnnotationToPythonType(annotation: unknown, depth = 0): PythonType {
344
+ if (annotation === null || annotation === undefined) {
345
+ return unknownType();
346
+ }
347
+ if (depth > 100) {
348
+ return unknownType();
349
+ }
350
+ const raw = String(annotation).trim();
351
+
352
+ // Handle built-in class repr: <class 'int'>
353
+ const classMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
354
+ if (classMatch) {
355
+ const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
356
+ const name = (inner.split('.').pop() ?? '').toString();
357
+ return mapSimpleName(name);
358
+ }
359
+
360
+ // PEP 604 unions: int | str | None
361
+ if (raw.includes(' | ')) {
362
+ const parts = splitTopLevel(raw, '|');
363
+ const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
364
+ return { kind: 'union', types };
365
+ }
366
+
367
+ // typing.Union[...]
368
+ if (raw.startsWith('typing.Union[') || raw.startsWith('Union[')) {
369
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
370
+ const parts = splitTopLevel(inner, ',');
371
+ const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
372
+ return { kind: 'union', types };
373
+ }
374
+
375
+ // Optional[T]
376
+ if (raw.startsWith('typing.Optional[') || raw.startsWith('Optional[')) {
377
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
378
+ const base = parseAnnotationToPythonType(inner, depth + 1);
379
+ return { kind: 'optional', type: base };
380
+ }
381
+
382
+ // Literal[...] -> literal values union
383
+ if (raw.startsWith('typing.Literal[') || raw.startsWith('Literal[')) {
384
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
385
+ const parts = splitTopLevel(inner, ',');
386
+ if (parts.length === 1) {
387
+ return mapLiteral(String(parts[0] ?? '').trim());
388
+ }
389
+ return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) } as PythonType;
390
+ }
391
+
392
+ // typing_extensions wrappers: ClassVar[T], Final[T], Required[T], NotRequired[T]
393
+ const extMatch = raw.match(
394
+ /^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/
395
+ );
396
+ if (extMatch) {
397
+ const inner = extMatch[3] ?? '';
398
+ const base = parseAnnotationToPythonType(inner, depth + 1);
399
+ return base;
400
+ }
401
+
402
+ // LiteralString
403
+ if (raw === 'typing.LiteralString' || raw === 'LiteralString') {
404
+ return { kind: 'primitive', name: 'str' } as PythonType;
405
+ }
406
+
407
+ // Callable[[...], R]
408
+ if (raw.startsWith('typing.Callable[') || raw.startsWith('Callable[')) {
409
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
410
+ const parts = splitTopLevel(inner, ',');
411
+ if (parts.length >= 2) {
412
+ const paramsPart = (parts[0] ?? '').trim();
413
+ const returnPart = parts.slice(1).join(',').trim();
414
+ const paramInner =
415
+ paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
416
+ const paramTypes = ((): PythonType[] => {
417
+ const trimmed = paramInner.trim();
418
+ if (trimmed === '...' || trimmed === 'Ellipsis') {
419
+ return [{ kind: 'custom', name: '...' } as PythonType];
420
+ }
421
+ return trimmed
422
+ ? splitTopLevel(trimmed, ',').map(p => parseAnnotationToPythonType(p.trim(), depth + 1))
423
+ : [];
424
+ })();
425
+ const returnType = parseAnnotationToPythonType(returnPart, depth + 1);
426
+ return { kind: 'callable', parameters: paramTypes, returnType } as PythonType;
427
+ }
428
+ }
429
+
430
+ // Mapping[K, V] / Dict[K, V] normalization
431
+ if (raw.startsWith('typing.Mapping[') || raw.startsWith('Mapping[')) {
432
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
433
+ const parts = splitTopLevel(inner, ',');
434
+ const itemTypes = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
435
+ return { kind: 'collection', name: 'dict', itemTypes } as PythonType;
436
+ }
437
+
438
+ // Annotated[T, ...] -> annotated node with base and metadata
439
+ if (raw.startsWith('typing.Annotated[') || raw.startsWith('Annotated[')) {
440
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
441
+ const parts = splitTopLevel(inner, ',');
442
+ if (parts.length > 0) {
443
+ const base = parseAnnotationToPythonType((parts[0] ?? '').trim(), depth + 1);
444
+ const metaParts = parts.slice(1).map(p => String(p).trim());
445
+ return { kind: 'annotated', base, metadata: metaParts } as PythonType;
446
+ }
447
+ }
448
+
449
+ // Collections: list[T], dict[K,V], tuple[...], set[T]
450
+ const coll = normalizeCollectionName(raw);
451
+ if (coll) {
452
+ const { name, inner } = coll;
453
+ const itemParts =
454
+ name === 'dict' ? splitTopLevel(inner ?? '', ',') : splitTopLevel(inner ?? '', ',');
455
+ const itemTypes = (inner ? itemParts : []).map(p =>
456
+ parseAnnotationToPythonType(p.trim(), depth + 1)
457
+ );
458
+ return { kind: 'collection', name, itemTypes };
459
+ }
460
+
461
+ // typing.Callable[...] unlikely for now - treat as custom
462
+
463
+ // Bare names like int, str, float, bool, bytes, None
464
+ return mapSimpleName(raw);
465
+ }
466
+
467
+ function mapSimpleName(name: string): PythonType {
468
+ const n = name.replace(/^typing\./, '').trim();
469
+ if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
470
+ // cast to the specific union type without using any
471
+ return { kind: 'primitive', name: n };
472
+ }
473
+ // Track unknown typing-ish names for diagnostics
474
+ if (
475
+ n === 'Any' ||
476
+ n === 'Never' ||
477
+ n === 'LiteralString' ||
478
+ n === 'ClassVar' ||
479
+ n === 'Final' ||
480
+ n === 'TypeAlias' ||
481
+ n === 'Required' ||
482
+ n === 'NotRequired'
483
+ ) {
484
+ try {
485
+ recordUnknown(n);
486
+ } catch {}
487
+ }
488
+ if (n === 'None' || n.toLowerCase() === 'nonetype') {
489
+ return { kind: 'primitive', name: 'None' };
490
+ }
491
+ if (n === 'list' || n === 'List') {
492
+ return { kind: 'collection', name: 'list', itemTypes: [] };
493
+ }
494
+ if (n === 'dict' || n === 'Dict') {
495
+ return { kind: 'collection', name: 'dict', itemTypes: [] };
496
+ }
497
+ if (n === 'tuple' || n === 'Tuple') {
498
+ return { kind: 'collection', name: 'tuple', itemTypes: [] };
499
+ }
500
+ if (n === 'set' || n === 'Set') {
501
+ return { kind: 'collection', name: 'set', itemTypes: [] };
502
+ }
503
+ return { kind: 'custom', name: n };
504
+ }
505
+
506
+ function normalizeCollectionName(
507
+ raw: string
508
+ ): { name: 'list' | 'dict' | 'tuple' | 'set' | 'frozenset'; inner?: string } | null {
509
+ const m = raw.match(/^(typing\.)?(List|Dict|Tuple|Set|list|dict|tuple|set)\[(.*)\]$/);
510
+ if (!m) {
511
+ return null;
512
+ }
513
+ const nameRaw = String(m[2] ?? '');
514
+ const name =
515
+ nameRaw.toLowerCase() === 'list'
516
+ ? 'list'
517
+ : nameRaw.toLowerCase() === 'dict'
518
+ ? 'dict'
519
+ : nameRaw.toLowerCase() === 'tuple'
520
+ ? 'tuple'
521
+ : 'set';
522
+ return { name, inner: String(m[3] ?? '') };
523
+ }
524
+
525
+ function splitTopLevel(input: string, sep: '|' | ','): string[] {
526
+ const results: string[] = [];
527
+ let level = 0;
528
+ let cur = '';
529
+ let guard = 0;
530
+ for (let i = 0; i < input.length; i++) {
531
+ // Depth/length guard to avoid pathological recursion/loops
532
+ guard++;
533
+ if (guard > 20000) {
534
+ // Bail out safely; push remaining and break
535
+ if (cur.trim()) {
536
+ results.push(cur.trim());
537
+ }
538
+ break;
539
+ }
540
+ const ch = input.charAt(i);
541
+ if (ch === '[' || ch === '(') {
542
+ level++;
543
+ }
544
+ if (ch === ']' || ch === ')') {
545
+ level = Math.max(0, level - 1);
546
+ }
547
+ if (level === 0 && input.slice(i, i + sep.length) === sep) {
548
+ results.push(cur.trim());
549
+ cur = '';
550
+ i += sep.length - 1;
551
+ continue;
552
+ }
553
+ cur += ch;
554
+ }
555
+ if (cur.trim()) {
556
+ results.push(cur.trim());
557
+ }
558
+ return results;
559
+ }
560
+
561
+ function mapLiteral(text: string): PythonType {
562
+ // strip quotes if present
563
+ const t = text.trim();
564
+ if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
565
+ return { kind: 'literal', value: t.slice(1, -1) } as PythonType;
566
+ }
567
+ if (t === 'True' || t === 'False') {
568
+ return { kind: 'literal', value: t === 'True' } as PythonType;
569
+ }
570
+ if (t === 'None') {
571
+ return { kind: 'literal', value: null } as PythonType;
572
+ }
573
+ const num = Number(t);
574
+ if (!Number.isNaN(num)) {
575
+ return { kind: 'literal', value: num } as PythonType;
576
+ }
577
+ // fallback
578
+ return { kind: 'custom', name: t } as PythonType;
579
+ }
580
+
581
+ /**
582
+ * Compute a stable cache key filename for a module and options
583
+ */
584
+ async function computeCacheKey(
585
+ moduleName: string,
586
+ options: Partial<TywrapOptions>
587
+ ): Promise<string> {
588
+ const modules = options.pythonModules ?? {};
589
+ const foundEntry = Object.entries(modules).find(([name]) => name === moduleName);
590
+ const moduleConfig = foundEntry ? foundEntry[1] : undefined;
591
+ const runtimePython = await resolvePythonExecutable({
592
+ pythonPath: options.runtime?.node?.pythonPath,
593
+ virtualEnv: options.runtime?.node?.virtualEnv,
594
+ });
595
+ const keyObject = {
596
+ module: moduleName,
597
+ moduleVersion: moduleConfig?.version ?? null,
598
+ runtime: {
599
+ pythonPath: runtimePython,
600
+ virtualEnv: options.runtime?.node?.virtualEnv ?? null,
601
+ },
602
+ output: {
603
+ format: options.output?.format ?? 'esm',
604
+ declaration: options.output?.declaration ?? false,
605
+ sourceMap: options.output?.sourceMap ?? false,
606
+ },
607
+ performance: {
608
+ caching: options.performance?.caching ?? false,
609
+ compression: options.performance?.compression ?? 'none',
610
+ },
611
+ typeHints: moduleConfig?.typeHints ?? 'strict',
612
+ } as const;
613
+ const normalized = JSON.stringify(keyObject);
614
+ const digest = await hashUtils.sha256Hex(normalized);
615
+ return `ir_${moduleName}_${digest.slice(0, 16)}.json`;
616
+ }
617
+
618
+ /**
619
+ * Very lightweight .d.ts emitter derived from generated TS wrappers
620
+ * This is intentionally minimal and stable for our wrappers shape.
621
+ */
622
+ function renderDts(generatedTs: string): string {
623
+ const header = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
624
+ const lines: string[] = [header];
625
+ // Extract function exports
626
+ const funcRegex = /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
627
+ let m: RegExpExecArray | null;
628
+ while ((m = funcRegex.exec(generatedTs)) !== null) {
629
+ const name = m[1];
630
+ const params = m[2];
631
+ const ret = m[3];
632
+ lines.push(`export function ${name}(${params}): Promise<${ret}>;`);
633
+ }
634
+ // Extract class exports and methods
635
+ const classRegex = /export\s+class\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
636
+ while ((m = classRegex.exec(generatedTs)) !== null) {
637
+ const className = String(m[1] ?? '');
638
+ const body = String(m[2] ?? '');
639
+ // Constructor is always variadic unknown[] in current generator
640
+ const methods: string[] = [];
641
+ const methodRegex = /\n\s+async\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
642
+ let mm: RegExpExecArray | null;
643
+ while ((mm = methodRegex.exec(body)) !== null) {
644
+ methods.push(` ${mm[1]}(${mm[2]}): Promise<${mm[3]}>;`);
645
+ }
646
+ lines.push(`export class ${className} {`);
647
+ lines.push(` constructor(...args: unknown[]);`);
648
+ if (methods.length > 0) {
649
+ lines.push(methods.join('\n'));
650
+ }
651
+ lines.push('}');
652
+ }
653
+ return `${lines.join('\n')}\n`;
654
+ }
655
+
656
+ /**
657
+ * Minimal source map placeholder (stable, empty mappings)
658
+ */
659
+ function renderSourceMapPlaceholder(moduleName: string | undefined): string {
660
+ const safe = moduleName ?? 'module';
661
+ const map = {
662
+ version: 3,
663
+ file: `${safe}.generated.ts`,
664
+ sources: [],
665
+ names: [],
666
+ mappings: '',
667
+ } as const;
668
+ return JSON.stringify(map, null, 0);
669
+ }