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.
- package/README.md +23 -3
- package/dist/cli.js +70 -3
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +47 -11
- package/dist/config/index.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +6 -2
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +16 -4
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +64 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -0
- package/dist/runtime/bridge-core.js +344 -0
- package/dist/runtime/bridge-core.js.map +1 -0
- package/dist/runtime/node.d.ts +5 -6
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +94 -190
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +4 -7
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +117 -194
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +20 -0
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -0
- package/dist/runtime/timed-out-request-tracker.js +48 -0
- package/dist/runtime/timed-out-request-tracker.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +17 -4
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +68 -35
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/codec.d.ts +22 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +245 -55
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +1 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js.map +1 -1
- package/package.json +11 -2
- package/runtime/python_bridge.py +450 -69
- package/src/cli.ts +75 -3
- package/src/config/index.ts +58 -15
- package/src/core/generator.ts +6 -2
- package/src/core/mapper.ts +16 -4
- package/src/index.ts +2 -1
- package/src/runtime/bridge-core.ts +454 -0
- package/src/runtime/node.ts +116 -220
- package/src/runtime/optimized-node.ts +160 -243
- package/src/runtime/timed-out-request-tracker.ts +58 -0
- package/src/types/index.ts +3 -0
- package/src/tywrap.ts +91 -36
- package/src/utils/codec.ts +299 -82
- package/src/utils/logger.ts +1 -0
- 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
|
-
|
|
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 });
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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}`);
|
package/src/core/generator.ts
CHANGED
|
@@ -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
|
|
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(', ');
|
package/src/core/mapper.ts
CHANGED
|
@@ -267,7 +267,8 @@ export class TypeMapper {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// Forward references and user types
|
|
270
|
-
|
|
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
|
|
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.
|
|
78
|
+
export const VERSION = '0.1.2';
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* Quick setup function for getting started
|