miniread 1.67.0 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/core/stable-naming.d.ts +1 -0
  2. package/dist/core/stable-naming.js +1 -0
  3. package/dist/transforms/_generated/manifest.js +6 -0
  4. package/dist/transforms/_generated/registry.js +2 -0
  5. package/dist/transforms/preset-stats.json +2 -2
  6. package/dist/transforms/recommended-transform-order.d.ts +1 -1
  7. package/dist/transforms/recommended-transform-order.js +3 -0
  8. package/dist/transforms/stabilize-nested-bindings/apply-renames.d.ts +9 -0
  9. package/dist/transforms/stabilize-nested-bindings/apply-renames.js +50 -0
  10. package/dist/transforms/stabilize-nested-bindings/collect-factory-bindings.d.ts +11 -0
  11. package/dist/transforms/stabilize-nested-bindings/collect-factory-bindings.js +72 -0
  12. package/dist/transforms/stabilize-nested-bindings/fingerprint-leaf-node.d.ts +2 -0
  13. package/dist/transforms/stabilize-nested-bindings/fingerprint-leaf-node.js +23 -0
  14. package/dist/transforms/stabilize-nested-bindings/fingerprint-scalar-fields.d.ts +2 -0
  15. package/dist/transforms/stabilize-nested-bindings/fingerprint-scalar-fields.js +140 -0
  16. package/dist/transforms/stabilize-nested-bindings/has-dynamic-name-lookup.d.ts +12 -0
  17. package/dist/transforms/stabilize-nested-bindings/has-dynamic-name-lookup.js +49 -0
  18. package/dist/transforms/stabilize-nested-bindings/hash-fingerprint-node.d.ts +1 -0
  19. package/dist/transforms/stabilize-nested-bindings/hash-fingerprint-node.js +9 -0
  20. package/dist/transforms/stabilize-nested-bindings/is-factory-callback.d.ts +6 -0
  21. package/dist/transforms/stabilize-nested-bindings/is-factory-callback.js +15 -0
  22. package/dist/transforms/stabilize-nested-bindings/manifest.json +13 -0
  23. package/dist/transforms/stabilize-nested-bindings/serialize-fingerprint-node.d.ts +3 -0
  24. package/dist/transforms/stabilize-nested-bindings/serialize-fingerprint-node.js +102 -0
  25. package/dist/transforms/stabilize-nested-bindings/stabilize-nested-bindings-transform.d.ts +2 -0
  26. package/dist/transforms/stabilize-nested-bindings/stabilize-nested-bindings-transform.js +75 -0
  27. package/package.json +1 -1
@@ -6,6 +6,7 @@
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.
9
10
  *
10
11
  * - Stable names (`$timeoutId`): Readable AND deterministic across versions
11
12
  * - Readable names (`timeoutId`): Semantic but order-dependent
@@ -6,6 +6,7 @@
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.
9
10
  *
10
11
  * - Stable names (`$timeoutId`): Readable AND deterministic across versions
11
12
  * - Readable names (`timeoutId`): Semantic but order-dependent
