tywrap 0.2.1 → 0.3.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 (138) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/bridge-core.d.ts +1 -0
  33. package/dist/runtime/bridge-core.d.ts.map +1 -1
  34. package/dist/runtime/bridge-core.js +52 -17
  35. package/dist/runtime/bridge-core.js.map +1 -1
  36. package/dist/runtime/bridge-protocol.d.ts +17 -0
  37. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  38. package/dist/runtime/bridge-protocol.js +97 -0
  39. package/dist/runtime/bridge-protocol.js.map +1 -1
  40. package/dist/runtime/errors.d.ts +10 -0
  41. package/dist/runtime/errors.d.ts.map +1 -1
  42. package/dist/runtime/errors.js +9 -0
  43. package/dist/runtime/errors.js.map +1 -1
  44. package/dist/runtime/http-io.d.ts.map +1 -1
  45. package/dist/runtime/http-io.js.map +1 -1
  46. package/dist/runtime/http.d.ts +1 -0
  47. package/dist/runtime/http.d.ts.map +1 -1
  48. package/dist/runtime/http.js +6 -0
  49. package/dist/runtime/http.js.map +1 -1
  50. package/dist/runtime/node.d.ts.map +1 -1
  51. package/dist/runtime/node.js +170 -32
  52. package/dist/runtime/node.js.map +1 -1
  53. package/dist/runtime/optimized-node.d.ts +1 -1
  54. package/dist/runtime/optimized-node.d.ts.map +1 -1
  55. package/dist/runtime/optimized-node.js +1 -1
  56. package/dist/runtime/optimized-node.js.map +1 -1
  57. package/dist/runtime/pooled-transport.d.ts +4 -0
  58. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  59. package/dist/runtime/pooled-transport.js +2 -0
  60. package/dist/runtime/pooled-transport.js.map +1 -1
  61. package/dist/runtime/process-io.d.ts +5 -0
  62. package/dist/runtime/process-io.d.ts.map +1 -1
  63. package/dist/runtime/process-io.js +64 -30
  64. package/dist/runtime/process-io.js.map +1 -1
  65. package/dist/runtime/pyodide-io.d.ts +4 -5
  66. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  67. package/dist/runtime/pyodide-io.js +99 -37
  68. package/dist/runtime/pyodide-io.js.map +1 -1
  69. package/dist/runtime/safe-codec.d.ts +25 -3
  70. package/dist/runtime/safe-codec.d.ts.map +1 -1
  71. package/dist/runtime/safe-codec.js +231 -57
  72. package/dist/runtime/safe-codec.js.map +1 -1
  73. package/dist/runtime/transport.d.ts +6 -1
  74. package/dist/runtime/transport.d.ts.map +1 -1
  75. package/dist/runtime/transport.js.map +1 -1
  76. package/dist/runtime/validators.d.ts +0 -11
  77. package/dist/runtime/validators.d.ts.map +1 -1
  78. package/dist/runtime/validators.js +13 -3
  79. package/dist/runtime/validators.js.map +1 -1
  80. package/dist/runtime/worker-pool.d.ts +12 -0
  81. package/dist/runtime/worker-pool.d.ts.map +1 -1
  82. package/dist/runtime/worker-pool.js +53 -5
  83. package/dist/runtime/worker-pool.js.map +1 -1
  84. package/dist/types/index.d.ts +51 -1
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/tywrap.d.ts.map +1 -1
  87. package/dist/tywrap.js +227 -328
  88. package/dist/tywrap.js.map +1 -1
  89. package/dist/utils/cache.d.ts.map +1 -1
  90. package/dist/utils/cache.js +53 -5
  91. package/dist/utils/cache.js.map +1 -1
  92. package/dist/utils/codec.d.ts.map +1 -1
  93. package/dist/utils/codec.js +10 -16
  94. package/dist/utils/codec.js.map +1 -1
  95. package/dist/utils/ir-cache.d.ts +28 -0
  96. package/dist/utils/ir-cache.d.ts.map +1 -0
  97. package/dist/utils/ir-cache.js +29 -0
  98. package/dist/utils/ir-cache.js.map +1 -0
  99. package/dist/utils/parallel-processor.d.ts +1 -1
  100. package/dist/utils/parallel-processor.d.ts.map +1 -1
  101. package/dist/utils/parallel-processor.js +111 -40
  102. package/dist/utils/parallel-processor.js.map +1 -1
  103. package/dist/utils/runtime.d.ts +5 -1
  104. package/dist/utils/runtime.d.ts.map +1 -1
  105. package/dist/utils/runtime.js +112 -18
  106. package/dist/utils/runtime.js.map +1 -1
  107. package/package.json +40 -18
  108. package/runtime/python_bridge.py +78 -12
  109. package/src/cli.ts +61 -2
  110. package/src/config/index.ts +63 -17
  111. package/src/core/analyzer.ts +52 -15
  112. package/src/core/annotation-parser.ts +500 -0
  113. package/src/core/discovery.ts +26 -12
  114. package/src/core/generator.ts +956 -116
  115. package/src/core/mapper.ts +158 -51
  116. package/src/index.ts +34 -5
  117. package/src/runtime/bridge-core.ts +58 -18
  118. package/src/runtime/bridge-protocol.ts +164 -0
  119. package/src/runtime/errors.ts +14 -0
  120. package/src/runtime/http-io.ts +2 -1
  121. package/src/runtime/http.ts +7 -0
  122. package/src/runtime/node.ts +264 -44
  123. package/src/runtime/optimized-node.ts +4 -1
  124. package/src/runtime/pooled-transport.ts +12 -1
  125. package/src/runtime/process-io.ts +83 -55
  126. package/src/runtime/pyodide-io.ts +110 -44
  127. package/src/runtime/safe-codec.ts +290 -82
  128. package/src/runtime/transport.ts +6 -1
  129. package/src/runtime/validators.ts +14 -3
  130. package/src/runtime/worker-pool.ts +67 -6
  131. package/src/types/global.d.ts +11 -1
  132. package/src/types/index.ts +63 -0
  133. package/src/tywrap.ts +279 -358
  134. package/src/utils/cache.ts +59 -5
  135. package/src/utils/codec.ts +11 -15
  136. package/src/utils/ir-cache.ts +51 -0
  137. package/src/utils/parallel-processor.ts +119 -43
  138. package/src/utils/runtime.ts +129 -18
