UNPKG

5.86 kBJavaScriptView Raw
1const url = require('url')
2const chalk = require('chalk')
3const prompts = require('prompts')
4const openBrowser = require('react-dev-utils/openBrowser')
5const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
6const clearConsole = require('react-dev-utils/clearConsole')
7const tsFormatter = require('react-dev-utils/typescriptFormatter')
8const FriendlyErrorsPlugin = require('@mara/friendly-errors-webpack-plugin')
9
10const isInteractive = process.stdout.isTTY
11const tsErrorFormat = msg => `${msg.file}\n${tsFormatter(msg, true)}`
12const noop = () => {}
13
14function printInstructions(appName, urls, useYarn) {
15 console.log(` App ${chalk.bold(appName)} running at:`)
16 console.log()
17
18 console.log(` - ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`)
19 console.log(` - ${chalk.bold('Network:')} ${urls.lanUrlForTerminal}`)
20
21 console.log()
22 console.log(' Note that the development build is not optimized.')
23 console.log(
24 ` To create a production build, use ` +
25 `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.`
26 )
27 console.log()
28}
29
30module.exports = class MaraDevServerPlugin {
31 constructor(options) {
32 const defOpt = {
33 port: '3022',
34 entry: '',
35 protocol: 'http',
36 host: 'localhost',
37 openBrowser: true,
38 clearConsole: true,
39 publicPath: '/',
40 useYarn: false,
41 useTypeScript: false,
42 onTsError: noop,
43 root: process.cwd()
44 }
45
46 this.tsMessagesPromise = Promise.resolve()
47 this.options = Object.assign(defOpt, options)
48 this.serverUrl = this.getServerURL()
49 }
50
51 apply(compiler) {
52 const pluginName = this.constructor.name
53 const useYarn = this.options.useYarn
54 let isFirstCompile = true
55 // friendly error plugin displays very confusing errors when webpack
56 // fails to resolve a loader, so we provide custom handlers to improve it
57 const friendErrors = new FriendlyErrorsPlugin({
58 showFirstError: true,
59 useYarn: useYarn,
60 onErrors(severity, topErrors) {
61 const hasLoaderError = topErrors.some(
62 e => e.type === FriendlyErrorsPlugin.TYPE.CANT_RESOVLE_LOADER
63 )
64
65 // loader 错误中断进程
66 if (hasLoaderError) {
67 process.kill(process.pid, 'SIGINT')
68 }
69 }
70 })
71
72 if (this.options.clearConsole) {
73 compiler.hooks.invalid.tap(pluginName, this.clearConsole)
74 compiler.hooks.done.tap(pluginName, this.clearConsole)
75 }
76
77 compiler.hooks.invalid.tap(pluginName, () => friendErrors.invalidFn())
78
79 if (this.options.useTypeScript) {
80 this.tsChecker(compiler)
81 }
82
83 compiler.hooks.done.tap(pluginName, async stats => {
84 if (this.options.useTypeScript && !stats.hasErrors()) {
85 const delayedMsg = setTimeout(() => {
86 friendErrors.invalidFn(
87 'Files successfully emitted, waiting for typecheck results...'
88 )
89 }, 100)
90
91 const tsMsg = await this.tsMessagesPromise
92 clearTimeout(delayedMsg)
93
94 // Push errors and warnings into compilation result
95 // to show them after page refresh triggered by user.
96 stats.compilation.errors.push(...tsMsg.errors)
97 stats.compilation.warnings.push(...tsMsg.warnings)
98
99 if (tsMsg.errors.length > 0) {
100 this.options.onTsError('error', tsMsg.errors.map(tsErrorFormat))
101 } else if (tsMsg.warnings.length > 0) {
102 this.options.onTsError('warning', tsMsg.warnings.map(tsErrorFormat))
103 }
104
105 this.clearConsole()
106 }
107
108 const isSuccessful = !stats.hasErrors() && !stats.hasWarnings()
109
110 isFirstCompile && this.options.spinner.stop()
111
112 friendErrors.doneFn(stats)
113
114 if (isSuccessful && (isInteractive || isFirstCompile)) {
115 printInstructions(
116 this.options.entry,
117 this.serverUrl,
118 this.options.useYarn
119 )
120 }
121
122 if (isFirstCompile && !stats.hasErrors()) {
123 if (this.options.openBrowser) {
124 openBrowser(this.serverUrl.lanUrl)
125 }
126
127 isFirstCompile = false
128 }
129 })
130 }
131
132 tsChecker(compiler) {
133 let tsMessagesResolver = noop
134
135 compiler.hooks.beforeCompile.tap('beforeCompile', () => {
136 this.tsMessagesPromise = new Promise(resolve => {
137 tsMessagesResolver = msgs => resolve(msgs)
138 })
139 })
140
141 ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler).receive.tap(
142 'afterTypeScriptCheck',
143 (diagnostics, lints) => {
144 const allMsgs = [...diagnostics, ...lints]
145
146 tsMessagesResolver({
147 errors: allMsgs.filter(msg => msg.severity === 'error'),
148 warnings: allMsgs.filter(msg => msg.severity === 'warning')
149 })
150 }
151 )
152 }
153
154 clearConsole() {
155 isInteractive && clearConsole()
156 }
157
158 getServerURL() {
159 const { protocol, host, port, entry } = this.options
160 let publicDevPath = this.options.publicPath
161
162 // 以绝对路径 / 开头时,加入 url 中在浏览器打开
163 // 以非 / 开头时,回退为 /,避免浏览器路径错乱
164 publicDevPath = publicDevPath.startsWith('/') ? publicDevPath : '/'
165
166 const prepareUrls = hostname => ({
167 plain: url.format({
168 protocol,
169 hostname,
170 port,
171 // 始终携带 view html
172 pathname: publicDevPath + `${entry}.html`
173 }),
174 pretty: chalk.cyan(
175 url.format({
176 protocol,
177 hostname,
178 port,
179 // 终端信息中省略 index.html
180 pathname:
181 publicDevPath +
182 (entry === 'index' ? '' : chalk.bold(`${entry}.html`))
183 })
184 )
185 })
186
187 const localUrl = prepareUrls('localhost')
188 const lanUrl = prepareUrls(host || 'localhost')
189
190 return {
191 localUrl: localUrl.plain,
192 lanUrl: lanUrl.plain,
193 localUrlForTerminal: localUrl.pretty,
194 lanUrlForTerminal: lanUrl.pretty
195 }
196 }
197}