tywrap 0.5.1 → 0.6.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/dist/cli.js +8 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +236 -147
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +93 -83
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +26 -0
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/emit-call.d.ts +58 -0
- package/dist/core/emit-call.d.ts.map +1 -0
- package/dist/core/emit-call.js +103 -0
- package/dist/core/emit-call.js.map +1 -0
- package/dist/core/generator.d.ts +2 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +54 -211
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +118 -104
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +6 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -31
- package/dist/index.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +2 -2
- package/dist/runtime/bounded-context.js +2 -2
- package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +11 -11
- package/dist/runtime/bridge-codec.d.ts.map +1 -0
- package/dist/runtime/{safe-codec.js → bridge-codec.js} +26 -26
- package/dist/runtime/bridge-codec.js.map +1 -0
- package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
- package/dist/runtime/http-transport.d.ts.map +1 -0
- package/dist/runtime/{http-io.js → http-transport.js} +3 -3
- package/dist/runtime/http-transport.js.map +1 -0
- package/dist/runtime/http.d.ts +3 -3
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +4 -4
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/index.d.ts +9 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +6 -6
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -7
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +11 -11
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +10 -10
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
- package/dist/runtime/pyodide-transport.d.ts.map +1 -0
- package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
- package/dist/runtime/pyodide-transport.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +3 -3
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +5 -5
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +5 -5
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +6 -7
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +9 -9
- package/dist/runtime/subprocess-transport.d.ts.map +1 -0
- package/dist/runtime/{process-io.js → subprocess-transport.js} +6 -6
- package/dist/runtime/subprocess-transport.js.map +1 -0
- package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
- package/dist/runtime/transport-pool.d.ts.map +1 -0
- package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
- package/dist/runtime/transport-pool.js.map +1 -0
- package/dist/runtime/transport.d.ts +14 -8
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +10 -4
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +2 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +2 -2
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +181 -182
- package/dist/utils/codec.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -2
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/python_bridge.py +54 -6
- package/runtime/safe_codec.py +8 -8
- package/runtime/tywrap_bridge_core.py +159 -23
- package/src/cli.ts +8 -12
- package/src/config/index.ts +271 -165
- package/src/core/annotation-parser.ts +112 -105
- package/src/core/discovery.ts +32 -0
- package/src/core/emit-call.ts +171 -0
- package/src/core/generator.ts +67 -263
- package/src/core/mapper.ts +125 -110
- package/src/index.ts +11 -68
- package/src/runtime/bounded-context.ts +2 -2
- package/src/runtime/{safe-codec.ts → bridge-codec.ts} +27 -27
- package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
- package/src/runtime/http.ts +5 -5
- package/src/runtime/index.ts +23 -1
- package/src/runtime/node.ts +15 -15
- package/src/runtime/pooled-transport.ts +15 -15
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
- package/src/runtime/pyodide.ts +6 -6
- package/src/runtime/rpc-client.ts +12 -8
- package/src/runtime/{process-io.ts → subprocess-transport.ts} +8 -8
- package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
- package/src/runtime/transport.ts +15 -8
- package/src/types/index.ts +10 -1
- package/src/utils/cache.ts +2 -2
- package/src/utils/codec.ts +217 -196
- package/src/version.ts +12 -0
- package/dist/runtime/http-io.d.ts.map +0 -1
- package/dist/runtime/http-io.js.map +0 -1
- package/dist/runtime/optimized-node.d.ts +0 -23
- package/dist/runtime/optimized-node.d.ts.map +0 -1
- package/dist/runtime/optimized-node.js +0 -23
- package/dist/runtime/optimized-node.js.map +0 -1
- package/dist/runtime/process-io.d.ts.map +0 -1
- package/dist/runtime/process-io.js.map +0 -1
- package/dist/runtime/protocol.d.ts +0 -3
- package/dist/runtime/protocol.d.ts.map +0 -1
- package/dist/runtime/protocol.js +0 -3
- package/dist/runtime/protocol.js.map +0 -1
- package/dist/runtime/pyodide-io.d.ts.map +0 -1
- package/dist/runtime/pyodide-io.js.map +0 -1
- package/dist/runtime/safe-codec.d.ts.map +0 -1
- package/dist/runtime/safe-codec.js.map +0 -1
- package/dist/runtime/worker-pool.d.ts.map +0 -1
- package/dist/runtime/worker-pool.js.map +0 -1
- package/dist/utils/bundle-optimizer.d.ts +0 -182
- package/dist/utils/bundle-optimizer.d.ts.map +0 -1
- package/dist/utils/bundle-optimizer.js +0 -680
- package/dist/utils/bundle-optimizer.js.map +0 -1
- package/dist/utils/memory-profiler.d.ts +0 -123
- package/dist/utils/memory-profiler.d.ts.map +0 -1
- package/dist/utils/memory-profiler.js +0 -426
- package/dist/utils/memory-profiler.js.map +0 -1
- package/src/runtime/optimized-node.ts +0 -25
- package/src/runtime/protocol.ts +0 -2
- package/src/utils/bundle-optimizer.ts +0 -876
- package/src/utils/memory-profiler.ts +0 -582
package/src/config/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
OutputConfig,
|
|
18
18
|
RuntimeConfig,
|
|
19
19
|
PerformanceConfig,
|
|
20
|
+
PythonModuleConfig,
|
|
20
21
|
} from '../types/index.js';
|
|
21
22
|
import { getDefaultPythonPath } from '../utils/python.js';
|
|
22
23
|
|
|
@@ -114,94 +115,174 @@ function detectLegacyFields(config: TywrapConfig): void {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
const ALLOWED_TOP_LEVEL = new Set([
|
|
119
|
+
'pythonModules',
|
|
120
|
+
'pythonImportPath',
|
|
121
|
+
'output',
|
|
122
|
+
'runtime',
|
|
123
|
+
'performance',
|
|
124
|
+
'types',
|
|
125
|
+
'debug',
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const VALID_OUTPUT_FORMATS = ['esm', 'cjs', 'both'];
|
|
129
|
+
const VALID_COMPRESSION = ['auto', 'gzip', 'brotli', 'none'];
|
|
130
|
+
const VALID_TYPE_HINTS = ['strict', 'loose', 'ignore'];
|
|
131
|
+
const VALID_TYPE_PRESETS = new Set([
|
|
132
|
+
'numpy',
|
|
133
|
+
'pandas',
|
|
134
|
+
'pydantic',
|
|
135
|
+
'stdlib',
|
|
136
|
+
'scipy',
|
|
137
|
+
'torch',
|
|
138
|
+
'sklearn',
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
function isStringArray(value: unknown): value is string[] {
|
|
142
|
+
return Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateModuleConfig(scope: string, moduleConfig: PythonModuleConfig): void {
|
|
146
|
+
const { version, alias, functions, classes, exclude, excludePatterns, typeHints } = moduleConfig;
|
|
147
|
+
if (version !== undefined && typeof version !== 'string') {
|
|
148
|
+
throw new Error(`${scope}.version must be a string`);
|
|
149
|
+
}
|
|
150
|
+
if (alias !== undefined && typeof alias !== 'string') {
|
|
151
|
+
throw new Error(`${scope}.alias must be a string`);
|
|
152
|
+
}
|
|
153
|
+
const stringArrayFields: ReadonlyArray<readonly [string, unknown]> = [
|
|
154
|
+
['functions', functions],
|
|
155
|
+
['classes', classes],
|
|
156
|
+
['exclude', exclude],
|
|
157
|
+
['excludePatterns', excludePatterns],
|
|
158
|
+
];
|
|
159
|
+
for (const [field, value] of stringArrayFields) {
|
|
160
|
+
if (value !== undefined && !isStringArray(value)) {
|
|
161
|
+
throw new Error(`${scope}.${field} must be an array of strings`);
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (typeof out.dir !== 'string') {
|
|
139
|
-
throw new Error('output.dir must be a string');
|
|
164
|
+
if (typeHints !== undefined && !VALID_TYPE_HINTS.includes(typeHints)) {
|
|
165
|
+
throw new Error(`${scope}.typeHints must be one of ${VALID_TYPE_HINTS.join(', ')}`);
|
|
140
166
|
}
|
|
167
|
+
// Note: `runtime` is a deprecated, dead per-module field (see PythonModuleConfig).
|
|
168
|
+
// It is intentionally not validated so legacy configs keep loading; it has no effect.
|
|
169
|
+
}
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
) {
|
|
147
|
-
throw new Error('pythonImportPath must be an array of strings');
|
|
171
|
+
function validatePythonModules(modules: Record<string, PythonModuleConfig>): void {
|
|
172
|
+
for (const [name, moduleConfig] of Object.entries(modules)) {
|
|
173
|
+
if (!isPlainObject(moduleConfig)) {
|
|
174
|
+
throw new Error(`pythonModules.${name} must be an object`);
|
|
148
175
|
}
|
|
176
|
+
validateModuleConfig(`pythonModules.${name}`, moduleConfig);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateOutput(out: OutputConfig): void {
|
|
181
|
+
if (typeof out.dir !== 'string') {
|
|
182
|
+
throw new Error('output.dir must be a string');
|
|
149
183
|
}
|
|
150
|
-
if (!
|
|
184
|
+
if (!VALID_OUTPUT_FORMATS.includes(out.format)) {
|
|
151
185
|
throw new Error('output.format must be one of "esm", "cjs" or "both"');
|
|
152
186
|
}
|
|
153
187
|
if (typeof out.declaration !== 'boolean' || typeof out.sourceMap !== 'boolean') {
|
|
154
188
|
throw new Error('output.declaration and output.sourceMap must be boolean');
|
|
155
189
|
}
|
|
190
|
+
}
|
|
156
191
|
|
|
157
|
-
|
|
192
|
+
function validateRuntime(runtime: RuntimeConfig): void {
|
|
158
193
|
if (runtime.node) {
|
|
159
194
|
if (runtime.node.pythonPath !== undefined && typeof runtime.node.pythonPath !== 'string') {
|
|
160
195
|
throw new Error('runtime.node.pythonPath must be a string');
|
|
161
196
|
}
|
|
162
|
-
if (runtime.node.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
if (runtime.node.virtualEnv !== undefined && typeof runtime.node.virtualEnv !== 'string') {
|
|
198
|
+
throw new Error('runtime.node.virtualEnv must be a string');
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
runtime.node.timeout !== undefined &&
|
|
202
|
+
(typeof runtime.node.timeout !== 'number' || runtime.node.timeout < 0)
|
|
203
|
+
) {
|
|
204
|
+
throw new Error('runtime.node.timeout must be a non-negative number');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (runtime.http) {
|
|
208
|
+
if (typeof runtime.http.baseURL !== 'string' || runtime.http.baseURL.length === 0) {
|
|
209
|
+
throw new Error('runtime.http.baseURL must be a non-empty string');
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
runtime.http.timeout !== undefined &&
|
|
213
|
+
(typeof runtime.http.timeout !== 'number' || runtime.http.timeout < 0)
|
|
214
|
+
) {
|
|
215
|
+
throw new Error('runtime.http.timeout must be a non-negative number');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (runtime.pyodide) {
|
|
219
|
+
if (runtime.pyodide.indexURL !== undefined && typeof runtime.pyodide.indexURL !== 'string') {
|
|
220
|
+
throw new Error('runtime.pyodide.indexURL must be a string');
|
|
221
|
+
}
|
|
222
|
+
if (runtime.pyodide.packages !== undefined && !isStringArray(runtime.pyodide.packages)) {
|
|
223
|
+
throw new Error('runtime.pyodide.packages must be an array of strings');
|
|
166
224
|
}
|
|
167
225
|
}
|
|
226
|
+
}
|
|
168
227
|
|
|
169
|
-
|
|
228
|
+
function validatePerformance(perf: PerformanceConfig): void {
|
|
170
229
|
if (typeof perf.caching !== 'boolean') {
|
|
171
230
|
throw new Error('performance.caching must be a boolean');
|
|
172
231
|
}
|
|
173
232
|
if (typeof perf.batching !== 'boolean') {
|
|
174
233
|
throw new Error('performance.batching must be a boolean');
|
|
175
234
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new Error(`performance.compression must be one of ${validCompression.join(', ')}`);
|
|
235
|
+
if (!VALID_COMPRESSION.includes(perf.compression)) {
|
|
236
|
+
throw new Error(`performance.compression must be one of ${VALID_COMPRESSION.join(', ')}`);
|
|
179
237
|
}
|
|
238
|
+
}
|
|
180
239
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
throw new Error('types.presets must be an array of strings');
|
|
195
|
-
}
|
|
196
|
-
for (const preset of presets) {
|
|
197
|
-
if (!validPresets.has(preset)) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`types.presets contains invalid value "${preset}". Allowed: ${Array.from(validPresets).join(', ')}`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
240
|
+
function validateTypes(types: NonNullable<ResolvedTywrapConfig['types']>): void {
|
|
241
|
+
const presets = types.presets;
|
|
242
|
+
if (presets === undefined) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!isStringArray(presets)) {
|
|
246
|
+
throw new Error('types.presets must be an array of strings');
|
|
247
|
+
}
|
|
248
|
+
for (const preset of presets) {
|
|
249
|
+
if (!VALID_TYPE_PRESETS.has(preset)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`types.presets contains invalid value "${preset}". Allowed: ${Array.from(VALID_TYPE_PRESETS).join(', ')}`
|
|
252
|
+
);
|
|
203
253
|
}
|
|
204
254
|
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate configuration values and throw user-friendly errors when invalid.
|
|
259
|
+
*/
|
|
260
|
+
function validateConfig(config: ResolvedTywrapConfig): void {
|
|
261
|
+
detectLegacyFields(config);
|
|
262
|
+
for (const key of Object.keys(config)) {
|
|
263
|
+
if (!ALLOWED_TOP_LEVEL.has(key)) {
|
|
264
|
+
throw new Error(`Unknown configuration option \"${key}\"`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (config.pythonModules !== undefined) {
|
|
269
|
+
if (!isPlainObject(config.pythonModules)) {
|
|
270
|
+
throw new Error('pythonModules must be an object');
|
|
271
|
+
}
|
|
272
|
+
validatePythonModules(config.pythonModules);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (config.pythonImportPath !== undefined && !isStringArray(config.pythonImportPath)) {
|
|
276
|
+
throw new Error('pythonImportPath must be an array of strings');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
validateOutput(config.output);
|
|
280
|
+
validateRuntime(config.runtime);
|
|
281
|
+
validatePerformance(config.performance);
|
|
282
|
+
|
|
283
|
+
if (config.types) {
|
|
284
|
+
validateTypes(config.types);
|
|
285
|
+
}
|
|
205
286
|
|
|
206
287
|
if (typeof config.debug !== 'boolean') {
|
|
207
288
|
throw new Error('debug must be a boolean');
|
|
@@ -264,128 +345,153 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
264
345
|
throw new Error(`Configuration file not found: ${resolved}`);
|
|
265
346
|
}
|
|
266
347
|
|
|
267
|
-
|
|
268
|
-
|
|
348
|
+
// Dispatch by file extension. Each loader receives the resolved absolute path
|
|
349
|
+
// and the lowercased extension and returns the parsed config object.
|
|
350
|
+
const loader = CONFIG_LOADERS[ext];
|
|
351
|
+
if (!loader) {
|
|
352
|
+
throw new Error(`Unsupported configuration file extension: ${ext}`);
|
|
353
|
+
}
|
|
354
|
+
return loader(resolved, ext);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function loadJsonConfig(resolved: string): Promise<Partial<TywrapOptions>> {
|
|
358
|
+
const txt = await safeReadFileAsync(resolved);
|
|
359
|
+
try {
|
|
360
|
+
const parsed = JSON.parse(txt) as unknown;
|
|
361
|
+
return ensureConfigObject(parsed, resolved);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
throw new Error(`Failed to parse JSON config ${resolved}: ${(err as Error).message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function loadCjsConfig(resolved: string): Promise<Partial<TywrapOptions>> {
|
|
368
|
+
const require = createRequire(import.meta.url);
|
|
369
|
+
// eslint-disable-next-line security/detect-non-literal-require -- config path is user-controlled and resolved
|
|
370
|
+
const mod = require(resolved) as Record<string, unknown>;
|
|
371
|
+
const loaded = mod.default ?? mod;
|
|
372
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function loadEsmConfig(resolved: string): Promise<Partial<TywrapOptions>> {
|
|
376
|
+
const mod = (await import(pathToFileURL(resolved).href)) as Record<string, unknown>;
|
|
377
|
+
const loaded = mod.default ?? mod;
|
|
378
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function loadTypeScriptConfig(
|
|
382
|
+
resolved: string,
|
|
383
|
+
ext: string
|
|
384
|
+
): Promise<Partial<TywrapOptions>> {
|
|
385
|
+
const ts = await import('typescript');
|
|
386
|
+
const source = await safeReadFileAsync(resolved);
|
|
387
|
+
// Why: many configs want to `import { defineConfig } from 'tywrap'`. The tywrap package is ESM,
|
|
388
|
+
// so evaluating a transpiled CommonJS config would try `require('tywrap')` and fail. Treat
|
|
389
|
+
// `.ts`/`.mts` configs as ESM, and only treat `.cts` as CommonJS to match Node conventions.
|
|
390
|
+
const emitCommonJs = ext === '.cts';
|
|
391
|
+
const output = ts.transpileModule(source, {
|
|
392
|
+
compilerOptions: {
|
|
393
|
+
module: emitCommonJs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ES2020,
|
|
394
|
+
target: ts.ScriptTarget.ES2020,
|
|
395
|
+
esModuleInterop: true,
|
|
396
|
+
},
|
|
397
|
+
fileName: resolved,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
let transpiledOutput = output.outputText;
|
|
401
|
+
if (!emitCommonJs) {
|
|
269
402
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
403
|
+
// Preserve support for `import { defineConfig } from 'tywrap'` when ESM config
|
|
404
|
+
// is evaluated from an OS temp directory outside the package scope.
|
|
405
|
+
const tywrapEntryHref = await import.meta.resolve('tywrap');
|
|
406
|
+
transpiledOutput = transpiledOutput
|
|
407
|
+
.replaceAll("'tywrap'", `'${tywrapEntryHref}'`)
|
|
408
|
+
.replaceAll('\"tywrap\"', `\"${tywrapEntryHref}\"`);
|
|
409
|
+
} catch {
|
|
410
|
+
// Best-effort: leave source as-is when tywrap cannot be resolved.
|
|
274
411
|
}
|
|
275
412
|
}
|
|
276
413
|
|
|
277
|
-
if (
|
|
414
|
+
if (emitCommonJs) {
|
|
415
|
+
// Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
|
|
416
|
+
// Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
|
|
417
|
+
// temp file. These are private Node APIs, so we keep this tooling path scoped and rely on
|
|
418
|
+
// supported Node versions (see package.json engines).
|
|
278
419
|
const require = createRequire(import.meta.url);
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
420
|
+
const nodeModule = require('module') as typeof import('module');
|
|
421
|
+
const moduleCtor = nodeModule.Module as unknown as typeof import('module').Module & {
|
|
422
|
+
_nodeModulePaths?: (path: string) => string[];
|
|
423
|
+
};
|
|
424
|
+
if (typeof moduleCtor !== 'function') {
|
|
425
|
+
throw new Error(
|
|
426
|
+
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node Module constructor'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const nodeModulePaths = moduleCtor._nodeModulePaths;
|
|
430
|
+
if (typeof nodeModulePaths !== 'function') {
|
|
431
|
+
throw new Error(
|
|
432
|
+
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._nodeModulePaths'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const mod = new moduleCtor(resolved) as import('module').Module & {
|
|
436
|
+
_compile?: (code: string, filename: string) => void;
|
|
437
|
+
};
|
|
438
|
+
const compile = mod._compile;
|
|
439
|
+
if (typeof compile !== 'function') {
|
|
440
|
+
throw new Error(
|
|
441
|
+
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._compile'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
mod.filename = resolved;
|
|
445
|
+
mod.paths = nodeModulePaths(dirname(resolved));
|
|
446
|
+
compile.call(mod, output.outputText, resolved);
|
|
447
|
+
const loaded = (mod.exports as Record<string, unknown>).default ?? mod.exports;
|
|
288
448
|
return ensureConfigObject(loaded ?? {}, resolved);
|
|
289
449
|
}
|
|
290
450
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
module: emitCommonJs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ES2020,
|
|
301
|
-
target: ts.ScriptTarget.ES2020,
|
|
302
|
-
esModuleInterop: true,
|
|
303
|
-
},
|
|
304
|
-
fileName: resolved,
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
let transpiledOutput = output.outputText;
|
|
308
|
-
if (!emitCommonJs) {
|
|
451
|
+
// Why: Node can't import ESM from a string without a custom loader. We write transpiled
|
|
452
|
+
// output to a temporary `.mjs` file under the OS temp directory (not next to user config),
|
|
453
|
+
// then clean up both file and temporary directory after loading.
|
|
454
|
+
const tempDir = await mkdtemp(resolve(tmpdir(), 'tywrap-config-'));
|
|
455
|
+
const tmpPath = resolve(tempDir, `.tywrap.config.${randomUUID()}.mjs`);
|
|
456
|
+
try {
|
|
457
|
+
const localNodeModules = resolve(process.cwd(), 'node_modules');
|
|
458
|
+
if (safeExists(localNodeModules)) {
|
|
459
|
+
const tempNodeModules = resolve(tempDir, 'node_modules');
|
|
309
460
|
try {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
const tywrapEntryHref = await import.meta.resolve('tywrap');
|
|
313
|
-
transpiledOutput = transpiledOutput
|
|
314
|
-
.replaceAll("'tywrap'", `'${tywrapEntryHref}'`)
|
|
315
|
-
.replaceAll('\"tywrap\"', `\"${tywrapEntryHref}\"`);
|
|
461
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
462
|
+
await symlink(localNodeModules, tempNodeModules, 'dir');
|
|
316
463
|
} catch {
|
|
317
|
-
// Best-effort
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (emitCommonJs) {
|
|
322
|
-
// Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
|
|
323
|
-
// Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
|
|
324
|
-
// temp file. These are private Node APIs, so we keep this tooling path scoped and rely on
|
|
325
|
-
// supported Node versions (see package.json engines).
|
|
326
|
-
const require = createRequire(import.meta.url);
|
|
327
|
-
const nodeModule = require('module') as typeof import('module');
|
|
328
|
-
const moduleCtor = nodeModule.Module as unknown as typeof import('module').Module & {
|
|
329
|
-
_nodeModulePaths?: (path: string) => string[];
|
|
330
|
-
};
|
|
331
|
-
if (typeof moduleCtor !== 'function') {
|
|
332
|
-
throw new Error(
|
|
333
|
-
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node Module constructor'
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
const nodeModulePaths = moduleCtor._nodeModulePaths;
|
|
337
|
-
if (typeof nodeModulePaths !== 'function') {
|
|
338
|
-
throw new Error(
|
|
339
|
-
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._nodeModulePaths'
|
|
340
|
-
);
|
|
464
|
+
// Best-effort only; regular resolution may still succeed without a symlink.
|
|
341
465
|
}
|
|
342
|
-
const mod = new moduleCtor(resolved) as import('module').Module & {
|
|
343
|
-
_compile?: (code: string, filename: string) => void;
|
|
344
|
-
};
|
|
345
|
-
const compile = mod._compile;
|
|
346
|
-
if (typeof compile !== 'function') {
|
|
347
|
-
throw new Error(
|
|
348
|
-
'[tywrap] Unable to evaluate .cts config in-memory (emitCommonJs=true): missing Node private API Module._compile'
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
mod.filename = resolved;
|
|
352
|
-
mod.paths = nodeModulePaths(dirname(resolved));
|
|
353
|
-
compile.call(mod, output.outputText, resolved);
|
|
354
|
-
const loaded = (mod.exports as Record<string, unknown>).default ?? mod.exports;
|
|
355
|
-
return ensureConfigObject(loaded ?? {}, resolved);
|
|
356
466
|
}
|
|
357
467
|
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const tempNodeModules = resolve(tempDir, 'node_modules');
|
|
367
|
-
try {
|
|
368
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
369
|
-
await symlink(localNodeModules, tempNodeModules, 'dir');
|
|
370
|
-
} catch {
|
|
371
|
-
// Best-effort only; regular resolution may still succeed without a symlink.
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
376
|
-
await writeFile(tmpPath, transpiledOutput, 'utf-8');
|
|
377
|
-
const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
|
|
378
|
-
const loaded = mod.default ?? mod;
|
|
379
|
-
return ensureConfigObject(loaded ?? {}, resolved);
|
|
380
|
-
} finally {
|
|
381
|
-
await rm(tmpPath, { force: true });
|
|
382
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
383
|
-
}
|
|
468
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
469
|
+
await writeFile(tmpPath, transpiledOutput, 'utf-8');
|
|
470
|
+
const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
|
|
471
|
+
const loaded = mod.default ?? mod;
|
|
472
|
+
return ensureConfigObject(loaded ?? {}, resolved);
|
|
473
|
+
} finally {
|
|
474
|
+
await rm(tmpPath, { force: true });
|
|
475
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
384
476
|
}
|
|
385
|
-
|
|
386
|
-
throw new Error(`Unsupported configuration file extension: ${ext}`);
|
|
387
477
|
}
|
|
388
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Loaders for each supported configuration file extension. The map keys are the
|
|
481
|
+
* lowercased extensions returned by `extname`; the matching loader parses the file.
|
|
482
|
+
*/
|
|
483
|
+
const CONFIG_LOADERS: Readonly<
|
|
484
|
+
Record<string, (resolved: string, ext: string) => Promise<Partial<TywrapOptions>>>
|
|
485
|
+
> = {
|
|
486
|
+
'.json': loadJsonConfig,
|
|
487
|
+
'.cjs': loadCjsConfig,
|
|
488
|
+
'.js': loadEsmConfig,
|
|
489
|
+
'.mjs': loadEsmConfig,
|
|
490
|
+
'.ts': loadTypeScriptConfig,
|
|
491
|
+
'.mts': loadTypeScriptConfig,
|
|
492
|
+
'.cts': loadTypeScriptConfig,
|
|
493
|
+
};
|
|
494
|
+
|
|
389
495
|
async function safeReadFileAsync(path: string): Promise<string> {
|
|
390
496
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- config path is user-controlled and resolved
|
|
391
497
|
return readFile(path, 'utf-8');
|