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 @@ + + + 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