Skip to content

Commit 2d9963f

Browse files
committed
Add support for TypeScript config paths
1 parent cab54d5 commit 2d9963f

File tree

15 files changed

+469
-1
lines changed

15 files changed

+469
-1
lines changed

packages/tailwindcss-language-server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
"rimraf": "3.0.2",
8282
"stack-trace": "0.0.10",
8383
"tailwindcss": "3.4.4",
84+
"tsconfck": "^3.1.4",
85+
"tsconfig-paths": "^4.2.0",
8486
"typescript": "5.3.3",
8587
"vite-tsconfig-paths": "^4.3.1",
8688
"vitest": "^1.4.0",

packages/tailwindcss-language-server/src/project-locator.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function testFixture(fixture: string, details: any[]) {
1818
let fixturePath = `${fixtures}/${fixture}`
1919

2020
test.concurrent(fixture, async ({ expect }) => {
21-
let resolver = await createResolver({ root: fixturePath })
21+
let resolver = await createResolver({ root: fixturePath, tsconfig: true })
2222
let locator = new ProjectLocator(fixturePath, settings, resolver)
2323
let projects = await locator.search()
2424

@@ -205,3 +205,17 @@ testFixture('v4/missing-files', [
205205
content: ['{URL}/package.json'],
206206
},
207207
])
208+
209+
testFixture('v4/path-mappings', [
210+
//
211+
{
212+
config: 'app.css',
213+
content: [
214+
'{URL}/package.json',
215+
'{URL}/src/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}',
216+
'{URL}/src/a/my-config.ts',
217+
'{URL}/src/a/my-plugin.ts',
218+
'{URL}/tsconfig.json',
219+
],
220+
},
221+
])

packages/tailwindcss-language-server/src/projects.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export async function createProjectService(
226226
async readDirectory(document, directory) {
227227
try {
228228
let baseDir = path.dirname(getFileFsPath(document.uri))
229+
directory = await resolver.substituteId(`${directory}/`, baseDir)
229230
directory = path.resolve(baseDir, directory)
230231

231232
let dirents = await fs.promises.readdir(directory, { withFileTypes: true })
@@ -1128,6 +1129,8 @@ export async function createProjectService(
11281129
let baseDir = path.dirname(documentPath)
11291130

11301131
async function resolveTarget(linkPath: string) {
1132+
linkPath = (await resolver.substituteId(linkPath, baseDir)) ?? linkPath
1133+
11311134
return URI.file(path.resolve(baseDir, linkPath)).toString()
11321135
}
11331136

packages/tailwindcss-language-server/src/resolver/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
FileSystem,
88
} from 'enhanced-resolve'
99
import { loadPnPApi, type PnpApi } from './pnp'
10+
import { loadTsConfig, type TSConfigApi } from './tsconfig'
1011

1112
export interface ResolverOptions {
1213
/**
@@ -23,6 +24,15 @@ export interface ResolverOptions {
2324
*/
2425
pnp?: boolean | PnpApi
2526

27+
/**
28+
* Whether or not the resolver should load tsconfig path mappings.
29+
*
30+
* If `true`, the resolver will look for all `tsconfig` files in the project
31+
* and use them to resolve module paths where possible. However, if an API is
32+
* provided, the resolver will use that API to resolve module paths.
33+
*/
34+
tsconfig?: boolean | TSConfigApi
35+
2636
/**
2737
* A filesystem to use for resolution. If not provided, the resolver will
2838
* create one and use it internally for itself and any child resolvers that
@@ -61,6 +71,23 @@ export interface Resolver {
6171
*/
6272
resolveCssId(id: string, base: string): Promise<string>
6373

74+
/**
75+
* Resolves a module to a possible file or directory path.
76+
*
77+
* This provides reasonable results when TypeScript config files are in use.
78+
* This file may not exist but is the likely path that would be used to load
79+
* the module if it were to exist.
80+
*
81+
* @param id The module, file, or directory to resolve
82+
* @param base The base directory to resolve the module from
83+
*/
84+
substituteId(id: string, base: string): Promise<string>
85+
86+
/**
87+
* Return a list of path resolution aliases for the given base directory
88+
*/
89+
aliases(base: string): Promise<Record<string, string[]>>
90+
6491
/**
6592
* Create a child resolver with the given options.
6693
*
@@ -83,6 +110,22 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
83110
pnpApi = await loadPnPApi(opts.root)
84111
}
85112

113+
let tsconfig: TSConfigApi | null = null
114+
115+
// Load TSConfig path mappings
116+
if (typeof opts.tsconfig === 'object') {
117+
tsconfig = opts.tsconfig
118+
} else if (opts.tsconfig) {
119+
try {
120+
tsconfig = await loadTsConfig(opts.root)
121+
} catch (err) {
122+
// We don't want to hard crash in case of an error handling tsconfigs
123+
// It does affect what projects we can resolve or how we load files
124+
// but the LSP shouldn't become unusable because of it.
125+
console.error('Failed to load tsconfig', err)
126+
}
127+
}
128+
86129
let esmResolver = ResolverFactory.createResolver({
87130
fileSystem,
88131
extensions: ['.mjs', '.js'],
@@ -128,6 +171,11 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
128171
if (base.startsWith('//')) base = `\\\\${base.slice(2)}`
129172
}
130173

174+
if (tsconfig) {
175+
let match = await tsconfig.resolveId(id, base)
176+
if (match) id = match
177+
}
178+
131179
return new Promise((resolve, reject) => {
132180
resolver.resolve({}, base, id, {}, (err, res) => {
133181
if (err) {
@@ -151,14 +199,29 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
151199
return (await resolveId(cssResolver, id, base)) || id
152200
}
153201

202+
// Takes a path which may or may not be complete and returns the aliased path
203+
// if possible
204+
async function substituteId(id: string, base: string): Promise<string> {
205+
return (await tsconfig?.substituteId(id, base)) ?? id
206+
}
207+
154208
async function setupPnP() {
155209
pnpApi?.setup()
156210
}
157211

212+
async function aliases(base: string) {
213+
if (!tsconfig) return {}
214+
215+
return await tsconfig.paths(base)
216+
}
217+
158218
return {
159219
setupPnP,
160220
resolveJsId,
161221
resolveCssId,
222+
substituteId,
223+
224+
aliases,
162225

163226
child(childOpts: Partial<ResolverOptions>) {
164227
return createResolver({
@@ -167,6 +230,7 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
167230

168231
// Inherit defaults from parent
169232
pnp: childOpts.pnp ?? pnpApi,
233+
tsconfig: childOpts.tsconfig ?? tsconfig,
170234
fileSystem: childOpts.fileSystem ?? fileSystem,
171235
})
172236
},

0 commit comments

Comments
 (0)