1+ import fs from 'node:fs' ;
12import path from 'node:path' ;
23import type { CompilerOptions } from 'typescript' ;
34import { normalizePath , type ResolvedConfig , type Plugin as VitePlugin } from 'vite' ;
5+
46import type { AstroSettings } from '../types/astro.js' ;
57
68type Alias = {
@@ -65,6 +67,51 @@ const getConfigAlias = (settings: AstroSettings): Alias[] | null => {
6567 return aliases ;
6668} ;
6769
70+ /** Generate vite.resolve.alias entries from tsconfig paths */
71+ const getViteResolveAlias = ( settings : AstroSettings ) => {
72+ const { tsConfig, tsConfigPath } = settings ;
73+ if ( ! tsConfig || ! tsConfigPath || ! tsConfig . compilerOptions ) return [ ] ;
74+
75+ const { baseUrl, paths } = tsConfig . compilerOptions as CompilerOptions ;
76+ const effectiveBaseUrl = baseUrl ?? ( paths ? '.' : undefined ) ;
77+ if ( ! effectiveBaseUrl ) return [ ] ;
78+
79+ const resolvedBaseUrl = path . resolve ( path . dirname ( tsConfigPath ) , effectiveBaseUrl ) ;
80+ const aliases : Array < { find : string | RegExp ; replacement : string ; customResolver ?: any } > = [ ] ;
81+
82+ // Build aliases with custom resolver that tries multiple paths
83+ if ( paths ) {
84+ for ( const [ aliasPattern , values ] of Object . entries ( paths ) ) {
85+ const resolvedValues = values . map ( ( v ) => path . resolve ( resolvedBaseUrl , v ) ) ;
86+
87+ const customResolver = ( id : string ) => {
88+ // Try each path in order
89+ // id is already the wildcard part (e.g., 'extra.css' for '@styles/*')
90+ // resolvedValues still have the * in them, so replace * with id
91+ for ( const resolvedValue of resolvedValues ) {
92+ const resolved = resolvedValue . replace ( '*' , id ) ;
93+ if ( fs . existsSync ( resolved ) ) {
94+ return resolved ;
95+ }
96+ }
97+ return null ;
98+ } ;
99+
100+ aliases . push ( {
101+ // Build regex from alias pattern (e.g., '@styles/*' -> /^@styles\/(.+)$/)
102+ // First, escape special regex chars. Then replace * with a capture group (.+)
103+ find : new RegExp (
104+ `^${ aliasPattern . replace ( / [ \\ ^ $ + ? . ( ) | [ \] { } ] / g, '\\$&' ) . replace ( / \* / g, '(.+)' ) } $` ,
105+ ) ,
106+ replacement : aliasPattern . includes ( '*' ) ? '$1' : aliasPattern ,
107+ customResolver,
108+ } ) ;
109+ }
110+ }
111+
112+ return aliases ;
113+ } ;
114+
68115/** Returns a Vite plugin used to alias paths from tsconfig.json and jsconfig.json. */
69116export default function configAliasVitePlugin ( {
70117 settings,
@@ -78,6 +125,14 @@ export default function configAliasVitePlugin({
78125 name : 'astro:tsconfig-alias' ,
79126 // use post to only resolve ids that all other plugins before it can't
80127 enforce : 'post' ,
128+ config ( ) {
129+ // Return vite.resolve.alias config with custom resolvers
130+ return {
131+ resolve : {
132+ alias : getViteResolveAlias ( settings ) ,
133+ } ,
134+ } ;
135+ } ,
81136 configResolved ( config ) {
82137 patchCreateResolver ( config , plugin ) ;
83138 } ,
@@ -109,11 +164,12 @@ export default function configAliasVitePlugin({
109164
110165/**
111166 * Vite's `createResolver` is used to resolve various things, including CSS `@import`.
112- * However, there's no way to extend this resolver, besides patching it. This function
113- * patches and adds a Vite plugin whose `resolveId` will be used to resolve before the
114- * internal plugins in ` createResolver` .
167+ * We use vite.resolve.alias with custom resolvers to handle tsconfig paths in most cases,
168+ * but for CSS imports, we still need to patch createResolver as vite. resolve.alias
169+ * doesn't apply there. This function patches createResolver to inject our custom resolver .
115170 *
116- * Vite may simplify this soon: https://github.com/vitejs/vite/pull/10555
171+ * TODO: Remove this function once all tests pass with only the vite.resolve.alias approach,
172+ * which means CSS @import resolution will work without patching createResolver.
117173 */
118174function patchCreateResolver ( config : ResolvedConfig , postPlugin : VitePlugin ) {
119175 const _createResolver = config . createResolver ;
0 commit comments