miniread 1.16.0 → 1.18.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.
@@ -0,0 +1 @@
1
+ export declare const formatCode: (code: string) => Promise<string>;
@@ -0,0 +1,4 @@
1
+ import * as prettier from "prettier";
2
+ export const formatCode = async (code) => {
3
+ return prettier.format(code, { parser: "babel" });
4
+ };
@@ -0,0 +1,6 @@
1
+ type PrintSnapshotDiffOptions = {
2
+ expectedContent: string;
3
+ actualOutput: string;
4
+ };
5
+ export declare const printSnapshotDiff: (options: PrintSnapshotDiffOptions) => void;
6
+ export {};
@@ -0,0 +1,26 @@
1
+ export const printSnapshotDiff = (options) => {
2
+ const { expectedContent, actualOutput } = options;
3
+ console.log(`\nDiff: expected vs actual`);
4
+ console.log("---");
5
+ const expectedLines = expectedContent.split("\n");
6
+ const actualLines = actualOutput.split("\n");
7
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
8
+ for (let lineIndex = 0; lineIndex < maxLines; lineIndex++) {
9
+ const expectedLine = expectedLines[lineIndex];
10
+ const actualLine = actualLines[lineIndex];
11
+ if (expectedLine === actualLine)
12
+ continue;
13
+ console.log(`Line ${lineIndex + 1}:`);
14
+ if (expectedLine !== undefined && actualLine === undefined) {
15
+ console.log(` - ${expectedLine}`);
16
+ continue;
17
+ }
18
+ if (expectedLine === undefined && actualLine !== undefined) {
19
+ console.log(` + ${actualLine}`);
20
+ continue;
21
+ }
22
+ console.log(` - ${expectedLine}`);
23
+ console.log(` + ${actualLine}`);
24
+ }
25
+ console.log("---");
26
+ };
@@ -0,0 +1,16 @@
1
+ type ResolveTestcaseInputOptions = {
2
+ testcase: string;
3
+ };
4
+ type ResolveTestcaseInputValue = {
5
+ testcaseDirectory: string;
6
+ basePath: string;
7
+ };
8
+ type Result<T> = {
9
+ ok: true;
10
+ value: T;
11
+ } | {
12
+ ok: false;
13
+ error: Error;
14
+ };
15
+ export declare const resolveTestcaseInput: (options: ResolveTestcaseInputOptions) => Promise<Result<ResolveTestcaseInputValue>>;
16
+ export {};
@@ -0,0 +1,30 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const TEST_CASES_DIR = path.resolve(import.meta.dirname, "../../../test-cases");
4
+ export const resolveTestcaseInput = async (options) => {
5
+ const { testcase } = options;
6
+ const testcaseDirectory = path.join(TEST_CASES_DIR, testcase);
7
+ try {
8
+ const stats = await fs.stat(testcaseDirectory);
9
+ if (!stats.isDirectory()) {
10
+ return { ok: false, error: new Error(`${testcase} is not a directory`) };
11
+ }
12
+ }
13
+ catch {
14
+ return {
15
+ ok: false,
16
+ error: new Error(`Test case directory not found: test-cases/${testcase}`),
17
+ };
18
+ }
19
+ const basePath = path.join(testcaseDirectory, "base.js");
20
+ try {
21
+ await fs.access(basePath);
22
+ }
23
+ catch {
24
+ return {
25
+ ok: false,
26
+ error: new Error(`base.js not found in test-cases/${testcase}/`),
27
+ };
28
+ }
29
+ return { ok: true, value: { testcaseDirectory, basePath } };
30
+ };
@@ -1,75 +1,39 @@
1
1
  import * as fs from "node:fs/promises";
2
- import { createRequire } from "node:module";
3
2
  import path from "node:path";
4
- import * as prettier from "prettier";
5
3
  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";
4
+ import { formatCode } from "./format-code.js";
9
5
  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
