1 | import * as connect from 'connect';
|
2 | import * as webpack from 'webpack';
|
3 | import * as url from 'url';
|
4 | import * as fs from 'fs';
|
5 | import * as path from 'path';
|
6 | import { requireNewCopy } from './RequireNewCopy';
|
7 |
|
8 | export type CreateDevServerResult = {
|
9 | Port: number,
|
10 | PublicPaths: string[],
|
11 | PublicPath: string
|
12 | };
|
13 |
|
14 | export interface CreateDevServerCallback {
|
15 | (error: any, result: CreateDevServerResult): void;
|
16 | }
|
17 |
|
18 |
|
19 | interface CreateDevServerOptions {
|
20 | webpackConfigPath: string;
|
21 | suppliedOptions: DevServerOptions;
|
22 | }
|
23 |
|
24 |
|
25 | interface DevServerOptions {
|
26 | HotModuleReplacement: boolean;
|
27 | HotModuleReplacementServerPort: number;
|
28 | ReactHotModuleReplacement: boolean;
|
29 | }
|
30 |
|
31 | function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrEndpoint: string) {
|
32 |
|
33 | if (enableHotModuleReplacement) {
|
34 |
|
35 |
|
36 | const entryPoints = webpackConfig.entry;
|
37 | const isObjectStyleConfig = entryPoints
|
38 | && typeof entryPoints === 'object'
|
39 | && !(entryPoints instanceof Array);
|
40 | if (!isObjectStyleConfig) {
|
41 | throw new Error('To use HotModuleReplacement, your webpack config must specify an \'entry\' value as a key-value object (e.g., "entry: { main: \'ClientApp/boot-client.ts\' }")');
|
42 | }
|
43 |
|
44 |
|
45 | Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => {
|
46 | const webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client';
|
47 | const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrEndpoint);
|
48 | if (typeof entryPoints[entryPointName] === 'string') {
|
49 | entryPoints[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints[entryPointName]];
|
50 | } else if (firstIndexOfStringStartingWith(entryPoints[entryPointName], webpackHotMiddlewareEntryPoint) < 0) {
|
51 | entryPoints[entryPointName].unshift(webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions);
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | const eventSourcePolyfillEntryPoint = 'event-source-polyfill';
|
61 | if (npmModuleIsPresent(eventSourcePolyfillEntryPoint)) {
|
62 | const entryPointsArray: string[] = entryPoints[entryPointName];
|
63 | if (entryPointsArray.indexOf(eventSourcePolyfillEntryPoint) < 0) {
|
64 | const webpackHmrIndex = firstIndexOfStringStartingWith(entryPointsArray, webpackHotMiddlewareEntryPoint);
|
65 | if (webpackHmrIndex < 0) {
|
66 |
|
67 | throw new Error('Cannot find ' + webpackHotMiddlewareEntryPoint + ' in entry points array: ' + entryPointsArray);
|
68 | }
|
69 |
|
70 |
|
71 | entryPointsArray.splice(webpackHmrIndex, 0, eventSourcePolyfillEntryPoint);
|
72 | }
|
73 | }
|
74 | });
|
75 |
|
76 | webpackConfig.plugins = [].concat(webpackConfig.plugins || []);
|
77 | webpackConfig.plugins.push(
|
78 | new webpack.HotModuleReplacementPlugin()
|
79 | );
|
80 |
|
81 |
|
82 | if (enableReactHotModuleReplacement) {
|
83 | let aspNetWebpackReactModule: any;
|
84 | try {
|
85 | aspNetWebpackReactModule = require('aspnet-webpack-react');
|
86 | } catch(ex) {
|
87 | throw new Error('ReactHotModuleReplacement failed because of an error while loading \'aspnet-webpack-react\'. Error was: ' + ex.stack);
|
88 | }
|
89 |
|
90 | aspNetWebpackReactModule.addReactHotModuleReplacementBabelTransform(webpackConfig);
|
91 | }
|
92 | }
|
93 |
|
94 |
|
95 | const compiler = webpack(webpackConfig);
|
96 | app.use(require('webpack-dev-middleware')(compiler, {
|
97 | noInfo: true,
|
98 | publicPath: webpackConfig.output.publicPath
|
99 | }));
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | (compiler as any).plugin('done', stats => {
|
110 | copyRecursiveToRealFsSync(compiler.outputFileSystem, '/', [/\.hot-update\.(js|json)$/]);
|
111 | });
|
112 |
|
113 | if (enableHotModuleReplacement) {
|
114 | let webpackHotMiddlewareModule;
|
115 | try {
|
116 | webpackHotMiddlewareModule = require('webpack-hot-middleware');
|
117 | } catch (ex) {
|
118 | throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack);
|
119 | }
|
120 | app.use(webpackHotMiddlewareModule(compiler));
|
121 | }
|
122 | }
|
123 |
|
124 | function copyRecursiveToRealFsSync(from: typeof fs, rootDir: string, exclude: RegExp[]) {
|
125 | from.readdirSync(rootDir).forEach(filename => {
|
126 | const fullPath = pathJoinSafe(rootDir, filename);
|
127 | const shouldExclude = exclude.filter(re => re.test(fullPath)).length > 0;
|
128 | if (!shouldExclude) {
|
129 | const fileStat = from.statSync(fullPath);
|
130 | if (fileStat.isFile()) {
|
131 | const fileBuf = from.readFileSync(fullPath);
|
132 | fs.writeFileSync(fullPath, fileBuf);
|
133 | } else if (fileStat.isDirectory()) {
|
134 | if (!fs.existsSync(fullPath)) {
|
135 | fs.mkdirSync(fullPath);
|
136 | }
|
137 | copyRecursiveToRealFsSync(from, fullPath, exclude);
|
138 | }
|
139 | }
|
140 | });
|
141 | }
|
142 |
|
143 | function pathJoinSafe(rootPath: string, filePath: string) {
|
144 |
|
145 |
|
146 |
|
147 | if (rootPath === '/' && path.sep === '\\' && filePath.match(/^[a-z0-9]+\:$/i)) {
|
148 | return filePath + '\\';
|
149 | } else {
|
150 | return path.join(rootPath, filePath);
|
151 | }
|
152 | }
|
153 |
|
154 | function beginWebpackWatcher(webpackConfig: webpack.Configuration) {
|
155 | const compiler = webpack(webpackConfig);
|
156 | compiler.watch({ }, (err, stats) => {
|
157 |
|
158 | });
|
159 | }
|
160 |
|
161 | export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) {
|
162 | const options: CreateDevServerOptions = JSON.parse(optionsJson);
|
163 |
|
164 |
|
165 | let webpackConfigArray: webpack.Configuration[] = requireNewCopy(options.webpackConfigPath);
|
166 | if (!(webpackConfigArray instanceof Array)) {
|
167 | webpackConfigArray = [webpackConfigArray as webpack.Configuration];
|
168 | }
|
169 |
|
170 | const enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement;
|
171 | const enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement;
|
172 | if (enableReactHotModuleReplacement && !enableHotModuleReplacement) {
|
173 | callback('To use ReactHotModuleReplacement, you must also enable the HotModuleReplacement option.', null);
|
174 | return;
|
175 | }
|
176 |
|
177 |
|
178 | const suggestedHMRPortOrZero = options.suppliedOptions.HotModuleReplacementServerPort || 0;
|
179 |
|
180 | const app = connect();
|
181 | const listener = app.listen(suggestedHMRPortOrZero, () => {
|
182 | try {
|
183 |
|
184 | const normalizedPublicPaths: string[] = [];
|
185 | webpackConfigArray.forEach(webpackConfig => {
|
186 | if (webpackConfig.target === 'node') {
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | beginWebpackWatcher(webpackConfig);
|
192 | } else {
|
193 |
|
194 |
|
195 | const publicPath = (webpackConfig.output.publicPath || '').trim();
|
196 | if (!publicPath) {
|
197 | throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack config (for any configuration that targets browsers)');
|
198 | }
|
199 | normalizedPublicPaths.push(removeTrailingSlash(publicPath));
|
200 |
|
201 | const hmrEndpoint = `http://localhost:${listener.address().port}/__webpack_hmr`;
|
202 | attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrEndpoint);
|
203 | }
|
204 | });
|
205 |
|
206 |
|
207 | callback(null, {
|
208 | Port: listener.address().port,
|
209 | PublicPaths: normalizedPublicPaths,
|
210 |
|
211 |
|
212 |
|
213 | PublicPath: normalizedPublicPaths[0]
|
214 | });
|
215 | } catch (ex) {
|
216 | callback(ex.stack, null);
|
217 | }
|
218 | });
|
219 | }
|
220 |
|
221 | function removeTrailingSlash(str: string) {
|
222 | if (str.lastIndexOf('/') === str.length - 1) {
|
223 | str = str.substring(0, str.length - 1);
|
224 | }
|
225 |
|
226 | return str;
|
227 | }
|
228 |
|
229 | function getPath(publicPath: string) {
|
230 | return url.parse(publicPath).path;
|
231 | }
|
232 |
|
233 | function firstIndexOfStringStartingWith(array: string[], prefixToFind: string) {
|
234 | for (let index = 0; index < array.length; index++) {
|
235 | const candidate = array[index];
|
236 | if ((typeof candidate === 'string') && (candidate.substring(0, prefixToFind.length) === prefixToFind)) {
|
237 | return index;
|
238 | }
|
239 | }
|
240 |
|
241 | return -1;
|
242 | }
|
243 |
|
244 | function npmModuleIsPresent(moduleName: string) {
|
245 | try {
|
246 | require.resolve(moduleName);
|
247 | return true;
|
248 | } catch (ex) {
|
249 | return false;
|
250 | }
|
251 | }
|