UNPKG

8.31 kBJavaScriptView Raw
1const path = require('path')
2const fs = require('fs-extra')
3const merge = require('lodash.merge')
4const logger = require('@poi/cli-utils/logger')
5const loadPlugins = require('./utils/load-plugins')
6const Plugin = require('./plugin')
7const loadConfig = require('./utils/load-config')
8const Hooks = require('./hooks')
9const loadPkg = require('./utils/load-pkg')
10
11class Poi {
12 constructor(options = {}, config) {
13 this.options = Object.assign({}, options, {
14 cliArgs: options.cliArgs || process.argv.slice(3),
15 baseDir: path.resolve(options.baseDir || '.'),
16 cleanOutDir:
17 options.cleanOutDir === undefined ? true : options.cleanOutDir
18 })
19 this.hooks = new Hooks()
20 this.config = Object.assign({}, config)
21 this.internals = {}
22 this.buildId = Math.random()
23 .toString(36)
24 .substring(7)
25
26 const { command } = this.options
27 process.env.POI_COMMAND = command
28
29 logger.setOptions({
30 debug: this.options.debug
31 })
32
33 this.pkg = loadPkg({ cwd: this.options.baseDir })
34
35 // Load .env file before loading config file
36 const envs = this.loadEnvs()
37
38 if (this.options.configFile !== false) {
39 const res = loadConfig.loadSync({
40 files:
41 typeof this.options.configFile === 'string'
42 ? [this.options.configFile]
43 : ['poi.config.js', 'package.json'],
44 cwd: this.options.baseDir,
45 packageKey: 'poi'
46 })
47 if (res.path) {
48 this.configFilePath = res.path
49 this.config = merge(res.data, this.config)
50 logger.debug(`Poi config file: ${this.configFilePath}`)
51 } else {
52 logger.debug('Poi is not using any config file')
53 }
54 }
55
56 let { entry } = this.config
57 if (!entry || (Array.isArray(entry) && entry.length === 0)) {
58 entry = './index.js'
59 }
60
61 this.config = Object.assign(
62 {
63 // Default values2
64 outDir: 'dist',
65 target: 'app',
66 publicPath: '/',
67 pluginOptions: {},
68 sourceMap: true,
69 babel: {}
70 },
71 this.config,
72 {
73 // Proper overrides
74 entry,
75 css: Object.assign(
76 {
77 loaderOptions: {}
78 },
79 this.config.css
80 ),
81 devServer: Object.assign(
82 {
83 host: this.config.host || process.env.HOST || '0.0.0.0',
84 port: this.config.port || process.env.PORT || 4000
85 },
86 this.config.devServer
87 )
88 }
89 )
90
91 // Merge envs with this.config.envs
92 // Allow to embed these env variables in app code
93 this.setAppEnvs(envs)
94
95 this.cli = require('cac')({ bin: 'poi' })
96 }
97
98 hook(name, fn) {
99 return this.hooks.add(name, fn)
100 }
101
102 resolve(...args) {
103 return path.resolve(this.options.baseDir, ...args)
104 }
105
106 prepare() {
107 this.applyPlugins()
108 logger.debug('App envs', JSON.stringify(this.getEnvs(), null, 2))
109
110 if (this.internals.watchPkg) {
111 this.pkg.watch()
112 }
113 }
114
115 loadEnvs() {
116 const { NODE_ENV } = process.env
117 const dotenvPath = this.resolve('.env')
118 const dotenvFiles = [
119 NODE_ENV && `${dotenvPath}.${NODE_ENV}.local`,
120 NODE_ENV && `${dotenvPath}.${NODE_ENV}`,
121 // Don't include `.env.local` for `test` environment
122 // since normally you expect tests to produce the same
123 // results for everyone
124 NODE_ENV !== 'test' && `${dotenvPath}.local`,
125 dotenvPath
126 ].filter(Boolean)
127
128 const envs = {}
129
130 dotenvFiles.forEach(dotenvFile => {
131 if (fs.existsSync(dotenvFile)) {
132 logger.debug('Using env file:', dotenvFile)
133 const config = require('dotenv-expand')(
134 require('dotenv').config({
135 path: dotenvFile
136 })
137 )
138 // Collect all variables from .env file
139 Object.assign(envs, config.parsed)
140 }
141 })
142
143 // Collect those temp envs starting with POI_ too
144 for (const name of Object.keys(process.env)) {
145 if (name.startsWith('POI_')) {
146 envs[name] = process.env[name]
147 }
148 }
149
150 return envs
151 }
152
153 // Get envs that will be embed in app code
154 getEnvs() {
155 return Object.assign({}, this.config.envs, {
156 NODE_ENV:
157 this.internals.mode === 'production' ? 'production' : 'development',
158 PUBLIC_PATH: this.config.publicPath
159 })
160 }
161
162 setAppEnvs(envs) {
163 this.config.envs = Object.assign({}, this.config.envs, envs)
164 return this
165 }
166
167 applyPlugins() {
168 const plugins = [
169 require.resolve('./plugins/config-base'),
170 require.resolve('./plugins/config-html'),
171 {
172 resolve: require.resolve('./plugins/config-electron'),
173 options: this.config.electron
174 },
175 require.resolve('./plugins/command-build'),
176 require.resolve('./plugins/command-dev'),
177 require.resolve('./plugins/command-watch'),
178 require.resolve('./plugins/command-why'),
179 ...(this.config.plugins || [])
180 ]
181
182 this.plugins = loadPlugins(plugins, this.options.baseDir)
183 for (const plugin of this.plugins) {
184 if (plugin.resolve.commandInternals) {
185 this.setCommandInternals(plugin.resolve)
186 }
187 }
188 for (const plugin of this.plugins) {
189 const { resolve, options } = plugin
190 const api = new Plugin(this, resolve.name)
191 resolve.apply(api, options)
192 }
193 }
194
195 setCommandInternals({ commandInternals, name }) {
196 for (const command of Object.keys(commandInternals)) {
197 if (this.options.command === command) {
198 const internals = commandInternals[command]
199 this.internals = Object.assign({}, this.internals, internals)
200 if (internals.mode) {
201 this.setAppEnvs({
202 POI_MODE: internals.mode
203 })
204 logger.debug(
205 `Plugin '${name}' sets the current command internals to '${JSON.stringify(
206 this.internals,
207 null,
208 2
209 )}'`
210 )
211 }
212 }
213 }
214 return this
215 }
216
217 async run() {
218 this.prepare()
219 await this.hooks.invokePromise('beforeRun')
220 return new Promise(resolve => {
221 const { input, flags } = this.cli.parse([
222 this.options.command,
223 ...this.options.cliArgs
224 ])
225 if (!this.cli.matchedCommand && !flags.help) {
226 if (input[0]) {
227 logger.error(
228 'Unknown command, run `poi --help` to get a list of available commands.'
229 )
230 } else {
231 this.cli.showHelp()
232 }
233 return resolve()
234 }
235 this.cli.on('executed', () => {
236 if (this._inspectWebpackConfigPath) {
237 require('@poi/dev-utils/open')(this._inspectWebpackConfigPath)
238 }
239 resolve()
240 })
241 })
242 }
243
244 resolveWebpackConfig(opts) {
245 const WebpackChain = require('webpack-chain')
246 const config = new WebpackChain()
247
248 opts = Object.assign({ type: 'client' }, opts)
249
250 this.hooks.invoke('chainWebpack', config, opts)
251
252 if (this.config.chainWebpack) {
253 this.config.chainWebpack(config, opts)
254 }
255
256 if (this.options.inspectWebpack) {
257 this._inspectWebpackConfigPath = path.join(
258 require('os').tmpdir(),
259 `poi-inspect-webpack-config-${this.buildId}.js`
260 )
261 fs.appendFileSync(
262 this._inspectWebpackConfigPath,
263 `//${JSON.stringify(opts)}\nconst ${opts.type} = ${config.toString()}\n`
264 )
265 }
266
267 return config.toConfig()
268 }
269
270 createWebpackCompiler(webpackConfig) {
271 return require('webpack')(webpackConfig)
272 }
273
274 runWebpack(webpackConfig) {
275 const compiler = this.createWebpackCompiler(webpackConfig)
276 return require('@poi/dev-utils/runCompiler')(compiler)
277 }
278
279 async bundle() {
280 const webpackConfig = this.resolveWebpackConfig()
281 if (this.options.cleanOutDir) {
282 await fs.remove(webpackConfig.output.path)
283 }
284 return this.runWebpack(webpackConfig)
285 }
286
287 hasDependency(name, type = 'all') {
288 const prodDeps = Object.keys(this.pkg.data.dependencies || {})
289 const devDeps = Object.keys(this.pkg.data.devDependencies || {})
290 if (type === 'all') {
291 return prodDeps.concat(devDeps).includes(name)
292 }
293 if (type === 'prod') {
294 return prodDeps.includes(name)
295 }
296 if (type === 'dev') {
297 return devDeps.includes(name)
298 }
299 throw new Error(`Unknow dep type: ${type}`)
300 }
301}
302
303module.exports = (...args) => new Poi(...args)