typia
Version:
Superfast runtime validators with only one line
165 lines (162 loc) • 7.14 kB
JavaScript
import path from 'path';
import ts from 'typescript';
var ImportTransformer;
(function (ImportTransformer) {
ImportTransformer.transform = (props) => (context) => (file) => transform_file({
top: props.from,
to: props.to,
context,
file,
});
const transform_file = (props) => {
if (props.file.isDeclarationFile)
return props.file;
const from = get_directory_path(path.resolve(props.file.getSourceFile().fileName));
const to = 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) => {
if (!ts.isImportDeclaration(props.node) ||
!ts.isStringLiteral(props.node.moduleSpecifier))
return props.node;
const text = props.node.moduleSpecifier.text;
if (text[0] !== ".")
return props.node;
const location = path.resolve(props.from, text);
if (location.indexOf(props.top) === 0)
return props.node;
const replaced = (() => {
const simple = 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);
};
})(ImportTransformer || (ImportTransformer = {}));
const get_directory_path = (file) => {
const split = 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) => {
const imports = 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();
const checkUsage = (node) => {
if (ts.isIdentifier(node) && imports.has(node.text)) {
const identifier = 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 = 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) => {
// Check if this is eventually part of a call expression
let current = 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) => {
let current = 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, usedImports) => {
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);
};
export { ImportTransformer };
//# sourceMappingURL=ImportTransformer.mjs.map