Skip to content

Instantly share code, notes, and snippets.

@DanielSRS
Created April 20, 2025 03:40
Show Gist options
  • Save DanielSRS/3485eff63926c1b7a69151e91b68c0f8 to your computer and use it in GitHub Desktop.
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
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