@@ -334,6 +334,11 @@ 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-nested-bindings": {
338
+ recommended: true,
339
+ 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).",
340
+ 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 } },
341
+ },
337
342
  "stabilize-top-level-bindings": {
338
343
  recommended: true,
339
344
  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 +376,7 @@ export const manifestEntries = Object.entries(transformRegistry)
371
376
  .toSorted((a, b) => a.id.localeCompare(b.id));
372
377
  export const recommendedTransformIds = [
373
378
  "stabilize-top-level-bindings",
379
+ "stabilize-nested-bindings",
374
380
  "expand-boolean-literals",
375
381
  "expand-sequence-expressions-v5",
376
382
  "expand-special-number-literals",
@@ -68,6 +68,7 @@ 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 { stabilizeNestedBindingsTransform } from "../stabilize-nested-bindings/stabilize-nested-bindings-transform.js";
71
72
  import { stabilizeTopLevelBindingsTransform } from "../stabilize-top-level-bindings/stabilize-top-level-bindings-transform.js";
72
73
  import { useObjectPropertyShorthandTransform } from "../use-object-property-shorthand/use-object-property-shorthand-transform.js";
73
74
  import { useObjectShorthandTransform } from "../use-object-shorthand/use-object-shorthand-transform.js";
@@ -141,6 +142,7 @@ export const transformRegistry = {
141
142
  [simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
142
143
  [simplifyStringTrimTransform.id]: simplifyStringTrimTransform,
143
144
  [splitVariableDeclarationsTransform.id]: splitVariableDeclarationsTransform,
145
+ [stabilizeNestedBindingsTransform.id]: stabilizeNestedBindingsTransform,
144
146
  [stabilizeTopLevelBindingsTransform.id]: stabilizeTopLevelBindingsTransform,
145
147
  [useObjectPropertyShorthandTransform.id]: useObjectPropertyShorthandTransform,
146
148
  [useObjectShorthandTransform.id]: useObjectShorthandTransform,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "recommended": {
3
- "diffSizePercent": 78.49967015361418,
4
- "notes": "Measured with baseline none: 78.50% of original diff. Stats re-evaluated against updated sources (Feb 2026)."
3
+ "diffSizePercent": 33.48924670219602,
4
+ "notes": "Measured with baseline none: 33.49% 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", "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,9 @@ 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",
11
14
  "expand-boolean-literals",
12
15
  "expand-sequence-expressions-v5",
13
16
  "expand-special-number-literals",
@@ -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 "./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,2 @@
1
+ import type { Node } from "@babel/types";
2
+ export declare const fingerprintLeafNode: (node: Node) => string | undefined;
@@ -0,0 +1,23 @@
1
+ export const fingerprintLeafNode = (node) => {
2
+ // Normalize identifiers — this is the key operation that makes fingerprints
3
+ // stable across minified name changes.
4
+ if (node.type === "Identifier")
5
+ return "_";
6
+ if (node.type === "StringLiteral")
7
+ return `S:${JSON.stringify(node.value)}`;
8
+ if (node.type === "NumericLiteral")
9
+ return `N:${node.value}`;
10
+ if (node.type === "BooleanLiteral")
11
+ return `B:${node.value}`;
12
+ if (node.type === "NullLiteral")
13
+ return "NL";
14
+ if (node.type === "BigIntLiteral")
15
+ return `BI:${JSON.stringify(node.value)}`;
16
+ if (node.type === "RegExpLiteral")
17
+ return `R:${JSON.stringify(node.pattern)}:${JSON.stringify(node.flags)}`;
18
+ if (node.type === "TemplateElement")
19
+ return `T:${JSON.stringify(node.value.raw)}`;
20
+ if (node.type === "DirectiveLiteral")
21
+ return `D:${JSON.stringify(node.value)}`;
22
+ return undefined;
23
+ };
@@ -0,0 +1,2 @@
1
+ import type { Node } from "@babel/types";
2
+ export declare const fingerprintScalarFields: (node: Node) => string[];
@@ -0,0 +1,140 @@
1
+ import * as t from "@babel/types";
2
+ const encodeScalar = (value) => value === undefined ? "undefined" : JSON.stringify(value);
3
+ export const fingerprintScalarFields = (node) => {
4
+ if (node.type === "AssignmentExpression" ||
5
+ node.type === "BinaryExpression" ||
6
+ node.type === "LogicalExpression" ||
7
+ node.type === "UnaryExpression") {
8
+ return [
9
+ `operator=${encodeScalar(node.operator)}`,
10
+ ];
11
+ }
12
+ if (node.type === "UpdateExpression") {
13
+ return [
14
+ `operator=${encodeScalar(node.operator)}`,
15
+ `prefix=${encodeScalar(node.prefix)}`,
16
+ ];
17
+ }
18
+ if (node.type === "MemberExpression" ||
19
+ node.type === "OptionalMemberExpression") {
20
+ const computed = node.computed;
21
+ const property = node
22
+ .property;
23
+ const propertyName = computed === false && t.isIdentifier(property)
24
+ ? property.name
25
+ : computed === false && t.isPrivateName(property)
26
+ ? `#${property.id.name}`
27
+ : undefined;
28
+ return [
29
+ `computed=${encodeScalar(computed)}`,
30
+ `optional=${encodeScalar(node.optional)}`,
31
+ `propertyName=${encodeScalar(propertyName)}`,
32
+ ];
33
+ }
34
+ if (node.type === "OptionalCallExpression") {
35
+ return [
36
+ `optional=${encodeScalar(node.optional)}`,
37
+ ];
38
+ }
39
+ if (node.type === "VariableDeclaration") {
40
+ return [
41
+ `kind=${encodeScalar(node.kind)}`,
42
+ ];
43
+ }
44
+ if (node.type === "FunctionDeclaration" ||
45
+ node.type === "FunctionExpression" ||
46
+ node.type === "ArrowFunctionExpression" ||
47
+ node.type === "ObjectMethod" ||
48
+ node.type === "ClassMethod" ||
49
+ node.type === "ClassPrivateMethod") {
50
+ const fields = [
51
+ `async=${encodeScalar(node.async)}`,
52
+ `generator=${encodeScalar(node.generator)}`,
53
+ `kind=${encodeScalar(node.kind)}`,
54
+ `static=${encodeScalar(node.static)}`,
55
+ `computed=${encodeScalar(node.computed)}`,
56
+ ];
57
+ if (node.type === "ObjectMethod") {
58
+ const computed = node.computed;
59
+ const key = node.key;
60
+ if (computed !== true) {
61
+ const keyName = t.isIdentifier(key)
62
+ ? key.name
63
+ : t.isStringLiteral(key)
64
+ ? key.value
65
+ : t.isNumericLiteral(key)
66
+ ? String(key.value)
67
+ : t.isPrivateName(key)
68
+ ? `#${key.id.name}`
69
+ : undefined;
70
+ fields.push(`keyName=${encodeScalar(keyName)}`);
71
+ }
72
+ }
73
+ if (node.type === "ClassMethod" || node.type === "ClassPrivateMethod") {
74
+ const computed = node.computed;
75
+ const key = node.key;
76
+ if (computed !== true) {
77
+ const keyName = t.isIdentifier(key)
78
+ ? key.name
79
+ : t.isStringLiteral(key)
80
+ ? key.value
81
+ : t.isNumericLiteral(key)
82
+ ? String(key.value)
83
+ : t.isPrivateName(key)
84
+ ? `#${key.id.name}`
85
+ : undefined;
86
+ fields.push(`keyName=${encodeScalar(keyName)}`);
87
+ }
88
+ }
89
+ return fields;
90
+ }
91
+ if (node.type === "ObjectProperty") {
92
+ const computed = node.computed;
93
+ const key = node.key;
94
+ const fields = [`computed=${encodeScalar(computed)}`];
95
+ if (computed !== true) {
96
+ const keyName = t.isIdentifier(key)
97
+ ? key.name
98
+ : t.isStringLiteral(key)
99
+ ? key.value
100
+ : t.isNumericLiteral(key)
101
+ ? String(key.value)
102
+ : t.isPrivateName(key)
103
+ ? `#${key.id.name}`
104
+ : undefined;
105
+ fields.push(`keyName=${encodeScalar(keyName)}`);
106
+ }
107
+ return fields;
108
+ }
109
+ if (node.type === "ClassProperty" || node.type === "ClassPrivateProperty") {
110
+ const computed = node.computed;
111
+ const key = node.key;
112
+ const keyName = computed === true
113
+ ? undefined
114
+ : t.isIdentifier(key)
115
+ ? key.name
116
+ : t.isStringLiteral(key)
117
+ ? key.value
118
+ : t.isNumericLiteral(key)
119
+ ? String(key.value)
120
+ : t.isPrivateName(key)
121
+ ? `#${key.id.name}`
122
+ : undefined;
123
+ return [
124
+ `static=${encodeScalar(node.static)}`,
125
+ `computed=${encodeScalar(computed)}`,
126
+ `keyName=${encodeScalar(keyName)}`,
127
+ ];
128
+ }
129
+ if (node.type === "ImportSpecifier") {
130
+ const imported = node
131
+ .imported;
132
+ const importedName = t.isIdentifier(imported)
133
+ ? imported.name
134
+ : t.isStringLiteral(imported)
135
+ ? imported.value
136
+ : undefined;
137
+ return [`importedName=${encodeScalar(importedName)}`];
138
+ }
139
+ return [];
140
+ };
@@ -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 @@
1
+ export declare const hashFingerprintNode: (node: unknown) => string;
@@ -0,0 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+ import { writeFingerprintNode } from "./serialize-fingerprint-node.js";
3
+ export const hashFingerprintNode = (node) => {
4
+ const hasher = createHash("sha256");
5
+ writeFingerprintNode(node, (text) => {
6
+ hasher.update(text);
7
+ });
8
+ return hasher.digest("hex").slice(0, 16);
9
+ };
@@ -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,15 @@
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
+ // Check if first param is an identifier (exports or module.exports pattern)
11
+ const firstParameter = parameters[0];
12
+ if (!t.isIdentifier(firstParameter))
13
+ return false;
14
+ return true;
15
+ };
@@ -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,3 @@
1
+ type Writer = (text: string) => void;
2
+ export declare const writeFingerprintNode: (value: unknown, write: Writer) => void;
3
+ export {};
@@ -0,0 +1,102 @@
1
+ import * as t from "@babel/types";
2
+ import { fingerprintLeafNode } from "./fingerprint-leaf-node.js";
3
+ import { fingerprintScalarFields } from "./fingerprint-scalar-fields.js";
4
+ const pushArrayTasks = (stack, value) => {
5
+ stack.push({ kind: "text", text: "]" });
6
+ for (let index = value.length - 1; index >= 0; index--) {
7
+ if (index < value.length - 1) {
8
+ stack.push({ kind: "text", text: "," });
9
+ }
10
+ stack.push({ kind: "value", value: value[index] });
11
+ }
12
+ stack.push({ kind: "text", text: "[" });
13
+ };
14
+ const pushNodeTasks = (stack, node) => {
15
+ const leaf = fingerprintLeafNode(node);
16
+ if (leaf !== undefined) {
17
+ stack.push({ kind: "text", text: leaf });
18
+ return;
19
+ }
20
+ const keys = t.VISITOR_KEYS[node.type];
21
+ if (!keys) {
22
+ stack.push({ kind: "text", text: node.type });
23
+ return;
24
+ }
25
+ let visitorKeys = keys;
26
+ const computed = node.computed;
27
+ if (computed !== true &&
28
+ (node.type === "ObjectProperty" ||
29
+ node.type === "ObjectMethod" ||
30
+ node.type === "ClassMethod" ||
31
+ node.type === "ClassPrivateMethod" ||
32
+ node.type === "ClassProperty" ||
33
+ node.type === "ClassPrivateProperty")) {
34
+ visitorKeys = keys.filter((key) => key !== "key");
35
+ }
36
+ const nodeTasks = [{ kind: "text", text: node.type }];
37
+ for (const field of fingerprintScalarFields(node)) {
38
+ nodeTasks.push({ kind: "text", text: field });
39
+ }
40
+ for (const key of visitorKeys) {
41
+ nodeTasks.push({
42
+ kind: "value",
43
+ value: node[key],
44
+ });
45
+ }
46
+ for (let index = nodeTasks.length - 1; index >= 0; index--) {
47
+ const nodeTask = nodeTasks[index];
48
+ if (!nodeTask)
49
+ continue;
50
+ stack.push(nodeTask);
51
+ if (index !== 0) {
52
+ stack.push({ kind: "text", text: "|" });
53
+ }
54
+ }
55
+ };
56
+ export const writeFingerprintNode = (value, write) => {
57
+ const stack = [{ kind: "value", value }];
58
+ while (stack.length > 0) {
59
+ const task = stack.pop();
60
+ if (!task)
61
+ break;
62
+ if (task.kind === "text") {
63
+ write(task.text);
64
+ continue;
65
+ }
66
+ const current = task.value;
67
+ if (current === null) {
68
+ write("null");
69
+ continue;
70
+ }
71
+ if (current === undefined) {
72
+ write("undefined");
73
+ continue;
74
+ }
75
+ if (typeof current === "string") {
76
+ write(JSON.stringify(current));
77
+ continue;
78
+ }
79
+ if (typeof current === "number" || typeof current === "boolean") {
80
+ write(String(current));
81
+ continue;
82
+ }
83
+ if (Array.isArray(current)) {
84
+ pushArrayTasks(stack, current);
85
+ continue;
86
+ }
87
+ if (typeof current !== "object") {
88
+ write(JSON.stringify(current));
89
+ continue;
90
+ }
91
+ const nodeCandidate = current;
92
+ if (!("type" in nodeCandidate)) {
93
+ write("{}");
94
+ continue;
95
+ }
96
+ if (nodeCandidate.type === "Identifier") {
97
+ write("_");
98
+ continue;
99
+ }
100
+ pushNodeTasks(stack, nodeCandidate);
101
+ }
102
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const stabilizeNestedBindingsTransform: Transform;
@@ -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
+ };
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.67.0",
5
+ "version": "1.68.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",