UNPKG

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