-
Notifications
You must be signed in to change notification settings - Fork 184
/
compileClient.js
executable file
·289 lines (256 loc) · 10.2 KB
/
compileClient.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import webpack from "webpack"
import path from "path"
import mkdirp from "mkdirp"
import fs from "fs"
import serverSideHotModuleReload from "./serverSideHotModuleReload"
// commented out to please eslint, but re-add if logging is needed in this file.
// import {logging} from "react-server"
// const logger = logging.getLogger(__LOGGER__);
// compiles the routes file for browser clients using webpack.
// returns a tuple of { compiler, serverRoutes }. compiler is a webpack compiler
// that is ready to have run called, and serverRoutes is a promise that resolve to
// a path to the transpiled server routes file path. The promise only resolves
// once the compiler has been run. The file path returned from the promise
// can be required and passed in to reactServer.middleware().
// TODO: add options for sourcemaps.
export default (opts = {}) => {
const {
routes,
webpackConfig,
workingDir = "./__clientTemp",
routesDir = ".",
outputDir = workingDir + "/build",
outputUrl = "/static/",
hot = true,
minify = false,
stats = false,
longTermCaching = false,
} = opts;
// support legacy webpack configuration name
if (longTermCaching && hot) {
// chunk hashes can't be used in hot mode, so we can't use long-term caching
// and hot mode at the same time.
throw new Error("Hot reload cannot be used with long-term caching. Please disable either long-term caching or hot reload.");
}
const workingDirAbsolute = path.resolve(process.cwd(), workingDir);
mkdirp.sync(workingDirAbsolute);
const outputDirAbsolute = path.resolve(process.cwd(), outputDir);
mkdirp.sync(outputDirAbsolute);
const routesDirAbsolute = path.resolve(process.cwd(), routesDir);
// for each route, let's create an entrypoint file that includes the page file and the routes file
let bootstrapFile = writeClientBootstrapFile(workingDirAbsolute, opts);
const entrypointBase = hot ? [
require.resolve("webpack-hot-middleware/client") + '?path=/__react_server_hmr__&timeout=20000&reload=true',
] : [];
let entrypoints = {};
for (let routeName of Object.keys(routes.routes)) {
let route = routes.routes[routeName];
let formats = normalizeRoutesPage(route.page);
for (let format of Object.keys(formats)) {
const absolutePathToPage = require.resolve(path.resolve(routesDirAbsolute, formats[format]));
entrypoints[`${routeName}${format !== "default" ? "-" + format : ""}`] = [
...entrypointBase,
bootstrapFile,
absolutePathToPage,
];
}
}
// now rewrite the routes file out in a webpack-compatible way.
writeWebpackCompatibleRoutesFile(routes, routesDir, workingDirAbsolute, null, true);
// finally, let's pack this up with webpack.
// support legacy webpack configuration name
const userWebpackConfigOpt = webpackConfig || opts['webpack-config'];
const config = getWebpackConfig(userWebpackConfigOpt, {
isServer: false,
outputDir: outputDirAbsolute,
entrypoints,
outputUrl,
hot,
minify,
longTermCaching,
stats,
});
const compiler = webpack(config);
const serverRoutes = new Promise((resolve) => {
compiler.hooks.done.tap("ReactServer", (stats) => {
const manifest = statsToManifest(stats);
fs.writeFileSync(path.join(outputDir, "manifest.json"), JSON.stringify(manifest));
const routesFilePath = writeWebpackCompatibleRoutesFile(routes, routesDir, workingDirAbsolute, outputUrl, false, manifest);
if (hot) {
serverSideHotModuleReload(stats);
}
resolve(routesFilePath);
});
});
return {
serverRoutes,
compiler,
config,
};
}
// get the webpack configuration object
// loads data from default configuration at webpack/webpack4.config.fn.js, and
// extends it by calling user-supplied function, if one was provided
function getWebpackConfig(userWebpackConfigOpt, wpAffectingOpts) {
let extend = (data) => { return data }
if (userWebpackConfigOpt) {
const userWebpackConfigPath = path.resolve(process.cwd(), userWebpackConfigOpt);
const userWebpackConfigFunc = require(userWebpackConfigPath);
extend = userWebpackConfigFunc.default;
}
const baseConfig = require(path.join(__dirname, "webpack/webpack4.config.fn.js")).default(wpAffectingOpts);
return extend(baseConfig);
}
// takes in the stats object from a successful compilation and returns a manifest
// object that characterizes the results of the compilation for CSS/JS loading
// and integrity checking. the manifest has the following entries:
// jsChunksByName: an object that maps chunk names to their JS entrypoint file.
// cssChunksByName: an object that maps chunk names to their CSS file. Note that
// not all named chunks necessarily have a CSS file, so not all names will
// show up as a key in this object.
// jsChunksById: an object that maps chunk ids to their JS entrypoint file.
// hash: the overall hash of the build, which can be used to check integrity
// with prebuilt sources.
function statsToManifest(stats) {
const jsChunksByName = {};
const cssChunksByName = {};
const jsChunksById = {};
const cssChunksById = {};
let file;
for (const chunk of stats.compilation.chunks) {
for (let i = 0; i < chunk.files.length; i++) {
file = chunk.files[i];
if (/\.css$/.test(file)) {
cssChunksById[chunk.id] = file;
if (chunk.name) {
cssChunksByName[chunk.name] = file;
}
} else if (/^((?!hot-update).)*\.js$/.test(file)) {
// Ensure we're building a manifest that includes the main JS bundle, not any simple hot updates
jsChunksById[chunk.id] = file;
if (chunk.name) {
jsChunksByName[chunk.name] = file;
}
}
}
}
return {
jsChunksByName,
jsChunksById,
cssChunksByName,
cssChunksById,
hash: stats.hash,
};
}
// writes out a routes file that can be used at runtime.
export function writeWebpackCompatibleRoutesFile(routes, routesDir, workingDirAbsolute, staticUrl, isClient, manifest) {
let routesOutput = [];
const coreMiddleware = JSON.stringify(require.resolve("react-server-core-middleware"));
const existingMiddleware = routes.middleware ? routes.middleware.map((middlewareRelativePath) => {
return `unwrapEs6Module(require(${JSON.stringify(path.relative(workingDirAbsolute, path.resolve(routesDir, middlewareRelativePath)))}))`
}) : [];
routesOutput.push(`
var manifest = ${manifest ? JSON.stringify(manifest) : "undefined"};
function unwrapEs6Module(module) { return module.__esModule ? module.default : module }
var coreJsMiddleware = require(${coreMiddleware}).coreJsMiddleware;
var coreCssMiddleware = require(${coreMiddleware}).coreCssMiddleware;
module.exports = {
middleware:[
coreJsMiddleware(${JSON.stringify(staticUrl)}, manifest),
coreCssMiddleware(${JSON.stringify(staticUrl)}, manifest),
${existingMiddleware.join(",")}
],
routes:{`);
for (let routeName of Object.keys(routes.routes)) {
let route = routes.routes[routeName];
// On the line below specifying 'method', if the route doesn't have a method, we'll set it to `undefined` so that
// routr passes through and matches any method
// https://github.com/yahoo/routr/blob/v2.1.0/lib/router.js#L49-L57
let method = route.method;
// Safely check for an empty object, array, or string and specifically set it to 'undefined'
if (method === undefined || method === null || method === {} || method.length === 0) {
method = undefined; // 'undefined' is the value that routr needs to accept any method
}
routesOutput.push(`
${routeName}: {
path: ${JSON.stringify(route.path)},
method: ${JSON.stringify(method)},`);
let formats = normalizeRoutesPage(route.page);
routesOutput.push(`
page: {`);
for (let format of Object.keys(formats)) {
const formatModule = formats[format];
const relativePathToPage = JSON.stringify(path.relative(workingDirAbsolute, path.resolve(routesDir, formatModule)));
routesOutput.push(`
${format}: function() {
return {
done: function(cb) {`);
if (isClient) {
// Turns out, for now, we do need to keep require.ensure() here. This should be migrated to
// use import() in the future, but there's an issue with Babel not recognizing import() without
// another plugin: https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/#installation
// The JS/CSS files loaded by FLAB are the entrypoint/initial files. Webpack's dynamic loader
// handles loading the other chunks.
routesOutput.push(`
require.ensure(${relativePathToPage}, function() {
cb(unwrapEs6Module(require(${relativePathToPage})));
});`);
} else {
routesOutput.push(`
try {
cb(unwrapEs6Module(require(${relativePathToPage})));
} catch (e) {
console.error('Failed to load page at ${relativePathToPage}', e.stack);
}`);
}
routesOutput.push(`
}
};
},`);
}
routesOutput.push(`
},
},`);
}
routesOutput.push(`
}
};`);
const routesContent = routesOutput.join("");
// make a unique file name so that when it is required, there are no collisions
// in the module loader between different invocations.
const routesFilePath = `${workingDirAbsolute}/routes_${isClient ? "client" : "server"}.js`;
fs.writeFileSync(routesFilePath, routesContent);
return routesFilePath;
}
// the page value for routes.routes["SomeRoute"] can either be a string for the default
// module name or an object mapping format names to module names. This method normalizes
// the value to an object.
function normalizeRoutesPage(page) {
if (typeof page === "string") {
return {default: page};
}
return page;
}
// writes out a bootstrap file for the client which in turn includes the client
// routes file. note that outputDir must be the same directory as the client routes
// file, which must be named "routes_client".
function writeClientBootstrapFile(outputDir, opts) {
const outputFile = outputDir + "/entry.js";
fs.writeFileSync(outputFile, `
import reactServer from "react-server";
import routes from "./routes_client";
if (typeof window !== "undefined") {
window.__setReactServerBase = (path) => {
__webpack_public_path__ = path;
window.__reactServerBase = path;
}
}
window.rfBootstrap = () => {
reactServer.logging.setLevel('main', ${JSON.stringify(opts.logLevel)});
reactServer.logging.setLevel('time', ${JSON.stringify(opts.timingLogLevel)});
reactServer.logging.setLevel('gauge', ${JSON.stringify(opts.gaugeLogLevel)});
new reactServer.ClientController({routes}).init();
}`
);
return outputFile;
}