diff --git a/packages/site/package.json b/packages/site/package.json index c6ac49ac6..11633c0ab 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -17,5 +17,9 @@ "lodash-es": "^4.17.21", "vue": "^3.2.29", "vue-router": "^4.0.0" + }, + "devDependencies": { + "@types/rimraf": "^3.0.2", + "rimraf": "^3.0.2" } } diff --git a/packages/site/plugins/themePlugin.ts b/packages/site/plugins/themePlugin.ts new file mode 100644 index 000000000..8de396e5e --- /dev/null +++ b/packages/site/plugins/themePlugin.ts @@ -0,0 +1,164 @@ +import { mkdirSync, readFileSync } from 'fs' +import { readFile, writeFile } from 'fs/promises' +import path from 'path' + +import { existsSync, writeFileSync } from 'fs-extra' +import rimraf from 'rimraf' +import { Plugin } from 'vite' + +import { compile } from '../../../scripts/gulp/build/less' + +export const themePlugin = (options?: Options): Plugin => { + const srcPath = path.join(process.cwd(), 'src') + let outputDir = '' + + const themeDirName = 'themes' + let basePath = '' + let isBuild = false + let originalThemeLess = '' + + const changeRuntimeTheme = async (theme: string): Promise => { + const lessFilePath = path.join(srcPath, 'styles/themes', 'index.less') + const themeContent = (await readFile(lessFilePath)).toString() + return writeFile(lessFilePath, themeContent.replace(/\/.*?.less/, `/${theme}.less`)) + } + + return { + name: 'idux:site-theme-plugin', + enforce: 'pre', + configResolved(config) { + basePath = config.base + outputDir = config.build.outDir + if (config.command === 'build') { + isBuild = true + } + // generate themes menus + writeFileSync( + path.join(srcPath, 'components/global/themeConfig.ts'), + `export const themeConfig = ${JSON.stringify(options?.themes)}`, + ) + }, + async configureServer(server) { + // default theme + await changeRuntimeTheme('default') + // change theme func on dev mode + server.middlewares.use('/themes/s', async (ctx, resp) => { + await changeRuntimeTheme(ctx.url!.split('/')[1]) + resp.write('hello idux!') + resp.end() + }) + }, + // clear user theme selected,and avoid theme css into chunk + buildStart() { + if (isBuild) { + const topPath = path.join(process.cwd(), 'src') + const lessFilePath = path.join(topPath, 'index.less') + + const themeContent = readFileSync(lessFilePath).toString() + if (!originalThemeLess) { + originalThemeLess = themeContent.match(/\/\/==themes\n(.*?)\n\/\/==/s)?.[1] ?? '' + } + writeFileSync(lessFilePath, themeContent.replace(originalThemeLess, '')) + } + }, + // restore user last modified theme code + buildEnd() { + if (isBuild) { + const lessFilePath = path.join(srcPath, 'index.less') + const themeContent = readFileSync(lessFilePath).toString() + writeFileSync( + lessFilePath, + themeContent.replace(/(\/\/==themes\n)(.*?)(\n\/\/==)/s, (_1, b, _2, d) => b + originalThemeLess + d), + ) + } + }, + async generateBundle() { + const buildThemeDir = path.join(outputDir, themeDirName) + if (existsSync(buildThemeDir)) { + rimraf.sync(buildThemeDir) + } + mkdirSync(buildThemeDir) + // resolve theme absolute path + const themeLessContent = originalThemeLess.replaceAll('./styles/', 'src/styles/') + // compile all theme + await Promise.all( + options!.themes!.map(async theme => { + const themeLess = themeLessContent.replace('themes/index', `themes/${theme.key}`) + await compile(themeLess, path.join(buildThemeDir, `${theme.key}.css`), true) + }), + ) + }, + // inject the default theme-css link and changeTheme() to index.html + transformIndexHtml(html) { + if (!isBuild) { + return html + } else { + return { + html, + tags: [ + { + tag: 'link', + attrs: { + type: 'text/css', + rel: 'stylesheet', + href: `${basePath}themes/default.css`, + id: 'theme-link', + }, + injectTo: 'head', + }, + { + tag: 'script', + //language='javascript' + children: ` + const createThemeLinkTag = (id, href) => { + const link = document.createElement('link') + link.type = 'text/css' + link.rel = 'stylesheet' + link.id = id + link.href = href + return link + } + window.changeTheme = (theme) => { + const linkId = 'theme-link' + const href = "${basePath}${themeDirName}/" + theme + ".css" + let styleLink = document.getElementById(linkId) + if (styleLink) { + styleLink.id = linkId + "_old" + const newLink = createThemeLinkTag(linkId, href) + if (styleLink.nextSibling) { + styleLink.parentNode.insertBefore(newLink, styleLink.nextSibling) + } else { + styleLink.parentNode.appendChild(newLink) + } + newLink.onload = () => { + requestAnimationFrame(() => { + styleLink.parentNode.removeChild(styleLink) + styleLink = null + }) + } + return + } + document.head.appendChild(createThemeLinkTag(linkId, href)) + }`, + injectTo: 'body', + }, + ], + } + } + }, + } +} + +export interface Theme { + key: string + label: string +} + +export interface Options { + /** + * theme config + * + * @default 'default' + */ + themes: Theme[] +} diff --git a/packages/site/src/App.vue b/packages/site/src/App.vue index 58bbe04e0..af1cda067 100644 --- a/packages/site/src/App.vue +++ b/packages/site/src/App.vue @@ -32,6 +32,7 @@ +