tywrap 0.1.1 → 0.1.2

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 (63) hide show
  1. package/README.md +23 -3
  2. package/dist/cli.js +70 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +47 -11
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/core/generator.d.ts.map +1 -1
  8. package/dist/core/generator.js +6 -2
  9. package/dist/core/generator.js.map +1 -1
  10. package/dist/core/mapper.d.ts.map +1 -1
  11. package/dist/core/mapper.js +16 -4
  12. package/dist/core/mapper.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/runtime/bridge-core.d.ts +64 -0
  18. package/dist/runtime/bridge-core.d.ts.map +1 -0
  19. package/dist/runtime/bridge-core.js +344 -0
  20. package/dist/runtime/bridge-core.js.map +1 -0
  21. package/dist/runtime/node.d.ts +5 -6
  22. package/dist/runtime/node.d.ts.map +1 -1
  23. package/dist/runtime/node.js +94 -190
  24. package/dist/runtime/node.js.map +1 -1
  25. package/dist/runtime/optimized-node.d.ts +4 -7
  26. package/dist/runtime/optimized-node.d.ts.map +1 -1
  27. package/dist/runtime/optimized-node.js +117 -194
  28. package/dist/runtime/optimized-node.js.map +1 -1
  29. package/dist/runtime/timed-out-request-tracker.d.ts +20 -0
  30. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -0
  31. package/dist/runtime/timed-out-request-tracker.js +48 -0
  32. package/dist/runtime/timed-out-request-tracker.js.map +1 -0
  33. package/dist/types/index.d.ts +3 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/tywrap.d.ts +17 -4
  36. package/dist/tywrap.d.ts.map +1 -1
  37. package/dist/tywrap.js +68 -35
  38. package/dist/tywrap.js.map +1 -1
  39. package/dist/utils/codec.d.ts +22 -0
  40. package/dist/utils/codec.d.ts.map +1 -1
  41. package/dist/utils/codec.js +245 -55
  42. package/dist/utils/codec.js.map +1 -1
  43. package/dist/utils/logger.d.ts.map +1 -1
  44. package/dist/utils/logger.js +1 -0
  45. package/dist/utils/logger.js.map +1 -1
  46. package/dist/utils/python.d.ts.map +1 -1
  47. package/dist/utils/python.js.map +1 -1
  48. package/package.json +11 -2
  49. package/runtime/python_bridge.py +450 -69
  50. package/src/cli.ts +75 -3
  51. package/src/config/index.ts +58 -15
  52. package/src/core/generator.ts +6 -2
  53. package/src/core/mapper.ts +16 -4
  54. package/src/index.ts +2 -1
  55. package/src/runtime/bridge-core.ts +454 -0
  56. package/src/runtime/node.ts +116 -220
  57. package/src/runtime/optimized-node.ts +160 -243
  58. package/src/runtime/timed-out-request-tracker.ts +58 -0
  59. package/src/types/index.ts +3 -0
  60. package/src/tywrap.ts +91 -36
  61. package/src/utils/codec.ts +299 -82
  62. package/src/utils/logger.ts +1 -0
  63. package/src/utils/python.ts +0 -1
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'node:fs';
3
- import { writeFile } from 'node:fs/promises';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
5
5
  import yargs, { type Argv, type ArgumentsCamelCase } from 'yargs';
6
6
  import { hideBin } from 'yargs/helpers';
