UNPKG

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