UNPKG

lambda-service

Version:
368 lines (333 loc) 9.44 kB
import chalk from 'chalk' import { join, dirname } from 'path' import { existsSync, readFileSync, writeFileSync } from 'fs' import assert, { deepEqual } from 'assert' import mkdirp from 'mkdirp' import { assign, cloneDeep } from 'lodash' import { parse } from 'dotenv' import signale from 'signale' import { deprecate, winPath } from 'umi-utils' import { UmiError, printUmiError } from 'umi-core/lib/error' import getPaths from './getPaths' import getPlugins from './getPlugins' import PluginAPI from './PluginAPI' import UserConfig from './UserConfig' import registerBabel from './registerBabel' import getCodeFrame from './utils/getCodeFrame' const debug = require('debug')('umi-build-dev:Service') export default class Service { constructor({ cwd }) { // 设置当前node的工作目录 this.cwd = cwd || process.cwd() // 获取package内容 try { this.pkg = require(join(this.cwd, 'package.json')) // eslint-disable-line } catch (e) { this.pkg = {} } // 使用babel/register 改造require使得导入模块时自动转译 // 默认忽略node_modules registerBabel({ cwd: this.cwd }) this.commands = {} this.pluginHooks = {} this.pluginMethods = {} this.generators = {} this.UmiError = UmiError this.printUmiError = printUmiError // 获取用户文件配置 this.config = UserConfig.getConfig({ cwd: this.cwd, service: this }) debug(`user config: ${JSON.stringify(this.config)}`) // resolve plugins this.plugins = this.resolvePlugins() this.extraPlugins = [] debug(`plugins: ${this.plugins.map(p => p.id).join(' | ')}`) // 获取paths配置 === api.paths this.paths = getPaths(this) } printUmiError(error, opts) { this.applyPlugins('onPrintUmiError', { args: { error, opts } }) printUmiError(error, opts) } resolvePlugins() { try { assert( Array.isArray(this.config.plugins || []), `Configure item ${chalk.underline.cyan( 'plugins' )} should be Array, but got ${chalk.red(typeof this.config.plugins)}` ) return getPlugins({ cwd: winPath(this.cwd), plugins: this.config.plugins || [] }) } catch (e) { if (process.env.UMI_TEST) { throw new Error(e) } else { this.printUmiError(e) process.exit(1) } } } initPlugin(plugin) { const { id, apply, opts } = plugin try { assert( typeof apply === 'function', ` plugin must export a function, e.g. export default function(api) { // Implement functions via api } `.trim() ) const api = new Proxy(new PluginAPI(id, this), { get: (target, prop) => { if (this.pluginMethods[prop]) { return this.pluginMethods[prop] } if ( [ // methods 'changePluginOption', 'applyPlugins', '_applyPluginsAsync', 'writeTmpFile', // properties 'cwd', 'config', 'webpackConfig', 'pkg', 'paths', 'routes', // error handler 'UmiError', 'printUmiError', // dev methods 'restart', 'printError', 'printWarn', 'refreshBrowser', 'rebuildTmpFiles', 'rebuildHTML' ].includes(prop) ) { if (typeof this[prop] === 'function') { return this[prop].bind(this) } else { return this[prop] } } else { return target[prop] } } }) api.onOptionChange = fn => { assert( typeof fn === 'function', `The first argument for api.onOptionChange should be function in ${id}.` ) plugin.onOptionChange = fn } apply(api, opts) plugin._api = api } catch (e) { if (process.env.UMI_TEST) { throw new Error(e) } else { signale.error( ` Plugin ${chalk.cyan.underline(id)} initialize failed ${getCodeFrame(e, { cwd: this.cwd })} `.trim() ) debug(e) process.exit(1) } } } initPlugins() { this.plugins.forEach(plugin => { this.initPlugin(plugin) }) let count = 0 while (this.extraPlugins.length) { const extraPlugins = cloneDeep(this.extraPlugins) this.extraPlugins = [] extraPlugins.forEach(plugin => { this.initPlugin(plugin) this.plugins.push(plugin) }) count += 1 assert(count <= 10, `插件注册死循环?`) } // Throw error for methods that can't be called after plugins is initialized this.plugins.forEach(plugin => { ;[ 'onOptionChange', 'register', 'registerMethod', 'registerPlugin' ].forEach(method => { plugin._api[method] = () => { throw new Error( `api.${method}() should not be called after plugin is initialized.` ) } }) }) } changePluginOption(id, newOpts) { assert(id, `id must supplied`) const plugin = this.plugins.filter(p => p.id === id)[0] assert(plugin, `plugin ${id} not found`) plugin.opts = newOpts if (plugin.onOptionChange) { plugin.onOptionChange(newOpts) } else { this.restart(`plugin ${id}'s option changed`) } } applyPlugins(key, opts = {}) { debug(`apply plugins ${key}`) return (this.pluginHooks[key] || []).reduce((memo, { fn }) => { try { return fn({ memo, args: opts.args }) } catch (e) { console.error(chalk.red(`Plugin apply failed: ${e.message}`)) throw e } }, opts.initialValue) } async _applyPluginsAsync(key, opts = {}) { debug(`apply plugins async ${key}`) const hooks = this.pluginHooks[key] || [] let memo = opts.initialValue for (const hook of hooks) { const { fn } = hook // eslint-disable-next-line no-await-in-loop memo = await fn({ memo, args: opts.args }) } return memo } loadEnv() { const basePath = join(this.cwd, '.env') const localPath = `${basePath}.local` const load = path => { if (existsSync(path)) { debug(`load env from ${path}`) const parsed = parse(readFileSync(path, 'utf-8')) Object.keys(parsed).forEach(key => { // eslint-disable-next-line no-prototype-builtins if (!process.env.hasOwnProperty(key)) { process.env[key] = parsed[key] } }) } } load(basePath) load(localPath) } writeTmpFile(file, content) { const { paths } = this const path = join(paths.absTmpDirPath, file) mkdirp.sync(dirname(path)) writeFileSync(path, content, 'utf-8') } init() { // load env this.loadEnv() // init plugins this.initPlugins() // reload user config const userConfig = new UserConfig(this) const config = userConfig.getConfig({ force: true }) mergeConfig(this.config, config) this.userConfig = userConfig if (config.browserslist) { deprecate('config.browserslist', 'use config.targets instead') } debug('got user config') debug(this.config) // assign user's outputPath config to paths object if (config.outputPath) { const { paths } = this paths.outputPath = config.outputPath paths.absOutputPath = join(paths.cwd, config.outputPath) } debug('got paths') debug(this.paths) } registerCommand(name, opts, fn) { if (typeof opts === 'function') { fn = opts opts = null } opts = opts || {} assert( !(name in this.commands), `Command ${name} exists, please select another one.` ) this.commands[name] = { fn, opts } } run(name = 'help', args) { this.init() return this.runCommand(name, args) } runCommand(rawName, rawArgs) { debug(`raw command name: ${rawName}, args: ${JSON.stringify(rawArgs)}`) const { name, args } = this.applyPlugins('_modifyCommand', { initialValue: { name: rawName, args: rawArgs } }) debug(`run ${name} with args ${JSON.stringify(args)}`) const command = this.commands[name] if (!command) { signale.error(`Command ${chalk.underline.cyan(name)} does not exists`) process.exit(1) } const { fn, opts } = command if (opts.webpack) { // webpack config this.webpackConfig = require('./getWebpackConfig').default(this) if (this.config.ssr) { // when use ssr, push client-manifest plugin into client webpackConfig this.webpackConfig.plugins.push( new (require('./plugins/commands/getChunkMapPlugin').default(this))() ) // server webpack config this.ssrWebpackConfig = require('./getWebpackConfig').default(this, { ssr: this.config.ssr }) } } return fn(args) } } function mergeConfig(oldConfig, newConfig) { Object.keys(oldConfig).forEach(key => { if (!(key in newConfig)) { delete oldConfig[key] } }) assign(oldConfig, newConfig) return oldConfig }