miniread 1.67.0 → 1.69.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/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/fingerprint-scalar-fields.js +20 -64
- package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/serialize-fingerprint-node.js +8 -10
- package/dist/core/stable-naming.d.ts +9 -0
- package/dist/core/stable-naming.js +9 -0
- package/dist/transforms/_generated/manifest.js +18 -0
- package/dist/transforms/_generated/registry.js +6 -0
- package/dist/transforms/preset-stats.json +2 -2
- package/dist/transforms/recommended-transform-order.d.ts +1 -1
- package/dist/transforms/recommended-transform-order.js +10 -0
- package/dist/transforms/stabilize-deferred-stable-rhs/check-rhs-stability.d.ts +6 -0
- package/dist/transforms/stabilize-deferred-stable-rhs/check-rhs-stability.js +51 -0
- package/dist/transforms/stabilize-deferred-stable-rhs/manifest.json +13 -0
- package/dist/transforms/stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.d.ts +2 -0
- package/dist/transforms/stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.js +125 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/collect-deferred-variables.d.ts +22 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/collect-deferred-variables.js +115 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/manifest.json +13 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/rename-helpers.d.ts +24 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/rename-helpers.js +92 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/stabilize-deferred-top-level-bindings-transform.d.ts +2 -0
- package/dist/transforms/stabilize-deferred-top-level-bindings/stabilize-deferred-top-level-bindings-transform.js +68 -0
- package/dist/transforms/stabilize-nested-bindings/apply-renames.d.ts +9 -0
- package/dist/transforms/stabilize-nested-bindings/apply-renames.js +50 -0
- package/dist/transforms/stabilize-nested-bindings/collect-factory-bindings.d.ts +11 -0
- package/dist/transforms/stabilize-nested-bindings/collect-factory-bindings.js +72 -0
- package/dist/transforms/stabilize-nested-bindings/has-dynamic-name-lookup.d.ts +12 -0
- package/dist/transforms/stabilize-nested-bindings/has-dynamic-name-lookup.js +49 -0
- package/dist/transforms/stabilize-nested-bindings/is-factory-callback.d.ts +6 -0
- package/dist/transforms/stabilize-nested-bindings/is-factory-callback.js +11 -0
- package/dist/transforms/stabilize-nested-bindings/manifest.json +13 -0
- package/dist/transforms/stabilize-nested-bindings/stabilize-nested-bindings-transform.d.ts +2 -0
- package/dist/transforms/stabilize-nested-bindings/stabilize-nested-bindings-transform.js +75 -0
- package/dist/transforms/stabilize-top-level-bindings/collect-rename-candidates.js +1 -1
- package/package.json +1 -1
- /package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/fingerprint-leaf-node.d.ts +0 -0
- /package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/fingerprint-leaf-node.js +0 -0
- /package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/fingerprint-scalar-fields.d.ts +0 -0
- /package/dist/{transforms/stabilize-top-level-bindings/fingerprint-node.d.ts → core/fingerprint/hash-fingerprint-node.d.ts} +0 -0
- /package/dist/{transforms/stabilize-top-level-bindings/fingerprint-node.js → core/fingerprint/hash-fingerprint-node.js} +0 -0
- /package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/serialize-fingerprint-node.d.ts +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
2
|
const encodeScalar = (value) => value === undefined ? "undefined" : JSON.stringify(value);
|
|
3
|
+
const extractKeyName = (key) => {
|
|
4
|
+
if (t.isIdentifier(key))
|
|
5
|
+
return key.name;
|
|
6
|
+
if (t.isStringLiteral(key))
|
|
7
|
+
return key.value;
|
|
8
|
+
if (t.isNumericLiteral(key))
|
|
9
|
+
return String(key.value);
|
|
10
|
+
if (t.isPrivateName(key))
|
|
11
|
+
return `#${key.id.name}`;
|
|
12
|
+
return undefined;
|
|
13
|
+
};
|
|
3
14
|
export const fingerprintScalarFields = (node) => {
|
|
4
15
|
if (node.type === "AssignmentExpression" ||
|
|
5
16
|
node.type === "BinaryExpression" ||
|
|
@@ -59,42 +70,13 @@ export const fingerprintScalarFields = (node) => {
|
|
|
59
70
|
`static=${encodeScalar(node.static)}`,
|
|
60
71
|
`computed=${encodeScalar(node.computed)}`,
|
|
61
72
|
];
|
|
62
|
-
if (node.type === "ObjectMethod"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (computed === true) {
|
|
66
|
-
// For computed keys, the fingerprint comes from the key expression itself.
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
const keyName = t.isIdentifier(key)
|
|
70
|
-
? key.name
|
|
71
|
-
: t.isStringLiteral(key)
|
|
72
|
-
? key.value
|
|
73
|
-
: t.isNumericLiteral(key)
|
|
74
|
-
? String(key.value)
|
|
75
|
-
: t.isPrivateName(key)
|
|
76
|
-
? `#${key.id.name}`
|
|
77
|
-
: undefined;
|
|
78
|
-
fields.push(`keyName=${encodeScalar(keyName)}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (node.type === "ClassMethod" || node.type === "ClassPrivateMethod") {
|
|
73
|
+
if (node.type === "ObjectMethod" ||
|
|
74
|
+
node.type === "ClassMethod" ||
|
|
75
|
+
node.type === "ClassPrivateMethod") {
|
|
82
76
|
const computed = node.computed;
|
|
83
77
|
const key = node.key;
|
|
84
|
-
if (computed
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
const keyName = t.isIdentifier(key)
|
|
89
|
-
? key.name
|
|
90
|
-
: t.isStringLiteral(key)
|
|
91
|
-
? key.value
|
|
92
|
-
: t.isNumericLiteral(key)
|
|
93
|
-
? String(key.value)
|
|
94
|
-
: t.isPrivateName(key)
|
|
95
|
-
? `#${key.id.name}`
|
|
96
|
-
: undefined;
|
|
97
|
-
fields.push(`keyName=${encodeScalar(keyName)}`);
|
|
78
|
+
if (computed !== true) {
|
|
79
|
+
fields.push(`keyName=${encodeScalar(extractKeyName(key))}`);
|
|
98
80
|
}
|
|
99
81
|
}
|
|
100
82
|
return fields;
|
|
@@ -103,36 +85,15 @@ export const fingerprintScalarFields = (node) => {
|
|
|
103
85
|
const computed = node.computed;
|
|
104
86
|
const key = node.key;
|
|
105
87
|
const fields = [`computed=${encodeScalar(computed)}`];
|
|
106
|
-
if (computed
|
|
107
|
-
|
|
108
|
-
return fields;
|
|
88
|
+
if (computed !== true) {
|
|
89
|
+
fields.push(`keyName=${encodeScalar(extractKeyName(key))}`);
|
|
109
90
|
}
|
|
110
|
-
const keyName = t.isIdentifier(key)
|
|
111
|
-
? key.name
|
|
112
|
-
: t.isStringLiteral(key)
|
|
113
|
-
? key.value
|
|
114
|
-
: t.isNumericLiteral(key)
|
|
115
|
-
? String(key.value)
|
|
116
|
-
: t.isPrivateName(key)
|
|
117
|
-
? `#${key.id.name}`
|
|
118
|
-
: undefined;
|
|
119
|
-
fields.push(`keyName=${encodeScalar(keyName)}`);
|
|
120
91
|
return fields;
|
|
121
92
|
}
|
|
122
93
|
if (node.type === "ClassProperty" || node.type === "ClassPrivateProperty") {
|
|
123
94
|
const computed = node.computed;
|
|
124
95
|
const key = node.key;
|
|
125
|
-
const keyName = computed === true
|
|
126
|
-
? undefined
|
|
127
|
-
: t.isIdentifier(key)
|
|
128
|
-
? key.name
|
|
129
|
-
: t.isStringLiteral(key)
|
|
130
|
-
? key.value
|
|
131
|
-
: t.isNumericLiteral(key)
|
|
132
|
-
? String(key.value)
|
|
133
|
-
: t.isPrivateName(key)
|
|
134
|
-
? `#${key.id.name}`
|
|
135
|
-
: undefined;
|
|
96
|
+
const keyName = computed === true ? undefined : extractKeyName(key);
|
|
136
97
|
return [
|
|
137
98
|
`static=${encodeScalar(node.static)}`,
|
|
138
99
|
`computed=${encodeScalar(computed)}`,
|
|
@@ -142,12 +103,7 @@ export const fingerprintScalarFields = (node) => {
|
|
|
142
103
|
if (node.type === "ImportSpecifier") {
|
|
143
104
|
const imported = node
|
|
144
105
|
.imported;
|
|
145
|
-
|
|
146
|
-
? imported.name
|
|
147
|
-
: t.isStringLiteral(imported)
|
|
148
|
-
? imported.value
|
|
149
|
-
: undefined;
|
|
150
|
-
return [`importedName=${encodeScalar(importedName)}`];
|
|
106
|
+
return [`importedName=${encodeScalar(extractKeyName(imported))}`];
|
|
151
107
|
}
|
|
152
108
|
return [];
|
|
153
109
|
};
|
|
@@ -26,17 +26,15 @@ const pushNodeTasks = (stack, node) => {
|
|
|
26
26
|
let visitorKeys = keys;
|
|
27
27
|
// For non-computed member keys, we already serialize `keyName` via scalar fields.
|
|
28
28
|
// Skipping the `key` child keeps fingerprints stable across equivalent key syntax
|
|
29
|
-
// like `{ foo: 1 }` vs `{
|
|
29
|
+
// like `{ foo: 1 }` vs `{ "foo": 1 }`.
|
|
30
30
|
const computed = node.computed;
|
|
31
|
-
if (computed
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
node.type === "ClassProperty" ||
|
|
39
|
-
node.type === "ClassPrivateProperty") {
|
|
31
|
+
if (computed !== true &&
|
|
32
|
+
(node.type === "ObjectProperty" ||
|
|
33
|
+
node.type === "ObjectMethod" ||
|
|
34
|
+
node.type === "ClassMethod" ||
|
|
35
|
+
node.type === "ClassPrivateMethod" ||
|
|
36
|
+
node.type === "ClassProperty" ||
|
|
37
|
+
node.type === "ClassPrivateProperty")) {
|
|
40
38
|
visitorKeys = keys.filter((key) => key !== "key");
|
|
41
39
|
}
|
|
42
40
|
const nodeTasks = [{ kind: "text", text: node.type }];
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Reserved prefixes:
|
|
8
8
|
* - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
|
|
9
|
+
* - `$f_`: used by `stabilize-nested-bindings` for factory-local content-hash-based names.
|
|
10
|
+
* - `$v_`: used by `stabilize-deferred-top-level-bindings` and `stabilize-deferred-stable-rhs`
|
|
11
|
+
* for deferred variable names derived from enclosing function or RHS hashes.
|
|
9
12
|
*
|
|
10
13
|
* - Stable names (`$timeoutId`): Readable AND deterministic across versions
|
|
11
14
|
* - Readable names (`timeoutId`): Semantic but order-dependent
|
|
@@ -16,6 +19,12 @@ import type { Scope } from "@babel/traverse";
|
|
|
16
19
|
* Transforms should skip variables with this prefix.
|
|
17
20
|
*/
|
|
18
21
|
export declare const isStableRenamed: (name: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the name uses a content-hash-based stable prefix
|
|
24
|
+
* (`$h_`, `$f_`, or `$v_`). Use this to check whether a binding was
|
|
25
|
+
* stabilized by one of the hash-based stabilization transforms.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isHashBasedStableName: (name: string) => boolean;
|
|
19
28
|
/**
|
|
20
29
|
* Entry for a planned rename operation.
|
|
21
30
|
*/
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Reserved prefixes:
|
|
8
8
|
* - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
|
|
9
|
+
* - `$f_`: used by `stabilize-nested-bindings` for factory-local content-hash-based names.
|
|
10
|
+
* - `$v_`: used by `stabilize-deferred-top-level-bindings` and `stabilize-deferred-stable-rhs`
|
|
11
|
+
* for deferred variable names derived from enclosing function or RHS hashes.
|
|
9
12
|
*
|
|
10
13
|
* - Stable names (`$timeoutId`): Readable AND deterministic across versions
|
|
11
14
|
* - Readable names (`timeoutId`): Semantic but order-dependent
|
|
@@ -20,6 +23,12 @@ const STABLE_PREFIX = "$";
|
|
|
20
23
|
export const isStableRenamed = (name) => {
|
|
21
24
|
return name.startsWith(STABLE_PREFIX);
|
|
22
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the name uses a content-hash-based stable prefix
|
|
28
|
+
* (`$h_`, `$f_`, or `$v_`). Use this to check whether a binding was
|
|
29
|
+
* stabilized by one of the hash-based stabilization transforms.
|
|
30
|
+
*/
|
|
31
|
+
export const isHashBasedStableName = (name) => name.startsWith("$h_") || name.startsWith("$f_") || name.startsWith("$v_");
|
|
23
32
|
/**
|
|
24
33
|
* Creates a stable name by adding the $ prefix.
|
|
25
34
|
* Internal helper - prefer using RenameGroup which handles stability logic automatically.
|
|
@@ -334,6 +334,21 @@ const manifestData = {
|
|
|
334
334
|
notes: "Auto-added by evaluation script. Measured with baseline none: -0.28%. Enabled in the recommended preset for readability and to normalize variable declarations even when line diffs increase.",
|
|
335
335
|
evaluations: { "legacy:evaluation": { "evaluatedAt": "2026-01-25T20:21:14.926Z", "changedLines": 208592, "durationSeconds": 0, "stableNames": 0, "diffSizePercent": 100.27969913444457 }, "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100.27969913444457, "evaluatedAt": "2026-01-25T23:41:19.807Z", "changedLines": 208592, "durationSeconds": 65.61050708399999, "stableNames": 1357 } },
|
|
336
336
|
},
|
|
337
|
+
"stabilize-deferred-stable-rhs": {
|
|
338
|
+
recommended: true,
|
|
339
|
+
notes: "Stabilizes deferred top-level vars whose RHS contains only stable identifiers (globals or $h_/$f_/$v_ prefixed names). Derives names by hashing the RHS code. Complements stabilize-deferred-top-level-bindings by handling multi-var functions.",
|
|
340
|
+
evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 95.54, "evaluatedAt": "2026-02-05T18:30:00.000Z", "changedLines": 3730, "durationSeconds": 30, "stableNames": 1741 } },
|
|
341
|
+
},
|
|
342
|
+
"stabilize-deferred-top-level-bindings": {
|
|
343
|
+
recommended: true,
|
|
344
|
+
notes: "Stabilizes top-level var declarations that are initialized inside stable-named functions (lazy init pattern). Only applies when the enclosing function has exactly one such deferred var to avoid order-dependent naming. Renames ~750 vars but diff reduction is minimal (0.03%) because most vars already have consistent minified names between versions. The stabilization helps when minifiers do vary names.",
|
|
345
|
+
evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 99.97, "evaluatedAt": "2026-02-05T17:32:00.000Z", "changedLines": 753, "durationSeconds": 26, "stableNames": 2110 } },
|
|
346
|
+
},
|
|
347
|
+
"stabilize-nested-bindings": {
|
|
348
|
+
recommended: true,
|
|
349
|
+
notes: "Stabilizes local bindings inside CommonJS factory callbacks using content-hash naming ($f_<hash>). Must run AFTER stabilize-top-level-bindings. Combined with top-level stabilization, achieves 33.49% diff size (67% reduction from baseline).",
|
|
350
|
+
evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 43.68, "evaluatedAt": "2026-02-05T13:38:00.000Z", "changedLines": 47159, "durationSeconds": 60, "stableNames": 14374 } },
|
|
351
|
+
},
|
|
337
352
|
"stabilize-top-level-bindings": {
|
|
338
353
|
recommended: true,
|
|
339
354
|
notes: "Renames program-scope bindings to stable `$h_<hash>` names (simple identifiers only; destructuring patterns are skipped; uninitialized var/let bindings are stabilized only via direct top-level `x = ...` assignment statements; collisions are disambiguated with an encounter-order suffix). For classic scripts/UMD bundles, renaming globals can still be risky; see transform description and safety gates. Note: in scripts, sloppy-mode `this.foo` is treated conservatively as a possible global-object property lookup, and dynamic computed global-object lookups (including `this[expr]`) cause `var`/`function` renames to be skipped for the file. Default behavior is now unsafe with respect to dynamic-name hazards (e.g. direct eval): it will still run, which improves diff stability but may produce non-runnable output.",
|
|
@@ -371,6 +386,9 @@ export const manifestEntries = Object.entries(transformRegistry)
|
|
|
371
386
|
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
372
387
|
export const recommendedTransformIds = [
|
|
373
388
|
"stabilize-top-level-bindings",
|
|
389
|
+
"stabilize-nested-bindings",
|
|
390
|
+
"stabilize-deferred-top-level-bindings",
|
|
391
|
+
"stabilize-deferred-stable-rhs",
|
|
374
392
|
"expand-boolean-literals",
|
|
375
393
|
"expand-sequence-expressions-v5",
|
|
376
394
|
"expand-special-number-literals",
|
|
@@ -68,6 +68,9 @@ import { replaceDynamicRequireEvalTransform } from "../replace-dynamic-require-e
|
|
|
68
68
|
import { simplifyBooleanNegationsTransform } from "../simplify-boolean-negations/simplify-boolean-negations-transform.js";
|
|
69
69
|
import { simplifyStringTrimTransform } from "../simplify-string-trim/simplify-string-trim-transform.js";
|
|
70
70
|
import { splitVariableDeclarationsTransform } from "../split-variable-declarations/split-variable-declarations-transform.js";
|
|
71
|
+
import { stabilizeDeferredStableRhsTransform } from "../stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.js";
|
|
72
|
+
import { stabilizeDeferredTopLevelBindingsTransform } from "../stabilize-deferred-top-level-bindings/stabilize-deferred-top-level-bindings-transform.js";
|
|
73
|
+
import { stabilizeNestedBindingsTransform } from "../stabilize-nested-bindings/stabilize-nested-bindings-transform.js";
|
|
71
74
|
import { stabilizeTopLevelBindingsTransform } from "../stabilize-top-level-bindings/stabilize-top-level-bindings-transform.js";
|
|
72
75
|
import { useObjectPropertyShorthandTransform } from "../use-object-property-shorthand/use-object-property-shorthand-transform.js";
|
|
73
76
|
import { useObjectShorthandTransform } from "../use-object-shorthand/use-object-shorthand-transform.js";
|
|
@@ -141,6 +144,9 @@ export const transformRegistry = {
|
|
|
141
144
|
[simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
|
|
142
145
|
[simplifyStringTrimTransform.id]: simplifyStringTrimTransform,
|
|
143
146
|
[splitVariableDeclarationsTransform.id]: splitVariableDeclarationsTransform,
|
|
147
|
+
[stabilizeDeferredStableRhsTransform.id]: stabilizeDeferredStableRhsTransform,
|
|
148
|
+
[stabilizeDeferredTopLevelBindingsTransform.id]: stabilizeDeferredTopLevelBindingsTransform,
|
|
149
|
+
[stabilizeNestedBindingsTransform.id]: stabilizeNestedBindingsTransform,
|
|
144
150
|
[stabilizeTopLevelBindingsTransform.id]: stabilizeTopLevelBindingsTransform,
|
|
145
151
|
[useObjectPropertyShorthandTransform.id]: useObjectPropertyShorthandTransform,
|
|
146
152
|
[useObjectShorthandTransform.id]: useObjectShorthandTransform,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"recommended": {
|
|
3
|
-
"diffSizePercent":
|
|
4
|
-
"notes": "Measured with baseline none:
|
|
3
|
+
"diffSizePercent": 33.47412783006388,
|
|
4
|
+
"notes": "Measured with baseline none: 33.47% of original diff."
|
|
5
5
|
}
|
|
6
6
|
}
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* This lets us tune transform interactions intentionally instead of relying on
|
|
5
5
|
* alphabetical ID sorting.
|
|
6
6
|
*/
|
|
7
|
-
export declare const recommendedTransformOrder: readonly ["stabilize-top-level-bindings", "expand-boolean-literals", "expand-sequence-expressions-v5", "expand-special-number-literals", "expand-typeof-undefined-comparisons", "expand-undefined-literals", "remove-redundant-else", "rename-arguments-length-flags", "rename-asap-wrappers", "rename-awaiter-parameters", "rename-awaiter-helper-functions", "rename-buffer-variables", "rename-to-buffer-results", "rename-catch-parameters", "rename-promise-catch-parameters", "rename-char-code-at", "rename-charcode-variables-v2", "rename-client-aliases", "rename-comparison-flags", "rename-date-now-start-times", "rename-default-options-parameters", "rename-deferred-resolve-parameters", "rename-destructured-aliases", "rename-rest-parameters", "rename-execfile-arguments", "rename-error-first-callback-parameters", "rename-error-variables", "rename-event-parameters", "rename-http-server-parameters", "rename-fs-sync-variables", "rename-http-method-parameters", "rename-interval-ids", "rename-loop-index-variables-v3", "rename-loop-length-variables", "rename-document-fragment-variables", "rename-object-keys-variables", "rename-object-keys-iterator-variables", "rename-parameters-to-match-properties-v2", "rename-promise-executor-parameters-v2", "rename-range-parameters", "rename-read-file-lines", "rename-regex-builders", "rename-search-parameters-variables", "rename-string-split-variables", "rename-this-aliases", "rename-timeout-ids", "rename-typeof-variables", "rename-typescript-helper-aliases", "rename-uint8array-concat-variables", "rename-url-parameters", "rename-url-variables", "rename-use-reference-guards-v2", "rename-use-reference-sets", "rename-worker-handles", "simplify-boolean-negations", "simplify-string-trim", "split-variable-declarations", "use-optional-chaining", "use-object-property-shorthand", "use-object-shorthand", "replace-dynamic-require-eval"];
|
|
7
|
+
export declare const recommendedTransformOrder: readonly ["stabilize-top-level-bindings", "stabilize-nested-bindings", "stabilize-deferred-top-level-bindings", "stabilize-deferred-stable-rhs", "expand-boolean-literals", "expand-sequence-expressions-v5", "expand-special-number-literals", "expand-typeof-undefined-comparisons", "expand-undefined-literals", "remove-redundant-else", "rename-arguments-length-flags", "rename-asap-wrappers", "rename-awaiter-parameters", "rename-awaiter-helper-functions", "rename-buffer-variables", "rename-to-buffer-results", "rename-catch-parameters", "rename-promise-catch-parameters", "rename-char-code-at", "rename-charcode-variables-v2", "rename-client-aliases", "rename-comparison-flags", "rename-date-now-start-times", "rename-default-options-parameters", "rename-deferred-resolve-parameters", "rename-destructured-aliases", "rename-rest-parameters", "rename-execfile-arguments", "rename-error-first-callback-parameters", "rename-error-variables", "rename-event-parameters", "rename-http-server-parameters", "rename-fs-sync-variables", "rename-http-method-parameters", "rename-interval-ids", "rename-loop-index-variables-v3", "rename-loop-length-variables", "rename-document-fragment-variables", "rename-object-keys-variables", "rename-object-keys-iterator-variables", "rename-parameters-to-match-properties-v2", "rename-promise-executor-parameters-v2", "rename-range-parameters", "rename-read-file-lines", "rename-regex-builders", "rename-search-parameters-variables", "rename-string-split-variables", "rename-this-aliases", "rename-timeout-ids", "rename-typeof-variables", "rename-typescript-helper-aliases", "rename-uint8array-concat-variables", "rename-url-parameters", "rename-url-variables", "rename-use-reference-guards-v2", "rename-use-reference-sets", "rename-worker-handles", "simplify-boolean-negations", "simplify-string-trim", "split-variable-declarations", "use-optional-chaining", "use-object-property-shorthand", "use-object-shorthand", "replace-dynamic-require-eval"];
|
|
@@ -8,6 +8,16 @@ export const recommendedTransformOrder = [
|
|
|
8
8
|
// Run first: gives all bindings stable names before other transforms run.
|
|
9
9
|
// Re-parses AST after renaming so subsequent transforms get fresh scope bindings.
|
|
10
10
|
"stabilize-top-level-bindings",
|
|
11
|
+
// Run second: stabilizes local bindings inside CommonJS factory callbacks.
|
|
12
|
+
// Depends on stabilize-top-level-bindings having renamed factory wrapper functions to $h_*.
|
|
13
|
+
"stabilize-nested-bindings",
|
|
14
|
+
// Run third: stabilizes top-level vars that are assigned inside stable-named functions.
|
|
15
|
+
// Depends on stabilize-top-level-bindings having renamed enclosing functions to $h_*.
|
|
16
|
+
"stabilize-deferred-top-level-bindings",
|
|
17
|
+
// Run fourth: stabilizes remaining deferred vars whose RHS has only stable identifiers.
|
|
18
|
+
// Depends on stabilize-top-level-bindings and stabilize-deferred-top-level-bindings
|
|
19
|
+
// having renamed identifiers to $h_*/$v_*.
|
|
20
|
+
"stabilize-deferred-stable-rhs",
|
|
11
21
|
"expand-boolean-literals",
|
|
12
22
|
"expand-sequence-expressions-v5",
|
|
13
23
|
"expand-special-number-literals",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Expression } from "@babel/types";
|
|
2
|
+
/**
|
|
3
|
+
* Check if all identifier references in an expression are stable
|
|
4
|
+
* (globals or already-stabilized names).
|
|
5
|
+
*/
|
|
6
|
+
export declare const hasAllStableReferences: (expression: Expression, programScopeBindings: Set<string>) => boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isHashBasedStableName } from "../../core/stable-naming.js";
|
|
2
|
+
/**
|
|
3
|
+
* Check if all identifier references in an expression are stable
|
|
4
|
+
* (globals or already-stabilized names).
|
|
5
|
+
*/
|
|
6
|
+
export const hasAllStableReferences = (expression, programScopeBindings) => {
|
|
7
|
+
const identifiers = collectIdentifierReferences(expression);
|
|
8
|
+
return identifiers.every((name) => isHashBasedStableName(name) || !programScopeBindings.has(name));
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Collect all identifier references in an expression,
|
|
12
|
+
* skipping non-computed property keys in member expressions and object literals.
|
|
13
|
+
*/
|
|
14
|
+
const collectIdentifierReferences = (node) => {
|
|
15
|
+
if (!node || typeof node !== "object")
|
|
16
|
+
return [];
|
|
17
|
+
const astNode = node;
|
|
18
|
+
if (astNode.type === "Identifier")
|
|
19
|
+
return [astNode.name];
|
|
20
|
+
if ((astNode.type === "MemberExpression" ||
|
|
21
|
+
astNode.type === "OptionalMemberExpression") &&
|
|
22
|
+
!astNode.computed) {
|
|
23
|
+
return collectIdentifierReferences(astNode.object);
|
|
24
|
+
}
|
|
25
|
+
// Skip non-computed, non-shorthand ObjectProperty keys — they're labels, not references.
|
|
26
|
+
if (astNode.type === "ObjectProperty" &&
|
|
27
|
+
!astNode.computed &&
|
|
28
|
+
!astNode.shorthand) {
|
|
29
|
+
return collectIdentifierReferences(astNode.value);
|
|
30
|
+
}
|
|
31
|
+
const result = [];
|
|
32
|
+
for (const [key, value] of Object.entries(astNode)) {
|
|
33
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc")
|
|
34
|
+
continue;
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
for (const item of value) {
|
|
37
|
+
if (item &&
|
|
38
|
+
typeof item === "object" &&
|
|
39
|
+
item.type) {
|
|
40
|
+
result.push(...collectIdentifierReferences(item));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (value &&
|
|
45
|
+
typeof value === "object" &&
|
|
46
|
+
value.type) {
|
|
47
|
+
result.push(...collectIdentifierReferences(value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recommended": true,
|
|
3
|
+
"notes": "Stabilizes deferred top-level vars whose RHS contains only stable identifiers (globals or $h_/$f_/$v_ prefixed names). Derives names by hashing the RHS code. Complements stabilize-deferred-top-level-bindings by handling multi-var functions.",
|
|
4
|
+
"evaluations": {
|
|
5
|
+
"claude-code-2.1.10:claude-code-2.1.11": {
|
|
6
|
+
"diffSizePercent": 95.54,
|
|
7
|
+
"evaluatedAt": "2026-02-05T18:30:00.000Z",
|
|
8
|
+
"changedLines": 3730,
|
|
9
|
+
"durationSeconds": 30,
|
|
10
|
+
"stableNames": 1741
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/transforms/stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import * as t from "@babel/types";
|
|
5
|
+
import { getFilesToProcess, } from "../../core/types.js";
|
|
6
|
+
import { generateCode } from "../../cli/generate-code.js";
|
|
7
|
+
import { isHashBasedStableName } from "../../core/stable-naming.js";
|
|
8
|
+
import { canRenameBindingSafely, findEnclosingBindingName, renameBindingInPlace, } from "../stabilize-deferred-top-level-bindings/rename-helpers.js";
|
|
9
|
+
import { hasAllStableReferences } from "./check-rhs-stability.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
12
|
+
const traverse = require("@babel/traverse").default;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
14
|
+
const generate = require("@babel/generator").default;
|
|
15
|
+
const hashCode = (code) => createHash("sha256").update(code).digest("hex").slice(0, 16);
|
|
16
|
+
export const stabilizeDeferredStableRhsTransform = {
|
|
17
|
+
id: "stabilize-deferred-stable-rhs",
|
|
18
|
+
description: "Renames deferred top-level vars whose RHS contains only stable identifiers " +
|
|
19
|
+
"(globals or $h_/$f_/$v_ prefixed). Derives stable names by hashing the RHS code. " +
|
|
20
|
+
"Complements stabilize-deferred-top-level-bindings by handling multi-var functions.",
|
|
21
|
+
scope: "file",
|
|
22
|
+
parallelizable: true,
|
|
23
|
+
transform(context) {
|
|
24
|
+
let nodesVisited = 0;
|
|
25
|
+
let transformationsApplied = 0;
|
|
26
|
+
for (const fileInfo of getFilesToProcess(context)) {
|
|
27
|
+
const program = fileInfo.ast.program;
|
|
28
|
+
let programScope;
|
|
29
|
+
traverse(fileInfo.ast, {
|
|
30
|
+
Program(path) {
|
|
31
|
+
programScope = path.scope;
|
|
32
|
+
path.stop();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!programScope)
|
|
36
|
+
continue;
|
|
37
|
+
const programScopeBindings = new Set(Object.keys(programScope.bindings));
|
|
38
|
+
// Step 1: Find all uninitialized top-level vars that aren't already stable
|
|
39
|
+
const uninitializedVariables = new Set();
|
|
40
|
+
traverse(fileInfo.ast, {
|
|
41
|
+
VariableDeclaration(path) {
|
|
42
|
+
nodesVisited++;
|
|
43
|
+
if (path.parent.type !== "Program")
|
|
44
|
+
return;
|
|
45
|
+
if (path.node.kind !== "var")
|
|
46
|
+
return;
|
|
47
|
+
for (const declaration of path.node.declarations) {
|
|
48
|
+
if (!declaration.init &&
|
|
49
|
+
t.isIdentifier(declaration.id) &&
|
|
50
|
+
!isHashBasedStableName(declaration.id.name)) {
|
|
51
|
+
uninitializedVariables.add(declaration.id.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// Step 2: Find assignments inside stable-named functions
|
|
57
|
+
const assignments = [];
|
|
58
|
+
traverse(fileInfo.ast, {
|
|
59
|
+
AssignmentExpression(path) {
|
|
60
|
+
nodesVisited++;
|
|
61
|
+
if (!t.isIdentifier(path.node.left))
|
|
62
|
+
return;
|
|
63
|
+
const variableName = path.node.left.name;
|
|
64
|
+
if (!uninitializedVariables.has(variableName))
|
|
65
|
+
return;
|
|
66
|
+
const functionPath = path.findParent((p) => p.isFunction());
|
|
67
|
+
if (!functionPath)
|
|
68
|
+
return;
|
|
69
|
+
const enclosingFunctionName = findEnclosingBindingName(functionPath);
|
|
70
|
+
if (!enclosingFunctionName ||
|
|
71
|
+
!isHashBasedStableName(enclosingFunctionName))
|
|
72
|
+
return;
|
|
73
|
+
// Check stability and capture RHS code NOW, before any renames happen.
|
|
74
|
+
// This prevents order-dependent cascading where renaming one var
|
|
75
|
+
// makes another var's RHS appear stable.
|
|
76
|
+
const rhsNode = path.node.right;
|
|
77
|
+
const isStable = hasAllStableReferences(rhsNode, programScopeBindings);
|
|
78
|
+
assignments.push({
|
|
79
|
+
variableName,
|
|
80
|
+
rhsNode,
|
|
81
|
+
enclosingFunctionName,
|
|
82
|
+
rhsCode: isStable
|
|
83
|
+
? generate(rhsNode, { concise: true }).code
|
|
84
|
+
: undefined,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
// Step 3: Apply renames for assignments with all-stable RHS.
|
|
89
|
+
// Deduplicate by variable name — if a var has multiple assignments,
|
|
90
|
+
// use only the first (order is AST traversal order).
|
|
91
|
+
// Track used target names to prevent collisions: two vars with identical
|
|
92
|
+
// RHS code produce the same hash, and renameBindingInPlace does not update
|
|
93
|
+
// Babel's scope map, so canRenameBindingSafely would not catch the second.
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const usedNewNames = new Set();
|
|
96
|
+
let fileTransformations = 0;
|
|
97
|
+
for (const { variableName, rhsCode } of assignments) {
|
|
98
|
+
if (!rhsCode)
|
|
99
|
+
continue;
|
|
100
|
+
if (seen.has(variableName))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(variableName);
|
|
103
|
+
const newName = `$v_${hashCode(rhsCode)}`;
|
|
104
|
+
if (usedNewNames.has(newName))
|
|
105
|
+
continue;
|
|
106
|
+
if (!canRenameBindingSafely(programScope, variableName, newName))
|
|
107
|
+
continue;
|
|
108
|
+
if (renameBindingInPlace(programScope, variableName, newName)) {
|
|
109
|
+
usedNewNames.add(newName);
|
|
110
|
+
fileTransformations++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
transformationsApplied += fileTransformations;
|
|
114
|
+
if (fileTransformations > 0) {
|
|
115
|
+
const code = generateCode(fileInfo.ast);
|
|
116
|
+
const newAst = parse(code, {
|
|
117
|
+
sourceType: program.sourceType,
|
|
118
|
+
plugins: ["jsx", "typescript"],
|
|
119
|
+
});
|
|
120
|
+
fileInfo.ast = newAst;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Promise.resolve({ nodesVisited, transformationsApplied });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Scope } from "@babel/traverse";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
type DeferredVariableInfo = {
|
|
4
|
+
variableName: string;
|
|
5
|
+
enclosingFunctionName: string;
|
|
6
|
+
scope: Scope;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Collect information about deferred top-level var declarations.
|
|
10
|
+
* A "deferred var" is declared at top-level without an initializer,
|
|
11
|
+
* then assigned inside a function.
|
|
12
|
+
*/
|
|
13
|
+
export declare const collectDeferredVariables: (ast: t.File, programScope: Scope) => {
|
|
14
|
+
deferredVariables: DeferredVariableInfo[];
|
|
15
|
+
nodesVisited: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Group deferred vars by their enclosing function.
|
|
19
|
+
* Returns only vars where the function has exactly ONE deferred var.
|
|
20
|
+
*/
|
|
21
|
+
export declare const filterToSingleVariableFunctions: (deferredVariables: DeferredVariableInfo[]) => DeferredVariableInfo[];
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
import { isHashBasedStableName } from "../../core/stable-naming.js";
|
|
4
|
+
import { findEnclosingBindingName } from "./rename-helpers.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
7
|
+
const traverse = require("@babel/traverse").default;
|
|
8
|
+
/**
|
|
9
|
+
* Collect information about deferred top-level var declarations.
|
|
10
|
+
* A "deferred var" is declared at top-level without an initializer,
|
|
11
|
+
* then assigned inside a function.
|
|
12
|
+
*/
|
|
13
|
+
export const collectDeferredVariables = (ast, programScope) => {
|
|
14
|
+
let nodesVisited = 0;
|
|
15
|
+
// Step 1: Find all top-level var declarations without initializers
|
|
16
|
+
const uninitializedVariables = new Set();
|
|
17
|
+
traverse(ast, {
|
|
18
|
+
VariableDeclaration(path) {
|
|
19
|
+
nodesVisited++;
|
|
20
|
+
if (path.parent.type !== "Program")
|
|
21
|
+
return;
|
|
22
|
+
if (path.node.kind !== "var")
|
|
23
|
+
return;
|
|
24
|
+
for (const decl of path.node.declarations) {
|
|
25
|
+
if (!decl.init && t.isIdentifier(decl.id)) {
|
|
26
|
+
uninitializedVariables.add(decl.id.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Step 2: Find assignments to these vars and track which function they're in
|
|
32
|
+
const variableAssignments = new Map();
|
|
33
|
+
traverse(ast, {
|
|
34
|
+
AssignmentExpression(path) {
|
|
35
|
+
nodesVisited++;
|
|
36
|
+
if (!t.isIdentifier(path.node.left))
|
|
37
|
+
return;
|
|
38
|
+
const variableName = path.node.left.name;
|
|
39
|
+
if (!uninitializedVariables.has(variableName))
|
|
40
|
+
return;
|
|
41
|
+
// Find enclosing function
|
|
42
|
+
const functionPath = path.findParent((p) => p.isFunction());
|
|
43
|
+
if (!functionPath) {
|
|
44
|
+
// Top-level assignment — mark as unsuitable. Preserve count from any
|
|
45
|
+
// earlier function-scoped assignment so the step-3 filter stays correct.
|
|
46
|
+
const existing = variableAssignments.get(variableName);
|
|
47
|
+
variableAssignments.set(variableName, {
|
|
48
|
+
enclosingFunctionName: undefined,
|
|
49
|
+
count: (existing?.count ?? 0) + 1,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Get the enclosing function's binding name
|
|
54
|
+
const functionName = findEnclosingBindingName(functionPath);
|
|
55
|
+
const existing = variableAssignments.get(variableName);
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.count++;
|
|
58
|
+
// If assigned in different functions or multiple times, mark as unsuitable
|
|
59
|
+
if (existing.enclosingFunctionName !== functionName) {
|
|
60
|
+
existing.enclosingFunctionName = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
variableAssignments.set(variableName, {
|
|
65
|
+
enclosingFunctionName: functionName,
|
|
66
|
+
count: 1,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Step 3: Filter to vars that:
|
|
72
|
+
// - Have exactly one assignment
|
|
73
|
+
// - Are assigned inside a function with a stable name
|
|
74
|
+
const deferredVariables = [];
|
|
75
|
+
for (const [variableName, info] of variableAssignments) {
|
|
76
|
+
if (info.count === 1 &&
|
|
77
|
+
info.enclosingFunctionName &&
|
|
78
|
+
isHashBasedStableName(info.enclosingFunctionName)) {
|
|
79
|
+
const binding = programScope.getBinding(variableName);
|
|
80
|
+
if (binding) {
|
|
81
|
+
deferredVariables.push({
|
|
82
|
+
variableName,
|
|
83
|
+
enclosingFunctionName: info.enclosingFunctionName,
|
|
84
|
+
scope: binding.scope,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { deferredVariables, nodesVisited };
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Group deferred vars by their enclosing function.
|
|
93
|
+
* Returns only vars where the function has exactly ONE deferred var.
|
|
94
|
+
*/
|
|
95
|
+
export const filterToSingleVariableFunctions = (deferredVariables) => {
|
|
96
|
+
// Group by enclosing function
|
|
97
|
+
const byFunction = new Map();
|
|
98
|
+
for (const info of deferredVariables) {
|
|
99
|
+
const existing = byFunction.get(info.enclosingFunctionName);
|
|
100
|
+
if (existing) {
|
|
101
|
+
existing.push(info);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
byFunction.set(info.enclosingFunctionName, [info]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Only keep vars where function has exactly one
|
|
108
|
+
const result = [];
|
|
109
|
+
for (const variables of byFunction.values()) {
|
|
110
|
+
if (variables.length === 1 && variables[0]) {
|
|
111
|
+
result.push(variables[0]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recommended": true,
|
|
3
|
+
"notes": "Stabilizes top-level var declarations that are initialized inside stable-named functions (lazy init pattern). Only applies when the enclosing function has exactly one such deferred var to avoid order-dependent naming. Renames ~750 vars but diff reduction is minimal (0.03%) because most vars already have consistent minified names between versions. The stabilization helps when minifiers do vary names.",
|
|
4
|
+
"evaluations": {
|
|
5
|
+
"claude-code-2.1.10:claude-code-2.1.11": {
|
|
6
|
+
"diffSizePercent": 99.97,
|
|
7
|
+
"evaluatedAt": "2026-02-05T17:32:00.000Z",
|
|
8
|
+
"changedLines": 753,
|
|
9
|
+
"durationSeconds": 26,
|
|
10
|
+
"stableNames": 2110
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NodePath, Scope } from "@babel/traverse";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the hash suffix from a stable function name (`$h_` or `$f_` prefix).
|
|
4
|
+
* Does not match `$v_` names — those are deferred variables, not functions.
|
|
5
|
+
*/
|
|
6
|
+
export declare const extractHashFromStableName: (name: string) => string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Rename a binding by mutating AST nodes directly.
|
|
9
|
+
* Does NOT update Babel's internal scope bindings map — callers that rename
|
|
10
|
+
* multiple bindings in a loop must track used names externally
|
|
11
|
+
* (see `usedNewNames` in stabilize-deferred-stable-rhs) and re-parse
|
|
12
|
+
* afterwards to refresh scope state.
|
|
13
|
+
*/
|
|
14
|
+
export declare const renameBindingInPlace: (bindingScope: Scope, fromName: string, toName: string) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Check if we can safely rename a binding.
|
|
17
|
+
*/
|
|
18
|
+
export declare const canRenameBindingSafely: (bindingScope: Scope, fromName: string, toName: string) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Walk up the AST to find the binding name of the enclosing function.
|
|
21
|
+
* Handles `var $h_xxx = () => { ... }`, `$h_xxx = fn(() => { ... })`,
|
|
22
|
+
* and `function $h_xxx() { ... }`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const findEnclosingBindingName: (path: NodePath) => string | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { hasShadowingRisk } from "../../core/has-shadowing-risk.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extract the hash suffix from a stable function name (`$h_` or `$f_` prefix).
|
|
5
|
+
* Does not match `$v_` names — those are deferred variables, not functions.
|
|
6
|
+
*/
|
|
7
|
+
export const extractHashFromStableName = (name) => {
|
|
8
|
+
const match = name.match(/^\$[fh]_(.+)$/u);
|
|
9
|
+
return match?.[1];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Rename a binding by mutating AST nodes directly.
|
|
13
|
+
* Does NOT update Babel's internal scope bindings map — callers that rename
|
|
14
|
+
* multiple bindings in a loop must track used names externally
|
|
15
|
+
* (see `usedNewNames` in stabilize-deferred-stable-rhs) and re-parse
|
|
16
|
+
* afterwards to refresh scope state.
|
|
17
|
+
*/
|
|
18
|
+
export const renameBindingInPlace = (bindingScope, fromName, toName) => {
|
|
19
|
+
const binding = bindingScope.getBinding(fromName);
|
|
20
|
+
if (!binding)
|
|
21
|
+
return false;
|
|
22
|
+
// Rename the declaration site
|
|
23
|
+
binding.identifier.name = toName;
|
|
24
|
+
// Rename all references (Identifier and JSXIdentifier)
|
|
25
|
+
for (const referencePath of binding.referencePaths) {
|
|
26
|
+
if (t.isIdentifier(referencePath.node, { name: fromName })) {
|
|
27
|
+
referencePath.node.name = toName;
|
|
28
|
+
}
|
|
29
|
+
else if (t.isJSXIdentifier(referencePath.node, { name: fromName })) {
|
|
30
|
+
referencePath.node.name = toName;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Rename mutation sites (assignments, updates, for-in/of)
|
|
34
|
+
for (const violationPath of binding.constantViolations) {
|
|
35
|
+
const node = violationPath.node;
|
|
36
|
+
if (t.isAssignmentExpression(node) &&
|
|
37
|
+
t.isIdentifier(node.left, { name: fromName })) {
|
|
38
|
+
node.left.name = toName;
|
|
39
|
+
}
|
|
40
|
+
else if (t.isUpdateExpression(node) &&
|
|
41
|
+
t.isIdentifier(node.argument, { name: fromName })) {
|
|
42
|
+
node.argument.name = toName;
|
|
43
|
+
}
|
|
44
|
+
else if ((t.isForInStatement(node) || t.isForOfStatement(node)) &&
|
|
45
|
+
t.isIdentifier(node.left, { name: fromName })) {
|
|
46
|
+
node.left.name = toName;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Check if we can safely rename a binding.
|
|
53
|
+
*/
|
|
54
|
+
export const canRenameBindingSafely = (bindingScope, fromName, toName) => {
|
|
55
|
+
if (fromName === toName)
|
|
56
|
+
return false;
|
|
57
|
+
if (bindingScope.hasOwnBinding(toName))
|
|
58
|
+
return false;
|
|
59
|
+
const programScope = bindingScope.getProgramParent();
|
|
60
|
+
if (Object.hasOwn(programScope.globals, toName))
|
|
61
|
+
return false;
|
|
62
|
+
const binding = bindingScope.getBinding(fromName);
|
|
63
|
+
if (!binding)
|
|
64
|
+
return false;
|
|
65
|
+
if (hasShadowingRisk(binding, bindingScope, toName))
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Walk up the AST to find the binding name of the enclosing function.
|
|
71
|
+
* Handles `var $h_xxx = () => { ... }`, `$h_xxx = fn(() => { ... })`,
|
|
72
|
+
* and `function $h_xxx() { ... }`.
|
|
73
|
+
*/
|
|
74
|
+
export const findEnclosingBindingName = (path) => {
|
|
75
|
+
// FunctionDeclaration: the path itself carries the id
|
|
76
|
+
if (path.isFunctionDeclaration() && path.node.id) {
|
|
77
|
+
return path.node.id.name;
|
|
78
|
+
}
|
|
79
|
+
let current = path.parentPath;
|
|
80
|
+
while (current) {
|
|
81
|
+
if (current.isVariableDeclarator() && t.isIdentifier(current.node.id)) {
|
|
82
|
+
return current.node.id.name;
|
|
83
|
+
}
|
|
84
|
+
if (current.isAssignmentExpression() && t.isIdentifier(current.node.left)) {
|
|
85
|
+
return current.node.left.name;
|
|
86
|
+
}
|
|
87
|
+
if (current.isProgram())
|
|
88
|
+
break;
|
|
89
|
+
current = current.parentPath;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import { getFilesToProcess, } from "../../core/types.js";
|
|
4
|
+
import { generateCode } from "../../cli/generate-code.js";
|
|
5
|
+
import { collectDeferredVariables, filterToSingleVariableFunctions, } from "./collect-deferred-variables.js";
|
|
6
|
+
import { canRenameBindingSafely, extractHashFromStableName, renameBindingInPlace, } from "./rename-helpers.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
9
|
+
const traverse = require("@babel/traverse").default;
|
|
10
|
+
export const stabilizeDeferredTopLevelBindingsTransform = {
|
|
11
|
+
id: "stabilize-deferred-top-level-bindings",
|
|
12
|
+
description: "Renames top-level var declarations without initializers (deferred bindings) to stable names " +
|
|
13
|
+
"derived from their enclosing stable function. Only applies when the function has exactly one " +
|
|
14
|
+
"such deferred var to avoid order-dependent naming.",
|
|
15
|
+
scope: "file",
|
|
16
|
+
parallelizable: true,
|
|
17
|
+
transform(context) {
|
|
18
|
+
let nodesVisited = 0;
|
|
19
|
+
let transformationsApplied = 0;
|
|
20
|
+
for (const fileInfo of getFilesToProcess(context)) {
|
|
21
|
+
const program = fileInfo.ast.program;
|
|
22
|
+
// Get program scope
|
|
23
|
+
let programScope;
|
|
24
|
+
traverse(fileInfo.ast, {
|
|
25
|
+
Program(path) {
|
|
26
|
+
programScope = path.scope;
|
|
27
|
+
path.stop();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!programScope)
|
|
31
|
+
continue;
|
|
32
|
+
// Collect deferred vars
|
|
33
|
+
const { deferredVariables, nodesVisited: fileNodesVisited } = collectDeferredVariables(fileInfo.ast, programScope);
|
|
34
|
+
nodesVisited += fileNodesVisited;
|
|
35
|
+
// Filter to vars where function has exactly one deferred var
|
|
36
|
+
const eligibleVariables = filterToSingleVariableFunctions(deferredVariables);
|
|
37
|
+
// Apply renames. Track used names for consistency with stabilize-deferred-stable-rhs,
|
|
38
|
+
// even though collisions are structurally prevented by unique enclosing function names.
|
|
39
|
+
const usedNewNames = new Set();
|
|
40
|
+
let fileTransformations = 0;
|
|
41
|
+
for (const { variableName, enclosingFunctionName, scope, } of eligibleVariables) {
|
|
42
|
+
const hash = extractHashFromStableName(enclosingFunctionName);
|
|
43
|
+
if (!hash)
|
|
44
|
+
continue;
|
|
45
|
+
const newName = `$v_${hash}`;
|
|
46
|
+
if (usedNewNames.has(newName))
|
|
47
|
+
continue;
|
|
48
|
+
if (!canRenameBindingSafely(scope, variableName, newName))
|
|
49
|
+
continue;
|
|
50
|
+
if (renameBindingInPlace(scope, variableName, newName)) {
|
|
51
|
+
usedNewNames.add(newName);
|
|
52
|
+
fileTransformations++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
transformationsApplied += fileTransformations;
|
|
56
|
+
// Re-parse the AST to refresh Babel's scope bindings for subsequent transforms
|
|
57
|
+
if (fileTransformations > 0) {
|
|
58
|
+
const code = generateCode(fileInfo.ast);
|
|
59
|
+
const newAst = parse(code, {
|
|
60
|
+
sourceType: program.sourceType,
|
|
61
|
+
plugins: ["jsx", "typescript"],
|
|
62
|
+
});
|
|
63
|
+
fileInfo.ast = newAst;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return Promise.resolve({ nodesVisited, transformationsApplied });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RenameCandidate } from "./collect-factory-bindings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Applies renames to candidates, handling hash collisions with index suffixes.
|
|
4
|
+
*
|
|
5
|
+
* Uses `scope.rename()` which invalidates Babel's scope internals after renaming.
|
|
6
|
+
* The caller must re-parse the AST after all renames are applied to refresh scope
|
|
7
|
+
* bindings for subsequent transforms (see stabilize-nested-bindings-transform.ts).
|
|
8
|
+
*/
|
|
9
|
+
export declare const applyRenames: (candidates: RenameCandidate[]) => number;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { hasShadowingRisk } from "../../core/has-shadowing-risk.js";
|
|
2
|
+
const NESTED_STABLE_PREFIX = "$f_";
|
|
3
|
+
/**
|
|
4
|
+
* Applies renames to candidates, handling hash collisions with index suffixes.
|
|
5
|
+
*
|
|
6
|
+
* Uses `scope.rename()` which invalidates Babel's scope internals after renaming.
|
|
7
|
+
* The caller must re-parse the AST after all renames are applied to refresh scope
|
|
8
|
+
* bindings for subsequent transforms (see stabilize-nested-bindings-transform.ts).
|
|
9
|
+
*/
|
|
10
|
+
export const applyRenames = (candidates) => {
|
|
11
|
+
// Group by hash to handle collisions
|
|
12
|
+
const hashGroups = new Map();
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
const group = hashGroups.get(candidate.hash);
|
|
15
|
+
if (group) {
|
|
16
|
+
group.push(candidate);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
hashGroups.set(candidate.hash, [candidate]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let transformationsApplied = 0;
|
|
23
|
+
for (const [hash, group] of hashGroups) {
|
|
24
|
+
for (let index = 0; index < group.length; index++) {
|
|
25
|
+
const candidate = group[index];
|
|
26
|
+
if (!candidate)
|
|
27
|
+
continue;
|
|
28
|
+
// Determine new name: $f_<hash> for unique, $f_<hash>_<index> for collisions
|
|
29
|
+
const newName = group.length === 1
|
|
30
|
+
? `${NESTED_STABLE_PREFIX}${hash}`
|
|
31
|
+
: `${NESTED_STABLE_PREFIX}${hash}_${index}`;
|
|
32
|
+
// Skip if name already matches
|
|
33
|
+
if (candidate.name === newName)
|
|
34
|
+
continue;
|
|
35
|
+
// Check if new name is safe (no collision in current scope)
|
|
36
|
+
if (candidate.scope.hasOwnBinding(newName))
|
|
37
|
+
continue;
|
|
38
|
+
// Check for shadowing risk in nested scopes
|
|
39
|
+
const binding = candidate.scope.getBinding(candidate.name);
|
|
40
|
+
if (!binding)
|
|
41
|
+
continue;
|
|
42
|
+
if (hasShadowingRisk(binding, candidate.scope, newName))
|
|
43
|
+
continue;
|
|
44
|
+
// Apply the rename
|
|
45
|
+
candidate.scope.rename(candidate.name, newName);
|
|
46
|
+
transformationsApplied++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return transformationsApplied;
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NodePath, Scope } from "@babel/traverse";
|
|
2
|
+
import type { ArrowFunctionExpression, FunctionExpression } from "@babel/types";
|
|
3
|
+
export type RenameCandidate = {
|
|
4
|
+
name: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
scope: Scope;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Collects local binding candidates from a factory callback body.
|
|
10
|
+
*/
|
|
11
|
+
export declare const collectFactoryBindings: (functionPath: NodePath<FunctionExpression | ArrowFunctionExpression>) => RenameCandidate[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { isStableRenamed } from "../../core/stable-naming.js";
|
|
3
|
+
import { hashFingerprintNode } from "../../core/fingerprint/hash-fingerprint-node.js";
|
|
4
|
+
/**
|
|
5
|
+
* Collects local binding candidates from a factory callback body.
|
|
6
|
+
*/
|
|
7
|
+
export const collectFactoryBindings = (functionPath) => {
|
|
8
|
+
const candidates = [];
|
|
9
|
+
const seenNames = new Set();
|
|
10
|
+
const functionScope = functionPath.scope;
|
|
11
|
+
// Get the function body
|
|
12
|
+
const body = functionPath.node.body;
|
|
13
|
+
if (!t.isBlockStatement(body))
|
|
14
|
+
return candidates;
|
|
15
|
+
// Traverse only the immediate function scope (don't recurse into nested functions)
|
|
16
|
+
functionPath.traverse({
|
|
17
|
+
VariableDeclarator(path) {
|
|
18
|
+
const id = path.node.id;
|
|
19
|
+
if (!t.isIdentifier(id))
|
|
20
|
+
return;
|
|
21
|
+
const name = id.name;
|
|
22
|
+
// Only process bindings that belong to the factory's scope
|
|
23
|
+
// (var declarations in nested blocks are hoisted to function scope)
|
|
24
|
+
if (path.scope.getBinding(name)?.scope !== functionScope)
|
|
25
|
+
return;
|
|
26
|
+
if (isStableRenamed(name))
|
|
27
|
+
return;
|
|
28
|
+
if (seenNames.has(name))
|
|
29
|
+
return;
|
|
30
|
+
// Must have an initializer to hash
|
|
31
|
+
const init = path.node.init;
|
|
32
|
+
if (!init)
|
|
33
|
+
return;
|
|
34
|
+
seenNames.add(name);
|
|
35
|
+
const hash = hashFingerprintNode(init);
|
|
36
|
+
candidates.push({ name, hash, scope: functionScope });
|
|
37
|
+
},
|
|
38
|
+
FunctionDeclaration(path) {
|
|
39
|
+
// Only process bindings in the factory's immediate scope
|
|
40
|
+
if (path.scope.parent !== functionScope)
|
|
41
|
+
return;
|
|
42
|
+
const id = path.node.id;
|
|
43
|
+
if (!id)
|
|
44
|
+
return;
|
|
45
|
+
const name = id.name;
|
|
46
|
+
if (isStableRenamed(name))
|
|
47
|
+
return;
|
|
48
|
+
if (seenNames.has(name))
|
|
49
|
+
return;
|
|
50
|
+
seenNames.add(name);
|
|
51
|
+
const hash = hashFingerprintNode(path.node);
|
|
52
|
+
candidates.push({ name, hash, scope: functionScope });
|
|
53
|
+
},
|
|
54
|
+
ClassDeclaration(path) {
|
|
55
|
+
// Only process bindings in the factory's immediate scope
|
|
56
|
+
if (path.scope.parent !== functionScope)
|
|
57
|
+
return;
|
|
58
|
+
const id = path.node.id;
|
|
59
|
+
if (!id)
|
|
60
|
+
return;
|
|
61
|
+
const name = id.name;
|
|
62
|
+
if (isStableRenamed(name))
|
|
63
|
+
return;
|
|
64
|
+
if (seenNames.has(name))
|
|
65
|
+
return;
|
|
66
|
+
seenNames.add(name);
|
|
67
|
+
const hash = hashFingerprintNode(path.node);
|
|
68
|
+
candidates.push({ name, hash, scope: functionScope });
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return candidates;
|
|
72
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NodePath } from "@babel/traverse";
|
|
2
|
+
import type { ArrowFunctionExpression, FunctionExpression } from "@babel/types";
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a function contains patterns that perform dynamic name lookup
|
|
5
|
+
* (eval, with, etc.) which would make local binding renames unsafe.
|
|
6
|
+
*
|
|
7
|
+
* For local/nested bindings, only direct eval and with statements can observe
|
|
8
|
+
* local scope. Indirect eval, Function(), and string timers run in global scope
|
|
9
|
+
* and cannot see local bindings—but we're conservative and flag any eval-like
|
|
10
|
+
* pattern to avoid breaking bundles that use these features.
|
|
11
|
+
*/
|
|
12
|
+
export declare const hasDynamicNameLookup: (functionPath: NodePath<FunctionExpression | ArrowFunctionExpression>) => boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { isEvalLikeCallCallee } from "../dynamic-name-lookup/eval-like-call-detection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a function contains patterns that perform dynamic name lookup
|
|
5
|
+
* (eval, with, etc.) which would make local binding renames unsafe.
|
|
6
|
+
*
|
|
7
|
+
* For local/nested bindings, only direct eval and with statements can observe
|
|
8
|
+
* local scope. Indirect eval, Function(), and string timers run in global scope
|
|
9
|
+
* and cannot see local bindings—but we're conservative and flag any eval-like
|
|
10
|
+
* pattern to avoid breaking bundles that use these features.
|
|
11
|
+
*/
|
|
12
|
+
export const hasDynamicNameLookup = (functionPath) => {
|
|
13
|
+
let found = false;
|
|
14
|
+
functionPath.traverse({
|
|
15
|
+
WithStatement(_path) {
|
|
16
|
+
found = true;
|
|
17
|
+
_path.stop();
|
|
18
|
+
},
|
|
19
|
+
ReferencedIdentifier(path) {
|
|
20
|
+
if (found)
|
|
21
|
+
return;
|
|
22
|
+
if (!t.isIdentifier(path.node, { name: "eval" }))
|
|
23
|
+
return;
|
|
24
|
+
// A locally-bound `eval` isn't necessarily the builtin eval function,
|
|
25
|
+
// so only bail on *free* `eval` references.
|
|
26
|
+
if (path.scope.getBinding("eval"))
|
|
27
|
+
return;
|
|
28
|
+
found = true;
|
|
29
|
+
path.stop();
|
|
30
|
+
},
|
|
31
|
+
CallExpression(path) {
|
|
32
|
+
if (found)
|
|
33
|
+
return;
|
|
34
|
+
if (isEvalLikeCallCallee(path.node.callee, path.scope)) {
|
|
35
|
+
found = true;
|
|
36
|
+
path.stop();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
OptionalCallExpression(path) {
|
|
40
|
+
if (found)
|
|
41
|
+
return;
|
|
42
|
+
if (isEvalLikeCallCallee(path.node.callee, path.scope)) {
|
|
43
|
+
found = true;
|
|
44
|
+
path.stop();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return found;
|
|
49
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ArrowFunctionExpression, FunctionExpression } from "@babel/types";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a function looks like a CommonJS factory callback.
|
|
4
|
+
* Heuristic: 1-2 parameters with the first being an identifier (e.g., exports, module).
|
|
5
|
+
*/
|
|
6
|
+
export declare const isFactoryCallback: (callback: FunctionExpression | ArrowFunctionExpression) => boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a function looks like a CommonJS factory callback.
|
|
4
|
+
* Heuristic: 1-2 parameters with the first being an identifier (e.g., exports, module).
|
|
5
|
+
*/
|
|
6
|
+
export const isFactoryCallback = (callback) => {
|
|
7
|
+
const parameters = callback.params;
|
|
8
|
+
if (parameters.length === 0 || parameters.length > 2)
|
|
9
|
+
return false;
|
|
10
|
+
return t.isIdentifier(parameters[0]);
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recommended": true,
|
|
3
|
+
"evaluations": {
|
|
4
|
+
"claude-code-2.1.10:claude-code-2.1.11": {
|
|
5
|
+
"diffSizePercent": 43.68,
|
|
6
|
+
"evaluatedAt": "2026-02-05T13:38:00.000Z",
|
|
7
|
+
"changedLines": 47159,
|
|
8
|
+
"durationSeconds": 60,
|
|
9
|
+
"stableNames": 14374
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"notes": "Stabilizes local bindings inside CommonJS factory callbacks using content-hash naming ($f_<hash>). Must run AFTER stabilize-top-level-bindings. Combined with top-level stabilization, achieves 33.49% diff size (67% reduction from baseline)."
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import * as t from "@babel/types";
|
|
4
|
+
import { getFilesToProcess, } from "../../core/types.js";
|
|
5
|
+
import { generateCode } from "../../cli/generate-code.js";
|
|
6
|
+
import { applyRenames } from "./apply-renames.js";
|
|
7
|
+
import { collectFactoryBindings } from "./collect-factory-bindings.js";
|
|
8
|
+
import { hasDynamicNameLookup } from "./has-dynamic-name-lookup.js";
|
|
9
|
+
import { isFactoryCallback } from "./is-factory-callback.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
12
|
+
const traverse = require("@babel/traverse").default;
|
|
13
|
+
/**
|
|
14
|
+
* Checks if a name looks like a stabilized top-level binding ($h_...)
|
|
15
|
+
*/
|
|
16
|
+
const isStabilizedTopLevel = (name) => {
|
|
17
|
+
return name.startsWith("$h_");
|
|
18
|
+
};
|
|
19
|
+
export const stabilizeNestedBindingsTransform = {
|
|
20
|
+
id: "stabilize-nested-bindings",
|
|
21
|
+
description: "Renames local bindings inside CommonJS factory callbacks to content-hash-based stable names ($f_<hash>). This stabilizes variable names that are randomly assigned by minifiers within factory functions.",
|
|
22
|
+
scope: "file",
|
|
23
|
+
parallelizable: true,
|
|
24
|
+
transform(context) {
|
|
25
|
+
let nodesVisited = 0;
|
|
26
|
+
let transformationsApplied = 0;
|
|
27
|
+
for (const fileInfo of getFilesToProcess(context)) {
|
|
28
|
+
let fileTransformations = 0;
|
|
29
|
+
traverse(fileInfo.ast, {
|
|
30
|
+
CallExpression(path) {
|
|
31
|
+
nodesVisited++;
|
|
32
|
+
// Check if callee is a stabilized top-level function ($h_...)
|
|
33
|
+
const callee = path.node.callee;
|
|
34
|
+
if (!t.isIdentifier(callee))
|
|
35
|
+
return;
|
|
36
|
+
if (!isStabilizedTopLevel(callee.name))
|
|
37
|
+
return;
|
|
38
|
+
// Check if first argument is a function (the factory callback)
|
|
39
|
+
const callArguments = path.node.arguments;
|
|
40
|
+
if (callArguments.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
const firstArgument = callArguments[0];
|
|
43
|
+
if (!t.isFunctionExpression(firstArgument) &&
|
|
44
|
+
!t.isArrowFunctionExpression(firstArgument))
|
|
45
|
+
return;
|
|
46
|
+
// Verify it looks like a factory callback
|
|
47
|
+
if (!isFactoryCallback(firstArgument))
|
|
48
|
+
return;
|
|
49
|
+
// Get the path to the function argument
|
|
50
|
+
const functionArgumentPath = path.get("arguments.0");
|
|
51
|
+
if (!functionArgumentPath.isFunctionExpression() &&
|
|
52
|
+
!functionArgumentPath.isArrowFunctionExpression())
|
|
53
|
+
return;
|
|
54
|
+
// Skip renaming if factory contains dynamic name lookup (eval, with, etc.)
|
|
55
|
+
if (hasDynamicNameLookup(functionArgumentPath))
|
|
56
|
+
return;
|
|
57
|
+
// Collect and rename local bindings
|
|
58
|
+
const candidates = collectFactoryBindings(functionArgumentPath);
|
|
59
|
+
fileTransformations += applyRenames(candidates);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
transformationsApplied += fileTransformations;
|
|
63
|
+
// Re-parse if we made changes to refresh scope bindings
|
|
64
|
+
if (fileTransformations > 0) {
|
|
65
|
+
const code = generateCode(fileInfo.ast);
|
|
66
|
+
const newAst = parse(code, {
|
|
67
|
+
sourceType: fileInfo.ast.program.sourceType,
|
|
68
|
+
plugins: ["jsx", "typescript"],
|
|
69
|
+
});
|
|
70
|
+
fileInfo.ast = newAst;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Promise.resolve({ nodesVisited, transformationsApplied });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
2
|
import { isStableRenamed } from "../../core/stable-naming.js";
|
|
3
|
-
import { hashFingerprintNode } from "
|
|
3
|
+
import { hashFingerprintNode } from "../../core/fingerprint/hash-fingerprint-node.js";
|
|
4
4
|
import { collectFirstProgramBodyAssignments } from "./collect-first-program-body-assignments.js";
|
|
5
5
|
import { collectNestedProgramScopeVariableInitializers } from "./collect-nested-program-scope-variable-initializers.js";
|
|
6
6
|
import { addRenameCandidate } from "./rename-candidate.js";
|
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.
|
|
5
|
+
"version": "1.69.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",
|
|
File without changes
|
/package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/fingerprint-leaf-node.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|