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.
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +263 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +41 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +234 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/analyzer.d.ts +63 -0
- package/dist/core/analyzer.d.ts.map +1 -0
- package/dist/core/analyzer.js +676 -0
- package/dist/core/analyzer.js.map +1 -0
- package/dist/core/discovery.d.ts +85 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +344 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/generator.d.ts +37 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/dist/core/generator.js +368 -0
- package/dist/core/generator.js.map +1 -0
- package/dist/core/mapper.d.ts +40 -0
- package/dist/core/mapper.d.ts.map +1 -0
- package/dist/core/mapper.js +401 -0
- package/dist/core/mapper.js.map +1 -0
- package/dist/core/validation.d.ts +79 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +486 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/base.d.ts +12 -0
- package/dist/runtime/base.d.ts.map +1 -0
- package/dist/runtime/base.js +6 -0
- package/dist/runtime/base.js.map +1 -0
- package/dist/runtime/errors.d.ts +17 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +21 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/http.d.ts +22 -0
- package/dist/runtime/http.d.ts.map +1 -0
- package/dist/runtime/http.js +79 -0
- package/dist/runtime/http.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node.d.ts +48 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +328 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/optimized-node.d.ts +131 -0
- package/dist/runtime/optimized-node.d.ts.map +1 -0
- package/dist/runtime/optimized-node.js +622 -0
- package/dist/runtime/optimized-node.js.map +1 -0
- package/dist/runtime/protocol.d.ts +3 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +3 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +27 -0
- package/dist/runtime/pyodide.d.ts.map +1 -0
- package/dist/runtime/pyodide.js +191 -0
- package/dist/runtime/pyodide.js.map +1 -0
- package/dist/tools/python_suite.js +173 -0
- package/dist/tools/python_suite.js.map +1 -0
- package/dist/types/index.d.ts +299 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/tywrap.d.ts +29 -0
- package/dist/tywrap.d.ts.map +1 -0
- package/dist/tywrap.js +589 -0
- package/dist/tywrap.js.map +1 -0
- package/dist/utils/bundle-optimizer.d.ts +182 -0
- package/dist/utils/bundle-optimizer.d.ts.map +1 -0
- package/dist/utils/bundle-optimizer.js +680 -0
- package/dist/utils/bundle-optimizer.js.map +1 -0
- package/dist/utils/cache.d.ts +149 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +492 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/codec.d.ts +94 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +150 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/logger.d.ts +42 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +140 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/memory-profiler.d.ts +123 -0
- package/dist/utils/memory-profiler.d.ts.map +1 -0
- package/dist/utils/memory-profiler.js +426 -0
- package/dist/utils/memory-profiler.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +146 -0
- package/dist/utils/parallel-processor.d.ts.map +1 -0
- package/dist/utils/parallel-processor.js +636 -0
- package/dist/utils/parallel-processor.js.map +1 -0
- package/dist/utils/python.d.ts +8 -0
- package/dist/utils/python.d.ts.map +1 -0
- package/dist/utils/python.js +56 -0
- package/dist/utils/python.js.map +1 -0
- package/dist/utils/runtime.d.ts +153 -0
- package/dist/utils/runtime.d.ts.map +1 -0
- package/dist/utils/runtime.js +519 -0
- package/dist/utils/runtime.js.map +1 -0
- package/package.json +136 -0
- package/runtime/python_bridge.py +476 -0
- package/src/cli.ts +331 -0
- package/src/config/index.ts +289 -0
- package/src/core/analyzer.ts +788 -0
- package/src/core/discovery.ts +415 -0
- package/src/core/generator.ts +447 -0
- package/src/core/mapper.ts +504 -0
- package/src/core/validation.ts +693 -0
- package/src/index.ts +83 -0
- package/src/runtime/base.ts +32 -0
- package/src/runtime/errors.ts +19 -0
- package/src/runtime/http.ts +132 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/node.ts +431 -0
- package/src/runtime/optimized-node.ts +838 -0
- package/src/runtime/protocol.ts +2 -0
- package/src/runtime/pyodide.ts +228 -0
- package/src/types/global.d.ts +60 -0
- package/src/types/index.ts +410 -0
- package/src/tywrap.ts +669 -0
- package/src/utils/bundle-optimizer.ts +876 -0
- package/src/utils/cache.ts +634 -0
- package/src/utils/codec.ts +275 -0
- package/src/utils/logger.ts +201 -0
- package/src/utils/memory-profiler.ts +582 -0
- package/src/utils/parallel-processor.ts +879 -0
- package/src/utils/python.ts +79 -0
- package/src/utils/runtime.ts +616 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import yargs, { type Argv, type ArgumentsCamelCase } from 'yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
|
|
8
|
+
import { resolveConfig, type ResolveConfigOptions } from './config/index.js';
|
|
9
|
+
import type { RuntimeStrategy, TywrapOptions } from './types/index.js';
|
|
10
|
+
import { getComponentLogger } from './utils/logger.js';
|
|
11
|
+
|
|
12
|
+
const log = getComponentLogger('CLI');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG_FILES = [
|
|
15
|
+
'tywrap.config.ts',
|
|
16
|
+
'tywrap.config.mts',
|
|
17
|
+
'tywrap.config.js',
|
|
18
|
+
'tywrap.config.mjs',
|
|
19
|
+
'tywrap.config.cjs',
|
|
20
|
+
'tywrap.config.json',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function resolveConfigPath(explicitPath?: string): { configPath?: string; explicit: boolean } {
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
if (explicitPath) {
|
|
26
|
+
const resolved = resolve(cwd, explicitPath);
|
|
27
|
+
return { configPath: resolved, explicit: true };
|
|
28
|
+
}
|
|
29
|
+
for (const name of DEFAULT_CONFIG_FILES) {
|
|
30
|
+
const candidate = resolve(cwd, name);
|
|
31
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config discovery uses user-controlled paths
|
|
32
|
+
if (existsSync(candidate)) {
|
|
33
|
+
return { configPath: candidate, explicit: false };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { configPath: undefined, explicit: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseModules(modulesList?: string): string[] {
|
|
40
|
+
if (!modulesList) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return modulesList
|
|
44
|
+
.split(',')
|
|
45
|
+
.map(m => m.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildModulesConfig(
|
|
50
|
+
modules: string[],
|
|
51
|
+
runtime: RuntimeStrategy
|
|
52
|
+
): TywrapOptions['pythonModules'] {
|
|
53
|
+
return Object.fromEntries(modules.map(name => [name, { runtime, typeHints: 'strict' }]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderConfigTemplate(options: {
|
|
57
|
+
format: 'ts' | 'json';
|
|
58
|
+
modules: string[];
|
|
59
|
+
runtime: RuntimeStrategy;
|
|
60
|
+
outputDir: string;
|
|
61
|
+
}): string {
|
|
62
|
+
const modules = options.modules.length > 0 ? options.modules : ['math'];
|
|
63
|
+
if (options.format === 'json') {
|
|
64
|
+
const config = {
|
|
65
|
+
pythonModules: buildModulesConfig(modules, options.runtime),
|
|
66
|
+
output: { dir: options.outputDir, format: 'esm', declaration: false, sourceMap: false },
|
|
67
|
+
runtime: { node: { pythonPath: 'python3' } },
|
|
68
|
+
types: { presets: ['stdlib'] },
|
|
69
|
+
};
|
|
70
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const moduleLines = modules
|
|
74
|
+
.map(
|
|
75
|
+
name => ` ${JSON.stringify(name)}: { runtime: '${options.runtime}', typeHints: 'strict' },`
|
|
76
|
+
)
|
|
77
|
+
.join('\n');
|
|
78
|
+
return `import { defineConfig } from 'tywrap';
|
|
79
|
+
|
|
80
|
+
export default defineConfig({
|
|
81
|
+
pythonModules: {
|
|
82
|
+
${moduleLines}
|
|
83
|
+
},
|
|
84
|
+
output: {
|
|
85
|
+
dir: '${options.outputDir}',
|
|
86
|
+
format: 'esm',
|
|
87
|
+
declaration: false,
|
|
88
|
+
sourceMap: false
|
|
89
|
+
},
|
|
90
|
+
runtime: {
|
|
91
|
+
node: {
|
|
92
|
+
pythonPath: 'python3'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
types: {
|
|
96
|
+
presets: ['stdlib']
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main(): Promise<void> {
|
|
103
|
+
await yargs(hideBin(process.argv))
|
|
104
|
+
.scriptName('tywrap')
|
|
105
|
+
.command(
|
|
106
|
+
'generate',
|
|
107
|
+
'Generate TypeScript wrappers',
|
|
108
|
+
(y: Argv) =>
|
|
109
|
+
y
|
|
110
|
+
.option('config', {
|
|
111
|
+
alias: 'c',
|
|
112
|
+
type: 'string',
|
|
113
|
+
describe: 'Path to config file (defaults to tywrap.config.* if present)',
|
|
114
|
+
})
|
|
115
|
+
.option('modules', {
|
|
116
|
+
type: 'string',
|
|
117
|
+
describe: 'Comma-separated list of Python modules to wrap',
|
|
118
|
+
})
|
|
119
|
+
.option('runtime', {
|
|
120
|
+
type: 'string',
|
|
121
|
+
choices: ['node', 'pyodide', 'http', 'auto'],
|
|
122
|
+
default: 'node',
|
|
123
|
+
describe: 'Runtime to use when --modules is provided',
|
|
124
|
+
})
|
|
125
|
+
.option('python', {
|
|
126
|
+
type: 'string',
|
|
127
|
+
describe: 'Path to Python executable',
|
|
128
|
+
})
|
|
129
|
+
.option('output-dir', {
|
|
130
|
+
type: 'string',
|
|
131
|
+
describe: 'Directory for generated wrappers',
|
|
132
|
+
})
|
|
133
|
+
.option('format', {
|
|
134
|
+
type: 'string',
|
|
135
|
+
choices: ['esm', 'cjs', 'both'],
|
|
136
|
+
describe: 'Output module format',
|
|
137
|
+
})
|
|
138
|
+
.option('declaration', {
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
describe: 'Emit TypeScript declaration files',
|
|
141
|
+
})
|
|
142
|
+
.option('source-map', {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
describe: 'Emit source maps for generated files',
|
|
145
|
+
})
|
|
146
|
+
.option('use-cache', {
|
|
147
|
+
alias: 'cache',
|
|
148
|
+
type: 'boolean',
|
|
149
|
+
describe: 'Enable on-disk caching (use --no-cache to disable)',
|
|
150
|
+
})
|
|
151
|
+
.option('debug', {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
describe: 'Enable debug logging',
|
|
154
|
+
})
|
|
155
|
+
.option('fail-on-warn', {
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
default: false,
|
|
158
|
+
describe: 'Exit with code 2 if generation emits warnings',
|
|
159
|
+
})
|
|
160
|
+
.strict(),
|
|
161
|
+
async (
|
|
162
|
+
argv: ArgumentsCamelCase<{
|
|
163
|
+
config?: string;
|
|
164
|
+
modules?: string;
|
|
165
|
+
runtime: RuntimeStrategy;
|
|
166
|
+
python?: string;
|
|
167
|
+
outputDir?: string;
|
|
168
|
+
format?: 'esm' | 'cjs' | 'both';
|
|
169
|
+
declaration?: boolean;
|
|
170
|
+
sourceMap?: boolean;
|
|
171
|
+
useCache?: boolean;
|
|
172
|
+
debug?: boolean;
|
|
173
|
+
failOnWarn: boolean;
|
|
174
|
+
}>
|
|
175
|
+
) => {
|
|
176
|
+
const { configPath, explicit } = resolveConfigPath(argv.config);
|
|
177
|
+
const modules = parseModules(argv.modules);
|
|
178
|
+
|
|
179
|
+
const overrides: NonNullable<ResolveConfigOptions['overrides']> = {};
|
|
180
|
+
if (modules.length > 0) {
|
|
181
|
+
overrides.pythonModules = buildModulesConfig(modules, argv.runtime);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
argv.outputDir ||
|
|
186
|
+
argv.format ||
|
|
187
|
+
typeof argv.declaration === 'boolean' ||
|
|
188
|
+
typeof argv.sourceMap === 'boolean'
|
|
189
|
+
) {
|
|
190
|
+
overrides.output = {
|
|
191
|
+
...(argv.outputDir ? { dir: argv.outputDir } : {}),
|
|
192
|
+
...(argv.format ? { format: argv.format } : {}),
|
|
193
|
+
...(typeof argv.declaration === 'boolean' ? { declaration: argv.declaration } : {}),
|
|
194
|
+
...(typeof argv.sourceMap === 'boolean' ? { sourceMap: argv.sourceMap } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof argv.useCache === 'boolean') {
|
|
199
|
+
overrides.performance = {
|
|
200
|
+
caching: argv.useCache,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (argv.python) {
|
|
205
|
+
overrides.runtime = {
|
|
206
|
+
node: { pythonPath: argv.python },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof argv.debug === 'boolean') {
|
|
211
|
+
overrides.debug = argv.debug;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!configPath && modules.length === 0) {
|
|
215
|
+
log.error('No config file found and no modules provided. Create a config with `tywrap init` or pass --modules.');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { generate } = await import('./tywrap.js');
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const options = await resolveConfig({
|
|
223
|
+
configFile: configPath,
|
|
224
|
+
overrides,
|
|
225
|
+
requireConfig: explicit,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!options.pythonModules || Object.keys(options.pythonModules).length === 0) {
|
|
229
|
+
log.error('No pythonModules configured. Use --modules or update your config.');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const res = await generate(options);
|
|
234
|
+
process.stdout.write(`Generated: ${res.written.join(', ')}\n`);
|
|
235
|
+
if (argv.failOnWarn && res.warnings.length > 0) {
|
|
236
|
+
log.error(`Warnings encountered (count ${res.warnings.length}). Failing due to --fail-on-warn.`);
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
} catch (err: unknown) {
|
|
240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
241
|
+
log.error('Generation failed', { error: message });
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
.command(
|
|
247
|
+
'init',
|
|
248
|
+
'Create a starter tywrap.config file',
|
|
249
|
+
(y: Argv) =>
|
|
250
|
+
y
|
|
251
|
+
.option('config', {
|
|
252
|
+
alias: 'c',
|
|
253
|
+
type: 'string',
|
|
254
|
+
describe: 'Path for the new config file',
|
|
255
|
+
})
|
|
256
|
+
.option('format', {
|
|
257
|
+
type: 'string',
|
|
258
|
+
choices: ['ts', 'json'],
|
|
259
|
+
default: 'ts',
|
|
260
|
+
describe: 'Config file format',
|
|
261
|
+
})
|
|
262
|
+
.option('modules', {
|
|
263
|
+
type: 'string',
|
|
264
|
+
describe: 'Comma-separated list of Python modules to wrap',
|
|
265
|
+
})
|
|
266
|
+
.option('runtime', {
|
|
267
|
+
type: 'string',
|
|
268
|
+
choices: ['node', 'pyodide', 'http', 'auto'],
|
|
269
|
+
default: 'node',
|
|
270
|
+
describe: 'Runtime for generated module entries',
|
|
271
|
+
})
|
|
272
|
+
.option('output-dir', {
|
|
273
|
+
type: 'string',
|
|
274
|
+
default: './generated',
|
|
275
|
+
describe: 'Output directory for generated wrappers',
|
|
276
|
+
})
|
|
277
|
+
.option('force', {
|
|
278
|
+
type: 'boolean',
|
|
279
|
+
default: false,
|
|
280
|
+
describe: 'Overwrite existing config file',
|
|
281
|
+
})
|
|
282
|
+
.strict(),
|
|
283
|
+
async (
|
|
284
|
+
argv: ArgumentsCamelCase<{
|
|
285
|
+
config?: string;
|
|
286
|
+
format: 'ts' | 'json';
|
|
287
|
+
modules?: string;
|
|
288
|
+
runtime: RuntimeStrategy;
|
|
289
|
+
outputDir: string;
|
|
290
|
+
force: boolean;
|
|
291
|
+
}>
|
|
292
|
+
) => {
|
|
293
|
+
const modules = parseModules(argv.modules);
|
|
294
|
+
const defaultName = argv.format === 'json' ? 'tywrap.config.json' : 'tywrap.config.ts';
|
|
295
|
+
const targetPath = resolve(process.cwd(), argv.config ?? defaultName);
|
|
296
|
+
|
|
297
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled
|
|
298
|
+
if (!argv.force && existsSync(targetPath)) {
|
|
299
|
+
log.error('Config file already exists. Use --force to overwrite.', { path: targetPath });
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const content = renderConfigTemplate({
|
|
304
|
+
format: argv.format,
|
|
305
|
+
modules,
|
|
306
|
+
runtime: argv.runtime,
|
|
307
|
+
outputDir: argv.outputDir,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled
|
|
312
|
+
await writeFile(targetPath, content, 'utf-8');
|
|
313
|
+
process.stdout.write(`Created ${targetPath}\n`);
|
|
314
|
+
} catch (err: unknown) {
|
|
315
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
316
|
+
log.error('Failed to write config', { error: message });
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
.demandCommand(1, 'Please specify a command')
|
|
322
|
+
.strict()
|
|
323
|
+
.help()
|
|
324
|
+
.version()
|
|
325
|
+
.parse();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
main().catch(err => {
|
|
329
|
+
log.error('Unexpected error', { error: err instanceof Error ? err.message : String(err) });
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration system
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration from defaults, optional config file and CLI overrides
|
|
5
|
+
* with basic validation of known options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import type {
|
|
14
|
+
TywrapOptions,
|
|
15
|
+
OutputConfig,
|
|
16
|
+
RuntimeConfig,
|
|
17
|
+
PerformanceConfig,
|
|
18
|
+
DevelopmentConfig,
|
|
19
|
+
} from '../types/index.js';
|
|
20
|
+
import { getDefaultPythonPath } from '../utils/python.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Public configuration type. Currently identical to {@link TywrapOptions}.
|
|
24
|
+
*/
|
|
25
|
+
export type TywrapConfig = TywrapOptions;
|
|
26
|
+
|
|
27
|
+
type DeepPartial<T> = {
|
|
28
|
+
[K in keyof T]?: T[K] extends Array<infer U>
|
|
29
|
+
? Array<U>
|
|
30
|
+
: T[K] extends object
|
|
31
|
+
? DeepPartial<T[K]>
|
|
32
|
+
: T[K];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default configuration values used when options are not supplied.
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_CONFIG: TywrapConfig = {
|
|
39
|
+
pythonModules: {},
|
|
40
|
+
output: { dir: './generated', format: 'esm', declaration: false, sourceMap: false },
|
|
41
|
+
runtime: { node: { pythonPath: getDefaultPythonPath(), timeout: 30000 } },
|
|
42
|
+
performance: { caching: false, batching: false, compression: 'none' },
|
|
43
|
+
development: { hotReload: false, sourceMap: false, validation: 'none' },
|
|
44
|
+
types: { presets: [] },
|
|
45
|
+
debug: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recursively merge two configuration objects. Arrays are overwritten by
|
|
50
|
+
* overrides instead of concatenated.
|
|
51
|
+
*/
|
|
52
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
53
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureConfigObject(value: unknown, configFile: string): Partial<TywrapOptions> {
|
|
57
|
+
if (!isPlainObject(value)) {
|
|
58
|
+
throw new Error(`Configuration file must export an object: ${configFile}`);
|
|
59
|
+
}
|
|
60
|
+
return value as Partial<TywrapOptions>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function merge<T>(base: T, override: DeepPartial<T>): T {
|
|
64
|
+
const result = new Map<string, unknown>(Object.entries(base as Record<string, unknown>));
|
|
65
|
+
for (const [key, val] of Object.entries(override)) {
|
|
66
|
+
if (val === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const current = result.get(key);
|
|
70
|
+
if (isPlainObject(val) && isPlainObject(current)) {
|
|
71
|
+
result.set(key, merge(current, val));
|
|
72
|
+
} else {
|
|
73
|
+
result.set(key, val);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Object.fromEntries(result) as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeExists(path: string): boolean {
|
|
80
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled and resolved
|
|
81
|
+
return existsSync(path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate configuration values and throw user-friendly errors when invalid.
|
|
86
|
+
*/
|
|
87
|
+
function validateConfig(config: TywrapConfig): void {
|
|
88
|
+
const allowedTopLevel = new Set([
|
|
89
|
+
'pythonModules',
|
|
90
|
+
'output',
|
|
91
|
+
'runtime',
|
|
92
|
+
'performance',
|
|
93
|
+
'development',
|
|
94
|
+
'types',
|
|
95
|
+
'debug',
|
|
96
|
+
]);
|
|
97
|
+
for (const key of Object.keys(config)) {
|
|
98
|
+
if (!allowedTopLevel.has(key)) {
|
|
99
|
+
throw new Error(`Unknown configuration option \"${key}\"`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const out: OutputConfig = config.output;
|
|
104
|
+
if (typeof out.dir !== 'string') {
|
|
105
|
+
throw new Error('output.dir must be a string');
|
|
106
|
+
}
|
|
107
|
+
if (!['esm', 'cjs', 'both'].includes(out.format)) {
|
|
108
|
+
throw new Error('output.format must be one of "esm", "cjs" or "both"');
|
|
109
|
+
}
|
|
110
|
+
if (typeof out.declaration !== 'boolean' || typeof out.sourceMap !== 'boolean') {
|
|
111
|
+
throw new Error('output.declaration and output.sourceMap must be boolean');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const runtime: RuntimeConfig = config.runtime;
|
|
115
|
+
if (runtime.node) {
|
|
116
|
+
if (runtime.node.pythonPath !== undefined && typeof runtime.node.pythonPath !== 'string') {
|
|
117
|
+
throw new Error('runtime.node.pythonPath must be a string');
|
|
118
|
+
}
|
|
119
|
+
if (runtime.node.timeout !== undefined) {
|
|
120
|
+
if (typeof runtime.node.timeout !== 'number' || runtime.node.timeout < 0) {
|
|
121
|
+
throw new Error('runtime.node.timeout must be a non-negative number');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const perf: PerformanceConfig = config.performance;
|
|
127
|
+
if (typeof perf.caching !== 'boolean') {
|
|
128
|
+
throw new Error('performance.caching must be a boolean');
|
|
129
|
+
}
|
|
130
|
+
if (typeof perf.batching !== 'boolean') {
|
|
131
|
+
throw new Error('performance.batching must be a boolean');
|
|
132
|
+
}
|
|
133
|
+
const validCompression = ['auto', 'gzip', 'brotli', 'none'];
|
|
134
|
+
if (!validCompression.includes(perf.compression)) {
|
|
135
|
+
throw new Error(`performance.compression must be one of ${validCompression.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dev: DevelopmentConfig = config.development;
|
|
139
|
+
if (typeof dev.hotReload !== 'boolean') {
|
|
140
|
+
throw new Error('development.hotReload must be a boolean');
|
|
141
|
+
}
|
|
142
|
+
if (typeof dev.sourceMap !== 'boolean') {
|
|
143
|
+
throw new Error('development.sourceMap must be a boolean');
|
|
144
|
+
}
|
|
145
|
+
const validValidation = ['runtime', 'compile', 'both', 'none'];
|
|
146
|
+
if (!validValidation.includes(dev.validation)) {
|
|
147
|
+
throw new Error(`development.validation must be one of ${validValidation.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.types) {
|
|
151
|
+
const presets = config.types.presets;
|
|
152
|
+
const validPresets = new Set(['numpy', 'pandas', 'pydantic', 'stdlib', 'scipy', 'torch', 'sklearn']);
|
|
153
|
+
if (presets !== undefined) {
|
|
154
|
+
if (!Array.isArray(presets) || presets.some(p => typeof p !== 'string')) {
|
|
155
|
+
throw new Error('types.presets must be an array of strings');
|
|
156
|
+
}
|
|
157
|
+
for (const preset of presets) {
|
|
158
|
+
if (!validPresets.has(preset)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`types.presets contains invalid value "${preset}". Allowed: ${Array.from(validPresets).join(', ')}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof config.debug !== 'boolean') {
|
|
168
|
+
throw new Error('debug must be a boolean');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a configuration object by merging defaults with overrides.
|
|
174
|
+
*
|
|
175
|
+
* @param overrides CLI or programmatic overrides
|
|
176
|
+
*/
|
|
177
|
+
export function createConfig(overrides: DeepPartial<TywrapOptions> = {}): TywrapConfig {
|
|
178
|
+
const merged = merge(DEFAULT_CONFIG, overrides);
|
|
179
|
+
validateConfig(merged);
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ResolveConfigOptions {
|
|
184
|
+
configFile?: string;
|
|
185
|
+
overrides?: DeepPartial<TywrapOptions>;
|
|
186
|
+
cwd?: string;
|
|
187
|
+
requireConfig?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve configuration by loading a config file (JSON/JS/TS) and merging
|
|
192
|
+
* defaults with any overrides.
|
|
193
|
+
*/
|
|
194
|
+
export async function resolveConfig(options: ResolveConfigOptions = {}): Promise<TywrapConfig> {
|
|
195
|
+
const cwd = options.cwd ?? process.cwd();
|
|
196
|
+
const overrides = options.overrides ?? {};
|
|
197
|
+
const configFile = options.configFile ? resolve(cwd, options.configFile) : undefined;
|
|
198
|
+
let fileConfig: DeepPartial<TywrapOptions> = {};
|
|
199
|
+
|
|
200
|
+
if (configFile) {
|
|
201
|
+
if (!safeExists(configFile)) {
|
|
202
|
+
if (options.requireConfig) {
|
|
203
|
+
throw new Error(`Configuration file not found: ${configFile}`);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
fileConfig = await loadConfigFile(configFile);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const merged = merge(merge(DEFAULT_CONFIG, fileConfig), overrides);
|
|
211
|
+
validateConfig(merged);
|
|
212
|
+
return merged;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load a tywrap config file. Supports JSON, JS/CJS/ESM, and TS files.
|
|
217
|
+
*/
|
|
218
|
+
export async function loadConfigFile(configFile: string): Promise<Partial<TywrapOptions>> {
|
|
219
|
+
const ext = extname(configFile).toLowerCase();
|
|
220
|
+
const resolved = resolve(configFile);
|
|
221
|
+
|
|
222
|
+
if (!safeExists(resolved)) {
|
|
223
|
+
throw new Error(`Configuration file not found: ${resolved}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (ext === '.json') {
|
|
227
|
+
const txt = await safeReadFileAsync(resolved);
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(txt) as unknown;
|
|
230
|
+
return ensureConfigObject(parsed, resolved);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
throw new Error(`Failed to parse JSON config ${resolved}: ${(err as Error).message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (ext === '.cjs') {
|
|
237
|
+
const require = createRequire(import.meta.url);
|
|
238
|
+
// eslint-disable-next-line security/detect-non-literal-require -- config path is user-controlled and resolved
|
|
239
|
+
const mod = require(resolved) as Record<string, unknown>;
|
|
240
|
+
const loaded = mod.default ?? mod;
|
|
241
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (ext === '.js' || ext === '.mjs') {
|
|
245
|
+
const mod = (await import(pathToFileURL(resolved).href)) as Record<string, unknown>;
|
|
246
|
+
const loaded = mod.default ?? mod;
|
|
247
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
|
|
251
|
+
const ts = await import('typescript');
|
|
252
|
+
const source = await safeReadFileAsync(resolved);
|
|
253
|
+
const output = ts.transpileModule(source, {
|
|
254
|
+
compilerOptions: {
|
|
255
|
+
module: ts.ModuleKind.CommonJS,
|
|
256
|
+
target: ts.ScriptTarget.ES2020,
|
|
257
|
+
esModuleInterop: true,
|
|
258
|
+
},
|
|
259
|
+
fileName: resolved,
|
|
260
|
+
});
|
|
261
|
+
const require = createRequire(import.meta.url);
|
|
262
|
+
const nodeModule = require('module') as typeof import('module');
|
|
263
|
+
const moduleCtor = nodeModule.Module as unknown as typeof import('module').Module & {
|
|
264
|
+
_nodeModulePaths: (path: string) => string[];
|
|
265
|
+
};
|
|
266
|
+
const mod = new moduleCtor(resolved) as import('module').Module & {
|
|
267
|
+
_compile: (code: string, filename: string) => void;
|
|
268
|
+
};
|
|
269
|
+
mod.filename = resolved;
|
|
270
|
+
mod.paths = moduleCtor._nodeModulePaths(dirname(resolved));
|
|
271
|
+
mod._compile(output.outputText, resolved);
|
|
272
|
+
const loaded = (mod.exports as Record<string, unknown>).default ?? mod.exports;
|
|
273
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new Error(`Unsupported configuration file extension: ${ext}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function safeReadFileAsync(path: string): Promise<string> {
|
|
280
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled and resolved
|
|
281
|
+
return readFile(path, 'utf-8');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Type helper for authoring tywrap configs with full type inference.
|
|
286
|
+
*/
|
|
287
|
+
export function defineConfig(config: TywrapConfig): TywrapConfig {
|
|
288
|
+
return config;
|
|
289
|
+
}
|