@nudojs/cli 0.1.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/chunk-3HCPXPEW.js +5286 -0
- package/dist/evaluator-api.d.ts +96 -2
- package/dist/evaluator-api.js +191 -3
- package/dist/index.js +595 -120
- package/examples/real-world.js +66 -0
- package/examples/validate-real-world.ts +160 -0
- package/package.json +28 -3
- 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 +283 -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-refined.test.ts +121 -0
- package/src/__tests__/evaluator-string-methods.test.ts +151 -0
- 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 +22 -0
- package/src/evaluator.ts +4218 -171
- package/src/index.ts +776 -125
- package/src/narrowing.ts +286 -15
- package/src/resolve-npm.ts +63 -0
- package/dist/chunk-JW3RMCNY.js +0 -1467
- package/dist/chunk-RBHWWZII.js +0 -1467
- package/dist/chunk-VN5HMBGB.js +0 -1467
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,18 +3333,42 @@ var {
|
|
|
3333
3333
|
|
|
3334
3334
|
// src/index.ts
|
|
3335
3335
|
import {
|
|
3336
|
+
T,
|
|
3336
3337
|
typeValueToString,
|
|
3337
|
-
simplifyUnion,
|
|
3338
3338
|
createEnvironment,
|
|
3339
|
-
|
|
3339
|
+
mockHelperToTypeValue
|
|
3340
3340
|
} from "@nudojs/core";
|
|
3341
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";
|
|
3342
3356
|
var program2 = new Command();
|
|
3343
3357
|
program2.name("nudo").description("Nudo type inference engine").version("0.0.1");
|
|
3344
3358
|
function applyMocks(directives, env, filePath) {
|
|
3345
3359
|
for (const d of directives) {
|
|
3346
3360
|
if (d.kind !== "mock") continue;
|
|
3347
|
-
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) {
|
|
3348
3372
|
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
3349
3373
|
} else if (d.fromPath) {
|
|
3350
3374
|
const mockPath = resolve(dirname(filePath), d.fromPath);
|
|
@@ -3357,148 +3381,152 @@ function applyMocks(directives, env, filePath) {
|
|
|
3357
3381
|
}
|
|
3358
3382
|
}
|
|
3359
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
|
+
}
|
|
3360
3398
|
function resolveModule(source, fromDir) {
|
|
3361
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
|
+
}
|
|
3362
3405
|
const basePath = resolve(fromDir, source);
|
|
3363
3406
|
for (const ext of ["", ...extensions]) {
|
|
3364
3407
|
const candidate = basePath + ext;
|
|
3365
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
|
+
}
|
|
3366
3416
|
const src = readFileSync(candidate, "utf-8");
|
|
3367
3417
|
return { ast: parse(src), filePath: candidate };
|
|
3368
3418
|
}
|
|
3369
3419
|
}
|
|
3370
3420
|
return null;
|
|
3371
3421
|
}
|
|
3372
|
-
function
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
const inner = entries.map(([k, v]) => `${k}: ${typeValueToTSType(v)}`).join("; ");
|
|
3387
|
-
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;
|
|
3388
3436
|
}
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3437
|
+
const siteFiles = statSync(sitePath).isDirectory() ? collectNudoFiles(sitePath) : [sitePath];
|
|
3438
|
+
for (const sf of siteFiles) {
|
|
3439
|
+
records.push(...collectCallRecords(sf, readFileSync(sf, "utf-8")));
|
|
3392
3440
|
}
|
|
3393
|
-
case "tuple":
|
|
3394
|
-
return `[${tv.elements.map(typeValueToTSType).join(", ")}]`;
|
|
3395
|
-
case "function":
|
|
3396
|
-
return `(${tv.params.map((p) => `${p}: unknown`).join(", ")}) => unknown`;
|
|
3397
|
-
case "promise":
|
|
3398
|
-
return `Promise<${typeValueToTSType(tv.value)}>`;
|
|
3399
|
-
case "instance":
|
|
3400
|
-
return tv.className;
|
|
3401
|
-
case "union":
|
|
3402
|
-
return tv.members.map(typeValueToTSType).join(" | ");
|
|
3403
|
-
case "never":
|
|
3404
|
-
return "never";
|
|
3405
|
-
case "unknown":
|
|
3406
|
-
return "unknown";
|
|
3407
3441
|
}
|
|
3442
|
+
return records.length > 0 ? records : void 0;
|
|
3408
3443
|
}
|
|
3409
|
-
function runInfer(file, options = {}) {
|
|
3444
|
+
async function runInfer(file, options = {}) {
|
|
3410
3445
|
const filePath = resolve(file);
|
|
3411
3446
|
const source = readFileSync(filePath, "utf-8");
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
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) {
|
|
3415
3459
|
console.log("No functions with @nudo:case directives found.");
|
|
3416
3460
|
return;
|
|
3417
3461
|
}
|
|
3418
|
-
resetMemo();
|
|
3419
|
-
setModuleResolver(resolveModule);
|
|
3420
|
-
setCurrentFileDir(dirname(filePath));
|
|
3421
|
-
const globalEnv = createEnvironment();
|
|
3422
|
-
evaluateProgram(ast, globalEnv);
|
|
3423
3462
|
const dtsLines = [];
|
|
3424
|
-
for (const fn of functions) {
|
|
3425
|
-
const loc = fn.
|
|
3426
|
-
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}`;
|
|
3427
3466
|
const header = options.showLoc ? `=== ${fn.name} (${locStr}) ===` : `=== ${fn.name} ===`;
|
|
3428
3467
|
console.log(`${header}
|
|
3429
3468
|
`);
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
console.log(`Skipped (declared): ${typeValueToString(skipDirective.returns)}`);
|
|
3437
|
-
if (options.dts) {
|
|
3438
|
-
dtsLines.push(`export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(skipDirective.returns)};`);
|
|
3439
|
-
}
|
|
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)}`);
|
|
3440
3475
|
} else {
|
|
3441
3476
|
console.log("Skipped (no return type declared)");
|
|
3442
3477
|
}
|
|
3443
3478
|
console.log();
|
|
3444
3479
|
continue;
|
|
3445
3480
|
}
|
|
3446
|
-
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
3453
|
-
const caseResults = [];
|
|
3454
|
-
for (const directive of caseDirectives) {
|
|
3455
|
-
const fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
3456
|
-
const argsStr = directive.args.map(typeValueToString).join(", ");
|
|
3457
|
-
const resultStr = typeValueToString(fullResult.value);
|
|
3458
|
-
const throwsStr = fullResult.throws.kind !== "never" ? typeValueToString(fullResult.throws) : void 0;
|
|
3459
|
-
caseResults.push({
|
|
3460
|
-
name: directive.name,
|
|
3461
|
-
args: directive.args,
|
|
3462
|
-
argsStr,
|
|
3463
|
-
result: resultStr,
|
|
3464
|
-
resultTV: fullResult.value,
|
|
3465
|
-
throws: throwsStr
|
|
3466
|
-
});
|
|
3467
|
-
let line = `Case "${directive.name}": (${argsStr}) => ${resultStr}`;
|
|
3468
|
-
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)}`;
|
|
3469
3485
|
console.log(line);
|
|
3470
3486
|
}
|
|
3471
|
-
if (
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
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) {
|
|
3476
3491
|
console.log(`
|
|
3477
|
-
Combined: ${typeValueToString(combined)}`);
|
|
3492
|
+
Combined: ${typeValueToString(fn.combined)}`);
|
|
3478
3493
|
}
|
|
3479
|
-
if (
|
|
3480
|
-
for (const
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
if (!matches) {
|
|
3484
|
-
console.log(`
|
|
3485
|
-
\u26A0 @nudo:returns assertion failed for case "${directive.name}": expected ${typeValueToString(returnsDirective.expected)}, got ${typeValueToString(result)}`);
|
|
3486
|
-
}
|
|
3494
|
+
if (fn.assertionErrors && fn.assertionErrors.length > 0) {
|
|
3495
|
+
for (const err of fn.assertionErrors) {
|
|
3496
|
+
console.log(`
|
|
3497
|
+
\u26A0 ${err}`);
|
|
3487
3498
|
}
|
|
3488
3499
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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);
|
|
3498
3522
|
}
|
|
3523
|
+
if (fn.cases.length > 1 && fn.combined) {
|
|
3524
|
+
console.log(`
|
|
3525
|
+
Combined: ${typeValueToString(fn.combined)}`);
|
|
3526
|
+
}
|
|
3527
|
+
console.log();
|
|
3499
3528
|
}
|
|
3500
3529
|
}
|
|
3501
|
-
console.log();
|
|
3502
3530
|
}
|
|
3503
3531
|
if (options.dts && dtsLines.length > 0) {
|
|
3504
3532
|
const dtsPath = filePath.replace(/\.js$/, ".d.ts");
|
|
@@ -3506,10 +3534,246 @@ Combined: ${typeValueToString(combined)}`);
|
|
|
3506
3534
|
writeFileSync(dtsPath, dtsContent, "utf-8");
|
|
3507
3535
|
console.log(`Generated: ${relative(process.cwd(), dtsPath)}`);
|
|
3508
3536
|
}
|
|
3509
|
-
|
|
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
|
+
}
|
|
3668
|
+
}
|
|
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;
|
|
3510
3774
|
}
|
|
3511
|
-
program2.command("
|
|
3512
|
-
|
|
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);
|
|
3513
3777
|
});
|
|
3514
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) => {
|
|
3515
3779
|
const resolved = resolve(watchPath);
|
|
@@ -3518,28 +3782,66 @@ program2.command("watch").description("Watch file(s) for changes and re-run infe
|
|
|
3518
3782
|
if (!isDir) return [resolved];
|
|
3519
3783
|
return collectNudoFiles(resolved);
|
|
3520
3784
|
};
|
|
3521
|
-
|
|
3785
|
+
let graph = buildModuleGraph(getFiles());
|
|
3786
|
+
const runAll = async () => {
|
|
3522
3787
|
console.clear();
|
|
3523
3788
|
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Analyzing...
|
|
3524
3789
|
`);
|
|
3525
3790
|
for (const f of getFiles()) {
|
|
3526
3791
|
try {
|
|
3527
|
-
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 });
|
|
3528
3818
|
} catch (err) {
|
|
3529
3819
|
console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
|
|
3530
3820
|
}
|
|
3531
3821
|
}
|
|
3822
|
+
const elapsed = Math.round(performance.now() - t0);
|
|
3823
|
+
console.log(`Incremental: re-analyzed ${ordered.length}, skipped ${files.length - ordered.length} (${elapsed}ms)`);
|
|
3532
3824
|
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Watching for changes...`);
|
|
3825
|
+
graph = buildModuleGraph(getFiles());
|
|
3533
3826
|
};
|
|
3534
|
-
runAll()
|
|
3827
|
+
runAll().catch(() => {
|
|
3828
|
+
});
|
|
3535
3829
|
let debounceTimer = null;
|
|
3536
3830
|
const watchTarget = isDir ? resolved : dirname(resolved);
|
|
3831
|
+
const pendingChanged = /* @__PURE__ */ new Set();
|
|
3537
3832
|
watch(watchTarget, { recursive: isDir }, (_event, filename) => {
|
|
3538
3833
|
if (!filename) return;
|
|
3539
3834
|
const fullPath = isDir ? join(watchTarget, filename) : resolved;
|
|
3540
3835
|
if (!fullPath.endsWith(".js")) return;
|
|
3836
|
+
if (!existsSync(fullPath)) return;
|
|
3837
|
+
pendingChanged.add(fullPath);
|
|
3541
3838
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3542
|
-
debounceTimer = setTimeout(
|
|
3839
|
+
debounceTimer = setTimeout(() => {
|
|
3840
|
+
const changedFiles = [...pendingChanged];
|
|
3841
|
+
pendingChanged.clear();
|
|
3842
|
+
runIncremental(changedFiles).catch(() => {
|
|
3843
|
+
});
|
|
3844
|
+
}, 200);
|
|
3543
3845
|
});
|
|
3544
3846
|
});
|
|
3545
3847
|
function collectNudoFiles(dir) {
|
|
@@ -3549,12 +3851,185 @@ function collectNudoFiles(dir) {
|
|
|
3549
3851
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
3550
3852
|
results.push(...collectNudoFiles(fullPath));
|
|
3551
3853
|
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
3552
|
-
|
|
3553
|
-
if (/@nudo:(case|mock|pure|skip|sample|returns)\b/.test(content)) {
|
|
3554
|
-
results.push(fullPath);
|
|
3555
|
-
}
|
|
3854
|
+
results.push(fullPath);
|
|
3556
3855
|
}
|
|
3557
3856
|
}
|
|
3558
3857
|
return results;
|
|
3559
3858
|
}
|
|
3560
|
-
|
|
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
|
+
});
|