@nudojs/cli 0.2.0 → 0.2.1
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/CHANGELOG.md +12 -0
- package/README.md +34 -0
- package/dist/chunk-3HCPXPEW.js +5286 -0
- package/dist/evaluator-api.d.ts +94 -2
- package/dist/evaluator-api.js +187 -3
- package/dist/index.js +595 -130
- package/examples/real-world.js +66 -0
- package/examples/validate-real-world.ts +160 -0
- package/package.json +17 -2
- package/src/__tests__/builtin-advanced.test.ts +292 -0
- package/src/__tests__/builtin-api.test.ts +308 -0
- package/src/__tests__/builtin-functions.test.ts +313 -0
- package/src/__tests__/builtin-map.test.ts +129 -0
- package/src/__tests__/builtin-promise.test.ts +150 -0
- package/src/__tests__/builtin-regexp.test.ts +133 -0
- package/src/__tests__/builtin-set.test.ts +163 -0
- package/src/__tests__/builtin-url.test.ts +256 -0
- package/src/__tests__/builtin-web.test.ts +340 -0
- package/src/__tests__/combination-scenarios.test.ts +384 -0
- package/src/__tests__/edge-cases.test.ts +749 -0
- package/src/__tests__/evaluator-loops.test.ts +120 -0
- package/src/__tests__/evaluator-nullish-coalescing.test.ts +86 -0
- package/src/__tests__/evaluator-optional-chaining.test.ts +37 -0
- package/src/__tests__/evaluator-phase3.test.ts +353 -2
- package/src/__tests__/evaluator.test.ts +702 -2
- package/src/__tests__/mock-arrow-function.test.ts +169 -0
- package/src/__tests__/mock-comparison.test.ts +221 -0
- package/src/__tests__/mock-comprehensive.test.ts +250 -0
- package/src/__tests__/mock-final-integration.test.ts +263 -0
- package/src/__tests__/mock-helpers.test.ts +174 -0
- package/src/__tests__/mock-realworld.test.ts +191 -0
- package/src/__tests__/mock-rest-debug.test.ts +110 -0
- package/src/__tests__/mock-rest-edge.test.ts +176 -0
- package/src/__tests__/mock-sinon-runtime.test.ts +168 -0
- package/src/__tests__/narrowing-discriminated.test.ts +35 -0
- package/src/__tests__/narrowing-in-operator.test.ts +56 -0
- package/src/__tests__/narrowing-switch.test.ts +56 -0
- package/src/__tests__/narrowing-truthiness.test.ts +52 -0
- package/src/__tests__/nudo-mock-syntax.test.ts +245 -0
- package/src/__tests__/realworld-patterns.test.ts +430 -0
- package/src/__tests__/syntax-sugar.test.ts +345 -0
- package/src/builtins/builtin-intl.ts +17 -0
- package/src/builtins/builtin-map.ts +174 -0
- package/src/builtins/builtin-promise.ts +116 -0
- package/src/builtins/builtin-reflect.ts +30 -0
- package/src/builtins/builtin-regexp.ts +34 -0
- package/src/builtins/builtin-set.ts +131 -0
- package/src/builtins/builtin-symbol.ts +16 -0
- package/src/builtins/builtin-url.ts +58 -0
- package/src/builtins/builtin-weak.ts +22 -0
- package/src/builtins/builtin-web.ts +111 -0
- package/src/builtins/index.ts +61 -0
- package/src/config.ts +32 -0
- package/src/env-loader.ts +174 -0
- package/src/evaluator-api.ts +20 -0
- package/src/evaluator.ts +4018 -342
- package/src/index.ts +776 -137
- package/src/narrowing.ts +230 -15
- package/src/resolve-npm.ts +63 -0
- package/dist/chunk-LGWVGEMB.js +0 -1817
package/dist/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
__commonJS,
|
|
3
3
|
__require,
|
|
4
4
|
__toESM,
|
|
5
|
-
evaluateFunction,
|
|
6
5
|
evaluateFunctionFull,
|
|
7
6
|
evaluateProgram,
|
|
8
7
|
resetMemo,
|
|
8
|
+
resolveNpmNudo,
|
|
9
9
|
setCurrentFileDir,
|
|
10
10
|
setModuleResolver
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-3HCPXPEW.js";
|
|
12
12
|
|
|
13
13
|
// ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
|
|
14
14
|
var require_error = __commonJS({
|
|
@@ -3312,7 +3312,7 @@ var require_commander = __commonJS({
|
|
|
3312
3312
|
|
|
3313
3313
|
// src/index.ts
|
|
3314
3314
|
import { readFileSync, existsSync, watch, readdirSync, statSync, writeFileSync } from "fs";
|
|
3315
|
-
import { resolve, dirname, relative, join } from "path";
|
|
3315
|
+
import { resolve, dirname, relative, join, basename } from "path";
|
|
3316
3316
|
|
|
3317
3317
|
// ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
|
|
3318
3318
|
var import_index = __toESM(require_commander(), 1);
|
|
@@ -3333,20 +3333,42 @@ var {
|
|
|
3333
3333
|
|
|
3334
3334
|
// src/index.ts
|
|
3335
3335
|
import {
|
|
3336
|
+
T,
|
|
3336
3337
|
typeValueToString,
|
|
3337
|
-
simplifyUnion,
|
|
3338
3338
|
createEnvironment,
|
|
3339
|
-
|
|
3340
|
-
isTemplate,
|
|
3341
|
-
getTemplateParts
|
|
3339
|
+
mockHelperToTypeValue
|
|
3342
3340
|
} from "@nudojs/core";
|
|
3343
3341
|
import { parse, extractDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
3342
|
+
import {
|
|
3343
|
+
typeValueToZodSchema,
|
|
3344
|
+
generateGuardFunction,
|
|
3345
|
+
generateFunctionDtsLines,
|
|
3346
|
+
analyzeFileAsync,
|
|
3347
|
+
buildModuleGraph,
|
|
3348
|
+
computeDirtySet,
|
|
3349
|
+
topoSortDirty,
|
|
3350
|
+
collectCallRecords,
|
|
3351
|
+
stripGeneratedCaseDirectives,
|
|
3352
|
+
insertGeneratedCaseDirectives,
|
|
3353
|
+
unifiedDiff
|
|
3354
|
+
} from "@nudojs/service";
|
|
3355
|
+
import { harvestDts, emitEnvModule } from "@nudojs/harvester";
|
|
3344
3356
|
var program2 = new Command();
|
|
3345
3357
|
program2.name("nudo").description("Nudo type inference engine").version("0.0.1");
|
|
3346
3358
|
function applyMocks(directives, env, filePath) {
|
|
3347
3359
|
for (const d of directives) {
|
|
3348
3360
|
if (d.kind !== "mock") continue;
|
|
3349
|
-
if (d.
|
|
3361
|
+
if (d.arrowFn) {
|
|
3362
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
3363
|
+
fnType._paramPatterns = d.arrowFn.paramPatterns;
|
|
3364
|
+
env.bind(d.name, fnType);
|
|
3365
|
+
} else if (d.nudoMock) {
|
|
3366
|
+
const typeVal = mockHelperToTypeValue(d.nudoMock, env);
|
|
3367
|
+
env.bind(d.name, typeVal);
|
|
3368
|
+
} else if (d.sinonExpr) {
|
|
3369
|
+
const sinonType = createSinonTypeValue(d.sinonExpr);
|
|
3370
|
+
env.bind(d.name, sinonType);
|
|
3371
|
+
} else if (d.expression) {
|
|
3350
3372
|
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
3351
3373
|
} else if (d.fromPath) {
|
|
3352
3374
|
const mockPath = resolve(dirname(filePath), d.fromPath);
|
|
@@ -3359,156 +3381,152 @@ function applyMocks(directives, env, filePath) {
|
|
|
3359
3381
|
}
|
|
3360
3382
|
}
|
|
3361
3383
|
}
|
|
3384
|
+
function createSinonTypeValue(sinonExpr) {
|
|
3385
|
+
const body = { type: "BlockStatement", body: [] };
|
|
3386
|
+
const fn = T.fn(["...args"], body, createEnvironment());
|
|
3387
|
+
if (sinonExpr.returnValue) {
|
|
3388
|
+
fn._directReturn = sinonExpr.returnValue;
|
|
3389
|
+
} else if (sinonExpr.resolvedValue) {
|
|
3390
|
+
fn._directReturn = T.promise(sinonExpr.resolvedValue);
|
|
3391
|
+
} else if (sinonExpr.rejectedValue) {
|
|
3392
|
+
fn._directReturn = T.never;
|
|
3393
|
+
} else {
|
|
3394
|
+
fn._directReturn = T.unknown;
|
|
3395
|
+
}
|
|
3396
|
+
return fn;
|
|
3397
|
+
}
|
|
3362
3398
|
function resolveModule(source, fromDir) {
|
|
3363
3399
|
const extensions = [".js", ".ts", ".mjs"];
|
|
3400
|
+
const nudoPath = resolveNpmNudo(source, fromDir);
|
|
3401
|
+
if (nudoPath) {
|
|
3402
|
+
const src = readFileSync(nudoPath, "utf-8");
|
|
3403
|
+
return { ast: parse(src), filePath: nudoPath };
|
|
3404
|
+
}
|
|
3364
3405
|
const basePath = resolve(fromDir, source);
|
|
3365
3406
|
for (const ext of ["", ...extensions]) {
|
|
3366
3407
|
const candidate = basePath + ext;
|
|
3367
3408
|
if (existsSync(candidate)) {
|
|
3409
|
+
if (candidate.endsWith(".json")) {
|
|
3410
|
+
try {
|
|
3411
|
+
return { ast: parse("module.exports = undefined;"), filePath: candidate, json: JSON.parse(readFileSync(candidate, "utf-8")) };
|
|
3412
|
+
} catch {
|
|
3413
|
+
return null;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3368
3416
|
const src = readFileSync(candidate, "utf-8");
|
|
3369
3417
|
return { ast: parse(src), filePath: candidate };
|
|
3370
3418
|
}
|
|
3371
3419
|
}
|
|
3372
3420
|
return null;
|
|
3373
3421
|
}
|
|
3374
|
-
function
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
const inner = parts.map((p) => p.kind === "literal" && typeof p.value === "string" ? p.value : `\${${typeValueToTSType(p)}}`).join("");
|
|
3389
|
-
return `\`${inner}\``;
|
|
3390
|
-
}
|
|
3391
|
-
return typeValueToTSType(tv.base);
|
|
3392
|
-
}
|
|
3393
|
-
case "object": {
|
|
3394
|
-
const entries = Object.entries(tv.properties);
|
|
3395
|
-
if (entries.length === 0) return "{}";
|
|
3396
|
-
const inner = entries.map(([k, v]) => `${k}: ${typeValueToTSType(v)}`).join("; ");
|
|
3397
|
-
return `{ ${inner} }`;
|
|
3422
|
+
async function reemitUpdate(filePath, source, callsites) {
|
|
3423
|
+
const stripped = stripGeneratedCaseDirectives(source);
|
|
3424
|
+
const result = await analyzeFileAsync(filePath, stripped.source, void 0, callsites);
|
|
3425
|
+
const emitOut = insertGeneratedCaseDirectives(stripped.source, result);
|
|
3426
|
+
return { result, emitOut, removed: stripped.removed };
|
|
3427
|
+
}
|
|
3428
|
+
function collectExternalRecords(sites) {
|
|
3429
|
+
const records = [];
|
|
3430
|
+
for (const site of sites) {
|
|
3431
|
+
const sitePath = resolve(site);
|
|
3432
|
+
if (!existsSync(sitePath)) {
|
|
3433
|
+
console.error(`Callsite file not found: ${sitePath}`);
|
|
3434
|
+
process.exitCode = 1;
|
|
3435
|
+
continue;
|
|
3398
3436
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3437
|
+
const siteFiles = statSync(sitePath).isDirectory() ? collectNudoFiles(sitePath) : [sitePath];
|
|
3438
|
+
for (const sf of siteFiles) {
|
|
3439
|
+
records.push(...collectCallRecords(sf, readFileSync(sf, "utf-8")));
|
|
3402
3440
|
}
|
|
3403
|
-
case "tuple":
|
|
3404
|
-
return `[${tv.elements.map(typeValueToTSType).join(", ")}]`;
|
|
3405
|
-
case "function":
|
|
3406
|
-
return `(${tv.params.map((p) => `${p}: unknown`).join(", ")}) => unknown`;
|
|
3407
|
-
case "promise":
|
|
3408
|
-
return `Promise<${typeValueToTSType(tv.value)}>`;
|
|
3409
|
-
case "instance":
|
|
3410
|
-
return tv.className;
|
|
3411
|
-
case "union":
|
|
3412
|
-
return tv.members.map(typeValueToTSType).join(" | ");
|
|
3413
|
-
case "never":
|
|
3414
|
-
return "never";
|
|
3415
|
-
case "unknown":
|
|
3416
|
-
return "unknown";
|
|
3417
3441
|
}
|
|
3442
|
+
return records.length > 0 ? records : void 0;
|
|
3418
3443
|
}
|
|
3419
|
-
function runInfer(file, options = {}) {
|
|
3444
|
+
async function runInfer(file, options = {}) {
|
|
3420
3445
|
const filePath = resolve(file);
|
|
3421
3446
|
const source = readFileSync(filePath, "utf-8");
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
if (
|
|
3447
|
+
let result = await analyzeFileAsync(filePath, source, void 0, options.callsites);
|
|
3448
|
+
let emitOut;
|
|
3449
|
+
if (options.emit) {
|
|
3450
|
+
if (options.emit.mode === "update") {
|
|
3451
|
+
const re = await reemitUpdate(filePath, source, options.callsites);
|
|
3452
|
+
result = re.result;
|
|
3453
|
+
emitOut = re.emitOut;
|
|
3454
|
+
} else {
|
|
3455
|
+
emitOut = insertGeneratedCaseDirectives(source, result);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
if (result.functions.length === 0 && !result.externalFunctions?.length) {
|
|
3425
3459
|
console.log("No functions with @nudo:case directives found.");
|
|
3426
3460
|
return;
|
|
3427
3461
|
}
|
|
3428
|
-
resetMemo();
|
|
3429
|
-
setModuleResolver(resolveModule);
|
|
3430
|
-
setCurrentFileDir(dirname(filePath));
|
|
3431
|
-
const globalEnv = createEnvironment();
|
|
3432
|
-
evaluateProgram(ast, globalEnv);
|
|
3433
3462
|
const dtsLines = [];
|
|
3434
|
-
for (const fn of functions) {
|
|
3435
|
-
const loc = fn.
|
|
3436
|
-
const locStr =
|
|
3463
|
+
for (const fn of result.functions) {
|
|
3464
|
+
const loc = fn.loc;
|
|
3465
|
+
const locStr = `${relative(process.cwd(), filePath)}:${loc.start.line}:${loc.start.column}`;
|
|
3437
3466
|
const header = options.showLoc ? `=== ${fn.name} (${locStr}) ===` : `=== ${fn.name} ===`;
|
|
3438
3467
|
console.log(`${header}
|
|
3439
3468
|
`);
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
console.log(`Skipped (declared): ${typeValueToString(skipDirective.returns)}`);
|
|
3447
|
-
if (options.dts) {
|
|
3448
|
-
dtsLines.push(`export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(skipDirective.returns)};`);
|
|
3449
|
-
}
|
|
3469
|
+
if (options.dts) {
|
|
3470
|
+
dtsLines.push(...generateFunctionDtsLines(fn));
|
|
3471
|
+
}
|
|
3472
|
+
if (fn.skipped) {
|
|
3473
|
+
if (fn.combined) {
|
|
3474
|
+
console.log(`Skipped (declared): ${typeValueToString(fn.combined)}`);
|
|
3450
3475
|
} else {
|
|
3451
3476
|
console.log("Skipped (no return type declared)");
|
|
3452
3477
|
}
|
|
3453
3478
|
console.log();
|
|
3454
3479
|
continue;
|
|
3455
3480
|
}
|
|
3456
|
-
|
|
3457
|
-
const
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
3463
|
-
const caseResults = [];
|
|
3464
|
-
for (const directive of caseDirectives) {
|
|
3465
|
-
const fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
3466
|
-
const argsStr = directive.args.map(typeValueToString).join(", ");
|
|
3467
|
-
const resultStr = typeValueToString(fullResult.value);
|
|
3468
|
-
const throwsStr = fullResult.throws.kind !== "never" ? typeValueToString(fullResult.throws) : void 0;
|
|
3469
|
-
caseResults.push({
|
|
3470
|
-
name: directive.name,
|
|
3471
|
-
args: directive.args,
|
|
3472
|
-
argsStr,
|
|
3473
|
-
result: resultStr,
|
|
3474
|
-
resultTV: fullResult.value,
|
|
3475
|
-
throws: throwsStr
|
|
3476
|
-
});
|
|
3477
|
-
let line = `Case "${directive.name}": (${argsStr}) => ${resultStr}`;
|
|
3478
|
-
if (throwsStr) line += ` throws ${throwsStr}`;
|
|
3481
|
+
for (const c of fn.cases) {
|
|
3482
|
+
const argsStr = c.args.map(typeValueToString).join(", ");
|
|
3483
|
+
let line = `Case "${c.name}": (${argsStr}) => ${typeValueToString(c.result)}`;
|
|
3484
|
+
if (c.throws.kind !== "never") line += ` throws ${typeValueToString(c.throws)}`;
|
|
3479
3485
|
console.log(line);
|
|
3480
3486
|
}
|
|
3481
|
-
if (
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
const combined = simplifyUnion(allResults.map((r) => r.value));
|
|
3487
|
+
if (fn.entryOnly) {
|
|
3488
|
+
console.log("# no call sites found; parameters default to unknown");
|
|
3489
|
+
}
|
|
3490
|
+
if (fn.cases.length > 1 && fn.combined) {
|
|
3486
3491
|
console.log(`
|
|
3487
|
-
Combined: ${typeValueToString(combined)}`);
|
|
3492
|
+
Combined: ${typeValueToString(fn.combined)}`);
|
|
3488
3493
|
}
|
|
3489
|
-
if (
|
|
3490
|
-
for (const
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
if (!matches) {
|
|
3494
|
-
console.log(`
|
|
3495
|
-
\u26A0 @nudo:returns assertion failed for case "${directive.name}": expected ${typeValueToString(returnsDirective.expected)}, got ${typeValueToString(result)}`);
|
|
3496
|
-
}
|
|
3494
|
+
if (fn.assertionErrors && fn.assertionErrors.length > 0) {
|
|
3495
|
+
for (const err of fn.assertionErrors) {
|
|
3496
|
+
console.log(`
|
|
3497
|
+
\u26A0 ${err}`);
|
|
3497
3498
|
}
|
|
3498
3499
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3500
|
+
console.log();
|
|
3501
|
+
}
|
|
3502
|
+
if (result.externalFunctions && result.externalFunctions.length > 0) {
|
|
3503
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
3504
|
+
for (const fn of result.externalFunctions) {
|
|
3505
|
+
const mod = fn.fromModule ?? "";
|
|
3506
|
+
const list = byModule.get(mod);
|
|
3507
|
+
if (list) list.push(fn);
|
|
3508
|
+
else byModule.set(mod, [fn]);
|
|
3509
|
+
}
|
|
3510
|
+
for (const [mod, fns] of byModule) {
|
|
3511
|
+
const rel = relative(process.cwd(), mod) || mod;
|
|
3512
|
+
console.log(`--- ${rel} (imported) ---
|
|
3513
|
+
`);
|
|
3514
|
+
for (const fn of fns) {
|
|
3515
|
+
console.log(`=== ${fn.name} ===
|
|
3516
|
+
`);
|
|
3517
|
+
for (const c of fn.cases) {
|
|
3518
|
+
const argsStr = c.args.map(typeValueToString).join(", ");
|
|
3519
|
+
let line = `Case "${c.name}": (${argsStr}) => ${typeValueToString(c.result)}`;
|
|
3520
|
+
if (c.throws.kind !== "never") line += ` throws ${typeValueToString(c.throws)}`;
|
|
3521
|
+
console.log(line);
|
|
3522
|
+
}
|
|
3523
|
+
if (fn.cases.length > 1 && fn.combined) {
|
|
3524
|
+
console.log(`
|
|
3525
|
+
Combined: ${typeValueToString(fn.combined)}`);
|
|
3508
3526
|
}
|
|
3527
|
+
console.log();
|
|
3509
3528
|
}
|
|
3510
3529
|
}
|
|
3511
|
-
console.log();
|
|
3512
3530
|
}
|
|
3513
3531
|
if (options.dts && dtsLines.length > 0) {
|
|
3514
3532
|
const dtsPath = filePath.replace(/\.js$/, ".d.ts");
|
|
@@ -3516,10 +3534,246 @@ Combined: ${typeValueToString(combined)}`);
|
|
|
3516
3534
|
writeFileSync(dtsPath, dtsContent, "utf-8");
|
|
3517
3535
|
console.log(`Generated: ${relative(process.cwd(), dtsPath)}`);
|
|
3518
3536
|
}
|
|
3519
|
-
|
|
3537
|
+
if (result.diagnostics.length > 0) {
|
|
3538
|
+
console.log("Diagnostics:\n");
|
|
3539
|
+
for (const d of result.diagnostics) {
|
|
3540
|
+
const loc = `${relative(process.cwd(), filePath)}:${d.range.start.line}:${d.range.start.column}`;
|
|
3541
|
+
console.log(` [${d.severity}] ${loc} ${d.message}${d.code ? ` (${d.code})` : ""}`);
|
|
3542
|
+
if (d.origin) {
|
|
3543
|
+
console.log(` \u2192 value originates at ${d.origin.line}:${d.origin.column}`);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
console.log();
|
|
3547
|
+
}
|
|
3548
|
+
if (options.emit && emitOut) {
|
|
3549
|
+
const relPath = relative(process.cwd(), filePath) || filePath;
|
|
3550
|
+
const skippedLines = emitOut.skipped.map((s) => ` ${s.fn}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
3551
|
+
if (emitOut.source === source) {
|
|
3552
|
+
console.log("No changes.");
|
|
3553
|
+
for (const line of skippedLines) console.log(line);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
if (options.emit.dryRun) {
|
|
3557
|
+
console.log(`Would emit cases \u2192 ${relPath} (dry run)`);
|
|
3558
|
+
for (const w of emitOut.written) console.log(` ${w.fn}: ${w.cases.join(", ")}`);
|
|
3559
|
+
for (const line of skippedLines) console.log(line);
|
|
3560
|
+
console.log();
|
|
3561
|
+
process.stdout.write(unifiedDiff(source, emitOut.source, relPath));
|
|
3562
|
+
if (options.emit.exitOnDiff) process.exitCode = 1;
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
writeFileSync(filePath, emitOut.source, "utf-8");
|
|
3566
|
+
const directiveCount = emitOut.written.reduce((n, w) => n + w.cases.length, 0);
|
|
3567
|
+
console.log(
|
|
3568
|
+
`Emitted cases \u2192 ${relPath} (${directiveCount} directive(s) across ${emitOut.written.length} function(s))`
|
|
3569
|
+
);
|
|
3570
|
+
for (const w of emitOut.written) console.log(` ${w.fn}: ${w.cases.join(", ")}`);
|
|
3571
|
+
for (const line of skippedLines) console.log(line);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
async function runInferJson(file, externalRecords) {
|
|
3575
|
+
const filePath = resolve(file);
|
|
3576
|
+
const source = readFileSync(filePath, "utf-8");
|
|
3577
|
+
const result = await analyzeFileAsync(filePath, source, void 0, externalRecords);
|
|
3578
|
+
const jsonOutput = {
|
|
3579
|
+
functions: result.functions.map((f) => ({
|
|
3580
|
+
name: f.name,
|
|
3581
|
+
loc: f.loc,
|
|
3582
|
+
cases: f.cases.map((c) => ({
|
|
3583
|
+
name: c.name,
|
|
3584
|
+
args: c.args.map(typeValueToString),
|
|
3585
|
+
result: typeValueToString(c.result),
|
|
3586
|
+
throws: c.throws.kind !== "never" ? typeValueToString(c.throws) : null,
|
|
3587
|
+
source: c.source ?? null
|
|
3588
|
+
})),
|
|
3589
|
+
entryOnly: f.entryOnly ?? false,
|
|
3590
|
+
assertionErrors: f.assertionErrors
|
|
3591
|
+
})),
|
|
3592
|
+
externalFunctions: result.externalFunctions?.map((f) => ({
|
|
3593
|
+
name: f.name,
|
|
3594
|
+
fromModule: f.fromModule,
|
|
3595
|
+
cases: f.cases.map((c) => ({
|
|
3596
|
+
name: c.name,
|
|
3597
|
+
args: c.args.map(typeValueToString),
|
|
3598
|
+
result: typeValueToString(c.result),
|
|
3599
|
+
throws: c.throws.kind !== "never" ? typeValueToString(c.throws) : null,
|
|
3600
|
+
source: c.source ?? null
|
|
3601
|
+
}))
|
|
3602
|
+
})),
|
|
3603
|
+
diagnostics: result.diagnostics.map((d) => ({
|
|
3604
|
+
range: d.range,
|
|
3605
|
+
severity: d.severity,
|
|
3606
|
+
message: d.message,
|
|
3607
|
+
code: d.code,
|
|
3608
|
+
suggestions: d.suggestions,
|
|
3609
|
+
tags: d.tags,
|
|
3610
|
+
origin: d.origin
|
|
3611
|
+
}))
|
|
3612
|
+
};
|
|
3613
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
3614
|
+
}
|
|
3615
|
+
program2.command("infer").description("Infer types from a JS file \u2014 functions with @nudo:case directives use them; all other functions are inferred from call sites (whole-program analysis)").argument("<file>", "Path to the JS file").option("--dts", "Generate .d.ts file").option("--loc", "Show source locations in output").option("--json", "Output as JSON").option("--callsites <paths...>", "Usage-site files (tests/apps) to harvest real call shapes from; their calls to this file's exports become synthesized cases").option("--emit-cases [mode]", "Write synthesized call-site cases back into the source as @nudo:case directives (call@ prefix); mode: update (default: add)").option("--dry-run", "With --emit-cases: print a unified diff instead of writing to disk").option("--exit-on-diff", "With --dry-run: exit with code 1 when the diff is non-empty").action(
|
|
3616
|
+
async (file, opts) => {
|
|
3617
|
+
let emit;
|
|
3618
|
+
if (opts.emitCases !== void 0) {
|
|
3619
|
+
let mode;
|
|
3620
|
+
if (opts.emitCases === true) mode = "add";
|
|
3621
|
+
else if (opts.emitCases === "update") mode = "update";
|
|
3622
|
+
else {
|
|
3623
|
+
console.error(`Invalid --emit-cases value: ${opts.emitCases} (expected: =update, or omit the value for add)`);
|
|
3624
|
+
process.exitCode = 1;
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
if (opts.json) {
|
|
3628
|
+
console.error("--emit-cases cannot be combined with --json");
|
|
3629
|
+
process.exitCode = 1;
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
emit = { mode, dryRun: opts.dryRun === true, exitOnDiff: opts.exitOnDiff === true };
|
|
3633
|
+
}
|
|
3634
|
+
if (opts.exitOnDiff && !opts.dryRun) {
|
|
3635
|
+
console.error("--exit-on-diff requires --dry-run");
|
|
3636
|
+
process.exitCode = 1;
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
let externalRecords;
|
|
3640
|
+
if (opts.callsites?.length) {
|
|
3641
|
+
externalRecords = collectExternalRecords(opts.callsites);
|
|
3642
|
+
}
|
|
3643
|
+
if (opts.json) {
|
|
3644
|
+
await runInferJson(file, externalRecords);
|
|
3645
|
+
} else {
|
|
3646
|
+
await runInfer(file, { dts: opts.dts, showLoc: opts.loc, callsites: externalRecords, emit });
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
);
|
|
3650
|
+
async function runCheck(file) {
|
|
3651
|
+
const filePath = resolve(file);
|
|
3652
|
+
const source = readFileSync(filePath, "utf-8");
|
|
3653
|
+
const result = await analyzeFileAsync(filePath, source);
|
|
3654
|
+
if (result.diagnostics.length === 0) {
|
|
3655
|
+
console.log("No issues found.");
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
for (const d of result.diagnostics) {
|
|
3659
|
+
const loc = `${relative(process.cwd(), filePath)}:${d.range.start.line}:${d.range.start.column}`;
|
|
3660
|
+
console.log(`[${d.severity}] ${loc} ${d.message}${d.code ? ` (${d.code})` : ""}`);
|
|
3661
|
+
if (d.origin) {
|
|
3662
|
+
console.log(` \u2192 value originates at ${d.origin.line}:${d.origin.column}`);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
if (result.diagnostics.some((d) => d.severity === "error")) {
|
|
3666
|
+
process.exitCode = 1;
|
|
3667
|
+
}
|
|
3520
3668
|
}
|
|
3521
|
-
program2.command("
|
|
3522
|
-
|
|
3669
|
+
program2.command("check").description("Check a JS file for type errors \u2014 exits with code 1 when errors are found").argument("<file>", "Path to the JS file").action(async (file) => {
|
|
3670
|
+
await runCheck(file);
|
|
3671
|
+
});
|
|
3672
|
+
var displayPath = (p) => {
|
|
3673
|
+
const rel = relative(process.cwd(), p);
|
|
3674
|
+
return rel === "" || rel.startsWith("..") ? p : rel;
|
|
3675
|
+
};
|
|
3676
|
+
async function doctorFile(filePath, records) {
|
|
3677
|
+
const report = { file: displayPath(filePath), functions: 0, entryOnly: 0, uncovered: [] };
|
|
3678
|
+
let source;
|
|
3679
|
+
try {
|
|
3680
|
+
source = readFileSync(filePath, "utf-8");
|
|
3681
|
+
} catch (err) {
|
|
3682
|
+
report.error = err.message;
|
|
3683
|
+
return report;
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
const result = await analyzeFileAsync(filePath, source, void 0, records);
|
|
3687
|
+
report.functions = result.functions.length;
|
|
3688
|
+
report.entryOnly = result.functions.filter((fn) => fn.entryOnly).length;
|
|
3689
|
+
report.uncovered = result.functions.filter((fn) => fn.cases.length === 0 && !fn.skipped && !fn.entryOnly).map((fn) => fn.name);
|
|
3690
|
+
if (records) {
|
|
3691
|
+
const { emitOut, removed } = await reemitUpdate(filePath, source, records);
|
|
3692
|
+
if (emitOut.source !== source) {
|
|
3693
|
+
report.drift = {
|
|
3694
|
+
added: emitOut.written.reduce((n, w) => n + w.cases.length, 0),
|
|
3695
|
+
removed: removed.length
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
} catch (err) {
|
|
3700
|
+
report.error = err.message;
|
|
3701
|
+
}
|
|
3702
|
+
return report;
|
|
3703
|
+
}
|
|
3704
|
+
async function runDoctor(paths, opts) {
|
|
3705
|
+
const targetPaths = paths.length > 0 ? paths : ["."];
|
|
3706
|
+
const externalRecords = opts.callsites?.length ? collectExternalRecords(opts.callsites) : void 0;
|
|
3707
|
+
const files = [];
|
|
3708
|
+
const reports = [];
|
|
3709
|
+
for (const p of targetPaths) {
|
|
3710
|
+
const abs = resolve(p);
|
|
3711
|
+
if (!existsSync(abs)) {
|
|
3712
|
+
reports.push({ file: displayPath(abs), functions: 0, entryOnly: 0, uncovered: [], error: `File not found: ${abs}` });
|
|
3713
|
+
continue;
|
|
3714
|
+
}
|
|
3715
|
+
files.push(...statSync(abs).isDirectory() ? collectNudoFiles(abs) : [abs]);
|
|
3716
|
+
}
|
|
3717
|
+
for (const filePath of files) {
|
|
3718
|
+
reports.push(await doctorFile(filePath, externalRecords));
|
|
3719
|
+
}
|
|
3720
|
+
const driftCount = reports.filter((r) => r.drift).length;
|
|
3721
|
+
const errorCount = reports.filter((r) => r.error).length;
|
|
3722
|
+
const uncoveredTotal = reports.reduce((n, r) => n + r.uncovered.length, 0);
|
|
3723
|
+
const failed = driftCount > 0 || errorCount > 0;
|
|
3724
|
+
if (opts.json) {
|
|
3725
|
+
console.log(
|
|
3726
|
+
JSON.stringify(
|
|
3727
|
+
{
|
|
3728
|
+
// ok 与退出码一致:drift/报错 → false;uncovered 仅为信息级不影响
|
|
3729
|
+
ok: !failed,
|
|
3730
|
+
files: reports.map((r) => ({
|
|
3731
|
+
file: r.file,
|
|
3732
|
+
functions: r.functions,
|
|
3733
|
+
entryOnly: r.entryOnly,
|
|
3734
|
+
uncovered: r.uncovered,
|
|
3735
|
+
...r.drift ? { drift: r.drift } : {},
|
|
3736
|
+
...r.error ? { error: r.error } : {}
|
|
3737
|
+
})),
|
|
3738
|
+
summary: { files: reports.length, drift: driftCount, errors: errorCount, uncovered: uncoveredTotal }
|
|
3739
|
+
},
|
|
3740
|
+
null,
|
|
3741
|
+
2
|
|
3742
|
+
)
|
|
3743
|
+
);
|
|
3744
|
+
} else {
|
|
3745
|
+
if (reports.length === 0) {
|
|
3746
|
+
console.log("No files to check.");
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
for (const r of reports) {
|
|
3750
|
+
console.log(`${r.file}`);
|
|
3751
|
+
if (r.error) {
|
|
3752
|
+
console.log(` \u2717 error: ${r.error}`);
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
const entryInfo = r.entryOnly > 0 ? `, ${r.entryOnly} entry-only` : "";
|
|
3756
|
+
console.log(` \xB7 ${r.functions} function(s)${entryInfo}`);
|
|
3757
|
+
if (r.uncovered.length > 0) {
|
|
3758
|
+
console.log(` \u26A0 uncovered (no cases): ${r.uncovered.join(", ")}`);
|
|
3759
|
+
}
|
|
3760
|
+
if (r.drift) {
|
|
3761
|
+
const refresh = `nudo infer ${r.file} --callsites ${(opts.callsites ?? []).join(" ")} --emit-cases=update`;
|
|
3762
|
+
console.log(
|
|
3763
|
+
` \u2717 drift: ${r.drift.added + r.drift.removed} directive(s) changed (+${r.drift.added} new, -${r.drift.removed} removed) \u2014 refresh with: ${refresh.trim()}`
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
console.log(
|
|
3768
|
+
`
|
|
3769
|
+
Summary: ${reports.length} file(s) \xB7 ${driftCount} drift \xB7 ${errorCount} error(s) \xB7 ${uncoveredTotal} uncovered function(s)`
|
|
3770
|
+
);
|
|
3771
|
+
console.log(failed ? "Result: FAIL (drift or errors found)" : "Result: OK (uncovered function(s) are informational only)");
|
|
3772
|
+
}
|
|
3773
|
+
if (failed) process.exitCode = 1;
|
|
3774
|
+
}
|
|
3775
|
+
program2.command("doctor").description("Health-check JS files: functions without cases, call-site solidification drift (--callsites), analysis errors \u2014 exits 1 on drift/errors").argument("[paths...]", "File(s) or directory(s) to check (default: current directory)").option("--callsites <paths...>", "Usage-site files (tests/apps): re-solidify per current call shapes and report drift when directives would change").option("--json", "Output as JSON").action(async (paths, opts) => {
|
|
3776
|
+
await runDoctor(paths, opts);
|
|
3523
3777
|
});
|
|
3524
3778
|
program2.command("watch").description("Watch file(s) for changes and re-run inference").argument("<path>", "File or directory to watch").option("--dts", "Generate .d.ts files on each change").action((watchPath, opts) => {
|
|
3525
3779
|
const resolved = resolve(watchPath);
|
|
@@ -3528,28 +3782,66 @@ program2.command("watch").description("Watch file(s) for changes and re-run infe
|
|
|
3528
3782
|
if (!isDir) return [resolved];
|
|
3529
3783
|
return collectNudoFiles(resolved);
|
|
3530
3784
|
};
|
|
3531
|
-
|
|
3785
|
+
let graph = buildModuleGraph(getFiles());
|
|
3786
|
+
const runAll = async () => {
|
|
3532
3787
|
console.clear();
|
|
3533
3788
|
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Analyzing...
|
|
3534
3789
|
`);
|
|
3535
3790
|
for (const f of getFiles()) {
|
|
3536
3791
|
try {
|
|
3537
|
-
runInfer(f, { dts: opts.dts, showLoc: true });
|
|
3792
|
+
await runInfer(f, { dts: opts.dts, showLoc: true });
|
|
3793
|
+
} catch (err) {
|
|
3794
|
+
console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
graph = buildModuleGraph(getFiles());
|
|
3798
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Watching for changes...`);
|
|
3799
|
+
};
|
|
3800
|
+
const runIncremental = async (changedFiles) => {
|
|
3801
|
+
const files = getFiles();
|
|
3802
|
+
const tracked = new Set(files);
|
|
3803
|
+
const dirtyUnion = /* @__PURE__ */ new Set();
|
|
3804
|
+
for (const cf of changedFiles) {
|
|
3805
|
+
for (const d of computeDirtySet(graph.dependents, cf)) dirtyUnion.add(d);
|
|
3806
|
+
}
|
|
3807
|
+
const dirty = [...dirtyUnion].filter((f) => tracked.has(f));
|
|
3808
|
+
if (dirty.length === 0) return;
|
|
3809
|
+
const ordered = topoSortDirty(graph.imports, dirty);
|
|
3810
|
+
console.clear();
|
|
3811
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Analyzing (incremental)...
|
|
3812
|
+
`);
|
|
3813
|
+
const t0 = performance.now();
|
|
3814
|
+
resetMemo();
|
|
3815
|
+
for (const f of ordered) {
|
|
3816
|
+
try {
|
|
3817
|
+
await runInfer(f, { dts: opts.dts, showLoc: true });
|
|
3538
3818
|
} catch (err) {
|
|
3539
3819
|
console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
|
|
3540
3820
|
}
|
|
3541
3821
|
}
|
|
3822
|
+
const elapsed = Math.round(performance.now() - t0);
|
|
3823
|
+
console.log(`Incremental: re-analyzed ${ordered.length}, skipped ${files.length - ordered.length} (${elapsed}ms)`);
|
|
3542
3824
|
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Watching for changes...`);
|
|
3825
|
+
graph = buildModuleGraph(getFiles());
|
|
3543
3826
|
};
|
|
3544
|
-
runAll()
|
|
3827
|
+
runAll().catch(() => {
|
|
3828
|
+
});
|
|
3545
3829
|
let debounceTimer = null;
|
|
3546
3830
|
const watchTarget = isDir ? resolved : dirname(resolved);
|
|
3831
|
+
const pendingChanged = /* @__PURE__ */ new Set();
|
|
3547
3832
|
watch(watchTarget, { recursive: isDir }, (_event, filename) => {
|
|
3548
3833
|
if (!filename) return;
|
|
3549
3834
|
const fullPath = isDir ? join(watchTarget, filename) : resolved;
|
|
3550
3835
|
if (!fullPath.endsWith(".js")) return;
|
|
3836
|
+
if (!existsSync(fullPath)) return;
|
|
3837
|
+
pendingChanged.add(fullPath);
|
|
3551
3838
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3552
|
-
debounceTimer = setTimeout(
|
|
3839
|
+
debounceTimer = setTimeout(() => {
|
|
3840
|
+
const changedFiles = [...pendingChanged];
|
|
3841
|
+
pendingChanged.clear();
|
|
3842
|
+
runIncremental(changedFiles).catch(() => {
|
|
3843
|
+
});
|
|
3844
|
+
}, 200);
|
|
3553
3845
|
});
|
|
3554
3846
|
});
|
|
3555
3847
|
function collectNudoFiles(dir) {
|
|
@@ -3559,12 +3851,185 @@ function collectNudoFiles(dir) {
|
|
|
3559
3851
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
3560
3852
|
results.push(...collectNudoFiles(fullPath));
|
|
3561
3853
|
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
3562
|
-
|
|
3563
|
-
if (/@nudo:(case|mock|pure|skip|sample|returns)\b/.test(content)) {
|
|
3564
|
-
results.push(fullPath);
|
|
3565
|
-
}
|
|
3854
|
+
results.push(fullPath);
|
|
3566
3855
|
}
|
|
3567
3856
|
}
|
|
3568
3857
|
return results;
|
|
3569
3858
|
}
|
|
3570
|
-
|
|
3859
|
+
function nodeLoc(node) {
|
|
3860
|
+
return {
|
|
3861
|
+
start: { line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 },
|
|
3862
|
+
end: { line: node.loc?.end.line ?? 1, column: node.loc?.end.column ?? 0 }
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
function fnParamNames(node) {
|
|
3866
|
+
const fn = node.type === "ExportDefaultDeclaration" ? node.declaration : node;
|
|
3867
|
+
const paramListOf = (params) => params.map((p) => {
|
|
3868
|
+
if (p.type === "Identifier") return p.name;
|
|
3869
|
+
if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
|
|
3870
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
|
|
3871
|
+
return "_";
|
|
3872
|
+
});
|
|
3873
|
+
if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression") {
|
|
3874
|
+
return paramListOf(fn.params);
|
|
3875
|
+
}
|
|
3876
|
+
if (fn.type === "VariableDeclaration") {
|
|
3877
|
+
const init = fn.declarations[0].init;
|
|
3878
|
+
if (init?.type === "FunctionExpression" || init?.type === "ArrowFunctionExpression") {
|
|
3879
|
+
return paramListOf(init.params);
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return [];
|
|
3883
|
+
}
|
|
3884
|
+
program2.command("generate").description("Generate runtime validators from inferred types").argument("<file>", "JavaScript file to analyze").option("--format <format>", "Output format: zod, guard, dts, all", "all").option("--output <dir>", "Output directory", ".").action((file, options) => {
|
|
3885
|
+
const filePath = resolve(file);
|
|
3886
|
+
const source = readFileSync(filePath, "utf-8");
|
|
3887
|
+
const ast = parse(source);
|
|
3888
|
+
const functions = extractDirectives(ast);
|
|
3889
|
+
if (functions.length === 0) {
|
|
3890
|
+
console.log("No functions with @nudo:case directives found.");
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
resetMemo();
|
|
3894
|
+
setModuleResolver(resolveModule);
|
|
3895
|
+
setCurrentFileDir(dirname(filePath));
|
|
3896
|
+
const globalEnv = createEnvironment();
|
|
3897
|
+
evaluateProgram(ast, globalEnv);
|
|
3898
|
+
for (const fn of functions) {
|
|
3899
|
+
applyMocks(fn.directives, globalEnv, filePath);
|
|
3900
|
+
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
3901
|
+
if (caseDirectives.length === 0) continue;
|
|
3902
|
+
const caseResults = caseDirectives.map((directive) => {
|
|
3903
|
+
const fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
3904
|
+
return {
|
|
3905
|
+
name: directive.name,
|
|
3906
|
+
args: directive.args,
|
|
3907
|
+
result: fullResult.value,
|
|
3908
|
+
throws: fullResult.throws,
|
|
3909
|
+
source: "directive"
|
|
3910
|
+
};
|
|
3911
|
+
});
|
|
3912
|
+
const baseName = fn.name;
|
|
3913
|
+
if (options.format === "zod" || options.format === "all") {
|
|
3914
|
+
console.log(`
|
|
3915
|
+
// === ${baseName} Zod Schemas ===`);
|
|
3916
|
+
for (const c of caseResults) {
|
|
3917
|
+
const inputSchemas = c.args.map((a, i) => `arg${i}: ${typeValueToZodSchema(a)}`).join(", ");
|
|
3918
|
+
const outputSchema = typeValueToZodSchema(c.result);
|
|
3919
|
+
console.log(`// Case "${c.name}":`);
|
|
3920
|
+
console.log(`// Input: { ${inputSchemas} }`);
|
|
3921
|
+
console.log(`// Output: ${outputSchema}`);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
if (options.format === "guard" || options.format === "all") {
|
|
3925
|
+
console.log(`
|
|
3926
|
+
// === ${baseName} Type Guards ===`);
|
|
3927
|
+
for (const c of caseResults) {
|
|
3928
|
+
const guard = generateGuardFunction(`is${baseName}${c.name.charAt(0).toUpperCase() + c.name.slice(1)}Output`, c.result);
|
|
3929
|
+
console.log(guard);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
if (options.format === "dts" || options.format === "all") {
|
|
3933
|
+
console.log(`
|
|
3934
|
+
// === ${baseName} TypeScript Declarations ===`);
|
|
3935
|
+
const analysis = {
|
|
3936
|
+
name: baseName,
|
|
3937
|
+
loc: nodeLoc(fn.node),
|
|
3938
|
+
paramNames: fnParamNames(fn.node),
|
|
3939
|
+
cases: caseResults
|
|
3940
|
+
};
|
|
3941
|
+
for (const line of generateFunctionDtsLines(analysis)) {
|
|
3942
|
+
console.log(line);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
setModuleResolver(null);
|
|
3947
|
+
});
|
|
3948
|
+
var HARVEST_MAX_FILES = 200;
|
|
3949
|
+
var REFERENCE_PATH_REGEX = /<reference\s+path=["']([^"']+)["']\s*\/>/g;
|
|
3950
|
+
var RELATIVE_FROM_REGEX = /\bfrom\s+["'](\.[^"']+)["']/g;
|
|
3951
|
+
function collectDtsFiles(entry) {
|
|
3952
|
+
const files = [];
|
|
3953
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3954
|
+
const queue = [entry];
|
|
3955
|
+
while (queue.length > 0 && files.length < HARVEST_MAX_FILES) {
|
|
3956
|
+
const current = queue.shift();
|
|
3957
|
+
if (seen.has(current) || !existsSync(current)) continue;
|
|
3958
|
+
seen.add(current);
|
|
3959
|
+
if (!current.endsWith(".d.ts")) continue;
|
|
3960
|
+
files.push(current);
|
|
3961
|
+
let text;
|
|
3962
|
+
try {
|
|
3963
|
+
text = readFileSync(current, "utf-8");
|
|
3964
|
+
} catch {
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3967
|
+
const dir = dirname(current);
|
|
3968
|
+
for (const match of text.matchAll(REFERENCE_PATH_REGEX)) {
|
|
3969
|
+
queue.push(resolve(dir, match[1]));
|
|
3970
|
+
}
|
|
3971
|
+
for (const match of text.matchAll(RELATIVE_FROM_REGEX)) {
|
|
3972
|
+
const base = resolve(dir, match[1]);
|
|
3973
|
+
for (const candidate of [`${base}.d.ts`, join(base, "index.d.ts")]) {
|
|
3974
|
+
if (existsSync(candidate)) {
|
|
3975
|
+
queue.push(candidate);
|
|
3976
|
+
break;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
return files;
|
|
3982
|
+
}
|
|
3983
|
+
function runHarvest(pkg, outOpt) {
|
|
3984
|
+
const typesDir = resolve(process.cwd(), "node_modules", "@types", pkg);
|
|
3985
|
+
if (!existsSync(typesDir)) {
|
|
3986
|
+
console.error(`Error: ${relative(process.cwd(), typesDir)} not found.`);
|
|
3987
|
+
console.error(`Install the package first, e.g. pnpm add -D @types/${pkg}`);
|
|
3988
|
+
process.exitCode = 1;
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
let entry = join(typesDir, "index.d.ts");
|
|
3992
|
+
const pkgJsonPath = join(typesDir, "package.json");
|
|
3993
|
+
if (existsSync(pkgJsonPath)) {
|
|
3994
|
+
try {
|
|
3995
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
3996
|
+
const declared = pkgJson.types ?? pkgJson.typings;
|
|
3997
|
+
if (typeof declared === "string") {
|
|
3998
|
+
const candidate = resolve(typesDir, declared);
|
|
3999
|
+
if (existsSync(candidate)) entry = candidate;
|
|
4000
|
+
}
|
|
4001
|
+
} catch {
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
if (!existsSync(entry)) {
|
|
4005
|
+
console.error(`Error: no .d.ts entry found in ${relative(process.cwd(), typesDir)} (tried ${basename(entry)}).`);
|
|
4006
|
+
process.exitCode = 1;
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
const files = collectDtsFiles(entry);
|
|
4010
|
+
if (files.length === 0) {
|
|
4011
|
+
console.error(`Error: no .d.ts files collected from ${relative(process.cwd(), entry)}.`);
|
|
4012
|
+
process.exitCode = 1;
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
const env = harvestDts(files);
|
|
4016
|
+
const code = emitEnvModule(env, `@types/${pkg}`);
|
|
4017
|
+
const out = resolve(outOpt ?? `nudo-harvest-${pkg}.ts`);
|
|
4018
|
+
writeFileSync(out, code, "utf-8");
|
|
4019
|
+
console.log(`Harvested @types/${pkg} \u2192 ${relative(process.cwd(), out) || out}`);
|
|
4020
|
+
console.log(` files: ${env.stats.files}`);
|
|
4021
|
+
console.log(` symbols: ${env.stats.symbols}`);
|
|
4022
|
+
console.log(` skipped: ${env.stats.skipped}`);
|
|
4023
|
+
console.log(`
|
|
4024
|
+
Usage \u2014 add this directive at the top of your JS file:`);
|
|
4025
|
+
const outDir = dirname(out);
|
|
4026
|
+
const hintPath = resolve(process.cwd()) === outDir ? basename(out) : out;
|
|
4027
|
+
console.log(` /// @nudo:env ${hintPath}`);
|
|
4028
|
+
}
|
|
4029
|
+
program2.command("harvest").description("Convert @types/<pkg> .d.ts declarations into a Nudo env file (TS source using T.* constructors)").argument("<pkg>", "Package name under @types (e.g. node)").option("--out <file>", "Output .ts env file (default: ./nudo-harvest-<pkg>.ts)").action((pkg, opts) => {
|
|
4030
|
+
runHarvest(pkg, opts.out);
|
|
4031
|
+
});
|
|
4032
|
+
program2.parseAsync(process.argv).catch((err) => {
|
|
4033
|
+
console.error(err instanceof Error ? process.env.NUDO_DEBUG ? err.stack : err.message : err);
|
|
4034
|
+
process.exitCode = 1;
|
|
4035
|
+
});
|