miniread 1.0.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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/bin/miniread +17 -0
  4. package/bin/miniread-evaluate +17 -0
  5. package/bin/miniread-sample +17 -0
  6. package/dist/cli/config.d.ts +17 -0
  7. package/dist/cli/config.js +85 -0
  8. package/dist/cli/generate-code.d.ts +2 -0
  9. package/dist/cli/generate-code.js +7 -0
  10. package/dist/cli/output.d.ts +7 -0
  11. package/dist/cli/output.js +119 -0
  12. package/dist/cli/run-transforms.d.ts +16 -0
  13. package/dist/cli/run-transforms.js +145 -0
  14. package/dist/cli/runner.d.ts +3 -0
  15. package/dist/cli/runner.js +2 -0
  16. package/dist/cli/transform-stdin.d.ts +14 -0
  17. package/dist/cli/transform-stdin.js +58 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +121 -0
  20. package/dist/core/paths.d.ts +8 -0
  21. package/dist/core/paths.js +9 -0
  22. package/dist/core/project-graph.d.ts +6 -0
  23. package/dist/core/project-graph.js +40 -0
  24. package/dist/core/types.d.ts +30 -0
  25. package/dist/core/types.js +1 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.js +7 -0
  28. package/dist/scripts/evaluate/create-evaluate-command.d.ts +5 -0
  29. package/dist/scripts/evaluate/create-evaluate-command.js +47 -0
  30. package/dist/scripts/evaluate/diff-utilities.d.ts +17 -0
  31. package/dist/scripts/evaluate/diff-utilities.js +114 -0
  32. package/dist/scripts/evaluate/evaluation-report.d.ts +22 -0
  33. package/dist/scripts/evaluate/evaluation-report.js +1 -0
  34. package/dist/scripts/evaluate/evaluation-types.d.ts +16 -0
  35. package/dist/scripts/evaluate/evaluation-types.js +1 -0
  36. package/dist/scripts/evaluate/metrics.d.ts +3 -0
  37. package/dist/scripts/evaluate/metrics.js +11 -0
  38. package/dist/scripts/evaluate/pair-evaluator.d.ts +21 -0
  39. package/dist/scripts/evaluate/pair-evaluator.js +104 -0
  40. package/dist/scripts/evaluate/parse-evaluate-cli-options.d.ts +40 -0
  41. package/dist/scripts/evaluate/parse-evaluate-cli-options.js +97 -0
  42. package/dist/scripts/evaluate/parse-transform-manifest.d.ts +22 -0
  43. package/dist/scripts/evaluate/parse-transform-manifest.js +29 -0
  44. package/dist/scripts/evaluate/resolve-evaluate-dependencies.d.ts +9 -0
  45. package/dist/scripts/evaluate/resolve-evaluate-dependencies.js +66 -0
  46. package/dist/scripts/evaluate/run-evaluate-cli.d.ts +1 -0
  47. package/dist/scripts/evaluate/run-evaluate-cli.js +112 -0
  48. package/dist/scripts/evaluate/run-evaluations.d.ts +25 -0
  49. package/dist/scripts/evaluate/run-evaluations.js +50 -0
  50. package/dist/scripts/evaluate/run-pair-transformations.d.ts +21 -0
  51. package/dist/scripts/evaluate/run-pair-transformations.js +57 -0
  52. package/dist/scripts/evaluate/sanitize.d.ts +2 -0
  53. package/dist/scripts/evaluate/sanitize.js +6 -0
  54. package/dist/scripts/evaluate/shell-utilities.d.ts +15 -0
  55. package/dist/scripts/evaluate/shell-utilities.js +68 -0
  56. package/dist/scripts/evaluate/transform-id-set.d.ts +3 -0
  57. package/dist/scripts/evaluate/transform-id-set.js +29 -0
  58. package/dist/scripts/evaluate/transform-manifest.d.ts +13 -0
  59. package/dist/scripts/evaluate/transform-manifest.js +102 -0
  60. package/dist/scripts/evaluate/write-evaluation-stdout.d.ts +7 -0
  61. package/dist/scripts/evaluate/write-evaluation-stdout.js +20 -0
  62. package/dist/scripts/evaluate/write-text-file-atomic.d.ts +5 -0
  63. package/dist/scripts/evaluate/write-text-file-atomic.js +47 -0
  64. package/dist/scripts/evaluate.d.ts +2 -0
  65. package/dist/scripts/evaluate.js +13 -0
  66. package/dist/scripts/sample/choose-line-window.d.ts +11 -0
  67. package/dist/scripts/sample/choose-line-window.js +19 -0
  68. package/dist/scripts/sample/clip-text-around-core.d.ts +12 -0
  69. package/dist/scripts/sample/clip-text-around-core.js +61 -0
  70. package/dist/scripts/sample/collect-function-candidates.d.ts +20 -0
  71. package/dist/scripts/sample/collect-function-candidates.js +82 -0
  72. package/dist/scripts/sample/create-function-excerpt.d.ts +20 -0
  73. package/dist/scripts/sample/create-function-excerpt.js +37 -0
  74. package/dist/scripts/sample/create-sample-command.d.ts +17 -0
  75. package/dist/scripts/sample/create-sample-command.js +50 -0
  76. package/dist/scripts/sample/extract-function-samples.d.ts +31 -0
  77. package/dist/scripts/sample/extract-function-samples.js +105 -0
  78. package/dist/scripts/sample/find-source-files.d.ts +1 -0
  79. package/dist/scripts/sample/find-source-files.js +49 -0
  80. package/dist/scripts/sample/format-sample-output.d.ts +22 -0
  81. package/dist/scripts/sample/format-sample-output.js +29 -0
  82. package/dist/scripts/sample/line-offsets.d.ts +4 -0
  83. package/dist/scripts/sample/line-offsets.js +40 -0
  84. package/dist/scripts/sample/parse-sample-cli-options.d.ts +18 -0
  85. package/dist/scripts/sample/parse-sample-cli-options.js +36 -0
  86. package/dist/scripts/sample/random.d.ts +10 -0
  87. package/dist/scripts/sample/random.js +60 -0
  88. package/dist/scripts/sample/run-sample-cli.d.ts +1 -0
  89. package/dist/scripts/sample/run-sample-cli.js +79 -0
  90. package/dist/scripts/sample.d.ts +2 -0
  91. package/dist/scripts/sample.js +13 -0
  92. package/dist/transforms/add-prefix/add-prefix-transform.d.ts +2 -0
  93. package/dist/transforms/add-prefix/add-prefix-transform.js +40 -0
  94. package/dist/transforms/add-suffix/add-suffix-transform.d.ts +2 -0
  95. package/dist/transforms/add-suffix/add-suffix-transform.js +40 -0
  96. package/dist/transforms/expand-boolean-literals/expand-boolean-literals-transform.d.ts +2 -0
  97. package/dist/transforms/expand-boolean-literals/expand-boolean-literals-transform.js +39 -0
  98. package/dist/transforms/expand-undefined-literals/expand-undefined-literals-transform.d.ts +2 -0
  99. package/dist/transforms/expand-undefined-literals/expand-undefined-literals-transform.js +40 -0
  100. package/dist/transforms/rename-catch-parameters/rename-catch-parameters-transform.d.ts +2 -0
  101. package/dist/transforms/rename-catch-parameters/rename-catch-parameters-transform.js +67 -0
  102. package/dist/transforms/rename-loop-index-variables/rename-loop-index-variables-transform.d.ts +2 -0
  103. package/dist/transforms/rename-loop-index-variables/rename-loop-index-variables-transform.js +125 -0
  104. package/dist/transforms/transform-presets.d.ts +3 -0
  105. package/dist/transforms/transform-presets.js +19 -0
  106. package/dist/transforms/transform-registry.d.ts +3 -0
  107. package/dist/transforms/transform-registry.js +15 -0
  108. package/package.json +84 -0
  109. package/transform-manifest.json +70 -0
