Skip to content

Commit 2deda99

Browse files
committed
add parcel watcher
1 parent 01278a9 commit 2deda99

File tree

9 files changed

+282
-30
lines changed

9 files changed

+282
-30
lines changed

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

+80-30
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
HoverRequest,
2626
DidChangeWatchedFilesNotification,
2727
FileChangeType,
28+
Disposable,
2829
} from 'vscode-languageserver/node'
2930
import { TextDocument } from 'vscode-languageserver-textdocument'
3031
import { URI } from 'vscode-uri'
@@ -73,6 +74,7 @@ import { debounce } from 'debounce'
7374
import { getModuleDependencies } from './util/getModuleDependencies'
7475
import assert from 'assert'
7576
// import postcssLoadConfig from 'postcss-load-config'
77+
import * as parcel from './watcher/index.js'
7678

7779
const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
7880
const TRIGGER_CHARACTERS = [
@@ -151,6 +153,7 @@ function first<T>(...options: Array<() => T>): T {
151153
interface ProjectService {
152154
state: State
153155
tryInit: () => Promise<void>
156+
dispose: () => void
154157
onUpdateSettings: (settings: any) => void
155158
onHover(params: TextDocumentPositionParams): Promise<Hover>
156159
onCompletion(params: CompletionParams): Promise<CompletionList>
@@ -167,6 +170,7 @@ async function createProjectService(
167170
params: InitializeParams,
168171
documentService: DocumentService
169172
): Promise<ProjectService> {
173+
const disposables: Disposable[] = []
170174
const state: State = {
171175
enabled: false,
172176
editor: {
@@ -208,7 +212,13 @@ async function createProjectService(
208212
const documentSettingsCache: Map<string, Settings> = new Map()
209213
let registrations: Promise<BulkUnregistration>
210214

211-
let watcher: FSWatcher
215+
let chokidarWatcher: FSWatcher
216+
let ignore = [
217+
'**/.git/objects/**',
218+
'**/.git/subtree-cache/**',
219+
'**/node_modules/**',
220+
'**/.hg/store/**',
221+
]
212222

213223
function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void {
214224
let needsInit = false
@@ -217,22 +227,30 @@ async function createProjectService(
217227
for (let change of changes) {
218228
let file = normalizePath(change.file)
219229

230+
for (let ignorePattern of ignore) {
231+
if (minimatch(file, ignorePattern)) {
232+
continue
233+
}
234+
}
235+
236+
let isConfigFile = minimatch(file, `**/${CONFIG_FILE_GLOB}`)
237+
let isPackageFile = minimatch(file, '**/package.json')
238+
let isDependency = state.dependencies && state.dependencies.includes(change.file)
239+
240+
if (!isConfigFile && !isPackageFile && !isDependency) continue
241+
220242
if (change.type === FileChangeType.Created) {
221243
needsInit = true
222244
break
223245
} else if (change.type === FileChangeType.Changed) {
224-
if (!state.enabled || minimatch(file, '**/package.json')) {
246+
if (!state.enabled || isPackageFile) {
225247
needsInit = true
226248
break
227249
} else {
228250
needsRebuild = true
229251
}
230252
} else if (change.type === FileChangeType.Deleted) {
231-
if (
232-
!state.enabled ||
233-
minimatch(file, '**/package.json') ||
234-
minimatch(file, `**/${CONFIG_FILE_GLOB}`)
235-
) {
253+
if (!state.enabled || isPackageFile || isConfigFile) {
236254
needsInit = true
237255
break
238256
} else {
@@ -261,34 +279,59 @@ async function createProjectService(
261279
connection.client.register(DidChangeWatchedFilesNotification.type, {
262280
watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: '**/package.json' }],
263281
})
264-
} else {
265-
watcher = chokidar.watch(
266-
[
267-
normalizePath(`${folder}/**/${CONFIG_FILE_GLOB}`),
268-
normalizePath(`${folder}/**/package.json`),
269-
],
282+
} else if (parcel.getBinding()) {
283+
let typeMap = {
284+
create: FileChangeType.Created,
285+
update: FileChangeType.Changed,
286+
delete: FileChangeType.Deleted,
287+
}
288+
289+
let subscription = await parcel.subscribe(
290+
folder,
291+
(err, events) => {
292+
onFileEvents(events.map((event) => ({ file: event.path, type: typeMap[event.type] })))
293+
},
270294
{
271-
ignorePermissionErrors: true,
272-
ignoreInitial: true,
273-
ignored: ['**/node_modules/**'],
274-
awaitWriteFinish: {
275-
stabilityThreshold: 100,
276-
pollInterval: 20,
277-
},
295+
ignore: ignore.map((ignorePattern) =>
296+
path.resolve(folder, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, ''))
297+
),
278298
}
279299
)
280300

301+
disposables.push({
302+
dispose() {
303+
subscription.unsubscribe()
304+
},
305+
})
306+
} else {
307+
chokidarWatcher = chokidar.watch([`**/${CONFIG_FILE_GLOB}`, '**/package.json'], {
308+
cwd: folder,
309+
ignorePermissionErrors: true,
310+
ignoreInitial: true,
311+
ignored: ignore,
312+
awaitWriteFinish: {
313+
stabilityThreshold: 100,
314+
pollInterval: 20,
315+
},
316+
})
317+
281318
await new Promise<void>((resolve) => {
282-
watcher.on('ready', () => resolve())
319+
chokidarWatcher.on('ready', () => resolve())
283320
})
284321

285-
watcher
322+
chokidarWatcher
286323
.on('add', (file) => onFileEvents([{ file, type: FileChangeType.Created }]))
287324
.on('change', (file) => onFileEvents([{ file, type: FileChangeType.Changed }]))
288325
.on('unlink', (file) => onFileEvents([{ file, type: FileChangeType.Deleted }]))
326+
327+
disposables.push({
328+
dispose() {
329+
chokidarWatcher.close()
330+
},
331+
})
289332
}
290333

291-
function registerCapabilities(watchFiles?: string[]): void {
334+
function registerCapabilities(watchFiles: string[] = []): void {
292335
if (supportsDynamicRegistration(connection, params)) {
293336
if (registrations) {
294337
registrations.then((r) => r.dispose())
@@ -310,7 +353,7 @@ async function createProjectService(
310353
resolveProvider: true,
311354
triggerCharacters: [...TRIGGER_CHARACTERS, state.separator],
312355
})
313-
if (watchFiles) {
356+
if (watchFiles.length > 0) {
314357
capabilities.add(DidChangeWatchedFilesNotification.type, {
315358
watchers: watchFiles.map((file) => ({ globPattern: file })),
316359
})
@@ -323,13 +366,13 @@ async function createProjectService(
323366
function resetState(): void {
324367
clearAllDiagnostics(state)
325368
Object.keys(state).forEach((key) => {
326-
if (key !== 'editor') {
369+
// Keep `dependencies` to ensure that they are still watched
370+
if (key !== 'editor' && key !== 'dependencies') {
327371
delete state[key]
328372
}
329373
})
330374
state.enabled = false
331-
registerCapabilities()
332-
// TODO reset watcher (remove config dependencies)
375+
registerCapabilities(state.dependencies)
333376
}
334377

335378
async function tryInit() {
@@ -813,10 +856,10 @@ async function createProjectService(
813856
}
814857

815858
if (state.dependencies) {
816-
watcher?.unwatch(state.dependencies)
859+
chokidarWatcher?.unwatch(state.dependencies)
817860
}
818861
state.dependencies = getModuleDependencies(state.configPath)
819-
watcher?.add(state.dependencies)
862+
chokidarWatcher?.add(state.dependencies)
820863

821864
state.configId = getConfigId(state.configPath, state.dependencies)
822865

@@ -837,6 +880,11 @@ async function createProjectService(
837880
return {
838881
state,
839882
tryInit,
883+
dispose() {
884+
for (let { dispose } of disposables) {
885+
dispose()
886+
}
887+
},
840888
onUpdateSettings(settings: any): void {
841889
documentSettingsCache.clear()
842890
if (state.enabled) {
@@ -1279,7 +1327,9 @@ class TW {
12791327
}
12801328

12811329
dispose(): void {
1282-
//
1330+
for (let [, project] of this.projects) {
1331+
project.dispose()
1332+
}
12831333
}
12841334
}
12851335

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const os = require('os')
2+
const path = require('path')
3+
const fs = require('fs')
4+
5+
const vars = (process.config && process.config.variables) || {}
6+
const arch = os.arch()
7+
const platform = os.platform()
8+
const abi = process.versions.modules
9+
const runtime = isElectron() ? 'electron' : 'node'
10+
const libc = process.env.LIBC || (isAlpine(platform) ? 'musl' : 'glibc')
11+
const armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : vars.arm_version) || ''
12+
const uv = (process.versions.uv || '').split('.')[0]
13+
14+
const prebuilds = {
15+
'darwin-x64': {
16+
'node.napi.glibc.node': () => require('./prebuilds/darwin-x64.node.napi.glibc.node'),
17+
},
18+
'linux-x64': {
19+
'node.napi.glibc.node': () => require('./prebuilds/linux-x64.node.napi.glibc.node'),
20+
'node.napi.musl.node': () => require('./prebuilds/linux-x64.node.napi.musl.node'),
21+
},
22+
'win32-x64': {
23+
'node.napi.glibc.node': () => require('./prebuilds/win32-x64.node.napi.glibc.node'),
24+
},
25+
}
26+
27+
let getBinding = () => {
28+
let resolved = resolve()
29+
getBinding = () => resolved
30+
return resolved
31+
}
32+
33+
exports.getBinding = getBinding
34+
35+
exports.writeSnapshot = (dir, snapshot, opts) => {
36+
return getBinding().writeSnapshot(
37+
path.resolve(dir),
38+
path.resolve(snapshot),
39+
normalizeOptions(dir, opts)
40+
)
41+
}
42+
43+
exports.getEventsSince = (dir, snapshot, opts) => {
44+
return getBinding().getEventsSince(
45+
path.resolve(dir),
46+
path.resolve(snapshot),
47+
normalizeOptions(dir, opts)
48+
)
49+
}
50+
51+
exports.subscribe = async (dir, fn, opts) => {
52+
dir = path.resolve(dir)
53+
opts = normalizeOptions(dir, opts)
54+
await getBinding().subscribe(dir, fn, opts)
55+
56+
return {
57+
unsubscribe() {
58+
return getBinding().unsubscribe(dir, fn, opts)
59+
},
60+
}
61+
}
62+
63+
exports.unsubscribe = (dir, fn, opts) => {
64+
return getBinding().unsubscribe(path.resolve(dir), fn, normalizeOptions(dir, opts))
65+
}
66+
67+
function resolve() {
68+
// Find most specific flavor first
69+
var list = prebuilds[platform + '-' + arch]
70+
var builds = Object.keys(list)
71+
var parsed = builds.map(parseTags)
72+
var candidates = parsed.filter(matchTags(runtime, abi))
73+
var winner = candidates.sort(compareTags(runtime))[0]
74+
if (winner) return list[winner.file]()
75+
}
76+
77+
function parseTags(file) {
78+
var arr = file.split('.')
79+
var extension = arr.pop()
80+
var tags = { file: file, specificity: 0 }
81+
82+
if (extension !== 'node') return
83+
84+
for (var i = 0; i < arr.length; i++) {
85+
var tag = arr[i]
86+
87+
if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') {
88+
tags.runtime = tag
89+
} else if (tag === 'napi') {
90+
tags.napi = true
91+
} else if (tag.slice(0, 3) === 'abi') {
92+
tags.abi = tag.slice(3)
93+
} else if (tag.slice(0, 2) === 'uv') {
94+
tags.uv = tag.slice(2)
95+
} else if (tag.slice(0, 4) === 'armv') {
96+
tags.armv = tag.slice(4)
97+
} else if (tag === 'glibc' || tag === 'musl') {
98+
tags.libc = tag
99+
} else {
100+
continue
101+
}
102+
103+
tags.specificity++
104+
}
105+
106+
return tags
107+
}
108+
109+
function matchTags(runtime, abi) {
110+
return function (tags) {
111+
if (tags == null) return false
112+
if (tags.runtime !== runtime && !runtimeAgnostic(tags)) return false
113+
if (tags.abi !== abi && !tags.napi) return false
114+
if (tags.uv && tags.uv !== uv) return false
115+
if (tags.armv && tags.armv !== armv) return false
116+
if (tags.libc && tags.libc !== libc) return false
117+
118+
return true
119+
}
120+
}
121+
122+
function runtimeAgnostic(tags) {
123+
return tags.runtime === 'node' && tags.napi
124+
}
125+
126+
function compareTags(runtime) {
127+
// Precedence: non-agnostic runtime, abi over napi, then by specificity.
128+
return function (a, b) {
129+
if (a.runtime !== b.runtime) {
130+
return a.runtime === runtime ? -1 : 1
131+
} else if (a.abi !== b.abi) {
132+
return a.abi ? -1 : 1
133+
} else if (a.specificity !== b.specificity) {
134+
return a.specificity > b.specificity ? -1 : 1
135+
} else {
136+
return 0
137+
}
138+
}
139+
}
140+
141+
function normalizeOptions(dir, opts = {}) {
142+
if (Array.isArray(opts.ignore)) {
143+
opts = Object.assign({}, opts, {
144+
ignore: opts.ignore.map((ignore) => path.resolve(dir, ignore)),
145+
})
146+
}
147+
148+
return opts
149+
}
150+
151+
function isElectron() {
152+
if (process.versions && process.versions.electron) return true
153+
if (process.env.ELECTRON_RUN_AS_NODE) return true
154+
return typeof window !== 'undefined' && window.process && window.process.type === 'renderer'
155+
}
156+
157+
function isAlpine(platform) {
158+
return platform === 'linux' && fs.existsSync('/etc/alpine-release')
159+
}

0 commit comments

Comments
 (0)