Skip to content

Commit eabd07e

Browse files
Fix issue when require()-ing packages that resolve to paths containing # (#1235)
Fixes #1221 (again, for real this time, I hope)
1 parent 8301032 commit eabd07e

File tree

4 files changed

+119
-3
lines changed

4 files changed

+119
-3
lines changed

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ export interface TestUtils {
1111

1212
export interface Storage {
1313
/** A list of files and their content */
14-
[filePath: string]: string | Uint8Array
14+
[filePath: string]: string | Uint8Array | { [IS_A_SYMLINK]: true; filepath: string }
1515
}
1616

1717
export interface TestConfig<Extras extends {}> {
1818
name: string
1919
fs?: Storage
20+
debug?: boolean
2021
prepare?(utils: TestUtils): Promise<Extras>
2122
handle(utils: TestUtils & Extras): void | Promise<void>
2223

@@ -56,6 +57,8 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
5657

5758
if (path.sep === '\\') return
5859

60+
if (config.debug) return
61+
5962
// Remove the directory on *nix systems. Recursive removal on Windows will
6063
// randomly fail b/c its slow and buggy.
6164
await fs.rm(doneDir, { recursive: true })
@@ -66,6 +69,14 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
6669
}
6770
}
6871

72+
const IS_A_SYMLINK = Symbol('is-a-symlink')
73+
export const symlinkTo = function (filepath: string) {
74+
return {
75+
[IS_A_SYMLINK]: true as const,
76+
filepath,
77+
}
78+
}
79+
6980
async function prepareFileSystem(base: string, storage: Storage) {
7081
// Create a temporary directory to store the test files
7182
await fs.mkdir(base, { recursive: true })
@@ -74,6 +85,13 @@ async function prepareFileSystem(base: string, storage: Storage) {
7485
for (let [filepath, content] of Object.entries(storage)) {
7586
let fullPath = path.resolve(base, filepath)
7687
await fs.mkdir(path.dirname(fullPath), { recursive: true })
88+
89+
if (typeof content === 'object' && IS_A_SYMLINK in content) {
90+
let target = path.resolve(base, content.filepath)
91+
await fs.symlink(target, fullPath)
92+
continue
93+
}
94+
7795
await fs.writeFile(fullPath, content, { encoding: 'utf-8' })
7896
}
7997
}

packages/tailwindcss-language-server/src/util/resolveFrom.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,17 @@ export function resolveFrom(from?: string, id?: string): string {
5656

5757
let result = resolver.resolveSync({}, from, id)
5858
if (result === false) throw Error()
59+
60+
// The `enhanced-resolve` package supports resolving paths with fragment
61+
// identifiers. For example, it can resolve `foo/bar#baz` to `foo/bar.js`
62+
// However, it's also possible that a path contains a `#` character as part
63+
// of the path itself. For example, `foo#bar` might point to a file named
64+
// `foo#bar.js`. The resolver distinguishes between these two cases by
65+
// escaping the `#` character with a NUL byte when it's part of the path.
66+
//
67+
// Since the real path doesn't actually contain NUL bytes, we need to remove
68+
// them to get the correct path otherwise readFileSync will throw an error.
69+
result = result.replace(/\0(.)/g, '$1')
70+
5971
return result
6072
}

packages/tailwindcss-language-server/tests/env/v4.test.js

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22

33
import { expect } from 'vitest'
4-
import { css, defineTest, html, js, json } from '../../src/testing'
4+
import { css, defineTest, html, js, json, symlinkTo } from '../../src/testing'
55
import dedent from 'dedent'
66
import { createClient } from '../utils/client'
77

@@ -666,3 +666,89 @@ defineTest({
666666
})
667667
},
668668
})
669+
670+
defineTest({
671+
// This test *always* passes inside Vitest because our custom version of
672+
// `Module._resolveFilename` is not called. Our custom implementation is
673+
// using enhanced-resolve under the hood which is affected by the `#`
674+
// character issue being considered a fragment identifier.
675+
//
676+
// This most commonly happens when dealing with PNPM packages that point
677+
// to a specific commit hash of a git repository.
678+
//
679+
// To simulate this, we need to:
680+
// - Add a local package to package.json
681+
// - Symlink that local package to a directory with `#` in the name
682+
// - Then run the test in a separate process (`spawn` mode)
683+
//
684+
// We can't use `file:./a#b` because NPM considers `#` to be a fragment
685+
// identifier and will not resolve the path the way we need it to.
686+
name: 'v3: require() works when path is resolved to contain a `#`',
687+
fs: {
688+
'package.json': json`
689+
{
690+
"dependencies": {
691+
"tailwindcss": "3.4.17",
692+
"some-pkg": "file:./packages/some-pkg"
693+
}
694+
}
695+
`,
696+
'tailwind.config.js': js`
697+
module.exports = {
698+
presets: [require('some-pkg/config/tailwind.config.js').default]
699+
}
700+
`,
701+
'packages/some-pkg': symlinkTo('packages/some-pkg#c3f1e'),
702+
'packages/some-pkg#c3f1e/package.json': json`
703+
{
704+
"name": "some-pkg",
705+
"version": "1.0.0",
706+
"main": "index.js"
707+
}
708+
`,
709+
'packages/some-pkg#c3f1e/config/tailwind.config.js': js`
710+
export default {
711+
plugins: [
712+
function ({ addUtilities }) {
713+
addUtilities({
714+
'.example': {
715+
color: 'red',
716+
},
717+
})
718+
}
719+
]
720+
}
721+
`,
722+
},
723+
prepare: async ({ root }) => ({
724+
client: await createClient({
725+
root,
726+
mode: 'spawn',
727+
}),
728+
}),
729+
handle: async ({ client }) => {
730+
let document = await client.open({
731+
lang: 'html',
732+
text: '<div class="example">',
733+
})
734+
735+
// <div class="example">
736+
// ^
737+
let hover = await document.hover({ line: 0, character: 13 })
738+
739+
expect(hover).toEqual({
740+
contents: {
741+
language: 'css',
742+
value: dedent`
743+
.example {
744+
color: red;
745+
}
746+
`,
747+
},
748+
range: {
749+
start: { line: 0, character: 12 },
750+
end: { line: 0, character: 19 },
751+
},
752+
})
753+
},
754+
})

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Prerelease
44

5-
- Nothing yet!
5+
- Don't throw when requiring() packages that resolve to a path containing a `#` character ([#1235](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1235))
66

77
# 0.14.7
88

0 commit comments

Comments
 (0)