import { join, relative } from 'path'; import { readFileSync, writeFileSync } from 'fs'; import mkdirp from 'mkdirp'; import chokidar from 'chokidar'; import assert from 'assert'; import chalk from 'chalk'; import { throttle, uniq } from 'lodash'; import Mustache from 'mustache'; import { winPath, findJS, prettierFile } from 'umi-utils'; import stripJSONQuote from './routes/stripJSONQuote'; import routesToJSON from './routes/routesToJSON'; import importsToStr from './importsToStr'; import { EXT_LIST } from './constants'; import getHtmlGenerator from './plugins/commands/getHtmlGenerator'; import htmlToJSX from './htmlToJSX'; import getRoutePaths from './routes/getRoutePaths'; import writeContent from './utils/writeContent.js'; const debug = require('debug')('umi:FilesGenerator'); export const watcherIgnoreRegExp = /(^|[\/\\])(_mock.js$|\..)/; function normalizePath(path, base = '/') { if (path.startsWith(base)) { path = path.replace(base, '/'); } return path; } export default class FilesGenerator { constructor(opts) { Object.keys(opts).forEach(key => { this[key] = opts[key]; }); this.routesContent = null; this.hasRebuildError = false; } generate() { debug('generate'); const { paths } = this.service; const { absTmpDirPath, tmpDirPath } = paths; debug(`mkdir tmp dir: ${tmpDirPath}`); mkdirp.sync(absTmpDirPath); this.generateFiles(); } createWatcher(path) { const watcher = chokidar.watch(path, { ignored: watcherIgnoreRegExp, // ignore .dotfiles and _mock.js ignoreInitial: true, }); watcher.on( 'all', throttle((event, path) => { debug(`${event} ${path}`); this.rebuild(); }, 100), ); return watcher; } watch() { if (process.env.WATCH_FILES === 'none') return; const { paths, config: { singular }, } = this.service; const layout = singular ? 'layout' : 'layouts'; let pageWatchers = [ paths.absPagesPath, ...EXT_LIST.map(ext => join(paths.absSrcPath, `${layout}/index${ext}`)), ...EXT_LIST.map(ext => join(paths.absSrcPath, `app${ext}`)), ]; if (this.modifyPageWatcher) { pageWatchers = this.modifyPageWatcher(pageWatchers); } this.watchers = pageWatchers.map(p => { return this.createWatcher(p); }); process.on('SIGINT', () => { this.unwatch(); }); } unwatch() { if (this.watchers) { this.watchers.forEach(watcher => { watcher.close(); }); } } rebuild() { const { refreshBrowser, printError } = this.service; const isDev = process.env.NODE_ENV === 'development'; try { this.service.applyPlugins('onGenerateFiles', { args: { isRebuild: true, }, }); this.generateRouterJS(); this.generateEntry(); this.generateHistory(); if (this.hasRebuildError) { if (isDev) refreshBrowser(); this.hasRebuildError = false; } } catch (e) { // 向浏览器发送出错信息 if (isDev) printError([e.message]); this.hasRebuildError = true; this.routesContent = null; // why? debug(`Generate failed: ${e.message}`); debug(e); console.error(chalk.red(e.message)); } } generateFiles() { this.service.applyPlugins('onGenerateFiles'); this.generateRouterJS(); this.generateEntry(); this.generateHistory(); } generateEntry() { const { paths, config } = this.service; // Generate umi.js const entryTpl = readFileSync(paths.defaultEntryTplPath, 'utf-8'); const initialRender = this.service.applyPlugins('modifyEntryRender', { initialValue: ` window.g_isBrowser = true; let props = {}; // Both support SSR and CSR if (window.g_useSSR) { // 如果开启服务端渲染则客户端组件初始化 props 使用服务端注入的数据 props = window.g_initialData; } else { const pathname = location.pathname; const activeRoute = findRoute(require('@@/router').routes, pathname); // 在客户端渲染前,执行 getInitialProps 方法 // 拿到初始数据 if (activeRoute && activeRoute.component && activeRoute.component.getInitialProps) { const initialProps = plugins.apply('modifyInitialProps', { initialValue: {}, }); props = activeRoute.component.getInitialProps ? await activeRoute.component.getInitialProps({ route: activeRoute, isServer: false, location, ...initialProps, }) : {}; } } const rootContainer = plugins.apply('rootContainer', { initialValue: React.createElement(require('./router').default, props), }); ReactDOM[window.g_useSSR ? 'hydrate' : 'render']( rootContainer, document.getElementById('${config.mountElementId}'), ); `.trim(), }); const moduleBeforeRenderer = this.service .applyPlugins('addRendererWrapperWithModule', { initialValue: [], }) .map((source, index) => { return { source, specifier: `moduleBeforeRenderer${index}`, }; }); const plugins = this.service .applyPlugins('addRuntimePlugin', { initialValue: [], }) .map(plugin => { return winPath(relative(paths.absTmpDirPath, plugin)); }); if (findJS(paths.absSrcPath, 'app')) { plugins.push('@/app'); } const validKeys = this.service.applyPlugins('addRuntimePluginKey', { initialValue: [ 'patchRoutes', 'render', 'rootContainer', 'modifyRouteProps', 'onRouteChange', 'modifyInitialProps', 'initialProps', ], }); assert( uniq(validKeys).length === validKeys.length, `Conflict keys found in [${validKeys.join(', ')}]`, ); let htmlTemplateMap = []; if (config.ssr) { const isProd = process.env.NODE_ENV === 'production'; const routePaths = getRoutePaths(this.RoutesManager.routes); htmlTemplateMap = routePaths.map(routePath => { let ssrHtml = '<>>'; const hg = getHtmlGenerator(this.service, { chunksMap: { // TODO, for dynamic chunks // placeholder waiting manifest umi: [ isProd ? '__UMI_SERVER__.js' : 'umi.js', isProd ? '__UMI_SERVER__.css' : 'umi.css', ], }, headScripts: [ { content: 'window.g_useSSR=true;'.trim(), }, ], scripts: [ { content: `window.g_initialData = \${stringify(props)};`.trim(), }, ], }); const content = hg.getMatchedContent(normalizePath(routePath, config.base)); ssrHtml = htmlToJSX(content).replace( `
`, `