add parcel watcher

master
Brad Cornes 2021-06-14 14:11:29 +01:00
parent 01278a9d4d
commit 2deda99fe7
9 changed files with 283 additions and 31 deletions

View File

@ -25,6 +25,7 @@ import {
HoverRequest,
DidChangeWatchedFilesNotification,
FileChangeType,
Disposable,
} from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
@ -73,6 +74,7 @@ import { debounce } from 'debounce'
import { getModuleDependencies } from './util/getModuleDependencies'
import assert from 'assert'
// import postcssLoadConfig from 'postcss-load-config'
import * as parcel from './watcher/index.js'
const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
const TRIGGER_CHARACTERS = [
@ -151,6 +153,7 @@ function first<T>(...options: Array<() => T>): T {
interface ProjectService {
state: State
tryInit: () => Promise<void>
dispose: () => void
onUpdateSettings: (settings: any) => void
onHover(params: TextDocumentPositionParams): Promise<Hover>
onCompletion(params: CompletionParams): Promise<CompletionList>
@ -167,6 +170,7 @@ async function createProjectService(
params: InitializeParams,
documentService: DocumentService
): Promise<ProjectService> {
const disposables: Disposable[] = []
const state: State = {
enabled: false,
editor: {
@ -208,7 +212,13 @@ async function createProjectService(
const documentSettingsCache: Map<string, Settings> = new Map()
let registrations: Promise<BulkUnregistration>
let watcher: FSWatcher
let chokidarWatcher: FSWatcher
let ignore = [
'**/.git/objects/**',
'**/.git/subtree-cache/**',
'**/node_modules/**',
'**/.hg/store/**',
]
function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void {
let needsInit = false
@ -217,22 +227,30 @@ async function createProjectService(
for (let change of changes) {
let file = normalizePath(change.file)
for (let ignorePattern of ignore) {
if (minimatch(file, ignorePattern)) {
continue
}
}
let isConfigFile = minimatch(file, `**/${CONFIG_FILE_GLOB}`)
let isPackageFile = minimatch(file, '**/package.json')
let isDependency = state.dependencies && state.dependencies.includes(change.file)
if (!isConfigFile && !isPackageFile && !isDependency) continue
if (change.type === FileChangeType.Created) {
needsInit = true
break
} else if (change.type === FileChangeType.Changed) {
if (!state.enabled || minimatch(file, '**/package.json')) {
if (!state.enabled || isPackageFile) {
needsInit = true
break
} else {
needsRebuild = true
}
} else if (change.type === FileChangeType.Deleted) {
if (
!state.enabled ||
minimatch(file, '**/package.json') ||
minimatch(file, `**/${CONFIG_FILE_GLOB}`)
) {
if (!state.enabled || isPackageFile || isConfigFile) {
needsInit = true
break
} else {
@ -261,34 +279,59 @@ async function createProjectService(
connection.client.register(DidChangeWatchedFilesNotification.type, {
watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: '**/package.json' }],
})
} else {
watcher = chokidar.watch(
[
normalizePath(`${folder}/**/${CONFIG_FILE_GLOB}`),
normalizePath(`${folder}/**/package.json`),
],
} else if (parcel.getBinding()) {
let typeMap = {
create: FileChangeType.Created,
update: FileChangeType.Changed,
delete: FileChangeType.Deleted,
}
let subscription = await parcel.subscribe(
folder,
(err, events) => {
onFileEvents(events.map((event) => ({ file: event.path, type: typeMap[event.type] })))
},
{
ignorePermissionErrors: true,
ignoreInitial: true,
ignored: ['**/node_modules/**'],
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 20,
},
ignore: ignore.map((ignorePattern) =>
path.resolve(folder, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, ''))
),
}
)
await new Promise<void>((resolve) => {
watcher.on('ready', () => resolve())
disposables.push({
dispose() {
subscription.unsubscribe()
},
})
} else {
chokidarWatcher = chokidar.watch([`**/${CONFIG_FILE_GLOB}`, '**/package.json'], {
cwd: folder,
ignorePermissionErrors: true,
ignoreInitial: true,
ignored: ignore,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 20,
},
})
watcher
await new Promise<void>((resolve) => {
chokidarWatcher.on('ready', () => resolve())
})
chokidarWatcher
.on('add', (file) => onFileEvents([{ file, type: FileChangeType.Created }]))
.on('change', (file) => onFileEvents([{ file, type: FileChangeType.Changed }]))
.on('unlink', (file) => onFileEvents([{ file, type: FileChangeType.Deleted }]))
disposables.push({
dispose() {
chokidarWatcher.close()
},
})
}
function registerCapabilities(watchFiles?: string[]): void {
function registerCapabilities(watchFiles: string[] = []): void {
if (supportsDynamicRegistration(connection, params)) {
if (registrations) {
registrations.then((r) => r.dispose())
@ -310,7 +353,7 @@ async function createProjectService(
resolveProvider: true,
triggerCharacters: [...TRIGGER_CHARACTERS, state.separator],
})
if (watchFiles) {
if (watchFiles.length > 0) {
capabilities.add(DidChangeWatchedFilesNotification.type, {
watchers: watchFiles.map((file) => ({ globPattern: file })),
})
@ -323,13 +366,13 @@ async function createProjectService(
function resetState(): void {
clearAllDiagnostics(state)
Object.keys(state).forEach((key) => {
if (key !== 'editor') {
// Keep `dependencies` to ensure that they are still watched
if (key !== 'editor' && key !== 'dependencies') {
delete state[key]
}
})
state.enabled = false
registerCapabilities()
// TODO reset watcher (remove config dependencies)
registerCapabilities(state.dependencies)
}
async function tryInit() {
@ -813,10 +856,10 @@ async function createProjectService(
}
if (state.dependencies) {
watcher?.unwatch(state.dependencies)
chokidarWatcher?.unwatch(state.dependencies)
}
state.dependencies = getModuleDependencies(state.configPath)
watcher?.add(state.dependencies)
chokidarWatcher?.add(state.dependencies)
state.configId = getConfigId(state.configPath, state.dependencies)
@ -837,6 +880,11 @@ async function createProjectService(
return {
state,
tryInit,
dispose() {
for (let { dispose } of disposables) {
dispose()
}
},
onUpdateSettings(settings: any): void {
documentSettingsCache.clear()
if (state.enabled) {
@ -1279,7 +1327,9 @@ class TW {
}
dispose(): void {
//
for (let [, project] of this.projects) {
project.dispose()
}
}
}

View File

@ -0,0 +1,159 @@
const os = require('os')
const path = require('path')
const fs = require('fs')
const vars = (process.config && process.config.variables) || {}
const arch = os.arch()
const platform = os.platform()
const abi = process.versions.modules
const runtime = isElectron() ? 'electron' : 'node'
const libc = process.env.LIBC || (isAlpine(platform) ? 'musl' : 'glibc')
const armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : vars.arm_version) || ''
const uv = (process.versions.uv || '').split('.')[0]
const prebuilds = {
'darwin-x64': {
'node.napi.glibc.node': () => require('./prebuilds/darwin-x64.node.napi.glibc.node'),
},
'linux-x64': {
'node.napi.glibc.node': () => require('./prebuilds/linux-x64.node.napi.glibc.node'),
'node.napi.musl.node': () => require('./prebuilds/linux-x64.node.napi.musl.node'),
},
'win32-x64': {
'node.napi.glibc.node': () => require('./prebuilds/win32-x64.node.napi.glibc.node'),
},
}
let getBinding = () => {
let resolved = resolve()
getBinding = () => resolved
return resolved
}
exports.getBinding = getBinding
exports.writeSnapshot = (dir, snapshot, opts) => {
return getBinding().writeSnapshot(
path.resolve(dir),
path.resolve(snapshot),
normalizeOptions(dir, opts)
)
}
exports.getEventsSince = (dir, snapshot, opts) => {
return getBinding().getEventsSince(
path.resolve(dir),
path.resolve(snapshot),
normalizeOptions(dir, opts)
)
}
exports.subscribe = async (dir, fn, opts) => {
dir = path.resolve(dir)
opts = normalizeOptions(dir, opts)
await getBinding().subscribe(dir, fn, opts)
return {
unsubscribe() {
return getBinding().unsubscribe(dir, fn, opts)
},
}
}
exports.unsubscribe = (dir, fn, opts) => {
return getBinding().unsubscribe(path.resolve(dir), fn, normalizeOptions(dir, opts))
}
function resolve() {
// Find most specific flavor first
var list = prebuilds[platform + '-' + arch]
var builds = Object.keys(list)
var parsed = builds.map(parseTags)
var candidates = parsed.filter(matchTags(runtime, abi))
var winner = candidates.sort(compareTags(runtime))[0]
if (winner) return list[winner.file]()
}
function parseTags(file) {
var arr = file.split('.')
var extension = arr.pop()
var tags = { file: file, specificity: 0 }
if (extension !== 'node') return
for (var i = 0; i < arr.length; i++) {
var tag = arr[i]
if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') {
tags.runtime = tag
} else if (tag === 'napi') {
tags.napi = true
} else if (tag.slice(0, 3) === 'abi') {
tags.abi = tag.slice(3)
} else if (tag.slice(0, 2) === 'uv') {
tags.uv = tag.slice(2)
} else if (tag.slice(0, 4) === 'armv') {
tags.armv = tag.slice(4)
} else if (tag === 'glibc' || tag === 'musl') {
tags.libc = tag
} else {
continue
}
tags.specificity++
}
return tags
}
function matchTags(runtime, abi) {
return function (tags) {
if (tags == null) return false
if (tags.runtime !== runtime && !runtimeAgnostic(tags)) return false
if (tags.abi !== abi && !tags.napi) return false
if (tags.uv && tags.uv !== uv) return false
if (tags.armv && tags.armv !== armv) return false
if (tags.libc && tags.libc !== libc) return false
return true
}
}
function runtimeAgnostic(tags) {
return tags.runtime === 'node' && tags.napi
}
function compareTags(runtime) {
// Precedence: non-agnostic runtime, abi over napi, then by specificity.
return function (a, b) {
if (a.runtime !== b.runtime) {
return a.runtime === runtime ? -1 : 1
} else if (a.abi !== b.abi) {
return a.abi ? -1 : 1
} else if (a.specificity !== b.specificity) {
return a.specificity > b.specificity ? -1 : 1
} else {
return 0
}
}
}
function normalizeOptions(dir, opts = {}) {
if (Array.isArray(opts.ignore)) {
opts = Object.assign({}, opts, {
ignore: opts.ignore.map((ignore) => path.resolve(dir, ignore)),
})
}
return opts
}
function isElectron() {
if (process.versions && process.versions.electron) return true
if (process.env.ELECTRON_RUN_AS_NODE) return true
return typeof window !== 'undefined' && window.process && window.process.type === 'renderer'
}
function isAlpine(platform) {
return platform === 'linux' && fs.existsSync('/etc/alpine-release')
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-present Devon Govett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Mathias Buus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -8,3 +8,4 @@ tests/**
**/*.map
.gitignore
**/tsconfig.json
**/*.node