2020-03-23 15:45:51 +01:00
|
|
|
/**
|
|
|
|
* This webpack resolver is largely based on TypeScript's "paths" handling
|
|
|
|
* The TypeScript license can be found here:
|
|
|
|
* https://github.com/microsoft/TypeScript/blob/214df64e287804577afa1fea0184c18c40f7d1ca/LICENSE.txt
|
|
|
|
*/
|
2020-04-29 23:56:18 +02:00
|
|
|
import path from 'path'
|
2021-01-14 02:59:08 +01:00
|
|
|
import { webpack } from 'next/dist/compiled/webpack/webpack'
|
2020-04-29 17:04:42 +02:00
|
|
|
import { debug } from 'next/dist/compiled/debug'
|
|
|
|
|
|
|
|
const log = debug('next:jsconfig-paths-plugin')
|
2020-03-23 15:45:51 +01:00
|
|
|
|
|
|
|
export interface Pattern {
|
|
|
|
prefix: string
|
|
|
|
suffix: string
|
|
|
|
}
|
|
|
|
|
|
|
|
const asterisk = 0x2a
|
|
|
|
|
|
|
|
export function hasZeroOrOneAsteriskCharacter(str: string): boolean {
|
|
|
|
let seenAsterisk = false
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
if (str.charCodeAt(i) === asterisk) {
|
|
|
|
if (!seenAsterisk) {
|
|
|
|
seenAsterisk = true
|
|
|
|
} else {
|
|
|
|
// have already seen asterisk
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-04-29 17:04:42 +02:00
|
|
|
/**
|
|
|
|
* Determines whether a path starts with a relative path component (i.e. `.` or `..`).
|
|
|
|
*/
|
2020-06-01 23:00:22 +02:00
|
|
|
export function pathIsRelative(testPath: string): boolean {
|
|
|
|
return /^\.\.?($|[\\/])/.test(testPath)
|
2020-04-29 17:04:42 +02:00
|
|
|
}
|
|
|
|
|
2020-03-23 15:45:51 +01:00
|
|
|
export function tryParsePattern(pattern: string): Pattern | undefined {
|
|
|
|
// This should be verified outside of here and a proper error thrown.
|
|
|
|
const indexOfStar = pattern.indexOf('*')
|
|
|
|
return indexOfStar === -1
|
|
|
|
? undefined
|
|
|
|
: {
|
2022-03-24 22:49:38 +01:00
|
|
|
prefix: pattern.slice(0, indexOfStar),
|
|
|
|
suffix: pattern.slice(indexOfStar + 1),
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) {
|
|
|
|
return (
|
|
|
|
candidate.length >= prefix.length + suffix.length &&
|
|
|
|
candidate.startsWith(prefix) &&
|
|
|
|
candidate.endsWith(suffix)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Return the object corresponding to the best pattern to match `candidate`. */
|
|
|
|
export function findBestPatternMatch<T>(
|
|
|
|
values: readonly T[],
|
|
|
|
getPattern: (value: T) => Pattern,
|
|
|
|
candidate: string
|
|
|
|
): T | undefined {
|
|
|
|
let matchedValue: T | undefined
|
|
|
|
// use length of prefix as betterness criteria
|
|
|
|
let longestMatchPrefixLength = -1
|
|
|
|
|
|
|
|
for (const v of values) {
|
|
|
|
const pattern = getPattern(v)
|
|
|
|
if (
|
|
|
|
isPatternMatch(pattern, candidate) &&
|
|
|
|
pattern.prefix.length > longestMatchPrefixLength
|
|
|
|
) {
|
|
|
|
longestMatchPrefixLength = pattern.prefix.length
|
|
|
|
matchedValue = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return matchedValue
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* patternStrings contains both pattern strings (containing "*") and regular strings.
|
|
|
|
* Return an exact match if possible, or a pattern match, or undefined.
|
|
|
|
* (These are verified by verifyCompilerOptions to have 0 or 1 "*" characters.)
|
|
|
|
*/
|
|
|
|
export function matchPatternOrExact(
|
|
|
|
patternStrings: readonly string[],
|
|
|
|
candidate: string
|
|
|
|
): string | Pattern | undefined {
|
|
|
|
const patterns: Pattern[] = []
|
|
|
|
for (const patternString of patternStrings) {
|
|
|
|
if (!hasZeroOrOneAsteriskCharacter(patternString)) continue
|
|
|
|
const pattern = tryParsePattern(patternString)
|
|
|
|
if (pattern) {
|
|
|
|
patterns.push(pattern)
|
|
|
|
} else if (patternString === candidate) {
|
|
|
|
// pattern was matched as is - no need to search further
|
|
|
|
return patternString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
return findBestPatternMatch(patterns, (_) => _, candidate)
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tests whether a value is string
|
|
|
|
*/
|
|
|
|
export function isString(text: unknown): text is string {
|
|
|
|
return typeof text === 'string'
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given that candidate matches pattern, returns the text matching the '*'.
|
|
|
|
* E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar"
|
|
|
|
*/
|
|
|
|
export function matchedText(pattern: Pattern, candidate: string): string {
|
|
|
|
return candidate.substring(
|
|
|
|
pattern.prefix.length,
|
|
|
|
candidate.length - pattern.suffix.length
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function patternText({ prefix, suffix }: Pattern): string {
|
|
|
|
return `${prefix}*${suffix}`
|
|
|
|
}
|
|
|
|
|
2021-10-13 12:17:54 +02:00
|
|
|
/**
|
|
|
|
* Calls the iterator function for each entry of the array
|
|
|
|
* until the first result or error is reached
|
|
|
|
*/
|
|
|
|
function forEachBail<TEntry>(
|
|
|
|
array: TEntry[],
|
|
|
|
iterator: (
|
|
|
|
entry: TEntry,
|
|
|
|
entryCallback: (err?: any, result?: any) => void
|
|
|
|
) => void,
|
|
|
|
callback: (err?: any, result?: any) => void
|
|
|
|
): void {
|
|
|
|
if (array.length === 0) return callback()
|
|
|
|
|
|
|
|
let i = 0
|
|
|
|
const next = () => {
|
|
|
|
let loop: boolean | undefined = undefined
|
|
|
|
iterator(array[i++], (err, result) => {
|
|
|
|
if (err || result !== undefined || i >= array.length) {
|
|
|
|
return callback(err, result)
|
|
|
|
}
|
|
|
|
if (loop === false) while (next());
|
|
|
|
loop = true
|
|
|
|
})
|
|
|
|
if (!loop) loop = false
|
|
|
|
return loop
|
|
|
|
}
|
|
|
|
while (next());
|
|
|
|
}
|
|
|
|
|
2020-03-23 15:45:51 +01:00
|
|
|
const NODE_MODULES_REGEX = /node_modules/
|
|
|
|
|
|
|
|
type Paths = { [match: string]: string[] }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles tsconfig.json or jsconfig.js "paths" option for webpack
|
|
|
|
* Largely based on how the TypeScript compiler handles it:
|
|
|
|
* https://github.com/microsoft/TypeScript/blob/1a9c8197fffe3dace5f8dca6633d450a88cba66d/src/compiler/moduleNameResolver.ts#L1362
|
|
|
|
*/
|
2022-08-16 11:55:37 +02:00
|
|
|
export class JsConfigPathsPlugin implements webpack.ResolvePluginInstance {
|
2020-03-23 15:45:51 +01:00
|
|
|
paths: Paths
|
|
|
|
resolvedBaseUrl: string
|
2022-08-23 20:16:47 +02:00
|
|
|
jsConfigPlugin: true
|
|
|
|
|
2020-03-23 15:45:51 +01:00
|
|
|
constructor(paths: Paths, resolvedBaseUrl: string) {
|
|
|
|
this.paths = paths
|
|
|
|
this.resolvedBaseUrl = resolvedBaseUrl
|
2022-08-23 20:16:47 +02:00
|
|
|
this.jsConfigPlugin = true
|
2020-04-29 17:04:42 +02:00
|
|
|
log('tsconfig.json or jsconfig.json paths: %O', paths)
|
|
|
|
log('resolved baseUrl: %s', resolvedBaseUrl)
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
apply(resolver: any) {
|
|
|
|
const target = resolver.ensureHook('resolve')
|
|
|
|
resolver
|
|
|
|
.getHook('described-resolve')
|
2021-10-13 12:17:54 +02:00
|
|
|
.tapAsync(
|
2020-03-23 15:45:51 +01:00
|
|
|
'JsConfigPathsPlugin',
|
2021-10-13 12:17:54 +02:00
|
|
|
(
|
|
|
|
request: any,
|
|
|
|
resolveContext: any,
|
|
|
|
callback: (err?: any, result?: any) => void
|
|
|
|
) => {
|
2022-08-23 20:16:47 +02:00
|
|
|
const paths = this.paths
|
|
|
|
const pathsKeys = Object.keys(paths)
|
|
|
|
|
|
|
|
// If no aliases are added bail out
|
|
|
|
if (pathsKeys.length === 0) {
|
|
|
|
log('paths are empty, bailing out')
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
|
2020-04-29 17:04:42 +02:00
|
|
|
const moduleName = request.request
|
|
|
|
|
2020-03-23 15:45:51 +01:00
|
|
|
// Exclude node_modules from paths support (speeds up resolving)
|
|
|
|
if (request.path.match(NODE_MODULES_REGEX)) {
|
2020-04-29 17:04:42 +02:00
|
|
|
log('skipping request as it is inside node_modules %s', moduleName)
|
2021-10-13 12:17:54 +02:00
|
|
|
return callback()
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
|
2020-04-29 23:56:18 +02:00
|
|
|
if (
|
|
|
|
path.posix.isAbsolute(moduleName) ||
|
|
|
|
(process.platform === 'win32' && path.win32.isAbsolute(moduleName))
|
|
|
|
) {
|
2020-04-29 17:04:42 +02:00
|
|
|
log('skipping request as it is an absolute path %s', moduleName)
|
2021-10-13 12:17:54 +02:00
|
|
|
return callback()
|
2020-04-29 17:04:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (pathIsRelative(moduleName)) {
|
|
|
|
log('skipping request as it is a relative path %s', moduleName)
|
2021-10-13 12:17:54 +02:00
|
|
|
return callback()
|
2020-04-29 17:04:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// log('starting to resolve request %s', moduleName)
|
2020-03-23 15:45:51 +01:00
|
|
|
|
|
|
|
// If the module name does not match any of the patterns in `paths` we hand off resolving to webpack
|
|
|
|
const matchedPattern = matchPatternOrExact(pathsKeys, moduleName)
|
|
|
|
if (!matchedPattern) {
|
2020-04-29 17:04:42 +02:00
|
|
|
log('moduleName did not match any paths pattern %s', moduleName)
|
2021-10-13 12:17:54 +02:00
|
|
|
return callback()
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const matchedStar = isString(matchedPattern)
|
|
|
|
? undefined
|
|
|
|
: matchedText(matchedPattern, moduleName)
|
|
|
|
const matchedPatternText = isString(matchedPattern)
|
|
|
|
? matchedPattern
|
|
|
|
: patternText(matchedPattern)
|
|
|
|
|
|
|
|
let triedPaths = []
|
|
|
|
|
2021-10-13 12:17:54 +02:00
|
|
|
forEachBail(
|
|
|
|
paths[matchedPatternText],
|
|
|
|
(subst, pathCallback) => {
|
|
|
|
const curPath = matchedStar
|
|
|
|
? subst.replace('*', matchedStar)
|
|
|
|
: subst
|
|
|
|
// Ensure .d.ts is not matched
|
|
|
|
if (curPath.endsWith('.d.ts')) {
|
|
|
|
// try next path candidate
|
|
|
|
return pathCallback()
|
|
|
|
}
|
2022-08-23 20:16:47 +02:00
|
|
|
const candidate = path.join(this.resolvedBaseUrl, curPath)
|
2020-03-23 15:45:51 +01:00
|
|
|
const obj = Object.assign({}, request, {
|
|
|
|
request: candidate,
|
|
|
|
})
|
|
|
|
resolver.doResolve(
|
|
|
|
target,
|
|
|
|
obj,
|
|
|
|
`Aliased with tsconfig.json or jsconfig.json ${matchedPatternText} to ${candidate}`,
|
|
|
|
resolveContext,
|
2021-10-13 12:17:54 +02:00
|
|
|
(resolverErr: any, resolverResult: any) => {
|
|
|
|
if (resolverErr || resolverResult === undefined) {
|
|
|
|
triedPaths.push(candidate)
|
|
|
|
// try next path candidate
|
|
|
|
return pathCallback()
|
|
|
|
}
|
|
|
|
return pathCallback(resolverErr, resolverResult)
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
)
|
2021-10-13 12:17:54 +02:00
|
|
|
},
|
|
|
|
callback
|
|
|
|
)
|
2020-03-23 15:45:51 +01:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|