UNPKG

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