UNPKG

9.06 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 hasPlugin(name) {
123 return [...this.plugins].find(plugin => plugin.name === name)
124 }
125
126 registerPlugin(plugin) {
127 // For legacy plugins
128 if (typeof plugin === 'function') {
129 plugin = { name: 'unknown', apply: plugin }
130 }
131
132 if (plugin.command) {
133 if (this.cli.isCurrentCommand(plugin.command)) {
134 this.plugins.add(plugin)
135 }
136 } else {
137 this.plugins.add(plugin)
138 }
139 return this
140 }
141
142 registerPlugins(plugins) {
143 if (typeof plugins === 'string') {
144 plugins = [plugins]
145 }
146 for (const plugin of parseJsonConfig(plugins, { prefix: 'poi-plugin-' })) {
147 this.registerPlugin(plugin)
148 }
149 return this
150 }
151
152 async prepare() {
153 let config
154
155 // Load Poi config file
156 // You can disable this by setting `config` to false
157 if (this.options.config !== false) {
158 const useConfig = new UseConfig({
159 name: 'poi',
160 files: this.options.config
161 ? [this.options.config]
162 : [
163 '{name}.config.js',
164 '{name}.config.ts',
165 '.{name}rc',
166 'package.json'
167 ]
168 })
169 useConfig.addLoader({
170 test: /\.ts$/,
171 loader(filepath) {
172 require(path.resolve('node_modules', 'ts-node')).register({
173 transpileOnly: true,
174 compilerOptions: {
175 module: 'commonjs',
176 moduleResolution: 'node'
177 }
178 })
179 const config = require(filepath)
180 return config.default || config
181 }
182 })
183 const poiConfig = await useConfig.load()
184 if (poiConfig.path) {
185 logger.debug('poi config path', poiConfig.path)
186 this.configFile = poiConfig.path
187 config = poiConfig.config
188 } else if (this.options.config) {
189 // Config file was specified but not found
190 throw new PoiError(
191 `Config file was not found at ${this.options.config}`
192 )
193 }
194 }
195
196 this.options = merge(
197 typeof config === 'function' ? config(this.options) : config,
198 this.options
199 )
200 this.options = await handleOptions(this)
201
202 logger.inspect('poi options', this.options)
203
204 // Register our internal plugins
205 this.registerPlugin(require('./plugins/baseConfig'))
206 this.registerPlugin(require('./plugins/develop'))
207 this.registerPlugin(require('./plugins/build'))
208 this.registerPlugin(require('./plugins/watch'))
209
210 // Register user plugins
211 if (this.options.plugins) {
212 this.registerPlugins(this.options.plugins)
213 }
214
215 // Call plugins
216 if (this.plugins.size > 0) {
217 for (const plugin of this.plugins) {
218 logger.debug(`Using plugin: ${plugin.name}`)
219 plugin.apply(this)
220 }
221 }
222
223 // Add options.chainWebpack to the end
224 const chainWebpack = this.options.chainWebpack || this.options.extendWebpack
225 if (chainWebpack) {
226 logger.debug('Use chainWebpack defined in your config file')
227 this.chainWebpack(chainWebpack)
228 }
229
230 const configureWebpack =
231 this.options.configureWebpack || this.options.webpack
232 if (configureWebpack) {
233 logger.debug('Use configureWebpack defined in your config file')
234 this.configureWebpack(configureWebpack)
235 }
236 }
237
238 async run() {
239 await this.prepare()
240 const res = await this.cli.runCommand()
241 this.watchRun(res)
242 return res
243 }
244
245 watchRun({ devServer, webpackWatcher } = {}) {
246 if (
247 this.options.restartOnFileChanges === false ||
248 !['watch', 'develop'].includes(this.command) ||
249 this.cli.willShowHelp()
250 ) {
251 return
252 }
253
254 const filesToWatch = [
255 ...[].concat(this.configFile || ['poi.config.js', '.poirc']),
256 ...[].concat(this.options.restartOnFileChanges || [])
257 ]
258
259 if (filesToWatch.length === 0) return
260
261 logger.debug('watching files', filesToWatch.join(', '))
262
263 const watcher = chokidar.watch(filesToWatch, {
264 ignoreInitial: true
265 })
266 const handleEvent = filepath => {
267 logger.warn(`Restarting due to changes made in: ${filepath}`)
268 watcher.close()
269 if (devServer) {
270 devServer.close(() => this.rerun())
271 } else if (webpackWatcher) {
272 webpackWatcher.close()
273 this.rerun()
274 }
275 }
276 watcher.on('change', handleEvent)
277 watcher.on('add', handleEvent)
278 watcher.on('unlink', handleEvent)
279 }
280
281 createWebpackConfig({ config, context, chainWebpack } = {}) {
282 config = config || new Config()
283 context = Object.assign({ type: 'client', command: this.command }, context)
284 this.hooks.invoke('chainWebpack', config, context)
285 if (chainWebpack) {
286 chainWebpack(config, context)
287 }
288 let webpackConfig = config.toConfig()
289 this.hooks.invoke('configureWebpack', userConfig => {
290 if (typeof userConfig === 'object') {
291 return webpackMerge(webpackConfig, userConfig)
292 }
293 if (typeof userConfig === 'function') {
294 return userConfig(webpackConfig, context) || webpackConfig
295 }
296 })
297 if (this.options.debugWebpack) {
298 logger.log(
299 chalk.bold(
300 `webpack config${
301 context && context.type ? ` for ${context.type}` : ''
302 }: `
303 ) +
304 require('util').inspect(
305 typeof this.options.debugWebpack === 'string'
306 ? get(webpackConfig, this.options.debugWebpack)
307 : webpackConfig,
308 {
309 depth: null,
310 colors: true
311 }
312 )
313 )
314 }
315 return webpackConfig
316 }
317
318 resolveCwd(...args) {
319 return path.resolve(this.options.cwd, ...args)
320 }
321
322 inferDefaultValue(value) {
323 if (typeof value !== 'undefined') {
324 return value
325 }
326 return this.command === 'build'
327 }
328}