miniread 1.68.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.
Files changed (39) hide show
  1. package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/fingerprint-scalar-fields.js +23 -54
  2. package/dist/{transforms/stabilize-top-level-bindings → core/fingerprint}/serialize-fingerprint-node.js +8 -10
  3. package/dist/core/stable-naming.d.ts +8 -0
  4. package/dist/core/stable-naming.js +8 -0
  5. package/dist/transforms/_generated/manifest.js +12 -0
  6. package/dist/transforms/_generated/registry.js +4 -0
  7. package/dist/transforms/preset-stats.json +2 -2
  8. package/dist/transforms/recommended-transform-order.d.ts +1 -1
  9. package/dist/transforms/recommended-transform-order.js +7 -0
  10. package/dist/transforms/stabilize-deferred-stable-rhs/check-rhs-stability.d.ts +6 -0
  11. package/dist/transforms/stabilize-deferred-stable-rhs/check-rhs-stability.js +51 -0
  12. package/dist/transforms/stabilize-deferred-stable-rhs/manifest.json +13 -0
  13. package/dist/transforms/stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.d.ts +2 -0
  14. package/dist/transforms/stabilize-deferred-stable-rhs/stabilize-deferred-stable-rhs-transform.js +125 -0
  15. package/dist/transforms/stabilize-deferred-top-level-bindings/collect-deferred-variables.d.ts +22 -0
  16. package/dist/transforms/stabilize-deferred-top-level-bindings/collect-deferred-variables.js +115 -0
  17. package/dist/transforms/stabilize-deferred-top-level-bindings/manifest.json +13 -0
  18. package/dist/transforms/stabilize-deferred-top-level-bindings/rename-helpers.d.ts +24 -0
  19. package/dist/transforms/stabilize-deferred-top-level-bindings/rename-helpers.js +92 -0
  20. package/dist/transforms/stabilize-deferred-top-level-bindings/stabilize-deferred-top-level-bindings-transform.d.ts +2 -0
  21. package/dist/transforms/stabilize-deferred-top-level-bindings/stabilize-deferred-top-level-bindings-transform.js +68 -0
  22. package/dist/transforms/stabilize-nested-bindings/collect-factory-bindings.js +1 -1
  23. package/dist/transforms/stabilize-nested-bindings/is-factory-callback.js +1 -5
  24. package/dist/transforms/stabilize-top-level-bindings/collect-rename-candidates.js +1 -1
  25. package/package.json +1 -1
  26. package/dist/transforms/stabilize-nested-bindings/serialize-fingerprint-node.js +0 -102
  27. package/dist/transforms/stabilize-top-level-bindings/fingerprint-leaf-node.d.ts +0 -2
  28. package/dist/transforms/stabilize-top-level-bindings/fingerprint-leaf-node.js +0 -23
  29. package/dist/transforms/stabilize-top-level-bindings/fingerprint-node.d.ts +0 -1
  30. package/dist/transforms/stabilize-top-level-bindings/fingerprint-node.js +0 -9
  31. package/dist/transforms/stabilize-top-level-bindings/fingerprint-scalar-fields.d.ts +0 -2
  32. package/dist/transforms/stabilize-top-level-bindings/fingerprint-scalar-fields.js +0 -153
  33. package/dist/transforms/stabilize-top-level-bindings/serialize-fingerprint-node.d.ts +0 -3
  34. /package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/fingerprint-leaf-node.d.ts +0 -0
  35. /package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/fingerprint-leaf-node.js +0 -0
  36. /package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/fingerprint-scalar-fields.d.ts +0 -0
  37. /package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/hash-fingerprint-node.d.ts +0 -0
  38. /package/dist/{transforms/stabilize-nested-bindings → core/fingerprint}/hash-fingerprint-node.js +0 -0
  39. /package/dist/{transforms/stabilize-nested-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" ||
@@ -20,6 +31,9 @@ export const fingerprintScalarFields = (node) => {
20
31
  const computed = node.computed;
21
32
  const property = node
22
33
  .property;
34
+ // Include non-computed property names to reduce collisions (e.g. `obj.foo` vs
35
+ // `obj.bar`). This trades some cross-build stability for fewer collisions if
36
+ // a toolchain mangles property names between builds.
23
37
  const propertyName = computed === false && t.isIdentifier(property)
24
38
  ? property.name
25
39
  : computed === false && t.isPrivateName(property)
@@ -47,6 +61,8 @@ export const fingerprintScalarFields = (node) => {
47
61
  node.type === "ObjectMethod" ||
48
62
  node.type === "ClassMethod" ||
49
63
  node.type === "ClassPrivateMethod") {
64
+ // Keep a uniform field set for all function-like nodes; some fields (e.g. `static`,
65
+ // `computed`, `kind`) only exist on specific node types and will be `undefined` otherwise.
50
66
  const fields = [
51
67
  `async=${encodeScalar(node.async)}`,
52
68
  `generator=${encodeScalar(node.generator)}`,
@@ -54,36 +70,13 @@ export const fingerprintScalarFields = (node) => {
54
70
  `static=${encodeScalar(node.static)}`,
55
71
  `computed=${encodeScalar(node.computed)}`,
56
72
  ];
57
- if (node.type === "ObjectMethod") {
73
+ if (node.type === "ObjectMethod" ||
74
+ node.type === "ClassMethod" ||
75
+ node.type === "ClassPrivateMethod") {
58
76
  const computed = node.computed;
59
77
  const key = node.key;
60
78
  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)}`);
79
+ fields.push(`keyName=${encodeScalar(extractKeyName(key))}`);
87
80
  }
88
81
  }
89
82
  return fields;
@@ -93,33 +86,14 @@ export const fingerprintScalarFields = (node) => {
93
86
  const key = node.key;
94
87
  const fields = [`computed=${encodeScalar(computed)}`];
95
88
  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)}`);
89
+ fields.push(`keyName=${encodeScalar(extractKeyName(key))}`);
106
90
  }
107
91
  return fields;
108
92
  }
109
93
  if (node.type === "ClassProperty" || node.type === "ClassPrivateProperty") {
110
94
  const computed = node.computed;
111
95
  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;
96
+ const keyName = computed === true ? undefined : extractKeyName(key);
123
97
  return [
124
98
  `static=${encodeScalar(node.static)}`,
125
99
  `computed=${encodeScalar(computed)}`,
@@ -129,12 +103,7 @@ export const fingerprintScalarFields = (node) => {
129
103
  if (node.type === "ImportSpecifier") {
130
104
  const imported = node
131
105
  .imported;
132
- const importedName = t.isIdentifier(imported)
133
- ? imported.name
134
- : t.isStringLiteral(imported)
135
- ? imported.value
136
- : undefined;
137
- return [`importedName=${encodeScalar(importedName)}`];
106
+ return [`importedName=${encodeScalar(extractKeyName(imported))}`];
138
107
  }
139
108
  return [];
140
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 `{ \"foo\": 1 }`.
29
+ // like `{ foo: 1 }` vs `{ "foo": 1 }`.
30
30
  const computed = node.computed;
31
- if (computed === true) {
32
- // Keep the key expression for computed properties.
33
- }
34
- else if (node.type === "ObjectProperty" ||
35
- node.type === "ObjectMethod" ||
36
- node.type === "ClassMethod" ||
37
- node.type === "ClassPrivateMethod" ||
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 }];
@@ -7,6 +7,8 @@
7
7
  * Reserved prefixes:
8
8
  * - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
9
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.
10
12
  *
11
13
  * - Stable names (`$timeoutId`): Readable AND deterministic across versions
12
14
  * - Readable names (`timeoutId`): Semantic but order-dependent
@@ -17,6 +19,12 @@ import type { Scope } from "@babel/traverse";
17
19
  * Transforms should skip variables with this prefix.
18
20
  */
19
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;
20
28
  /**
21
29
  * Entry for a planned rename operation.
22
30
  */
@@ -7,6 +7,8 @@
7
7
  * Reserved prefixes:
8
8
  * - `$h_`: used by `stabilize-top-level-bindings` for content-hash-based names.
9
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.
10
12
  *
11
13
  * - Stable names (`$timeoutId`): Readable AND deterministic across versions
12
14
  * - Readable names (`timeoutId`): Semantic but order-dependent
@@ -21,6 +23,12 @@ const STABLE_PREFIX = "$";
21
23
  export const isStableRenamed = (name) => {
22
24
  return name.startsWith(STABLE_PREFIX);
23
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_");
24
32
  /**
25
33
  * Creates a stable name by adding the $ prefix.
26
34
  * Internal helper - prefer using RenameGroup which handles stability logic automatically.
@@ -334,6 +334,16 @@ 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
+ },
337
347
  "stabilize-nested-bindings": {
338
348
  recommended: true,
339
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).",
@@ -377,6 +387,8 @@ export const manifestEntries = Object.entries(transformRegistry)
377
387
  export const recommendedTransformIds = [
378
388
  "stabilize-top-level-bindings",
379
389
  "stabilize-nested-bindings",
390
+ "stabilize-deferred-top-level-bindings",
391
+ "stabilize-deferred-stable-rhs",
380
392
  "expand-boolean-literals",
381
393
  "expand-sequence-expressions-v5",
382
394
  "expand-special-number-literals",
@@ -68,6 +68,8 @@ 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";
71
73
  import { stabilizeNestedBindingsTransform } from "../stabilize-nested-bindings/stabilize-nested-bindings-transform.js";
72
74
  import { stabilizeTopLevelBindingsTransform } from "../stabilize-top-level-bindings/stabilize-top-level-bindings-transform.js";
73
75
  import { useObjectPropertyShorthandTransform } from "../use-object-property-shorthand/use-object-property-shorthand-transform.js";
@@ -142,6 +144,8 @@ export const transformRegistry = {
142
144
  [simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
143
145
  [simplifyStringTrimTransform.id]: simplifyStringTrimTransform,
144
146
  [splitVariableDeclarationsTransform.id]: splitVariableDeclarationsTransform,
147
+ [stabilizeDeferredStableRhsTransform.id]: stabilizeDeferredStableRhsTransform,
148
+ [stabilizeDeferredTopLevelBindingsTransform.id]: stabilizeDeferredTopLevelBindingsTransform,
145
149
  [stabilizeNestedBindingsTransform.id]: stabilizeNestedBindingsTransform,
146
150
  [stabilizeTopLevelBindingsTransform.id]: stabilizeTopLevelBindingsTransform,
147
151
  [useObjectPropertyShorthandTransform.id]: useObjectPropertyShorthandTransform,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "recommended": {
3
- "diffSizePercent": 33.48924670219602,
4
- "notes": "Measured with baseline none: 33.49% of original diff."
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", "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"];
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"];
@@ -11,6 +11,13 @@ export const recommendedTransformOrder = [
11
11
  // Run second: stabilizes local bindings inside CommonJS factory callbacks.
12
12
  // Depends on stabilize-top-level-bindings having renamed factory wrapper functions to $h_*.
13
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",
14
21
  "expand-boolean-literals",
15
22
  "expand-sequence-expressions-v5",
16
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
+ }
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const stabilizeDeferredStableRhsTransform: Transform;
@@ -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,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const stabilizeDeferredTopLevelBindingsTransform: Transform;
@@ -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
+ };
@@ -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 "./hash-fingerprint-node.js";
3
+ import { hashFingerprintNode } from "../../core/fingerprint/hash-fingerprint-node.js";
4
4
  /**
5
5
  * Collects local binding candidates from a factory callback body.
6
6
  */
@@ -7,9 +7,5 @@ export const isFactoryCallback = (callback) => {
7
7
  const parameters = callback.params;
8
8
  if (parameters.length === 0 || parameters.length > 2)
9
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;
10
+ return t.isIdentifier(parameters[0]);
15
11
  };
@@ -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 "./fingerprint-node.js";
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.68.0",
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",
@@ -1,102 +0,0 @@
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
- };
@@ -1,2 +0,0 @@
1
- import type { Node } from "@babel/types";
2
- export declare const fingerprintLeafNode: (node: Node) => string | undefined;
@@ -1,23 +0,0 @@
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
- };
@@ -1 +0,0 @@
1
- export declare const hashFingerprintNode: (node: unknown) => string;
@@ -1,9 +0,0 @@
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
- };
@@ -1,2 +0,0 @@
1
- import type { Node } from "@babel/types";
2
- export declare const fingerprintScalarFields: (node: Node) => string[];
@@ -1,153 +0,0 @@
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
- // Include non-computed property names to reduce collisions (e.g. `obj.foo` vs
24
- // `obj.bar`). This trades some cross-build stability for fewer collisions if
25
- // a toolchain mangles property names between builds.
26
- const propertyName = computed === false && t.isIdentifier(property)
27
- ? property.name
28
- : computed === false && t.isPrivateName(property)
29
- ? `#${property.id.name}`
30
- : undefined;
31
- return [
32
- `computed=${encodeScalar(computed)}`,
33
- `optional=${encodeScalar(node.optional)}`,
34
- `propertyName=${encodeScalar(propertyName)}`,
35
- ];
36
- }
37
- if (node.type === "OptionalCallExpression") {
38
- return [
39
- `optional=${encodeScalar(node.optional)}`,
40
- ];
41
- }
42
- if (node.type === "VariableDeclaration") {
43
- return [
44
- `kind=${encodeScalar(node.kind)}`,
45
- ];
46
- }
47
- if (node.type === "FunctionDeclaration" ||
48
- node.type === "FunctionExpression" ||
49
- node.type === "ArrowFunctionExpression" ||
50
- node.type === "ObjectMethod" ||
51
- node.type === "ClassMethod" ||
52
- node.type === "ClassPrivateMethod") {
53
- // Keep a uniform field set for all function-like nodes; some fields (e.g. `static`,
54
- // `computed`, `kind`) only exist on specific node types and will be `undefined` otherwise.
55
- const fields = [
56
- `async=${encodeScalar(node.async)}`,
57
- `generator=${encodeScalar(node.generator)}`,
58
- `kind=${encodeScalar(node.kind)}`,
59
- `static=${encodeScalar(node.static)}`,
60
- `computed=${encodeScalar(node.computed)}`,
61
- ];
62
- if (node.type === "ObjectMethod") {
63
- const computed = node.computed;
64
- const key = node.key;
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") {
82
- const computed = node.computed;
83
- const key = node.key;
84
- if (computed === true) {
85
- // For computed keys, the fingerprint comes from the key expression itself.
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)}`);
98
- }
99
- }
100
- return fields;
101
- }
102
- if (node.type === "ObjectProperty") {
103
- const computed = node.computed;
104
- const key = node.key;
105
- const fields = [`computed=${encodeScalar(computed)}`];
106
- if (computed === true) {
107
- // For computed keys, the fingerprint comes from the key expression itself.
108
- return fields;
109
- }
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
- return fields;
121
- }
122
- if (node.type === "ClassProperty" || node.type === "ClassPrivateProperty") {
123
- const computed = node.computed;
124
- 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;
136
- return [
137
- `static=${encodeScalar(node.static)}`,
138
- `computed=${encodeScalar(computed)}`,
139
- `keyName=${encodeScalar(keyName)}`,
140
- ];
141
- }
142
- if (node.type === "ImportSpecifier") {
143
- const imported = node
144
- .imported;
145
- const importedName = t.isIdentifier(imported)
146
- ? imported.name
147
- : t.isStringLiteral(imported)
148
- ? imported.value
149
- : undefined;
150
- return [`importedName=${encodeScalar(importedName)}`];
151
- }
152
- return [];
153
- };
@@ -1,3 +0,0 @@
1
- type Writer = (text: string) => void;
2
- export declare const writeFingerprintNode: (value: unknown, write: Writer) => void;
3
- export {};