| | |
| | |
| | |
| | |
| | |
| | import path from 'path' |
| | import type { webpack } from 'next/dist/compiled/webpack/webpack' |
| | import { debug } from 'next/dist/compiled/debug' |
| | import type { ResolvedBaseUrl } from '../../load-jsconfig' |
| |
|
| | const log = debug('next:jsconfig-paths-plugin') |
| |
|
| | 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 { |
| | |
| | return false |
| | } |
| | } |
| | } |
| | return true |
| | } |
| |
|
| | |
| | |
| | |
| | export function pathIsRelative(testPath: string): boolean { |
| | return /^\.\.?($|[\\/])/.test(testPath) |
| | } |
| |
|
| | export function tryParsePattern(pattern: string): Pattern | undefined { |
| | |
| | const indexOfStar = pattern.indexOf('*') |
| | return indexOfStar === -1 |
| | ? undefined |
| | : { |
| | prefix: pattern.slice(0, indexOfStar), |
| | suffix: pattern.slice(indexOfStar + 1), |
| | } |
| | } |
| |
|
| | function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { |
| | return ( |
| | candidate.length >= prefix.length + suffix.length && |
| | candidate.startsWith(prefix) && |
| | candidate.endsWith(suffix) |
| | ) |
| | } |
| |
|
| | |
| | export function findBestPatternMatch<T>( |
| | values: readonly T[], |
| | getPattern: (value: T) => Pattern, |
| | candidate: string |
| | ): T | undefined { |
| | let matchedValue: T | undefined |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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) { |
| | |
| | return patternString |
| | } |
| | } |
| |
|
| | return findBestPatternMatch(patterns, (_) => _, candidate) |
| | } |
| |
|
| | |
| | |
| | |
| | export function isString(text: unknown): text is string { |
| | return typeof text === 'string' |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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}` |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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()); |
| | } |
| |
|
| | const NODE_MODULES_REGEX = /node_modules/ |
| |
|
| | type Paths = { [match: string]: string[] } |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | type NonFunction<T> = T extends Function ? never : T |
| |
|
| | |
| | type ResolvePluginPlugin = NonFunction<webpack.ResolvePluginInstance> |
| | export class JsConfigPathsPlugin implements ResolvePluginPlugin { |
| | paths: Paths |
| | resolvedBaseUrl: ResolvedBaseUrl |
| | jsConfigPlugin: true |
| |
|
| | constructor(paths: Paths, resolvedBaseUrl: ResolvedBaseUrl) { |
| | this.paths = paths |
| | this.resolvedBaseUrl = resolvedBaseUrl |
| | this.jsConfigPlugin = true |
| | log('tsconfig.json or jsconfig.json paths: %O', paths) |
| | log('resolved baseUrl: %s', resolvedBaseUrl) |
| | } |
| | apply(resolver: webpack.Resolver) { |
| | const target = resolver.ensureHook('resolve') |
| | resolver |
| | .getHook('described-resolve') |
| | .tapAsync( |
| | 'JsConfigPathsPlugin', |
| | ( |
| | request: any, |
| | resolveContext: any, |
| | callback: (err?: any, result?: any) => void |
| | ) => { |
| | const resolvedBaseUrl = this.resolvedBaseUrl |
| | if (resolvedBaseUrl === undefined) { |
| | return callback() |
| | } |
| | const paths = this.paths |
| | const pathsKeys = Object.keys(paths) |
| |
|
| | |
| | if (pathsKeys.length === 0) { |
| | log('paths are empty, bailing out') |
| | return callback() |
| | } |
| |
|
| | const moduleName = request.request |
| |
|
| | |
| | if (request.path.match(NODE_MODULES_REGEX)) { |
| | log('skipping request as it is inside node_modules %s', moduleName) |
| | return callback() |
| | } |
| |
|
| | if ( |
| | path.posix.isAbsolute(moduleName) || |
| | (process.platform === 'win32' && path.win32.isAbsolute(moduleName)) |
| | ) { |
| | log('skipping request as it is an absolute path %s', moduleName) |
| | return callback() |
| | } |
| |
|
| | if (pathIsRelative(moduleName)) { |
| | log('skipping request as it is a relative path %s', moduleName) |
| | return callback() |
| | } |
| |
|
| | |
| |
|
| | |
| | const matchedPattern = matchPatternOrExact(pathsKeys, moduleName) |
| | if (!matchedPattern) { |
| | log('moduleName did not match any paths pattern %s', moduleName) |
| | return callback() |
| | } |
| |
|
| | const matchedStar = isString(matchedPattern) |
| | ? undefined |
| | : matchedText(matchedPattern, moduleName) |
| | const matchedPatternText = isString(matchedPattern) |
| | ? matchedPattern |
| | : patternText(matchedPattern) |
| |
|
| | let triedPaths = [] |
| |
|
| | forEachBail( |
| | paths[matchedPatternText], |
| | (subst, pathCallback) => { |
| | const curPath = matchedStar |
| | ? subst.replace('*', matchedStar) |
| | : subst |
| | |
| | if (curPath.endsWith('.d.ts')) { |
| | |
| | return pathCallback() |
| | } |
| | const candidate = path.join(resolvedBaseUrl.baseUrl, curPath) |
| | const obj = Object.assign({}, request, { |
| | request: candidate, |
| | }) |
| | resolver.doResolve( |
| | target, |
| | obj, |
| | `Aliased with tsconfig.json or jsconfig.json ${matchedPatternText} to ${candidate}`, |
| | resolveContext, |
| | (resolverErr: any, resolverResult: any) => { |
| | if (resolverErr || resolverResult === undefined) { |
| | triedPaths.push(candidate) |
| | |
| | return pathCallback() |
| | } |
| | return pathCallback(resolverErr, resolverResult) |
| | } |
| | ) |
| | }, |
| | callback |
| | ) |
| | } |
| | ) |
| | } |
| | } |
| |
|