Skip to content

Commit

Permalink
feat(scripts): add dynamic theme vite-plugin (#1034)
Browse files Browse the repository at this point in the history
  • Loading branch information
tuchg committed Jul 26, 2022
1 parent f948786 commit 556a4df
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 7 deletions.
4 changes: 4 additions & 0 deletions packages/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
164 changes: 164 additions & 0 deletions packages/site/plugins/themePlugin.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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[]
}
1 change: 1 addition & 0 deletions packages/site/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<LayoutFooter></LayoutFooter>
</IxCol>
</IxRow>
<GlobalTheme />
</div>
<template v-else>
<router-view></router-view>
Expand Down
63 changes: 63 additions & 0 deletions packages/site/src/components/global/GlobalTheme.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<div class="floatButton">
<IxDropdown placement="top" :offset="[0, 16]">
<span class="ix-dropdown-trigger">
<IxIcon name="setting" />
</span>
<template #overlay>
<IxMenu :dataSource="dataSource" @click="changeTheme"></IxMenu>
</template>
</IxDropdown>
</div>
</template>

<script setup lang="ts">
import { MenuData } from '@idux/components/menu'
import { themeConfig } from './themeConfig'
const dataSource: MenuData[] = themeConfig.map(item => {
return {
type: 'item',
...item,
}
})
dataSource.push(
...[
{ type: 'divider', key: 'divider', label: '' },
{ type: 'item', key: 'title', label: 'Theme', disabled: true },
],
)
const changeTheme = async ({ key }) => {
if (window.changeTheme) {
window.changeTheme(key)
} else {
await fetch('/themes/s/' + key)
}
}
</script>

<style lang="less">
.floatButton {
position: fixed;
right: 72px;
bottom: 82px;
z-index: 99;
cursor: pointer;
// TODO need less var
color: #000;
background-color: #fff;
padding: 6px;
border-radius: 50%;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
&:hover {
color: #1c6eff;
}
.ix-icon {
font-size: 20px;
}
}
</style>
4 changes: 4 additions & 0 deletions packages/site/src/components/global/themeConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const themeConfig = [
{ key: 'default', label: 'Default' },
{ key: 'seer', label: 'Seer' },
]
9 changes: 3 additions & 6 deletions packages/site/src/index.less
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
@import '@idux/cdk/index.less';

@import '@idux/components/style/core/reset.default.less';
@import '@idux/components/style/core/reset-scroll.default.less';
@import '@idux/components/default.less';

@import '@idux/pro/default.less';

//==themes
@import "./styles/themes/index.less";
@import './styles/index.less';
//==
9 changes: 9 additions & 0 deletions packages/site/src/styles/themes/default.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @import '@idux/components/style/core/reset.default.less';
// @import '@idux/components/style/core/reset-scroll.default.less';
// @import '@idux/components/default.less';
// @import '@idux/pro/default.less';

@import '../../../../components/style/core/reset.default.less';
@import '../../../../components/style/core/reset-scroll.default.less';
@import '../../../../components/default.less';
@import '../../../../pro/default.less';
2 changes: 2 additions & 0 deletions packages/site/src/styles/themes/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// auto generated file
@import './default.less';
9 changes: 9 additions & 0 deletions packages/site/src/styles/themes/seer.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @import '@idux/components/style/core/reset.seer.less';
// @import '@idux/components/style/core/reset-scroll.seer.less';
// @import '@idux/components/seer.less';
// @import '@idux/pro/seer.less';

@import '../../../../components/style/core/reset.seer.less';
@import '../../../../components/style/core/reset-scroll.seer.less';
@import '../../../../components/seer.less';
@import '../../../../pro/seer.less';
7 changes: 7 additions & 0 deletions packages/site/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'

import { mdPlugin } from './plugins/mdPlugin'
import { themePlugin } from './plugins/themePlugin'
import { transformIndexPlugin } from './plugins/transformIndexPlugin'

export default defineConfig(({ command }) => {
Expand All @@ -22,6 +23,12 @@ export default defineConfig(({ command }) => {
dts: true,
}),
transformIndexPlugin(),
themePlugin({
themes: [
{ key: 'default', label: 'Default' },
{ key: 'seer', label: 'Seer' },
],
}),
],
resolve: {
alias: [
Expand Down
2 changes: 1 addition & 1 deletion scripts/gulp/build/less.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { gulpConfig } from '../gulpConfig'

const { themes } = gulpConfig.build

async function compile(content: string, savePath: string, min: boolean, rootPath?: string): Promise<void> {
export async function compile(content: string, savePath: string, min: boolean, rootPath?: string): Promise<void> {
const plugins: Less.Plugin[] = []
if (min) {
plugins.push(new LessPluginCleanCSS({ advanced: true }))
Expand Down

0 comments on commit 556a4df

Please sign in to comment.