1 | const path = require('path')
|
2 | const fs = require('fs-extra')
|
3 | const merge = require('lodash.merge')
|
4 | const logger = require('@poi/cli-utils/logger')
|
5 | const loadPlugins = require('./utils/load-plugins')
|
6 | const Plugin = require('./plugin')
|
7 | const loadConfig = require('./utils/load-config')
|
8 | const Hooks = require('./hooks')
|
9 | const loadPkg = require('./utils/load-pkg')
|
10 |
|
11 | class 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 |
|
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 |
|
64 | outDir: 'dist',
|
65 | target: 'app',
|
66 | publicPath: '/',
|
67 | pluginOptions: {},
|
68 | sourceMap: true,
|
69 | babel: {}
|
70 | },
|
71 | this.config,
|
72 | {
|
73 |
|
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 |
|
92 |
|
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 |
|
122 |
|
123 |
|
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 |
|
139 | Object.assign(envs, config.parsed)
|
140 | }
|
141 | })
|
142 |
|
143 |
|
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 |
|
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 |
|
303 | module.exports = (...args) => new Poi(...args)
|