miniread 1.6.0 → 1.8.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 (74) hide show
  1. package/bin/miniread-snapshot +17 -0
  2. package/dist/core/normalize-code.d.ts +17 -0
  3. package/dist/core/normalize-code.js +35 -0
  4. package/dist/core/stable-naming.d.ts +61 -0
  5. package/dist/core/stable-naming.js +121 -0
  6. package/dist/scripts/evaluate/check-expected-evaluations.js +2 -0
  7. package/dist/scripts/evaluate/check-recommended-snapshot.d.ts +11 -0
  8. package/dist/scripts/evaluate/check-recommended-snapshot.js +53 -0
  9. package/dist/scripts/evaluate/check-snapshots.d.ts +1 -0
  10. package/dist/scripts/evaluate/check-snapshots.js +21 -2
  11. package/dist/scripts/evaluate/transform-content.d.ts +5 -0
  12. package/dist/scripts/evaluate/transform-content.js +23 -13
  13. package/dist/scripts/snapshot/create-snapshot-command.d.ts +9 -0
  14. package/dist/scripts/snapshot/create-snapshot-command.js +32 -0
  15. package/dist/scripts/snapshot/run-snapshot-cli.d.ts +1 -0
  16. package/dist/scripts/snapshot/run-snapshot-cli.js +162 -0
  17. package/dist/scripts/snapshot.d.ts +2 -0
  18. package/dist/scripts/snapshot.js +13 -0
  19. package/dist/transforms/rename-catch-parameters/rename-catch-parameters-transform.js +15 -38
  20. package/dist/transforms/rename-destructured-aliases/rename-destructured-aliases-transform.js +13 -12
  21. package/dist/transforms/rename-event-parameters/process-event-handler-function.d.ts +2 -3
  22. package/dist/transforms/rename-event-parameters/process-event-handler-function.js +13 -9
  23. package/dist/transforms/rename-event-parameters/rename-event-parameters-transform.d.ts +1 -1
  24. package/dist/transforms/rename-event-parameters/rename-event-parameters-transform.js +10 -7
  25. package/dist/transforms/rename-loop-index-variables/rename-loop-index-variables-transform.js +19 -13
  26. package/dist/transforms/rename-promise-executor-parameters/rename-promise-executor-parameters-transform.js +12 -5
  27. package/dist/transforms/rename-timeout-ids/rename-timeout-ids-transform.d.ts +1 -1
  28. package/dist/transforms/rename-timeout-ids/rename-timeout-ids-transform.js +10 -11
  29. package/dist/transforms/rename-use-reference-guards/rename-use-reference-guards-transform.js +10 -11
  30. package/dist/transforms/rename-use-reference-guards-v2/rename-use-reference-guards-v2-transform.d.ts +1 -1
  31. package/dist/transforms/rename-use-reference-guards-v2/rename-use-reference-guards-v2-transform.js +14 -12
  32. package/dist/transforms/transform-registry.js +0 -10
  33. package/package.json +4 -2
  34. package/transform-manifest.json +3 -53
  35. package/dist/transforms/expand-return-sequence/expand-return-sequence-transform.d.ts +0 -2
  36. package/dist/transforms/expand-return-sequence/expand-return-sequence-transform.js +0 -91
  37. package/dist/transforms/expand-sequence-expressions/expand-expression-statement-sequence.d.ts +0 -3
  38. package/dist/transforms/expand-sequence-expressions/expand-expression-statement-sequence.js +0 -57
  39. package/dist/transforms/expand-sequence-expressions/expand-sequence-expressions-transform.d.ts +0 -2
  40. package/dist/transforms/expand-sequence-expressions/expand-sequence-expressions-transform.js +0 -34
  41. package/dist/transforms/expand-sequence-expressions/expand-variable-declaration-sequence.d.ts +0 -3
  42. package/dist/transforms/expand-sequence-expressions/expand-variable-declaration-sequence.js +0 -93
  43. package/dist/transforms/expand-sequence-expressions-v2/expand-expression-statement-sequence.d.ts +0 -3
  44. package/dist/transforms/expand-sequence-expressions-v2/expand-expression-statement-sequence.js +0 -55
  45. package/dist/transforms/expand-sequence-expressions-v2/expand-return-sequence.d.ts +0 -3
  46. package/dist/transforms/expand-sequence-expressions-v2/expand-return-sequence.js +0 -86
  47. package/dist/transforms/expand-sequence-expressions-v2/expand-sequence-expressions-v2-transform.d.ts +0 -2
  48. package/dist/transforms/expand-sequence-expressions-v2/expand-sequence-expressions-v2-transform.js +0 -41
  49. package/dist/transforms/expand-sequence-expressions-v2/expand-variable-declaration-sequence.d.ts +0 -3
  50. package/dist/transforms/expand-sequence-expressions-v2/expand-variable-declaration-sequence.js +0 -93
  51. package/dist/transforms/expand-sequence-expressions-v3/expand-expression-statement-sequence.d.ts +0 -3
  52. package/dist/transforms/expand-sequence-expressions-v3/expand-expression-statement-sequence.js +0 -64
  53. package/dist/transforms/expand-sequence-expressions-v3/expand-return-sequence.d.ts +0 -3
  54. package/dist/transforms/expand-sequence-expressions-v3/expand-return-sequence.js +0 -91
  55. package/dist/transforms/expand-sequence-expressions-v3/expand-sequence-expressions-v3-transform.d.ts +0 -2
  56. package/dist/transforms/expand-sequence-expressions-v3/expand-sequence-expressions-v3-transform.js +0 -48
  57. package/dist/transforms/expand-sequence-expressions-v3/expand-throw-sequence.d.ts +0 -3
  58. package/dist/transforms/expand-sequence-expressions-v3/expand-throw-sequence.js +0 -101
  59. package/dist/transforms/expand-sequence-expressions-v3/expand-variable-declaration-sequence.d.ts +0 -3
  60. package/dist/transforms/expand-sequence-expressions-v3/expand-variable-declaration-sequence.js +0 -99
  61. package/dist/transforms/expand-throw-sequence/expand-throw-sequence-transform.d.ts +0 -2
  62. package/dist/transforms/expand-throw-sequence/expand-throw-sequence-transform.js +0 -117
  63. package/dist/transforms/rename-binding/get-target-name.d.ts +0 -4
  64. package/dist/transforms/rename-binding/get-target-name.js +0 -25
  65. package/dist/transforms/rename-binding/is-valid-binding-identifier.d.ts +0 -1
  66. package/dist/transforms/rename-binding/is-valid-binding-identifier.js +0 -10
  67. package/dist/transforms/rename-use-reference-guards/get-target-name.d.ts +0 -2
  68. package/dist/transforms/rename-use-reference-guards/get-target-name.js +0 -23
  69. package/dist/transforms/rename-use-reference-guards/is-valid-binding-identifier.d.ts +0 -1
  70. package/dist/transforms/rename-use-reference-guards/is-valid-binding-identifier.js +0 -10
  71. package/dist/transforms/rename-use-reference-guards-v2/get-target-name.d.ts +0 -2
  72. package/dist/transforms/rename-use-reference-guards-v2/get-target-name.js +0 -23
  73. package/dist/transforms/rename-use-reference-guards-v2/is-valid-binding-identifier.d.ts +0 -1
  74. package/dist/transforms/rename-use-reference-guards-v2/is-valid-binding-identifier.js +0 -10
