diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b77a8b2a4f..221739347a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,12 @@ catalogs: '@iconify-json/ri': specifier: 1.2.10 version: 1.2.10 + '@iconify/tools': + specifier: 4.2.0 + version: 4.2.0 + '@iconify/utils': + specifier: 3.1.0 + version: 3.1.0 '@lexical/link': specifier: 0.42.0 version: 0.42.0 @@ -1006,6 +1012,12 @@ importers: '@iconify-json/ri': specifier: 'catalog:' version: 1.2.10 + '@iconify/tools': + specifier: 'catalog:' + version: 4.2.0 + '@iconify/utils': + specifier: 'catalog:' + version: 3.1.0 '@mdx-js/loader': specifier: 'catalog:' version: 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b48e4a9985..4d195a92ca 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -86,6 +86,8 @@ catalog: "@hono/node-server": 1.19.11 "@iconify-json/heroicons": 1.2.3 "@iconify-json/ri": 1.2.10 + "@iconify/tools": 4.2.0 + "@iconify/utils": 3.1.0 "@lexical/code": 0.42.0 "@lexical/link": 0.42.0 "@lexical/list": 0.42.0 diff --git a/web/package.json b/web/package.json index fba3f8d85a..5334cb908a 100644 --- a/web/package.json +++ b/web/package.json @@ -163,6 +163,8 @@ "@hono/node-server": "catalog:", "@iconify-json/heroicons": "catalog:", "@iconify-json/ri": "catalog:", + "@iconify/tools": "catalog:", + "@iconify/utils": "catalog:", "@mdx-js/loader": "catalog:", "@mdx-js/react": "catalog:", "@mdx-js/rollup": "catalog:", diff --git a/web/uno.config.ts b/web/uno.config.ts index 02689bddc0..a4e036dfff 100644 --- a/web/uno.config.ts +++ b/web/uno.config.ts @@ -1,11 +1,12 @@ /* eslint-disable ts/ban-ts-comment */ // @ts-nocheck +import { readdirSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { icons as heroicons } from '@iconify-json/heroicons' -import { icons as ri } from '@iconify-json/ri' -import { importSvgCollections } from 'iconify-import-svg' +import { cleanupSVG, deOptimisePaths, isEmptyColor, parseColors, runSVGO, SVG } from '@iconify/tools' +import { compareColors, stringToColor } from '@iconify/utils/lib/colors' +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' import { defineConfig, presetIcons, presetTypography, presetWind3, transformerDirectives } from 'unocss' import tailwindThemeVarDefine from './themes/tailwind-theme-var-define.ts' @@ -13,10 +14,83 @@ const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)) -const parseColorOptions = { - fallback: () => 'currentColor', +const blackColor = stringToColor('black') +const whiteColor = stringToColor('white') + +const transformSvgToCurrentColor = (source: string) => { + const svg = new SVG(source) + + cleanupSVG(svg) + parseColors(svg, { + defaultColor: 'currentColor', + callback: (attr, colorString, color) => { + if (!color) + throw new Error(`Invalid color: "${colorString}" in attribute ${attr}`) + if (isEmptyColor(color)) + return color + if (compareColors(color, blackColor)) + return 'currentColor' + if (compareColors(color, whiteColor)) + return 'remove' + return 'currentColor' + }, + }) + runSVGO(svg) + deOptimisePaths(svg) + + return svg.toString() } +const findSvgDirectories = (rootDir: string) => { + const result: string[] = [] + + const walk = (dir: string) => { + const entries = readdirSync(dir, { withFileTypes: true }) + const subdirs: string[] = [] + let hasSvgFiles = false + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.svg')) + hasSvgFiles = true + else if (entry.isDirectory()) + subdirs.push(path.join(dir, entry.name)) + } + + if (hasSvgFiles) + result.push(dir) + + for (const subdir of subdirs) + walk(subdir) + } + + walk(rootDir) + return result +} + +const createCollectionLoaders = (source: string, prefix: string) => { + const directories = findSvgDirectories(source) + + return Object.fromEntries( + directories.map((dir) => { + const pathPrefix = path.relative(source, dir).split(path.sep).join('-') + return [ + `${prefix}-${pathPrefix}`, + FileSystemIconLoader(dir, transformSvgToCurrentColor), + ] + }), + ) +} + +const publicCollections = createCollectionLoaders( + path.resolve(dirname, 'app/components/base/icons/assets/public'), + 'custom-public', +) + +const venderCollections = createCollectionLoaders( + path.resolve(dirname, 'app/components/base/icons/assets/vender'), + 'custom-vender', +) + export default defineConfig({ blocklist: [ /\$\{/, @@ -46,26 +120,8 @@ export default defineConfig({ presetTypography(), presetIcons({ collections: { - heroicons, - ri, - ...importSvgCollections({ - source: path.resolve(dirname, 'app/components/base/icons/assets/public'), - prefix: 'custom-public', - ignoreImportErrors: true, - cleanupSVG: true, - deOptimisePaths: true, - runSVGO: true, - parseColors: parseColorOptions, - }), - ...importSvgCollections({ - source: path.resolve(dirname, 'app/components/base/icons/assets/vender'), - prefix: 'custom-vender', - ignoreImportErrors: true, - cleanupSVG: true, - deOptimisePaths: true, - runSVGO: true, - parseColors: parseColorOptions, - }), + ...publicCollections, + ...venderCollections, }, extraProperties: { width: '1rem', diff --git a/web/vite.config.ts b/web/vite.config.ts index 92762676d1..c65ea1215b 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,5 +1,7 @@ import { fileURLToPath } from 'node:url' import react from '@vitejs/plugin-react' +import autoprefixer from 'autoprefixer' +import UnoCSS from 'unocss/vite' import vinext from 'vinext' import Inspect from 'vite-plugin-inspect' import { defineConfig } from 'vite-plus' @@ -11,6 +13,36 @@ import { nextStaticImageTestPlugin } from './plugins/vite/next-static-image-test const projectRoot = fileURLToPath(new URL('.', import.meta.url)) const isCI = !!process.env.CI const rootClientInjectTarget = getRootClientInjectTarget(projectRoot) +const unoCssEntryTarget = `${projectRoot}app/layout.tsx` +const unoGlobalsCssTarget = `${projectRoot}app/styles/globals.css` + +const injectUnoCssPlugin = { + name: 'inject-uno-css-entry', + enforce: 'pre' as const, + transform(code: string, id: string) { + if (id !== unoCssEntryTarget || code.includes('virtual:uno.css')) + return + + return { + code: `import 'virtual:uno.css'\n${code}`, + map: null, + } + }, +} + +const stripUnoPreflightDirectivePlugin = { + name: 'strip-uno-preflight-directive', + enforce: 'pre' as const, + transform(code: string, id: string) { + if (id !== unoGlobalsCssTarget || !code.includes('@unocss !preflights;')) + return + + return { + code: code.replace('@unocss !preflights;', ''), + map: null, + } + }, +} export default defineConfig(({ mode }) => { const isTest = mode === 'test' @@ -37,6 +69,7 @@ export default defineConfig(({ mode }) => { ] : isStorybook ? [ + UnoCSS(), react(), ] : [ @@ -48,6 +81,9 @@ export default defineConfig(({ mode }) => { injectTarget: rootClientInjectTarget, projectRoot, }), + injectUnoCssPlugin, + stripUnoPreflightDirectivePlugin, + UnoCSS(), react(), vinext({ react: false }), customI18nHmrPlugin({ injectTarget: rootClientInjectTarget }), @@ -59,6 +95,13 @@ export default defineConfig(({ mode }) => { resolve: { tsconfigPaths: true, }, + css: { + postcss: { + plugins: [ + autoprefixer(), + ], + }, + }, // vinext related config ...(!isTest && !isStorybook