miniread 1.17.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 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
+ };
@@ -21,6 +21,7 @@ import { renameThisAliasesTransform } from "./rename-this-aliases/rename-this-al
21
21
  import { renameTimeoutIdsTransform } from "./rename-timeout-ids/rename-timeout-ids-transform.js";
22
22
  import { renameUseReferenceGuardsTransform } from "./rename-use-reference-guards/rename-use-reference-guards-transform.js";
23
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";
24
25
  import { splitVariableDeclarationsTransform } from "./split-variable-declarations/split-variable-declarations-transform.js";
25
26
  export const transformRegistry = {
26
27
  [expandBooleanLiteralsTransform.id]: expandBooleanLiteralsTransform,
@@ -46,6 +47,7 @@ export const transformRegistry = {
46
47
  [renameTimeoutIdsTransform.id]: renameTimeoutIdsTransform,
47
48
  [renameUseReferenceGuardsTransform.id]: renameUseReferenceGuardsTransform,
48
49
  [renameUseReferenceGuardsV2Transform.id]: renameUseReferenceGuardsV2Transform,
50
+ [simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
49
51
  [splitVariableDeclarationsTransform.id]: splitVariableDeclarationsTransform,
50
52
  };
51
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.17.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)",