@@ -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,17 @@
1
+ /**
2
+ * Normalizes JavaScript/TypeScript code through the parse → generate → format pipeline.
3
+ *
4
+ * This ensures consistent whitespace and formatting regardless of the original source.
5
+ * Use this when comparing code that may have gone through different pipelines
6
+ * (e.g., comparing base.js to transform output).
7
+ *
8
+ * The pipeline:
9
+ * 1. Parse with Babel (normalizes AST structure)
10
+ * 2. Generate with Babel (produces consistent whitespace)
11
+ * 3. Format with Prettier (final formatting)
12
+ */
13
+ /**
14
+ * Normalizes code through parse → generate → format pipeline.
15
+ * Returns the normalized code string.
16
+ */
17
+ export declare const normalizeCode: (code: string) => Promise<string>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Normalizes JavaScript/TypeScript code through the parse → generate → format pipeline.
3
+ *
4
+ * This ensures consistent whitespace and formatting regardless of the original source.
5
+ * Use this when comparing code that may have gone through different pipelines
6
+ * (e.g., comparing base.js to transform output).
7
+ *
8
+ * The pipeline:
9
+ * 1. Parse with Babel (normalizes AST structure)
10
+ * 2. Generate with Babel (produces consistent whitespace)
11
+ * 3. Format with Prettier (final formatting)
12
+ */
13
+ import { createRequire } from "node:module";
14
+ import * as prettier from "prettier";
15
+ const require = createRequire(import.meta.url);
16
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
17
+ const parser = require("@babel/parser");
18
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19
+ const parse = parser.parse;
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
21
+ const generate = require("@babel/generator").default;
22
+ const PARSER_OPTIONS = {
23
+ sourceType: "unambiguous",
24
+ plugins: ["jsx", "typescript", "importAttributes"],
25
+ };
26
+ /**
27
+ * Normalizes code through parse → generate → format pipeline.
28
+ * Returns the normalized code string.
29
+ */
30
+ export const normalizeCode = async (code) => {
31
+ const ast = parse(code, PARSER_OPTIONS);
32
+ const generated = generate(ast);
33
+ const formatted = await prettier.format(generated.code, { parser: "babel" });
34
+ return formatted;
35
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Helpers for the stable name prefix convention (§5.6-5.9 in workflow.md).
3
+ *
4
+ * The `$` prefix signals that a variable has been renamed stably by a transform
5
+ * and should be skipped by other transforms.
6
+ *
7
+ * - Stable names (`$timeoutId`): Readable AND deterministic across versions
8
+ * - Readable names (`timeoutId`): Semantic but order-dependent
9
+ */
10
+ import type { Scope } from "@babel/traverse";
11
+ /**
12
+ * Returns true if the name has been stable-renamed (starts with $).
13
+ * Transforms should skip variables with this prefix.
14
+ */
15
+ export declare const isStableRenamed: (name: string) => boolean;
16
+ /**
17
+ * Entry for a planned rename operation.
18
+ */
19
+ type RenameEntry = {
20
+ /** The Babel scope containing the binding */
21
+ scope: Scope;
22
+ /** Current name of the variable */
23
+ currentName: string;
24
+ /** Desired semantic base name (e.g., "timeoutId", "error") */
25
+ baseName: string;
26
+ };
27
+ /**
28
+ * Collects rename operations and applies them with automatic stability logic.
29
+ *
30
+ * Usage:
31
+ * ```typescript
32
+ * const group = new RenameGroup();
33
+ *
34
+ * // Collect renames during traversal
35
+ * group.add({ scope: path.scope, currentName: "a", baseName: "timeoutId" });
36
+ * group.add({ scope: path.scope, currentName: "b", baseName: "timeoutId" });
37
+ *
38
+ * // Apply with automatic stability
39
+ * const count = group.apply();
40
+ * // Result: a → timeoutId, b → timeoutId2 (readable, 2+ unstable)
41
+ * // Or if only one: a → $timeoutId (stable, exactly 1 unstable)
42
+ * ```
43
+ *
44
+ * Stability rules (per scope, per baseName):
45
+ * - If exactly 1 unstable variable wants this name → stable (`$baseName`)
46
+ * - If 2+ unstable variables want the same name → readable only (`baseName`, `baseName2`, ...)
47
+ */
48
+ export declare class RenameGroup {
49
+ private entries;
50
+ /**
51
+ * Add a variable to be renamed.
52
+ * Already-stable names (starting with $) are automatically skipped.
53
+ */
54
+ add(entry: RenameEntry): void;
55
+ /**
56
+ * Apply all collected renames with automatic stability logic.
57
+ * Returns the number of transformations applied.
58
+ */
59
+ apply(): number;
60
+ }
61
+ export {};
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Helpers for the stable name prefix convention (§5.6-5.9 in workflow.md).
3
+ *
4
+ * The `$` prefix signals that a variable has been renamed stably by a transform
5
+ * and should be skipped by other transforms.
6
+ *
7
+ * - Stable names (`$timeoutId`): Readable AND deterministic across versions
8
+ * - Readable names (`timeoutId`): Semantic but order-dependent
9
+ */
10
+ const STABLE_PREFIX = "$";
11
+ /**
12
+ * Returns true if the name has been stable-renamed (starts with $).
13
+ * Transforms should skip variables with this prefix.
14
+ */
15
+ export const isStableRenamed = (name) => {
16
+ return name.startsWith(STABLE_PREFIX);
17
+ };
18
+ /**
19
+ * Creates a stable name by adding the $ prefix.
20
+ * Internal helper - prefer using RenameGroup which handles stability logic automatically.
21
+ */
22
+ const makeStableName = (baseName) => {
23
+ return `${STABLE_PREFIX}${baseName}`;
24
+ };
25
+ /**
26
+ * Finds the next available name slot in a scope.
27
+ */
28
+ const findAvailableName = (scope, baseName, startIndex, canBeStable) => {
29
+ let index = startIndex;
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31
+ while (true) {
32
+ const suffix = index === 1 ? "" : String(index);
33
+ const candidateBase = `${baseName}${suffix}`;
34
+ const candidateStable = makeStableName(candidateBase);
35
+ const candidateReadable = candidateBase;
36
+ // Check if either form is already taken in this scope
37
+ const stableTaken = scope.hasBinding(candidateStable);
38
+ const readableTaken = scope.hasBinding(candidateReadable);
39
+ if (!stableTaken && !readableTaken) {
40
+ // This slot is available
41
+ const name = canBeStable ? candidateStable : candidateReadable;
42
+ return { name, nextIndex: index + 1 };
43
+ }
44
+ index++;
45
+ }
46
+ };
47
+ /**
48
+ * Collects rename operations and applies them with automatic stability logic.
49
+ *
50
+ * Usage:
51
+ * ```typescript
52
+ * const group = new RenameGroup();
53
+ *
54
+ * // Collect renames during traversal
55
+ * group.add({ scope: path.scope, currentName: "a", baseName: "timeoutId" });
56
+ * group.add({ scope: path.scope, currentName: "b", baseName: "timeoutId" });
57
+ *
58
+ * // Apply with automatic stability
59
+ * const count = group.apply();
60
+ * // Result: a → timeoutId, b → timeoutId2 (readable, 2+ unstable)
61
+ * // Or if only one: a → $timeoutId (stable, exactly 1 unstable)
62
+ * ```
63
+ *
64
+ * Stability rules (per scope, per baseName):
65
+ * - If exactly 1 unstable variable wants this name → stable (`$baseName`)
66
+ * - If 2+ unstable variables want the same name → readable only (`baseName`, `baseName2`, ...)
67
+ */
68
+ export class RenameGroup {
69
+ entries = [];
70
+ /**
71
+ * Add a variable to be renamed.
72
+ * Already-stable names (starting with $) are automatically skipped.
73
+ */
74
+ add(entry) {
75
+ if (isStableRenamed(entry.currentName))
76
+ return;
77
+ this.entries.push(entry);
78
+ }
79
+ /**
80
+ * Apply all collected renames with automatic stability logic.
81
+ * Returns the number of transformations applied.
82
+ */
83
+ apply() {
84
+ // Group entries by (scope, baseName) using a two-level Map
85
+ const scopeGroups = new Map();
86
+ for (const entry of this.entries) {
87
+ let baseNameMap = scopeGroups.get(entry.scope);
88
+ if (!baseNameMap) {
89
+ baseNameMap = new Map();
90
+ scopeGroups.set(entry.scope, baseNameMap);
91
+ }
92
+ let group = baseNameMap.get(entry.baseName);
93
+ if (!group) {
94
+ group = [];
95
+ baseNameMap.set(entry.baseName, group);
96
+ }
97
+ group.push(entry);
98
+ }
99
+ let transformationsApplied = 0;
100
+ // Process each scope
101
+ for (const [scope, baseNameMap] of scopeGroups) {
102
+ // Process each baseName group within this scope
103
+ for (const [baseName, group] of baseNameMap) {
104
+ // Determine if names can be stable (only 1 unstable entry for this baseName)
105
+ const canBeStable = group.length === 1;
106
+ // Track which index to try next
107
+ let index = 1;
108
+ for (const entry of group) {
109
+ // Find next available name slot
110
+ const targetName = findAvailableName(scope, baseName, index, canBeStable);
111
+ index = targetName.nextIndex;
112
+ if (entry.currentName !== targetName.name) {
113
+ scope.rename(entry.currentName, targetName.name);
114
+ transformationsApplied++;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return transformationsApplied;
120
+ }
121
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { allTransformIds } from "../../transforms/transform-registry.js";
3
3
  import { transformRegistry } from "../../transforms/transform-registry.js";
4
+ import { transformPresets } from "../../transforms/transform-presets.js";
4
5
  import { parseTransformManifest } from "./parse-transform-manifest.js";
5
6
  import { checkAllSnapshots } from "./check-snapshots.js";
6
7
  export const checkExpectedEvaluations = async (options) => {
@@ -24,6 +25,7 @@ export const checkExpectedEvaluations = async (options) => {
24
25
  testCasesDirectory,
25
26
  transformIds,
26
27
  transformRegistry,
28
+ recommendedTransformIds: transformPresets.recommended,
27
29
  });
28
30
  const allPresent = missingManifestEntries.length === 0 && snapshotIssues.length === 0;
29
31
  return {
@@ -0,0 +1,11 @@
1
+ import type { Transform } from "../../core/types.js";
2
+ import type { SnapshotIssue } from "./check-snapshots.js";
3
+ type CheckRecommendedSnapshotOptions = {
4
+ basePath: string;
5
+ baseContent: string;
6
+ snapshotPath: string;
7
+ testCaseName: string;
8
+ transforms: Transform[];
9
+ };
10
+ export declare const checkRecommendedSnapshot: (options: CheckRecommendedSnapshotOptions) => Promise<SnapshotIssue | undefined>;
11
+ export {};
@@ -0,0 +1,53 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as prettier from "prettier";
3
+ import { transformContentWithMultiple } from "./transform-content.js";
4
+ const formatCode = async (code) => {
5
+ return prettier.format(code, { parser: "babel" });
6
+ };
7
+ const fileExists = async (filePath) => {
8
+ try {
9
+ await fs.access(filePath);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ };
16
+ export const checkRecommendedSnapshot = async (options) => {
17
+ const { basePath, baseContent, snapshotPath, testCaseName, transforms } = options;
18
+ const snapshotExists = await fileExists(snapshotPath);
19
+ // Run all recommended transforms to get the output
20
+ const result = await transformContentWithMultiple({
21
+ content: baseContent,
22
+ filePath: basePath,
23
+ transforms,
24
+ });
25
+ if (!result.ok) {
26
+ // Transform failed, skip this check
27
+ return undefined;
28
+ }
29
+ const formattedOutput = await formatCode(result.output);
30
+ if (snapshotExists) {
31
+ const snapshotContent = await fs.readFile(snapshotPath, "utf8");
32
+ const formattedSnapshot = await formatCode(snapshotContent);
33
+ // Check if snapshot is outdated (recommended snapshots are never "unnecessary")
34
+ if (formattedOutput !== formattedSnapshot) {
35
+ return {
36
+ transformId: "recommended",
37
+ testCase: testCaseName,
38
+ snapshotPath,
39
+ issue: "outdated",
40
+ };
41
+ }
42
+ }
43
+ else {
44
+ // Snapshot doesn't exist - it's always required for recommended
45
+ return {
46
+ transformId: "recommended",
47
+ testCase: testCaseName,
48
+ snapshotPath,
49
+ issue: "missing-required",
50
+ };
51
+ }
52
+ return undefined;
53
+ };
@@ -9,6 +9,7 @@ type CheckAllSnapshotsOptions = {
9
9
  testCasesDirectory: string;
10
10
  transformIds: string[];
11
11
  transformRegistry: Record<string, Transform>;
12
+ recommendedTransformIds: string[];
12
13
  };
13
14
  export declare const checkAllSnapshots: (options: CheckAllSnapshotsOptions) => Promise<SnapshotIssue[]>;
14
15
  export {};
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import * as prettier from "prettier";
4
+ import { normalizeCode } from "../../core/normalize-code.js";
4
5
  import { transformContent } from "./transform-content.js";
6
+ import { checkRecommendedSnapshot } from "./check-recommended-snapshot.js";
5
7
  const formatCode = async (code) => {
6
8
  return prettier.format(code, { parser: "babel" });
7
9
  };
@@ -84,13 +86,18 @@ const getTestCaseNames = async (testCasesDirectory) => {
84
86
  return testCaseNames.toSorted();
85
87
  };
86
88
  export const checkAllSnapshots = async (options) => {
87
- const { testCasesDirectory, transformIds, transformRegistry } = options;
89
+ const { testCasesDirectory, transformIds, transformRegistry, recommendedTransformIds, } = options;
88
90
  const snapshotIssues = [];
89
91
  const testCaseNames = await getTestCaseNames(testCasesDirectory);
92
+ // Get the recommended transforms in order
93
+ const recommendedTransforms = recommendedTransformIds
94
+ .map((id) => transformRegistry[id])
95
+ .filter((t) => t !== undefined);
90
96
  for (const testCaseName of testCaseNames) {
91
97
  const basePath = path.join(testCasesDirectory, testCaseName, "base.js");
92
98
  const baseContent = await fs.readFile(basePath, "utf8");
93
- const formattedBase = await formatCode(baseContent);
99
+ const formattedBase = await normalizeCode(baseContent);
100
+ // Check individual transform snapshots
94
101
  for (const transformId of transformIds) {
95
102
  const snapshotPath = path.join(testCasesDirectory, testCaseName, `${transformId}.js`);
96
103
  const issue = await checkSnapshot({
@@ -106,6 +113,18 @@ export const checkAllSnapshots = async (options) => {
106
113
  snapshotIssues.push(issue);
107
114
  }
108
115
  }
116
+ // Check recommended snapshot (always required)
117
+ const recommendedSnapshotPath = path.join(testCasesDirectory, testCaseName, "recommended.js");
118
+ const recommendedIssue = await checkRecommendedSnapshot({
119
+ basePath,
120
+ baseContent,
121
+ snapshotPath: recommendedSnapshotPath,
122
+ testCaseName,
123
+ transforms: recommendedTransforms,
124
+ });
125
+ if (recommendedIssue) {
126
+ snapshotIssues.push(recommendedIssue);
127
+ }
109
128
  }
110
129
  return snapshotIssues;
111
130
  };
@@ -11,4 +11,9 @@ export declare const transformContent: (options: {
11
11
  filePath: string;
12
12
  transform: Transform;
13
13
  }) => Promise<TransformContentResult>;
14
+ export declare const transformContentWithMultiple: (options: {
15
+ content: string;
16
+ filePath: string;
17
+ transforms: Transform[];
18
+ }) => Promise<TransformContentResult>;
14
19
  export {};
@@ -2,24 +2,34 @@ import { buildProjectGraph } from "../../core/project-graph.js";
2
2
  import { generateCode } from "../../cli/generate-code.js";
3
3
  export const transformContent = async (options) => {
4
4
  const { content, filePath, transform } = options;
5
+ return transformContentWithMultiple({
6
+ content,
7
+ filePath,
8
+ transforms: [transform],
9
+ });
10
+ };
11
+ export const transformContentWithMultiple = async (options) => {
12
+ const { content, filePath, transforms } = options;
5
13
  const projectGraph = buildProjectGraph([{ path: filePath, content }]);
6
14
  const file = projectGraph.files.get(filePath);
7
15
  if (!file) {
8
16
  return { ok: false, error: "Failed to parse input" };
9
17
  }
10
- try {
11
- await transform.transform({
12
- projectGraph,
13
- currentFile: undefined,
14
- options: {},
15
- });
16
- }
17
- catch (error) {
18
- const message = error instanceof Error ? error.message : String(error);
19
- return {
20
- ok: false,
21
- error: `Transform '${transform.id}' failed: ${message}`,
22
- };
18
+ for (const transform of transforms) {
19
+ try {
20
+ await transform.transform({
21
+ projectGraph,
22
+ currentFile: undefined,
23
+ options: {},
24
+ });
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ return {
29
+ ok: false,
30
+ error: `Transform '${transform.id}' failed: ${message}`,
31
+ };
32
+ }
23
33
  }
24
34
  const generated = generateCode(file.ast);
25
35
  const output = generated === ""
@@ -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 {};