UNPKG

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