Created
April 20, 2025 03:40
-
-
Save DanielSRS/3485eff63926c1b7a69151e91b68c0f8 to your computer and use it in GitHub Desktop.
Bundle a set of typescript files into a single one without modifying the source code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ts from "typescript"; | |
import fs from "node:fs"; | |
import path from "node:path"; | |
import { fileURLToPath } from "node:url"; | |
const SKIP_THIRD_PARTY_MODULES = true; | |
const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
/** | |
* Get the module path based on the base directory. | |
* If the path is absolute, return it as is. | |
* If the path is relative, resolve it against the base directory. | |
* @param filePath The module path to resolve. | |
* @param baseDir The base directory to resolve against. | |
* @returns The resolved module path. | |
*/ | |
function resolveModule(filePath: string, baseDir: string): string { | |
return path.isAbsolute(filePath) ? filePath : path.resolve(baseDir, filePath); | |
} | |
function bundleFiles(entryFile: string, outputFile: string) { | |
const visitedFiles = new Set<string>(); | |
const output: string[] = []; | |
processFile(visitedFiles, output, entryFile); | |
fs.writeFileSync(outputFile, output.join("\n"), "utf-8"); | |
console.log(`Bundled file written to ${outputFile}`); | |
} | |
/** | |
* Recursively process a TypeScript file, resolving and inlining imports. | |
* It removes the `export` keyword from the statements and handles third-party modules. | |
* @param visitedFiles A set to keep track of visited files to avoid circular dependencies. | |
* @param output An array to store the processed source files. | |
* @param filePath The path of the TypeScript file to process. | |
*/ | |
function processFile( | |
visitedFiles: Set<string>, | |
output: string[], | |
filePath: string | |
) { | |
/** | |
* Do nothing if the file has already been visited | |
*/ | |
if (visitedFiles.has(filePath)) return; | |
visitedFiles.add(filePath); | |
const fileContent = fs.readFileSync(filePath, "utf-8"); | |
/** | |
* Create a TypeScript source file from the file content | |
* This allows us to traverse the AST and manipulate the nodes | |
* using TypeScript's API | |
*/ | |
const sourceFile = ts.createSourceFile( | |
filePath, | |
fileContent, | |
ts.ScriptTarget.Latest, | |
true | |
); | |
/** | |
* For each statement in the source file, check if its an import. if it is, | |
* resolve and inline the module, otherwise, remove the `export` keyword | |
*/ | |
sourceFile.statements.forEach((statement) => { | |
if (ts.isImportDeclaration(statement) && statement.moduleSpecifier) { | |
const moduleName = (statement.moduleSpecifier as ts.StringLiteral).text; | |
/** | |
* Check if is module from node_modules | |
* If is, keep the import statement as is | |
* and do not resolve it | |
*/ | |
if ( | |
SKIP_THIRD_PARTY_MODULES && | |
!moduleName.startsWith(".") && | |
!moduleName.startsWith("/") && | |
!moduleName.startsWith("file:") | |
) { | |
// Keep the import statement for node_modules dependencies | |
const cleanedStatement = statement.getFullText(sourceFile); | |
output.push(cleanedStatement); | |
return; | |
} | |
const resolvedPath = resolveModule( | |
moduleName + ".ts", | |
path.dirname(filePath) | |
); | |
processFile(visitedFiles, output, resolvedPath); | |
} | |
if (ts.isExportDeclaration(statement) || ts.isExportAssignment(statement)) { | |
// Skip `export` declarations (e.g., `export { ... }` or `export default`) | |
return; | |
} | |
// Remove only the `export` keyword while preserving the rest | |
const cleanedStatement = cleanExportKeyword(statement, sourceFile); | |
output.push(cleanedStatement); | |
}); | |
} | |
/** | |
* Remove the `export` keyword from the beginning of a statement | |
*/ | |
function cleanExportKeyword( | |
statement: ts.Statement, | |
sourceFile: ts.SourceFile | |
): string { | |
const printer = ts.createPrinter(); | |
const statementText = printer.printNode( | |
ts.EmitHint.Unspecified, | |
statement, | |
sourceFile | |
); | |
// Use a regex to remove only the `export` keyword at the beginning of a line or statement | |
return statementText.replace(/^export\s+/gm, "").trim(); | |
} | |
const entryFile = path.resolve(__dirname, "src/index.ts"); | |
const outputFile = path.resolve(__dirname, "dist/output.ts"); | |
bundleFiles(entryFile, outputFile); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment