๐Ÿ“ฆ mui / mui-x

๐Ÿ“„ vite-plugin-filter-replace.mts ยท 90 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90import { Plugin } from 'vite';
import path from 'path';

interface RedirectRule {
  /**
   * A regex to test importer path against.
   *
   * Eg: /\\/AdapterDateFnsV2\\// will match `/src/AdapterDateFnsV2/index.js`
   */
  test: RegExp;
  /**
   * The import path to match. Any import path that starts with this will be redirected.
   *
   * Eg: 'date-fns' will match `import { format } from 'date-fns'` and `import { format } from 'date-fns/addDays'`
   */
  from: string;
  /**
   * The import path to redirect to. This will be replaced in place of `from`.
   *
   * Eg: 'date-fns-v2' will redirect `import { format } from 'date-fns'` to `import { format } from 'date-fns-v2'`
   */
  to: string;
  /**
   * An array of import paths to include. Use this to force the inclusion of certain dependencies.
   * This is useful when you want to include a dependency that is not included by default.
   *
   * Eg: ['date-fns-v2/**\/*.js']
   */
  include?: string[];
}

const cleanDepName = (name: string) => {
  // If the name starts with '@', we need to split it into scope and lib
  // e.g. `@mui/material/Button` -> `@mui/material`
  if (name.startsWith('@')) {
    const [scope, lib] = name.split('/');
    return `${scope}/${lib}`;
  }
  // If the name does not start with '@', we only care about the first part
  // e.g. `material/Button` -> `material`
  return name.split('/')[0];
};

export function redirectImports(rules: RedirectRule[]): Plugin {
  return {
    name: 'vite-plugin-redirect-imports',
    enforce: 'pre',

    config(config) {
      config.optimizeDeps ??= {};
      config.optimizeDeps.include ??= [];

      const depsToInclude = new Set<string>([
        ...rules.flatMap((rule) => rule.include ?? []),
        ...rules.flatMap((rule) => cleanDepName(rule.to)),
      ]);

      // Ignore already-included deps
      config.optimizeDeps.include.forEach((dep) => depsToInclude.delete(dep));
      config.optimizeDeps.include.push(...depsToInclude);
    },

    async resolveId(source, importer) {
      if (!importer) {
        return null;
      }

      const normalizedImporter = importer.split(path.sep).join('/');

      for (const rule of rules) {
        if (!rule.test.test(normalizedImporter)) {
          continue;
        }

        // Match `from` or `from/...`
        const match = source === rule.from || source.startsWith(`${rule.from}/`);
        if (!match) {
          continue;
        }

        const newSource = rule.to + source.slice(rule.from.length);

        return this.resolve(newSource, importer, { skipSelf: true });
      }

      return null;
    },
  };
}