Skip to content

Commit

Permalink
feat(inspector): analyzer (#2762)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
enkot and antfu committed Aug 9, 2023
1 parent 382c832 commit ef6a3bc
Show file tree
Hide file tree
Showing 36 changed files with 2,495 additions and 733 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"esno": "^0.17.0",
"execa": "^7.2.0",
"fast-glob": "^3.3.1",
"floating-vue": "2.0.0-beta.24",
"fs-extra": "^11.1.1",
"gzip-size": "^6.0.0",
"jsdom": "^22.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/extractors/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { Extractor } from '../types'
export const defaultSplitRE = /[\\:]?[\s'"`;{}]+/g
export const splitWithVariantGroupRE = /([\\:]?[\s"'`;<>]|:\(|\)"|\)\s)/g

export function splitCode(code: string) {
return [...new Set(code.split(defaultSplitRE))]
export function splitCode(code: string): string[] {
return code.split(defaultSplitRE)
}

export const extractorSplit: Extractor = {
Expand Down
80 changes: 65 additions & 15 deletions packages/core/src/generator/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createNanoEvents } from '../utils/events'
import type { CSSEntries, CSSObject, DynamicRule, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, RuleContext, RuleMeta, Shortcut, ShortcutValue, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantHandlerContext, VariantMatchedResult } from '../types'
import type { CSSEntries, CSSObject, DynamicRule, ExtendedTokenInfo, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, RuleContext, RuleMeta, Shortcut, ShortcutValue, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantHandlerContext, VariantMatchedResult } from '../types'
import { resolveConfig } from '../config'
import { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, isString, noop, normalizeCSSEntries, normalizeCSSValues, notNull, toArray, uniq, warnOnce } from '../utils'
import { CONTROL_SHORTCUT_NO_MERGE, CountableSet, TwoKeyMap, e, entriesToCss, expandVariantGroup, isCountableSet, isRawUtil, isStaticShortcut, isString, noop, normalizeCSSEntries, normalizeCSSValues, notNull, toArray, uniq, warnOnce } from '../utils'
import { version } from '../../package.json'
import { LAYER_DEFAULT, LAYER_PREFLIGHTS } from '../constants'

Expand Down Expand Up @@ -39,21 +39,40 @@ export class UnoGenerator<Theme extends object = object> {
this.events.emit('config', this.config)
}

applyExtractors(
code: string,
id?: string,
extracted?: Set<string>,
): Promise<Set<string>>
applyExtractors(
code: string,
id?: string,
extracted?: CountableSet<string>,
): Promise<CountableSet<string>>
async applyExtractors(
code: string,
id?: string,
extracted = new Set<string>(),
): Promise<Set<string>> {
extracted: Set<string> | CountableSet<string> = new Set<string>(),
): Promise<Set<string> | CountableSet<string>> {
const context: ExtractorContext = {
original: code,
code,
id,
extracted,
envMode: this.config.envMode,
}

for (const extractor of this.config.extractors) {
const result = await extractor.extract?.(context)
if (result) {

if (!result)
continue

if (isCountableSet(result) && isCountableSet(extracted)) {
for (const token of result)
extracted.setCount(token, extracted.getCount(token) + result.getCount(token))
}
else {
for (const token of result)
extracted.add(token)
}
Expand Down Expand Up @@ -127,31 +146,54 @@ export class UnoGenerator<Theme extends object = object> {
this._cache.set(cacheKey, null)
}

generate(
input: string | Set<string> | CountableSet<string> | string[],
options?: GenerateOptions<false>
): Promise<GenerateResult<Set<string>>>
generate(
input: string | Set<string> | CountableSet<string> | string[],
options?: GenerateOptions<true>
): Promise<GenerateResult<Map<string, ExtendedTokenInfo<Theme>>>>
async generate(
input: string | Set<string> | string[],
options: GenerateOptions = {},
): Promise<GenerateResult> {
input: string | Set<string> | CountableSet<string> | string[],
options: GenerateOptions<boolean> = {},
): Promise<GenerateResult<unknown>> {
const {
id,
scope,
preflights = true,
safelist = true,
minify = false,
extendedInfo = false,
} = options

const tokens: Readonly<Set<string>> = isString(input)
? await this.applyExtractors(input, id)
const tokens: Readonly<Set<string> | CountableSet<string>> = isString(input)
? await this.applyExtractors(
input,
id,
extendedInfo
? new CountableSet<string>()
: new Set<string>(),
)
: Array.isArray(input)
? new Set(input)
? new Set<string>(input)
: input

if (safelist)
this.config.safelist.forEach(s => tokens.add(s))
if (safelist) {
this.config.safelist.forEach((s) => {
// We don't want to increment count if token is already in the set
if (!tokens.has(s))
tokens.add(s)
})
}

const nl = minify ? '' : '\n'

const layerSet = new Set<string>([LAYER_DEFAULT])
const matched = new Set<string>()
const matched = extendedInfo
? new Map<string, ExtendedTokenInfo<Theme>>()
: new Set<string>()

const sheet = new Map<string, StringifiedUtil<Theme>[]>()
let preflightsMap: Record<string, string> = {}

Expand All @@ -163,7 +205,15 @@ export class UnoGenerator<Theme extends object = object> {
if (payload == null)
return

matched.add(raw)
if (matched instanceof Map) {
matched.set(raw, {
data: payload,
count: isCountableSet(tokens) ? tokens.getCount(raw) : -1,
})
}
else {
matched.add(raw)
}

for (const item of payload) {
const parent = item[3] || ''
Expand Down
32 changes: 26 additions & 6 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { LoadConfigResult } from 'unconfig'
import type MagicString from 'magic-string'
import type { UnoGenerator } from './generator'
import type { BetterMap } from './utils'
import type { BetterMap, CountableSet } from './utils'

export type Awaitable<T> = T | Promise<T>
export type Arrayable<T> = T | T[]
Expand Down Expand Up @@ -116,7 +116,8 @@ export interface ExtractorContext {
readonly original: string
code: string
id?: string
extracted: Set<string>
extracted: Set<string> | CountableSet<string>
envMode?: 'dev' | 'build'
}

export interface PreflightContext<Theme extends object = object> {
Expand All @@ -138,7 +139,7 @@ export interface Extractor {
*
* Return `undefined` to skip this extractor.
*/
extract?(ctx: ExtractorContext): Awaitable<Set<string> | string[] | undefined | void>
extract?(ctx: ExtractorContext): Awaitable<Set<string> | CountableSet<string> | string[] | undefined | void>
}

export interface RuleMeta {
Expand Down Expand Up @@ -781,12 +782,12 @@ RequiredByKey<UserConfig<Theme>, 'mergeSelectors' | 'theme' | 'rules' | 'variant
separators: string[]
}

export interface GenerateResult {
export interface GenerateResult<T = Set<string>> {
css: string
layers: string[]
getLayer(name?: string): string | undefined
getLayers(includes?: string[], excludes?: string[]): string
matched: Set<string>
matched: T
}

export type VariantMatchedResult<Theme extends object = object> = readonly [
Expand Down Expand Up @@ -840,7 +841,21 @@ export interface UtilObject {
noMerge: boolean | undefined
}

export interface GenerateOptions {
/**
* Returned from `uno.generate()` when `extendedInfo` option is enabled.
*/
export interface ExtendedTokenInfo<Theme extends object = object> {
/**
* Stringified util data
*/
data: StringifiedUtil<Theme>[]
/**
* Return -1 if the data structure is not countable
*/
count: number
}

export interface GenerateOptions<T extends boolean> {
/**
* Filepath of the file being processed.
*/
Expand Down Expand Up @@ -868,4 +883,9 @@ export interface GenerateOptions {
* @experimental
*/
scope?: string

/**
* If return extended "matched" with payload and count
*/
extendedInfo?: T
}
39 changes: 39 additions & 0 deletions packages/core/src/utils/countable-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export class CountableSet<K> extends Set<K> {
_map: Map<K, number>

constructor(values?: Iterable<K>) {
super(values)
this._map ??= new Map()
for (const value of values ?? [])
this.add(value)
}

add(key: K) {
this._map ??= new Map()
this._map.set(key, (this._map.get(key) ?? 0) + 1)
return super.add(key)
}

delete(key: K) {
this._map.delete(key)
return super.delete(key)
}

clear() {
this._map.clear()
super.clear()
}

getCount(key: K) {
return this._map.get(key) ?? 0
}

setCount(key: K, count: number) {
this._map.set(key, count)
return super.add(key)
}
}

export function isCountableSet<T = string>(value: any): value is CountableSet<T> {
return value instanceof CountableSet
}
3 changes: 2 additions & 1 deletion packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export * from './object'
export * from './basic'
export * from './helpers'
export * from './map'
export * from './countable-set'
export * from './layer'
export * from './variantGroup'
export * from './variant-group'
export * from './warn'
export * from './handlers'
File renamed without changes.
12 changes: 6 additions & 6 deletions packages/extractor-arbitrary-variants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ export const quotedArbitraryValuesRE = /(?:[\w&:[\]-]|\[\S+=\S+\])+\[\\?['"]?\S+
export const arbitraryPropertyRE = /\[(\\\W|[\w-])+:[^\s:]*?("\S+?"|'\S+?'|`\S+?`|[^\s:]+?)[^\s:]*?\)?\]/g
const arbitraryPropertyCandidateRE = /^\[(\\\W|[\w-])+:['"]?\S+?['"]?\]$/

export function splitCodeWithArbitraryVariants(code: string) {
const result = new Set<string>()
export function splitCodeWithArbitraryVariants(code: string): string[] {
const result: string[] = []

for (const match of code.matchAll(arbitraryPropertyRE)) {
if (!code[match.index! - 1]?.match(/^[\s'"`]/))
continue

result.add(match[0])
result.push(match[0])
}

for (const match of code.matchAll(quotedArbitraryValuesRE))
result.add(match[0])
result.push(match[0])

code
.split(defaultSplitRE)
.forEach((match) => {
if (isValidSelector(match) && !arbitraryPropertyCandidateRE.test(match))
result.add(match)
result.push(match)
})

return [...result]
return result
}

export const extractorArbitraryVariants: Extractor = {
Expand Down
5 changes: 5 additions & 0 deletions packages/inspector/client/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ export {}

declare module 'vue' {
export interface GlobalComponents {
Analyzer: typeof import('./components/Analyzer.vue')['default']
AnalyzerItem: typeof import('./components/AnalyzerItem.vue')['default']
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
Copy: typeof import('./components/Copy.vue')['default']
FileIcon: typeof import('./components/FileIcon.vue')['default']
ModuleId: typeof import('./components/ModuleId.vue')['default']
ModuleInfo: typeof import('./components/ModuleInfo.vue')['default']
ModuleTreeNode: typeof import('./components/ModuleTreeNode.vue')['default']
NarBar: typeof import('./components/NarBar.vue')['default']
Overview: typeof import('./components/Overview.vue')['default']
OverviewTabs: typeof import('./components/OverviewTabs.vue')['default']
ReplPlayground: typeof import('./components/ReplPlayground.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./components/Sidebar.vue')['default']
StatusBar: typeof import('./components/StatusBar.vue')['default']
Tabs: typeof import('./components/Tabs.vue')['default']
TitleBar: typeof import('./components/TitleBar.vue')['default']
}
}

0 comments on commit ef6a3bc

Please sign in to comment.