diff --git a/examples/astro-vue/.gitignore b/examples/astro-vue/.gitignore
new file mode 100644
index 0000000000..7329a851d0
--- /dev/null
+++ b/examples/astro-vue/.gitignore
@@ -0,0 +1,20 @@
+# build output
+dist/
+.output/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/examples/astro-vue/astro.config.ts b/examples/astro-vue/astro.config.ts
new file mode 100644
index 0000000000..669068cda2
--- /dev/null
+++ b/examples/astro-vue/astro.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'astro/config'
+import UnoCSS from 'unocss/astro'
+import vue from '@astrojs/vue'
+
+export default defineConfig({
+ integrations: [
+ vue(),
+ UnoCSS(),
+ ],
+})
diff --git a/examples/astro-vue/package.json b/examples/astro-vue/package.json
new file mode 100644
index 0000000000..3f06dda24a
--- /dev/null
+++ b/examples/astro-vue/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "unocss-astro",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "vue": "^3.2.30"
+ },
+ "devDependencies": {
+ "@astrojs/vue": "^1.2.1",
+ "astro": "^1.6.11",
+ "unocss": "link:../../packages/unocss"
+ }
+}
diff --git a/examples/astro-vue/pnpm-workspace.yaml b/examples/astro-vue/pnpm-workspace.yaml
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/astro-vue/src/components/Card.astro b/examples/astro-vue/src/components/Card.astro
new file mode 100644
index 0000000000..09730224b7
--- /dev/null
+++ b/examples/astro-vue/src/components/Card.astro
@@ -0,0 +1,21 @@
+---
+export interface Props {
+ title: string;
+ body: string;
+ href: string;
+}
+
+const { href, title, body } = Astro.props as Props;
+---
+
+
+
+
+ {title}
+ →
+
+
+ {body}
+
+
+
diff --git a/examples/astro-vue/src/components/Example.vue b/examples/astro-vue/src/components/Example.vue
new file mode 100644
index 0000000000..bb85d1f1de
--- /dev/null
+++ b/examples/astro-vue/src/components/Example.vue
@@ -0,0 +1,11 @@
+
+
+ Example Vue component
+
+
+
+
diff --git a/examples/astro-vue/src/env.d.ts b/examples/astro-vue/src/env.d.ts
new file mode 100644
index 0000000000..f964fe0cff
--- /dev/null
+++ b/examples/astro-vue/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/astro-vue/src/layouts/Layout.astro b/examples/astro-vue/src/layouts/Layout.astro
new file mode 100644
index 0000000000..8196555856
--- /dev/null
+++ b/examples/astro-vue/src/layouts/Layout.astro
@@ -0,0 +1,19 @@
+---
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props as Props;
+---
+
+
+
+
+
+
+ {title}
+
+
+
+
+
diff --git a/examples/astro-vue/src/pages/foo.astro b/examples/astro-vue/src/pages/foo.astro
new file mode 100644
index 0000000000..8988bfcabe
--- /dev/null
+++ b/examples/astro-vue/src/pages/foo.astro
@@ -0,0 +1,15 @@
+---
+import Layout from '../layouts/Layout.astro';
+import Card from '../components/Card.astro';
+---
+
+
+ Foo
+
+
diff --git a/examples/astro-vue/src/pages/index.astro b/examples/astro-vue/src/pages/index.astro
new file mode 100644
index 0000000000..7c7fd298a0
--- /dev/null
+++ b/examples/astro-vue/src/pages/index.astro
@@ -0,0 +1,25 @@
+---
+import Layout from '../layouts/Layout.astro';
+import Card from '../components/Card.astro';
+import Example from '../components/Example.vue';
+---
+
+
+
+
+ Welcome to Astro
+
+ Check out the src/pages
directory to get started.
+
+
+
+
diff --git a/examples/astro-vue/tsconfig.json b/examples/astro-vue/tsconfig.json
new file mode 100644
index 0000000000..8610f86adc
--- /dev/null
+++ b/examples/astro-vue/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ }
+}
diff --git a/examples/astro-vue/uno.config.ts b/examples/astro-vue/uno.config.ts
new file mode 100644
index 0000000000..3e140fcf7d
--- /dev/null
+++ b/examples/astro-vue/uno.config.ts
@@ -0,0 +1,28 @@
+import {
+ defineConfig,
+ presetIcons,
+ presetUno,
+ transformerCompileClass,
+ transformerDirectives,
+ transformerVariantGroup,
+} from 'unocss'
+
+export default defineConfig({
+ shortcuts: [
+ { 'i-logo': 'i-logos-astro w-6em h-6em transform transition-800' },
+ ],
+ transformers: [
+ transformerDirectives(),
+ transformerCompileClass(),
+ transformerVariantGroup(),
+ ],
+ presets: [
+ presetUno(),
+ presetIcons({
+ extraProperties: {
+ 'display': 'inline-block',
+ 'vertical-align': 'middle',
+ },
+ }),
+ ],
+})
diff --git a/packages/astro/src/index.ts b/packages/astro/src/index.ts
index 626a7fb92e..ead5c3ccd2 100644
--- a/packages/astro/src/index.ts
+++ b/packages/astro/src/index.ts
@@ -1,3 +1,5 @@
+import { resolve } from 'path'
+import { fileURLToPath } from 'url'
import type { AstroIntegration } from 'astro'
import type { VitePluginConfig } from '@unocss/vite'
import VitePlugin from '@unocss/vite'
@@ -38,6 +40,11 @@ export default function UnoCSSAstroIntegration(
name: 'unocss',
hooks: {
'astro:config:setup': async ({ config, injectScript }) => {
+ // Adding components to UnoCSS's extra content
+ options.extraContent ||= {}
+ options.extraContent.filesystem ||= []
+ options.extraContent.filesystem.push(resolve(fileURLToPath(config.root), 'src/components/**/*').replace(/\\/g, '/'))
+
config.vite.plugins ||= []
config.vite.plugins.push(...VitePlugin(options, defaults) as any)
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 33087ea014..e03a135fa8 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -531,6 +531,13 @@ export interface UnocssPluginContext {
/** Module IDs that been affected by UnoCSS */
affectedModules: Set
+ /** Pending promises */
+ tasks: Promise[]
+ /**
+ * Await all pending tasks
+ */
+ flushTasks(): Promise
+
filter: (code: string, id: string) => boolean
extract: (code: string, id?: string) => Promise
@@ -581,6 +588,19 @@ export interface SourceCodeTransformer {
transform: (code: MagicString, id: string, ctx: UnocssPluginContext) => Awaitable
}
+export interface ExtraContentOptions {
+ /**
+ * Glob patterns to match the files to be extracted
+ * In dev mode, the files will be watched and trigger HMR
+ */
+ filesystem?: string[]
+
+ /**
+ * Plain text to be extracted
+ */
+ plain?: string[]
+}
+
/**
* For other modules to aggregate the options
*/
@@ -611,6 +631,11 @@ export interface PluginOptions {
* Custom transformers to the source code
*/
transformers?: SourceCodeTransformer[]
+
+ /**
+ * Extra content outside of build pipeline (assets, backend, etc.) to be extracted
+ */
+ extraContent?: ExtraContentOptions
}
export interface UserConfig extends ConfigBase, UserOnlyOptions, GeneratorOptions, PluginOptions, CliOptions {}
diff --git a/packages/shared-integration/src/context.ts b/packages/shared-integration/src/context.ts
index 0971c63f7a..d5d5db8252 100644
--- a/packages/shared-integration/src/context.ts
+++ b/packages/shared-integration/src/context.ts
@@ -23,6 +23,7 @@ export function createContext = UserConfig>(
const modules = new BetterMap()
const tokens = new Set()
+ const tasks: Promise[] = []
const affectedModules = new Set()
let ready = reloadConfig()
@@ -83,7 +84,7 @@ export function createContext = UserConfig>(
invalidate()
}
- const filter = (code: string, id: string) => {
+ function filter(code: string, id: string) {
if (code.includes(IGNORE_COMMENT))
return false
return code.includes(INCLUDE_COMMENT) || code.includes(CSS_PLACEHOLDER) || rollupFilter(id.replace(/\?v=\w+$/, ''))
@@ -94,6 +95,12 @@ export function createContext = UserConfig>(
return rawConfig
}
+ async function flushTasks() {
+ const _tasks = [...tasks]
+ await Promise.all(_tasks)
+ tasks.splice(0, _tasks.length)
+ }
+
return {
get ready() {
return ready
@@ -101,6 +108,8 @@ export function createContext = UserConfig>(
tokens,
modules,
affectedModules,
+ tasks,
+ flushTasks,
invalidate,
onInvalidate(fn: () => void) {
invalidations.push(fn)
diff --git a/packages/shared-integration/src/extra-content.ts b/packages/shared-integration/src/extra-content.ts
new file mode 100644
index 0000000000..40e98cde1e
--- /dev/null
+++ b/packages/shared-integration/src/extra-content.ts
@@ -0,0 +1,55 @@
+import fs from 'fs/promises'
+import { resolve } from 'path'
+import fg from 'fast-glob'
+import type { UnocssPluginContext } from '@unocss/core'
+import { applyTransformers } from './transformers'
+
+export async function setupExtraContent(ctx: UnocssPluginContext, shouldWatch = false) {
+ const { extraContent } = await ctx.getConfig()
+ const { extract, tasks, root, filter } = ctx
+
+ // plain text
+ if (extraContent?.plain) {
+ await Promise.all(
+ extraContent.plain.map((code, idx) => {
+ return extract(code, `__extra_content_${idx}__`)
+ }),
+ )
+ }
+
+ // filesystem
+ if (extraContent?.filesystem) {
+ const files = await fg(extraContent.filesystem, { cwd: root })
+
+ async function extractFile(file: string) {
+ const code = await fs.readFile(file, 'utf-8')
+ if (!filter(code, file))
+ return
+ const preTransform = await applyTransformers(ctx, code, file, 'pre')
+ const defaultTransform = await applyTransformers(ctx, preTransform?.code || code, file)
+ await applyTransformers(ctx, defaultTransform?.code || preTransform?.code || code, file, 'post')
+ return await extract(preTransform?.code || code, file)
+ }
+
+ if (shouldWatch) {
+ const { watch } = await import('chokidar')
+ const ignored = ['**/{.git,node_modules}/**']
+
+ const watcher = watch(files, {
+ ignorePermissionErrors: true,
+ ignored,
+ cwd: root,
+ ignoreInitial: true,
+ })
+
+ watcher.on('all', (type, file) => {
+ if (type === 'add' || type === 'change') {
+ const absolutePath = resolve(root, file)
+ tasks.push(extractFile(absolutePath))
+ }
+ })
+ }
+
+ await Promise.all(files.map(extractFile))
+ }
+}
diff --git a/packages/vite/package.json b/packages/vite/package.json
index feed50530e..8fdd0a5a15 100644
--- a/packages/vite/package.json
+++ b/packages/vite/package.json
@@ -53,6 +53,8 @@
"@unocss/inspector": "workspace:*",
"@unocss/scope": "workspace:*",
"@unocss/transformer-directives": "workspace:*",
+ "chokidar": "^3.5.3",
+ "fast-glob": "^3.2.12",
"magic-string": "^0.27.0"
},
"devDependencies": {
diff --git a/packages/vite/src/modes/global/build.ts b/packages/vite/src/modes/global/build.ts
index 96c93ba632..ac2d964710 100644
--- a/packages/vite/src/modes/global/build.ts
+++ b/packages/vite/src/modes/global/build.ts
@@ -14,11 +14,12 @@ import {
resolveLayer,
} from '../../integration'
import type { VitePluginConfig } from '../../types'
+import { setupExtraContent } from '../../../../shared-integration/src/extra-content'
-export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, getConfig }: UnocssPluginContext): Plugin[] {
+export function GlobalModeBuildPlugin(ctx: UnocssPluginContext): Plugin[] {
+ const { uno, ready, extract, tokens, filter, getConfig, tasks, flushTasks } = ctx
const vfsLayers = new Set()
const layerImporterMap = new Map()
- let tasks: Promise[] = []
let viteConfig: ResolvedConfig
// use maps to differentiate multiple build. using outDir as key
@@ -46,7 +47,7 @@ export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, get
let lastTokenSize = 0
let lastResult: GenerateResult | undefined
async function generateAll() {
- await Promise.all(tasks)
+ await flushTasks()
if (lastResult && lastTokenSize === tokens.size)
return lastResult
lastResult = await uno.generate(tokens, { minify: true })
@@ -54,13 +55,16 @@ export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, get
return lastResult
}
+ let replaced = false
+
return [
{
name: 'unocss:global:build:scan',
apply: 'build',
enforce: 'pre',
- buildStart() {
- tasks = []
+ async buildStart() {
+ vfsLayers.clear()
+ tasks.length = 0
lastTokenSize = 0
lastResult = undefined
},
@@ -143,6 +147,16 @@ export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, get
return null
},
},
+ {
+ name: 'unocss:global:content',
+ enforce: 'pre',
+ configResolved(config) {
+ viteConfig = config
+ },
+ buildStart() {
+ tasks.push(setupExtraContent(ctx, viteConfig.command === 'serve'))
+ },
+ },
{
name: 'unocss:global:build:generate',
apply: 'build',
@@ -175,9 +189,6 @@ export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, get
{
name: 'unocss:global:build:bundle',
apply: 'build',
- configResolved(config) {
- viteConfig = config
- },
enforce: 'post',
// rewrite the css placeholders
async generateBundle(options, bundle) {
@@ -189,12 +200,17 @@ export function GlobalModeBuildPlugin({ uno, ready, extract, tokens, filter, get
return
if (!vfsLayers.size) {
+ // If `vfsLayers` is empty and `replaced` is true, that means
+ // `generateBundle` hook is called on previous build pipeline. e.g. ssr
+ // Since we already replaced the layers and don't have any more layers
+ // to replace on current build pipeline, we can skip the warning.
+ if (replaced)
+ return
const msg = '[unocss] entry module not found, have you add `import \'uno.css\'` in your main entry?'
this.warn(msg)
return
}
- let replaced = false
const getLayer = (layer: string, input: string, replace = false) => {
const re = new RegExp(`#--unocss-layer-start--${layer}--\\{start:${layer}\\}([\\s\\S]*?)#--unocss-layer-end--${layer}--\\{end:${layer}\\}`, 'g')
if (replace)
diff --git a/packages/vite/src/modes/global/dev.ts b/packages/vite/src/modes/global/dev.ts
index 8aa5d81519..b11845bc7b 100644
--- a/packages/vite/src/modes/global/dev.ts
+++ b/packages/vite/src/modes/global/dev.ts
@@ -7,11 +7,9 @@ const WARN_TIMEOUT = 20000
const WS_EVENT_PREFIX = 'unocss:hmr'
const HASH_LENGTH = 6
-export function GlobalModeDevPlugin({ uno, tokens, affectedModules, onInvalidate, extract, filter }: UnocssPluginContext): Plugin[] {
+export function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules, onInvalidate, extract, filter }: UnocssPluginContext): Plugin[] {
const servers: ViteDevServer[] = []
let base = ''
-
- const tasks: Promise[] = []
const entries = new Set()
let invalidateTimer: any
@@ -21,7 +19,7 @@ export function GlobalModeDevPlugin({ uno, tokens, affectedModules, onInvalidate
let resolvedWarnTimer: any
async function generateCSS(layer: string) {
- await Promise.all(tasks)
+ await flushTasks()
let result: GenerateResult
let tokensSize = tokens.size
do {
diff --git a/packages/webpack/package.json b/packages/webpack/package.json
index 89b09b84bc..3749f191d0 100644
--- a/packages/webpack/package.json
+++ b/packages/webpack/package.json
@@ -44,6 +44,8 @@
"@rollup/pluginutils": "^5.0.2",
"@unocss/config": "workspace:*",
"@unocss/core": "workspace:*",
+ "chokidar": "^3.5.3",
+ "fast-glob": "^3.2.12",
"magic-string": "^0.27.0",
"unplugin": "^1.0.1",
"webpack-sources": "^3.2.3"
diff --git a/packages/webpack/src/index.ts b/packages/webpack/src/index.ts
index 72043aad37..573a5e980c 100644
--- a/packages/webpack/src/index.ts
+++ b/packages/webpack/src/index.ts
@@ -3,6 +3,7 @@ import type { ResolvedUnpluginOptions, UnpluginOptions } from 'unplugin'
import { createUnplugin } from 'unplugin'
import WebpackSources from 'webpack-sources'
import { createContext } from '../../shared-integration/src/context'
+import { setupExtraContent } from '../../shared-integration/src/extra-content'
import { getHash } from '../../shared-integration/src/hash'
import { HASH_PLACEHOLDER_RE, LAYER_MARK_ALL, LAYER_PLACEHOLDER_RE, RESOLVED_ID_RE, getHashPlaceholder, getLayerPlaceholder, resolveId, resolveLayer } from '../../shared-integration/src/layers'
import { applyTransformers } from '../../shared-integration/src/transformers'
@@ -23,7 +24,7 @@ export default function WebpackPlugin(
) {
return createUnplugin(() => {
const ctx = createContext(configOrPath as any, defaults)
- const { uno, tokens, filter, extract, onInvalidate } = ctx
+ const { uno, tokens, filter, extract, onInvalidate, tasks, flushTasks } = ctx
let timer: any
onInvalidate(() => {
@@ -41,7 +42,9 @@ export default function WebpackPlugin(
)
}
- const tasks: Promise[] = []
+ // TODO: detect webpack's watch mode and enable watcher
+ tasks.push(setupExtraContent(ctx))
+
const entries = new Set()
const hashes = new Map()
@@ -92,7 +95,7 @@ export default function WebpackPlugin(
compilation.hooks.optimizeAssets.tapPromise(PLUGIN_NAME, async () => {
const files = Object.keys(compilation.assets)
- await Promise.all(tasks)
+ await flushTasks()
const result = await uno.generate(tokens, { minify: true })
for (const file of files) {
@@ -133,7 +136,7 @@ export default function WebpackPlugin(
if (!plugin.__vfsModules)
return
- await Promise.all(tasks)
+ await flushTasks()
const result = await uno.generate(tokens)
if (lastTokenSize === tokens.size)
return
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 92c3f8ed9b..c3704ac39e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -608,6 +608,8 @@ importers:
'@unocss/scope': workspace:*
'@unocss/shared-integration': workspace:*
'@unocss/transformer-directives': workspace:*
+ chokidar: ^3.5.3
+ fast-glob: ^3.2.12
magic-string: ^0.27.0
vite: ^4.0.0
dependencies:
@@ -618,6 +620,8 @@ importers:
'@unocss/inspector': link:../inspector
'@unocss/scope': link:../scope
'@unocss/transformer-directives': link:../transformer-directives
+ chokidar: 3.5.3
+ fast-glob: 3.2.12
magic-string: 0.27.0
devDependencies:
'@unocss/shared-integration': link:../shared-integration
@@ -651,6 +655,8 @@ importers:
'@types/webpack-sources': ^3.2.0
'@unocss/config': workspace:*
'@unocss/core': workspace:*
+ chokidar: ^3.5.3
+ fast-glob: ^3.2.12
magic-string: ^0.27.0
unplugin: ^1.0.1
webpack: ^5.75.0
@@ -660,6 +666,8 @@ importers:
'@rollup/pluginutils': 5.0.2
'@unocss/config': link:../config
'@unocss/core': link:../core
+ chokidar: 3.5.3
+ fast-glob: 3.2.12
magic-string: 0.27.0
unplugin: 1.0.1
webpack-sources: 3.2.3
@@ -19654,7 +19662,7 @@ packages:
dependencies:
'@types/node': 18.11.13
esbuild: 0.16.4
- postcss: 8.4.20
+ postcss: 8.4.19
resolve: 1.22.1
rollup: 3.7.2
terser: 5.16.1