tywrap 0.1.0 → 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 +54 -221
- 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 +56 -12
- 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 +20 -5
- 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 +95 -193
- 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 -186
- 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 +7 -4
- 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 +81 -5
- package/src/config/index.ts +67 -16
- package/src/core/generator.ts +6 -2
- package/src/core/mapper.ts +21 -8
- package/src/index.ts +2 -1
- package/src/runtime/bridge-core.ts +454 -0
- package/src/runtime/node.ts +117 -224
- package/src/runtime/optimized-node.ts +160 -235
- 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 +306 -83
- package/src/utils/logger.ts +11 -7
- 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);
|
|
@@ -212,7 +258,9 @@ async function main(): Promise<void> {
|
|
|
212
258
|
}
|
|
213
259
|
|
|
214
260
|
if (!configPath && modules.length === 0) {
|
|
215
|
-
log.error(
|
|
261
|
+
log.error(
|
|
262
|
+
'No config file found and no modules provided. Create a config with `tywrap init` or pass --modules.'
|
|
263
|
+
);
|
|
216
264
|
process.exit(1);
|
|
217
265
|
}
|
|
218
266
|
|
|
@@ -230,10 +278,28 @@ async function main(): Promise<void> {
|
|
|
230
278
|
process.exit(1);
|
|
231
279
|
}
|
|
232
280
|
|
|
233
|
-
const res = await generate(options);
|
|
234
|
-
|
|
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
|
+
}
|
|
235
299
|
if (argv.failOnWarn && res.warnings.length > 0) {
|
|
236
|
-
log.error(
|
|
300
|
+
log.error(
|
|
301
|
+
`Warnings encountered (count ${res.warnings.length}). Failing due to --fail-on-warn.`
|
|
302
|
+
);
|
|
237
303
|
process.exit(2);
|
|
238
304
|
}
|
|
239
305
|
} catch (err: unknown) {
|
|
@@ -279,6 +345,12 @@ async function main(): Promise<void> {
|
|
|
279
345
|
default: false,
|
|
280
346
|
describe: 'Overwrite existing config file',
|
|
281
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
|
+
})
|
|
282
354
|
.strict(),
|
|
283
355
|
async (
|
|
284
356
|
argv: ArgumentsCamelCase<{
|
|
@@ -288,6 +360,7 @@ async function main(): Promise<void> {
|
|
|
288
360
|
runtime: RuntimeStrategy;
|
|
289
361
|
outputDir: string;
|
|
290
362
|
force: boolean;
|
|
363
|
+
scripts: boolean;
|
|
291
364
|
}>
|
|
292
365
|
) => {
|
|
293
366
|
const modules = parseModules(argv.modules);
|
|
@@ -311,6 +384,9 @@ async function main(): Promise<void> {
|
|
|
311
384
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled
|
|
312
385
|
await writeFile(targetPath, content, 'utf-8');
|
|
313
386
|
process.stdout.write(`Created ${targetPath}\n`);
|
|
387
|
+
if (argv.scripts) {
|
|
388
|
+
await addRecommendedScriptsToPackageJson(process.cwd());
|
|
389
|
+
}
|
|
314
390
|
} catch (err: unknown) {
|
|
315
391
|
const message = err instanceof Error ? err.message : String(err);
|
|
316
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';
|
|
@@ -149,7 +150,15 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
149
150
|
|
|
150
151
|
if (config.types) {
|
|
151
152
|
const presets = config.types.presets;
|
|
152
|
-
const validPresets = new Set([
|
|
153
|
+
const validPresets = new Set([
|
|
154
|
+
'numpy',
|
|
155
|
+
'pandas',
|
|
156
|
+
'pydantic',
|
|
157
|
+
'stdlib',
|
|
158
|
+
'scipy',
|
|
159
|
+
'torch',
|
|
160
|
+
'sklearn',
|
|
161
|
+
]);
|
|
153
162
|
if (presets !== undefined) {
|
|
154
163
|
if (!Array.isArray(presets) || presets.some(p => typeof p !== 'string')) {
|
|
155
164
|
throw new Error('types.presets must be an array of strings');
|
|
@@ -250,27 +259,69 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
250
259
|
if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
|
|
251
260
|
const ts = await import('typescript');
|
|
252
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';
|
|
253
266
|
const output = ts.transpileModule(source, {
|
|
254
267
|
compilerOptions: {
|
|
255
|
-
module: ts.ModuleKind.CommonJS,
|
|
268
|
+
module: emitCommonJs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ES2020,
|
|
256
269
|
target: ts.ScriptTarget.ES2020,
|
|
257
270
|
esModuleInterop: true,
|
|
258
271
|
},
|
|
259
272
|
fileName: resolved,
|
|
260
273
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
325
|
}
|
|
275
326
|
|
|
276
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
|
}
|
|
@@ -441,9 +453,7 @@ export class TypeMapper {
|
|
|
441
453
|
const allowModule = !moduleName || moduleName.startsWith('scipy');
|
|
442
454
|
if (allowModule) {
|
|
443
455
|
const baseProps = [prop('shape', numberArray), prop('dtype', stringType, true)];
|
|
444
|
-
const buildSparse = (
|
|
445
|
-
format: 'csr' | 'csc' | 'coo'
|
|
446
|
-
): TSObjectType => ({
|
|
456
|
+
const buildSparse = (format: 'csr' | 'csc' | 'coo'): TSObjectType => ({
|
|
447
457
|
kind: 'object',
|
|
448
458
|
properties: [
|
|
449
459
|
prop('format', { kind: 'literal', value: format }),
|
|
@@ -464,7 +474,10 @@ export class TypeMapper {
|
|
|
464
474
|
return buildSparse('coo');
|
|
465
475
|
}
|
|
466
476
|
if (name === 'spmatrix') {
|
|
467
|
-
return {
|
|
477
|
+
return {
|
|
478
|
+
kind: 'union',
|
|
479
|
+
types: [buildSparse('csr'), buildSparse('csc'), buildSparse('coo')],
|
|
480
|
+
};
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
483
|
}
|
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
|