prevent crash on config error

master
Brad Cornes 2020-04-22 20:29:36 +01:00
parent 850ad5c0a5
commit 568e078522
3 changed files with 97 additions and 55 deletions

View File

@ -17,7 +17,7 @@ function glob(pattern, options = {}) {
let g = new nodeGlob.Glob(pattern, options) let g = new nodeGlob.Glob(pattern, options)
let matches = [] let matches = []
let max = dlv(options, 'max', Infinity) let max = dlv(options, 'max', Infinity)
g.on('match', match => { g.on('match', (match) => {
matches.push(path.resolve(options.cwd || process.cwd(), match)) matches.push(path.resolve(options.cwd || process.cwd(), match))
if (matches.length === max) { if (matches.length === max) {
g.abort() g.abort()
@ -38,39 +38,35 @@ function arraysEqual(arr1, arr2) {
) )
} }
const CONFIG_GLOB =
'**/{tailwind,tailwind.config,tailwind-config,.tailwindrc}.js'
export default async function getClassNames( export default async function getClassNames(
cwd = process.cwd(), cwd = process.cwd(),
{ onChange = () => {} } = {} { onChange = () => {} } = {}
) { ) {
async function run() {
let configPath let configPath
let postcss let postcss
let tailwindcss let tailwindcss
let version let version
try { configPath = await glob(CONFIG_GLOB, {
configPath = await glob(
'**/{tailwind,tailwind.config,tailwind-config,.tailwindrc}.js',
{
cwd, cwd,
ignore: '**/node_modules/**', ignore: '**/node_modules/**',
max: 1 max: 1,
} })
)
invariant(configPath.length === 1, 'No Tailwind CSS config found.') invariant(configPath.length === 1, 'No Tailwind CSS config found.')
configPath = configPath[0] configPath = configPath[0]
postcss = importFrom(cwd, 'postcss') postcss = importFrom(cwd, 'postcss')
tailwindcss = importFrom(cwd, 'tailwindcss') tailwindcss = importFrom(cwd, 'tailwindcss')
version = importFrom(cwd, 'tailwindcss/package.json').version version = importFrom(cwd, 'tailwindcss/package.json').version
} catch (_) {
return null
}
async function run() {
const sepLocation = semver.gte(version, '0.99.0') const sepLocation = semver.gte(version, '0.99.0')
? ['separator'] ? ['separator']
: ['options', 'separator'] : ['options', 'separator']
let userSeperator let userSeperator
let hook = Hook(configPath, exports => { let hook = Hook(configPath, (exports) => {
userSeperator = dlv(exports, sepLocation) userSeperator = dlv(exports, sepLocation)
dset(exports, sepLocation, '__TAILWIND_SEPARATOR__') dset(exports, sepLocation, '__TAILWIND_SEPARATOR__')
return exports return exports
@ -97,35 +93,48 @@ export default async function getClassNames(
} }
return { return {
configPath,
config: resolveConfig({ cwd, config }), config: resolveConfig({ cwd, config }),
separator: typeof userSeperator === 'undefined' ? ':' : userSeperator, separator: typeof userSeperator === 'undefined' ? ':' : userSeperator,
classNames: await extractClassNames(ast), classNames: await extractClassNames(ast),
dependencies: [configPath, ...hook.deps], dependencies: hook.deps,
plugins: getPlugins(config), plugins: getPlugins(config),
variants: getVariants({ config, version, postcss }) variants: getVariants({ config, version, postcss }),
} }
} }
let watcher let watcher
function watch(files) { function watch(files = []) {
if (watcher) watcher.close() if (watcher) watcher.close()
watcher = chokidar watcher = chokidar
.watch(files) .watch([CONFIG_GLOB, ...files])
.on('change', handleChange) .on('change', handleChange)
.on('unlink', handleChange) .on('unlink', handleChange)
} }
let result = await run()
watch(result.dependencies)
async function handleChange() { async function handleChange() {
const prevDeps = result.dependencies const prevDeps = result ? result.dependencies : []
try {
result = await run() result = await run()
} catch (_) {
onChange(null)
return
}
if (!arraysEqual(prevDeps, result.dependencies)) { if (!arraysEqual(prevDeps, result.dependencies)) {
watch(result.dependencies) watch(result.dependencies)
} }
onChange(result) onChange(result)
} }
let result
try {
result = await run()
} catch (_) {
watch()
return null
}
watch(result.dependencies)
return result return result
} }

View File

@ -18,7 +18,7 @@ import {
DidChangeConfigurationNotification, DidChangeConfigurationNotification,
} from 'vscode-languageserver' } from 'vscode-languageserver'
import getTailwindState from 'tailwindcss-class-names' import getTailwindState from 'tailwindcss-class-names'
import { State, Settings } from './util/state' import { State, Settings, EditorState } from './util/state'
import { import {
provideCompletions, provideCompletions,
resolveCompletionItem, resolveCompletionItem,
@ -27,7 +27,7 @@ import { provideHover } from './providers/hoverProvider'
import { URI } from 'vscode-uri' import { URI } from 'vscode-uri'
import { getDocumentSettings } from './util/getDocumentSettings' import { getDocumentSettings } from './util/getDocumentSettings'
let state: State = null let state: State = { enabled: false }
let connection = createConnection(ProposedFeatures.all) let connection = createConnection(ProposedFeatures.all)
let documents = new TextDocuments() let documents = new TextDocuments()
let workspaceFolder: string | null let workspaceFolder: string | null
@ -46,28 +46,43 @@ documents.listen(connection)
connection.onInitialize( connection.onInitialize(
async (params: InitializeParams): Promise<InitializeResult> => { async (params: InitializeParams): Promise<InitializeResult> => {
state = await getTailwindState(
params.rootPath || URI.parse(params.rootUri).path,
{
onChange: (newState: State): void => {
state = { ...newState, editor: state.editor }
connection.sendNotification('tailwindcss/configUpdated', [
state.dependencies[0],
state.config,
state.plugins,
])
},
}
)
const capabilities = params.capabilities const capabilities = params.capabilities
state.editor = { const editorState: EditorState = {
connection, connection,
documents, documents,
documentSettings, documentSettings,
globalSettings, globalSettings,
capabilities: { configuration: capabilities.workspace && !!capabilities.workspace.configuration }, capabilities: {
configuration:
capabilities.workspace && !!capabilities.workspace.configuration,
},
}
const tailwindState = await getTailwindState(
params.rootPath || URI.parse(params.rootUri).path,
{
onChange: (newState: State): void => {
if (newState) {
state = { ...newState, enabled: true, editor: editorState }
connection.sendNotification('tailwindcss/configUpdated', [
state.configPath,
state.config,
state.plugins,
])
} else {
state = { enabled: false, editor: editorState }
// TODO
// connection.sendNotification('tailwindcss/configUpdated', [null])
}
},
}
)
if (tailwindState) {
state = { enabled: true, editor: editorState, ...tailwindState }
} else {
state = { enabled: false, editor: editorState }
} }
return { return {
@ -79,7 +94,20 @@ connection.onInitialize(
textDocumentSync: documents.syncKind, textDocumentSync: documents.syncKind,
completionProvider: { completionProvider: {
resolveProvider: true, resolveProvider: true,
triggerCharacters: ['"', "'", '`', ' ', '.', '[', state.separator], triggerCharacters: [
// class attributes
'"',
"'",
'`',
// between class names
' ',
// @apply and emmet-style
'.',
// config/theme helper
'[',
// TODO: restart server if separater changes?
typeof state.separator === 'undefined' ? ':' : state.separator,
],
}, },
hoverProvider: true, hoverProvider: true,
}, },
@ -97,7 +125,7 @@ connection.onInitialized &&
} }
connection.sendNotification('tailwindcss/configUpdated', [ connection.sendNotification('tailwindcss/configUpdated', [
state.dependencies[0], state.configPath,
state.config, state.config,
state.plugins, state.plugins,
]) ])
@ -120,18 +148,21 @@ connection.onDidChangeConfiguration((change) => {
connection.onCompletion( connection.onCompletion(
(params: CompletionParams): Promise<CompletionList> => { (params: CompletionParams): Promise<CompletionList> => {
if (!state.enabled) return null
return provideCompletions(state, params) return provideCompletions(state, params)
} }
) )
connection.onCompletionResolve( connection.onCompletionResolve(
(item: CompletionItem): CompletionItem => { (item: CompletionItem): CompletionItem => {
if (!state.enabled) return null
return resolveCompletionItem(state, item) return resolveCompletionItem(state, item)
} }
) )
connection.onHover( connection.onHover(
(params: TextDocumentPositionParams): Hover => { (params: TextDocumentPositionParams): Hover => {
if (!state.enabled) return null
return provideHover(state, params) return provideHover(state, params)
} }
) )

View File

@ -28,13 +28,15 @@ export type Settings = {
} }
export type State = null | { export type State = null | {
config: any enabled: boolean
separator: string configPath?: string
plugins: any[] config?: any
variants: string[] separator?: string
classNames: ClassNames plugins?: any[]
dependencies: string[] variants?: string[]
editor: EditorState classNames?: ClassNames
dependencies?: string[]
editor?: EditorState
} }
export type DocumentClassList = { export type DocumentClassList = {