UNPKG

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
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