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.
- package/dist/core/stable-naming.d.ts +1 -0
- package/dist/core/stable-naming.js +1 -0
- package/dist/transforms/_generated/manifest.js +6 -0
- package/dist/transforms/_generated/registry.js +2 -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 +3 -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/fingerprint-leaf-node.d.ts +2 -0
- package/dist/transforms/stabilize-nested-bindings/fingerprint-leaf-node.js +23 -0
- package/dist/transforms/stabilize-nested-bindings/fingerprint-scalar-fields.d.ts +2 -0
- package/dist/transforms/stabilize-nested-bindings/fingerprint-scalar-fields.js +140 -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/hash-fingerprint-node.d.ts +1 -0
- package/dist/transforms/stabilize-nested-bindings/hash-fingerprint-node.js +9 -0
- package/dist/transforms/stabilize-nested-bindings/is-factory-callback.d.ts +6 -0
- package/dist/transforms/stabilize-nested-bindings/is-factory-callback.js +15 -0
- package/dist/transforms/stabilize-nested-bindings/manifest.json +13 -0
- package/dist/transforms/stabilize-nested-bindings/serialize-fingerprint-node.d.ts +3 -0
- package/dist/transforms/stabilize-nested-bindings/serialize-fingerprint-node.js +102 -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/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":
|
|
4
|
-
"notes": "Measured with baseline none:
|
|
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,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,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,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,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.
|
|
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",
|