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-html') },
188 { resolve: require.resolve('./plugins/config-electron') },
189 { resolve: require.resolve('./plugins/config-misc-loaders') },
190 { resolve: require.resolve('./plugins/watch') },
191 { resolve: require.resolve('./plugins/serve') },
192 { resolve: require.resolve('./plugins/eject-html') }
193 ]
194 .concat(mergePlugins(configPlugins, cliPlugins))
195 .map(plugin => {
196 if (typeof plugin.resolve === 'string') {
197 plugin._resolve = plugin.resolve
198 plugin.resolve = require(plugin.resolve)
199 }
200 return plugin
201 })
202
203 // Run plugin's `filterPlugins` method
204 for (const plugin of this.plugins) {
205 if (plugin.resolve.filterPlugins) {
206 this.plugins = plugin.resolve.filterPlugins(
207 this.plugins,
208 plugin.resolve.options
209 )
210 }
211 }
212
213 // Run plugin's `apply` method
214 for (const plugin of this.plugins) {
215 if (plugin.resolve.apply) {
216 logger.debug(`Using plugin: \`${plugin.resolve.name}\``)
217 if (plugin._resolve) {
218 logger.debug(`location: ${plugin._resolve}`)
219 }
220 plugin.resolve.apply(this, plugin.resolve.options)
221 }
222 }
223 }
224
225 hasPlugin(name) {
226 return (
227 this.plugins &&
228 this.plugins.find(plugin => {
229 return plugin.resolve.name === name
230 })
231 )
232 }
233
234 hook(name, fn) {
235 this.hooks.add(name, fn)
236 return this
237 }
238
239 registerTestRunner(name, runner) {
240 if (this.testRunners.has(name)) {
241 throw new PoiError({
242 message: `Test runner "${name}" has already been registered!`
243 })
244 }
245 this.testRunners.set(name, runner)
246 return this
247 }
248
249 resolveCwd(...args) {
250 return path.resolve(this.cwd, ...args)
251 }
252
253 resolveOutDir(...args) {
254 return this.resolveCwd(this.config.output.dir, ...args)
255 }
256
257 async run() {
258 this.cli.parse(this.args, { run: false })
259
260 logger.debug('CLI args', this.cli.args)
261 logger.debug('CLI options', this.cli.options)
262
263 const cliConfig = this.createConfigFromCLIOptions()
264 logger.debug(`Config from CLI options`, cliConfig)
265
266 this.config = validateConfig(this, merge({}, this.config, cliConfig))
267
268 this.hooks.invoke('createConfig', this.config)
269
270 await this.cli.runMatchedCommand()
271
272 await this.hooks.invokePromise('afterRun')
273 }
274
275 createConfigFromCLIOptions() {
276 const {
277 minimize,
278 sourceMap,
279 format,
280 moduleName,
281 outDir,
282 publicUrl,
283 target,
284 clean,
285 parallel,
286 cache,
287 jsx,
288 extractCss,
289 hot,
290 host,
291 port,
292 open,
293 proxy,
294 fileNames,
295 html,
296 publicFolder
297 } = this.cli.options
298 return {
299 entry: this.cli.args.length > 0 ? this.cli.args : undefined,
300 output: {
301 minimize,
302 sourceMap,
303 format,
304 moduleName,
305 dir: outDir,
306 publicUrl,
307 target,
308 clean,
309 fileNames,
310 html
311 },
312 parallel,
313 cache,
314 publicFolder,
315 babel: {
316 jsx
317 },
318 css: {
319 extract: extractCss
320 },
321 devServer: {
322 hot,
323 host,
324 port,
325 open,
326 proxy
327 }
328 }
329 }
330
331 createWebpackChain(opts) {
332 const WebpackChain = require('./utils/WebpackChain')
333
334 opts = Object.assign({ type: 'client', mode: this.mode }, opts)
335
336 const config = new WebpackChain({
337 configureWebpack: this.config.configureWebpack,
338 opts
339 })
340
341 require('./webpack/webpack.config')(config, this)
342
343 this.hooks.invoke('createWebpackChain', config, opts)
344
345 if (this.config.chainWebpack) {
346 this.config.chainWebpack(config, opts)
347 }
348
349 if (this.cli.options.inspectWebpack) {
350 const inspect = () => {
351 const id = Math.random()
352 .toString(36)
353 .substring(7)
354 const outFile = path.join(
355 os.tmpdir(),
356 `poi-inspect-webpack-config-${id}.js`
357 )
358 const configString = `// ${JSON.stringify(
359 opts
360 )}\nvar config = ${config.toString()}\n\n`
361 fs.writeFileSync(outFile, configString, 'utf8')
362 require('@poi/dev-utils/open')(outFile, {
363 wait: false
364 })
365 }
366
367 config.plugin('inspect-webpack').use(
368 class InspectWebpack {
369 apply(compiler) {
370 compiler.hooks.afterEnvironment.tap('inspect-webpack', inspect)
371 }
372 }
373 )
374 }
375
376 return config
377 }
378
379 runCompiler(compiler, watch) {
380 return new Promise((resolve, reject) => {
381 if (watch) {
382 compiler.watch({}, err => {
383 if (err) return reject(err)
384 resolve()
385 })
386 } else {
387 compiler.run((err, stats) => {
388 if (err) return reject(err)
389 resolve(stats)
390 })
391 }
392 })
393 }
394
395 createWebpackCompiler(config) {
396 return require('webpack')(config)
397 }
398
399 getCacheConfig(dir, keys, files) {
400 let content = ''
401 if (files) {
402 const file = this.configLoader.resolve({ files: [].concat(files) })
403 if (file) {
404 content = fs.readFileSync(file, 'utf8')
405 }
406 }
407
408 return {
409 cacheDirectory: this.resolveCwd('node_modules/.cache', dir),
410 cacheIdentifier: `${JSON.stringify(keys)}::${content}`
411 }
412 }
413
414 localResolve(name, cwd = this.cwd) {
415 return resolveFrom.silent(cwd, name)
416 }
417
418 localRequire(name, cwd) {
419 const resolved = this.localResolve(name, cwd)
420 return resolved ? require(resolved) : null
421 }
422}