1// From TypeScript: https://github.com/microsoft/TypeScript/blob/5b1897969769449217237aecbe364f823096c63e/src/compiler/core.ts
2// License: https://github.com/microsoft/TypeScript/blob/214df64/LICENSE.txt
3
4export interface Pattern {
5  prefix: string;
6  suffix: string;
7}
8
9const asterisk = 0x2a;
10
11function hasZeroOrOneAsteriskCharacter(str: string): boolean {
12  let seenAsterisk = false;
13  for (let i = 0; i < str.length; i++) {
14    if (str.charCodeAt(i) === asterisk) {
15      if (!seenAsterisk) {
16        seenAsterisk = true;
17      } else {
18        return false;
19      }
20    }
21  }
22  return true;
23}
24
25function tryParsePattern(pattern: string): Pattern | undefined {
26  // This should be verified outside of here and a proper error thrown.
27  const indexOfStar = pattern.indexOf('*');
28  return indexOfStar === -1
29    ? undefined
30    : {
31        prefix: pattern.slice(0, indexOfStar),
32        suffix: pattern.slice(indexOfStar + 1),
33      };
34}
35
36function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) {
37  return (
38    candidate.length >= prefix.length + suffix.length &&
39    candidate.startsWith(prefix) &&
40    candidate.endsWith(suffix)
41  );
42}
43
44/**
45 * Return the object corresponding to the best pattern to match `candidate`.
46 *
47 * @internal
48 */
49function findBestPatternMatch<T>(
50  values: readonly T[],
51  getPattern: (value: T) => Pattern,
52  candidate: string
53): T | undefined {
54  let matchedValue: T | undefined;
55  // use length of prefix as betterness criteria
56  let longestMatchPrefixLength = -1;
57
58  for (const v of values) {
59    const pattern = getPattern(v);
60    if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) {
61      longestMatchPrefixLength = pattern.prefix.length;
62      matchedValue = v;
63    }
64  }
65
66  return matchedValue;
67}
68
69/**
70 * patternStrings contains both pattern strings (containing "*") and regular strings.
71 * Return an exact match if possible, or a pattern match, or undefined.
72 * (These are verified by verifyCompilerOptions to have 0 or 1 "*" characters.)
73 */
74function matchPatternOrExact(
75  patternStrings: readonly string[],
76  candidate: string
77): string | Pattern | undefined {
78  const patterns: Pattern[] = [];
79  for (const patternString of patternStrings) {
80    if (!hasZeroOrOneAsteriskCharacter(patternString)) continue;
81    const pattern = tryParsePattern(patternString);
82    if (pattern) {
83      patterns.push(pattern);
84    } else if (patternString === candidate) {
85      // pattern was matched as is - no need to search further
86      return patternString;
87    }
88  }
89
90  return findBestPatternMatch(patterns, (_) => _, candidate);
91}
92
93/**
94 * Given that candidate matches pattern, returns the text matching the '*'.
95 * E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar"
96 */
97function matchedText(pattern: Pattern, candidate: string): string {
98  return candidate.substring(pattern.prefix.length, candidate.length - pattern.suffix.length);
99}
100
101function getStar(matchedPattern: string | Pattern, moduleName: string) {
102  return typeof matchedPattern === 'string' ? undefined : matchedText(matchedPattern, moduleName);
103}
104
105export function matchTsConfigPathAlias(pathsKeys: string[], moduleName: string) {
106  // If the module name does not match any of the patterns in `paths` we hand off resolving to webpack
107  const matchedPattern = matchPatternOrExact(pathsKeys, moduleName);
108  if (!matchedPattern) {
109    return null;
110  }
111
112  return {
113    star: getStar(matchedPattern, moduleName),
114    text:
115      typeof matchedPattern === 'string'
116        ? matchedPattern
117        : `${matchedPattern.prefix}*${matchedPattern.suffix}`,
118  };
119}
120