UNPKG

lambda-service

Version:
414 lines (380 loc) 11.4 kB
import { join, relative } from 'path' import { writeFileSync, readFileSync } from 'fs' import mkdirp from 'mkdirp' import chokidar from 'chokidar' import assert from 'assert' import chalk from 'chalk' import { debounce, uniq } from 'lodash' import Mustache from 'mustache' 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 { winPath, findJS, prettierFile } from 'lambda-base-utils' const debug = require('debug')('umi:FilesGenerator') const stripJSONQuote = function(jsonStr) { return jsonStr .replace(/\"component\": (\"(.+?)\")/g, (global, m1, m2) => { return `"component": ${m2.replace(/\^/g, '"')}` }) .replace(/\"Routes\": (\"(.+?)\")/g, `"Routes": $2`) .replace(/\\r\\n/g, '\r\n') .replace(/\\n/g, '\r\n') } function normalizePath(path, base = '/') { if (path.startsWith(base)) { path = path.replace(base, '/') } return path } export const watcherIgnoreRegExp = /(^|[\/\\])(_mock.js$|\..)/ 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', debounce((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 try { this.service.applyPlugins('onGenerateFiles', { args: { isRebuild: true } }) this.generateRouterJS() this.generateEntry() this.generateHistory() if (this.hasRebuildError) { refreshBrowser() this.hasRebuildError = false } } catch (e) { // 向浏览器发送出错信息 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('@tmp/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, ...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; window.g_initialData = \${require('${winPath( require.resolve('serialize-javascript') )}')(props)}; `.trim() } ] }) const content = hg.getMatchedContent( normalizePath(routePath, config.base) ) ssrHtml = htmlToJSX(content).replace( `<div id="${config.mountElementId || 'root'}"></div>`, `<div id="${config.mountElementId || 'root'}">{ rootContainer }</div>` ) return `'${routePath}': (${ssrHtml}),` }) } const entryContent = Mustache.render(entryTpl, { globalVariables: !this.service.config.disableGlobalVariables, code: this.service .applyPlugins('addEntryCode', { initialValue: [] }) .join('\n\n'), codeAhead: this.service .applyPlugins('addEntryCodeAhead', { initialValue: [] }) .join('\n\n'), imports: importsToStr( this.service.applyPlugins('addEntryImport', { initialValue: moduleBeforeRenderer }) ).join('\n'), importsAhead: importsToStr( this.service.applyPlugins('addEntryImportAhead', { initialValue: [] }) ).join('\n'), polyfillImports: importsToStr( this.service.applyPlugins('addEntryPolyfillImports', { initialValue: [] }) ).join('\n'), moduleBeforeRenderer, render: initialRender, plugins, validKeys, htmlTemplateMap: htmlTemplateMap.join('\n'), findRoutePath: winPath(require.resolve('./findRoute')) }) writeFileSync( paths.absLibraryJSPath, prettierFile(`${entryContent.trim()}\n`), 'utf-8' ) } generateHistory() { const { paths, config } = this.service const tpl = readFileSync(paths.defaultHistoryTplPath, 'utf-8') const initialHistory = ` require('lambda-echo/lib/createHistory').default({ basename: window.routerBase, }) `.trim() let history = this.service.applyPlugins('modifyEntryHistory', { initialValue: initialHistory }) if (config.ssr) { history = ` __IS_BROWSER ? ${initialHistory} : require('history').createMemoryHistory() `.trim() } const content = Mustache.render(tpl, { globalVariables: !this.service.config.disableGlobalVariables, history }) writeFileSync( join(paths.absTmpDirPath, 'history.js'), prettierFile(`${content.trim()}\n`), 'utf-8' ) } generateRouterJS() { const { paths } = this.service const { absRouterJSPath } = paths this.RoutesManager.fetchRoutes() const routesContent = this.getRouterJSContent() // 避免文件写入导致不必要的 webpack 编译 if (this.routesContent !== routesContent) { writeFileSync( absRouterJSPath, prettierFile(`${routesContent.trim()}\n`), 'utf-8' ) this.routesContent = routesContent } } getRouterJSContent() { const { paths } = this.service const routerTpl = readFileSync(paths.defaultRouterTplPath, 'utf-8') // replace {{ routes }} 的 routes 对象 const routes = stripJSONQuote( this.getRoutesJSON({ env: process.env.NODE_ENV }) ) const rendererWrappers = this.service .applyPlugins('addRendererWrapperWithComponent', { initialValue: [] }) .map((source, index) => { return { source, specifier: `RendererWrapper${index}` } }) const routerContent = this.getRouterContent(rendererWrappers) return Mustache.render(routerTpl, { globalVariables: !this.service.config.disableGlobalVariables, imports: importsToStr( this.service.applyPlugins('addRouterImport', { initialValue: rendererWrappers }) ).join('\n'), importsAhead: importsToStr( this.service.applyPlugins('addRouterImportAhead', { initialValue: [] }) ).join('\n'), routes, routerContent, RouterRootComponent: this.service.applyPlugins( 'modifyRouterRootComponent', { initialValue: 'DefaultRouter' } ) }) } fixHtmlSuffix(routes) { routes.forEach(route => { if (route.routes) { route.path = `${route.path}(.html)?` this.fixHtmlSuffix(route.routes) } }) } getRoutesJSON(opts = {}) { const { env } = opts return routesToJSON(this.RoutesManager.routes, this.service, env) } getRouterContent(rendererWrappers) { const defaultRenderer = ` <Router history={history}> { renderRoutes(routes, props) } </Router> `.trim() return rendererWrappers.reduce((memo, wrapper) => { return ` <${wrapper.specifier}> ${memo} </${wrapper.specifier}> `.trim() }, defaultRenderer) } }