tsconfig-replace-paths
Version:
Replace absolute paths to relative paths for package compilation
233 lines (186 loc) • 6.79 kB
text/typescript
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`)