Skip to content

Commit

Permalink
refactor(postcss): perf and stability improvements (#2267)
Browse files Browse the repository at this point in the history
  • Loading branch information
sibbng committed Feb 28, 2023
1 parent 36d4100 commit d311221
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 98 deletions.
2 changes: 1 addition & 1 deletion packages/postcss/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { expandVariantGroup, notNull, regexScopePlaceholder } from '@unocss/core

type Writeable<T> = { -readonly [P in keyof T]: T[P] }

export function parseApply(root: Root, uno: UnoGenerator, directiveName: string) {
export async function parseApply(root: Root, uno: UnoGenerator, directiveName: string) {
// @ts-expect-error types doesn't allow async callback but it seems work
root.walkAtRules(directiveName, async (rule) => {
if (!rule.parent)
Expand Down
247 changes: 154 additions & 93 deletions packages/postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,99 @@ import { createGenerator, warnOnce } from '@unocss/core'
import { loadConfig } from '@unocss/config'
import { defaultIncludeGlobs } from '../../shared-integration/src/defaults'
import { parseApply } from './apply'
import { parseTheme } from './theme'
import { parseTheme, themeFnRE } from './theme'
import { parseScreen } from './screen'
import type { UnoPostcssPluginOptions } from './types'

export * from './types'

function unocss({ content, directiveMap, cwd, configOrPath }: UnoPostcssPluginOptions = {
cwd: process.cwd(),
}) {
function unocss(options: UnoPostcssPluginOptions = {}) {
warnOnce(
'`@unocss/postcss` package is in an experimental state right now. '
+ 'It doesn\'t follow semver, and may introduce breaking changes in patch versions.',
)

const {
cwd = process.cwd(),
directiveMap = {
apply: 'apply',
theme: 'theme',
screen: 'screen',
unocss: 'unocss',
},
content,
configOrPath,
} = options

const fileMap = new Map()
const fileClassMap = new Map()
const classes = new Set<string>()
const targetCache = new Set<string>()
const config = loadConfig(cwd, configOrPath)

let uno: UnoGenerator
let promises: Promise<void>[] = []
let last_config_mtime = 0
const targetRE = new RegExp(Object.values(directiveMap).join('|'))

warnOnce(
'`@unocss/postcss` package is in an experimental state right now. '
+ 'It doesn\'t follow semver, and may introduce breaking changes in patch versions.',
)
return {
postcssPlugin: 'unocss',
postcssPlugin: directiveMap.unocss,
plugins: [
async function (root: Root, result: Result) {
const cfg = await config

if (!Object.keys(!cfg.config))
throw new Error('UnoCSS config file not found.')
if (!result.opts.from?.split('?')[0].endsWith('.css'))
return

let isTarget = false

if (targetRE.test(root.toString())) {
if (!targetCache.has(result.opts.from)) {
root.walkAtRules((rule) => {
if (
rule.name === directiveMap.unocss
|| rule.name === directiveMap.apply
|| rule.name === directiveMap.theme
|| rule.name === directiveMap.screen
)
isTarget = true
})

if (!isTarget) {
const themeFn = themeFnRE(directiveMap.theme)
root.walkDecls((decl) => {
if (themeFn.test(decl.value))
isTarget = true
})
}
}
else {
isTarget = true
}
}
else if (targetCache.has(result.opts.from)) {
targetCache.delete(result.opts.from)
}

if (!uno)
uno = createGenerator(cfg.config)
if (!isTarget)
return
else
targetCache.add(result.opts.from)

await parseApply(root, uno, directiveMap?.apply || 'apply')
parseTheme(root, uno, directiveMap?.theme || 'theme')
await parseScreen(root, uno, directiveMap?.screen || 'screen')
try {
const cfg = await config
if (!uno) {
uno = createGenerator(cfg.config)
}
else if (cfg.sources.length) {
const config_mtime = (await stat(cfg.sources[0])).mtimeMs
if (config_mtime > last_config_mtime) {
uno = createGenerator((await loadConfig(cwd, configOrPath)).config)
last_config_mtime = config_mtime
}
}
}
catch (error: any) {
throw new Error (`UnoCSS config not found: ${error.message}`)
}

const globs = content?.filter(v => typeof v === 'string') as string[] ?? defaultIncludeGlobs
const rawContent = content?.filter(v => typeof v === 'object') as {
Expand All @@ -53,85 +112,87 @@ function unocss({ content, directiveMap, cwd, configOrPath }: UnoPostcssPluginOp
dot: true,
absolute: true,
ignore: ['**/{.git,node_modules}/**'],
stats: true,
}) as unknown as { path: string; mtimeMs: number }[]

result.messages.push({
type: 'dependency',
plugin: directiveMap.unocss,
file: result.opts.from,
parent: result.opts.from,
})

if (result.opts.from?.split('?')[0].endsWith('.css')) {
result.messages.push({
type: 'dependency',
plugin: 'unocss',
file: result.opts.from,
parent: result.opts.from,
})

await Promise.all(
[
...rawContent.map(async (v) => {
const { matched } = await uno.generate(v.raw, {
id: `unocss${v.extension}`,
})

for (const candidate of matched)
classes.add(candidate)
}),
...entries.map(async (file) => {
result.messages.push({
type: 'dependency',
plugin: 'unocss',
file,
parent: result.opts.from,
})

const { mtimeMs } = await stat(file)
if (fileMap.has(file) && mtimeMs <= fileMap.get(file))
return

else
fileMap.set(file, mtimeMs)

const content = await readFile(file, 'utf8')
const { matched } = await uno.generate(content, {
id: file,
})

fileClassMap.set(file, matched)
}),
],
)
for (const set of fileClassMap.values()) {
for (const candidate of set)
await parseApply(root, uno, directiveMap.apply)
await parseTheme(root, uno, directiveMap.theme)
await parseScreen(root, uno, directiveMap.screen)

promises.push(
...rawContent.map(async (v) => {
const { matched } = await uno.generate(v.raw, {
id: `unocss.${v.extension}`,
})

for (const candidate of matched)
classes.add(candidate)
}
const c = await uno.generate(classes)
classes.clear()
const excludes: string[] = []
root.walkAtRules('unocss', (rule) => {
if (rule.params) {
const source = rule.source
const layers = rule.params.split(',').map(v => v.trim())
const css = postcss.parse(
layers
.map(i => (i === 'all' ? c.getLayers() : c.getLayer(i)) || '')
.filter(Boolean)
.join('\n'),
)
css.walkDecls((declaration) => {
declaration.source = source
})
rule.replaceWith(css)
excludes.push(rule.params)
}
})
root.walkAtRules('unocss', (rule) => {
if (!rule.params) {
const source = rule.source
const css = postcss.parse(c.getLayers(undefined, excludes) || '')
css.walkDecls((declaration) => {
declaration.source = source
})
rule.replaceWith(css)
}
})
}),
...entries.map(async ({ path: file, mtimeMs }) => {
result.messages.push({
type: 'dependency',
plugin: directiveMap.unocss,
file,
parent: result.opts.from,
})

if (fileMap.has(file) && mtimeMs <= fileMap.get(file))
return

else
fileMap.set(file, mtimeMs)

const content = await readFile(file, 'utf8')
const { matched } = await uno.generate(content, {
id: file,
})

fileClassMap.set(file, matched)
}),
)
await Promise.all(promises)
promises = []
for (const set of fileClassMap.values()) {
for (const candidate of set)
classes.add(candidate)
}
const c = await uno.generate(classes)
classes.clear()
const excludes: string[] = []
root.walkAtRules(directiveMap.unocss, (rule) => {
if (rule.params) {
const source = rule.source
const layers = rule.params.split(',').map(v => v.trim())
const css = postcss.parse(
layers
.map(i => (i === 'all' ? c.getLayers() : c.getLayer(i)) || '')
.filter(Boolean)
.join('\n'),
)
css.walkDecls((declaration) => {
declaration.source = source
})
rule.replaceWith(css)
excludes.push(rule.params)
}
})
root.walkAtRules(directiveMap.unocss, (rule) => {
if (!rule.params) {
const source = rule.source
const css = postcss.parse(c.getLayers(undefined, excludes) || '')
css.walkDecls((declaration) => {
declaration.source = source
})
rule.replaceWith(css)
}
})
},
],
}
Expand Down
2 changes: 1 addition & 1 deletion packages/postcss/src/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Root } from 'postcss'

import type { Theme } from '@unocss/preset-mini'

export function parseScreen(root: Root, uno: UnoGenerator, directiveName: string) {
export async function parseScreen(root: Root, uno: UnoGenerator, directiveName: string) {
// @ts-expect-error types
root.walkAtRules(directiveName, async (rule) => {
let breakpointName = ''
Expand Down
6 changes: 3 additions & 3 deletions packages/postcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { UnoGenerator } from '@unocss/core'
import type { Root } from 'postcss'
import MagicString from 'magic-string'

export function parseTheme(root: Root, uno: UnoGenerator, directiveName: string) {
const themeFnRE = new RegExp(`${directiveName}\\((.*?)\\)`, 'g')
export const themeFnRE = (directiveName: string) => new RegExp(`${directiveName}\\((.*?)\\)`, 'g')
export async function parseTheme(root: Root, uno: UnoGenerator, directiveName: string) {
root.walkDecls((decl) => {
const matches = Array.from(decl.value.matchAll(themeFnRE))
const matches = Array.from(decl.value.matchAll(themeFnRE(directiveName)))

if (!matches.length)
return
Expand Down
1 change: 1 addition & 0 deletions packages/postcss/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface UnoPostcssPluginOptions {
apply: string
screen: string
theme: string
unocss: string
}
cwd?: string
configOrPath?: string | UserConfig
Expand Down

0 comments on commit d311221

Please sign in to comment.