UNPKG

8.44 kBJavaScriptView Raw
1const path = require('path')
2const EventEmitter = require('events')
3const Config = require('webpack-chain')
4const webpackMerge = require('webpack-merge')
5const UseConfig = require('use-config')
6const chalk = require('chalk')
7const get = require('lodash/get')
8const merge = require('lodash/merge')
9const parseJsonConfig = require('parse-json-config')
10const chokidar = require('chokidar')
11const CLIEngine = require('./cliEngine')
12const handleOptions = require('./handleOptions')
13const logger = require('@poi/logger')
14const { ownDir } = require('./utils/dir')
15const deleteCache = require('./utils/deleteCache')
16const PoiError = require('./utils/PoiError')
17const loadEnv = require('./utils/loadEnv')
18const Hooks = require('./utils/hooks')
19
20module.exports = class Poi extends EventEmitter {
21 constructor(command = 'build', options = {}) {
22 super()
23 logger.setOptions(options)
24 logger.debug('command', command)
25
26 if (typeof options.require === 'string' || Array.isArray(options.require)) {
27 const requires = [].concat(options.require)
28 requires.forEach(name => {
29 if (name === 'ts-node/register') {
30 return logger.warn(
31 `TypeScript is supported by default, no need to require ${name}`
32 )
33 }
34 require(path.resolve('node_modules', name))
35 })
36 }
37
38 // Assign stuffs to context so external plugins can access them as well
39 this.logger = logger
40 this.ownDir = ownDir
41
42 this.command = command
43 this.options = Object.assign({}, options)
44 this.rerun = () => {
45 // Delete cache
46 deleteCache()
47
48 const poi = new Poi(command, options)
49 return poi.run()
50 }
51
52 this.cli = new CLIEngine(command)
53 this.plugins = new Set()
54 this.hooks = new Hooks()
55
56 this.cli.cac.on('error', err => {
57 if (err.name === 'PoiError') {
58 logger.error(err.message)
59 } else {
60 logger.error(chalk.dim(err.stack))
61 }
62 })
63
64 if (!process.env.NODE_ENV) {
65 switch (this.command) {
66 case 'build':
67 process.env.NODE_ENV = 'production'
68 break
69 case 'test':
70 process.env.NODE_ENV = 'test'
71 break
72 default:
73 process.env.NODE_ENV = 'development'
74 }
75 }
76 this.env = {
77 NODE_ENV: process.env.NODE_ENV
78 }
79 if (this.options.env !== false) {
80 Object.assign(this.env, loadEnv(process.env.NODE_ENV))
81 }
82 logger.inspect('env', this.env)
83 }
84
85 chainWebpack(fn) {
86 this.hooks.add('chainWebpack', fn)
87 return this
88 }
89
90 configureWebpack(fn) {
91 this.hooks.add('configureWebpack', updateConfig => {
92 updateConfig(fn)
93 })
94 return this
95 }
96
97 configureDevServer(fn) {
98 this.hooks.add('configureDevServer', fn)
99 return this
100 }
101
102 createCompiler(webpackConfig) {
103 webpackConfig = webpackConfig || this.createWebpackConfig()
104 const compiler = require('@poi/core/webpack')(webpackConfig)
105 // TODO: Handle MultiCompiler
106 if (this.options.outputFileSystem) {
107 compiler.outputFileSystem = this.options.outputFileSystem
108 }
109 return compiler
110 }
111
112 runCompiler(webpackConfig) {
113 const compiler = this.createCompiler(webpackConfig)
114 return new Promise((resolve, reject) => {
115 compiler.run((err, stats) => {
116 if (err) return reject(err)
117 resolve(stats)
118 })
119 })
120 }
121
122 registerPlugin(plugin) {
123 this.plugins.add(plugin)
124 return this
125 }
126
127 registerPlugins(plugins) {
128 if (typeof plugins === 'string') {
129 plugins = [plugins]
130 }
131 for (const plugin of parseJsonConfig(plugins, { prefix: 'poi-plugin-' })) {
132 this.registerPlugin(plugin)
133 }
134 return this
135 }
136
137 async prepare() {
138 let config
139
140 // Load Poi config file
141 // You can disable this by setting `config` to false
142 if (this.options.config !== false) {
143 const useConfig = new UseConfig({
144 name: 'poi',
145 files: this.options.config
146 ? [this.options.config]
147 : [
148 '{name}.config.js',
149 '{name}.config.ts',
150 '.{name}rc',
151 'package.json'
152 ]
153 })
154 useConfig.addLoader({
155 test: /\.ts$/,
156 loader(filepath) {
157 require(path.resolve('node_modules', 'ts-node/register'))
158 return require(filepath).default
159 }
160 })
161 const poiConfig = await useConfig.load()
162 if (poiConfig.path) {
163 logger.debug('poi config path', poiConfig.path)
164 this.configFile = poiConfig.path
165 config = poiConfig.config
166 } else if (this.options.config) {
167 // Config file was specified but not found
168 throw new PoiError(
169 `Config file was not found at ${this.options.config}`
170 )
171 }
172 }
173
174 this.options = merge(
175 typeof config === 'function' ? config(this.options) : config,
176 this.options
177 )
178 this.options = await handleOptions(this)
179
180 logger.inspect('poi options', this.options)
181
182 // Register our internal plugins
183 this.registerPlugin(require('./plugins/baseConfig'))
184 this.registerPlugin(require('./plugins/develop'))
185 this.registerPlugin(require('./plugins/build'))
186 this.registerPlugin(require('./plugins/watch'))
187
188 // Register user plugins
189 if (this.options.plugins) {
190 this.registerPlugins(this.options.plugins)
191 }
192
193 // Call plugins
194 if (this.plugins.size > 0) {
195 for (const plugin of this.plugins) {
196 plugin(this)
197 }
198 }
199
200 // Add options.chainWebpack to the end
201 const chainWebpack = this.options.chainWebpack || this.options.extendWebpack
202 if (chainWebpack) {
203 logger.debug('Use chainWebpack defined in your config file')
204 this.chainWebpack(chainWebpack)
205 }
206
207 const configureWebpack =
208 this.options.configureWebpack || this.options.webpack
209 if (configureWebpack) {
210 logger.debug('Use configureWebpack defined in your config file')
211 this.configureWebpack(configureWebpack)
212 }
213 }
214
215 async run() {
216 await this.prepare()
217 const res = await this.cli.runCommand()
218 this.watchRun(res)
219 return res
220 }
221
222 watchRun({ devServer, webpackWatcher } = {}) {
223 if (
224 this.options.restartOnFileChanges === false ||
225 !['watch', 'develop'].includes(this.command) ||
226 this.cli.willShowHelp()
227 ) {
228 return
229 }
230
231 const filesToWatch = [
232 ...[].concat(this.configFile || ['poi.config.js', '.poirc']),
233 ...[].concat(this.options.restartOnFileChanges || [])
234 ]
235
236 if (filesToWatch.length === 0) return
237
238 logger.debug('watching files', filesToWatch.join(', '))
239
240 const watcher = chokidar.watch(filesToWatch, {
241 ignoreInitial: true
242 })
243 const handleEvent = filepath => {
244 logger.warn(`Restarting due to changes made in: ${filepath}`)
245 watcher.close()
246 if (devServer) {
247 devServer.close(() => this.rerun())
248 } else if (webpackWatcher) {
249 webpackWatcher.close()
250 this.rerun()
251 }
252 }
253 watcher.on('change', handleEvent)
254 watcher.on('add', handleEvent)
255 watcher.on('unlink', handleEvent)
256 }
257
258 createWebpackConfig({ config, context, chainWebpack } = {}) {
259 config = config || new Config()
260 context = Object.assign({ type: 'client', command: this.command }, context)
261 this.hooks.invoke('chainWebpack', config, context)
262 if (chainWebpack) {
263 chainWebpack(config, context)
264 }
265 let webpackConfig = config.toConfig()
266 this.hooks.invoke('configureWebpack', userConfig => {
267 if (typeof userConfig === 'object') {
268 return webpackMerge(webpackConfig, userConfig)
269 }
270 if (typeof userConfig === 'function') {
271 return userConfig(webpackConfig, context) || webpackConfig
272 }
273 })
274 if (this.options.debugWebpack) {
275 logger.log(
276 chalk.bold(
277 `webpack config${
278 context && context.type ? ` for ${context.type}` : ''
279 }: `
280 ) +
281 require('util').inspect(
282 typeof this.options.debugWebpack === 'string'
283 ? get(webpackConfig, this.options.debugWebpack)
284 : webpackConfig,
285 {
286 depth: null,
287 colors: true
288 }
289 )
290 )
291 }
292 return webpackConfig
293 }
294
295 resolveCwd(...args) {
296 return path.resolve(this.options.cwd, ...args)
297 }
298
299 inferDefaultValue(value) {
300 if (typeof value !== 'undefined') {
301 return value
302 }
303 return this.command === 'build'
304 }
305}