import fs from 'fs'; import path from 'path'; import { createHash } from 'crypto'; import type { IApi, IRoute } from 'dumi'; import ReactTechStack from 'dumi/dist/techStacks/react'; import chalk from 'chalk'; import sylvanas from 'sylvanas'; import createEmotionServer from '@emotion/server/create-instance'; import localPackage from '../../package.json'; function extractEmotionStyle(html: string) { // copy from emotion ssr // https://github.com/vercel/next.js/blob/deprecated-main/examples/with-emotion-vanilla/pages/_document.js const styles = global.__ANTD_STYLE_CACHE_MANAGER_FOR_SSR__.getCacheList().map((cache) => { const result = createEmotionServer(cache).extractCritical(html); if (!result.css) return null; const { css, ids } = result; return { key: cache.key, css, ids, tag: `<style data-emotion="${cache.key} ${result.ids.join(' ')}">${result.css}</style>`, }; }); return styles.filter(Boolean); } export const getHash = (str: string, length = 8) => createHash('md5').update(str).digest('hex').slice(0, length); /** * extends dumi internal tech stack, for customize previewer props */ class AntdReactTechStack extends ReactTechStack { // eslint-disable-next-line class-methods-use-this generatePreviewerProps(...[props, opts]: any) { if (opts.type === 'external') { // try to find md file with the same name as the demo tsx file const locale = opts.mdAbsPath.match(/index\.([a-z-]+)\.md$/i)?.[1]; const mdPath = opts.fileAbsPath!.replace(/\.\w+$/, '.md'); const md = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : ''; const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx'); const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : ''; props.pkgDependencyList = { ...localPackage.devDependencies, ...localPackage.dependencies, }; props.jsx = sylvanas.parseText(code); if (md) { // extract description & css style from md file const description = md.match( new RegExp(`(?:^|\\n)## ${locale}([^]+?)(\\n## [a-z]|\\n\`\`\`|\\n<style>|$)`), )?.[1]; const style = md.match(/\r?\n(?:```css|<style>)\r?\n([^]+?)\r?\n(?:```|<\/style>)/)?.[1]; props.description ??= description?.trim(); props.style ??= style; } } return props; } } const resolve = (p: string): string => require.resolve(p); const RoutesPlugin = (api: IApi) => { // const ssrCssFileName = `ssr-${Date.now()}.css`; const writeCSSFile = (key: string, hashKey: string, cssString: string) => { const fileName = `style-${key}.${getHash(hashKey)}.css`; const filePath = path.join(api.paths.absOutputPath, fileName); if (!fs.existsSync(filePath)) { api.logger.event(chalk.grey(`write to: ${filePath}`)); fs.writeFileSync(filePath, cssString, 'utf8'); } return fileName; }; const addLinkStyle = (html: string, cssFile: string, prepend = false) => { const prefix = api.userConfig.publicPath || api.config.publicPath; if (prepend) { return html.replace('<head>', `<head><link rel="stylesheet" href="${prefix + cssFile}">`); } return html.replace('</head>', `<link rel="stylesheet" href="${prefix + cssFile}"></head>`); }; api.registerTechStack(() => new AntdReactTechStack()); api.modifyRoutes((routes) => { // TODO: append extra routes, such as home, changelog, form-v3 const extraRoutesList: IRoute[] = [ { id: 'changelog-cn', path: 'changelog-cn', absPath: '/changelog-cn', parentId: 'DocLayout', file: resolve('../../CHANGELOG.zh-CN.md'), }, { id: 'changelog', path: 'changelog', absPath: '/changelog', parentId: 'DocLayout', file: resolve('../../CHANGELOG.en-US.md'), }, ]; extraRoutesList.forEach((itemRoute) => { routes[itemRoute.path] = itemRoute; }); return routes; }); api.modifyExportHTMLFiles((files) => files // exclude dynamic route path, to avoid deploy failed by `:id` directory .filter((f) => !f.path.includes(':')) .map((file) => { let globalStyles = ''; // Debug for file content: uncomment this if need check raw out // const tmpFileName = `_${file.path.replace(/\//g, '-')}`; // const tmpFilePath = path.join(api.paths.absOutputPath, tmpFileName); // fs.writeFileSync(tmpFilePath, file.content, 'utf8'); // extract all emotion style tags from body file.content = file.content.replace( /<style (data-emotion|data-sandpack)[\S\s]+?<\/style>/g, (s) => { globalStyles += s; return ''; }, ); // insert emotion style tags to head file.content = file.content.replace('</head>', `${globalStyles}</head>`); // 1. 提取 antd-style 样式 const styles = extractEmotionStyle(file.content); // 2. 提取每个样式到独立 css 文件 styles.forEach((result) => { api.logger.event( `${chalk.yellow(file.path)} include ${chalk.blue`[${result!.key}]`} ${chalk.yellow( result!.ids.length, )} styles`, ); const cssFile = writeCSSFile(result!.key, result!.ids.join(''), result!.css); file.content = addLinkStyle(file.content, cssFile); }); // Insert antd style to head const matchRegex = /<style data-type="antd-cssinjs">([\S\s]+?)<\/style>/; const matchList = file.content.match(matchRegex) || []; let antdStyle = ''; matchList.forEach((text) => { file.content = file.content.replace(text, ''); antdStyle += text.replace(matchRegex, '$1'); }); const cssFile = writeCSSFile('antd', antdStyle, antdStyle); file.content = addLinkStyle(file.content, cssFile, true); // Insert antd cssVar to head const cssVarMatchRegex = /<style data-type="antd-css-var"[\S\s]+?<\/style>/; const cssVarMatchList = file.content.match(cssVarMatchRegex) || []; cssVarMatchList.forEach((text) => { file.content = file.content.replace(text, ''); file.content = file.content.replace('<head>', `<head>${text}`); }); return file; }), ); // add ssr css file to html api.modifyConfig((memo) => { memo.styles ??= []; // memo.styles.push(`/${ssrCssFileName}`); return memo; }); }; export default RoutesPlugin;