UNPKG

11.9 kBJavaScriptView Raw
1const os = require('os')
2const path = require('path')
3const fs = require('fs-extra')
4const resolveFrom = require('resolve-from')
5const cac = require('cac')
6const merge = require('lodash.merge')
7const logger = require('@poi/logger')
8const Hooks = require('./Hooks')
9const WebpackUtils = require('./WebpackUtils')
10const createConfigLoader = require('./utils/createConfigLoader')
11const loadEnvs = require('./utils/loadEnvs')
12const parseArgs = require('./utils/parseArgs')
13const PoiError = require('./utils/PoiError')
14const spinner = require('./utils/spinner')
15const validateConfig = require('./utils/validateConfig')
16const { normalizePlugins, mergePlugins } = require('./utils/plugins')
17
18module.exports = class PoiCore {
19 constructor(
20 args = process.argv,
21 {
22 defaultConfigFiles = [
23 'poi.config.js',
24 'package.json',
25 '.poirc',
26 '.poirc.json',
27 '.poirc.js'
28 ],
29 extendConfigLoader,
30 config: externalConfig
31 } = {}
32 ) {
33 this.args = args
34 this.logger = logger
35 this.spinner = spinner
36 this.PoiError = PoiError
37 // For plugins, it's only used in createCLI hook
38 this.parsedArgs = parseArgs(args)
39 this.hooks = new Hooks()
40 this.testRunners = new Map()
41
42 if (this.parsedArgs.has('debug')) {
43 logger.setOptions({ debug: true })
44 }
45
46 this.mode = this.parsedArgs.get('mode')
47 if (!this.mode) {
48 this.mode = 'development'
49 }
50
51 if (this.parsedArgs.has('prod') || this.parsedArgs.has('production')) {
52 this.mode = 'production'
53 }
54
55 if (this.parsedArgs.has('test')) {
56 this.mode = 'test'
57 }
58
59 this.cwd = this.parsedArgs.get('cwd')
60 if (!this.cwd) {
61 this.cwd = process.cwd()
62 }
63
64 this.configLoader = createConfigLoader(this.cwd)
65
66 // For other tools that use Poi under the hood
67 if (extendConfigLoader) {
68 extendConfigLoader(this.configLoader)
69 }
70
71 // Load .env files
72 loadEnvs(this.mode, this.resolveCwd('.env'))
73
74 if (!process.env.NODE_ENV) {
75 process.env.NODE_ENV = this.mode
76 }
77
78 this.webpackUtils = new WebpackUtils(this)
79
80 // Try to load config file
81 if (externalConfig || this.parsedArgs.get('config') === false) {
82 logger.debug('Poi config file was disabled')
83 this.config = externalConfig || {}
84 } else {
85 const configFiles =
86 typeof this.parsedArgs.get('config') === 'string'
87 ? [this.parsedArgs.get('config')]
88 : defaultConfigFiles
89 const { path: configPath, data: configFn } = this.configLoader.load({
90 files: configFiles,
91 packageKey: 'poi'
92 })
93 if (configPath) {
94 logger.debug(`Using Poi config file:`, configPath)
95 } else {
96 logger.debug(`Not using any Poi config file`)
97 }
98 this.configPath = configPath
99 this.config =
100 typeof configFn === 'function'
101 ? configFn(this.parsedArgs.options)
102 : configFn
103 this.config = this.config || {}
104 }
105
106 this.pkg = this.configLoader.load({
107 files: ['package.json']
108 })
109 this.pkg.data = this.pkg.data || {}
110
111 // Apply plugins
112 this.applyPlugins()
113
114 this.initCLI()
115 }
116
117 get isProd() {
118 return this.mode === 'production'
119 }
120
121 initCLI() {
122 const cli = (this.cli = cac())
123 const command = (this.defaultCommand = cli
124 .command('[...entries]', 'Entry files to start bundling', {
125 ignoreOptionDefaultValue: true
126 })
127 .usage('[...entries] [options]')).action(async () => {
128 logger.debug(`Using default handler`)
129 const chain = this.createWebpackChain()
130 const compiler = this.createWebpackCompiler(chain.toConfig())
131 await this.runCompiler(compiler)
132 })
133
134 this.hooks.invoke('createCLI', { command, args: this.parsedArgs })
135
136 // Global options
137 cli
138 .option('--mode <mode>', 'Set mode', 'development')
139 .option('--prod, --production', 'Alias for --mode production')
140 .option('--test', 'Alias for --mode test')
141 .option('--no-config', 'Disable config file')
142 .option('--config <path>', 'Set the path to config file')
143 .option(
144 '--plugin, --plugins <plugin>',
145 'Add a plugin (can be used for multiple times)'
146 )
147 .option('--debug', 'Show debug logs')
148 .option('--inspect-webpack', 'Inspect webpack config in your editor')
149 .version(require('../package').version)
150 .help(sections => {
151 for (const section of sections) {
152 if (section.title && section.title.includes('For more info')) {
153 const body = section.body.split('\n')
154 body.shift()
155 body.unshift(
156 ` $ ${cli.name} --help`,
157 ` $ ${cli.name} --serve --help`,
158 ` $ ${cli.name} --prod --help`
159 )
160 section.body = body.join('\n')
161 }
162 }
163 })
164 }
165
166 hasDependency(name) {
167 return [
168 ...Object.keys(this.pkg.data.dependencies || {}),
169 ...Object.keys(this.pkg.data.devDependencies || {})
170 ].includes(name)
171 }
172
173 /**
174 * @private
175 * @returns {void}
176 */
177 applyPlugins() {
178 const cwd = this.resolveCwd()
179 const cliPlugins = normalizePlugins(
180 this.parsedArgs.get('plugin') || this.parsedArgs.get('plugins'),
181 cwd
182 )
183 const configPlugins = normalizePlugins(this.config.plugins, cwd)
184
185 this.plugins = [
186 { resolve: require.resolve('./plugins/command-options') },
187 { resolve: require.resolve('./plugins/config-babel') },
188 { resolve: require.resolve('./plugins/config-vue') },
189 { resolve: require.resolve('./plugins/config-css') },
190 { resolve: require.resolve('./plugins/config-font') },
191 { resolve: require.resolve('./plugins/config-image') },
192 { resolve: require.resolve('./plugins/config-eval') },
193 { resolve: require.resolve('./plugins/config-html') },
194 { resolve: require.resolve('./plugins/config-electron') },
195 { resolve: require.resolve('./plugins/config-misc-loaders') },
196 { resolve: require.resolve('./plugins/config-reason') },
197 { resolve: require.resolve('./plugins/watch') },
198 { resolve: require.resolve('./plugins/serve') },
199 { resolve: require.resolve('./plugins/eject-html') }
200 ]
201 .concat(mergePlugins(configPlugins, cliPlugins))
202 .map(plugin => {
203 if (typeof plugin.resolve === 'string') {
204 plugin._resolve = plugin.resolve
205 plugin.resolve = require(plugin.resolve)
206 }
207 return plugin
208 })
209 .filter(plugin => {
210 return plugin.resolve.when ? plugin.resolve.when(this) : true
211 })
212
213 // Run plugin's `filterPlugins` method
214 for (const plugin of this.plugins) {
215 if (plugin.resolve.filterPlugins) {
216 this.plugins = plugin.resolve.filterPlugins(
217 this.plugins,
218 plugin.options
219 )
220 }
221 }
222
223 // Run plugin's `apply` method
224 for (const plugin of this.plugins) {
225 if (plugin.resolve.apply) {
226 logger.debug(`Using plugin: \`${plugin.resolve.name}\``)
227 if (plugin._resolve) {
228 logger.debug(`location: ${plugin._resolve}`)
229 }
230 plugin.resolve.apply(this, plugin.options)
231 }
232 }
233 }
234
235 hasPlugin(name) {
236 return (
237 this.plugins &&
238 this.plugins.find(plugin => {
239 return plugin.resolve.name === name
240 })
241 )
242 }
243
244 hook(name, fn) {
245 this.hooks.add(name, fn)
246 return this
247 }
248
249 registerTestRunner(name, runner) {
250 if (this.testRunners.has(name)) {
251 throw new PoiError({
252 message: `Test runner "${name}" has already been registered!`
253 })
254 }
255 this.testRunners.set(name, runner)
256 return this
257 }
258
259 resolveCwd(...args) {
260 return path.resolve(this.cwd, ...args)
261 }
262
263 resolveOutDir(...args) {
264 return this.resolveCwd(this.config.output.dir, ...args)
265 }
266
267 async run() {
268 this.cli.parse(this.args, { run: false })
269
270 logger.debug('CLI args', this.cli.args)
271 logger.debug('CLI options', this.cli.options)
272
273 const cliConfig = this.createConfigFromCLIOptions()
274 logger.debug(`Config from CLI options`, cliConfig)
275
276 this.config = validateConfig(this, merge({}, this.config, cliConfig))
277
278 this.hooks.invoke('createConfig', this.config)
279
280 await this.cli.runMatchedCommand()
281
282 await this.hooks.invokePromise('afterRun')
283 }
284
285 createConfigFromCLIOptions() {
286 const {
287 minimize,
288 sourceMap,
289 format,
290 moduleName,
291 outDir,
292 publicUrl,
293 target,
294 clean,
295 parallel,
296 cache,
297 jsx,
298 extractCss,
299 hot,
300 host,
301 port,
302 open,
303 proxy,
304 fileNames,
305 html,
306 publicFolder
307 } = this.cli.options
308 return {
309 entry: this.cli.args.length > 0 ? this.cli.args : undefined,
310 output: {
311 minimize,
312 sourceMap,
313 format,
314 moduleName,
315 dir: outDir,
316 publicUrl,
317 target,
318 clean,
319 fileNames,
320 html
321 },
322 parallel,
323 cache,
324 publicFolder,
325 babel: {
326 jsx
327 },
328 css: {
329 extract: extractCss
330 },
331 devServer: {
332 hot,
333 host,
334 port,
335 open,
336 proxy
337 }
338 }
339 }
340
341 createWebpackChain(opts) {
342 const WebpackChain = require('./utils/WebpackChain')
343
344 opts = Object.assign({ type: 'client', mode: this.mode }, opts)
345
346 const config = new WebpackChain({
347 configureWebpack: this.config.configureWebpack,
348 opts
349 })
350
351 require('./webpack/webpack.config')(config, this)
352
353 this.hooks.invoke('createWebpackChain', config, opts)
354
355 if (this.config.chainWebpack) {
356 this.config.chainWebpack(config, opts)
357 }
358
359 if (this.cli.options.inspectWebpack) {
360 const inspect = () => {
361 const id = Math.random()
362 .toString(36)
363 .substring(7)
364 const outFile = path.join(
365 os.tmpdir(),
366 `poi-inspect-webpack-config-${id}.js`
367 )
368 const configString = `// ${JSON.stringify(
369 opts
370 )}\nvar config = ${config.toString()}\n\n`
371 fs.writeFileSync(outFile, configString, 'utf8')
372 require('@poi/dev-utils/open')(outFile, {
373 wait: false
374 })
375 }
376
377 config.plugin('inspect-webpack').use(
378 class InspectWebpack {
379 apply(compiler) {
380 compiler.hooks.afterEnvironment.tap('inspect-webpack', inspect)
381 }
382 }
383 )
384 }
385
386 return config
387 }
388
389 runCompiler(compiler, watch) {
390 return new Promise((resolve, reject) => {
391 if (watch) {
392 compiler.watch({}, err => {
393 if (err) return reject(err)
394 resolve()
395 })
396 } else {
397 compiler.run((err, stats) => {
398 if (err) return reject(err)
399 resolve(stats)
400 })
401 }
402 })
403 }
404
405 createWebpackCompiler(config) {
406 const compiler = require('webpack')(config)
407
408 // Override the .watch method so we can handle error here instead of letting WDM handle it
409 // And in fact we disabled WDM logger so errors will never show displayed there
410 const originalWatch = compiler.watch.bind(compiler)
411 compiler.watch = (options, cb) => {
412 return originalWatch(options, (err, stats) => {
413 if (err) {
414 throw err
415 }
416 cb(null, stats)
417 })
418 }
419
420 return compiler
421 }
422
423 getCacheConfig(dir, keys, files) {
424 let content = ''
425 if (files) {
426 const file = this.configLoader.resolve({ files: [].concat(files) })
427 if (file) {
428 content = fs.readFileSync(file, 'utf8')
429 }
430 }
431
432 return {
433 cacheDirectory: this.resolveCwd('node_modules/.cache', dir),
434 cacheIdentifier: `${JSON.stringify(keys)}::${content}`
435 }
436 }
437
438 localResolve(name, cwd = this.cwd) {
439 return resolveFrom.silent(cwd, name)
440 }
441
442 localRequire(name, cwd) {
443 const resolved = this.localResolve(name, cwd)
444 return resolved ? require(resolved) : null
445 }
446}