miniread 1.7.0 → 1.9.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 (26) hide show
  1. package/bin/miniread-snapshot +17 -0
  2. package/dist/scripts/snapshot/create-snapshot-command.d.ts +9 -0
  3. package/dist/scripts/snapshot/create-snapshot-command.js +32 -0
  4. package/dist/scripts/snapshot/run-snapshot-cli.d.ts +1 -0
  5. package/dist/scripts/snapshot/run-snapshot-cli.js +162 -0
  6. package/dist/scripts/snapshot.d.ts +2 -0
  7. package/dist/scripts/snapshot.js +13 -0
  8. package/dist/transforms/rename-loop-index-variables-v2/get-loop-counter-name.d.ts +5 -0
  9. package/dist/transforms/rename-loop-index-variables-v2/get-loop-counter-name.js +123 -0
  10. package/dist/transforms/rename-loop-index-variables-v2/get-renamed-ancestor-loop-count.d.ts +3 -0
  11. package/dist/transforms/rename-loop-index-variables-v2/get-renamed-ancestor-loop-count.js +19 -0
  12. package/dist/transforms/rename-loop-index-variables-v2/get-target-index-base-name.d.ts +1 -0
  13. package/dist/transforms/rename-loop-index-variables-v2/get-target-index-base-name.js +6 -0
  14. package/dist/transforms/rename-loop-index-variables-v2/rename-loop-index-variables-v2-transform.d.ts +2 -0
  15. package/dist/transforms/rename-loop-index-variables-v2/rename-loop-index-variables-v2-transform.js +47 -0
  16. package/dist/transforms/rename-loop-index-variables-v3/get-loop-counter-name.d.ts +5 -0
  17. package/dist/transforms/rename-loop-index-variables-v3/get-loop-counter-name.js +121 -0
  18. package/dist/transforms/rename-loop-index-variables-v3/get-renamed-ancestor-loop-count.d.ts +3 -0
  19. package/dist/transforms/rename-loop-index-variables-v3/get-renamed-ancestor-loop-count.js +19 -0
  20. package/dist/transforms/rename-loop-index-variables-v3/get-target-index-base-name.d.ts +1 -0
  21. package/dist/transforms/rename-loop-index-variables-v3/get-target-index-base-name.js +6 -0
  22. package/dist/transforms/rename-loop-index-variables-v3/rename-loop-index-variables-v3-transform.d.ts +2 -0
  23. package/dist/transforms/rename-loop-index-variables-v3/rename-loop-index-variables-v3-transform.js +47 -0
  24. package/dist/transforms/transform-registry.js +4 -0
  25. package/package.json +4 -2
  26. package/transform-manifest.json +24 -2
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point that dynamically imports the compiled TypeScript.
4
+ *
5
+ * Uses top-level await to ensure module evaluation errors are handled
6
+ * properly. Without await, errors during import would surface as unhandled
7
+ * rejections instead of clean CLI failures with appropriate exit codes.
8
+ */
9
+ try {
10
+ await import("../dist/scripts/snapshot.js");
11
+ } catch (error) {
12
+ console.error(
13
+ "Failed to start miniread-snapshot:",
14
+ error instanceof Error ? error.message : error,
15
+ );
16
+ process.exitCode = 1;
17
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ export type SnapshotCliRawOptions = {
3
+ testcase: string;
4
+ transform: string;
5
+ expected: boolean;
6
+ };
7
+ export declare const createSnapshotCommand: (options: {
8
+ version: string;
9
+ }) => Command;
@@ -0,0 +1,32 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ export const createSnapshotCommand = (options) => {
3
+ const program = new Command()
4
+ .name("miniread-snapshot")
5
+ .description("Generate test case snapshots for transform development workflow.")
6
+ .version(options.version)
7
+ .showHelpAfterError("(add --help for additional information)")
8
+ .showSuggestionAfterError()
9
+ .requiredOption("--testcase <name>", "Test case directory name (e.g., boolean-literals)")
10
+ .requiredOption("--transform <id>", "Transform ID to run (e.g., expand-boolean-literals)")
11
+ .option("--expected", "Write {transform}-expected.js, compare with actual, delete expected on success", false);
12
+ program.addHelpText("after", `
13
+ Examples:
14
+ # Generate actual snapshot only (for updating existing snapshots)
15
+ pnpm run snapshot -- --testcase boolean-literals --transform expand-boolean-literals
16
+
17
+ # Full expected-file workflow (Design → Test phase)
18
+ pnpm run snapshot -- --testcase boolean-literals --transform expand-boolean-literals --expected
19
+ 1. Writes {transform}-expected.js (copy of base.js for manual editing)
20
+ 2. Writes {transform}.js (actual transform output)
21
+ 3. Runs diff between expected and actual
22
+ 4. Deletes expected.js on success (no diff)
23
+
24
+ Workflow:
25
+ 1. Create test-cases/{testcase}/base.js with your minified snippet
26
+ 2. Run with --expected to generate the expected file template
27
+ 3. Manually edit {transform}-expected.js to match your intent
28
+ 4. Re-run with --expected to compare actual output
29
+ 5. On success (no diff), expected file is auto-deleted
30
+ `);
31
+ return program;
32
+ };
@@ -0,0 +1 @@
1
+ export declare const runSnapshotCli: (argv: string[]) => Promise<number>;
@@ -0,0 +1,162 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import * as prettier from "prettier";
5
+ import packageJson from "../../../package.json" with { type: "json" };
6
+ import { buildProjectGraph } from "../../core/project-graph.js";
7
+ import { transformPresets } from "../../transforms/transform-presets.js";
8
+ import { transformRegistry } from "../../transforms/transform-registry.js";
9
+ import { createSnapshotCommand, } from "./create-snapshot-command.js";
10
+ const require = createRequire(import.meta.url);
11
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
12
+ const generate = require("@babel/generator").default;
13
+ const TEST_CASES_DIR = path.resolve(import.meta.dirname, "../../../test-cases");
14
+ const formatCode = async (code) => prettier.format(code, { parser: "babel" });
15
+ const runTransform = async (inputPath, transformId) => {
16
+ const content = await fs.readFile(inputPath, "utf8");
17
+ // Handle "recommended" as a special case that runs all recommended transforms
18
+ const transformIds = transformId === "recommended"
19
+ ? transformPresets.recommended
20
+ : [transformId];
21
+ const transforms = transformIds.map((id) => {
22
+ const transform = transformRegistry[id];
23
+ if (!transform) {
24
+ throw new Error(`Unknown transform: ${id}. Run 'miniread --list-transforms' to see available transforms.`);
25
+ }
26
+ return transform;
27
+ });
28
+ const projectGraph = buildProjectGraph([{ path: inputPath, content }]);
29
+ for (const transform of transforms) {
30
+ await transform.transform({
31
+ projectGraph,
32
+ currentFile: undefined,
33
+ options: {},
34
+ });
35
+ }
36
+ const fileInfo = projectGraph.files.get(inputPath);
37
+ if (!fileInfo) {
38
+ return content;
39
+ }
40
+ return generate(fileInfo.ast).code;
41
+ };
42
+ export const runSnapshotCli = async (argv) => {
43
+ const program = createSnapshotCommand({ version: packageJson.version });
44
+ program.parse(argv.filter((argument) => argument !== "--"));
45
+ const rawOptions = program.opts();
46
+ const { testcase, transform, expected } = rawOptions;
47
+ // Validate test case directory exists
48
+ const testCaseDirectory = path.join(TEST_CASES_DIR, testcase);
49
+ try {
50
+ const stats = await fs.stat(testCaseDirectory);
51
+ if (!stats.isDirectory()) {
52
+ console.error(`Error: ${testcase} is not a directory`);
53
+ return 1;
54
+ }
55
+ }
56
+ catch {
57
+ console.error(`Error: Test case directory not found: test-cases/${testcase}`);
58
+ console.error(`Create it first: mkdir -p test-cases/${testcase}`);
59
+ return 1;
60
+ }
61
+ // Validate base.js exists
62
+ const basePath = path.join(testCaseDirectory, "base.js");
63
+ try {
64
+ await fs.access(basePath);
65
+ }
66
+ catch {
67
+ console.error(`Error: base.js not found in test-cases/${testcase}/`);
68
+ console.error(`Create test-cases/${testcase}/base.js with your minified snippet first.`);
69
+ return 1;
70
+ }
71
+ const expectedPath = path.join(testCaseDirectory, `${transform}-expected.js`);
72
+ const actualPath = path.join(testCaseDirectory, `${transform}.js`);
73
+ // Run the transform and format
74
+ let actualOutput;
75
+ try {
76
+ const rawOutput = await runTransform(basePath, transform);
77
+ actualOutput = await formatCode(rawOutput);
78
+ }
79
+ catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.error(`Error running transform: ${message}`);
82
+ return 1;
83
+ }
84
+ if (expected) {
85
+ // Expected-file workflow
86
+ // Step 1: Check if expected file exists, create from base.js if not
87
+ let expectedExists = false;
88
+ try {
89
+ await fs.access(expectedPath);
90
+ expectedExists = true;
91
+ }
92
+ catch {
93
+ // Expected file doesn't exist, create it from base.js
94
+ const baseContent = await fs.readFile(basePath, "utf8");
95
+ const formattedBase = await formatCode(baseContent);
96
+ await fs.writeFile(expectedPath, formattedBase);
97
+ console.log(`Created: test-cases/${testcase}/${transform}-expected.js`);
98
+ console.log("Edit this file to match your expected output, then re-run.");
99
+ }
100
+ // Step 2: Write actual output
101
+ await fs.writeFile(actualPath, actualOutput);
102
+ console.log(`Created: test-cases/${testcase}/${transform}.js`);
103
+ if (!expectedExists) {
104
+ // First run - just created expected file, user needs to edit it
105
+ return 0;
106
+ }
107
+ // Step 3: Compare expected vs actual
108
+ const expectedContent = await fs.readFile(expectedPath, "utf8");
109
+ if (expectedContent === actualOutput) {
110
+ // Success! Delete expected file
111
+ await fs.unlink(expectedPath);
112
+ console.log(`\nSuccess: Output matches expected.`);
113
+ console.log(`Deleted: test-cases/${testcase}/${transform}-expected.js`);
114
+ console.log(`\nSnapshot ready: test-cases/${testcase}/${transform}.js`);
115
+ return 0;
116
+ }
117
+ else {
118
+ // Diff - show the difference
119
+ console.log(`\nDiff: expected vs actual`);
120
+ console.log("---");
121
+ // Simple line-by-line diff output
122
+ const expectedLines = expectedContent.split("\n");
123
+ const actualLines = actualOutput.split("\n");
124
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
125
+ let hasDiff = false;
126
+ for (let lineIndex = 0; lineIndex < maxLines; lineIndex++) {
127
+ const exp = expectedLines[lineIndex];
128
+ const act = actualLines[lineIndex];
129
+ if (exp !== act) {
130
+ hasDiff = true;
131
+ if (exp !== undefined && act === undefined) {
132
+ console.log(`Line ${lineIndex + 1}:`);
133
+ console.log(` - ${exp}`);
134
+ }
135
+ else if (exp === undefined && act !== undefined) {
136
+ console.log(`Line ${lineIndex + 1}:`);
137
+ console.log(` + ${act}`);
138
+ }
139
+ else {
140
+ console.log(`Line ${lineIndex + 1}:`);
141
+ console.log(` - ${exp}`);
142
+ console.log(` + ${act}`);
143
+ }
144
+ }
145
+ }
146
+ if (!hasDiff) {
147
+ // Shouldn't happen since we already checked equality, but just in case
148
+ console.log("(no visible differences)");
149
+ }
150
+ console.log("---");
151
+ console.log(`\nExpected file preserved: test-cases/${testcase}/${transform}-expected.js`);
152
+ console.log("Fix the implementation or update the expected file, then re-run.");
153
+ return 1;
154
+ }
155
+ }
156
+ else {
157
+ // Simple mode: just write actual output
158
+ await fs.writeFile(actualPath, actualOutput);
159
+ console.log(`Created: test-cases/${testcase}/${transform}.js`);
160
+ return 0;
161
+ }
162
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { runSnapshotCli } from "./snapshot/run-snapshot-cli.js";
3
+ try {
4
+ const exitCode = await runSnapshotCli(process.argv);
5
+ if (exitCode !== 0) {
6
+ process.exitCode = exitCode;
7
+ }
8
+ }
9
+ catch (error) {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ console.error(`Error: ${message}`);
12
+ process.exitCode = 1;
13
+ }
@@ -0,0 +1,5 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { ForStatement } from "@babel/types";
3
+ export declare const LOOP_INDEX_BASE_NAME = "index";
4
+ export declare const getStableRenamedIndexLoopCounterName: (path: NodePath<ForStatement>) => string | undefined;
5
+ export declare const getLoopCounterNameIfEligible: (path: NodePath<ForStatement>) => string | undefined;
@@ -0,0 +1,123 @@
1
+ import { isStableRenamed } from "../../core/stable-naming.js";
2
+ export const LOOP_INDEX_BASE_NAME = "index";
3
+ const isEligibleVariableDeclarationKind = (kind) => {
4
+ return kind === "let" || kind === "var";
5
+ };
6
+ const isStableRenamedIndexLoopCounterName = (name) => {
7
+ if (!isStableRenamed(name))
8
+ return false;
9
+ return name.startsWith(`$${LOOP_INDEX_BASE_NAME}`);
10
+ };
11
+ const isLoopCounterDeclarator = (loopCounterName, path) => {
12
+ const test = path.node.test;
13
+ if (!test)
14
+ return false;
15
+ if (test.type !== "BinaryExpression")
16
+ return false;
17
+ if (test.operator !== "<" && test.operator !== "<=")
18
+ return false;
19
+ if (test.left.type !== "Identifier")
20
+ return false;
21
+ if (test.left.name !== loopCounterName)
22
+ return false;
23
+ if (test.right.type !== "MemberExpression")
24
+ return false;
25
+ if (test.right.computed)
26
+ return false;
27
+ if (test.right.property.type !== "Identifier")
28
+ return false;
29
+ if (test.right.property.name !== "length")
30
+ return false;
31
+ const update = path.node.update;
32
+ if (!update)
33
+ return false;
34
+ if (update.type === "UpdateExpression") {
35
+ if (update.operator !== "++")
36
+ return false;
37
+ if (update.argument.type !== "Identifier")
38
+ return false;
39
+ if (update.argument.name !== loopCounterName)
40
+ return false;
41
+ }
42
+ else if (update.type === "AssignmentExpression") {
43
+ if (update.operator !== "+=")
44
+ return false;
45
+ if (update.left.type !== "Identifier")
46
+ return false;
47
+ if (update.left.name !== loopCounterName)
48
+ return false;
49
+ if (update.right.type !== "NumericLiteral")
50
+ return false;
51
+ if (update.right.value <= 0)
52
+ return false;
53
+ }
54
+ else {
55
+ return false;
56
+ }
57
+ return true;
58
+ };
59
+ export const getStableRenamedIndexLoopCounterName = (path) => {
60
+ const init = path.node.init;
61
+ if (!init)
62
+ return;
63
+ if (init.type !== "VariableDeclaration")
64
+ return;
65
+ if (!isEligibleVariableDeclarationKind(init.kind))
66
+ return;
67
+ if (init.declarations.length === 0)
68
+ return;
69
+ let stableName;
70
+ for (const declarator of init.declarations) {
71
+ if (declarator.id.type !== "Identifier")
72
+ continue;
73
+ const loopCounterName = declarator.id.name;
74
+ if (!isStableRenamedIndexLoopCounterName(loopCounterName))
75
+ continue;
76
+ if (!declarator.init)
77
+ continue;
78
+ if (declarator.init.type !== "NumericLiteral")
79
+ continue;
80
+ if (declarator.init.value !== 0)
81
+ continue;
82
+ if (!isLoopCounterDeclarator(loopCounterName, path))
83
+ continue;
84
+ if (stableName !== undefined)
85
+ return;
86
+ stableName = loopCounterName;
87
+ }
88
+ return stableName;
89
+ };
90
+ export const getLoopCounterNameIfEligible = (path) => {
91
+ const init = path.node.init;
92
+ if (!init)
93
+ return;
94
+ if (init.type !== "VariableDeclaration")
95
+ return;
96
+ if (!isEligibleVariableDeclarationKind(init.kind))
97
+ return;
98
+ if (init.declarations.length === 0)
99
+ return;
100
+ if (init.kind === "let" && init.declarations.length < 2)
101
+ return;
102
+ const eligibleNames = [];
103
+ for (const declarator of init.declarations) {
104
+ if (declarator.id.type !== "Identifier")
105
+ continue;
106
+ if (!declarator.init)
107
+ continue;
108
+ if (declarator.init.type !== "NumericLiteral")
109
+ continue;
110
+ if (declarator.init.value !== 0)
111
+ continue;
112
+ const loopCounterName = declarator.id.name;
113
+ if (!isLoopCounterDeclarator(loopCounterName, path))
114
+ continue;
115
+ eligibleNames.push(loopCounterName);
116
+ if (eligibleNames.length > 1)
117
+ return;
118
+ }
119
+ const name0 = eligibleNames[0];
120
+ if (!name0)
121
+ return;
122
+ return name0;
123
+ };
@@ -0,0 +1,3 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { ForStatement } from "@babel/types";
3
+ export declare const getRenamedAncestorLoopCount: (path: NodePath<ForStatement>, renamedLoops: WeakMap<ForStatement, string>) => number;
@@ -0,0 +1,19 @@
1
+ import { getStableRenamedIndexLoopCounterName } from "./get-loop-counter-name.js";
2
+ export const getRenamedAncestorLoopCount = (path, renamedLoops) => {
3
+ let count = 0;
4
+ for (const ancestor of path.getAncestry()) {
5
+ if (ancestor === path)
6
+ continue;
7
+ if (!ancestor.isForStatement())
8
+ continue;
9
+ if (renamedLoops.has(ancestor.node)) {
10
+ count++;
11
+ continue;
12
+ }
13
+ const stableName = getStableRenamedIndexLoopCounterName(ancestor);
14
+ if (!stableName)
15
+ continue;
16
+ count++;
17
+ }
18
+ return count;
19
+ };
@@ -0,0 +1 @@
1
+ export declare const getTargetIndexBaseName: (renamedAncestorLoopCount: number) => string;
@@ -0,0 +1,6 @@
1
+ import { LOOP_INDEX_BASE_NAME } from "./get-loop-counter-name.js";
2
+ export const getTargetIndexBaseName = (renamedAncestorLoopCount) => {
3
+ if (renamedAncestorLoopCount === 0)
4
+ return LOOP_INDEX_BASE_NAME;
5
+ return `${LOOP_INDEX_BASE_NAME}${renamedAncestorLoopCount + 1}`;
6
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renameLoopIndexVariablesV2Transform: Transform;
@@ -0,0 +1,47 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ import { getLoopCounterNameIfEligible } from "./get-loop-counter-name.js";
5
+ import { getRenamedAncestorLoopCount } from "./get-renamed-ancestor-loop-count.js";
6
+ import { getTargetIndexBaseName } from "./get-target-index-base-name.js";
7
+ const require = createRequire(import.meta.url);
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
9
+ const traverse = require("@babel/traverse").default;
10
+ export const renameLoopIndexVariablesV2Transform = {
11
+ id: "rename-loop-index-variables-v2",
12
+ description: "Renames numeric for-loop counters in `for (var ...)` (single or multi-declarator) and `for (let ...)` (multi-declarator) to $index/$index2/...",
13
+ scope: "file",
14
+ parallelizable: true,
15
+ transform(context) {
16
+ let nodesVisited = 0;
17
+ let transformationsApplied = 0;
18
+ for (const fileInfo of getFilesToProcess(context)) {
19
+ const group = new RenameGroup();
20
+ const eligibleLoops = new WeakMap();
21
+ traverse(fileInfo.ast, {
22
+ ForStatement(path) {
23
+ nodesVisited++;
24
+ const loopCounterName = getLoopCounterNameIfEligible(path);
25
+ if (!loopCounterName)
26
+ return;
27
+ // Skip already-stable names
28
+ if (isStableRenamed(loopCounterName))
29
+ return;
30
+ const renamedAncestorLoopCount = getRenamedAncestorLoopCount(path, eligibleLoops);
31
+ const baseName = getTargetIndexBaseName(renamedAncestorLoopCount);
32
+ eligibleLoops.set(path.node, baseName);
33
+ group.add({
34
+ scope: path.scope,
35
+ currentName: loopCounterName,
36
+ baseName,
37
+ });
38
+ },
39
+ });
40
+ transformationsApplied += group.apply();
41
+ }
42
+ return Promise.resolve({
43
+ nodesVisited,
44
+ transformationsApplied,
45
+ });
46
+ },
47
+ };
@@ -0,0 +1,5 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { ForStatement } from "@babel/types";
3
+ export declare const LOOP_INDEX_BASE_NAME = "index";
4
+ export declare const getStableRenamedIndexLoopCounterName: (path: NodePath<ForStatement>) => string | undefined;
5
+ export declare const getLoopCounterNameIfEligible: (path: NodePath<ForStatement>) => string | undefined;
@@ -0,0 +1,121 @@
1
+ import { isStableRenamed } from "../../core/stable-naming.js";
2
+ export const LOOP_INDEX_BASE_NAME = "index";
3
+ const isEligibleVariableDeclarationKind = (kind) => {
4
+ return kind === "let" || kind === "var";
5
+ };
6
+ const isStableRenamedIndexLoopCounterName = (name) => {
7
+ if (!isStableRenamed(name))
8
+ return false;
9
+ return name.startsWith(`$${LOOP_INDEX_BASE_NAME}`);
10
+ };
11
+ const isLoopCounterDeclarator = (loopCounterName, path) => {
12
+ const test = path.node.test;
13
+ if (!test)
14
+ return false;
15
+ if (test.type !== "BinaryExpression")
16
+ return false;
17
+ if (test.operator !== "<" && test.operator !== "<=")
18
+ return false;
19
+ if (test.left.type !== "Identifier")
20
+ return false;
21
+ if (test.left.name !== loopCounterName)
22
+ return false;
23
+ if (test.right.type !== "MemberExpression")
24
+ return false;
25
+ if (test.right.computed)
26
+ return false;
27
+ if (test.right.property.type !== "Identifier")
28
+ return false;
29
+ if (test.right.property.name !== "length")
30
+ return false;
31
+ const update = path.node.update;
32
+ if (!update)
33
+ return false;
34
+ if (update.type === "UpdateExpression") {
35
+ if (update.operator !== "++")
36
+ return false;
37
+ if (update.argument.type !== "Identifier")
38
+ return false;
39
+ if (update.argument.name !== loopCounterName)
40
+ return false;
41
+ }
42
+ else if (update.type === "AssignmentExpression") {
43
+ if (update.operator !== "+=")
44
+ return false;
45
+ if (update.left.type !== "Identifier")
46
+ return false;
47
+ if (update.left.name !== loopCounterName)
48
+ return false;
49
+ if (update.right.type !== "NumericLiteral")
50
+ return false;
51
+ if (update.right.value <= 0)
52
+ return false;
53
+ }
54
+ else {
55
+ return false;
56
+ }
57
+ return true;
58
+ };
59
+ export const getStableRenamedIndexLoopCounterName = (path) => {
60
+ const init = path.node.init;
61
+ if (!init)
62
+ return;
63
+ if (init.type !== "VariableDeclaration")
64
+ return;
65
+ if (!isEligibleVariableDeclarationKind(init.kind))
66
+ return;
67
+ if (init.declarations.length === 0)
68
+ return;
69
+ let stableName;
70
+ for (const declarator of init.declarations) {
71
+ if (declarator.id.type !== "Identifier")
72
+ continue;
73
+ const loopCounterName = declarator.id.name;
74
+ if (!isStableRenamedIndexLoopCounterName(loopCounterName))
75
+ continue;
76
+ if (!declarator.init)
77
+ continue;
78
+ if (declarator.init.type !== "NumericLiteral")
79
+ continue;
80
+ if (declarator.init.value !== 0)
81
+ continue;
82
+ if (!isLoopCounterDeclarator(loopCounterName, path))
83
+ continue;
84
+ if (stableName !== undefined)
85
+ return;
86
+ stableName = loopCounterName;
87
+ }
88
+ return stableName;
89
+ };
90
+ export const getLoopCounterNameIfEligible = (path) => {
91
+ const init = path.node.init;
92
+ if (!init)
93
+ return;
94
+ if (init.type !== "VariableDeclaration")
95
+ return;
96
+ if (!isEligibleVariableDeclarationKind(init.kind))
97
+ return;
98
+ if (init.declarations.length === 0)
99
+ return;
100
+ const eligibleNames = [];
101
+ for (const declarator of init.declarations) {
102
+ if (declarator.id.type !== "Identifier")
103
+ continue;
104
+ if (!declarator.init)
105
+ continue;
106
+ if (declarator.init.type !== "NumericLiteral")
107
+ continue;
108
+ if (declarator.init.value !== 0)
109
+ continue;
110
+ const loopCounterName = declarator.id.name;
111
+ if (!isLoopCounterDeclarator(loopCounterName, path))
112
+ continue;
113
+ eligibleNames.push(loopCounterName);
114
+ if (eligibleNames.length > 1)
115
+ return;
116
+ }
117
+ const name0 = eligibleNames[0];
118
+ if (!name0)
119
+ return;
120
+ return name0;
121
+ };
@@ -0,0 +1,3 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { ForStatement } from "@babel/types";
3
+ export declare const getRenamedAncestorLoopCount: (path: NodePath<ForStatement>, renamedLoops: WeakMap<ForStatement, string>) => number;
@@ -0,0 +1,19 @@
1
+ import { getStableRenamedIndexLoopCounterName } from "./get-loop-counter-name.js";
2
+ export const getRenamedAncestorLoopCount = (path, renamedLoops) => {
3
+ let count = 0;
4
+ for (const ancestor of path.getAncestry()) {
5
+ if (ancestor === path)
6
+ continue;
7
+ if (!ancestor.isForStatement())
8
+ continue;
9
+ if (renamedLoops.has(ancestor.node)) {
10
+ count++;
11
+ continue;
12
+ }
13
+ const stableName = getStableRenamedIndexLoopCounterName(ancestor);
14
+ if (!stableName)
15
+ continue;
16
+ count++;
17
+ }
18
+ return count;
19
+ };
@@ -0,0 +1 @@
1
+ export declare const getTargetIndexBaseName: (renamedAncestorLoopCount: number) => string;
@@ -0,0 +1,6 @@
1
+ import { LOOP_INDEX_BASE_NAME } from "./get-loop-counter-name.js";
2
+ export const getTargetIndexBaseName = (renamedAncestorLoopCount) => {
3
+ if (renamedAncestorLoopCount === 0)
4
+ return LOOP_INDEX_BASE_NAME;
5
+ return `${LOOP_INDEX_BASE_NAME}${renamedAncestorLoopCount + 1}`;
6
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renameLoopIndexVariablesV3Transform: Transform;
@@ -0,0 +1,47 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ import { getLoopCounterNameIfEligible } from "./get-loop-counter-name.js";
5
+ import { getRenamedAncestorLoopCount } from "./get-renamed-ancestor-loop-count.js";
6
+ import { getTargetIndexBaseName } from "./get-target-index-base-name.js";
7
+ const require = createRequire(import.meta.url);
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
9
+ const traverse = require("@babel/traverse").default;
10
+ export const renameLoopIndexVariablesV3Transform = {
11
+ id: "rename-loop-index-variables-v3",
12
+ description: "Renames numeric for-loop counters (single or multi-declarator `for (var/let ...)`) to $index/$index2/... based on nesting depth",
13
+ scope: "file",
14
+ parallelizable: true,
15
+ transform(context) {
16
+ let nodesVisited = 0;
17
+ let transformationsApplied = 0;
18
+ for (const fileInfo of getFilesToProcess(context)) {
19
+ const group = new RenameGroup();
20
+ const eligibleLoops = new WeakMap();
21
+ traverse(fileInfo.ast, {
22
+ ForStatement(path) {
23
+ nodesVisited++;
24
+ const loopCounterName = getLoopCounterNameIfEligible(path);
25
+ if (!loopCounterName)
26
+ return;
27
+ // Skip already-stable names
28
+ if (isStableRenamed(loopCounterName))
29
+ return;
30
+ const renamedAncestorLoopCount = getRenamedAncestorLoopCount(path, eligibleLoops);
31
+ const baseName = getTargetIndexBaseName(renamedAncestorLoopCount);
32
+ eligibleLoops.set(path.node, baseName);
33
+ group.add({
34
+ scope: path.scope,
35
+ currentName: loopCounterName,
36
+ baseName,
37
+ });
38
+ },
39
+ });
40
+ transformationsApplied += group.apply();
41
+ }
42
+ return Promise.resolve({
43
+ nodesVisited,
44
+ transformationsApplied,
45
+ });
46
+ },
47
+ };
@@ -6,6 +6,8 @@ import { renameCatchParametersTransform } from "./rename-catch-parameters/rename
6
6
  import { renameDestructuredAliasesTransform } from "./rename-destructured-aliases/rename-destructured-aliases-transform.js";
7
7
  import { renameEventParametersTransform } from "./rename-event-parameters/rename-event-parameters-transform.js";
8
8
  import { renameLoopIndexVariablesTransform } from "./rename-loop-index-variables/rename-loop-index-variables-transform.js";
9
+ import { renameLoopIndexVariablesV2Transform } from "./rename-loop-index-variables-v2/rename-loop-index-variables-v2-transform.js";
10
+ import { renameLoopIndexVariablesV3Transform } from "./rename-loop-index-variables-v3/rename-loop-index-variables-v3-transform.js";
9
11
  import { renamePromiseExecutorParametersTransform } from "./rename-promise-executor-parameters/rename-promise-executor-parameters-transform.js";
10
12
  import { renameTimeoutIdsTransform } from "./rename-timeout-ids/rename-timeout-ids-transform.js";
11
13
  import { renameUseReferenceGuardsTransform } from "./rename-use-reference-guards/rename-use-reference-guards-transform.js";
@@ -20,6 +22,8 @@ export const transformRegistry = {
20
22
  [renameDestructuredAliasesTransform.id]: renameDestructuredAliasesTransform,
21
23
  [renameEventParametersTransform.id]: renameEventParametersTransform,
22
24
  [renameLoopIndexVariablesTransform.id]: renameLoopIndexVariablesTransform,
25
+ [renameLoopIndexVariablesV2Transform.id]: renameLoopIndexVariablesV2Transform,
26
+ [renameLoopIndexVariablesV3Transform.id]: renameLoopIndexVariablesV3Transform,
23
27
  [renamePromiseExecutorParametersTransform.id]: renamePromiseExecutorParametersTransform,
24
28
  [renameTimeoutIdsTransform.id]: renameTimeoutIdsTransform,
25
29
  [renameUseReferenceGuardsTransform.id]: renameUseReferenceGuardsTransform,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "miniread",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.7.0",
5
+ "version": "1.9.0",
6
6
  "description": "Transform minified JavaScript/TypeScript into a more readable form using deterministic AST-based transforms.",
7
7
  "repository": {
8
8
  "type": "git",
@@ -24,7 +24,8 @@
24
24
  "bin": {
25
25
  "miniread": "bin/miniread",
26
26
  "miniread-evaluate": "bin/miniread-evaluate",
27
- "miniread-sample": "bin/miniread-sample"
27
+ "miniread-sample": "bin/miniread-sample",
28
+ "miniread-snapshot": "bin/miniread-snapshot"
28
29
  },
29
30
  "files": [
30
31
  "bin/",
@@ -47,6 +48,7 @@
47
48
  "miniread": "pnpm -s run rebuild && node bin/miniread",
48
49
  "miniread-evaluate": "pnpm -s run rebuild && node bin/miniread-evaluate",
49
50
  "miniread-sample": "pnpm -s run rebuild && node bin/miniread-sample",
51
+ "snapshot": "pnpm -s run rebuild && node bin/miniread-snapshot",
50
52
  "rebuild": "pnpm run clean && pnpm run build",
51
53
  "test": "vitest run",
52
54
  "test:coverage": "vitest run --coverage",
@@ -26,9 +26,31 @@
26
26
  "scope": "file",
27
27
  "parallelizable": true,
28
28
  "diffReductionImpact": 0,
29
- "recommended": true,
29
+ "recommended": false,
30
30
  "evaluatedAt": "2026-01-21T21:06:19.512Z",
31
- "notes": "Auto-added by evaluation script."
31
+ "notes": "Superseded by rename-loop-index-variables-v3.",
32
+ "supersededBy": "rename-loop-index-variables-v3"
33
+ },
34
+ {
35
+ "id": "rename-loop-index-variables-v2",
36
+ "description": "Renames numeric for-loop counters in `for (var ...)` (single or multi-declarator) and `for (let ...)` (multi-declarator) inits to index/index2/...",
37
+ "scope": "file",
38
+ "parallelizable": true,
39
+ "diffReductionImpact": 0,
40
+ "recommended": false,
41
+ "evaluatedAt": "2026-01-23T16:34:06.000Z",
42
+ "notes": "Superseded by rename-loop-index-variables-v3.",
43
+ "supersededBy": "rename-loop-index-variables-v3"
44
+ },
45
+ {
46
+ "id": "rename-loop-index-variables-v3",
47
+ "description": "Renames numeric for-loop counters (single or multi-declarator `for (var/let ...)`) to index/index2/... based on nesting depth",
48
+ "scope": "file",
49
+ "parallelizable": true,
50
+ "diffReductionImpact": 0,
51
+ "recommended": true,
52
+ "evaluatedAt": "2026-01-23T17:09:04.000Z",
53
+ "notes": "Unifies rename-loop-index-variables and rename-loop-index-variables-v2 without modifying older transforms. Measured with baseline none: 0.00%."
32
54
  },
33
55
  {
34
56
  "id": "rename-catch-parameters",