package/dist/cli.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
3
+ import packageJson from "../package.json" with { type: "json" };
4
+ import { getTransformsToRun, getTransformSummaries } from "./cli/config.js";
5
+ import { runTransforms, transformStdin } from "./cli/runner.js";
6
+ const parseListFormat = (value) => {
7
+ if (value === "text" || value === "tsv" || value === "json")
8
+ return value;
9
+ throw new InvalidArgumentError(`invalid value '${value}' (expected: text, tsv, json)`);
10
+ };
11
+ const parsePositiveInt = (value) => {
12
+ const parsed = Number.parseInt(value);
13
+ if (!Number.isFinite(parsed) || parsed < 1) {
14
+ throw new InvalidArgumentError("must be a positive integer");
15
+ }
16
+ return parsed;
17
+ };
18
+ const program = new Command()
19
+ .name(packageJson.name)
20
+ .description(packageJson.description)
21
+ .version(packageJson.version)
22
+ .showHelpAfterError("(add --help for additional information)")
23
+ .showSuggestionAfterError()
24
+ .option("-v, --verbose", "Enable verbose output")
25
+ .option("-i, --input <path>", "Input file or directory (default: stdin)")
26
+ .option("-o, --output <path>", "Output directory (required when using --input)")
27
+ .option("-t, --transforms <list>", "Comma-separated list of transforms to run (or preset: recommended, all, none)")
28
+ .option("--list-transforms", "List available transforms")
29
+ .option("--format <format>", "Output format for --list-transforms: text, tsv, json", parseListFormat, "text")
30
+ .option("--dry-run", "Show what would be changed without writing files")
31
+ .option("-w, --workers <n>", "Number of parallel workers", parsePositiveInt, 4)
32
+ .option("-f, --overwrite", "Overwrite existing files in output directory");
33
+ program.addHelpText("after", `
34
+ Examples:
35
+ # Read from stdin, write to stdout (filter mode)
36
+ cat bundle.min.js | miniread > bundle.js
37
+
38
+ # Process a directory into an output directory
39
+ miniread --input ./minified --output ./readable
40
+
41
+ # Use the recommended preset explicitly
42
+ miniread --input ./minified --output ./readable --transforms recommended
43
+
44
+ # Run all transforms (including non-recommended ones)
45
+ miniread --input ./minified --output ./readable --transforms all
46
+
47
+ # List available transforms (one per line)
48
+ miniread --list-transforms
49
+
50
+ # Pipeline example: find frequent identifiers after transforming
51
+ cat bundle.min.js | miniread | grep -oE '[A-Za-z_][A-Za-z0-9_]+' | sort | uniq -c | sort -rn | head -20
52
+ `);
53
+ // Package managers often pass a standalone `--` to separate script args.
54
+ // Commander treats `--` as "end of options", which would cause flags like
55
+ // `--help` to be parsed as positional arguments. Strip it for robustness.
56
+ program.parse(process.argv.filter((argument) => argument !== "--"));
57
+ const options = program.opts();
58
+ if (options.listTransforms) {
59
+ const summaries = getTransformSummaries();
60
+ if (options.format === "json") {
61
+ process.stdout.write(JSON.stringify(summaries, undefined, 2) + "\n");
62
+ }
63
+ else if (options.format === "tsv") {
64
+ process.stdout.write("ID\tDESCRIPTION\tSCOPE\tPARALLELIZABLE\n");
65
+ for (const summary of summaries) {
66
+ process.stdout.write(`${summary.id}\t${summary.description}\t${summary.scope}\t${summary.parallelizable}\n`);
67
+ }
68
+ }
69
+ else {
70
+ for (const summary of summaries) {
71
+ process.stdout.write(`${summary.id}\n`);
72
+ }
73
+ }
74
+ }
75
+ else if (options.input) {
76
+ if (!options.output) {
77
+ program.error("--output is required when using --input");
78
+ process.exit(1);
79
+ }
80
+ const transformsResult = getTransformsToRun(options.transforms);
81
+ if (!transformsResult.ok) {
82
+ program.error(transformsResult.error);
83
+ process.exit(1);
84
+ }
85
+ const result = await runTransforms({
86
+ input: options.input,
87
+ output: options.output,
88
+ transforms: transformsResult.transforms,
89
+ dryRun: options.dryRun ?? false,
90
+ workers: options.workers,
91
+ overwrite: options.overwrite ?? false,
92
+ verbose: options.verbose ?? false,
93
+ });
94
+ if (result.errors.length > 0) {
95
+ console.error("Errors:");
96
+ for (const error of result.errors) {
97
+ console.error(`- ${error}`);
98
+ }
99
+ console.error(`Try '${packageJson.name} --help' for details.`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+ else {
104
+ if (options.output) {
105
+ program.error("--output cannot be used without --input");
106
+ }
107
+ const transformsResult = getTransformsToRun(options.transforms);
108
+ if (!transformsResult.ok) {
109
+ program.error(transformsResult.error);
110
+ process.exit(1);
111
+ }
112
+ const result = await transformStdin({
113
+ transforms: transformsResult.transforms,
114
+ verbose: options.verbose ?? false,
115
+ });
116
+ if (!result.ok) {
117
+ console.error(`Error: ${result.error}`);
118
+ process.exit(1);
119
+ }
120
+ process.stdout.write(result.output);
121
+ }
@@ -0,0 +1,8 @@
1
+ export type AppPaths = {
2
+ data: string;
3
+ config: string;
4
+ cache: string;
5
+ log: string;
6
+ temp: string;
7
+ };
8
+ export declare const appPaths: AppPaths;
@@ -0,0 +1,9 @@
1
+ import envPaths from "env-paths";
2
+ const paths = envPaths("miniread");
3
+ export const appPaths = {
4
+ data: paths.data,
5
+ config: paths.config,
6
+ cache: paths.cache,
7
+ log: paths.log,
8
+ temp: paths.temp,
9
+ };
@@ -0,0 +1,6 @@
1
+ import type { ProjectGraph } from "./types.js";
2
+ export type ParsedFile = {
3
+ path: string;
4
+ content: string;
5
+ };
6
+ export declare const buildProjectGraph: (files: ParsedFile[]) => ProjectGraph;
@@ -0,0 +1,40 @@
1
+ import { parse } from "@babel/parser";
2
+ const getPlugins = (filePath) => {
3
+ const plugins = [];
4
+ if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
5
+ plugins.push("typescript");
6
+ }
7
+ if (filePath.endsWith(".jsx") || filePath.endsWith(".tsx")) {
8
+ plugins.push("jsx");
9
+ }
10
+ return plugins;
11
+ };
12
+ export const buildProjectGraph = (files) => {
13
+ const fileMap = new Map();
14
+ for (const file of files) {
15
+ try {
16
+ const ast = parse(file.content, {
17
+ sourceType: "unambiguous",
18
+ plugins: getPlugins(file.path),
19
+ sourceFilename: file.path,
20
+ });
21
+ fileMap.set(file.path, {
22
+ path: file.path,
23
+ content: file.content,
24
+ ast,
25
+ });
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ console.warn(` Warning: Parse error in ${file.path}: ${message}`);
30
+ }
31
+ }
32
+ return {
33
+ files: fileMap,
34
+ // Stubbed for future implementation
35
+ symbols: new Map(),
36
+ references: new Map(),
37
+ exports: new Map(),
38
+ imports: new Map(),
39
+ };
40
+ };
@@ -0,0 +1,30 @@
1
+ import type { File } from "@babel/types";
2
+ export type SourceFileInfo = {
3
+ path: string;
4
+ content: string;
5
+ ast: File;
6
+ };
7
+ export type ProjectGraph = {
8
+ files: Map<string, SourceFileInfo>;
9
+ symbols: Map<string, unknown>;
10
+ references: Map<string, unknown>;
11
+ exports: Map<string, unknown>;
12
+ imports: Map<string, unknown>;
13
+ };
14
+ export type TransformContext = {
15
+ projectGraph: ProjectGraph;
16
+ currentFile: SourceFileInfo | undefined;
17
+ options: Record<string, unknown>;
18
+ };
19
+ export type TransformStats = {
20
+ nodesVisited: number;
21
+ transformationsApplied: number;
22
+ };
23
+ export type TransformScope = "file" | "project";
24
+ export type Transform = {
25
+ id: string;
26
+ description: string;
27
+ scope: TransformScope;
28
+ parallelizable: boolean;
29
+ transform: (context: TransformContext) => Promise<TransformStats>;
30
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export type { Transform, TransformContext, TransformScope, ProjectGraph, SourceFileInfo, TransformStats, } from "./core/types.js";
2
+ export { appPaths } from "./core/paths.js";
3
+ export type { AppPaths } from "./core/paths.js";
4
+ export { buildProjectGraph } from "./core/project-graph.js";
5
+ export { allTransformIds, transformRegistry, } from "./transforms/transform-registry.js";
6
+ export { runTransforms } from "./cli/runner.js";
7
+ export type { RunnerOptions, RunnerResult } from "./cli/runner.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Core utilities
2
+ export { appPaths } from "./core/paths.js";
3
+ export { buildProjectGraph } from "./core/project-graph.js";
4
+ // Transform registry
5
+ export { allTransformIds, transformRegistry, } from "./transforms/transform-registry.js";
6
+ // Runner (for programmatic use)
7
+ export { runTransforms } from "./cli/runner.js";
@@ -0,0 +1,5 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ export type OutputFormat = "tsv" | "json";
3
+ export declare const createEvaluateCommand: (options: {
4
+ version: string;
5
+ }) => Command;
@@ -0,0 +1,47 @@
1
+ import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
2
+ const parseOutputFormat = (value) => {
3
+ if (value === "tsv" || value === "json")
4
+ return value;
5
+ throw new InvalidArgumentError(`invalid value '${value}' (expected: tsv, json)`);
6
+ };
7
+ export const createEvaluateCommand = (options) => {
8
+ const program = new Command()
9
+ .name("miniread-evaluate")
10
+ .description("Compare miniread transform configurations by measuring diff reduction across version pairs.")
11
+ .version(options.version)
12
+ .showHelpAfterError("(add --help for additional information)")
13
+ .showSuggestionAfterError()
14
+ .option("-v, --verbose", "Enable verbose output")
15
+ .option("-c, --compare <from:to>", "Version pair to compare (repeatable)", (value, previous) => [...previous, value], [])
16
+ .option("-b, --baseline <transforms>", "Comma-separated baseline transforms (or preset: recommended, all, none)", "none")
17
+ .option("-t, --test <transforms>", "Comma-separated test transforms (or preset: recommended, all, none)", "none")
18
+ .option("--sources <path>", "Sources directory", "sources")
19
+ .option("--evaluation-dir <path>", "Output directory for patches/metrics", "evaluation")
20
+ .option("--format <format>", "Output format: tsv, json", parseOutputFormat, "tsv")
21
+ .option("--write-metrics", "Write metrics JSON to the evaluation directory")
22
+ .option("--metrics-file <path>", "Write metrics JSON to a specific path")
23
+ .option("--write-patches", "Write baseline/test diff patches to the evaluation directory")
24
+ .option("-u, --update-manifest", "Update transform-manifest.json based on results")
25
+ .option("--manifest-file <path>", "Path to transform-manifest.json", "transform-manifest.json")
26
+ .option("-f, --overwrite", "Overwrite existing metrics/patch files")
27
+ .option("--no-format-code", "Skip formatting temp directories with Prettier before diffing");
28
+ program.addHelpText("after", `
29
+ Dependencies:
30
+ - diff (override: MINIREAD_EVALUATE_DIFF_PATH)
31
+ - pnpm + prettier (used unless --no-format-code; overrides: MINIREAD_EVALUATE_PNPM_PATH, MINIREAD_EVALUATE_PRETTIER_PATH)
32
+
33
+ Examples:
34
+ # Compare baseline none vs a transform
35
+ miniread-evaluate --compare v1.0:v1.1 --baseline none --test add-prefix
36
+
37
+ # Multiple pairs, output JSON and inspect with jq
38
+ miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test add-prefix --format json | jq '.results[] | {pair, reductionRatio}'
39
+
40
+ # Pipeline example: sort by reduction ratio (best first)
41
+ miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test add-prefix | tail -n +2 | sort -k5,5nr | head -10
42
+
43
+ # Write patches + metrics (refuses to overwrite unless -f/--overwrite)
44
+ miniread-evaluate --compare v1.0:v1.1 --baseline none --test add-prefix --write-patches --write-metrics
45
+ `);
46
+ return program;
47
+ };
@@ -0,0 +1,17 @@
1
+ export type DiffStats = {
2
+ totalLines: number;
3
+ sizeBytes: number;
4
+ addedLines: number;
5
+ removedLines: number;
6
+ changedHunks: number;
7
+ };
8
+ export declare const createTransformSlug: (transforms: string[]) => string;
9
+ type ComputeDirectoryDiffOptions = {
10
+ diffPath: string;
11
+ directoryA: string;
12
+ directoryB: string;
13
+ outputPath: string | undefined;
14
+ overwrite: boolean;
15
+ };
16
+ export declare const computeDirectoryDiff: (options: ComputeDirectoryDiffOptions) => Promise<DiffStats>;
17
+ export {};
@@ -0,0 +1,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import { once } from "node:events";
3
+ import fs from "node:fs";
4
+ import * as fsPromises from "node:fs/promises";
5
+ import path from "node:path";
6
+ import readline from "node:readline";
7
+ export const createTransformSlug = (transforms) => {
8
+ return transforms.length > 0 ? transforms.join("-") : "none";
9
+ };
10
+ const isErrnoException = (error) => {
11
+ return error instanceof Error;
12
+ };
13
+ export const computeDirectoryDiff = async (options) => {
14
+ const { diffPath, directoryA, directoryB, outputPath, overwrite } = options;
15
+ if (outputPath) {
16
+ await fsPromises.mkdir(path.dirname(outputPath), { recursive: true });
17
+ }
18
+ const diffProcess = spawn(diffPath, ["-ru", directoryA, directoryB], {
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ });
21
+ const stderrChunks = [];
22
+ diffProcess.stderr.setEncoding("utf8");
23
+ diffProcess.stderr.on("data", (chunk) => {
24
+ if (stderrChunks.join("").length > 200_000)
25
+ return;
26
+ stderrChunks.push(chunk);
27
+ });
28
+ const outputStream = outputPath
29
+ ? fs.createWriteStream(outputPath, { flags: overwrite ? "w" : "wx" })
30
+ : undefined;
31
+ let outputStreamError;
32
+ if (outputStream) {
33
+ outputStream.on("error", (error) => {
34
+ outputStreamError = error;
35
+ diffProcess.kill();
36
+ outputStream.destroy();
37
+ });
38
+ }
39
+ let totalLines = 0;
40
+ let sizeBytes = 0;
41
+ let addedLines = 0;
42
+ let removedLines = 0;
43
+ let changedHunks = 0;
44
+ const rl = readline.createInterface({
45
+ input: diffProcess.stdout,
46
+ crlfDelay: Number.POSITIVE_INFINITY,
47
+ });
48
+ try {
49
+ for await (const line of rl) {
50
+ const normalizedLine = line.replaceAll(/[\t ]+$/gu, "");
51
+ const outputLine = `${normalizedLine}\n`;
52
+ totalLines += 1;
53
+ sizeBytes += Buffer.byteLength(outputLine, "utf8");
54
+ if (normalizedLine.startsWith("+") && !normalizedLine.startsWith("+++")) {
55
+ addedLines += 1;
56
+ }
57
+ else if (normalizedLine.startsWith("-") &&
58
+ !normalizedLine.startsWith("---")) {
59
+ removedLines += 1;
60
+ }
61
+ else if (normalizedLine.startsWith("@@")) {
62
+ changedHunks += 1;
63
+ }
64
+ if (!outputStream)
65
+ continue;
66
+ if (outputStreamError)
67
+ throw outputStreamError;
68
+ if (outputStream.write(outputLine))
69
+ continue;
70
+ await once(outputStream, "drain");
71
+ }
72
+ }
73
+ finally {
74
+ rl.close();
75
+ }
76
+ if (outputStreamError) {
77
+ if (outputPath && isErrnoException(outputStreamError)) {
78
+ if (outputStreamError.code !== "EEXIST")
79
+ throw outputStreamError;
80
+ throw new Error(`Refusing to overwrite existing diff output file: ${outputPath}. Re-run with --overwrite to replace.`);
81
+ }
82
+ throw outputStreamError;
83
+ }
84
+ const exitCode = await new Promise((resolve, reject) => {
85
+ diffProcess.on("error", reject);
86
+ diffProcess.on("close", (code) => {
87
+ resolve(code);
88
+ });
89
+ });
90
+ if (outputStream) {
91
+ await new Promise((resolve, reject) => {
92
+ outputStream.on("error", reject);
93
+ outputStream.end(() => {
94
+ resolve();
95
+ });
96
+ });
97
+ }
98
+ // diff exits:
99
+ // - 0: no differences
100
+ // - 1: differences found (expected)
101
+ // - 2: error
102
+ if (exitCode !== 0 && exitCode !== 1) {
103
+ const stderr = stderrChunks.join("").trim();
104
+ const stderrSuffix = stderr ? `\n\nstderr:\n${stderr}` : "";
105
+ throw new Error(`Failed to run diff (exit code ${exitCode}).${stderrSuffix}`);
106
+ }
107
+ return {
108
+ totalLines,
109
+ sizeBytes,
110
+ addedLines,
111
+ removedLines,
112
+ changedHunks,
113
+ };
114
+ };
@@ -0,0 +1,22 @@
1
+ import type { ComparisonPair, EvaluationResult } from "./evaluation-types.js";
2
+ export type EvaluationError = {
3
+ pair: ComparisonPair;
4
+ error: string;
5
+ };
6
+ export type EvaluationOutcome = {
7
+ ok: true;
8
+ pair: ComparisonPair;
9
+ result: EvaluationResult;
10
+ } | {
11
+ ok: false;
12
+ pair: ComparisonPair;
13
+ error: string;
14
+ };
15
+ export type EvaluationReport = {
16
+ version: string;
17
+ baselineTransforms: string[];
18
+ testTransforms: string[];
19
+ pairs: ComparisonPair[];
20
+ results: EvaluationResult[];
21
+ errors: EvaluationError[];
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { DiffStats } from "./diff-utilities.js";
2
+ export type ComparisonPair = {
3
+ from: string;
4
+ to: string;
5
+ };
6
+ export type EvaluationResult = {
7
+ timestamp: string;
8
+ pair: ComparisonPair;
9
+ baselineTransforms: string[];
10
+ testTransforms: string[];
11
+ baselineDiff: DiffStats;
12
+ testDiff: DiffStats;
13
+ reductionRatio: number;
14
+ baselinePatchPath?: string;
15
+ testPatchPath?: string;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { DiffStats } from "./diff-utilities.js";
2
+ export declare const calculateReductionRatio: (baselineDiff: DiffStats, testDiff: DiffStats) => number;
3
+ export declare const formatDiffStats: (diff: DiffStats) => string;
@@ -0,0 +1,11 @@
1
+ export const calculateReductionRatio = (baselineDiff, testDiff) => {
2
+ const baselineChangedLines = baselineDiff.addedLines + baselineDiff.removedLines;
3
+ const testChangedLines = testDiff.addedLines + testDiff.removedLines;
4
+ return baselineChangedLines === 0
5
+ ? 0
6
+ : 1 - testChangedLines / baselineChangedLines;
7
+ };
8
+ export const formatDiffStats = (diff) => {
9
+ const changedLines = diff.addedLines + diff.removedLines;
10
+ return `${changedLines.toLocaleString()} changed lines (${diff.changedHunks.toLocaleString()} hunks)`;
11
+ };
@@ -0,0 +1,21 @@
1
+ import type { ComparisonPair, EvaluationResult } from "./evaluation-types.js";
2
+ type EvaluatePairOptions = {
3
+ pair: ComparisonPair;
4
+ baselineTransforms: string[];
5
+ testTransforms: string[];
6
+ sourcesDirectory: string;
7
+ rootDirectory: string;
8
+ nodePath: string;
9
+ minireadCliPath: string;
10
+ diffPath: string;
11
+ pnpmPath: string | undefined;
12
+ prettierPath: string | undefined;
13
+ formatCode: boolean;
14
+ baselinePatchPath: string | undefined;
15
+ testPatchPath: string | undefined;
16
+ overwrite: boolean;
17
+ verbose: boolean;
18
+ log: (message: string) => void;
19
+ };
20
+ export declare const evaluatePair: (options: EvaluatePairOptions) => Promise<EvaluationResult>;
21
+ export {};
@@ -0,0 +1,104 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import envPaths from "env-paths";
4
+ import { computeDirectoryDiff } from "./diff-utilities.js";
5
+ import { calculateReductionRatio, formatDiffStats } from "./metrics.js";
6
+ import { formatDirectoryWithPrettier } from "./shell-utilities.js";
7
+ import { runPairTransformations } from "./run-pair-transformations.js";
8
+ const appPaths = envPaths("miniread");
9
+ export const evaluatePair = async (options) => {
10
+ const { pair, baselineTransforms, testTransforms, sourcesDirectory, rootDirectory, nodePath, minireadCliPath, diffPath, pnpmPath, prettierPath, formatCode, baselinePatchPath, testPatchPath, overwrite, verbose, log, } = options;
11
+ if (verbose) {
12
+ log(`Evaluating: ${pair.from} -> ${pair.to}`);
13
+ }
14
+ const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ const temporaryDirectory = path.join(appPaths.temp, `miniread-evaluate-${runId}`);
16
+ await fs.mkdir(temporaryDirectory, { recursive: true });
17
+ try {
18
+ if (verbose) {
19
+ log(`Running miniread with baseline [${baselineTransforms.join(", ") || "none"}] and test [${testTransforms.join(", ") || "none"}]...`);
20
+ }
21
+ const { baselineOutputFrom, baselineOutputTo, testOutputFrom, testOutputTo, } = await runPairTransformations({
22
+ pair,
23
+ baselineTransforms,
24
+ testTransforms,
25
+ sourcesDirectory,
26
+ temporaryDirectory,
27
+ rootDirectory,
28
+ nodePath,
29
+ minireadCliPath,
30
+ verbose,
31
+ });
32
+ if (formatCode) {
33
+ if (verbose)
34
+ log("Formatting output with Prettier...");
35
+ await Promise.all([
36
+ formatDirectoryWithPrettier({
37
+ directory: baselineOutputFrom,
38
+ rootDirectory,
39
+ pnpmPath,
40
+ prettierPath,
41
+ }),
42
+ formatDirectoryWithPrettier({
43
+ directory: baselineOutputTo,
44
+ rootDirectory,
45
+ pnpmPath,
46
+ prettierPath,
47
+ }),
48
+ formatDirectoryWithPrettier({
49
+ directory: testOutputFrom,
50
+ rootDirectory,
51
+ pnpmPath,
52
+ prettierPath,
53
+ }),
54
+ formatDirectoryWithPrettier({
55
+ directory: testOutputTo,
56
+ rootDirectory,
57
+ pnpmPath,
58
+ prettierPath,
59
+ }),
60
+ ]);
61
+ }
62
+ if (verbose)
63
+ log("Computing diffs...");
64
+ const [baselineDiff, testDiff] = await Promise.all([
65
+ computeDirectoryDiff({
66
+ diffPath,
67
+ directoryA: baselineOutputFrom,
68
+ directoryB: baselineOutputTo,
69
+ outputPath: baselinePatchPath,
70
+ overwrite,
71
+ }),
72
+ computeDirectoryDiff({
73
+ diffPath,
74
+ directoryA: testOutputFrom,
75
+ directoryB: testOutputTo,
76
+ outputPath: testPatchPath,
77
+ overwrite,
78
+ }),
79
+ ]);
80
+ if (verbose) {
81
+ log(`Baseline: ${formatDiffStats(baselineDiff)}`);
82
+ log(`Test: ${formatDiffStats(testDiff)}`);
83
+ }
84
+ const reductionRatio = calculateReductionRatio(baselineDiff, testDiff);
85
+ const percentReduction = (reductionRatio * 100).toFixed(2);
86
+ const status = reductionRatio >= 0 ? "GOOD" : "BAD";
87
+ if (verbose)
88
+ log(`Reduction ratio: ${percentReduction}% (${status})`);
89
+ return {
90
+ timestamp: new Date().toISOString(),
91
+ pair,
92
+ baselineTransforms,
93
+ testTransforms,
94
+ baselineDiff,
95
+ testDiff,
96
+ reductionRatio,
97
+ baselinePatchPath,
98
+ testPatchPath,
99
+ };
100
+ }
101
+ finally {
102
+ await fs.rm(temporaryDirectory, { recursive: true, force: true });
103
+ }
104
+ };
@@ -0,0 +1,40 @@
1
+ import type { ComparisonPair } from "./evaluation-types.js";
2
+ import type { OutputFormat } from "./create-evaluate-command.js";
3
+ export type EvaluateCliRawOptions = {
4
+ verbose?: boolean;
5
+ compare: string[];
6
+ baseline?: string;
7
+ test?: string;
8
+ sources: string;
9
+ evaluationDir: string;
10
+ format: OutputFormat;
11
+ writeMetrics?: boolean;
12
+ metricsFile?: string;
13
+ writePatches?: boolean;
14
+ updateManifest?: boolean;
15
+ manifestFile: string;
16
+ overwrite?: boolean;
17
+ formatCode: boolean;
18
+ };
19
+ export type EvaluateCliOptions = {
20
+ verbose: boolean;
21
+ pairs: ComparisonPair[];
22
+ baselineTransforms: string[];
23
+ testTransforms: string[];
24
+ sourcesDirectory: string;
25
+ evaluationDirectory: string;
26
+ format: OutputFormat;
27
+ shouldWriteMetrics: boolean;
28
+ shouldWritePatches: boolean;
29
+ shouldUpdateManifest: boolean;
30
+ metricsPath: string;
31
+ manifestPath: string;
32
+ overwrite: boolean;
33
+ formatCode: boolean;
34
+ baselineSlug: string;
35
+ testSlug: string;
36
+ };
37
+ export declare const parseEvaluateCliOptions: (options: {
38
+ cwd: string;
39
+ rawOptions: EvaluateCliRawOptions;
40
+ }) => EvaluateCliOptions;