Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Fix issues with module resolution & config parsing #4

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
module.exports = {
extends: ['standard']
extends: ['standard'],
rules: {
'no-prototype-builtins': 'off'
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ npm install -D babel-plugin-tsconfig-paths
"babel-plugin-tsconfig-paths",
{
"relative": true,
"keepSourceExt": false,
"extensions": [
".js",
".jsx",
Expand Down Expand Up @@ -50,6 +51,9 @@ npm install -D babel-plugin-tsconfig-paths
converting aliased import paths.
* Default: `true`

* `keepSourceExt` : Should the resolved alias maintain the extension of the source file?
* Default: `false`

* `extensions` : Which file extensions to resolve.
* Default: `[".js", ".jsx", ".ts", ".tsx", ".es", ".es6", ".mjs"]`

Expand Down
12 changes: 12 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const path = require('path')

module.exports = ({
verbose: true,
transform: {
'\\.m?[jt]sx?$': require.resolve('babel-jest')
},
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
path.join(__dirname, 'tests', '?(*.)+(spec|test).[jt]s?(x)')
]
})
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = ({ types }) => ({
name: 'tsconfig-paths-resolver',

pre () {
this.opts = this.opts || {}
this.types = types
this.runtimeOpts = getOptions(this.opts)
this.resolverVisited = new Set()
Expand Down
113 changes: 76 additions & 37 deletions lib/opts.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const fs = require('fs')
const path = require('path')
const JSON5 = require('json5')
const {
cacheTransformString,
cacheTsPaths,
objKeyCache,
stringKeyCache,
escapeRegExp
} = require('./utils')

Expand All @@ -20,53 +22,89 @@ const transformFunctions = [
'require.requireMock'
]

const getRootDir = cacheTransformString((str) => {
if (!str) str = process.cwd()
if (!path.isAbsolute(str)) {
str = path.resolve(
path.join(process.cwd(), str)
)
}
return str
})
const getConfigPath = cacheTransformString((config = 'tsconfig.json', root = process.cwd()) => {
if (!path.isAbsolute(config)) {
config = path.resolve(
path.join(root, config)
)
}
return config
})
const getTsconfigAliases = cacheTsPaths((tsconfig = {
const getTsconfigAliases = (tsconfig = {
compilerOptions: {}
}) => {
if (!tsconfig.compilerOptions) return tsconfig

const aliases = tsconfig.compilerOptions.paths || {}

const resolveAliases = []
const base = tsconfig.compilerOptions.baseUrl
if (!base) return []

Object.entries(aliases).forEach(([alias, resolutions]) => {
const newAlias = new RegExp(`^${escapeRegExp(alias).replace(/\*/g, '(.+)')}`)
const resolveAliases = Object.entries(aliases).reduce((
arr,
[alias, resolutions]
) => {
const aliasPattern = new RegExp(`^${escapeRegExp(alias).replace(/\*/g, '(.*?)')}$`)
const transformers = resolutions.map((res) => {
let cnt = 1
do {
const hasWildcard = res.indexOf('*') > -1
if (!hasWildcard) break
while (res.indexOf('*') > -1) {
res = res.replace('*', `$${cnt++}`)
} while (true) // eslint-disable-line no-constant-condition
}
return res
})
resolveAliases.push({
alias: newAlias,
arr.push({
base,
alias: aliasPattern,
transformers
})
})
return arr
}, [])

return resolveAliases
}

const getConfig = stringKeyCache((tsconfigPath) => {
try {
const rawConfig = fs.readFileSync(tsconfigPath, 'utf-8')
let tsconfig = JSON5.parse(rawConfig)

const tsconfigDir = path.dirname(tsconfigPath)

if (tsconfig.compilerOptions && tsconfig.compilerOptions.baseUrl) {
tsconfig.compilerOptions.baseUrl = path.resolve(
tsconfigDir,
tsconfig.compilerOptions.baseUrl
)
}

if (tsconfig.extends) {
const extendPath = path.resolve(tsconfigDir, tsconfig.extends)

const {
tsconfig: newTsconfig = {}
} = getConfig(extendPath)

tsconfig = {
...newTsconfig,
...tsconfig,
compilerOptions: {
...newTsconfig.compilerOptions || {},
...tsconfig.compilerOptions || {}
}
}
}

return {
tsconfig,
aliases: getTsconfigAliases(tsconfig)
}
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`Unable to read tsconfig; file does not exist: ${path}`)
}
throw err
}
})

exports.getOptions = (opts) => {
const rootDir = getRootDir(opts.rootDir)
const configPath = getConfigPath(opts.tsconfig, rootDir)
const tsconfig = require(configPath)
exports.getOptions = objKeyCache((opts) => {
const configPath = path.resolve(
opts.rootDir || process.cwd(),
opts.tsconfig || 'tsconfig.json'
)

const { tsconfig, aliases } = getConfig(configPath)
const base = tsconfig.compilerOptions.baseUrl || ''
const basePath = path.isAbsolute(base)
? base
Expand All @@ -80,10 +118,11 @@ exports.getOptions = (opts) => {
return {
configPath,
basePath,
aliases: getTsconfigAliases(tsconfig),
relative: opts.relative || true,
aliases,
keepSourceExt: opts.keepSourceExt == null ? false : opts.keepSourceExt,
relative: opts.relative == null ? true : opts.relative,
extensions: opts.extensions || extensions,
transformFunctions: opts.transformFunctions || transformFunctions,
skipModuleResolver: !base
}
}
})
111 changes: 69 additions & 42 deletions lib/resolve.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const fs = require('fs')
const path = require('path')
const {
stringKeyCache,
objKeyCache,
isRelative,
checkFile
} = require('./utils')

const getResolvers = (importPath, aliases) => {
const resolver = aliases.find(({ alias }) => alias.test(importPath))
Expand All @@ -12,11 +18,61 @@ const getRelativePath = (from, to) => {
to
)

return relPath.startsWith('./') || relPath.startsWith('../')
return isRelative(relPath)
? relPath
: `./${relPath}`
}

const getImportTransformer = objKeyCache((aliases, opts) => stringKeyCache((importPath) => {
const { extensions, keepSourceExt } = opts

const resolver = getResolvers(importPath, aliases)
if (!resolver) return

const { base, alias, transformers } = resolver

for (const transformer of transformers) {
const transformedImport = path.join(
base,
importPath.replace(alias, transformer)
)

const checkImports = [transformedImport]
try {
const stat = fs.statSync(transformedImport)
if (stat && stat.isDirectory()) {
checkImports.push(path.join(transformedImport, 'index'))
}
} catch (err) {
// noop
}

// If the original import has an extension then check for it, too, when
// checking for existence of the file
const checkExtensions = [''].concat(extensions)

for (const curImport of checkImports) {
const fileVariations = checkExtensions.map((ext) => curImport + ext)

const realFile = fileVariations.find((file) => {
const { exists, isDirectory } = checkFile(file)

// If the import path is a directory, then it should resolve to the index module
// otherwise, the import is invalid and should not be resolved
//
// This should allow us to resolve modules that also have the same name as the directory,
// or resolve the index when the directory is the desired import.
return exists && !isDirectory
})

if (realFile) {
if (keepSourceExt) return realFile
return curImport
}
}
}
}))

/**
* Assuming an alias like this
* ```
Expand Down Expand Up @@ -57,40 +113,20 @@ const getRelativePath = (from, to) => {
* @returns {string} The resolved path of the import. If unable to return a
* resolved path, then void is returned.
*/
const resolvePath = (sourceFile, importPath, basePath, extensions, aliases, relative) => {
const resolver = getResolvers(importPath, aliases)
if (!resolver) return
const resolvePath = (sourceFile, importPath, opts) => {
if (isRelative(importPath)) return importPath

const { alias, transformers } = resolver
const { aliases, relative } = opts

for (const transformer of transformers) {
const transformedImport = path.join(
basePath,
importPath.replace(alias, transformer)
)
let checkImport = transformedImport
try {
const stat = fs.statSync(transformedImport)
if (stat && stat.isDirectory()) {
checkImport = path.join(transformedImport, 'index')
}
} catch (err) {
// noop
}
const transformImport = getImportTransformer(aliases, opts)
const validImport = transformImport(importPath)

// If the original import has an extension, then check for it, too, when
// checking for existence of the file
const checkExtensions = [''].concat(extensions)
const fileVariations = checkExtensions.map((ext) => checkImport + ext)

const realFile = fileVariations.find((file) => fs.existsSync(file))
if (realFile) {
return (
relative
? getRelativePath(sourceFile, realFile)
: realFile
).replace(/\\/g, '/')
}
if (validImport) {
return (
relative
? getRelativePath(sourceFile, validImport)
: validImport
).replace(/\\/g, '/')
}
}

Expand All @@ -104,19 +140,10 @@ exports.updateImportPath = (nodePath, state) => {

const currentFile = state.file.opts.filename
const importPath = nodePath.node.value
const {
basePath,
aliases,
relative,
extensions
} = state.runtimeOpts
const modulePath = resolvePath(
currentFile,
importPath,
basePath,
extensions,
aliases,
relative
state.runtimeOpts
)
if (modulePath) {
nodePath.replaceWith(state.types.stringLiteral(modulePath))
Expand Down
Loading