UNPKG

8.65 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 transpileOnly: true,
159 compilerOptions: {
160 module: 'commonjs',
161 moduleResolution: 'node'
162 }
163 })
164 const config = require(filepath)
165 return config.default || config
166 }
167 })
168 const poiConfig = await useConfig.load()
169 if (poiConfig.path) {
170 logger.debug('poi config path', poiConfig.path)
171 this.configFile = poiConfig.path
172 config = poiConfig.config
173 } else if (this.options.config) {
174 // Config file was specified but not found
175 throw new PoiError(
176 `Config file was not found at ${this.options.config}`
177 )
178 }
179 }
180
181 this.options = merge(
182 typeof config === 'function' ? config(this.options) : config,
183 this.options
184 )
185 this.options = await handleOptions(this)
186
187 logger.inspect('poi options', this.options)
188
189 // Register our internal plugins
190 this.registerPlugin(require('./plugins/baseConfig'))
191 this.registerPlugin(require('./plugins/develop'))
192 this.registerPlugin(require('./plugins/build'))
193 this.registerPlugin(require('./plugins/watch'))
194
195 // Register user plugins
196 if (this.options.plugins) {
197 this.registerPlugins(this.options.plugins)
198 }
199
200 // Call plugins
201 if (this.plugins.size > 0) {
202 for (const plugin of this.plugins) {
203 plugin(this)
204 }
205 }
206
207 // Add options.chainWebpack to the end
208 const chainWebpack = this.options.chainWebpack || this.options.extendWebpack
209 if (chainWebpack) {
210 logger.debug('Use chainWebpack defined in your config file')
211 this.chainWebpack(chainWebpack)
212 }
213
214 const configureWebpack =
215 this.options.configureWebpack || this.options.webpack
216 if (configureWebpack) {
217 logger.debug('Use configureWebpack defined in your config file')
218 this.configureWebpack(configureWebpack)
219 }
220 }
221
222 async run() {
223 await this.prepare()
224 const res = await this.cli.runCommand()
225 this.watchRun(res)
226 return res
227 }
228
229 watchRun({ devServer, webpackWatcher } = {}) {
230 if (
231 this.options.restartOnFileChanges === false ||
232 !['watch', 'develop'].includes(this.command) ||
233 this.cli.willShowHelp()
234 ) {
235 return
236 }
237
238 const filesToWatch = [
239 ...[].concat(this.configFile || ['poi.config.js', '.poirc']),
240 ...[].concat(this.options.restartOnFileChanges || [])
241 ]
242
243 if (filesToWatch.length === 0) return
244
245 logger.debug('watching files', filesToWatch.join(', '))
246
247 const watcher = chokidar.watch(filesToWatch, {
248 ignoreInitial: true
249 })
250 const handleEvent = filepath => {
251 logger.warn(`Restarting due to changes made in: ${filepath}`)
252 watcher.close()
253 if (devServer) {
254 devServer.close(() => this.rerun())
255 } else if (webpackWatcher) {
256 webpackWatcher.close()
257 this.rerun()
258 }
259 }
260 watcher.on('change', handleEvent)
261 watcher.on('add', handleEvent)
262 watcher.on('unlink', handleEvent)
263 }
264
265 createWebpackConfig({ config, context, chainWebpack } = {}) {
266 config = config || new Config()
267 context = Object.assign({ type: 'client', command: this.command }, context)
268 this.hooks.invoke('chainWebpack', config, context)
269 if (chainWebpack) {
270 chainWebpack(config, context)
271 }
272 let webpackConfig = config.toConfig()
273 this.hooks.invoke('configureWebpack', userConfig => {
274 if (typeof userConfig === 'object') {
275 return webpackMerge(webpackConfig, userConfig)
276 }
277 if (typeof userConfig === 'function') {
278 return userConfig(webpackConfig, context) || webpackConfig
279 }
280 })
281 if (this.options.debugWebpack) {
282 logger.log(
283 chalk.bold(
284 `webpack config${
285 context && context.type ? ` for ${context.type}` : ''
286 }: `
287 ) +
288 require('util').inspect(
289 typeof this.options.debugWebpack === 'string'
290 ? get(webpackConfig, this.options.debugWebpack)
291 : webpackConfig,
292 {
293 depth: null,
294 colors: true
295 }
296 )
297 )
298 }
299 return webpackConfig
300 }
301
302 resolveCwd(...args) {
303 return path.resolve(this.options.cwd, ...args)
304 }
305
306 inferDefaultValue(value) {
307 if (typeof value !== 'undefined') {
308 return value
309 }
310 return this.command === 'build'
311 }
312}