ts-add-js-extension
Version:
Add .js extension to each relative ESM import and export statement in JavaScript file
247 lines (246 loc) • 7.69 kB
JavaScript
import fs from "fs";
import path from "path";
import tsc from "typescript";
import { extensions, matchJs, separator } from "./const.js";
import { guard } from "./type.js";
const formProperFilePath = (props) => {
return props.filePath.split(separator).filter(Boolean).join(separator);
};
const checkJavaScriptFileExistByAppend = (props) => {
const result = extensions.javaScript
.map((extension) => {
return {
extension,
filePath: `${props.filePath}${extension}`,
};
})
.find((filePath) => {
return fs.existsSync(filePath.filePath);
});
return result ?? false;
};
const checkTypeDefinitionFileExistByAppend = (props) => {
const [js, mjs] = extensions.javaScript;
const [dts, mdts] = extensions.typeDefinition;
const dtsFilePath = `${props.filePath}${dts}`;
if (fs.existsSync(dtsFilePath)) {
return { extension: js, filePath: dtsFilePath };
}
const mdtsFilePath = `${props.filePath}${mdts}`;
if (fs.existsSync(mdtsFilePath)) {
return { extension: mjs, filePath: mdtsFilePath };
}
return false;
};
const isDirectory = (path) => {
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
};
const addJSExtensionConditionally = (props) => {
const check = props.checkType === "js"
? checkJavaScriptFileExistByAppend
: checkTypeDefinitionFileExistByAppend;
const skip = {
procedure: "skip",
};
if (isDirectory(props.filePath)) {
const result = check({
filePath: path.posix.join(props.filePath, "index"),
});
if (!result) {
return skip;
}
const file = `index${result.extension}`;
return {
procedure: "proceed",
absolutePath: result.filePath,
importPath: formProperFilePath({
filePath: `${props.importPath}${separator}${file}`,
}),
};
}
const result = check({
filePath: props.filePath,
});
if (!result) {
return skip;
}
return {
procedure: "proceed",
absolutePath: result.filePath,
importPath: formProperFilePath({
filePath: `${props.importPath}${result.extension}`,
}),
};
};
const addJSExtension = (props) => {
if (matchJs(props.filePath)) {
return {
procedure: "skip",
};
}
const result = addJSExtensionConditionally({
...props,
checkType: "js",
});
switch (result.procedure) {
case "proceed": {
return result;
}
case "skip": {
return addJSExtensionConditionally({
...props,
checkType: "dts",
});
}
}
};
const generateModuleSpecifier = (props) => {
if (!props.moduleSpecifier.startsWith(".")) {
return undefined;
}
const result = addJSExtension({
importPath: props.moduleSpecifier,
filePath: path.posix.join(props.file, "..", props.moduleSpecifier),
});
switch (result.procedure) {
case "proceed": {
if (props.files.find((file) => {
return file.endsWith(result.absolutePath);
})) {
return result.importPath;
}
}
}
return undefined;
};
const nodeIsStringLiteral = (node) => {
return (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node));
};
const dynamicJsImport = (props) => {
const { node } = props;
if (node.expression.kind === tsc.SyntaxKind.ImportKeyword) {
const argument = guard({
value: node.arguments[0],
error: new Error(`Dynamic import must have a path`),
});
if (nodeIsStringLiteral(argument)) {
const text = generateModuleSpecifier({
...props.fileInfo,
moduleSpecifier: argument.text,
});
if (!text) {
return node;
}
props.fileInfo.addFile();
return props.context.factory.updateCallExpression(node, node.expression, node.typeArguments, [props.context.factory.createStringLiteral(text)]);
}
}
return node;
};
const dynamicDtsImport = (props) => {
const { node } = props;
const { argument } = node;
if (tsc.isLiteralTypeNode(argument)) {
const { literal } = argument;
if (nodeIsStringLiteral(literal)) {
const text = generateModuleSpecifier({
...props.fileInfo,
moduleSpecifier: literal.text,
});
if (!text) {
return node;
}
props.fileInfo.addFile();
return props.context.factory.updateImportTypeNode(node, props.context.factory.updateLiteralTypeNode(argument, props.context.factory.createStringLiteral(text)), node.attributes, node.qualifier, undefined, node.isTypeOf);
}
}
return node;
};
const staticImportExport = (props) => {
const { node } = props;
const { moduleSpecifier } = node;
if (!moduleSpecifier || !tsc.isStringLiteral(moduleSpecifier)) {
return node;
}
const text = generateModuleSpecifier({
...props.fileInfo,
moduleSpecifier: moduleSpecifier.text,
});
if (!text) {
return node;
}
props.fileInfo.addFile();
const newModuleSpecifier = {
...moduleSpecifier,
text,
};
if (tsc.isImportDeclaration(node)) {
return props.context.factory.updateImportDeclaration(node, node.modifiers, node.importClause, newModuleSpecifier, node.attributes);
}
return props.context.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, newModuleSpecifier, node.attributes);
};
const updateImportExport = (props) => {
return (parent) => {
const node = tsc.visitEachChild(parent, updateImportExport(props), props.context);
if (tsc.isImportDeclaration(node) || tsc.isExportDeclaration(node)) {
return staticImportExport({
...props,
node,
});
}
else if (tsc.isCallExpression(node)) {
return dynamicJsImport({
...props,
node,
});
}
else if (tsc.isImportTypeNode(node)) {
return dynamicDtsImport({
...props,
node,
});
}
return node;
};
};
const traverse = (props) => {
return (context) => {
return (rootNode) => {
return tsc.visitNode(rootNode, updateImportExport({
...props,
context,
}));
};
};
};
const traverseAndUpdateFile = (metainfo) => {
const printer = tsc.createPrinter();
const updatedFiles = new Set();
const { fileName: file } = metainfo.sourceFile;
const transformer = traverse({
fileInfo: {
files: metainfo.files,
file,
addFile: () => {
updatedFiles.add(file);
},
},
});
const code = printer.printNode(tsc.EmitHint.Unspecified, guard({
value: tsc
.transform(metainfo.sourceFile, [transformer])
.transformed.at(0),
error: new Error("Transformer should have a transformed value"),
}), tsc.createSourceFile("", "", tsc.ScriptTarget.Latest));
if (!updatedFiles.has(file)) {
return [];
}
return [
{
file,
code,
},
];
};
export default traverseAndUpdateFile;
//# sourceMappingURL=traverse-and-update.js.map