miniread 1.53.0 → 1.55.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.
- package/README.md +7 -0
- package/dist/cli/execute-transforms.d.ts +6 -1
- package/dist/cli/execute-transforms.js +9 -3
- package/dist/cli/find-input-files.d.ts +13 -0
- package/dist/cli/find-input-files.js +68 -0
- package/dist/cli/run-transforms.d.ts +1 -0
- package/dist/cli/run-transforms.js +7 -59
- package/dist/cli/transform-stdin.d.ts +1 -0
- package/dist/cli/transform-stdin.js +4 -3
- package/dist/cli.js +24 -1
- package/dist/core/collect-exported-names.js +28 -8
- package/dist/core/create-has-binding-options-up-to-scope.d.ts +8 -0
- package/dist/core/create-has-binding-options-up-to-scope.js +7 -0
- package/dist/core/default-transform-options.d.ts +10 -0
- package/dist/core/default-transform-options.js +5 -0
- package/dist/core/has-shadowing-risk.d.ts +2 -0
- package/dist/core/has-shadowing-risk.js +24 -0
- package/dist/core/stable-naming.d.ts +3 -0
- package/dist/core/stable-naming.js +28 -34
- package/dist/core/types.d.ts +1 -1
- package/dist/scripts/snapshot/run-testcase-transform.js +3 -1
- package/dist/transforms/_generated/manifest.js +70 -8
- package/dist/transforms/_generated/registry.js +6 -0
- package/dist/transforms/preset-stats.json +2 -2
- package/dist/transforms/recommended-transform-order.d.ts +7 -0
- package/dist/transforms/recommended-transform-order.js +59 -0
- package/dist/transforms/rename-deferred-resolve-parameters/manifest.json +2 -2
- package/dist/transforms/rename-deferred-resolve-parameters/rename-binding-if-safe.js +1 -10
- package/dist/transforms/rename-event-parameters/can-rename-binding-safely.js +1 -13
- package/dist/transforms/rename-event-parameters/manifest.json +3 -3
- package/dist/transforms/rename-interval-ids/manifest.json +12 -0
- package/dist/transforms/rename-interval-ids/rename-interval-ids-transform.d.ts +2 -0
- package/dist/transforms/rename-interval-ids/rename-interval-ids-transform.js +85 -0
- package/dist/transforms/rename-promise-executor-parameters/rename-binding-if-safe.js +1 -14
- package/dist/transforms/rename-promise-executor-parameters-v2/manifest.json +3 -3
- package/dist/transforms/rename-promise-executor-parameters-v2/rename-binding-if-safe.js +1 -23
- package/dist/transforms/replace-dynamic-require-eval/manifest.json +13 -0
- package/dist/transforms/replace-dynamic-require-eval/replace-dynamic-require-eval-transform.d.ts +2 -0
- package/dist/transforms/replace-dynamic-require-eval/replace-dynamic-require-eval-transform.js +134 -0
- package/dist/transforms/stabilize-top-level-bindings/analyze-global-object-member-access.d.ts +23 -0
- package/dist/transforms/stabilize-top-level-bindings/analyze-global-object-member-access.js +22 -0
- package/dist/transforms/stabilize-top-level-bindings/can-rename-binding-safely.d.ts +2 -0
- package/dist/transforms/stabilize-top-level-bindings/can-rename-binding-safely.js +24 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-first-program-body-assignments.d.ts +12 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-first-program-body-assignments.js +29 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-nested-program-scope-variable-initializers.d.ts +16 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-nested-program-scope-variable-initializers.js +63 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-rename-candidates.d.ts +15 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-rename-candidates.js +185 -0
- package/dist/transforms/stabilize-top-level-bindings/create-global-object-member-expression-visitors.d.ts +13 -0
- package/dist/transforms/stabilize-top-level-bindings/create-global-object-member-expression-visitors.js +41 -0
- package/dist/transforms/stabilize-top-level-bindings/detect-dynamic-name-lookup.d.ts +26 -0
- package/dist/transforms/stabilize-top-level-bindings/detect-dynamic-name-lookup.js +151 -0
- package/dist/transforms/stabilize-top-level-bindings/detect-global-object-property-access.d.ts +21 -0
- package/dist/transforms/stabilize-top-level-bindings/detect-global-object-property-access.js +29 -0
- package/dist/transforms/stabilize-top-level-bindings/eval-like-call-detection.d.ts +4 -0
- package/dist/transforms/stabilize-top-level-bindings/eval-like-call-detection.js +73 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-leaf-node.d.ts +2 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-leaf-node.js +23 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-node.d.ts +1 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-node.js +9 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-scalar-fields.d.ts +2 -0
- package/dist/transforms/stabilize-top-level-bindings/fingerprint-scalar-fields.js +153 -0
- package/dist/transforms/stabilize-top-level-bindings/function-constructor-call-detection.d.ts +3 -0
- package/dist/transforms/stabilize-top-level-bindings/function-constructor-call-detection.js +63 -0
- package/dist/transforms/stabilize-top-level-bindings/global-object-access.d.ts +10 -0
- package/dist/transforms/stabilize-top-level-bindings/global-object-access.js +118 -0
- package/dist/transforms/stabilize-top-level-bindings/is-global-eval-destructuring.d.ts +7 -0
- package/dist/transforms/stabilize-top-level-bindings/is-global-eval-destructuring.js +29 -0
- package/dist/transforms/stabilize-top-level-bindings/is-valid-binding-identifier.d.ts +1 -0
- package/dist/transforms/stabilize-top-level-bindings/is-valid-binding-identifier.js +10 -0
- package/dist/transforms/stabilize-top-level-bindings/manifest.json +13 -0
- package/dist/transforms/stabilize-top-level-bindings/rename-binding-in-place.d.ts +13 -0
- package/dist/transforms/stabilize-top-level-bindings/rename-binding-in-place.js +140 -0
- package/dist/transforms/stabilize-top-level-bindings/rename-candidate.d.ts +19 -0
- package/dist/transforms/stabilize-top-level-bindings/rename-candidate.js +14 -0
- package/dist/transforms/stabilize-top-level-bindings/serialize-fingerprint-node.d.ts +3 -0
- package/dist/transforms/stabilize-top-level-bindings/serialize-fingerprint-node.js +112 -0
- package/dist/transforms/stabilize-top-level-bindings/stabilize-top-level-bindings-transform.d.ts +2 -0
- package/dist/transforms/stabilize-top-level-bindings/stabilize-top-level-bindings-transform.js +101 -0
- package/dist/transforms/stabilize-top-level-bindings/string-timer-call-detection.d.ts +4 -0
- package/dist/transforms/stabilize-top-level-bindings/string-timer-call-detection.js +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,8 +17,15 @@ miniread --list-transforms
|
|
|
17
17
|
miniread --input ./minified --output ./readable --transforms recommended
|
|
18
18
|
miniread --input ./minified --output ./readable --dry-run
|
|
19
19
|
miniread --input ./minified --output ./readable --workers 8
|
|
20
|
+
miniread --input ./minified --output ./readable --safe-stabilize-top-level-bindings
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
## Output stability
|
|
24
|
+
|
|
25
|
+
The recommended preset includes `stabilize-top-level-bindings`, which renames program-scope bindings to stable hash-based names (`$h_<hash>`). This produces deterministic output across minifier versions, making diffs more useful.
|
|
26
|
+
|
|
27
|
+
By default, this transform runs even when it detects code patterns (like `eval`) that could make the output non-runnable. Use `--safe-stabilize-top-level-bindings` or set `MINIREAD_UNSAFE_STABILIZE_TOP_LEVEL_BINDINGS` to a falsy value (`0`, `false`, `no`, or empty string) to skip renaming in such cases.
|
|
28
|
+
|
|
22
29
|
## Examples
|
|
23
30
|
|
|
24
31
|
### Find the most common identifiers in a minified bundle
|
|
@@ -5,5 +5,10 @@ type TransformExecutionResult = {
|
|
|
5
5
|
error?: string;
|
|
6
6
|
};
|
|
7
7
|
type VerboseLogger = (message: string) => void;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param transformOptions - Shared across all transform invocations; shallow-frozen
|
|
10
|
+
* at runtime to prevent accidental top-level property mutation. Nested objects are
|
|
11
|
+
* not frozen (current options are all primitives).
|
|
12
|
+
*/
|
|
13
|
+
export declare const executeTransforms: (transforms: Transform[], projectGraph: ProjectGraph, queue: PQueue, transformOptions: Readonly<Record<string, unknown>>, log: VerboseLogger) => Promise<TransformExecutionResult>;
|
|
9
14
|
export {};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @param transformOptions - Shared across all transform invocations; shallow-frozen
|
|
3
|
+
* at runtime to prevent accidental top-level property mutation. Nested objects are
|
|
4
|
+
* not frozen (current options are all primitives).
|
|
5
|
+
*/
|
|
6
|
+
export const executeTransforms = async (transforms, projectGraph, queue, transformOptions, log) => {
|
|
7
|
+
const frozenOptions = Object.freeze({ ...transformOptions });
|
|
2
8
|
let totalTransformations = 0;
|
|
3
9
|
for (const transform of transforms) {
|
|
4
10
|
log(`Running transform: ${transform.id}`);
|
|
@@ -9,7 +15,7 @@ export const executeTransforms = async (transforms, projectGraph, queue, log) =>
|
|
|
9
15
|
const statsPromises = fileInfos.map((fileInfo) => queue.add(() => transform.transform({
|
|
10
16
|
projectGraph,
|
|
11
17
|
currentFile: fileInfo,
|
|
12
|
-
options:
|
|
18
|
+
options: frozenOptions,
|
|
13
19
|
})));
|
|
14
20
|
const allStats = await Promise.all(statsPromises);
|
|
15
21
|
const transformApplied = allStats.reduce((sum, stats) => sum + stats.transformationsApplied, 0);
|
|
@@ -21,7 +27,7 @@ export const executeTransforms = async (transforms, projectGraph, queue, log) =>
|
|
|
21
27
|
const stats = await transform.transform({
|
|
22
28
|
projectGraph,
|
|
23
29
|
currentFile: undefined,
|
|
24
|
-
options:
|
|
30
|
+
options: frozenOptions,
|
|
25
31
|
});
|
|
26
32
|
totalTransformations += stats.transformationsApplied;
|
|
27
33
|
log(`Applied ${stats.transformationsApplied} transformation(s)`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type FindInputFilesOk = {
|
|
2
|
+
ok: true;
|
|
3
|
+
inputBase: string;
|
|
4
|
+
sourceFilePaths: string[];
|
|
5
|
+
otherFilePaths: string[];
|
|
6
|
+
};
|
|
7
|
+
type FindInputFilesError = {
|
|
8
|
+
ok: false;
|
|
9
|
+
errors: string[];
|
|
10
|
+
};
|
|
11
|
+
type FindInputFilesResult = FindInputFilesOk | FindInputFilesError;
|
|
12
|
+
export declare const findInputFiles: (input: string) => Promise<FindInputFilesResult>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const isSourceFile = (fileName) => {
|
|
4
|
+
return (!fileName.endsWith(".d.ts") &&
|
|
5
|
+
(fileName.endsWith(".ts") ||
|
|
6
|
+
fileName.endsWith(".tsx") ||
|
|
7
|
+
fileName.endsWith(".js") ||
|
|
8
|
+
fileName.endsWith(".jsx")));
|
|
9
|
+
};
|
|
10
|
+
const findAllFiles = async (directory) => {
|
|
11
|
+
const sourceFiles = [];
|
|
12
|
+
const otherFiles = [];
|
|
13
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = path.join(directory, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
const subResult = await findAllFiles(fullPath);
|
|
18
|
+
sourceFiles.push(...subResult.sourceFiles);
|
|
19
|
+
otherFiles.push(...subResult.otherFiles);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!entry.isFile())
|
|
23
|
+
continue;
|
|
24
|
+
if (isSourceFile(entry.name)) {
|
|
25
|
+
sourceFiles.push(fullPath);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
otherFiles.push(fullPath);
|
|
29
|
+
}
|
|
30
|
+
return { sourceFiles, otherFiles };
|
|
31
|
+
};
|
|
32
|
+
export const findInputFiles = async (input) => {
|
|
33
|
+
let inputStat;
|
|
34
|
+
try {
|
|
35
|
+
inputStat = await fs.stat(input);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : "Unknown error accessing path";
|
|
39
|
+
return { ok: false, errors: [`Cannot access input path: ${message}`] };
|
|
40
|
+
}
|
|
41
|
+
let sourceFilePaths;
|
|
42
|
+
let otherFilePaths;
|
|
43
|
+
try {
|
|
44
|
+
if (inputStat.isDirectory()) {
|
|
45
|
+
const allFiles = await findAllFiles(input);
|
|
46
|
+
sourceFilePaths = allFiles.sourceFiles;
|
|
47
|
+
otherFilePaths = allFiles.otherFiles;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
sourceFilePaths = isSourceFile(input) ? [input] : [];
|
|
51
|
+
otherFilePaths = isSourceFile(input) ? [] : [input];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : "Unknown error reading files";
|
|
56
|
+
return { ok: false, errors: [`Error reading input: ${message}`] };
|
|
57
|
+
}
|
|
58
|
+
if (sourceFilePaths.length === 0) {
|
|
59
|
+
return { ok: false, errors: ["No source files found in input path"] };
|
|
60
|
+
}
|
|
61
|
+
const inputBase = inputStat.isDirectory() ? input : path.dirname(input);
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
inputBase,
|
|
65
|
+
sourceFilePaths,
|
|
66
|
+
otherFilePaths,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -1,77 +1,26 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import PQueue from "p-queue";
|
|
4
3
|
import { buildProjectGraph } from "../core/project-graph.js";
|
|
5
4
|
import { printDryRunSummary, writeOutputFiles } from "./output.js";
|
|
6
5
|
import { generateCode } from "./generate-code.js";
|
|
7
6
|
import { executeTransforms } from "./execute-transforms.js";
|
|
7
|
+
import { findInputFiles } from "./find-input-files.js";
|
|
8
8
|
const logVerbose = (verbose, message) => {
|
|
9
9
|
if (!verbose)
|
|
10
10
|
return;
|
|
11
11
|
console.error(message);
|
|
12
12
|
};
|
|
13
|
-
const isSourceFile = (fileName) => {
|
|
14
|
-
return (!fileName.endsWith(".d.ts") &&
|
|
15
|
-
(fileName.endsWith(".ts") ||
|
|
16
|
-
fileName.endsWith(".tsx") ||
|
|
17
|
-
fileName.endsWith(".js") ||
|
|
18
|
-
fileName.endsWith(".jsx")));
|
|
19
|
-
};
|
|
20
|
-
const findAllFiles = async (directory) => {
|
|
21
|
-
const sourceFiles = [];
|
|
22
|
-
const otherFiles = [];
|
|
23
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
const fullPath = path.join(directory, entry.name);
|
|
26
|
-
if (entry.isDirectory()) {
|
|
27
|
-
const subResult = await findAllFiles(fullPath);
|
|
28
|
-
sourceFiles.push(...subResult.sourceFiles);
|
|
29
|
-
otherFiles.push(...subResult.otherFiles);
|
|
30
|
-
}
|
|
31
|
-
else if (entry.isFile()) {
|
|
32
|
-
if (isSourceFile(entry.name)) {
|
|
33
|
-
sourceFiles.push(fullPath);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
otherFiles.push(fullPath);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { sourceFiles, otherFiles };
|
|
41
|
-
};
|
|
42
13
|
export const runTransforms = async (options) => {
|
|
43
|
-
const { input, output, transforms, dryRun, workers, overwrite, verbose } = options;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await fs.access(input);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return {
|
|
50
|
-
filesProcessed: 0,
|
|
51
|
-
totalTransformations: 0,
|
|
52
|
-
errors: [`Input path does not exist: ${input}`],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
// Find all files
|
|
56
|
-
const inputStat = await fs.stat(input);
|
|
57
|
-
let sourceFilePaths;
|
|
58
|
-
let otherFilePaths;
|
|
59
|
-
if (inputStat.isDirectory()) {
|
|
60
|
-
const allFiles = await findAllFiles(input);
|
|
61
|
-
sourceFilePaths = allFiles.sourceFiles;
|
|
62
|
-
otherFilePaths = allFiles.otherFiles;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
sourceFilePaths = isSourceFile(input) ? [input] : [];
|
|
66
|
-
otherFilePaths = isSourceFile(input) ? [] : [input];
|
|
67
|
-
}
|
|
68
|
-
if (sourceFilePaths.length === 0) {
|
|
14
|
+
const { input, output, transforms, transformOptions = {}, dryRun, workers, overwrite, verbose, } = options;
|
|
15
|
+
const inputFilesResult = await findInputFiles(input);
|
|
16
|
+
if (!inputFilesResult.ok) {
|
|
69
17
|
return {
|
|
70
18
|
filesProcessed: 0,
|
|
71
19
|
totalTransformations: 0,
|
|
72
|
-
errors:
|
|
20
|
+
errors: inputFilesResult.errors,
|
|
73
21
|
};
|
|
74
22
|
}
|
|
23
|
+
const { inputBase, sourceFilePaths, otherFilePaths } = inputFilesResult;
|
|
75
24
|
logVerbose(verbose, `Found ${sourceFilePaths.length} source file(s)`);
|
|
76
25
|
if (otherFilePaths.length > 0) {
|
|
77
26
|
logVerbose(verbose, `Found ${otherFilePaths.length} other file(s) to copy`);
|
|
@@ -92,7 +41,7 @@ export const runTransforms = async (options) => {
|
|
|
92
41
|
const log = (message) => {
|
|
93
42
|
logVerbose(verbose, message);
|
|
94
43
|
};
|
|
95
|
-
const result = await executeTransforms(transforms, projectGraph, queue, log);
|
|
44
|
+
const result = await executeTransforms(transforms, projectGraph, queue, transformOptions, log);
|
|
96
45
|
if (result.error) {
|
|
97
46
|
return {
|
|
98
47
|
filesProcessed: 0,
|
|
@@ -108,7 +57,6 @@ export const runTransforms = async (options) => {
|
|
|
108
57
|
finalFiles.set(filePath, generated);
|
|
109
58
|
}
|
|
110
59
|
// Output results
|
|
111
|
-
const inputBase = inputStat.isDirectory() ? input : path.dirname(input);
|
|
112
60
|
if (dryRun) {
|
|
113
61
|
printDryRunSummary(finalFiles, otherFilePaths, inputBase);
|
|
114
62
|
}
|
|
@@ -19,7 +19,7 @@ const readStdin = async () => {
|
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
export const transformStdin = async (options) => {
|
|
22
|
-
const { transforms, verbose } = options;
|
|
22
|
+
const { transforms, transformOptions, verbose } = options;
|
|
23
23
|
const inputText = await readStdin();
|
|
24
24
|
const stdinPath = "stdin.tsx";
|
|
25
25
|
const parsedFiles = [{ path: stdinPath, content: inputText }];
|
|
@@ -28,13 +28,14 @@ export const transformStdin = async (options) => {
|
|
|
28
28
|
if (!stdinFile) {
|
|
29
29
|
return { ok: false, error: "Failed to parse input" };
|
|
30
30
|
}
|
|
31
|
+
const frozenOptions = Object.freeze({ ...transformOptions });
|
|
31
32
|
let totalTransformations = 0;
|
|
32
33
|
for (const transform of transforms) {
|
|
33
34
|
try {
|
|
34
35
|
const stats = await transform.transform({
|
|
35
36
|
projectGraph,
|
|
36
|
-
currentFile:
|
|
37
|
-
options:
|
|
37
|
+
currentFile: stdinFile,
|
|
38
|
+
options: frozenOptions,
|
|
38
39
|
});
|
|
39
40
|
totalTransformations += stats.transformationsApplied;
|
|
40
41
|
logVerbose(verbose, `Applied ${stats.transformationsApplied} transformation(s) from ${transform.id}`);
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
|
|
|
3
3
|
import packageJson from "../package.json" with { type: "json" };
|
|
4
4
|
import { getTransformsToRun, getTransformSummaries } from "./cli/config.js";
|
|
5
5
|
import { runTransforms, transformStdin } from "./cli/runner.js";
|
|
6
|
+
import { getDefaultTransformOptions } from "./core/default-transform-options.js";
|
|
6
7
|
const parseListFormat = (value) => {
|
|
7
8
|
if (value === "text" || value === "tsv" || value === "json")
|
|
8
9
|
return value;
|
|
@@ -15,6 +16,19 @@ const parsePositiveInt = (value) => {
|
|
|
15
16
|
}
|
|
16
17
|
return parsed;
|
|
17
18
|
};
|
|
19
|
+
const isEnvironmentFalsy = (value) => {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return false;
|
|
22
|
+
const trimmed = value.trim().toLowerCase();
|
|
23
|
+
return trimmed === "" || ["0", "false", "no"].includes(trimmed);
|
|
24
|
+
};
|
|
25
|
+
const shouldUnsafeStabilize = (safeFlag) => {
|
|
26
|
+
if (safeFlag === true)
|
|
27
|
+
return false;
|
|
28
|
+
if (isEnvironmentFalsy(process.env.MINIREAD_UNSAFE_STABILIZE_TOP_LEVEL_BINDINGS))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
18
32
|
const program = new Command()
|
|
19
33
|
.name(packageJson.name)
|
|
20
34
|
.description(packageJson.description)
|
|
@@ -29,7 +43,8 @@ const program = new Command()
|
|
|
29
43
|
.option("--format <format>", "Output format for --list-transforms: text, tsv, json", parseListFormat, "text")
|
|
30
44
|
.option("--dry-run", "Show what would be changed without writing files")
|
|
31
45
|
.option("-w, --workers <n>", "Number of parallel workers", parsePositiveInt, 4)
|
|
32
|
-
.option("-f, --overwrite", "Overwrite existing files in output directory")
|
|
46
|
+
.option("-f, --overwrite", "Overwrite existing files in output directory")
|
|
47
|
+
.option("--safe-stabilize-top-level-bindings", "Make stabilize-top-level-bindings bail out on dynamic-name hazards (safer; output more likely runnable)");
|
|
33
48
|
program.addHelpText("after", `
|
|
34
49
|
Examples:
|
|
35
50
|
# Read from stdin, write to stdout (filter mode)
|
|
@@ -86,6 +101,10 @@ else if (options.input) {
|
|
|
86
101
|
input: options.input,
|
|
87
102
|
output: options.output,
|
|
88
103
|
transforms: transformsResult.transforms,
|
|
104
|
+
transformOptions: {
|
|
105
|
+
...getDefaultTransformOptions(),
|
|
106
|
+
unsafeStabilizeTopLevelBindings: shouldUnsafeStabilize(options.safeStabilizeTopLevelBindings),
|
|
107
|
+
},
|
|
89
108
|
dryRun: options.dryRun ?? false,
|
|
90
109
|
workers: options.workers,
|
|
91
110
|
overwrite: options.overwrite ?? false,
|
|
@@ -111,6 +130,10 @@ else {
|
|
|
111
130
|
}
|
|
112
131
|
const result = await transformStdin({
|
|
113
132
|
transforms: transformsResult.transforms,
|
|
133
|
+
transformOptions: {
|
|
134
|
+
...getDefaultTransformOptions(),
|
|
135
|
+
unsafeStabilizeTopLevelBindings: shouldUnsafeStabilize(options.safeStabilizeTopLevelBindings),
|
|
136
|
+
},
|
|
114
137
|
verbose: options.verbose ?? false,
|
|
115
138
|
});
|
|
116
139
|
if (!result.ok) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArrayPattern, isAssignmentPattern, isClassDeclaration, isExportNamedDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isObjectPattern, isObjectProperty, isRestElement, isVariableDeclaration, } from "@babel/types";
|
|
1
|
+
import { isArrayPattern, isAssignmentPattern, isClassDeclaration, isExportDefaultDeclaration, isExportNamedDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isObjectPattern, isObjectProperty, isRestElement, isVariableDeclaration, } from "@babel/types";
|
|
2
2
|
const addBindingNamesFromNode = (node, out) => {
|
|
3
3
|
const babelNode = node;
|
|
4
4
|
if (!babelNode)
|
|
@@ -38,6 +38,22 @@ const addBindingNamesFromNode = (node, out) => {
|
|
|
38
38
|
export const collectExportedNames = (program) => {
|
|
39
39
|
const exportedNames = new Set();
|
|
40
40
|
for (const statement of program.body) {
|
|
41
|
+
if (isExportDefaultDeclaration(statement)) {
|
|
42
|
+
const declaration = statement.declaration;
|
|
43
|
+
if (isIdentifier(declaration)) {
|
|
44
|
+
// `export default foo` doesn't declare a new binding, but `foo` is still
|
|
45
|
+
// part of the module's public surface, so be conservative and skip it.
|
|
46
|
+
exportedNames.add(declaration.name);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isFunctionDeclaration(declaration) ||
|
|
50
|
+
isClassDeclaration(declaration)) {
|
|
51
|
+
const id = declaration.id;
|
|
52
|
+
if (id)
|
|
53
|
+
exportedNames.add(id.name);
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
41
57
|
if (!isExportNamedDeclaration(statement))
|
|
42
58
|
continue;
|
|
43
59
|
const declaration = statement.declaration;
|
|
@@ -55,13 +71,17 @@ export const collectExportedNames = (program) => {
|
|
|
55
71
|
exportedNames.add(id.name);
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
// Re-exports like `export { foo } from "mod"` do not reference local bindings,
|
|
75
|
+
// so they should not affect rename-skipping decisions.
|
|
76
|
+
if (!statement.source) {
|
|
77
|
+
for (const specifier of statement.specifiers) {
|
|
78
|
+
if (!isExportSpecifier(specifier))
|
|
79
|
+
continue;
|
|
80
|
+
const local = specifier.local;
|
|
81
|
+
if (!isIdentifier(local))
|
|
82
|
+
continue;
|
|
83
|
+
exportedNames.add(local.name);
|
|
84
|
+
}
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
87
|
return exportedNames;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type DefaultTransformOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* When true, `stabilize-top-level-bindings` runs even if the file contains
|
|
4
|
+
* dynamic-name hazards (e.g. eval/with). This can improve diff stability and
|
|
5
|
+
* readability, but the transformed output may not be runnable.
|
|
6
|
+
*/
|
|
7
|
+
unsafeStabilizeTopLevelBindings: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const getDefaultTransformOptions: () => DefaultTransformOptions;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { createHasBindingOptionsUpToScope } from "./create-has-binding-options-up-to-scope.js";
|
|
3
|
+
export const hasShadowingRisk = (binding, bindingScope, targetName) => {
|
|
4
|
+
// Include reassignment sites (`x = ...`) in addition to references so we also catch
|
|
5
|
+
// potential capture/shadowing risks at write locations (see has-shadowing-risk.test.ts).
|
|
6
|
+
const identifierPaths = [
|
|
7
|
+
...binding.referencePaths,
|
|
8
|
+
...binding.constantViolations,
|
|
9
|
+
];
|
|
10
|
+
return identifierPaths.some((referencePath) => {
|
|
11
|
+
if (referencePath.scope === bindingScope)
|
|
12
|
+
return false;
|
|
13
|
+
// Inside a `with` statement, identifier resolution is runtime-dependent.
|
|
14
|
+
// Be conservative and refuse renames that touch such references.
|
|
15
|
+
if (referencePath.findParent((path) => {
|
|
16
|
+
return t.isWithStatement(path.node);
|
|
17
|
+
})) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
// Only consider bindings between this reference’s scope and the binding scope.
|
|
21
|
+
// This avoids incorrectly blocking safe renames due to unrelated outer bindings.
|
|
22
|
+
return referencePath.scope.hasBinding(targetName, createHasBindingOptionsUpToScope(bindingScope));
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* The `$` prefix signals that a variable has been renamed stably by a transform
|
|
5
5
|
* and should be skipped by other transforms.
|
|
6
6
|
*
|
|
7
|
+
* Reserved prefixes:
|
|
8
|
+
* - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
|
|
9
|
+
*
|
|
7
10
|
* - Stable names (`$timeoutId`): Readable AND deterministic across versions
|
|
8
11
|
* - Readable names (`timeoutId`): Semantic but order-dependent
|
|
9
12
|
*/
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* The `$` prefix signals that a variable has been renamed stably by a transform
|
|
5
5
|
* and should be skipped by other transforms.
|
|
6
6
|
*
|
|
7
|
+
* Reserved prefixes:
|
|
8
|
+
* - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
|
|
9
|
+
*
|
|
7
10
|
* - Stable names (`$timeoutId`): Readable AND deterministic across versions
|
|
8
11
|
* - Readable names (`timeoutId`): Semantic but order-dependent
|
|
9
12
|
*/
|
|
13
|
+
import { hasShadowingRisk } from "./has-shadowing-risk.js";
|
|
10
14
|
import { isTypeOnlyGlobalReference } from "./is-type-only-global-reference.js";
|
|
11
15
|
const STABLE_PREFIX = "$";
|
|
12
16
|
/**
|
|
@@ -23,19 +27,6 @@ export const isStableRenamed = (name) => {
|
|
|
23
27
|
const makeStableName = (baseName) => {
|
|
24
28
|
return `${STABLE_PREFIX}${baseName}`;
|
|
25
29
|
};
|
|
26
|
-
const hasShadowingRisk = (binding, bindingScope, targetName) => binding.referencePaths.some((referencePath) => {
|
|
27
|
-
if (referencePath.scope === bindingScope)
|
|
28
|
-
return false;
|
|
29
|
-
// Only consider bindings between this reference’s scope and the binding scope.
|
|
30
|
-
// This avoids incorrectly blocking safe renames due to unrelated outer bindings.
|
|
31
|
-
const hasBindingOptions = {
|
|
32
|
-
noGlobals: true,
|
|
33
|
-
noUids: true,
|
|
34
|
-
// Present in Babel scope implementation, but missing from our TypeScript types.
|
|
35
|
-
upToScope: bindingScope,
|
|
36
|
-
};
|
|
37
|
-
return referencePath.scope.hasBinding(targetName, hasBindingOptions);
|
|
38
|
-
});
|
|
39
30
|
// Cache is keyed by the program `Scope` instance produced by Babel traversal,
|
|
40
31
|
// so it won't leak across separate traversals / transform passes.
|
|
41
32
|
const programValueGlobalsCache = new WeakMap();
|
|
@@ -105,33 +96,36 @@ const findAvailableName = (scope, binding, baseName, startIndex, canBeStable, cu
|
|
|
105
96
|
? false
|
|
106
97
|
: scope.hasBinding(candidateStable) ||
|
|
107
98
|
programGlobals.has(candidateStable);
|
|
108
|
-
if (canBeStable) {
|
|
109
|
-
if (stableTaken) {
|
|
110
|
-
index++;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
// Stable names (`$foo`) don't collide with readable names (`foo`), so when
|
|
114
|
-
// we know we'll emit a stable name, we only need to check the stable slot.
|
|
115
|
-
const name = candidateStable;
|
|
116
|
-
if (binding && hasShadowingRisk(binding, scope, name)) {
|
|
117
|
-
index++;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
return { name, nextIndex: index + 1 };
|
|
121
|
-
}
|
|
122
99
|
const readableTaken = candidateReadable === currentName
|
|
123
100
|
? false
|
|
124
101
|
: scope.hasBinding(candidateReadable) ||
|
|
125
102
|
programGlobals.has(candidateReadable);
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
index
|
|
130
|
-
|
|
103
|
+
if (canBeStable) {
|
|
104
|
+
if (!stableTaken &&
|
|
105
|
+
(!binding || !hasShadowingRisk(binding, scope, candidateStable))) {
|
|
106
|
+
return { name: candidateStable, nextIndex: index + 1 };
|
|
107
|
+
}
|
|
108
|
+
// Prefer stable names, but fall back to the readable form (same suffix)
|
|
109
|
+
// before incrementing when the stable variant is blocked by shadowing.
|
|
110
|
+
if (!stableTaken &&
|
|
111
|
+
!readableTaken &&
|
|
112
|
+
(!binding || !hasShadowingRisk(binding, scope, candidateReadable))) {
|
|
113
|
+
return { name: candidateReadable, nextIndex: index + 1 };
|
|
131
114
|
}
|
|
132
|
-
|
|
115
|
+
index++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Reserve the stable name slot even for readable renames so a pre-existing
|
|
119
|
+
// `$foo` blocks introducing `foo` (keeps stable/readable "slots" aligned).
|
|
120
|
+
if (stableTaken || readableTaken) {
|
|
121
|
+
index++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (binding && hasShadowingRisk(binding, scope, candidateReadable)) {
|
|
125
|
+
index++;
|
|
126
|
+
continue;
|
|
133
127
|
}
|
|
134
|
-
index
|
|
128
|
+
return { name: candidateReadable, nextIndex: index + 1 };
|
|
135
129
|
}
|
|
136
130
|
};
|
|
137
131
|
/**
|
package/dist/core/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type ProjectGraph = {
|
|
|
14
14
|
export type TransformContext = {
|
|
15
15
|
projectGraph: ProjectGraph;
|
|
16
16
|
currentFile: SourceFileInfo | undefined;
|
|
17
|
-
options: Record<string, unknown
|
|
17
|
+
options: Readonly<Record<string, unknown>>;
|
|
18
18
|
};
|
|
19
19
|
export type TransformStats = {
|
|
20
20
|
nodesVisited: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { buildProjectGraph } from "../../core/project-graph.js";
|
|
4
|
+
import { getDefaultTransformOptions } from "../../core/default-transform-options.js";
|
|
4
5
|
import { transformPresets } from "../../transforms/transform-presets.js";
|
|
5
6
|
import { transformRegistry } from "../../transforms/transform-registry.js";
|
|
6
7
|
import { formatCode } from "./format-code.js";
|
|
@@ -20,11 +21,12 @@ const runTransform = async (inputPath, transformId) => {
|
|
|
20
21
|
return transform;
|
|
21
22
|
});
|
|
22
23
|
const projectGraph = buildProjectGraph([{ path: inputPath, content }]);
|
|
24
|
+
const options = getDefaultTransformOptions();
|
|
23
25
|
for (const transform of transforms) {
|
|
24
26
|
await transform.transform({
|
|
25
27
|
projectGraph,
|
|
26
28
|
currentFile: undefined,
|
|
27
|
-
options
|
|
29
|
+
options,
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
const fileInfo = projectGraph.files.get(inputPath);
|