esbuild-fix-imports-plugin
Version:
An ESBuild plugin that fixes import paths by applying fixAliasPlugin, fixFolderImportsPlugin, and fixExtensionsPlugin. It ensures correct file extensions, resolves path aliases, and fixes directory imports in your build output when using 'tsup' with 'bund
1 lines • 14.9 kB
Source Map (JSON)
{"version":3,"sources":["../../src/fixAliasPlugin.ts"],"sourcesContent":["import { dirname, join, parse, relative, resolve } from \"path\";\nimport { type Plugin } from \"esbuild\";\nimport { loadTsConfig } from \"load-tsconfig\";\nimport { existsSync } from \"fs\";\n\n// Get the current working directory as the origin absolute path.\nconst originAbsolutePath = process.cwd();\n\n/**\n * ESBuild plugin to forcefully replace path aliases with relative paths in the output files.\n * It reads the alias configuration from tsconfig.json and modifies the output files to resolve the aliases.\n * Also handles baseUrl-based imports when no explicit path aliases are configured.\n *\n * @returns {import('esbuild').Plugin} An ESBuild plugin object.\n */\nexport const fixAliasPlugin = (): Plugin => ({\n name: \"fixAliasPlugin\",\n setup: (build) => {\n // Resolve the absolute path to the output directory.\n const outDir = build.initialOptions.outdir ?? \"dist\";\n const outDirAbsolutePath = resolve(originAbsolutePath, outDir);\n\n // Determine the output file extension based on the build options.\n const outExtension = build.initialOptions.outExtension?.[\".js\"] ?? \".js\";\n\n // Import the tsconfig.json\n const tsConfig = loadTsConfig(\n originAbsolutePath,\n build.initialOptions.tsconfig ?? \"./tsConfig.ts\"\n );\n\n // Extract the 'paths' from the tsconfig compilerOptions, or default to an empty object.\n const alias = tsConfig?.data.compilerOptions?.paths ?? {};\n\n // Extract the 'baseUrl' from the tsconfig compilerOptions\n const baseUrl = tsConfig?.data.compilerOptions?.baseUrl;\n\n // Hook into the 'onEnd' event of the build process.\n build.onEnd((result) => {\n // If there are errors, do not proceed.\n if (result.errors.length > 0) {\n return;\n }\n\n const entryFiles = (build.initialOptions.entryPoints ?? []) as string[];\n const outputFiles = (result.outputFiles ?? []).filter((outputFile) =>\n outputFile.path.endsWith(outExtension)\n );\n\n // Iterate over each output file generated by ESBuild.\n for (const outputFile of outputFiles) {\n // Only target files with the specified output extension.\n // This ignores additional files emitted, like sourcemaps (e.g., \"*.js.map\").\n\n // If `entry` field is defined in the tsup.config.ts. It will make dismatch between the output file path and the relative path.\n // Result 'folder/index.mjs',\n const relativeOutputPath = relative(\n // '/Users/user/Documents/app/dist/esm/',\n outDirAbsolutePath,\n // '/Users/user/Documents/app/dist/esm/folder/index.mjs',\n outputFile.path\n );\n\n // 'folder/index',\n const relativeOutputPathWithoutExt =\n getPathWithoutExtension(relativeOutputPath);\n\n // 'src/folder/index.ts',\n const entryFile =\n entryFiles.find((filePath) =>\n filePath.includes(relativeOutputPathWithoutExt)\n ) ?? \"\";\n\n // 'src/folder/index',\n const entryFilePathWithoutExt = getPathWithoutExtension(entryFile);\n\n // 'src/',\n const ignoredPath = entryFilePathWithoutExt.replace(\n relativeOutputPathWithoutExt,\n \"\"\n );\n\n // Calculate the relative path from the output file to the output directory.\n // '../',\n const relativePath = relative(\n // '/Users/user/Documents/app/dist/esm/folder',\n dirname(outputFile.path),\n // '/Users/user/Documents/app/dist/esm/',\n outDirAbsolutePath\n );\n\n // Get the original file contents.\n const fileContents = outputFile.text;\n // Modify the file contents by replacing aliases with relative paths.\n const nextFileContents = modifyAlias(\n fileContents,\n relativePath,\n alias,\n ignoredPath,\n baseUrl,\n originAbsolutePath\n );\n\n // Update the output file contents with the modified contents.\n outputFile.contents = Buffer.from(nextFileContents);\n }\n });\n },\n});\n\n/**\n * Gets the path without the file extension.\n *\n * @param {string} path - The path to process.\n * @returns {string} The path without the file extension.\n */\nexport const getPathWithoutExtension = (path: string): string => {\n // Parse the file path\n const parsedPath = parse(path);\n // Reconstruct the path without the extension\n return join(parsedPath.dir, parsedPath.name);\n};\n\n// Regular expressions to match ESM import statements and CJS require statements.\nconst ESM_IMPORT_EXP = /from\\s*['\"]([^'\"]+)['\"]/g;\nconst CJS_REQUIRE_EXP = /require\\s*\\(\\s*['\"]([^'\"]+?)['\"]\\s*\\)/g;\n\n/**\n * Modifies the contents of a file by replacing path aliases with relative paths.\n *\n * @param {string} contents - The contents of the file to modify.\n * @param {string} relativePath - The relative path from the file to the output directory.\n * @param {Record<string, string[]>} alias - The alias configuration from tsconfig.json.\n * @param {string} ignoredPath - The ignored path from the entry file to the output file\n * @param {string} baseUrl - The baseUrl from the tsconfig compilerOptions\n * @param {string} originAbsolutePath - The origin absolute path\n * @returns {string} The modified file contents.\n */\nconst modifyAlias = (\n contents: string,\n relativePath: string,\n alias: Record<string, string[]>,\n ignoredPath: string,\n baseUrl: string | undefined,\n originAbsolutePath: string\n) => {\n let result = contents;\n\n // Replace ESM import paths that match aliases with the new relative paths.\n result = result.replace(ESM_IMPORT_EXP, (match, importPath) => {\n const newImportPath = replaceAliasInPath(\n importPath,\n relativePath,\n alias,\n ignoredPath,\n baseUrl,\n originAbsolutePath\n );\n return match.replace(importPath, newImportPath);\n });\n\n // Replace CJS require paths that match aliases with the new relative paths.\n result = result.replace(CJS_REQUIRE_EXP, (match, importPath) => {\n const newImportPath = replaceAliasInPath(\n importPath,\n relativePath,\n alias,\n ignoredPath,\n baseUrl,\n originAbsolutePath\n );\n return match.replace(importPath, newImportPath);\n });\n\n return result;\n};\n\n/**\n * Cleans up the path by removing redundant './' and '../' segments.\n *\n * @param {string} path - The path to clean up.\n * @returns {string} The cleaned up path.\n */\nconst cleanPath = (path: string) => path.replace(\"/./\", \"/\").replace(\"//\", \"/\");\n\n/**\n * Replaces an import path if it matches any alias with the corresponding relative path.\n *\n * @param {string} importPath - The original import path from the file.\n * @param {string} relativePath - The relative path from the file to the output directory.\n * @param {Record<string, string[]>} alias - The alias configuration from tsconfig.json.\n * @param {string} ignoredPath - The ignored path from the entry file to the output file\n * @param {string} baseUrl - The baseUrl from the tsconfig compilerOptions\n * @param {string} originAbsolutePath - The origin absolute path\n * @returns {string} The new import path with aliases replaced by relative paths.\n */\nconst replaceAliasInPath = (\n importPath: string,\n relativePath: string,\n alias: Record<string, string[]>,\n ignoredPath: string,\n baseUrl: string | undefined,\n originAbsolutePath: string\n) => {\n // Iterate over each alias key in the alias configuration.\n for (const aliasKey in alias) {\n const aliasPatterns = alias[aliasKey]; // Array of alias paths\n\n // Iterate over each pattern associated with the alias key.\n for (const aliasPattern of aliasPatterns) {\n const fixedAliasPattern = aliasPattern.replace(ignoredPath, \"\");\n\n if (aliasKey.endsWith(\"*\")) {\n // Handle wildcard aliases.\n const aliasKeyBase = aliasKey.slice(0, -1);\n const aliasPatternBase = fixedAliasPattern.slice(0, -1);\n\n // Check if the import path starts with the alias base.\n if (importPath.startsWith(aliasKeyBase)) {\n const restOfPath = importPath.slice(aliasKeyBase.length);\n\n // Construct the new relative path.\n const newRelativePath = `./${relativePath}/${aliasPatternBase}${restOfPath}`;\n\n // Clean up the new relative path.\n let cleanedNewRelativePath = cleanPath(newRelativePath);\n\n return cleanedNewRelativePath;\n }\n } else if (importPath === aliasKey) {\n // Construct the new relative path.\n const newRelativePath = `./${relativePath}/${fixedAliasPattern}`;\n\n // Clean up the new relative path.\n let cleanedNewRelativePath = cleanPath(newRelativePath);\n\n return cleanedNewRelativePath;\n }\n }\n }\n\n // If no explicit alias matches and baseUrl is configured, try to handle baseUrl-based imports\n if (\n baseUrl &&\n !isRelativeImport(importPath) &&\n !isNodeModuleImport(importPath)\n ) {\n // Check if this could be a baseUrl-relative import\n const resolvedBaseUrl = resolve(originAbsolutePath, baseUrl);\n\n // Try to find the file with common extensions\n const possibleExtensions = [\"\", \".ts\", \".tsx\", \".js\", \".jsx\", \".json\"];\n let foundPath = null;\n\n for (const ext of possibleExtensions) {\n const fullPath = resolve(resolvedBaseUrl, importPath + ext);\n if (existsSync(fullPath)) {\n foundPath = fullPath;\n break;\n }\n\n // Also try with index file\n const indexPath = resolve(resolvedBaseUrl, importPath, \"index\" + ext);\n if (existsSync(indexPath)) {\n foundPath = indexPath;\n break;\n }\n }\n\n if (foundPath) {\n // This is a valid baseUrl-based import, convert it to relative\n const relativeToBase = relative(resolvedBaseUrl, foundPath);\n const cleanedRelativeToBase = getPathWithoutExtension(relativeToBase);\n\n // Construct the new relative path\n const newRelativePath = `./${relativePath}/${cleanedRelativeToBase}`;\n const cleanedNewRelativePath = cleanPath(newRelativePath);\n\n return cleanedNewRelativePath;\n }\n }\n\n // Return the original import path if no alias matches.\n return importPath;\n};\n\n/**\n * Checks if an import path is a relative import (starts with ./ or ../)\n */\nconst isRelativeImport = (importPath: string): boolean => {\n return importPath.startsWith(\"./\") || importPath.startsWith(\"../\");\n};\n\n/**\n * Checks if an import path is a node module import (not relative and not absolute)\n * This is a simple heuristic - if it doesn't start with ./ or ../ or /,\n * and doesn't contain a file extension, it's likely a node module\n */\nconst isNodeModuleImport = (importPath: string): boolean => {\n // If it starts with a dot or slash, it's not a node module\n if (importPath.startsWith(\".\") || importPath.startsWith(\"/\")) {\n return false;\n }\n\n // If it contains no slashes, it's likely a node module (e.g., 'react', 'lodash')\n if (!importPath.includes(\"/\")) {\n return true;\n }\n\n // If it starts with @, it's likely a scoped package (e.g., '@types/node')\n if (importPath.startsWith(\"@\")) {\n return true;\n }\n\n // Otherwise, we assume it could be a baseUrl-based import\n return false;\n};\n"],"mappings":"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","names":["originAbsolutePath"]}