package/dist/tywrap.js CHANGED
@@ -3,11 +3,14 @@
3
3
  */
4
4
  import { CodeGenerator } from './core/generator.js';
5
5
  import { TypeMapper } from './core/mapper.js';
6
+ import { parseAnnotationToPythonType } from './core/annotation-parser.js';
6
7
  import { createConfig } from './config/index.js';
7
- import { fsUtils, pathUtils, processUtils, hashUtils } from './utils/runtime.js';
8
+ import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
8
9
  import { globalCache } from './utils/cache.js';
9
10
  import { globalParallelProcessor } from './utils/parallel-processor.js';
10
11
  import { resolvePythonExecutable } from './utils/python.js';
12
+ import { computeIrCacheFilename } from './utils/ir-cache.js';
13
+ const TYWRAP_IR_VERSION = '0.2.0';
11
14
  // Collect unknown typing constructs encountered during annotation parsing (per-generate run)
12
15
  let unknownTypeNamesCollector = new Map();
13
16
  function recordUnknown(name) {
@@ -88,13 +91,14 @@ export async function generate(options, runOptions = {}) {
88
91
  const modules = resolvedOptions.pythonModules ?? {};
89
92
  for (const entry of Object.entries(modules)) {
90
93
  const moduleKey = entry[0];
94
+ const moduleConfig = entry[1];
91
95
  // reset collector for each module
92
96
  unknownTypeNamesCollector = new Map();
93
97
  // Prefer Python IR extractor over TS analyzer with optional cache
94
- const cacheKey = await computeCacheKey(moduleKey, options);
98
+ const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
95
99
  let ir = null;
96
100
  let irError;
97
- if (caching && fsUtils.isAvailable()) {
101
+ if (caching && fsUtils.isAvailable() && !checkMode) {
98
102
  try {
99
103
  const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
100
104
  ir = JSON.parse(cached);
@@ -104,7 +108,10 @@ export async function generate(options, runOptions = {}) {
104
108
  }
105
109
  }
106
110
  if (!ir) {
107
- const fetchResult = await fetchPythonIr(moduleKey, pythonPath);
111
+ const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
112
+ timeoutMs: resolvedOptions.runtime?.node?.timeout,
113
+ pythonImportPath: resolvedOptions.pythonImportPath,
114
+ });
108
115
  ir = fetchResult.ir;
109
116
  irError = fetchResult.error;
110
117
  if (ir && caching && fsUtils.isAvailable() && !checkMode) {
@@ -119,6 +126,65 @@ export async function generate(options, runOptions = {}) {
119
126
  continue;
120
127
  }
121
128
  const moduleModel = transformIrToTsModel(ir);
129
+ // Apply module-level export filtering (functions/classes + excludes).
130
+ {
131
+ const builtInDefaultExcludes = new Set([
132
+ 'dataclass',
133
+ 'property',
134
+ 'staticmethod',
135
+ 'classmethod',
136
+ 'abstractmethod',
137
+ 'cached_property',
138
+ ]);
139
+ const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
140
+ const excludeRegexes = [];
141
+ for (const pattern of moduleConfig.excludePatterns ?? []) {
142
+ try {
143
+ excludeRegexes.push(new RegExp(String(pattern)));
144
+ }
145
+ catch (err) {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ warnings.push(`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`);
148
+ }
149
+ }
150
+ const shouldExclude = (name, applyBuiltInDefaults) => {
151
+ if (excludeExact.has(name)) {
152
+ return true;
153
+ }
154
+ if (excludeRegexes.some(r => r.test(name))) {
155
+ return true;
156
+ }
157
+ if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
158
+ return true;
159
+ }
160
+ return false;
161
+ };
162
+ if (Array.isArray(moduleConfig.functions)) {
163
+ const allow = new Set(moduleConfig.functions.map(String));
164
+ for (const requested of allow) {
165
+ if (!moduleModel.functions.some(f => f.name === requested)) {
166
+ warnings.push(`Module ${moduleKey}: configured function "${requested}" not found in IR`);
167
+ }
168
+ }
169
+ moduleModel.functions = moduleModel.functions.filter(f => allow.has(f.name) && !shouldExclude(f.name, false));
170
+ }
171
+ else {
172
+ moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
173
+ }
174
+ if (Array.isArray(moduleConfig.classes)) {
175
+ const allow = new Set(moduleConfig.classes.map(String));
176
+ for (const requested of allow) {
177
+ if (!moduleModel.classes.some(c => c.name === requested)) {
178
+ warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
179
+ }
180
+ }
181
+ moduleModel.classes = moduleModel.classes.filter(c => allow.has(c.name) && !shouldExclude(c.name, false));
182
+ }
183
+ else {
184
+ moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
185
+ }
186
+ moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(alias => !shouldExclude(alias.name, false));
187
+ }
122
188
  // Generate module code
123
189
  const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
124
190
  const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
@@ -130,7 +196,7 @@ export async function generate(options, runOptions = {}) {
130
196
  if (resolvedOptions.output?.declaration) {
131
197
  filesToEmit.push({
132
198
  path: pathUtils.join(outputDir, `${baseName}.generated.d.ts`),
133
- content: renderDts(gen.typescript),
199
+ content: gen.declaration,
134
200
  });
135
201
  }
136
202
  // Optional source map emission (placeholder mapping for now)
@@ -190,18 +256,21 @@ export async function generate(options, runOptions = {}) {
190
256
  /**
191
257
  * Invoke the Python IR CLI to get JSON IR for a module.
192
258
  */
193
- async function fetchPythonIr(moduleName, pythonPath) {
259
+ async function fetchPythonIr(moduleName, pythonPath, options = {}) {
194
260
  if (!processUtils.isAvailable()) {
195
261
  return { ir: null, error: 'Subprocess operations not available in this runtime' };
196
262
  }
263
+ const delimiter = isWindows() ? ';' : ':';
264
+ const extraPaths = (options.pythonImportPath ?? []).filter(Boolean);
265
+ const existingPyPath = typeof process !== 'undefined' && typeof process.env === 'object' && process.env
266
+ ? process.env.PYTHONPATH
267
+ : undefined;
268
+ const mergedPyPath = [...extraPaths, ...(existingPyPath ? [existingPyPath] : [])]
269
+ .filter(Boolean)
270
+ .join(delimiter);
271
+ const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
197
272
  try {
198
- const result = await processUtils.exec(pythonPath, [
199
- '-m',
200
- 'tywrap_ir',
201
- '--module',
202
- moduleName,
203
- '--no-pretty',
204
- ]);
273
+ const result = await processUtils.exec(pythonPath, ['-m', 'tywrap_ir', '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'], { timeoutMs: options.timeoutMs, env });
205
274
  if (result.code === 0) {
206
275
  try {
207
276
  return { ir: JSON.parse(result.stdout) };
@@ -213,18 +282,34 @@ async function fetchPythonIr(moduleName, pythonPath) {
213
282
  };
214
283
  }
215
284
  }
216
- // Fallback to invoking local __main__.py
285
+ const stderrText = result.stderr.trim();
286
+ const isTywrapIrMissing = stderrText.includes('No module named') && stderrText.includes('tywrap_ir');
287
+ if (!isTywrapIrMissing) {
288
+ return { ir: null, error: `tywrap_ir failed. stderr: ${stderrText || 'empty'}` };
289
+ }
290
+ // Fallback to invoking local __main__.py (useful when running from the repo).
217
291
  const localMain = pathUtils.join(process.cwd(), 'tywrap_ir', 'tywrap_ir', '__main__.py');
218
- const fallback = await processUtils.exec(pythonPath, [
219
- localMain,
220
- '--module',
221
- moduleName,
222
- '--no-pretty',
223
- ]);
292
+ let localMainExists = false;
293
+ if (fsUtils.isAvailable()) {
294
+ try {
295
+ await fsUtils.readFile(localMain);
296
+ localMainExists = true;
297
+ }
298
+ catch {
299
+ localMainExists = false;
300
+ }
301
+ }
302
+ if (!localMainExists) {
303
+ return {
304
+ ir: null,
305
+ error: `tywrap_ir not found on PYTHONPATH. stderr: ${stderrText || 'empty'}`,
306
+ };
307
+ }
308
+ const fallback = await processUtils.exec(pythonPath, [localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'], { timeoutMs: options.timeoutMs, env });
224
309
  if (fallback.code !== 0) {
225
310
  return {
226
311
  ir: null,
227
- error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || result.stderr.trim() || 'empty'}`,
312
+ error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || stderrText || 'empty'}`,
228
313
  };
229
314
  }
230
315
  try {
@@ -244,70 +329,134 @@ async function fetchPythonIr(moduleName, pythonPath) {
244
329
  /**
245
330
  * Convert JSON IR from Python into the internal TypeScript model used by the generator.
246
331
  */
332
+ function collectModuleTypeVarNames(obj) {
333
+ const names = new Set();
334
+ const constants = Array.isArray(obj.constants) ? obj.constants : [];
335
+ for (const constant of constants) {
336
+ const entry = constant;
337
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
338
+ if (!name) {
339
+ continue;
340
+ }
341
+ const valueRepr = typeof entry.value_repr === 'string' ? entry.value_repr.trim() : '';
342
+ if (/^~?[A-Za-z_][A-Za-z0-9_]*$/.test(valueRepr) && valueRepr.replace(/^~/, '') === name) {
343
+ names.add(name);
344
+ continue;
345
+ }
346
+ const annotation = typeof entry.annotation === 'string' ? entry.annotation.trim() : '';
347
+ if (/(^|\.)(TypeVar|ParamSpec|TypeVarTuple)(\[|\(|$)/.test(annotation)) {
348
+ names.add(name);
349
+ }
350
+ }
351
+ return names;
352
+ }
247
353
  function transformIrToTsModel(ir) {
248
354
  const obj = typeof ir === 'object' && ir !== null ? ir : {};
249
355
  const functions = obj.functions ?? [];
250
356
  const classes = obj.classes ?? [];
251
- const mapParam = (p) => ({
357
+ const aliases = obj.type_aliases ?? [];
358
+ const moduleTypeVarNames = collectModuleTypeVarNames(obj);
359
+ const parseType = (annotation, typeParameters = []) => parseAnnotationToPythonType(annotation, {
360
+ onUnknownTypeName: recordUnknown,
361
+ knownTypeVarNames: moduleTypeVarNames,
362
+ typeParameters,
363
+ });
364
+ const mapTypeParameters = (value) => Array.isArray(value.type_params)
365
+ ? value.type_params.map(v => {
366
+ const param = (v ?? {});
367
+ return {
368
+ name: String(param.name ?? ''),
369
+ kind: String(param.kind ?? 'typevar'),
370
+ bound: param.bound ? parseType(param.bound) : undefined,
371
+ constraints: Array.isArray(param.constraints)
372
+ ? param.constraints.map(item => parseType(item))
373
+ : undefined,
374
+ variance: param.variance === 'covariant' ||
375
+ param.variance === 'contravariant' ||
376
+ param.variance === 'invariant'
377
+ ? param.variance
378
+ : undefined,
379
+ };
380
+ })
381
+ : [];
382
+ const mapParam = (p, typeParameters = []) => ({
252
383
  name: String(p.name ?? ''),
253
- type: parseAnnotationToPythonType(p.annotation),
384
+ type: parseType(p.annotation, typeParameters),
254
385
  optional: Boolean(p.default),
255
386
  varArgs: p.kind === 'VAR_POSITIONAL',
256
387
  kwArgs: p.kind === 'VAR_KEYWORD',
388
+ positionalOnly: p.kind === 'POSITIONAL_ONLY',
389
+ keywordOnly: p.kind === 'KEYWORD_ONLY',
257
390
  });
258
- const mapFunc = (f) => ({
259
- name: String(f.name ?? ''),
260
- signature: {
261
- parameters: Array.isArray(f.parameters)
262
- ? f.parameters.map(v => mapParam((v ?? {})))
263
- : [],
264
- returnType: parseAnnotationToPythonType(f.returns),
391
+ const mapFunc = (f, inheritedTypeParameters = []) => {
392
+ const localTypeParameters = mapTypeParameters(f);
393
+ const annotationTypeParameters = [...inheritedTypeParameters, ...localTypeParameters];
394
+ return {
395
+ name: String(f.name ?? ''),
396
+ signature: {
397
+ parameters: Array.isArray(f.parameters)
398
+ ? f.parameters.map(v => mapParam((v ?? {}), annotationTypeParameters))
399
+ : [],
400
+ returnType: parseType(f.returns, annotationTypeParameters),
401
+ isAsync: Boolean(f.is_async),
402
+ isGenerator: Boolean(f.is_generator),
403
+ },
404
+ docstring: f.docstring ?? undefined,
405
+ decorators: [],
265
406
  isAsync: Boolean(f.is_async),
266
407
  isGenerator: Boolean(f.is_generator),
267
- },
268
- docstring: f.docstring ?? undefined,
269
- decorators: [],
270
- isAsync: Boolean(f.is_async),
271
- isGenerator: Boolean(f.is_generator),
272
- returnType: parseAnnotationToPythonType(f.returns),
273
- parameters: Array.isArray(f.parameters)
274
- ? f.parameters.map(v => mapParam((v ?? {})))
275
- : [],
276
- });
277
- const mapClass = (c) => ({
278
- name: String(c.name ?? ''),
279
- bases: Array.isArray(c.bases) ? c.bases : [],
280
- methods: Array.isArray(c.methods)
281
- ? c.methods.map(v => mapFunc((v ?? {})))
282
- : [],
283
- properties: Array.isArray(c.fields)
284
- ? c.fields.map(v => {
285
- const p = (v ?? {});
286
- const optional = Boolean(p.default);
287
- return {
288
- name: String(p.name ?? ''),
289
- type: parseAnnotationToPythonType(p.annotation),
290
- readonly: false,
291
- setter: false,
292
- getter: true,
293
- optional,
294
- };
295
- })
296
- : [],
297
- docstring: c.docstring ?? undefined,
298
- decorators: c.typed_dict ? ['__typed_dict__'] : [],
299
- kind: c.typed_dict
300
- ? 'typed_dict'
301
- : c.is_protocol
302
- ? 'protocol'
303
- : c.is_namedtuple
304
- ? 'namedtuple'
305
- : c.is_dataclass
306
- ? 'dataclass'
307
- : c.is_pydantic
308
- ? 'pydantic'
309
- : 'class',
310
- });
408
+ typeParameters: [...localTypeParameters],
409
+ returnType: parseType(f.returns, annotationTypeParameters),
410
+ parameters: Array.isArray(f.parameters)
411
+ ? f.parameters.map(v => mapParam((v ?? {}), annotationTypeParameters))
412
+ : [],
413
+ };
414
+ };
415
+ const mapClass = (c) => {
416
+ const classTypeParameters = mapTypeParameters(c);
417
+ return {
418
+ name: String(c.name ?? ''),
419
+ bases: Array.isArray(c.bases) ? c.bases : [],
420
+ methods: Array.isArray(c.methods)
421
+ ? c.methods.map(v => mapFunc((v ?? {}), classTypeParameters))
422
+ : [],
423
+ properties: Array.isArray(c.fields)
424
+ ? c.fields.map(v => {
425
+ const p = (v ?? {});
426
+ return {
427
+ name: String(p.name ?? ''),
428
+ type: parseType(p.annotation, classTypeParameters),
429
+ readonly: false,
430
+ setter: false,
431
+ getter: true,
432
+ optional: Boolean(p.default),
433
+ };
434
+ })
435
+ : [],
436
+ docstring: c.docstring ?? undefined,
437
+ decorators: c.typed_dict ? ['__typed_dict__'] : [],
438
+ kind: c.typed_dict
439
+ ? 'typed_dict'
440
+ : c.is_protocol
441
+ ? 'protocol'
442
+ : c.is_namedtuple
443
+ ? 'namedtuple'
444
+ : c.is_dataclass
445
+ ? 'dataclass'
446
+ : c.is_pydantic
447
+ ? 'pydantic'
448
+ : 'class',
449
+ typeParameters: classTypeParameters,
450
+ };
451
+ };
452
+ const mapTypeAlias = (alias) => {
453
+ const typeParameters = mapTypeParameters(alias);
454
+ return {
455
+ name: String(alias.name ?? ''),
456
+ type: parseType(alias.definition, typeParameters),
457
+ typeParameters,
458
+ };
459
+ };
311
460
  const moduleModel = {
312
461
  name: obj.module ?? 'module',
313
462
  path: undefined,
@@ -316,225 +465,12 @@ function transformIrToTsModel(ir) {
316
465
  : undefined,
317
466
  functions: functions.map(v => mapFunc((v ?? {}))),
318
467
  classes: classes.map(v => mapClass((v ?? {}))),
468
+ typeAliases: aliases.map(v => mapTypeAlias((v ?? {}))),
319
469
  imports: [],
320
470
  exports: [],
321
471
  };
322
472
  return moduleModel;
323
473
  }
324
- function unknownType() {
325
- return { kind: 'custom', name: 'Any', module: 'typing' };
326
- }
327
- // Parse Python IR annotation string -> PythonType
328
- function parseAnnotationToPythonType(annotation, depth = 0) {
329
- if (annotation === null || annotation === undefined) {
330
- return unknownType();
331
- }
332
- if (depth > 100) {
333
- return unknownType();
334
- }
335
- const raw = String(annotation).trim();
336
- // Handle built-in class repr: <class 'int'>
337
- const classMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
338
- if (classMatch) {
339
- const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
340
- const name = (inner.split('.').pop() ?? '').toString();
341
- return mapSimpleName(name);
342
- }
343
- // PEP 604 unions: int | str | None
344
- if (raw.includes(' | ')) {
345
- const parts = splitTopLevel(raw, '|');
346
- const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
347
- return { kind: 'union', types };
348
- }
349
- // typing.Union[...]
350
- if (raw.startsWith('typing.Union[') || raw.startsWith('Union[')) {
351
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
352
- const parts = splitTopLevel(inner, ',');
353
- const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
354
- return { kind: 'union', types };
355
- }
356
- // Optional[T]
357
- if (raw.startsWith('typing.Optional[') || raw.startsWith('Optional[')) {
358
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
359
- const base = parseAnnotationToPythonType(inner, depth + 1);
360
- return { kind: 'optional', type: base };
361
- }
362
- // Literal[...] -> literal values union
363
- if (raw.startsWith('typing.Literal[') || raw.startsWith('Literal[')) {
364
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
365
- const parts = splitTopLevel(inner, ',');
366
- if (parts.length === 1) {
367
- return mapLiteral(String(parts[0] ?? '').trim());
368
- }
369
- return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) };
370
- }
371
- // typing_extensions wrappers: ClassVar[T], Final[T], Required[T], NotRequired[T]
372
- const extMatch = raw.match(/^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/);
373
- if (extMatch) {
374
- const inner = extMatch[3] ?? '';
375
- const base = parseAnnotationToPythonType(inner, depth + 1);
376
- return base;
377
- }
378
- // LiteralString
379
- if (raw === 'typing.LiteralString' || raw === 'LiteralString') {
380
- return { kind: 'primitive', name: 'str' };
381
- }
382
- // Callable[[...], R]
383
- if (raw.startsWith('typing.Callable[') || raw.startsWith('Callable[')) {
384
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
385
- const parts = splitTopLevel(inner, ',');
386
- if (parts.length >= 2) {
387
- const paramsPart = (parts[0] ?? '').trim();
388
- const returnPart = parts.slice(1).join(',').trim();
389
- const paramInner = paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
390
- const paramTypes = (() => {
391
- const trimmed = paramInner.trim();
392
- if (trimmed === '...' || trimmed === 'Ellipsis') {
393
- return [{ kind: 'custom', name: '...' }];
394
- }
395
- return trimmed
396
- ? splitTopLevel(trimmed, ',').map(p => parseAnnotationToPythonType(p.trim(), depth + 1))
397
- : [];
398
- })();
399
- const returnType = parseAnnotationToPythonType(returnPart, depth + 1);
400
- return { kind: 'callable', parameters: paramTypes, returnType };
401
- }
402
- }
403
- // Mapping[K, V] / Dict[K, V] normalization
404
- if (raw.startsWith('typing.Mapping[') || raw.startsWith('Mapping[')) {
405
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
406
- const parts = splitTopLevel(inner, ',');
407
- const itemTypes = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
408
- return { kind: 'collection', name: 'dict', itemTypes };
409
- }
410
- // Annotated[T, ...] -> annotated node with base and metadata
411
- if (raw.startsWith('typing.Annotated[') || raw.startsWith('Annotated[')) {
412
- const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
413
- const parts = splitTopLevel(inner, ',');
414
- if (parts.length > 0) {
415
- const base = parseAnnotationToPythonType((parts[0] ?? '').trim(), depth + 1);
416
- const metaParts = parts.slice(1).map(p => String(p).trim());
417
- return { kind: 'annotated', base, metadata: metaParts };
418
- }
419
- }
420
- // Collections: list[T], dict[K,V], tuple[...], set[T]
421
- const coll = normalizeCollectionName(raw);
422
- if (coll) {
423
- const { name, inner } = coll;
424
- const itemParts = name === 'dict' ? splitTopLevel(inner ?? '', ',') : splitTopLevel(inner ?? '', ',');
425
- const itemTypes = (inner ? itemParts : []).map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
426
- return { kind: 'collection', name, itemTypes };
427
- }
428
- // typing.Callable[...] unlikely for now - treat as custom
429
- // Bare names like int, str, float, bool, bytes, None
430
- return mapSimpleName(raw);
431
- }
432
- function mapSimpleName(name) {
433
- const n = name.replace(/^typing\./, '').trim();
434
- if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
435
- // cast to the specific union type without using any
436
- return { kind: 'primitive', name: n };
437
- }
438
- // Track unknown typing-ish names for diagnostics
439
- if (n === 'Any' ||
440
- n === 'Never' ||
441
- n === 'LiteralString' ||
442
- n === 'ClassVar' ||
443
- n === 'Final' ||
444
- n === 'TypeAlias' ||
445
- n === 'Required' ||
446
- n === 'NotRequired') {
447
- try {
448
- recordUnknown(n);
449
- }
450
- catch { }
451
- }
452
- if (n === 'None' || n.toLowerCase() === 'nonetype') {
453
- return { kind: 'primitive', name: 'None' };
454
- }
455
- if (n === 'list' || n === 'List') {
456
- return { kind: 'collection', name: 'list', itemTypes: [] };
457
- }
458
- if (n === 'dict' || n === 'Dict') {
459
- return { kind: 'collection', name: 'dict', itemTypes: [] };
460
- }
461
- if (n === 'tuple' || n === 'Tuple') {
462
- return { kind: 'collection', name: 'tuple', itemTypes: [] };
463
- }
464
- if (n === 'set' || n === 'Set') {
465
- return { kind: 'collection', name: 'set', itemTypes: [] };
466
- }
467
- return { kind: 'custom', name: n };
468
- }
469
- function normalizeCollectionName(raw) {
470
- const m = raw.match(/^(typing\.)?(List|Dict|Tuple|Set|list|dict|tuple|set)\[(.*)\]$/);
471
- if (!m) {
472
- return null;
473
- }
474
- const nameRaw = String(m[2] ?? '');
475
- const name = nameRaw.toLowerCase() === 'list'
476
- ? 'list'
477
- : nameRaw.toLowerCase() === 'dict'
478
- ? 'dict'
479
- : nameRaw.toLowerCase() === 'tuple'
480
- ? 'tuple'
481
- : 'set';
482
- return { name, inner: String(m[3] ?? '') };
483
- }
484
- function splitTopLevel(input, sep) {
485
- const results = [];
486
- let level = 0;
487
- let cur = '';
488
- let guard = 0;
489
- for (let i = 0; i < input.length; i++) {
490
- // Depth/length guard to avoid pathological recursion/loops
491
- guard++;
492
- if (guard > 20000) {
493
- // Bail out safely; push remaining and break
494
- if (cur.trim()) {
495
- results.push(cur.trim());
496
- }
497
- break;
498
- }
499
- const ch = input.charAt(i);
500
- if (ch === '[' || ch === '(') {
501
- level++;
502
- }
503
- if (ch === ']' || ch === ')') {
504
- level = Math.max(0, level - 1);
505
- }
506
- if (level === 0 && input.slice(i, i + sep.length) === sep) {
507
- results.push(cur.trim());
508
- cur = '';
509
- i += sep.length - 1;
510
- continue;
511
- }
512
- cur += ch;
513
- }
514
- if (cur.trim()) {
515
- results.push(cur.trim());
516
- }
517
- return results;
518
- }
519
- function mapLiteral(text) {
520
- // strip quotes if present
521
- const t = text.trim();
522
- if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
523
- return { kind: 'literal', value: t.slice(1, -1) };
524
- }
525
- if (t === 'True' || t === 'False') {
526
- return { kind: 'literal', value: t === 'True' };
527
- }
528
- if (t === 'None') {
529
- return { kind: 'literal', value: null };
530
- }
531
- const num = Number(t);
532
- if (!Number.isNaN(num)) {
533
- return { kind: 'literal', value: num };
534
- }
535
- // fallback
536
- return { kind: 'custom', name: t };
537
- }
538
474
  /**
539
475
  * Compute a stable cache key filename for a module and options
540
476
  */
@@ -549,6 +485,8 @@ async function computeCacheKey(moduleName, options) {
549
485
  const keyObject = {
550
486
  module: moduleName,
551
487
  moduleVersion: moduleConfig?.version ?? null,
488
+ irVersion: TYWRAP_IR_VERSION,
489
+ pythonImportPath: options.pythonImportPath ?? [],
552
490
  runtime: {
553
491
  pythonPath: runtimePython,
554
492
  virtualEnv: options.runtime?.node?.virtualEnv ?? null,
@@ -564,46 +502,7 @@ async function computeCacheKey(moduleName, options) {
564
502
  },
565
503
  typeHints: moduleConfig?.typeHints ?? 'strict',
566
504
  };
567
- const normalized = JSON.stringify(keyObject);
568
- const digest = await hashUtils.sha256Hex(normalized);
569
- return `ir_${moduleName}_${digest.slice(0, 16)}.json`;
570
- }
571
- /**
572
- * Very lightweight .d.ts emitter derived from generated TS wrappers
573
- * This is intentionally minimal and stable for our wrappers shape.
574
- */
575
- function renderDts(generatedTs) {
576
- const header = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
577
- const lines = [header];
578
- // Extract function exports
579
- const funcRegex = /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
580
- let m;
581
- while ((m = funcRegex.exec(generatedTs)) !== null) {
582
- const name = m[1];
583
- const params = m[2];
584
- const ret = m[3];
585
- lines.push(`export function ${name}(${params}): Promise<${ret}>;`);
586
- }
587
- // Extract class exports and methods
588
- const classRegex = /export\s+class\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
589
- while ((m = classRegex.exec(generatedTs)) !== null) {
590
- const className = String(m[1] ?? '');
591
- const body = String(m[2] ?? '');
592
- // Constructor is always variadic unknown[] in current generator
593
- const methods = [];
594
- const methodRegex = /\n\s+async\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
595
- let mm;
596
- while ((mm = methodRegex.exec(body)) !== null) {
597
- methods.push(` ${mm[1]}(${mm[2]}): Promise<${mm[3]}>;`);
598
- }
599
- lines.push(`export class ${className} {`);
600
- lines.push(` constructor(...args: unknown[]);`);
601
- if (methods.length > 0) {
602
- lines.push(methods.join('\n'));
603
- }
604
- lines.push('}');
605
- }
606
- return `${lines.join('\n')}\n`;
505
+ return await computeIrCacheFilename(keyObject);
607
506
  }
608
507
  /**
609
508
  * Minimal source map placeholder (stable, empty mappings)