From 9da885b81b68b62994b02ae040d35fe56a3eab83 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 18 Jun 2022 18:14:56 +0800 Subject: [PATCH] perf(vscode): improve config loading perf --- packages/vscode/package.json | 4 +- packages/vscode/src/annonation.ts | 6 +- packages/vscode/src/autocomplete.ts | 2 +- packages/vscode/src/contextLoader.ts | 203 +++++++++++++++------------ 4 files changed, 119 insertions(+), 96 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 3834c8b2ef..141b159468 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,6 +1,6 @@ { "publisher": "antfu", - "name": "@unocss/vscode", + "name": "unocss", "displayName": "UnoCSS", "version": "0.39.1", "private": true, @@ -53,7 +53,7 @@ }, "scripts": { "build": "tsup", - "dev": "nr build --watch src", + "dev": "tsup --watch src", "publish": "esno ./scripts/publish.ts" }, "devDependencies": { diff --git a/packages/vscode/src/annonation.ts b/packages/vscode/src/annonation.ts index ec2df347f0..9ec855ba3e 100644 --- a/packages/vscode/src/annonation.ts +++ b/packages/vscode/src/annonation.ts @@ -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)}`) @@ -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 diff --git a/packages/vscode/src/autocomplete.ts b/packages/vscode/src/autocomplete.ts index ef07efaf03..71605b2e56 100644 --- a/packages/vscode/src/autocomplete.ts +++ b/packages/vscode/src/autocomplete.ts @@ -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 diff --git a/packages/vscode/src/contextLoader.ts b/packages/vscode/src/contextLoader.ts index 59ac964ab4..c3ae3feca2 100644 --- a/packages/vscode/src/contextLoader.ts +++ b/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' @@ -13,7 +15,8 @@ export class ContextLoader { public cwd: string public ready: Promise public defaultContext: UnocssPluginContext> - public contexts = new Map>>() + public contextsMap = new Map> | null>() + private fileContextCache = new Map> | null>() private configExistsCache = new Map() public events = createNanoEvents<{ @@ -37,6 +40,10 @@ export class ContextLoader { }) } + get contexts() { + return Array.from(new Set(this.contextsMap.values())).filter(notNull) + } + async reload() { this.ready = this._reload() await this.ready @@ -44,20 +51,20 @@ export class ContextLoader { } 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) @@ -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 @@ -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 @@ -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