UNPKG

tsconfig-replace-paths

Version:

Replace absolute paths to relative paths for package compilation

233 lines (186 loc) 6.79 kB
#! /usr/bin/env node import * as program from 'commander' import { existsSync, readFileSync, writeFileSync } from 'fs' import { sync } from 'globby' import { dirname, relative, resolve } from 'path' import { loadConfig } from './util' program .version('0.0.1') .option('-p, --project <file>', 'path to tsconfig.json') .option('-s, --src <path>', 'source root path') .option('-o, --out <path>', 'output root path') .option('-v, --verbose', 'output logs') program.on('--help', () => { console.log(` $ tscpath -p tsconfig.json `) }) program.parse(process.argv) const { out: flagOut, project = 'tsconfig.json', src: flagSrc, verbose = false, } = program as { out?: string | undefined project?: string src?: string | undefined verbose?: boolean } const verboseLog = (...args: any[]): void => { if (verbose) { console.log(...args) } } const configFile = resolve(process.cwd(), project) const rootDir = resolve(process.cwd()) verboseLog(`Using tsconfig: ${configFile}`) const exitingErr = (): any => { throw new Error('--- exiting tsconfig-replace-paths due to parameters missing ---') } const missingConfigErr = (property: string): any => { console.error(`Whoops! Please set ${property} in your tsconfig or supply a flag`) exitingErr() } const missingDirectoryErr = (directory: string, flag: string): any => { console.error( `Whoops! ${directory} must be specified in your project => --project ${project}, or flagged with directory => ${flag} './path'`, ) exitingErr() } // Imported the TS Config const returnedTsConfig = loadConfig(configFile) // Destructure only the necessary keys, and rename to give context const { baseUrl, paths, outDir: tsConfigOutDir = '', rootDir: tsConfigRootDir = rootDir } = returnedTsConfig // If no flagSrc or tsConfigRootDir, error if (!flagSrc && tsConfigRootDir === '') { missingConfigErr('compilerOptions.rootDir') } // If no flagOut or tsConfigOutDir, error if (!flagOut && tsConfigOutDir === '') { missingConfigErr('compilerOptions.outDir') } // Are we going to use the flag or ts config for src? let usingSrcDir: string if (flagSrc) { verboseLog('Using flag --src') usingSrcDir = resolve(flagSrc) } else { verboseLog('Using compilerOptions.rootDir from your tsconfig') usingSrcDir = resolve(tsConfigRootDir) } if (!usingSrcDir) { missingDirectoryErr('rootDir', '--src') } // Log which src is being used verboseLog(`Using src: ${usingSrcDir}`) // Are we going to use the flag or ts config for out? let usingOutDir: string if (flagOut) { verboseLog('Using flag --out') usingOutDir = resolve(flagOut) } else { verboseLog('Using compilerOptions.outDir from your tsconfig') usingOutDir = resolve(tsConfigOutDir) } if (!usingOutDir) { missingDirectoryErr('outDir', '--out') } // Log which out is being used verboseLog(`Using out: ${usingOutDir}`) if (!baseUrl) { throw new Error('compilerOptions.baseUrl is not set') } if (!paths) { throw new Error('compilerOptions.paths is not set') } if (!usingOutDir) { throw new Error('compilerOptions.outDir is not set') } if (!usingSrcDir) { throw new Error('compilerOptions.rootDir is not set') } verboseLog(`baseUrl: ${baseUrl}`) verboseLog(`rootDir: ${usingSrcDir}`) verboseLog(`outDir: ${usingOutDir}`) verboseLog(`paths: ${JSON.stringify(paths, null, 2)}`) const configDir = dirname(configFile) const basePath = resolve(configDir, baseUrl) verboseLog(`basePath: ${basePath}`) const outPath = usingOutDir || resolve(basePath, usingOutDir) verboseLog(`outPath: ${outPath}`) const outFileToSrcFile = (x: string): string => resolve(usingSrcDir, relative(outPath, x)) const aliases = Object.keys(paths) .map(alias => ({ prefix: alias.replace(/\*$/, ''), aliasPaths: paths[alias as keyof typeof paths].map(p => resolve(basePath, p.replace(/\*$/, ''))), })) .filter(({ prefix }) => prefix) verboseLog(`aliases: ${JSON.stringify(aliases, null, 2)}`) const toRelative = (from: string, x: string): string => { const rel = relative(from, x) return (rel.startsWith('.') ? rel : `./${rel}`).replace(/\\/g, '/') } const exts = ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.json'] let replaceCount = 0 const absToRel = (modulePath: string, outFile: string): string => { const alen = aliases.length for (let j = 0; j < alen; j += 1) { const { prefix, aliasPaths } = aliases[j] if (modulePath.startsWith(prefix)) { const modulePathRel = modulePath.substring(prefix.length) const srcFile = outFileToSrcFile(outFile) const outRel = relative(basePath, outFile) verboseLog(`${outRel} (source: ${relative(basePath, srcFile)}):`) verboseLog(`\timport '${modulePath}'`) const len = aliasPaths.length for (let i = 0; i < len; i += 1) { const apath = aliasPaths[i] let moduleSrc = resolve(apath, modulePathRel) const moduleExt = exts.find(ext => moduleSrc.endsWith(ext)) if (moduleExt) { moduleSrc = moduleSrc.replace(moduleExt, '') } if (existsSync(moduleSrc) || exts.some(ext => existsSync(moduleSrc + ext))) { const rel = toRelative(dirname(srcFile), moduleExt ? `${moduleSrc}${moduleExt}` : moduleSrc) replaceCount += 1 verboseLog(`\treplacing '${modulePath}' -> '${rel}' referencing ${relative(basePath, moduleSrc)}`) return rel } } verboseLog(`\tcould not replace ${modulePath}`) } } return modulePath } const requireRegex = /(?:import|require)\(['"]([^'"]*)['"]\)/g const importRegex = /(?:import|from) ['"]([^'"]*)['"]/g const replaceImportStatement = (orig: string, matched: string, outFile: string): string => { const index = orig.indexOf(matched) return orig.substring(0, index) + absToRel(matched, outFile) + orig.substring(index + matched.length) } const replaceAlias = (text: string, outFile: string): string => text .replace(requireRegex, (orig, matched) => replaceImportStatement(orig, matched, outFile)) .replace(importRegex, (orig, matched) => replaceImportStatement(orig, matched, outFile)) // import relative to absolute path const files = sync(`${outPath.replaceAll('\\', '/')}/**/*.{js,jsx,ts,tsx}`, { dot: true, noDir: true, } as any).map(x => resolve(x)) let changedFileCount = 0 const flen = files.length let count = 0 for (let i = 0; i < flen; i += 1) { const file = files[i] const text = readFileSync(file, 'utf8') const prevReplaceCount = replaceCount const newText = replaceAlias(text, file) if (text !== newText) { changedFileCount += 1 verboseLog(`${file}: replaced ${replaceCount - prevReplaceCount} paths`) writeFileSync(file, newText, 'utf8') count = count + 1 } } console.log(`Replaced ${replaceCount} paths in ${changedFileCount} files`)