UNPKG

typia

Version:

Superfast runtime validators with only one line

254 lines (225 loc) 7.53 kB
import path from "path"; import ts from "typescript"; export namespace ImportTransformer { export const transform = (props: { from: string; to: string }) => (context: ts.TransformationContext) => (file: ts.SourceFile) => transform_file({ top: props.from, to: props.to, context, file, }); const transform_file = (props: { top: string; to: string; context: ts.TransformationContext; file: ts.SourceFile; }): ts.SourceFile => { if (props.file.isDeclarationFile) return props.file; const from: string = get_directory_path( path.resolve(props.file.getSourceFile().fileName), ); const to: string = from.replace(props.top, props.to); // First pass: transform relative imports let transformedFile = ts.visitEachChild( props.file, (node) => transform_node({ top: props.top, from, to, node, }), props.context, ); // Second pass: remove unused typia imports transformedFile = removeUnusedTypiaImports(transformedFile); return transformedFile; }; const transform_node = (props: { top: string; from: string; to: string; node: ts.Node; }) => { if ( !ts.isImportDeclaration(props.node) || !ts.isStringLiteral(props.node.moduleSpecifier) ) return props.node; const text: string = props.node.moduleSpecifier.text; if (text[0] !== ".") return props.node; const location: string = path.resolve(props.from, text); if (location.indexOf(props.top) === 0) return props.node; const replaced: string = (() => { const simple: string = path .relative(props.to, location) .split(path.sep) .join("/"); return simple[0] === "." ? simple : `./${simple}`; })(); return ts.factory.createImportDeclaration( undefined, props.node.importClause, ts.factory.createStringLiteral(replaced), props.node.assertClause, ); }; } const get_directory_path = (file: string): string => { const split: string[] = path.resolve(file).split(path.sep); split.pop(); return path.resolve(split.join(path.sep)); }; /** Remove unused typia imports that are only used in transformable calls */ const removeUnusedTypiaImports = (file: ts.SourceFile): ts.SourceFile => { // Find typia imports and collect all identifiers interface ImportMetadata { declaration: ts.ImportDeclaration; default: boolean; } const imports: Map<string, ImportMetadata> = new Map(); for (const stmt of file.statements) { if ( ts.isImportDeclaration(stmt) === false || ts.isStringLiteral(stmt.moduleSpecifier) === false || stmt.moduleSpecifier.text !== "typia" || stmt.importClause === undefined ) continue; // Track default import (import typia from 'typia') if (stmt.importClause.name) imports.set(stmt.importClause.name.text, { declaration: stmt, default: true, }); // Track named imports (import { tags } from 'typia') - keep these if ( stmt.importClause.namedBindings && ts.isNamedImports(stmt.importClause.namedBindings) ) for (const element of stmt.importClause.namedBindings.elements) imports.set(element.name.text, { declaration: stmt, default: false, }); } if (imports.size === 0) return file; // No typia imports to check // Find usage of typia identifiers that are NOT transformable calls const nonTransformableUsage = new Set<string>(); const checkUsage = (node: ts.Node) => { if (ts.isIdentifier(node) && imports.has(node.text)) { const identifier: string = node.text; // Check if this identifier is being used as part of a property access if ( node.parent && ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node ) { // This is typia.something - check if it's a transformable call pattern if (!isLikelyTransformableCall(node.parent)) nonTransformableUsage.add(identifier); } else // Direct usage of the typia identifier (not as property access) // This is definitely non-transformable usage nonTransformableUsage.add(identifier); } ts.forEachChild(node, checkUsage); }; // Check all statements except import declarations for (const stmt of file.statements) if ( !ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier) || stmt.moduleSpecifier.text !== "typia" ) checkUsage(stmt); // Update import statements const newStatements: ts.Statement[] = file.statements .map((stmt) => { if ( ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === "typia" && stmt.importClause ) { const newImportClause = filterTypiaImportClause( stmt.importClause, nonTransformableUsage, ); if (newImportClause) return ts.factory.createImportDeclaration( stmt.modifiers, newImportClause, stmt.moduleSpecifier, stmt.attributes, ); return null; // Skip adding the import if all imports are unused } return stmt; }) .filter((stmt) => stmt !== null); return ts.factory.updateSourceFile( file, newStatements, file.isDeclarationFile, file.referencedFiles, file.typeReferenceDirectives, file.hasNoDefaultLib, file.libReferenceDirectives, ); }; /** * Check if a property access expression looks like a transformable typia call * This uses heuristics to detect patterns like typia.xxx(), * typia.namespace.xxx() */ const isLikelyTransformableCall = ( node: ts.PropertyAccessExpression, ): boolean => { // Check if this is eventually part of a call expression let current: ts.Node = node; // Walk up the chain to find if this leads to a call expression // Handle patterns like: typia.xxx(), typia.namespace.xxx() while (ts.isPropertyAccessExpression(current)) { current = current.parent; } // If the final result is a call expression, this is likely transformable if ( ts.isCallExpression(current) && (current.expression === node || (ts.isPropertyAccessExpression(current.expression) && isTypiaPropertyChain(current.expression))) ) { return true; } return false; }; /** Check if a property access expression is part of a typia.xxx.yyy chain */ const isTypiaPropertyChain = (node: ts.PropertyAccessExpression): boolean => { let current: ts.Expression = node; while (ts.isPropertyAccessExpression(current)) { current = current.expression; } return ts.isIdentifier(current) && current.text === "typia"; }; /** Filter import clause to remove unused default imports */ const filterTypiaImportClause = ( importClause: ts.ImportClause, usedImports: Set<string>, ): ts.ImportClause | undefined => { const hasDefaultImport = importClause.name && usedImports.has(importClause.name.text); const namedBindings = importClause.namedBindings; // Always keep named bindings like { tags } // Return undefined if no imports are used if (!hasDefaultImport && !namedBindings) { return undefined; } return ts.factory.createImportClause( importClause.isTypeOnly, hasDefaultImport ? importClause.name : undefined, namedBindings, ); };