chore: v17.0.0 release

This commit is contained in:
absidue
2026-03-16 18:38:10 +00:00
parent d46768dedb
commit cc09231db2
52 changed files with 1838 additions and 922 deletions

View File

@@ -1,6 +1,5 @@
import type { ESTree } from 'meriyah';
import { parseScript } from 'meriyah';
import { WALK_STOP, jsBuiltIns, memberBaseName, memberToString, walkAst } from './helpers.ts';
import { parseScript, type ESTree } from 'meriyah';
import { jsBuiltIns, memberBaseName, memberToString, walkAst } from './helpers.ts';
export interface ExtractionConfig {
/**
@@ -14,9 +13,20 @@ export interface ExtractionConfig {
collectDependencies?: boolean;
/**
* When `true`, traversal stops once the extraction is matched and all its dependencies (when `collectDependencies=true`) resolve.
* Only useful for small functions/vars without too many dependencies.
* Only useful for small functions/vars without too many dependencies. Deeper dependency trees will usually have the unresolvable
* member expression here and there, for example:
* ```js
* var Vmi = g.dX.window, Wr = Vmi?.yt?.config_ || Vmi?.ytcfg?.data_ || {};
* ```
*
* Since `Vmi.ytcfg` is a dependency, it will never resolve because it comes from `g.dX.window`, which is an external object we don't have access to.
* In cases like this, `stopWhenReady` option does nothing useful.
*/
stopWhenReady?: boolean;
/**
* If `true`, dependency collection is limited to the match context node itself.
*/
onlyProcessMatchContext?: boolean;
/**
* Name for easier identification of extractions.
*/
@@ -35,6 +45,7 @@ export interface VariableMetadata {
node?: any;
dependencies: Set<string>;
dependents: Set<string>;
prototypeAliases: Map<string, Set<VariableMetadata>>;
predeclared: boolean;
}
@@ -64,9 +75,10 @@ export class JsAnalyzer {
private readonly hasExtractions: boolean;
private readonly extractionStates: ExtractionState[];
private readonly dependentsTracker: Map<string, Set<string>> = new Map();
private pendingPrototypeAliasBinding: [string, VariableMetadata] | null = null;
public declaredVariables: Map<string, VariableMetadata> = new Map();
public iifeParamName: string | null = null;
public readonly declaredVariables: Map<string, VariableMetadata> = new Map();
/**
* Creates a new instance over the provided source.
@@ -135,7 +147,37 @@ export class JsAnalyzer {
const left = assignment.left;
const right = assignment.right;
// Detect things like `a.b = g.c.prototype` so later `a.b.foo = ...` can be attributed back to `g.c`.
if (
right.type === 'MemberExpression' &&
!right.computed &&
right.property.type === 'Identifier' &&
right.property.name === 'prototype'
) {
const prototypeSourceExpr = memberToString(right, this.source);
const aliasTargetExpr = left.type === 'Identifier' ? left.name : memberToString(left, this.source);
if (prototypeSourceExpr) {
const prototypeOwnerMeta = this.declaredVariables.get(
prototypeSourceExpr.replace('.prototype', '')
);
if (aliasTargetExpr && prototypeOwnerMeta) {
const aliasedPrototypeMembers = new Set<VariableMetadata>();
const aliasExpr = `${aliasTargetExpr}.`; // Had to add a dot here so we can detect it later when matching member expressions..
// Activate an alias binding context, so subsequent member assignments to the alias (`a.b.foo = ...`) can be tracked.
// NOTE: This assumes that the alias members come right after this declaration and are grouped together in the code, hehe :)
this.pendingPrototypeAliasBinding = [ aliasExpr, prototypeOwnerMeta ];
prototypeOwnerMeta.prototypeAliases.set(aliasExpr, aliasedPrototypeMembers);
}
}
}
if (left.type === 'Identifier') {
// This identifier existing means it was a pre-declared and
// we just got to it.
const existingVariable = this.declaredVariables.get(left.name);
if (!existingVariable) continue;
@@ -148,6 +190,33 @@ export class JsAnalyzer {
if (this.onMatch(existingVariable.node, existingVariable)) return;
} else if (assignment.left.type === 'MemberExpression') {
const memberName = memberToString(assignment.left, this.source);
const activeAliasExpr = this.pendingPrototypeAliasBinding?.[0];
// While an alias binding is active, collect member assignments made through the alias (`g.q.foo = ...`).
if (activeAliasExpr && (memberName?.includes(activeAliasExpr) || memberName === activeAliasExpr.slice(0, -1))) {
const aliasOwnerMeta = this.declaredVariables.get(this.pendingPrototypeAliasBinding?.[1].name || '');
if (aliasOwnerMeta) {
const existingAliasedMembers = aliasOwnerMeta.prototypeAliases.get(activeAliasExpr);
const aliasedMemberMeta: VariableMetadata = {
name: memberName,
node: currentNode,
dependents: this.dependentsTracker.get(memberName) || new Set<string>(),
predeclared: false,
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
dependencies: this.findDependencies(right, memberName)
};
if (existingAliasedMembers) {
existingAliasedMembers.add(aliasedMemberMeta);
} else {
aliasOwnerMeta.prototypeAliases.set(activeAliasExpr, new Set([ aliasedMemberMeta ]));
}
}
} else {
this.pendingPrototypeAliasBinding = null;
}
if (!memberName || this.declaredVariables.has(memberName)) continue;
const metadata: VariableMetadata = {
@@ -155,6 +224,7 @@ export class JsAnalyzer {
node: currentNode,
dependents: this.dependentsTracker.get(memberName) || new Set<string>(),
predeclared: false,
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
dependencies: this.findDependencies(right, memberName)
};
@@ -174,6 +244,8 @@ export class JsAnalyzer {
break;
}
case 'VariableDeclaration': {
this.pendingPrototypeAliasBinding = null;
for (const declaration of currentNode.declarations) {
if (declaration.id.type !== 'Identifier') continue;
@@ -181,6 +253,7 @@ export class JsAnalyzer {
name: declaration.id.name,
node: declaration,
dependents: this.dependentsTracker.get(declaration.id.name) || new Set<string>(),
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
dependencies: new Set(),
predeclared: false
};
@@ -188,9 +261,8 @@ export class JsAnalyzer {
const init = declaration.init;
if (!init && currentNode.kind === 'var') {
metadata.predeclared = true;
metadata.predeclared = true; // "var x, y, z;"
} else if (init && this.needsDependencyAnalysis(init)) {
// "var x, y, z;"
metadata.dependencies = this.findDependencies(init, metadata.name);
}
@@ -225,6 +297,7 @@ export class JsAnalyzer {
case 'ConditionalExpression':
case 'ObjectExpression':
case 'SequenceExpression':
case 'ClassExpression':
case 'Identifier':
return true;
default:
@@ -248,9 +321,23 @@ export class JsAnalyzer {
for (const state of this.extractionStates) {
if (!state.node) {
if (node.type === 'VariableDeclarator' && !node.init) continue;
result = state.config.match(node);
if (!result) continue;
state.node = node;
matched = true;
if (metadata) {
state.metadata = metadata;
state.dependents = metadata.dependents;
state.dependencies = metadata.dependencies;
if (typeof result !== 'boolean')
state.matchContext = result;
}
this.refreshExtractionState(state);
} else if (state.node !== node) {
// Use this as a chance to refresh readiness in case dependencies were resolved since last time
// we checked.
@@ -259,21 +346,7 @@ export class JsAnalyzer {
if (this.shouldStopTraversal()) {
return true;
}
continue;
}
matched = true;
if (metadata) {
state.metadata = metadata;
state.dependents = metadata.dependents;
state.dependencies = metadata.dependencies;
if (typeof result !== 'boolean')
state.matchContext = result;
}
this.refreshExtractionState(state);
}
if (!matched) return false;
@@ -423,6 +496,9 @@ export class JsAnalyzer {
walkAst(rootNode, {
enter: (n, parent) => {
switch (n.type) {
// Note for anybody debugging this in the future:
// *DO NOT* add MethodDefinition here.
// MethodDefinition.value is a FunctionExpression, so it is already handled...
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
@@ -456,9 +532,12 @@ export class JsAnalyzer {
break;
}
case 'VariableDeclaration': {
const scope = currentScope();
// var hoists to function scope...
const targetScope = n.kind === 'var'
? scopeStack.findLast((s) => s.type === 'function') ?? currentScope()
: currentScope();
for (const d of n.declarations) {
collectBindingIdentifiers(d.id, scope.names);
collectBindingIdentifiers(d.id, targetScope.names);
}
break;
}
@@ -477,8 +556,10 @@ export class JsAnalyzer {
// Ignore if it's a property name (e.g., "obj.prop" or "{prop: 1}"", we don't care about the "prop" name itself).
if (parent?.type === 'Property' && parent.key === n && !parent.computed) return;
// Ignore class method names. They are declarations, not external dependencies.
if (parent?.type === 'MethodDefinition' && parent.key === n && !parent.computed) return;
if (parent?.type === 'MemberExpression' && parent.property === n && !parent.computed) {
if (parent.object.type === 'ThisExpression') return; // Skip 'this.property' stuff.
if (parent.object.type === 'ThisExpression') return; // Skip 'this.property', etc.
const full = memberToString(parent, this.source);
if (!full) return;
@@ -489,6 +570,7 @@ export class JsAnalyzer {
dependencies.add(full);
} else if (parent.object.type === 'Identifier') {
const baseName = parent.object.name;
const declaredBaseVariable = this.declaredVariables.get(baseName);
if (
(declaredBaseVariable || baseName === this.iifeParamName) &&
@@ -509,6 +591,10 @@ export class JsAnalyzer {
return;
}
if (parent?.type === 'MetaProperty') {
return; // Skip stuff like "new.target" or "import.meta"
}
if (isInScope(n.name) || jsBuiltIns.has(n.name)) return;
// It's a free variable, so it's a dependency.
@@ -528,6 +614,12 @@ export class JsAnalyzer {
}
break;
}
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement': {
scopeStack.push({ names: new Set<string>(), type: 'block' });
break;
}
}
},
leave: (n: any) => {
@@ -537,6 +629,9 @@ export class JsAnalyzer {
case 'ArrowFunctionExpression':
case 'BlockStatement':
case 'CatchClause':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
if (scopeStack.length > 1) scopeStack.pop();
break;
}