Skip to content

Commit ad5a5ce

Browse files
authored
chore(vite): auto-generate resolve aliases from package.json exports (#8489)
1 parent cd203e7 commit ad5a5ce

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

config/vite-monorepo-aliases.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { readFileSync, readdirSync } from 'node:fs';
2+
import { join, resolve } from 'node:path';
3+
import type { Alias } from 'vite';
4+
5+
/**
6+
* Reads the `exports` field from each internal package's `package.json` and
7+
* generates Vite `resolve.alias` entries that map subpath imports to source
8+
* files (`src/`) during dev, so `dist/` doesn't need to exist.
9+
*
10+
* Uses Vite's built-in alias plugin (Rolldown's `viteAliasPlugin`) which
11+
* feeds replacements back into Vite's resolver, so `.ts`/`.tsx` extension
12+
* resolution is handled automatically — no filesystem probing at runtime.
13+
*
14+
* Ordering: wildcard (regex) entries come first, then exact (string) entries
15+
* sorted by descending subpath length. This prevents Vite's alias plugin
16+
* from prefix-matching string entries before regex wildcards are checked.
17+
*/
18+
export function generateMonorepoAliases(packagesDir: string): Alias[] {
19+
const wildcardAliases: Alias[] = [];
20+
const exactAliases: Alias[] = [];
21+
const absPackagesDir = resolve(packagesDir);
22+
23+
const packageDirs = readdirSync(absPackagesDir, { withFileTypes: true })
24+
.filter((d) => d.isDirectory())
25+
.map((d) => join(absPackagesDir, d.name));
26+
27+
for (const pkgDir of packageDirs) {
28+
let pkgJson: Record<string, unknown>;
29+
try {
30+
pkgJson = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
31+
} catch {
32+
continue;
33+
}
34+
35+
const name = pkgJson.name as string | undefined;
36+
const exports = pkgJson.exports as Record<string, unknown> | undefined;
37+
if (!name || !exports) {
38+
continue;
39+
}
40+
41+
const exactEntries: Array<{ subpath: string; srcRelative: string }> = [];
42+
const wildcardEntries: Array<{ subpath: string; srcRelative: string }> = [];
43+
44+
for (const [subpath, target] of Object.entries(exports)) {
45+
const targetPath = typeof target === 'string' ? target : (target as Record<string, string>)?.default;
46+
if (!targetPath || typeof targetPath !== 'string') continue;
47+
if (!targetPath.startsWith('./dist/')) continue;
48+
49+
const srcRelative = targetPath.slice('./dist/'.length).replace(/\.js$/, '');
50+
51+
if (subpath.includes('*')) {
52+
wildcardEntries.push({ subpath, srcRelative });
53+
} else {
54+
exactEntries.push({ subpath, srcRelative });
55+
}
56+
}
57+
58+
// Sort exact entries by subpath length descending so more-specific aliases come first.
59+
exactEntries.sort((a, b) => b.subpath.length - a.subpath.length);
60+
61+
const srcDir = join(pkgDir, 'src');
62+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
63+
64+
// Wildcards first — they must be checked before string entries because
65+
// Vite's alias plugin uses prefix matching for strings, which would
66+
// incorrectly intercept imports like `pkg/dist/foo` via the `pkg` root alias.
67+
for (const { subpath, srcRelative } of wildcardEntries) {
68+
const subpathPattern = subpath
69+
.slice(2)
70+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
71+
.replace(/\\\*/, '(.+)');
72+
const find = new RegExp(`^${escapedName}/${subpathPattern}$`);
73+
const replacement = join(srcDir, srcRelative).replace(/\*/g, '$1');
74+
wildcardAliases.push({ find, replacement });
75+
}
76+
77+
for (const { subpath, srcRelative } of exactEntries) {
78+
const find = subpath === '.' ? name : name + '/' + subpath.slice(2);
79+
exactAliases.push({ find, replacement: join(srcDir, srcRelative) });
80+
}
81+
}
82+
83+
return [...wildcardAliases, ...exactAliases];
84+
}

vite.config.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
33
import { defineConfig } from 'vite';
44
import IstanbulPlugin from 'vite-plugin-istanbul';
55
import tsconfigPaths from 'vite-tsconfig-paths';
6+
import { generateMonorepoAliases } from './config/vite-monorepo-aliases.js';
67

78
// https://vitejs.dev/config/
89

@@ -12,13 +13,11 @@ export default defineConfig(() => {
1213
'process.env.STORYBOOK_ENV': `'${process.env.STORYBOOK_ENV}'`,
1314
},
1415
resolve: {
15-
alias: {
16-
'@sb': fileURLToPath(new URL('./.storybook', import.meta.url)),
17-
'@ui5/webcomponents-react-charts': fileURLToPath(new URL('./packages/charts/src/index.ts', import.meta.url)),
18-
'@ui5/webcomponents-react/dist': fileURLToPath(new URL('./packages/main/src/', import.meta.url)),
19-
'@ui5/webcomponents-react': fileURLToPath(new URL('./packages/main/src/index.ts', import.meta.url)),
20-
'@/': fileURLToPath(new URL('./', import.meta.url)),
21-
},
16+
alias: [
17+
{ find: '@sb', replacement: fileURLToPath(new URL('./.storybook', import.meta.url)) },
18+
...generateMonorepoAliases(fileURLToPath(new URL('./packages', import.meta.url))),
19+
{ find: '@/', replacement: fileURLToPath(new URL('./', import.meta.url)) },
20+
],
2221
},
2322
optimizeDeps: {
2423
esbuildOptions: {

0 commit comments

Comments
 (0)