@@ -99,6 +99,46 @@ ${moduleLines}
99
99
  `;
100
100
  }
101
101
 
102
+ async function addRecommendedScriptsToPackageJson(cwd: string): Promise<void> {
103
+ const packageJsonPath = resolve(cwd, 'package.json');
104
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- path is derived from cwd
105
+ if (!existsSync(packageJsonPath)) {
106
+ return;
107
+ }
108
+ try {
109
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- path is derived from cwd
110
+ const raw = await readFile(packageJsonPath, 'utf-8');
111
+ const pkg = JSON.parse(raw) as Record<string, unknown>;
112
+ const scripts =
113
+ typeof pkg.scripts === 'object' && pkg.scripts !== null && !Array.isArray(pkg.scripts)
114
+ ? (pkg.scripts as Record<string, unknown>)
115
+ : {};
116
+ const nextScripts: Record<string, unknown> = { ...scripts };
117
+ let changed = false;
118
+
119
+ if (nextScripts['tywrap:generate'] === undefined) {
120
+ nextScripts['tywrap:generate'] = 'tywrap generate';
121
+ changed = true;
122
+ }
123
+ if (nextScripts['tywrap:check'] === undefined) {
124
+ nextScripts['tywrap:check'] = 'tywrap generate --check';
125
+ changed = true;
126
+ }
127
+
128
+ if (!changed) {
129
+ return;
130
+ }
131
+
132
+ pkg.scripts = nextScripts;
133
+ const out = `${JSON.stringify(pkg, null, 2)}\n`;
134
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- path is derived from cwd
135
+ await writeFile(packageJsonPath, out, 'utf-8');
136
+ } catch (err: unknown) {
137
+ const message = err instanceof Error ? err.message : String(err);
138
+ log.warn('Failed to update package.json scripts', { error: message });
139
+ }
140
+ }
141
+
102
142
  async function main(): Promise<void> {
103
143
  await yargs(hideBin(process.argv))
104
144
  .scriptName('tywrap')
@@ -157,6 +197,11 @@ async function main(): Promise<void> {
157
197
  default: false,
158
198
  describe: 'Exit with code 2 if generation emits warnings',
159
199
  })
200
+ .option('check', {
201
+ type: 'boolean',
202
+ default: false,
203
+ describe: 'Check whether generated wrappers are up to date without writing files',
204
+ })
160
205
  .strict(),
161
206
  async (
162
207
  argv: ArgumentsCamelCase<{
@@ -171,6 +216,7 @@ async function main(): Promise<void> {
171
216
  useCache?: boolean;
172
217
  debug?: boolean;
173
218
  failOnWarn: boolean;
219
+ check: boolean;
174
220
  }>
175
221
  ) => {
176
222
  const { configPath, explicit } = resolveConfigPath(argv.config);
@@ -232,8 +278,24 @@ async function main(): Promise<void> {
232
278
  process.exit(1);
233
279
  }
234
280
 
235
- const res = await generate(options);
236
- process.stdout.write(`Generated: ${res.written.join(', ')}\n`);
281
+ const res = await generate(options, { check: argv.check });
282
+ if (argv.check) {
283
+ const outOfDate = res.outOfDate ?? [];
284
+ if (outOfDate.length === 0) {
285
+ process.stdout.write('Generated wrappers are up to date.\n');
286
+ } else {
287
+ process.stderr.write('Generated wrappers are out of date:\n');
288
+ for (const file of outOfDate) {
289
+ process.stderr.write(`- ${file}\n`);
290
+ }
291
+ process.stderr.write('\nRun `tywrap generate` to update.\n');
292
+ if (!(argv.failOnWarn && res.warnings.length > 0)) {
293
+ process.exit(3);
294
+ }
295
+ }
296
+ } else {
297
+ process.stdout.write(`Generated: ${res.written.join(', ')}\n`);
298
+ }
237
299
  if (argv.failOnWarn && res.warnings.length > 0) {
238
300
  log.error(
239
301
  `Warnings encountered (count ${res.warnings.length}). Failing due to --fail-on-warn.`
@@ -283,6 +345,12 @@ async function main(): Promise<void> {
283
345
  default: false,
284
346
  describe: 'Overwrite existing config file',
285
347
  })
348
+ .option('scripts', {
349
+ type: 'boolean',
350
+ default: true,
351
+ describe:
352
+ 'Add recommended tywrap scripts to package.json (use --no-scripts to disable)',
353
+ })
286
354
  .strict(),
287
355
  async (
288
356
  argv: ArgumentsCamelCase<{
@@ -292,6 +360,7 @@ async function main(): Promise<void> {
292
360
  runtime: RuntimeStrategy;
293
361
  outputDir: string;
294
362
  force: boolean;
363
+ scripts: boolean;
295
364
  }>
296
365
  ) => {
297
366
  const modules = parseModules(argv.modules);
@@ -315,6 +384,9 @@ async function main(): Promise<void> {
315
384
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled
316
385
  await writeFile(targetPath, content, 'utf-8');
317
386
  process.stdout.write(`Created ${targetPath}\n`);
387
+ if (argv.scripts) {
388
+ await addRecommendedScriptsToPackageJson(process.cwd());
389
+ }
318
390
  } catch (err: unknown) {
319
391
  const message = err instanceof Error ? err.message : String(err);
320
392
  log.error('Failed to write config', { error: message });
@@ -5,8 +5,9 @@
5
5
  * with basic validation of known options.
6
6
  */
7
7
 
8
+ import { randomUUID } from 'node:crypto';
8
9
  import { existsSync } from 'node:fs';
9
- import { readFile } from 'node:fs/promises';
10
+ import { readFile, rm, writeFile } from 'node:fs/promises';
10
11
  import { createRequire } from 'node:module';
11
12
  import { dirname, extname, resolve } from 'node:path';
12
13
  import { pathToFileURL } from 'node:url';
@@ -258,27 +259,69 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
258
259
  if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
259
260
  const ts = await import('typescript');
260
261
  const source = await safeReadFileAsync(resolved);
262
+ // Why: many configs want to `import { defineConfig } from 'tywrap'`. The tywrap package is ESM,
263
+ // so evaluating a transpiled CommonJS config would try `require('tywrap')` and fail. Treat
264
+ // `.ts`/`.mts` configs as ESM, and only treat `.cts` as CommonJS to match Node conventions.
265
+ const emitCommonJs = ext === '.cts';
261
266
  const output = ts.transpileModule(source, {
262
267
  compilerOptions: {
263
- module: ts.ModuleKind.CommonJS,
268
+ module: emitCommonJs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ES2020,
264
269
  target: ts.ScriptTarget.ES2020,
265
270
  esModuleInterop: true,
266
271
  },
267
272
  fileName: resolved,
268
273
  });
269
- const require = createRequire(import.meta.url);
270
- const nodeModule = require('module') as typeof import('module');
271
- const moduleCtor = nodeModule.Module as unknown as typeof import('module').Module & {
272
- _nodeModulePaths: (path: string) => string[];
273
- };
274
- const mod = new moduleCtor(resolved) as import('module').Module & {
275
- _compile: (code: string, filename: string) => void;
276
- };
277
- mod.filename = resolved;
278
- mod.paths = moduleCtor._nodeModulePaths(dirname(resolved));
279
- mod._compile(output.outputText, resolved);
280
- const loaded = (mod.exports as Record<string, unknown>).default ?? mod.exports;
281
- return ensureConfigObject(loaded ?? {}, resolved);
274
+
275
+ if (emitCommonJs) {
276
+ // Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
277
+ // Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
278
+ // temp file. These are private Node APIs, so we keep this tooling path scoped and rely on
279
+ // supported Node versions (see package.json engines).
280
+ const require = createRequire(import.meta.url);
281
+ const nodeModule = require('module') as typeof import('module');
282
+ const moduleCtor = nodeModule.Module as unknown as typeof import('module').Module & {
283
+ _nodeModulePaths?: (path: string) => string[];
284
+ };
285
+ if (typeof moduleCtor !== 'function') {
286
+ throw new Error(
287
+ '[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node Module constructor'
288
+ );
289
+ }
290
+ const nodeModulePaths = moduleCtor._nodeModulePaths;
291
+ if (typeof nodeModulePaths !== 'function') {
292
+ throw new Error(
293
+ '[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._nodeModulePaths'
294
+ );
295
+ }
296
+ const mod = new moduleCtor(resolved) as import('module').Module & {
297
+ _compile?: (code: string, filename: string) => void;
298
+ };
299
+ const compile = mod._compile;
300
+ if (typeof compile !== 'function') {
301
+ throw new Error(
302
+ '[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._compile'
303
+ );
304
+ }
305
+ mod.filename = resolved;
306
+ mod.paths = nodeModulePaths(dirname(resolved));
307
+ compile.call(mod, output.outputText, resolved);
308
+ const loaded = (mod.exports as Record<string, unknown>).default ?? mod.exports;
309
+ return ensureConfigObject(loaded ?? {}, resolved);
310
+ }
311
+
312
+ // Why: Node can't import ESM from a string without a custom loader. We write the transpiled
313
+ // output to a temporary `.mjs` file next to the source config so relative imports and Node
314
+ // resolution behave naturally, then clean it up immediately after loading.
315
+ const tmpPath = resolve(dirname(resolved), `.tywrap.config.${randomUUID()}.mjs`);
316
+ try {
317
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from config location
318
+ await writeFile(tmpPath, output.outputText, 'utf-8');
319
+ const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
320
+ const loaded = mod.default ?? mod;
321
+ return ensureConfigObject(loaded ?? {}, resolved);
322
+ } finally {
323
+ await rm(tmpPath, { force: true });
324
+ }
282
325
  }
283
326
 
284
327
  throw new Error(`Unsupported configuration file extension: ${ext}`);
@@ -141,12 +141,16 @@ export class CodeGenerator {
141
141
  const fname = this.escapeIdentifier(func.name);
142
142
  const moduleId = moduleName ?? '__main__';
143
143
 
144
- // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs)
144
+ // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
145
+ // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
146
+ // `fn(a)`, `fn(a, b)`, ... all to typecheck. We emit a family of overloads that progressively
147
+ // "drop" optional tail args, but also include the full positional signature (<= length) so a
148
+ // call that supplies all args still matches an overload.
145
149
  const positional = filteredParams.filter(p => !p.varArgs && !p.kwArgs);
146
150
  const firstOptionalIndex = positional.findIndex(p => p.optional);
147
151
  const overloads: string[] = [];
148
152
  if (firstOptionalIndex >= 0) {
149
- for (let i = firstOptionalIndex; i < positional.length; i++) {
153
+ for (let i = firstOptionalIndex; i <= positional.length; i++) {
150
154
  const head = positional.slice(0, i);
151
155
  const rest = filteredParams.filter(p => p.varArgs || p.kwArgs);
152
156
  const sigParams = [...head, ...rest].map(renderParam).join(', ');
@@ -267,7 +267,8 @@ export class TypeMapper {
267
267
  }
268
268
 
269
269
  // Forward references and user types
270
- return { kind: 'custom', name: type.name, module: type.module };
270
+ const normalized = this.normalizeCustomType(type);
271
+ return { kind: 'custom', name: normalized.name, module: normalized.module };
271
272
  }
272
273
 
273
274
  mapCallableType(type: {
@@ -348,15 +349,26 @@ export class TypeMapper {
348
349
  module?: string;
349
350
  rawName: string;
350
351
  } {
352
+ // Why: the Python IR sometimes represents qualified names like "pandas.DataFrame" as a single
353
+ // `name` string (with dots) instead of splitting into { module, name }. Dots are not valid in
354
+ // TypeScript identifiers, and we want stable test expectations, so normalize to a leaf `name`
355
+ // plus a `module` path when possible.
351
356
  const rawName = type.name;
352
357
  if (type.module) {
353
358
  return { name: type.name, module: type.module, rawName };
354
359
  }
355
360
  if (rawName.includes('.')) {
356
- const parts = rawName.split('.');
361
+ const parts = rawName.split('.').filter(part => part.length > 0);
362
+ if (parts.length === 0) {
363
+ return { name: rawName, module: undefined, rawName };
364
+ }
365
+ const name = parts[parts.length - 1];
366
+ if (!name) {
367
+ return { name: rawName, module: undefined, rawName };
368
+ }
357
369
  return {
358
- name: parts[parts.length - 1] ?? rawName,
359
- module: parts.slice(0, -1).join('.'),
370
+ name,
371
+ module: parts.length > 1 ? parts.slice(0, -1).join('.') : undefined,
360
372
  rawName,
361
373
  };
362
374
  }
package/src/index.ts CHANGED
@@ -69,12 +69,13 @@ export { detectRuntime, isNodejs, isDeno, isBun, isBrowser } from './utils/runti
69
69
  export {
70
70
  decodeValue,
71
71
  decodeValueAsync,
72
+ autoRegisterArrowDecoder,
72
73
  registerArrowDecoder,
73
74
  clearArrowDecoder,
74
75
  } from './utils/codec.js';
75
76
 
76
77
  // Version info
77
- export const VERSION = '0.1.1';
78
+ export const VERSION = '0.1.2';
78
79
 
79
80
  /**
80
81
  * Quick setup function for getting started