UNPKG

7.93 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 const message = arg.replace(homeRe, '~')
142 return message.length > 40
143 ? `...${message.substr(message.length - 39)}`
144 : message
145 })
146 .join(' ')}`
147
148 if (per === 0) {
149 spinner.start(msg)
150 } else if (per === 1) {
151 spinner.stop()
152 } else {
153 spinner.text = msg
154 }
155 }
156 ])
157 }
158
159 /** Add a default status reporter */
160 if (api.mode !== 'test') {
161 config.plugin('print-status').use(require('./PrintStatusPlugin'), [
162 {
163 printFileStats: true,
164 clearConsole: api.cli.options.clearConsole
165 }
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}