- };
6
+ import { printSnapshotDiff } from "./print-snapshot-diff.js";
7
+ import { resolveTestcaseInput } from "./resolve-testcase-input.js";
8
+ import { runTestcaseTransform } from "./run-testcase-transform.js";
42
9
  export const runSnapshotCli = async (argv) => {
43
10
  const program = createSnapshotCommand({ version: packageJson.version });
44
11
  program.parse(argv.filter((argument) => argument !== "--"));
45
12
  const rawOptions = program.opts();
46
13
  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`);
14
+ const resolved = await resolveTestcaseInput({ testcase });
15
+ if (!resolved.ok) {
16
+ const message = resolved.error.message;
17
+ if (message === `${testcase} is not a directory`) {
18
+ console.error(`Error: ${message}`);
53
19
  return 1;
54
20
  }
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.`);
21
+ if (message.startsWith("Test case directory not found:")) {
22
+ console.error(`Error: ${message}`);
23
+ console.error(`Create it first: mkdir -p test-cases/${testcase}`);
24
+ return 1;
25
+ }
26
+ if (message.startsWith("base.js not found in test-cases/")) {
27
+ console.error(`Error: ${message}`);
28
+ console.error(`Create test-cases/${testcase}/base.js with your minified snippet first.`);
29
+ return 1;
30
+ }
31
+ console.error(`Error: ${message}`);
69
32
  return 1;
70
33
  }
