UNPKG

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