UNPKG

9.43 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')
16
17module.exports = class PoiCore {
18 constructor(
19 args = process.argv,
20 {
21 defaultConfigFiles = [
22 'poi.config.js',
23 '.poirc',
24 '.poirc.json',
25 '.poirc.js'
26 ],
27 extendConfigLoader,
28 config: externalConfig
29 } = {}
30 ) {
31 this.args = args
32 this.logger = logger
33 this.spinner = spinner
34 this.PoiError = PoiError
35 // For plugins, it's only used in onCreateCLI hook
36 this.parsedArgs = parseArgs(args.slice(2))
37 this.hooks = new Hooks()
38 this.testRunners = new Map()
39
40 if (this.parsedArgs.has('debug')) {
41 logger.setOptions({ debug: true })
42 }
43
44 this.mode = this.parsedArgs.getValue('mode')
45 if (!this.mode) {
46 this.mode = 'development'
47 }
48
49 if (this.parsedArgs.has('prod') || this.parsedArgs.has('production')) {
50 this.mode = 'production'
51 }
52
53 if (this.parsedArgs.has('test')) {
54 this.mode = 'test'
55 }
56
57 this.cwd = this.parsedArgs.getValue('cwd')
58 if (!this.cwd) {
59 this.cwd = process.cwd()
60 }
61
62 this.configLoader = createConfigLoader(this.cwd)
63
64 // For other tools that use Poi under the hood
65 if (extendConfigLoader) {
66 extendConfigLoader(this.configLoader)
67 }
68
69 // Load .env files
70 loadEnvs(this.mode, this.resolveCwd('.env'))
71
72 if (!process.env.NODE_ENV) {
73 process.env.NODE_ENV = this.mode
74 }
75
76 this.webpackUtils = new WebpackUtils(this)
77
78 // Try to load config file
79 if (externalConfig || this.parsedArgs.has('no-config')) {
80 logger.debug('Poi config file was disabled')
81 this.config = externalConfig
82 } else {
83 const configFiles = this.parsedArgs.has('config')
84 ? [this.parsedArgs.getValue('config')]
85 : defaultConfigFiles
86 const { path: configPath, data: config } = this.configLoader.load({
87 files: configFiles
88 })
89 if (configPath) {
90 logger.debug(`Using Poi config file:`, configPath)
91 } else {
92 logger.debug(`Not using any Poi config file`)
93 }
94 this.configPath = configPath
95 this.config = config || {}
96 }
97
98 this.pkg = this.configLoader.load({
99 files: ['package.json']
100 })
101 this.pkg.data = this.pkg.data || {}
102
103 // Apply plugins
104 this.applyPlugins()
105
106 this.initCLI()
107 }
108
109 get isProd() {
110 return this.mode === 'production'
111 }
112
113 initCLI() {
114 const cli = (this.cli = cac())
115 const command = (this.defaultCommand = cli
116 .command('[...entries]', 'Entry files to start bundling', {
117 ignoreOptionDefaultValue: true
118 })
119 .usage('[...entries] [options]')).action(async () => {
120 logger.debug(`Using default handler`)
121 const config = this.createWebpackConfig()
122 const compiler = this.createWebpackCompiler(config.toConfig())
123 await this.runCompiler(compiler)
124 })
125
126 this.hooks.invoke('onCreateCLI', { command, args: this.parsedArgs })
127
128 // Global options
129 cli
130 .option('--mode <mode>', 'Set mode', 'development')
131 .option('--prod, --production', 'Alias for --mode production')
132 .option('--test', 'Alias for --mode test')
133 .option('--no-config', 'Disable config file')
134 .option('--config <path>', 'Set the path to config file')
135 .option('--debug', 'Show debug logs')
136 .option('--inspect-webpack', 'Inspect webpack config in your editor')
137 .version(require('../package').version)
138 .help()
139 }
140
141 hasDependency(name) {
142 return [
143 ...Object.keys(this.pkg.data.dependencies || {}),
144 ...Object.keys(this.pkg.data.devDependencies || {})
145 ].includes(name)
146 }
147
148 /**
149 * @private
150 * @returns {void}
151 */
152 applyPlugins() {
153 this.plugins = [
154 require.resolve('./plugins/command-options'),
155 require.resolve('./plugins/config-babel'),
156 require.resolve('./plugins/config-vue'),
157 require.resolve('./plugins/config-css'),
158 require.resolve('./plugins/config-font'),
159 require.resolve('./plugins/config-image'),
160 require.resolve('./plugins/config-html'),
161 require.resolve('./plugins/config-electron'),
162 require.resolve('./plugins/config-misc-loaders'),
163 require.resolve('./plugins/watch'),
164 require.resolve('./plugins/serve'),
165 require.resolve('./plugins/eject-html')
166 ]
167 .concat(this.config.plugins || [])
168 .map(v => {
169 if (typeof v === 'string') {
170 v = { resolve: v }
171 }
172 if (typeof v.resolve === 'string') {
173 v = Object.assign({
174 resolve: require(resolveFrom(this.resolveCwd(), v.resolve))
175 })
176 }
177 return v
178 })
179 .filter(Boolean)
180
181 // Run plugin's `filterPlugins` method
182 for (const plugin of this.plugins) {
183 if (plugin.resolve.filterPlugins) {
184 this.plugins = plugin.resolve.filterPlugins(
185 this.plugins,
186 plugin.resolve.options
187 )
188 }
189 }
190
191 // Run plugin's `apply` method
192 for (const plugin of this.plugins) {
193 if (plugin.resolve.apply) {
194 logger.debug(`Using plugin: "${plugin.resolve.name}"`)
195 plugin.resolve.apply(this, plugin.resolve.options)
196 }
197 }
198 }
199
200 hook(name, fn) {
201 this.hooks.add(name, fn)
202 return this
203 }
204
205 registerTestRunner(name, runner) {
206 if (this.testRunners.has(name)) {
207 throw new PoiError({
208 message: `Test runner "${name}" has already been registered!`
209 })
210 }
211 this.testRunners.set(name, runner)
212 return this
213 }
214
215 resolveCwd(...args) {
216 return path.resolve(this.cwd, ...args)
217 }
218
219 resolveOutDir(...args) {
220 return this.resolveCwd(this.config.output.dir, ...args)
221 }
222
223 async run() {
224 this.cli.parse(this.args, { run: false })
225
226 logger.debug('CLI args', this.cli.args)
227 logger.debug('CLI options', this.cli.options)
228
229 const cliConfig = this.createConfigFromCLIOptions()
230 logger.debug(`Config from CLI options`, cliConfig)
231
232 this.config = validateConfig(this, merge(this.config, cliConfig))
233
234 await this.cli.runMatchedCommand()
235 }
236
237 createConfigFromCLIOptions() {
238 const {
239 minimize,
240 sourceMap,
241 format,
242 moduleName,
243 outDir,
244 publicUrl,
245 target,
246 clean,
247 parallel,
248 cache,
249 jsx,
250 extractCss,
251 hot,
252 host,
253 port,
254 open,
255 proxy,
256 fileNames,
257 html
258 } = this.cli.options
259 return {
260 entry: this.cli.args.length > 0 ? this.cli.args : undefined,
261 output: {
262 minimize,
263 sourceMap,
264 format,
265 moduleName,
266 dir: outDir,
267 publicUrl,
268 target,
269 clean,
270 fileNames,
271 html
272 },
273 parallel,
274 cache,
275 babel: {
276 jsx
277 },
278 css: {
279 extract: extractCss
280 },
281 devServer: {
282 hot,
283 host,
284 port,
285 open,
286 proxy
287 }
288 }
289 }
290
291 createWebpackConfig(opts) {
292 const WebpackChain = require('webpack-chain')
293
294 const config = new WebpackChain()
295
296 require('./webpack/webpack.config')(config, this)
297
298 opts = Object.assign({ type: 'client' }, opts)
299
300 this.hooks.invoke('onCreateWebpackConfig', config, opts)
301
302 if (this.config.chainWebpack) {
303 this.config.chainWebpack(config, opts)
304 }
305
306 if (this.cli.options.inspectWebpack) {
307 const configString = `// ${JSON.stringify(
308 opts
309 )}\n${config.toString()}\n\n`
310
311 config.plugin('inspect-webpack').use(
312 class InspectWebpack {
313 apply(compiler) {
314 compiler.hooks.beforeRun.tapPromise('inspect-webpack', async () => {
315 const id = Math.random()
316 .toString(36)
317 .substring(7)
318 const outFile = path.join(
319 os.tmpdir(),
320 `poi-inspect-webpack-config-${id}.js`
321 )
322 await fs.writeFile(outFile, configString, 'utf8')
323 require('@poi/dev-utils/open')(outFile)
324 })
325 }
326 }
327 )
328 }
329
330 return config
331 }
332
333 runCompiler(compiler) {
334 return new Promise((resolve, reject) => {
335 compiler.run((err, stats) => {
336 if (err) return reject(err)
337 resolve(stats)
338 })
339 })
340 }
341
342 createWebpackCompiler(config) {
343 return require('webpack')(config)
344 }
345
346 getCacheConfig(dir, keys, files) {
347 let content = ''
348 if (files) {
349 const file = this.configLoader.resolve({ files: [].concat(files) })
350 if (file) {
351 content = fs.readFileSync(file, 'utf8')
352 }
353 }
354
355 return {
356 cacheDirectory: this.resolveCwd('node_modules/.cache', dir),
357 cacheIdentifier: `${JSON.stringify(keys)}::${content}`
358 }
359 }
360
361 localResolve(name, cwd = this.cwd) {
362 return resolveFrom.silent(cwd, name)
363 }
364
365 localRequire(name, cwd) {
366 const resolved = this.localResolve(name, cwd)
367 return resolved ? require(resolved) : null
368 }
369}