Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/tailwindcss-language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
},
"homepage": "https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme",
"scripts": {
"build": "pnpm run clean && pnpm run _esbuild && pnpm run _esbuild:css",
"build": "pnpm run clean && pnpm run _esbuild && pnpm run _esbuild:oxide && pnpm run _esbuild:css",
"_esbuild": "node ../../esbuild.mjs src/server.ts --outfile=bin/tailwindcss-language-server --minify",
"_esbuild:oxide": "node ../../esbuild.mjs src/oxide-helper.ts --outfile=bin/oxide-helper.js --minify",
"_esbuild:css": "node ../../esbuild.mjs src/language/css.ts --outfile=bin/css-language-server --minify",
"clean": "rimraf bin",
"prepublishOnly": "pnpm run build",
Expand Down
14 changes: 14 additions & 0 deletions packages/tailwindcss-language-server/src/oxide-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node

import * as rpc from 'vscode-jsonrpc/node'
import { scan, type ScanOptions, type ScanResult } from './oxide'

let connection = rpc.createMessageConnection(
new rpc.IPCMessageReader(process),
new rpc.IPCMessageWriter(process),
)

let scanRequest = new rpc.RequestType<ScanOptions, ScanResult, void>('scan')
connection.onRequest<ScanOptions, ScanResult, void>(scanRequest, (options) => scan(options))

connection.listen()
93 changes: 93 additions & 0 deletions packages/tailwindcss-language-server/src/oxide-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as rpc from 'vscode-jsonrpc/node'
import * as proc from 'node:child_process'
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import { type ScanOptions, type ScanResult } from './oxide'

/**
* This helper starts a session in which we can use Oxide in *another process*
* to communicate content scanning results.
*
* Thie exists for two reasons:
* - The Oxide API has changed over time so this function presents a unified
* interface that works with all versions of the Oxide API. The results may
* vary but the structure of the results will always be identical.
*
* - Requiring a native node module on Windows permanently keeps an open handle
* to the binary for the duration of the process. This prevents unlinking the
* file like happens when running `npm ci`. Running an ephemeral process lets
* us sidestep the problem as the process will only be running as needed.
*/
export class OxideSession {
helper: proc.ChildProcess | null = null
connection: rpc.MessageConnection | null = null

public async scan(options: ScanOptions): Promise<ScanResult> {
await this.startIfNeeded()

return await this.connection.sendRequest('scan', options)
}

async startIfNeeded(): Promise<void> {
if (this.connection) return

// TODO: Can we find a way to not require a build first?
// let module = path.resolve(path.dirname(__filename), './oxide-helper.ts')

let modulePaths = [
// Separate Language Server package
'../bin/oxide-helper.js',

// Bundled with the VSCode extension
'../dist/oxide-helper.js',
]

let module: string | null = null

for (let relativePath of modulePaths) {
let filepath = path.resolve(path.dirname(__filename), relativePath)

if (
await fs.access(filepath).then(
() => true,
() => false,
)
) {
module = filepath
break
}
}

if (!module) throw new Error('unable to load')

let helper = proc.fork(module)
let connection = rpc.createMessageConnection(
new rpc.IPCMessageReader(helper),
new rpc.IPCMessageWriter(helper),
)

helper.on('disconnect', () => {
connection.dispose()
this.connection = null
this.helper = null
})

helper.on('exit', () => {
connection.dispose()
this.connection = null
this.helper = null
})

connection.listen()

this.helper = helper
this.connection = connection
}

async stop() {
if (!this.helper) return

this.helper.disconnect()
this.helper.kill()
}
}
4 changes: 2 additions & 2 deletions packages/tailwindcss-language-server/src/oxide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ interface SourceEntry {
negated: boolean
}

interface ScanOptions {
export interface ScanOptions {
oxidePath: string
oxideVersion: string
basePath: string
sources: Array<SourceEntry>
}

interface ScanResult {
export interface ScanResult {
files: Array<string>
globs: Array<GlobEntry>
}
Expand Down
53 changes: 45 additions & 8 deletions packages/tailwindcss-language-server/src/project-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils'
import postcss from 'postcss'
import * as oxide from './oxide'
import { analyzeStylesheet, TailwindStylesheet } from './version-guesser'
import { OxideSession } from './oxide-session'

export interface ProjectConfig {
/** The folder that contains the project */
Expand Down Expand Up @@ -60,7 +61,10 @@ export class ProjectLocator {
let configs = await this.findConfigs()

// Create a project for each of the config files
let results = await Promise.allSettled(configs.map((config) => this.createProject(config)))
let session = new OxideSession()
let results = await Promise.allSettled(
configs.map((config) => this.createProject(config, session)),
)
let projects: ProjectConfig[] = []

for (let result of results) {
Expand All @@ -71,6 +75,8 @@ export class ProjectLocator {
}
}

console.log(projects[0])

if (projects.length === 1) {
projects[0].additionalSelectors.push({
pattern: normalizePath(path.join(this.base, '**')),
Expand Down Expand Up @@ -98,6 +104,8 @@ export class ProjectLocator {
}
}

await session.stop()

return projects
}

Expand Down Expand Up @@ -148,7 +156,10 @@ export class ProjectLocator {
}
}

private async createProject(config: ConfigEntry): Promise<ProjectConfig | null> {
private async createProject(
config: ConfigEntry,
session: OxideSession,
): Promise<ProjectConfig | null> {
let tailwind = await this.detectTailwindVersion(config)

let possibleVersions = config.entries.flatMap((entry) => entry.meta?.versions ?? [])
Expand Down Expand Up @@ -218,7 +229,12 @@ export class ProjectLocator {
// Look for the package root for the config
config.packageRoot = await getPackageRoot(path.dirname(config.path), this.base)

let selectors = await calculateDocumentSelectors(config, tailwind.features, this.resolver)
let selectors = await calculateDocumentSelectors(
config,
tailwind.features,
this.resolver,
session,
)

return {
config,
Expand Down Expand Up @@ -520,10 +536,11 @@ function contentSelectorsFromConfig(
entry: ConfigEntry,
features: Feature[],
resolver: Resolver,
session: OxideSession,
actualConfig?: any,
): AsyncIterable<DocumentSelector> {
if (entry.type === 'css') {
return contentSelectorsFromCssConfig(entry, resolver)
return contentSelectorsFromCssConfig(entry, resolver, session)
}

if (entry.type === 'js') {
Expand Down Expand Up @@ -582,6 +599,7 @@ async function* contentSelectorsFromJsConfig(
async function* contentSelectorsFromCssConfig(
entry: ConfigEntry,
resolver: Resolver,
session: OxideSession,
): AsyncIterable<DocumentSelector> {
let auto = false
for (let item of entry.content) {
Expand All @@ -606,6 +624,7 @@ async function* contentSelectorsFromCssConfig(
entry.path,
sources,
resolver,
session,
)) {
yield {
pattern,
Expand All @@ -621,14 +640,15 @@ async function* detectContentFiles(
inputFile: string,
sources: SourcePattern[],
resolver: Resolver,
session: OxideSession,
): AsyncIterable<string> {
try {
let oxidePath = await resolver.resolveJsId('@tailwindcss/oxide', base)
oxidePath = pathToFileURL(oxidePath).href
let oxidePackageJsonPath = await resolver.resolveJsId('@tailwindcss/oxide/package.json', base)
let oxidePackageJson = JSON.parse(await fs.readFile(oxidePackageJsonPath, 'utf8'))

let result = await oxide.scan({
let result = await session.scan({
oxidePath,
oxideVersion: oxidePackageJson.version,
basePath: base,
Expand All @@ -654,8 +674,8 @@ async function* detectContentFiles(
base = normalizeDriveLetter(base)
yield `${base}/${pattern}`
}
} catch {
//
} catch (err) {
console.log({ err })
}
}

Expand Down Expand Up @@ -812,8 +832,15 @@ export async function calculateDocumentSelectors(
config: ConfigEntry,
features: Feature[],
resolver: Resolver,
session?: OxideSession,
actualConfig?: any,
) {
let hasTemporarySession = false
if (!session) {
hasTemporarySession = true
session = new OxideSession()
}

let selectors: DocumentSelector[] = []

// selectors:
Expand All @@ -834,7 +861,13 @@ export async function calculateDocumentSelectors(
})

// - Content patterns from config
for await (let selector of contentSelectorsFromConfig(config, features, resolver, actualConfig)) {
for await (let selector of contentSelectorsFromConfig(
config,
features,
resolver,
session,
actualConfig,
)) {
selectors.push(selector)
}

Expand Down Expand Up @@ -876,5 +909,9 @@ export async function calculateDocumentSelectors(
return 0
})

if (hasTemporarySession) {
await session.stop()
}

return selectors
}
1 change: 1 addition & 0 deletions packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ export async function createProjectService(
projectConfig.config,
state.features,
resolver,
undefined,
originalConfig,
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@
}
},
"scripts": {
"_esbuild": "node ../../esbuild.mjs src/extension.ts src/server.ts src/cssServer.ts --outdir=dist",
"_esbuild": "node ../../esbuild.mjs src/extension.ts src/server.ts src/cssServer.ts src/oxide-helper.ts --outdir=dist",
"dev": "concurrently --raw --kill-others \"pnpm run watch\" \"pnpm run check --watch\"",
"watch": "pnpm run clean && pnpm run _esbuild --watch",
"build": "pnpm run check && pnpm run clean && pnpm run _esbuild --minify && move-file dist/server.js dist/tailwindServer.js && move-file dist/cssServer.js dist/tailwindModeServer.js",
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/src/oxide-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@tailwindcss/language-server/src/oxide-helper'
Loading