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 | const originalFileSystem = compiler.outputFileSystem;
|
97 | app.use(require('webpack-dev-middleware')(compiler, {
|
98 | noInfo: true,
|
99 | publicPath: webpackConfig.output.publicPath
|
100 | }));
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | (compiler as any).plugin('done', stats => {
|
111 | copyRecursiveSync(compiler.outputFileSystem, originalFileSystem, '/', [/\.hot-update\.(js|json)$/]);
|
112 | });
|
113 |
|
114 | if (enableHotModuleReplacement) {
|
115 | let webpackHotMiddlewareModule;
|
116 | try {
|
117 | webpackHotMiddlewareModule = require('webpack-hot-middleware');
|
118 | } catch (ex) {
|
119 | throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack);
|
120 | }
|
121 | app.use(webpackHotMiddlewareModule(compiler));
|
122 | }
|
123 | }
|
124 |
|
125 | function copyRecursiveSync(from: typeof fs, to: typeof fs, rootDir: string, exclude: RegExp[]) {
|
126 | from.readdirSync(rootDir).forEach(filename => {
|
127 | const fullPath = pathJoinSafe(rootDir, filename);
|
128 | const shouldExclude = exclude.filter(re => re.test(fullPath)).length > 0;
|
129 | if (!shouldExclude) {
|
130 | const fileStat = from.statSync(fullPath);
|
131 | if (fileStat.isFile()) {
|
132 | const fileBuf = from.readFileSync(fullPath);
|
133 | to.writeFile(fullPath, fileBuf);
|
134 | } else if (fileStat.isDirectory()) {
|
135 | copyRecursiveSync(from, to, fullPath, exclude);
|
136 | }
|
137 | }
|
138 | });
|
139 | }
|
140 |
|
141 | function pathJoinSafe(rootPath: string, filePath: string) {
|
142 |
|
143 |
|
144 |
|
145 | if (rootPath === '/' && path.sep === '\\' && filePath.match(/^[a-z0-9]+\:$/i)) {
|
146 | return filePath + '\\';
|
147 | } else {
|
148 | return path.join(rootPath, filePath);
|
149 | }
|
150 | }
|
151 |
|
152 | function beginWebpackWatcher(webpackConfig: webpack.Configuration) {
|
153 | const compiler = webpack(webpackConfig);
|
154 | compiler.watch({ }, (err, stats) => {
|
155 |
|
156 | });
|
157 | }
|
158 |
|
159 | export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) {
|
160 | const options: CreateDevServerOptions = JSON.parse(optionsJson);
|
161 |
|
162 |
|
163 | let webpackConfigArray: webpack.Configuration[] = requireNewCopy(options.webpackConfigPath);
|
164 | if (!(webpackConfigArray instanceof Array)) {
|
165 | webpackConfigArray = [webpackConfigArray as webpack.Configuration];
|
166 | }
|
167 |
|
168 | const enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement;
|
169 | const enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement;
|
170 | if (enableReactHotModuleReplacement && !enableHotModuleReplacement) {
|
171 | callback('To use ReactHotModuleReplacement, you must also enable the HotModuleReplacement option.', null);
|
172 | return;
|
173 | }
|
174 |
|
175 |
|
176 | const suggestedHMRPortOrZero = options.suppliedOptions.HotModuleReplacementServerPort || 0;
|
177 |
|
178 | const app = connect();
|
179 | const listener = app.listen(suggestedHMRPortOrZero, () => {
|
180 | try {
|
181 |
|
182 | const normalizedPublicPaths: string[] = [];
|
183 | webpackConfigArray.forEach(webpackConfig => {
|
184 | if (webpackConfig.target === 'node') {
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | beginWebpackWatcher(webpackConfig);
|
190 | } else {
|
191 |
|
192 |
|
193 | const publicPath = (webpackConfig.output.publicPath || '').trim();
|
194 | if (!publicPath) {
|
195 | 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)');
|
196 | }
|
197 | normalizedPublicPaths.push(removeTrailingSlash(publicPath));
|
198 |
|
199 | const hmrEndpoint = `http://localhost:${listener.address().port}/__webpack_hmr`;
|
200 | attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrEndpoint);
|
201 | }
|
202 | });
|
203 |
|
204 |
|
205 | callback(null, {
|
206 | Port: listener.address().port,
|
207 | PublicPaths: normalizedPublicPaths,
|
208 |
|
209 |
|
210 |
|
211 | PublicPath: normalizedPublicPaths[0]
|
212 | });
|
213 | } catch (ex) {
|
214 | callback(ex.stack, null);
|
215 | }
|
216 | });
|
217 | }
|
218 |
|
219 | function removeTrailingSlash(str: string) {
|
220 | if (str.lastIndexOf('/') === str.length - 1) {
|
221 | str = str.substring(0, str.length - 1);
|
222 | }
|
223 |
|
224 | return str;
|
225 | }
|
226 |
|
227 | function getPath(publicPath: string) {
|
228 | return url.parse(publicPath).path;
|
229 | }
|
230 |
|
231 | function firstIndexOfStringStartingWith(array: string[], prefixToFind: string) {
|
232 | for (let index = 0; index < array.length; index++) {
|
233 | const candidate = array[index];
|
234 | if ((typeof candidate === 'string') && (candidate.substring(0, prefixToFind.length) === prefixToFind)) {
|
235 | return index;
|
236 | }
|
237 | }
|
238 |
|
239 | return -1;
|
240 | }
|
241 |
|
242 | function npmModuleIsPresent(moduleName: string) {
|
243 | try {
|
244 | require.resolve(moduleName);
|
245 | return true;
|
246 | } catch (ex) {
|
247 | return false;
|
248 | }
|
249 | }
|