UNPKG

8.52 kBJavaScriptView Raw
1const path = require('path')
2const os = require('os')
3const chalk = require('chalk')
4const fs = require('fs-extra')
5
6const isLocalPath = v => /^[./]|(^[a-zA-Z]:)/.test(v)
7
8const normalizeEntry = v => {
9 if (v.startsWith('module:')) {
10 return v.replace(/^module:/, '')
11 }
12 if (isLocalPath(v)) {
13 return v
14 }
15 return `./${v}`
16}
17
18module.exports = (config, api) => {
19 /** Set entry */
20
21 const webpackEntry = {}
22 const { entry, pages } = api.config
23 if (pages) {
24 for (const entryName of Object.keys(pages)) {
25 const value = pages[entryName]
26 webpackEntry[entryName] = [].concat(
27 typeof value === 'string' ? value : value.entry
28 )
29 }
30 api.logger.debug('Using `pages` option thus `entry` is ignored')
31 } else if (typeof entry === 'string') {
32 webpackEntry.index = [entry]
33 } else if (Array.isArray(entry)) {
34 webpackEntry.index = entry
35 } else if (typeof entry === 'object') {
36 Object.assign(webpackEntry, entry)
37 }
38
39 for (const name of Object.keys(webpackEntry)) {
40 webpackEntry[name] = Array.isArray(webpackEntry[name])
41 ? webpackEntry[name].map(v => normalizeEntry(v))
42 : normalizeEntry(webpackEntry[name])
43 }
44
45 config.merge({ entry: webpackEntry })
46
47 /** Set extensions */
48 config.resolve.extensions.merge([
49 '.wasm',
50 '.mjs',
51 '.js',
52 '.jsx',
53 '.ts',
54 '.tsx',
55 '.json'
56 ])
57
58 /** Support react-native-web by default, cuz why not? */
59 // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
60 config.resolve.alias
61 .set('react-native', 'react-native-web')
62 .set('#webpack-hot-client$', require.resolve('@poi/dev-utils/hotDevClient'))
63
64 // output.sourceMap defaults to false in production mode
65 config.devtool(
66 api.config.output.sourceMap === false
67 ? false
68 : api.mode === 'production'
69 ? 'source-map'
70 : api.mode === 'test'
71 ? 'cheap-module-eval-source-map'
72 : 'cheap-module-source-map'
73 )
74
75 /** Alias @ to `src` folder since many apps store app code here */
76 config.resolve.alias.set('@', api.resolveCwd('src'))
77
78 /** Set mode */
79 config.mode(api.mode === 'production' ? 'production' : 'development')
80
81 config.merge({
82 // Disable webpack's default minimizer
83 // Minimization will be handled by mode:production plugin
84 optimization: {
85 minimize: false
86 },
87 // Disable default performance hints
88 // TODO: maybe add our custom one
89 performance: {
90 hints: false
91 }
92 })
93
94 /** Set output */
95 config.output.path(api.resolveOutDir())
96 config.output.filename(api.config.output.fileNames.js)
97 config.output.chunkFilename(
98 api.config.output.fileNames.js.replace(/\.js$/, '.chunk.js')
99 )
100 config.output.publicPath(api.config.output.publicUrl)
101
102 /** Set format */
103 const { format, moduleName } = api.config.output
104 if (format === 'cjs') {
105 config.output.libraryTarget('commonjs2')
106 } else if (format === 'umd') {
107 if (!moduleName) {
108 api.logger.error(
109 `"moduleName" is missing for ${chalk.bold('umd')} format`
110 )
111 api.logger.tip(
112 `To add it, simply use flag ${chalk.cyan('--module-name <name>')}`
113 )
114 api.logger.tip(
115 `Like ${chalk.cyan(
116 '--module-name React'
117 )} if you're building the React.js source code`
118 )
119 throw new api.PoiError({
120 message: 'missing moduleName for umd format',
121 dismiss: true
122 })
123 }
124 config.output.libraryTarget('umd')
125 config.output.library(moduleName)
126 }
127
128 // Set output target
129 const { target } = api.config.output
130 config.target(target === 'electron' ? 'electron-renderer' : target)
131
132 const inYarnWorkspaces = __dirname.includes('/poi/core/poi')
133 const poiDependenciesDir = inYarnWorkspaces
134 ? path.join(__dirname, '../../../../node_modules')
135 : path.join(__dirname, '../../../')
136
137 /** Resolve loaders */
138 config.resolveLoader.modules.add('node_modules').add(poiDependenciesDir)
139
140 /** Resolve modules */
141 config.resolve.modules.add('node_modules').add(poiDependenciesDir)
142
143 // Add progress bar
144 if (
145 api.cli.options.progress !== false &&
146 process.stdout.isTTY &&
147 !process.env.CI
148 ) {
149 const homeRe = new RegExp(os.homedir(), 'g')
150 config.plugin('progress').use(require('webpack').ProgressPlugin, [
151 /**
152 * @param {Number} per
153 * @param {string} message
154 * @param {string[]} args
155 */
156 (per, message, ...args) => {
157 const spinner = require('../utils/spinner')
158
159 const msg = `${(per * 100).toFixed(2)}% ${message} ${args
160 .map(arg => {
161 const message = arg.replace(homeRe, '~')
162 return message.length > 40
163 ? `...${message.substr(message.length - 39)}`
164 : message
165 })
166 .join(' ')}`
167
168 if (per === 0) {
169 spinner.start(msg)
170 } else if (per === 1) {
171 spinner.stop()
172 } else {
173 spinner.text = msg
174 }
175 }
176 ])
177 }
178
179 /** Add a default status reporter */
180 config.plugin('print-status').use(require('./PrintStatusPlugin'), [
181 {
182 printFileStats: api.mode !== 'test',
183 printSucessMessage: api.mode !== 'test',
184 clearConsole: api.cli.options.clearConsole
185 }
186 ])
187
188 /** Add constants plugin */
189 config
190 .plugin('constants')
191 .use(require('webpack').DefinePlugin, [api.webpackUtils.constants])
192
193 /** Inject envs */
194 const { envs } = api.webpackUtils
195 config.plugin('envs').use(require('webpack').DefinePlugin, [
196 Object.keys(envs).reduce((res, name) => {
197 res[`process.env.${name}`] = JSON.stringify(envs[name])
198 return res
199 }, {})
200 ])
201
202 /** Miniize JS files */
203 if (api.config.output.minimize) {
204 config.plugin('minimize').use(require('terser-webpack-plugin'), [
205 {
206 cache: true,
207 parallel: true,
208 sourceMap: api.config.output.sourceMap,
209 terserOptions: {
210 parse: {
211 // we want terser to parse ecma 8 code. However, we don't want it
212 // to apply any minfication steps that turns valid ecma 5 code
213 // into invalid ecma 5 code. This is why the 'compress' and 'output'
214 // sections only apply transformations that are ecma 5 safe
215 // https://github.com/facebook/create-react-app/pull/4234
216 ecma: 8
217 },
218 compress: {
219 ecma: 5,
220 warnings: false,
221 // Disabled because of an issue with Uglify breaking seemingly valid code:
222 // https://github.com/facebook/create-react-app/issues/2376
223 // Pending further investigation:
224 // https://github.com/mishoo/UglifyJS2/issues/2011
225 comparisons: false,
226 // Disabled because of an issue with Terser breaking valid code:
227 // https://github.com/facebook/create-react-app/issues/5250
228 // Pending futher investigation:
229 // https://github.com/terser-js/terser/issues/120
230 inline: 2
231 },
232 mangle: {
233 safari10: true
234 },
235 output: {
236 ecma: 5,
237 comments: false,
238 // Turned on because emoji and regex is not minified properly using default
239 // https://github.com/facebook/create-react-app/issues/2488
240 ascii_only: true
241 }
242 }
243 }
244 ])
245 }
246
247 if (!api.isProd) {
248 config
249 .plugin('case-sensitive-paths')
250 .use(require('case-sensitive-paths-webpack-plugin'))
251
252 const nodeModulesDir =
253 api.configLoader.resolve('node_modules') || api.resolveCwd('node_modules')
254 config
255 .plugin('watch-missing-node-modules')
256 .use(require('@poi/dev-utils/WatchMissingNodeModulesPlugin'), [
257 nodeModulesDir
258 ])
259 }
260
261 config.plugin('copy-public-folder').use(require('copy-webpack-plugin'), [
262 [
263 {
264 from: {
265 glob: '**/*',
266 dot: true
267 },
268 context: api.resolveCwd(api.config.publicFolder),
269 to: '.',
270 ignore: ['.DS_Store']
271 }
272 ]
273 ])
274
275 if (api.config.output.clean !== false) {
276 config.plugin('clean-out-dir').use(
277 class CleanOutDir {
278 apply(compiler) {
279 compiler.hooks.beforeRun.tapPromise('clean-out-dir', async () => {
280 if (api.resolveOutDir() === process.cwd()) {
281 api.logger.error(`Refused to clean current working directory`)
282 return
283 }
284 await fs.remove(api.resolveOutDir())
285 })
286 }
287 }
288 )
289 }
290}