UNPKG

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