diff --git a/Example/Input/Expected.ts b/Example/Input/Expected.ts new file mode 100644 index 00000000..37832f02 --- /dev/null +++ b/Example/Input/Expected.ts @@ -0,0 +1,24 @@ +// @ts-nocheck + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from "fs"; +import * as path from "path"; + +const root = path.dirname(path.dirname(__dirname)); + +const platform = process.platform; + +console.log( + path.join( + root, + ".build", + "node", + `v${/^target="(.*)"$/m.exec(fs.readFileSync(path.join(root, "remote", ".npmrc"), "utf8"))![1]}`, + `${platform}-${process.arch}`, + platform === "win32" ? "node.exe" : "node", + ), +); diff --git a/Example/Output/Expected.ts b/Example/Output/Expected.ts new file mode 100644 index 00000000..dc772dc4 --- /dev/null +++ b/Example/Output/Expected.ts @@ -0,0 +1,11 @@ +// @ts-nocheck +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fs from "fs"; +import * as path from "path"; +const root = path.dirname(path.dirname(__dirname)); +const platform = process.platform; +console.log(path.join(path.dirname(path.dirname(__dirname)), ".build", "node", `v${/^target="(.*)"$/m.exec(fs.readFileSync(path.join(path.dirname(path.dirname(__dirname)), "remote", ".npmrc"), "utf8"))![1]}`, `${process.platform}-${process.arch}`, process.platform + === "win32" ? "node.exe" : "node")); diff --git a/Example/Output/Predefined.ts b/Example/Output/Predefined.ts index c14c0d34..4a6cd60d 100644 --- a/Example/Output/Predefined.ts +++ b/Example/Output/Predefined.ts @@ -5,6 +5,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from "fs"; import * as path from "path"; + const root = path.dirname(path.dirname(__dirname)); const npmrcPath = path.join(root, "remote", ".npmrc"); const npmrc = fs.readFileSync(npmrcPath, "utf8"); @@ -12,5 +13,21 @@ const version = /^target="(.*)"$/m.exec(npmrc)![1]; const platform = process.platform; const arch = process.arch; const node = platform === "win32" ? "node.exe" : "node"; -const nodePath = path.join(root, ".build", "node", `v${version}`, `${platform}-${arch}`, node); -console.log(path.join(root, ".build", "node", `v${version}`, `${platform}-${arch}`, node)); +const nodePath = path.join( + root, + ".build", + "node", + `v${version}`, + `${platform}-${arch}`, + node, +); +console.log( + path.join( + path.dirname(path.dirname(__dirname)), + ".build", + "node", + `v${/^target="(.*)"$/m.exec(fs.readFileSync(path.join(path.dirname(path.dirname(__dirname)), "remote", ".npmrc"), "utf8"))![1]}`, + `${process.platform}-${process.arch}`, + process.platform === "win32" ? "node.exe" : "node", + ), +); diff --git a/Source/Function/Output/Transformer/Visit.ts b/Source/Function/Output/Transformer/Visit.ts index 6d76d5bd..7bfe937c 100644 --- a/Source/Function/Output/Transformer/Visit.ts +++ b/Source/Function/Output/Transformer/Visit.ts @@ -26,15 +26,22 @@ class DeclarationTracker { }>; isInlined: boolean; + + declarationNode: VariableStatement; } >(); - trackVariable(name: string, initializer: Expression): void { + trackVariable( + name: string, + initializer: Expression, + declarationNode: VariableStatement, + ): void { if (!this.variableMap.has(name)) { this.variableMap.set(name, { initializer, uses: new Set(), isInlined: false, + declarationNode, }); } } @@ -68,6 +75,9 @@ class DeclarationTracker { ); }); + console.log(entry.initializer.getText()); + console.log(referenceUses.length); + // Only inline if: // 1. Has initializer // 2. Not reassigned @@ -91,9 +101,24 @@ class DeclarationTracker { } } + shouldRemoveDeclaration(statement: VariableStatement): boolean { + // Check if all variables in this statement have been inlined + return statement.declarationList.declarations.every((decl) => { + if (!ts.isIdentifier(decl.name)) return false; + + return this.variableMap.get(decl.name.text)?.isInlined === true; + }); + } + clear(): void { this.variableMap.clear(); } + + hasInlinedVariables(): boolean { + return Array.from(this.variableMap.values()).some( + (entry) => entry.isInlined, + ); + } } class Transformer { @@ -140,39 +165,22 @@ class Transformer { } private visitVariableStatement(node: VariableStatement): Statement { - const declarations = node.declarationList.declarations; - - // Track all variable declarations and their initializers - declarations.forEach((decl) => { + // First track all declarations in this statement + node.declarationList.declarations.forEach((decl) => { if (ts.isIdentifier(decl.name) && decl.initializer) { - const name = decl.name.text; - - this.tracker.trackVariable(name, decl.initializer); + this.tracker.trackVariable( + decl.name.text, + decl.initializer, + node, + ); } }); - // Filter out declarations that have been inlined - const remainingDeclarations = declarations.filter((decl) => { - if (!ts.isIdentifier(decl.name)) return true; - - return !this.tracker.shouldInline(decl.name.text); - }); - - if (remainingDeclarations.length === 0) { + // If all declarations in this statement have been inlined, remove it + if (this.tracker.shouldRemoveDeclaration(node)) { return ts.factory.createEmptyStatement(); } - if (remainingDeclarations.length !== declarations.length) { - return ts.factory.updateVariableStatement( - node, - node.modifiers, - ts.factory.createVariableDeclarationList( - remainingDeclarations, - node.declarationList.flags, - ), - ); - } - return node; } @@ -199,7 +207,16 @@ class Transformer { ); } - transform(sourceFile: Node): Node { + transform(sourceFile: Node, passCount: number = 0): Node { + const MAX_PASSES = 10; + + if (passCount >= MAX_PASSES) { + return sourceFile; + } + + // Clear the tracker for this pass + this.tracker.clear(); + // First pass: collect all declarations and uses ts.visitNode(sourceFile, (node) => { if (ts.isVariableStatement(node)) { @@ -208,6 +225,7 @@ class Transformer { this.tracker.trackVariable( decl.name.text, decl.initializer, + node, ); } }); @@ -223,8 +241,15 @@ class Transformer { return node; }); - // Second pass: perform the actual transformation - return ts.visitNode(sourceFile, (node) => this.visitNode(node)); + // Second pass: perform the transformation + const result = ts.visitNode(sourceFile, (node) => this.visitNode(node)); + + // If we made any changes in this pass, do another pass + if (this.tracker.hasInlinedVariables() && result !== sourceFile) { + return this.transform(result, passCount + 1); + } + + return result; } } diff --git a/Source/Function/Output/Transformer/test.ts b/Source/Function/Output/Transformer/test.ts index e69de29b..c9c4a6b2 100644 --- a/Source/Function/Output/Transformer/test.ts +++ b/Source/Function/Output/Transformer/test.ts @@ -0,0 +1,250 @@ +import type Interface from "@Interface/Output/Transformer/Visit.js"; +import type { + Expression, + Identifier, + Node, + Statement, + TransformationContext, + VariableStatement, +} from "typescript"; + +class DeclarationTracker { + private variableMap = new Map< + string, + { + initializer: Expression; + uses: Set<{ + node: Node; + isReference: boolean; + }>; + isInlined: boolean; + declarationNode: VariableStatement; + } + >(); + + private inlinedDeclarations = new Set(); + + trackVariable( + name: string, + initializer: Expression, + declarationNode: VariableStatement, + ): void { + if (!this.variableMap.has(name)) { + this.variableMap.set(name, { + initializer, + uses: new Set(), + isInlined: false, + declarationNode, + }); + } + } + + trackUse(name: string, node: Node, isReference: boolean = true): void { + const entry = this.variableMap.get(name); + if (entry) { + entry.uses.add({ node, isReference }); + } + } + + shouldInline(name: string): boolean { + const entry = this.variableMap.get(name); + if (!entry || entry.isInlined) return false; + + const referenceUses = Array.from(entry.uses).filter( + (use) => use.isReference, + ); + + // Check for reassignment + const isReassigned = Array.from(entry.uses).some((use) => { + const parent = use.node.parent; + return ( + ts.isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isIdentifier(parent.left) && + parent.left.text === name + ); + }); + + // Only inline if: + // 1. Has initializer + // 2. Not reassigned + // 3. Used between 1-2 times as reference + const shouldInline = !isReassigned && + referenceUses.length > 0 && + referenceUses.length <= 2; + + if (shouldInline) { + // Track the declaration for removal + this.inlinedDeclarations.add(entry.declarationNode); + } + + return shouldInline; + } + + getInitializer(name: string): Expression | undefined { + return this.variableMap.get(name)?.initializer; + } + + markInlined(name: string): void { + const entry = this.variableMap.get(name); + if (entry) { + entry.isInlined = true; + } + } + + shouldRemoveDeclaration(statement: VariableStatement): boolean { + // Check if this statement has been marked for removal + if (this.inlinedDeclarations.has(statement)) { + return true; + } + + // Fallback: Check if all variables in this statement have been inlined + return statement.declarationList.declarations.every((decl) => { + if (!ts.isIdentifier(decl.name)) return false; + const entry = this.variableMap.get(decl.name.text); + return entry?.isInlined === true; + }); + } + + clear(): void { + this.variableMap.clear(); + this.inlinedDeclarations.clear(); + } + + hasInlinedVariables(): boolean { + return Array.from(this.variableMap.values()).some( + (entry) => entry.isInlined, + ); + } +} + +class Transformer { + private readonly context: TransformationContext; + private readonly tracker: DeclarationTracker; + + constructor(context: TransformationContext) { + this.context = context; + this.tracker = new DeclarationTracker(); + } + + private visitIdentifier(node: Identifier): Expression { + const name = node.text; + + // Don't process identifiers that are property names or declaration names + if ( + (ts.isPropertyAccessExpression(node.parent) && + node.parent.name === node) || + ts.isVariableDeclaration(node.parent) || + ts.isBindingElement(node.parent) + ) { + return node; + } + + if (this.tracker.shouldInline(name)) { + const initializer = this.tracker.getInitializer(name); + if (initializer) { + // Mark as inlined + this.tracker.markInlined(name); + + // Create a deep copy of the initializer and return it directly + return ts.factory.cloneNode(initializer); + } + } + + return node; + } + + private visitVariableStatement(node: VariableStatement): Statement | undefined { + // Track all declarations in this statement + node.declarationList.declarations.forEach((decl) => { + if (ts.isIdentifier(decl.name) && decl.initializer) { + this.tracker.trackVariable( + decl.name.text, + decl.initializer, + node, + ); + } + }); + + // Remove if all declarations are inlined + if (this.tracker.shouldRemoveDeclaration(node)) { + return undefined; + } + + return node; + } + + private visitNode(node: Node): Node | undefined { + // Collect variable uses + if (ts.isIdentifier(node) && !ts.isVariableDeclaration(node.parent)) { + this.tracker.trackUse(node.text, node); + } + + // Handle specific node types + if (ts.isVariableStatement(node)) { + return this.visitVariableStatement(node); + } + + if (ts.isIdentifier(node)) { + return this.visitIdentifier(node); + } + + // Recursively visit children, filtering out undefined results + return ts.visitEachChild( + node, + (child) => this.visitNode(child), + this.context, + ); + } + + transform(sourceFile: Node, passCount: number = 0): Node { + const MAX_PASSES = 10; + if (passCount >= MAX_PASSES) { + return sourceFile; + } + + this.tracker.clear(); + + // First pass: collect declarations and uses + ts.visitNode(sourceFile, (node) => { + if (ts.isVariableStatement(node)) { + node.declarationList.declarations.forEach((decl) => { + if (ts.isIdentifier(decl.name) && decl.initializer) { + this.tracker.trackVariable( + decl.name.text, + decl.initializer, + node, + ); + } + }); + } + + if (ts.isIdentifier(node) && !ts.isVariableDeclaration(node.parent)) { + this.tracker.trackUse(node.text, node); + } + + return node; + }); + + // Second pass: perform transformation + const result = ts.visitNode(sourceFile, (node) => this.visitNode(node)); + + // If changes were made, do another pass + if (this.tracker.hasInlinedVariables() && result !== sourceFile) { + return this.transform(result, passCount + 1); + } + + return result; + } +} + +export const { + default: ts, + isIdentifier, + factory, +} = await import("typescript"); + +export default ((context: TransformationContext) => (rootNode) => + new Transformer(context).transform( + rootNode, + )) satisfies Interface as Interface; diff --git a/Target/Function/Output/Transformer/Visit.js b/Target/Function/Output/Transformer/Visit.js index 36fc78e5..ed8515b3 100644 --- a/Target/Function/Output/Transformer/Visit.js +++ b/Target/Function/Output/Transformer/Visit.js @@ -1 +1 @@ -class l{variableMap=new Map;trackVariable(t,e){this.variableMap.has(t)||this.variableMap.set(t,{initializer:e,uses:new Set,isInlined:!1})}trackUse(t,e,r=!0){const a=this.variableMap.get(t);a&&a.uses.add({node:e,isReference:r})}shouldInline(t){const e=this.variableMap.get(t);if(!e||e.isInlined)return!1;const r=Array.from(e.uses).filter(n=>n.isReference);return!Array.from(e.uses).some(n=>{const s=n.node.parent;return i.isBinaryExpression(s)&&s.operatorToken.kind===i.SyntaxKind.EqualsToken&&i.isIdentifier(s.left)&&s.left.text===t})&&r.length>0&&r.length<=2}getInitializer(t){return this.variableMap.get(t)?.initializer}markInlined(t){const e=this.variableMap.get(t);e&&(e.isInlined=!0)}clear(){this.variableMap.clear()}}class f{context;tracker;constructor(t){this.context=t,this.tracker=new l}visitIdentifier(t){const e=t.text;if(i.isPropertyAccessExpression(t.parent)&&t.parent.name===t||i.isVariableDeclaration(t.parent)||i.isBindingElement(t.parent))return t;if(this.tracker.shouldInline(e)){const r=this.tracker.getInitializer(e);if(r)return this.tracker.markInlined(e),i.visitNode(r,a=>i.isExpression(a)?a:i.factory.createIdentifier(e))}return t}visitVariableStatement(t){const e=t.declarationList.declarations;e.forEach(a=>{if(i.isIdentifier(a.name)&&a.initializer){const n=a.name.text;this.tracker.trackVariable(n,a.initializer)}});const r=e.filter(a=>i.isIdentifier(a.name)?!this.tracker.shouldInline(a.name.text):!0);return r.length===0?i.factory.createEmptyStatement():r.length!==e.length?i.factory.updateVariableStatement(t,t.modifiers,i.factory.createVariableDeclarationList(r,t.declarationList.flags)):t}visitNode(t){return i.isIdentifier(t)&&!i.isVariableDeclaration(t.parent)&&this.tracker.trackUse(t.text,t),i.isVariableStatement(t)?this.visitVariableStatement(t):i.isIdentifier(t)?this.visitIdentifier(t):i.visitEachChild(t,e=>this.visitNode(e),this.context)}transform(t){return i.visitNode(t,e=>(i.isVariableStatement(e)&&e.declarationList.declarations.forEach(r=>{i.isIdentifier(r.name)&&r.initializer&&this.tracker.trackVariable(r.name.text,r.initializer)}),i.isIdentifier(e)&&!i.isVariableDeclaration(e.parent)&&this.tracker.trackUse(e.text,e),e)),i.visitNode(t,e=>this.visitNode(e))}}const{default:i,isIdentifier:c,factory:p}=await import("typescript");var m=o=>t=>new f(o).transform(t);export{m as default,p as factory,c as isIdentifier,i as ts}; +class l{variableMap=new Map;trackVariable(e,t,a){this.variableMap.has(e)||this.variableMap.set(e,{initializer:t,uses:new Set,isInlined:!1,declarationNode:a})}trackUse(e,t,a=!0){const n=this.variableMap.get(e);n&&n.uses.add({node:t,isReference:a})}shouldInline(e){const t=this.variableMap.get(e);if(!t||t.isInlined)return!1;const a=Array.from(t.uses).filter(r=>r.isReference),n=Array.from(t.uses).some(r=>{const s=r.node.parent;return i.isBinaryExpression(s)&&s.operatorToken.kind===i.SyntaxKind.EqualsToken&&i.isIdentifier(s.left)&&s.left.text===e});return console.log(t.initializer.getText()),console.log(a.length),!n&&a.length>0&&a.length<=2}getInitializer(e){return this.variableMap.get(e)?.initializer}markInlined(e){const t=this.variableMap.get(e);t&&(t.isInlined=!0)}shouldRemoveDeclaration(e){return e.declarationList.declarations.every(t=>i.isIdentifier(t.name)?this.variableMap.get(t.name.text)?.isInlined===!0:!1)}clear(){this.variableMap.clear()}hasInlinedVariables(){return Array.from(this.variableMap.values()).some(e=>e.isInlined)}}class f{context;tracker;constructor(e){this.context=e,this.tracker=new l}visitIdentifier(e){const t=e.text;if(i.isPropertyAccessExpression(e.parent)&&e.parent.name===e||i.isVariableDeclaration(e.parent)||i.isBindingElement(e.parent))return e;if(this.tracker.shouldInline(t)){const a=this.tracker.getInitializer(t);if(a)return this.tracker.markInlined(t),i.visitNode(a,n=>i.isExpression(n)?n:i.factory.createIdentifier(t))}return e}visitVariableStatement(e){return e.declarationList.declarations.forEach(t=>{i.isIdentifier(t.name)&&t.initializer&&this.tracker.trackVariable(t.name.text,t.initializer,e)}),this.tracker.shouldRemoveDeclaration(e)?i.factory.createEmptyStatement():e}visitNode(e){return i.isIdentifier(e)&&!i.isVariableDeclaration(e.parent)&&this.tracker.trackUse(e.text,e),i.isVariableStatement(e)?this.visitVariableStatement(e):i.isIdentifier(e)?this.visitIdentifier(e):i.visitEachChild(e,t=>this.visitNode(t),this.context)}transform(e,t=0){if(t>=10)return e;this.tracker.clear(),i.visitNode(e,r=>(i.isVariableStatement(r)&&r.declarationList.declarations.forEach(s=>{i.isIdentifier(s.name)&&s.initializer&&this.tracker.trackVariable(s.name.text,s.initializer,r)}),i.isIdentifier(r)&&!i.isVariableDeclaration(r.parent)&&this.tracker.trackUse(r.text,r),r));const n=i.visitNode(e,r=>this.visitNode(r));return this.tracker.hasInlinedVariables()&&n!==e?this.transform(n,t+1):n}}const{default:i,isIdentifier:c,factory:p}=await import("typescript");var m=o=>e=>new f(o).transform(e);export{m as default,p as factory,c as isIdentifier,i as ts}; diff --git a/Target/Function/Output/Transformer/test.d.ts b/Target/Function/Output/Transformer/test.d.ts deleted file mode 100644 index e69de29b..00000000