71
- const expectedPath = path.join(testCaseDirectory, `${transform}-expected.js`);
72
- const actualPath = path.join(testCaseDirectory, `${transform}.js`);
34
+ const { testcaseDirectory, basePath } = resolved.value;
35
+ const expectedPath = path.join(testcaseDirectory, `${transform}-expected.js`);
36
+ const actualPath = path.join(testcaseDirectory, `${transform}.js`);
73
37
  if (expected) {
74
38
  // Expected-file workflow
75
39
  // Step 1: Check if expected file exists, create from base.js if not
@@ -83,69 +47,53 @@ export const runSnapshotCli = async (argv) => {
83
47
  await fs.writeFile(expectedPath, formattedBase);
84
48
  console.log(`Created: test-cases/${testcase}/${transform}-expected.js`);
85
49
  console.log("Edit this file to match your expected output, then re-run.");
50
+ // Design phase: allow creating the expected file before the transform exists.
51
+ // The next run (after implementing the transform) will write the actual output
52
+ // and compare it against this expected snapshot.
86
53
  return 0;
87
54
  }
88
- }
89
- // Common path: Run the transform, format, and write actual output
90
- let actualOutput;
91
- try {
92
- const rawOutput = await runTransform(basePath, transform);
93
- actualOutput = await formatCode(rawOutput);
94
- }
95
- catch (error) {
96
- const message = error instanceof Error ? error.message : String(error);
97
- console.error(`Error running transform: ${message}`);
98
- return 1;
99
- }
100
- await fs.writeFile(actualPath, actualOutput);
101
- console.log(`Created: test-cases/${testcase}/${transform}.js`);
102
- if (!expected) {
103
- return 0;
104
- }
105
- // Step 3: Compare expected vs actual
106
- const expectedContent = await fs.readFile(expectedPath, "utf8");
107
- if (expectedContent === actualOutput) {
108
- // Success! Delete expected file
109
- await fs.unlink(expectedPath);
110
- console.log(`\nSuccess: Output matches expected.`);
111
- console.log(`Deleted: test-cases/${testcase}/${transform}-expected.js`);
112
- console.log(`\nSnapshot ready: test-cases/${testcase}/${transform}.js`);
113
- return 0;
114
- }
115
- // Diff - show the difference
116
- console.log(`\nDiff: expected vs actual`);
117
- console.log("---");
118
- // Simple line-by-line diff output
119
- const expectedLines = expectedContent.split("\n");
120
- const actualLines = actualOutput.split("\n");
121
- const maxLines = Math.max(expectedLines.length, actualLines.length);
122
- let hasDiff = false;
123
- for (let lineIndex = 0; lineIndex < maxLines; lineIndex++) {
124
- const exp = expectedLines[lineIndex];
125
- const act = actualLines[lineIndex];
126
- if (exp !== act) {
127
- hasDiff = true;
128
- if (exp !== undefined && act === undefined) {
129
- console.log(`Line ${lineIndex + 1}:`);
130
- console.log(` - ${exp}`);
131
- }
132
- else if (exp === undefined && act !== undefined) {
133
- console.log(`Line ${lineIndex + 1}:`);
134
- console.log(` + ${act}`);
135
- }
136
- else {
137
- console.log(`Line ${lineIndex + 1}:`);
138
- console.log(` - ${exp}`);
139
- console.log(` + ${act}`);
140
- }
55
+ const outputResult = await runTestcaseTransform({
56
+ inputPath: basePath,
57
+ transformId: transform,
58
+ });
59
+ if (!outputResult.ok) {
60
+ console.error(`Error running transform: ${outputResult.error.message}`);
61
+ return 1;
62
+ }
63
+ const actualOutput = outputResult.value;
64
+ // Step 2: Write actual output
65
+ await fs.writeFile(actualPath, actualOutput);
66
+ console.log(`Created: test-cases/${testcase}/${transform}.js`);
67
+ // Step 3: Compare expected vs actual
68
+ const expectedContent = await fs.readFile(expectedPath, "utf8");
69
+ if (expectedContent === actualOutput) {
70
+ // Success! Delete expected file
71
+ await fs.unlink(expectedPath);
72
+ console.log(`\nSuccess: Output matches expected.`);
73
+ console.log(`Deleted: test-cases/${testcase}/${transform}-expected.js`);
74
+ console.log(`\nSnapshot ready: test-cases/${testcase}/${transform}.js`);
75
+ return 0;
76
+ }
77
+ else {
78
+ printSnapshotDiff({ expectedContent, actualOutput });
79
+ console.log(`\nExpected file preserved: test-cases/${testcase}/${transform}-expected.js`);
80
+ console.log("Fix the implementation or update the expected file, then re-run.");
81
+ return 1;
141
82
  }
142
83
  }
143
- if (!hasDiff) {
144
- // Shouldn't happen since we already checked equality, but just in case
145
- console.log("(no visible differences)");
84
+ else {
85
+ const outputResult = await runTestcaseTransform({
86
+ inputPath: basePath,
87
+ transformId: transform,
88
+ });
89
+ if (!outputResult.ok) {
90
+ console.error(`Error running transform: ${outputResult.error.message}`);
91
+ return 1;
92
+ }
93
+ const actualOutput = outputResult.value;
94
+ // Simple mode: just write actual output
95
+ await fs.writeFile(actualPath, actualOutput);
96
+ console.log(`Created: test-cases/${testcase}/${transform}.js`);
97
+ return 0;
146
98
  }
147
- console.log("---");
148
- console.log(`\nExpected file preserved: test-cases/${testcase}/${transform}-expected.js`);
149
- console.log("Fix the implementation or update the expected file, then re-run.");
150
- return 1;
151
99
  };
@@ -0,0 +1,13 @@
1
+ type Result<T> = {
2
+ ok: true;
3
+ value: T;
4
+ } | {
5
+ ok: false;
6
+ error: Error;
7
+ };
8
+ type RunTestcaseTransformOptions = {
9
+ inputPath: string;
10
+ transformId: string;
11
+ };
12
+ export declare const runTestcaseTransform: (options: RunTestcaseTransformOptions) => Promise<Result<string>>;
13
+ export {};
@@ -0,0 +1,46 @@
1
+ import { createRequire } from "node:module";
2
+ import * as fs from "node:fs/promises";
3
+ import { buildProjectGraph } from "../../core/project-graph.js";
4
+ import { transformPresets } from "../../transforms/transform-presets.js";
5
+ import { transformRegistry } from "../../transforms/transform-registry.js";
6
+ import { formatCode } from "./format-code.js";
7
+ const require = createRequire(import.meta.url);
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
9
+ const generate = require("@babel/generator").default;
10
+ const runTransform = async (inputPath, transformId) => {
11
+ const content = await fs.readFile(inputPath, "utf8");
12
+ const transformIds = transformId === "recommended"
13
+ ? transformPresets.recommended
14
+ : [transformId];
15
+ const transforms = transformIds.map((id) => {
16
+ const transform = transformRegistry[id];
17
+ if (!transform) {
18
+ throw new Error(`Unknown transform: ${id}. Run 'miniread --list-transforms' to see available transforms.`);
19
+ }
20
+ return transform;
21
+ });
22
+ const projectGraph = buildProjectGraph([{ path: inputPath, content }]);
23
+ for (const transform of transforms) {
24
+ await transform.transform({
25
+ projectGraph,
26
+ currentFile: undefined,
27
+ options: {},
28
+ });
29
+ }
30
+ const fileInfo = projectGraph.files.get(inputPath);
31
+ if (!fileInfo)
32
+ return content;
33
+ return generate(fileInfo.ast).code;
34
+ };
35
+ export const runTestcaseTransform = async (options) => {
36
+ const { inputPath, transformId } = options;
37
+ try {
38
+ const rawOutput = await runTransform(inputPath, transformId);
39
+ const output = await formatCode(rawOutput);
40
+ return { ok: true, value: output };
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ return { ok: false, error: new Error(message) };
45
+ }
46
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renameCharcodeVariablesTransform: Transform;
@@ -0,0 +1,67 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ const require = createRequire(import.meta.url);
5
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
6
+ const traverse = require("@babel/traverse").default;
7
+ const BASE_NAME = "charCode";
8
+ /**
9
+ * Detects if the init is a call to .charCodeAt()
10
+ * e.g., `var a = str.charCodeAt(0)`
11
+ */
12
+ const isCharCodeAtCall = (path) => {
13
+ const init = path.node.init;
14
+ if (init?.type !== "CallExpression")
15
+ return false;
16
+ const callee = init.callee;
17
+ if (callee.type !== "MemberExpression")
18
+ return false;
19
+ const property = callee.property;
20
+ if (property.type !== "Identifier")
21
+ return false;
22
+ if (property.name !== "charCodeAt")
23
+ return false;
24
+ // If charCodeAt is accessed via computed property, skip
25
+ if (callee.computed)
26
+ return false;
27
+ return true;
28
+ };
29
+ export const renameCharcodeVariablesTransform = {
30
+ id: "rename-charcode-variables",
31
+ description: "Renames variables assigned from .charCodeAt() to $charCode or charCode/charCode2/...",
32
+ scope: "file",
33
+ parallelizable: true,
34
+ transform(context) {
35
+ let nodesVisited = 0;
36
+ let transformationsApplied = 0;
37
+ for (const fileInfo of getFilesToProcess(context)) {
38
+ const group = new RenameGroup();
39
+ traverse(fileInfo.ast, {
40
+ VariableDeclarator(path) {
41
+ nodesVisited++;
42
+ const id = path.node.id;
43
+ if (id.type !== "Identifier")
44
+ return;
45
+ // Skip already-stable names
46
+ if (isStableRenamed(id.name))
47
+ return;
48
+ if (!isCharCodeAtCall(path))
49
+ return;
50
+ const binding = path.scope.getBinding(id.name);
51
+ if (!binding)
52
+ return;
53
+ // Skip if the variable is reassigned
54
+ if (!binding.constant)
55
+ return;
56
+ group.add({
57
+ scope: path.scope,
58
+ currentName: id.name,
59
+ baseName: BASE_NAME,
60
+ });
61
+ },
62
+ });
63
+ transformationsApplied += group.apply();
64
+ }
65
+ return Promise.resolve({ nodesVisited, transformationsApplied });
66
+ },
67
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renameCharcodeVariablesV2Transform: Transform;
@@ -0,0 +1,135 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ const require = createRequire(import.meta.url);
5
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
6
+ const traverse = require("@babel/traverse").default;
7
+ /**
8
+ * Capitalizes the first letter of a string.
9
+ */
10
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
11
+ const MAX_RECURSION_DEPTH = 5;
12
+ /**
13
+ * Converts a charCodeAt argument expression to a suffix for the variable name.
14
+ * Returns undefined if the expression is too complex to represent.
15
+ *
16
+ * Examples:
17
+ * - `0` → "0"
18
+ * - `index` → "Index"
19
+ * - `index + 1` → "IndexPlus1"
20
+ * - `i` → "I"
21
+ * - `pos` → "Pos"
22
+ */
23
+ const getArgumentSuffix = (expression, depth = 0) => {
24
+ // Prevent stack overflow from deeply nested expressions
25
+ if (depth > MAX_RECURSION_DEPTH)
26
+ return undefined;
27
+ // Numeric literal: charCodeAt(0) → "0"
28
+ if (expression.type === "NumericLiteral") {
29
+ if (Number.isInteger(expression.value) && expression.value >= 0) {
30
+ return String(expression.value);
31
+ }
32
+ return undefined;
33
+ }
34
+ // Identifier: charCodeAt(index) → "Index"
35
+ if (expression.type === "Identifier") {
36
+ return capitalize(expression.name);
37
+ }
38
+ // Binary expression with +: charCodeAt(index + 1) → "IndexPlus1"
39
+ if (expression.type === "BinaryExpression") {
40
+ // Skip if left is a PrivateName (can happen in class fields)
41
+ if (expression.left.type === "PrivateName")
42
+ return undefined;
43
+ if (expression.operator === "+") {
44
+ const leftSuffix = getArgumentSuffix(expression.left, depth + 1);
45
+ const rightSuffix = getArgumentSuffix(expression.right, depth + 1);
46
+ if (leftSuffix !== undefined && rightSuffix !== undefined) {
47
+ return `${leftSuffix}Plus${rightSuffix}`;
48
+ }
49
+ }
50
+ if (expression.operator === "-") {
51
+ const leftSuffix = getArgumentSuffix(expression.left, depth + 1);
52
+ const rightSuffix = getArgumentSuffix(expression.right, depth + 1);
53
+ if (leftSuffix !== undefined && rightSuffix !== undefined) {
54
+ return `${leftSuffix}Minus${rightSuffix}`;
55
+ }
56
+ }
57
+ }
58
+ return undefined;
59
+ };
60
+ /**
61
+ * Detects if the init is a call to .charCodeAt() and extracts the argument.
62
+ * Returns the argument expression if valid, undefined otherwise.
63
+ */
64
+ const getCharCodeAtArgument = (path) => {
65
+ const init = path.node.init;
66
+ if (init?.type !== "CallExpression")
67
+ return undefined;
68
+ const callee = init.callee;
69
+ if (callee.type !== "MemberExpression")
70
+ return undefined;
71
+ const property = callee.property;
72
+ if (property.type !== "Identifier")
73
+ return undefined;
74
+ if (property.name !== "charCodeAt")
75
+ return undefined;
76
+ // If charCodeAt is accessed via computed property, skip
77
+ if (callee.computed)
78
+ return undefined;
79
+ // Must have exactly one argument
80
+ if (init.arguments.length !== 1)
81
+ return undefined;
82
+ const firstArgument = init.arguments[0];
83
+ if (!firstArgument)
84
+ return undefined;
85
+ if (firstArgument.type === "SpreadElement")
86
+ return undefined;
87
+ if (firstArgument.type === "ArgumentPlaceholder")
88
+ return undefined;
89
+ return firstArgument;
90
+ };
91
+ export const renameCharcodeVariablesV2Transform = {
92
+ id: "rename-charcode-variables-v2",
93
+ description: "Renames variables assigned from .charCodeAt(arg) to $charCodeAt{Arg} based on the argument",
94
+ scope: "file",
95
+ parallelizable: true,
96
+ transform(context) {
97
+ let nodesVisited = 0;
98
+ let transformationsApplied = 0;
99
+ for (const fileInfo of getFilesToProcess(context)) {
100
+ const group = new RenameGroup();
101
+ traverse(fileInfo.ast, {
102
+ VariableDeclarator(path) {
103
+ nodesVisited++;
104
+ const id = path.node.id;
105
+ if (id.type !== "Identifier")
106
+ return;
107
+ // Skip already-stable names
108
+ if (isStableRenamed(id.name))
109
+ return;
110
+ const argument = getCharCodeAtArgument(path);
111
+ if (argument === undefined)
112
+ return;
113
+ const binding = path.scope.getBinding(id.name);
114
+ if (!binding)
115
+ return;
116
+ // Skip if the variable is reassigned
117
+ if (!binding.constant)
118
+ return;
119
+ // Try to derive a suffix from the argument
120
+ const suffix = getArgumentSuffix(argument);
121
+ if (suffix === undefined)
122
+ return;
123
+ const baseName = `charCodeAt${suffix}`;
124
+ group.add({
125
+ scope: path.scope,
126
+ currentName: id.name,
127
+ baseName,
128
+ });
129
+ },
130
+ });
131
+ transformationsApplied += group.apply();
132
+ }
133
+ return Promise.resolve({ nodesVisited, transformationsApplied });
134
+ },
135
+ };
@@ -0,0 +1,2 @@
1
+ import type { Transform } from "../../core/types.js";
2
+ export declare const simplifyBooleanNegationsTransform: Transform;
@@ -0,0 +1,90 @@
1
+ import { createRequire } from "node:module";
2
+ import { getFilesToProcess } from "../../core/types.js";
3
+ const require = createRequire(import.meta.url);
4
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
5
+ const traverse = require("@babel/traverse").default;
6
+ const t = require("@babel/types");
7
+ const isInBooleanTestContext = (path) => {
8
+ let current = path;
9
+ // A `!!x` node is safe to rewrite as `x` only when its value is consumed
10
+ // purely for truthiness (control-flow tests and boolean operator chains).
11
+ // If we encounter any other parent expression before reaching the test
12
+ // boundary, the expression value is being used (e.g. as a call argument),
13
+ // and rewriting would be unsafe.
14
+ for (;;) {
15
+ const parent = current.parentPath;
16
+ if (!parent)
17
+ return false;
18
+ if ((parent.isIfStatement() ||
19
+ parent.isWhileStatement() ||
20
+ parent.isDoWhileStatement() ||
21
+ parent.isForStatement() ||
22
+ parent.isConditionalExpression()) &&
23
+ current.key === "test") {
24
+ return true;
25
+ }
26
+ if (parent.isLogicalExpression()) {
27
+ if (current.key !== "left" && current.key !== "right")
28
+ return false;
29
+ current = parent;
30
+ continue;
31
+ }
32
+ if (parent.isUnaryExpression({ operator: "!" })) {
33
+ if (current.key !== "argument")
34
+ return false;
35
+ current = parent;
36
+ continue;
37
+ }
38
+ if (parent.isSequenceExpression()) {
39
+ if (current.listKey !== "expressions")
40
+ return false;
41
+ const expressions = parent.node.expressions;
42
+ const lastExpression = expressions.at(-1);
43
+ if (!lastExpression)
44
+ return false;
45
+ if (lastExpression !== current.node)
46
+ return false;
47
+ current = parent;
48
+ continue;
49
+ }
50
+ if (parent.isParenthesizedExpression()) {
51
+ if (current.key !== "expression")
52
+ return false;
53
+ current = parent;
54
+ continue;
55
+ }
56
+ return false;
57
+ }
58
+ };
59
+ export const simplifyBooleanNegationsTransform = {
60
+ id: "simplify-boolean-negations",
61
+ description: "Simplifies redundant boolean negations: !true/!false and !!x in control-flow tests",
62
+ scope: "file",
63
+ parallelizable: true,
64
+ transform(context) {
65
+ let nodesVisited = 0;
66
+ let transformationsApplied = 0;
67
+ for (const fileInfo of getFilesToProcess(context)) {
68
+ traverse(fileInfo.ast, {
69
+ UnaryExpression(path) {
70
+ nodesVisited++;
71
+ if (path.node.operator !== "!")
72
+ return;
73
+ const argument = path.node.argument;
74
+ if (argument.type === "BooleanLiteral") {
75
+ path.replaceWith(t.booleanLiteral(!argument.value));
76
+ transformationsApplied++;
77
+ return;
78
+ }
79
+ if (argument.type === "UnaryExpression" &&
80
+ argument.operator === "!" &&
81
+ isInBooleanTestContext(path)) {
82
+ path.replaceWith(argument.argument);
83
+ transformationsApplied++;
84
+ }
85
+ },
86
+ });
87
+ }
88
+ return Promise.resolve({ nodesVisited, transformationsApplied });
89
+ },
90
+ };
@@ -5,23 +5,25 @@ import { expandSequenceExpressionsV5Transform } from "./expand-sequence-expressi
5
5
  import { expandUndefinedLiteralsTransform } from "./expand-undefined-literals/expand-undefined-literals-transform.js";
6
6
  import { removeRedundantElseTransform } from "./remove-redundant-else/remove-redundant-else-transform.js";
7
7
  import { renameCatchParametersTransform } from "./rename-catch-parameters/rename-catch-parameters-transform.js";
8
+ import { renameCharCodeAtTransform } from "./rename-char-code-at/rename-char-code-at-transform.js";
9
+ import { renameCharcodeVariablesTransform } from "./rename-charcode-variables/rename-charcode-variables-transform.js";
10
+ import { renameCharcodeVariablesV2Transform } from "./rename-charcode-variables-v2/rename-charcode-variables-v2-transform.js";
8
11
  import { renameComparisonFlagsTransform } from "./rename-comparison-flags/rename-comparison-flags-transform.js";
9
12
  import { renameDestructuredAliasesTransform } from "./rename-destructured-aliases/rename-destructured-aliases-transform.js";
10
13
  import { renameEventParametersTransform } from "./rename-event-parameters/rename-event-parameters-transform.js";
11
14
  import { renameLoopIndexVariablesTransform } from "./rename-loop-index-variables/rename-loop-index-variables-transform.js";
12
15
  import { renameLoopIndexVariablesV2Transform } from "./rename-loop-index-variables-v2/rename-loop-index-variables-v2-transform.js";
13
16
  import { renameLoopIndexVariablesV3Transform } from "./rename-loop-index-variables-v3/rename-loop-index-variables-v3-transform.js";
17
+ import { renameParametersToMatchPropertiesTransform } from "./rename-parameters-to-match-properties/rename-parameters-to-match-properties-transform.js";
14
18
  import { renamePromiseExecutorParametersTransform } from "./rename-promise-executor-parameters/rename-promise-executor-parameters-transform.js";
15
19
  import { renameReplaceChildParametersTransform } from "./rename-replace-child-parameters/rename-replace-child-parameters-transform.js";
16
20
  import { renameThisAliasesTransform } from "./rename-this-aliases/rename-this-aliases-transform.js";
17
21
  import { renameTimeoutIdsTransform } from "./rename-timeout-ids/rename-timeout-ids-transform.js";
18
22
  import { renameUseReferenceGuardsTransform } from "./rename-use-reference-guards/rename-use-reference-guards-transform.js";
19
23
  import { renameUseReferenceGuardsV2Transform } from "./rename-use-reference-guards-v2/rename-use-reference-guards-v2-transform.js";
24
+ import { simplifyBooleanNegationsTransform } from "./simplify-boolean-negations/simplify-boolean-negations-transform.js";
20
25
  import { splitVariableDeclarationsTransform } from "./split-variable-declarations/split-variable-declarations-transform.js";
21
- import { renameCharCodeAtTransform } from "./rename-char-code-at/rename-char-code-at-transform.js";
22
- import { renameParametersToMatchPropertiesTransform } from "./rename-parameters-to-match-properties/rename-parameters-to-match-properties-transform.js";
23
26
  export const transformRegistry = {
24
- [renameCharCodeAtTransform.id]: renameCharCodeAtTransform,
25
27
  [expandBooleanLiteralsTransform.id]: expandBooleanLiteralsTransform,
26
28
  [expandSpecialNumberLiteralsTransform.id]: expandSpecialNumberLiteralsTransform,
27
29
  [expandSequenceExpressionsV4Transform.id]: expandSequenceExpressionsV4Transform,
@@ -29,19 +31,23 @@ export const transformRegistry = {
29
31
  [expandUndefinedLiteralsTransform.id]: expandUndefinedLiteralsTransform,
30
32
  [removeRedundantElseTransform.id]: removeRedundantElseTransform,
31
33
  [renameCatchParametersTransform.id]: renameCatchParametersTransform,
34
+ [renameCharCodeAtTransform.id]: renameCharCodeAtTransform,
35
+ [renameCharcodeVariablesTransform.id]: renameCharcodeVariablesTransform,
36
+ [renameCharcodeVariablesV2Transform.id]: renameCharcodeVariablesV2Transform,
32
37
  [renameComparisonFlagsTransform.id]: renameComparisonFlagsTransform,
33
38
  [renameDestructuredAliasesTransform.id]: renameDestructuredAliasesTransform,
34
39
  [renameEventParametersTransform.id]: renameEventParametersTransform,
35
40
  [renameLoopIndexVariablesTransform.id]: renameLoopIndexVariablesTransform,
36
41
  [renameLoopIndexVariablesV2Transform.id]: renameLoopIndexVariablesV2Transform,
37
42
  [renameLoopIndexVariablesV3Transform.id]: renameLoopIndexVariablesV3Transform,
43
+ [renameParametersToMatchPropertiesTransform.id]: renameParametersToMatchPropertiesTransform,
38
44
  [renamePromiseExecutorParametersTransform.id]: renamePromiseExecutorParametersTransform,
39
45
  [renameReplaceChildParametersTransform.id]: renameReplaceChildParametersTransform,
40
46
  [renameThisAliasesTransform.id]: renameThisAliasesTransform,
41
47
  [renameTimeoutIdsTransform.id]: renameTimeoutIdsTransform,
42
48
  [renameUseReferenceGuardsTransform.id]: renameUseReferenceGuardsTransform,
43
49
  [renameUseReferenceGuardsV2Transform.id]: renameUseReferenceGuardsV2Transform,
50
+ [simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
44
51
  [splitVariableDeclarationsTransform.id]: splitVariableDeclarationsTransform,
45
- [renameParametersToMatchPropertiesTransform.id]: renameParametersToMatchPropertiesTransform,
46
52
  };
47
53
  export const allTransformIds = Object.keys(transformRegistry);
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.16.0",
5
+ "version": "1.18.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",
@@ -10,6 +10,15 @@
10
10
  "evaluatedAt": "2026-01-21T15:01:23.708Z",
11
11
  "notes": "Improves readability but does not reduce diffs (boolean literals are already deterministic)"
12
12
  },
13
+ {
14
+ "id": "simplify-boolean-negations",
15
+ "description": "Simplifies redundant boolean negations: !true/!false and !!x in control-flow tests",
16
+ "scope": "file",
17
+ "parallelizable": true,
18
+ "diffReductionImpact": 0,
19
+ "recommended": true,
20
+ "notes": "Added manually; improves readability and cleans up `expand-boolean-literals` output."
21
+ },
13
22
  {
14
23
  "id": "expand-undefined-literals",
15
24
  "description": "Expands void 0 to undefined (when undefined is not shadowed)",
@@ -219,6 +228,27 @@
219
228
  "diffReductionImpact": 0.00003774938185385768,
220
229
  "recommended": true,
221
230
  "notes": "Added manually based on high-confidence heuristic."
231
+ },
232
+ {
233
+ "id": "rename-charcode-variables",
234
+ "description": "Renames variables assigned from .charCodeAt() to $charCode or charCode/charCode2/...",
235
+ "scope": "file",
236
+ "parallelizable": true,
237
+ "diffReductionImpact": 0,
238
+ "recommended": false,
239
+ "evaluatedAt": "2026-01-23T17:59:00.000Z",
240
+ "notes": "Superseded by rename-charcode-variables-v2.",
241
+ "supersededBy": "rename-charcode-variables-v2"
242
+ },
243
+ {
244
+ "id": "rename-charcode-variables-v2",
245
+ "description": "Renames variables assigned from .charCodeAt(arg) to $charCodeAt{Arg} based on the argument",
246
+ "scope": "file",
247
+ "parallelizable": true,
248
+ "diffReductionImpact": 0,
249
+ "recommended": true,
250
+ "evaluatedAt": "2026-01-23T18:10:00.000Z",
251
+ "notes": "Derives names from charCodeAt argument for better stability (e.g., $charCodeAtIndex, $charCodeAtIndexPlus1)."
222
252
  }
223
253
  ],
224
254
  "presetStats": {