Skip to content

Commit

Permalink
perf(vscode): improve config loading perf
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jun 18, 2022
1 parent 4804271 commit 9da885b
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 96 deletions.
4 changes: 2 additions & 2 deletions packages/vscode/package.json
@@ -1,6 +1,6 @@
{
"publisher": "antfu",
"name": "@unocss/vscode",
"name": "unocss",
"displayName": "UnoCSS",
"version": "0.39.1",
"private": true,
Expand Down Expand Up @@ -53,7 +53,7 @@
},
"scripts": {
"build": "tsup",
"dev": "nr build --watch src",
"dev": "tsup --watch src",
"publish": "esno ./scripts/publish.ts"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/vscode/src/annonation.ts
Expand Up @@ -24,8 +24,8 @@ export async function registerAnnonations(
const id = doc.uri.fsPath
const dir = path.dirname(id)

if (contextLoader.contexts.has(dir)) {
const ctx = contextLoader.contexts.get(dir)!
if (contextLoader.contextsMap.has(dir)) {
const ctx = contextLoader.contextsMap.get(dir)!
try {
await ctx.reloadConfig()
log.appendLine(`Config reloaded by ${path.relative(cwd, doc.uri.fsPath)}`)
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function registerAnnonations(

let ctx = await contextLoader.resolveContext(code, id)
if (!ctx && (code.includes(INCLUDE_COMMENT_IDE) || isCssId(id)))
ctx = await contextLoader.resolveCloestContext(code, id)
ctx = await contextLoader.resolveClosestContext(code, id)
else if (!ctx?.filter(code, id))
return null

Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/src/autocomplete.ts
Expand Up @@ -72,7 +72,7 @@ export async function registerAutoComplete(

let ctx = await contextLoader.resolveContext(code, id)
if (!ctx && isCssId(id))
ctx = await contextLoader.resolveCloestContext(code, id)
ctx = await contextLoader.resolveClosestContext(code, id)
else if (!ctx?.filter(code, id))
return null

Expand Down
203 changes: 113 additions & 90 deletions packages/vscode/src/contextLoader.ts
@@ -1,6 +1,8 @@
import { readdir } from 'fs/promises'
import path from 'path'
import fs from 'fs'
import type { UnocssPluginContext, UserConfig } from '@unocss/core'
import { notNull } from '@unocss/core'
import { sourceObjectFields, sourcePluginFactory } from 'unconfig/presets'
import presetUno from '@unocss/preset-uno'
import { resolveOptions as resolveNuxtOptions } from '../../nuxt/src/options'
Expand All @@ -13,7 +15,8 @@ export class ContextLoader {
public cwd: string
public ready: Promise<void>
public defaultContext: UnocssPluginContext<UserConfig<any>>
public contexts = new Map<string, UnocssPluginContext<UserConfig<any>>>()
public contextsMap = new Map<string, UnocssPluginContext<UserConfig<any>> | null>()

private fileContextCache = new Map<string, UnocssPluginContext<UserConfig<any>> | null>()
private configExistsCache = new Map<string, boolean>()
public events = createNanoEvents<{
Expand All @@ -37,27 +40,31 @@ export class ContextLoader {
})
}

get contexts() {
return Array.from(new Set(this.contextsMap.values())).filter(notNull)
}

async reload() {
this.ready = this._reload()
await this.ready
this.events.emit('reload')
}

private async _reload() {
for (const dir of this.contexts.keys())
for (const dir of this.contextsMap.keys())
this.unloadContext(dir)
this.fileContextCache.clear()
this.configExistsCache.clear()

await this.loadConfigInDirectory(this.cwd)
await this.loadContextInDirectory(this.cwd)
}

async unloadContext(configDir: string) {
const context = this.contexts.get(configDir)
const context = this.contextsMap.get(configDir)
if (!context)
return

this.contexts.delete(configDir)
this.contextsMap.delete(configDir)

for (const [path, ctx] of this.fileContextCache) {
if (ctx === context)
Expand All @@ -68,94 +75,106 @@ export class ContextLoader {
}

async configExists(dir: string) {
const files = await readdir(dir)
return files.some(f => /^(vite|svelte|astro|iles|nuxt|unocss|uno)\.config/.test(f))
if (!this.configExistsCache.has(dir)) {
const files = await readdir(dir)
this.configExistsCache.set(dir, files.some(f => /^(vite|svelte|astro|iles|nuxt|unocss|uno)\.config/.test(f)))
}
return this.configExistsCache.has(dir)!
}

async loadConfigInDirectory(dir: string) {
const cached = this.contexts.get(dir)
if (cached)
async loadContextInDirectory(dir: string) {
const cached = this.contextsMap.get(dir)
if (cached !== undefined)
return cached

const context = createContext(
dir,
undefined,
[
sourcePluginFactory({
files: [
'vite.config',
'svelte.config',
'astro.config',
'iles.config',
],
targetModule: 'unocss/vite',
parameters: [{ command: 'serve', mode: 'development' }],
}),
sourceObjectFields({
files: 'nuxt.config',
fields: 'unocss',
}),
],
(result) => {
if (result.sources.some(s => s.includes('nuxt.config')))
resolveNuxtOptions(result.config)
},
)

context.updateRoot(dir)

let sources = []
try {
sources = (await context.ready).sources
}
catch (e) {
log.appendLine(`[error] ${String(e)}`)
log.appendLine(`[error] Error occurred while loading config. Config directory: ${dir}`)
return null
}
const load = async () => {
log.appendLine(`[info] Resolving config for ${dir}`)
const context = createContext(
dir,
undefined,
[
sourcePluginFactory({
files: [
'vite.config',
'svelte.config',
'astro.config',
'iles.config',
],
targetModule: 'unocss/vite',
parameters: [{ command: 'serve', mode: 'development' }],
}),
sourceObjectFields({
files: 'nuxt.config',
fields: 'unocss',
}),
],
(result) => {
if (result.sources.some(s => s.includes('nuxt.config')))
resolveNuxtOptions(result.config)
},
)

context.updateRoot(dir)
let sources = []
try {
sources = (await context.ready).sources
}
catch (e) {
log.appendLine(`[error] ${String(e)}`)
log.appendLine(`[error] Error occurred while loading config. Config directory: ${dir}`)
return null
}

if (!sources.length)
return null
if (!sources.length)
return null

const baseDir = path.dirname(sources[0])
if (this.contexts.has(baseDir))
return this.contexts.get(baseDir)!
const baseDir = path.dirname(sources[0])
if (baseDir !== dir) {
// exists on upper level, skip
this.contextsMap.set(dir, null)
return null
}

this.configExistsCache.set(baseDir, true)
context.onReload(() => {
for (const [path, ctx] of this.fileContextCache) {
if (ctx === context || !ctx)
this.fileContextCache.delete(path)
}
this.configExistsCache.clear()
this.events.emit('contextReload', context)
})

context.onReload(() => {
for (const [path, ctx] of this.fileContextCache) {
if (ctx === context || !ctx)
if (!ctx)
this.fileContextCache.delete(path)
}

this.configExistsCache.clear()
this.events.emit('contextReload', context)
})

for (const [path, ctx] of this.fileContextCache) {
if (!ctx)
this.fileContextCache.delete(path)
}

this.events.emit('contextLoaded', context)
this.events.emit('contextLoaded', context)

log.appendLine(`[info] New configuration loaded from\n ${sources.map(s => ` - ${s}`).join('\n')}`)
log.appendLine(`[info] New configuration loaded from\n ${sources.map(s => ` - ${s}`).join('\n')}`)

this.contexts.set(baseDir, context)
return context
}

const context = await load()
if (!this.contextsMap.has(dir))
this.contextsMap.set(dir, context)
return context
}

async resolveContext(code: string, file: string) {
if (file.match(/[\/](node_modules|dist|\.temp|\.cache)[\/]/g))
return

const cached = this.fileContextCache.get(file)
if (cached !== undefined)
return cached

log.appendLine(`[info] Resolving config for ${file}`)

// try finding an existing context that includes the file
for (const [configDir, context] of this.contexts) {
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue

if (!isSubdir(configDir, file))
continue

Expand All @@ -167,37 +186,38 @@ export class ContextLoader {
}

// try finding a config from disk
let dir = path.dirname(file)
while (isSubdir(this.cwd, dir)) {
if (this.configExistsCache.get(dir) === false)
continue

if (!this.configExists(dir)) {
this.configExistsCache.set(dir, false)
continue
if (fs.existsSync(file)) {
let dir = path.dirname(file)
while (isSubdir(this.cwd, dir)) {
if (!await this.configExists(dir))
continue

const context = await this.loadContextInDirectory(dir)
if (context?.filter(code, file)) {
this.fileContextCache.set(file, context)
return context
}

const newDir = path.dirname(dir)
if (newDir === dir)
break
dir = newDir
}

const context = await this.loadConfigInDirectory(dir)
this.configExistsCache.set(dir, !!context)

if (context?.filter(code, file)) {
this.fileContextCache.set(file, context)
return context
}

dir = path.dirname(dir)
}

this.fileContextCache.set(file, null)
return null
}

async resolveCloestContext(code: string, file: string) {
async resolveClosestContext(code: string, file: string) {
const cached = this.fileContextCache.get(file)
if (cached)
return cached

for (const [configDir, context] of this.contexts) {
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue

if (!isSubdir(configDir, file))
continue

Expand All @@ -208,7 +228,10 @@ export class ContextLoader {
return context
}

for (const [configDir, context] of this.contexts) {
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue

if (isSubdir(configDir, file)) {
this.fileContextCache.set(file, context)
return context
Expand Down

0 comments on commit 9da885b

Please sign in to comment.