Last active
June 14, 2018 11:58
-
-
Save f-space/e584c1db96e62c25671945bdc13de6d3 to your computer and use it in GitHub Desktop.
Webpack resolver plugin for tsconfig.json (baseUrl, paths, rootDirs).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs'); | |
const { posix, win32 } = require('path'); | |
const path = (process.platform !== 'win32') ? posix : { | |
resolve() { return replaceSep(win32.resolve.apply(this, arguments)); }, | |
relative() { return replaceSep(win32.relative.apply(this, arguments)); }, | |
normalize() { return replaceSep(win32.normalize.apply(this, arguments)); }, | |
join() { return replaceSep(win32.join.apply(this, arguments)); }, | |
dirname() { return replaceSep(win32.dirname.apply(this, arguments)); }, | |
}; | |
function replaceSep(p) { | |
return p.replace(/\\/g, '/'); | |
} | |
function byTs(pattern, request) { | |
return pattern.test(request.context.issuer); | |
} | |
function normalizeOptions(options) { | |
if (typeof options === "string") { | |
return normalizeOptions({ config: options }); | |
} else if (typeof options !== "object" || options === null) { | |
return normalizeOptions({}); | |
} else if (typeof options.config !== "string" && options.config !== null) { | |
return normalizeOptions(Object.assign(options, { config: "tsconfig.json" })) | |
} | |
if (options.config !== null && !options.loaded) { | |
options.options = Object.assign(loadTsCompilerOptions(options.config), options.options); | |
options.loaded = true; | |
} else { | |
options.options = options.options || {}; | |
} | |
options.test = RegExp(options.test || /\.tsx?$/); | |
return options; | |
} | |
function loadTsCompilerOptions(path) { | |
if (fs.existsSync(path)) { | |
const config = fs.readFileSync(path, { encoding: 'utf8' }); | |
const json = JSON.parse(config); | |
const options = json && json.compilerOptions; | |
return options || {}; | |
} else { | |
throw new Error(`'${path}' not found.`); | |
} | |
} | |
class TsBaseUrlPlugin { | |
constructor(options) { | |
this.options = normalizeOptions(options); | |
const { baseUrl } = this.options.options; | |
this.baseUrl = (typeof baseUrl === 'string') ? path.normalize(baseUrl) : ""; | |
} | |
apply(resolver) { | |
if (this.baseUrl) { | |
const configPath = this.options.config || "."; | |
const root = path.resolve(path.dirname(configPath), this.baseUrl); | |
const target = resolver.ensureHook('resolve'); | |
resolver.getHook('module').tapAsync(this.constructor.name, (request, context, callback) => { | |
if (byTs(this.options.test, request)) { | |
const newRequest = Object.assign({}, request, { path: root, request: "./" + request.request }); | |
const message = `looking for modules in ${root}`; | |
return resolver.doResolve(target, newRequest, message, context, callback); | |
} | |
return callback(); | |
}); | |
} | |
} | |
} | |
class TsPathsPlugin { | |
constructor(options) { | |
this.options = normalizeOptions(options); | |
const { paths } = this.options.options; | |
this.mappings = this.makeMapping(paths); | |
} | |
apply(resolver) { | |
for (const { pattern, replacement } of this.mappings) { | |
const target = resolver.ensureHook('resolve'); | |
resolver.getHook('described-resolve').tapAsync(this.constructor.name, (request, context, callback) => { | |
if (byTs(this.options.test, request)) { | |
const innerRequest = request.request; | |
if (innerRequest && match(innerRequest, pattern)) { | |
const newInnerRequest = replace(capture(innerRequest, pattern), replacement) | |
const newRequest = Object.assign({}, request, { request: newInnerRequest, }); | |
const message = `aliased with mapping '${innerRequest}': '${toString(pattern)}' to '${toString(replacement)}'`; | |
return resolver.doResolve(target, newRequest, message, context, callback); | |
} | |
} | |
return callback(); | |
}); | |
} | |
function match(path, { prefix, suffix }) { | |
return (path.length >= prefix.length + suffix.length && path.startsWith(prefix) && path.endsWith(suffix)); | |
} | |
function capture(path, { prefix, suffix }) { | |
return path.substr(prefix.length, path.length - (prefix.length + suffix.length)); | |
} | |
function replace(captured, { prefix, suffix }) { | |
return prefix + captured + suffix; | |
} | |
function toString({ prefix, suffix }) { | |
return `${prefix}*${suffix}`; | |
} | |
} | |
makeMapping(paths = {}) { | |
const mappings = [] | |
for (const [key, values] of Object.entries(paths)) { | |
const [patternPrefix, patternSuffix] = key.split("*", 2); | |
if (Array.isArray(values)) { | |
for (const value of values) { | |
if (typeof value === 'string') { | |
const [replacementPrefix, replacementSuffix] = value.split("*", 2); | |
mappings.push({ | |
pattern: { | |
prefix: patternPrefix || "", | |
suffix: patternSuffix || "", | |
}, | |
replacement: { | |
prefix: replacementPrefix || "", | |
suffix: replacementSuffix || "", | |
}, | |
}); | |
} | |
} | |
} | |
} | |
return mappings; | |
} | |
} | |
class TsRootDirsPlugin { | |
constructor(options) { | |
this.options = normalizeOptions(options); | |
const { rootDirs } = this.options.options; | |
this.rootDirs = (rootDirs || []).filter(dir => typeof dir === 'string').map(dir => path.normalize(dir)); | |
} | |
apply(resolver) { | |
const configPath = this.options.config || "."; | |
const roots = this.rootDirs.map(dir => path.resolve(path.dirname(configPath), dir)); | |
for (const root of roots) { | |
const target = resolver.ensureHook('resolve'); | |
resolver.getHook('described-resolve').tapAsync(this.constructor.name, (request, context, callback) => { | |
if (byTs(this.options.test, request)) { | |
const innerRequest = request.request; | |
if (innerRequest && isRelative(innerRequest)) { | |
const absolutePath = path.join(request.path, innerRequest); | |
const matchedRoot = roots.find(root => absolutePath.startsWith(root)); | |
if (matchedRoot && matchedRoot !== root) { | |
const newInnerRequest = path.resolve(root, path.relative(matchedRoot, absolutePath)); | |
const newRequest = Object.assign({}, request, { request: newInnerRequest, }); | |
const message = `looking for modules in ${root}`; | |
return resolver.doResolve(target, newRequest, message, context, callback); | |
} | |
} | |
} | |
return callback(); | |
}); | |
} | |
function isRelative(p) { | |
return /^\.\.?$|^\.\.?\//.test(p); | |
} | |
} | |
} | |
class TsConfigPlugin { | |
constructor(options) { | |
this.options = normalizeOptions(options); | |
this.plugins = [ | |
new TsBaseUrlPlugin(this.options), | |
new TsPathsPlugin(this.options), | |
new TsRootDirsPlugin(this.options), | |
]; | |
} | |
apply(resolver) { | |
for (const plugin of this.plugins) { | |
plugin.apply(resolver); | |
} | |
} | |
} | |
module.exports = TsConfigPlugin; | |
module.exports.TsBaseUrlPlugin = TsBaseUrlPlugin; | |
module.exports.TsPathsPlugin = TsPathsPlugin; | |
module.exports.TsRootDirsPlugin = TsRootDirsPlugin; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment