Disallow *.server.js imports from any other files (#20309)
This convention ensures that you can declare that you intend for a file to only be used on the server (even if it technically might resolve on the client).
Sebastian Markbåge committed
Nov 30, 2020 at 17:25 UTC
565148d7516f9f0c4d90a7f06ab479c79ad56c69
5 files changed
+56
-3
fixtures/flight/server/cli.server.js
renamed
fixtures/flight/server/package.json
+2
-1
@@ -1,3 +1,4 @@
1
{
2
- "type": "commonjs"
2
+ "type": "commonjs",
3
+ "main": "./cli.server.js"
4
}
packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js
+28
-1
@@ -31,12 +31,39 @@ type GetSourceFunction = (
31
32
type Source = string | ArrayBuffer | Uint8Array;
33
34
+let warnedAboutConditionsFlag = false;
35
+
36
export async function resolve(
37
specifier: string,
38
context: ResolveContext,
39
defaultResolve: ResolveFunction,
40
): Promise<string> {
39
- // TODO: Resolve server-only files.
41
+ if (!context.conditions.includes('react-server')) {
42
+ context = {
43
+ ...context,
44
+ conditions: [...context.conditions, 'react-server'],
45
+ };
46
+ if (!warnedAboutConditionsFlag) {
47
+ warnedAboutConditionsFlag = true;
48
+ // eslint-disable-next-line react-internal/no-production-logging
49
+ console.warn(
50
+ 'You did not run Node.js with the `--conditions react-server` flag. ' +
51
+ 'Any "react-server" override will only work with ESM imports.',
52
+ );
53
+ }
54
+ }
55
+ // We intentionally check the specifier here instead of the resolved file.
56
+ // This allows package exports to configure non-server aliases that resolve to server files
57
+ // depending on environment. It's probably a bad idea to export a server file as "main" though.
58
+ if (specifier.endsWith('.server.js')) {
59
+ if (context.parentURL && !context.parentURL.endsWith('.server.js')) {
60
+ throw new Error(
61
+ `Cannot import "${specifier}" from "${context.parentURL}". ` +
62
+ 'By react-server convention, .server.js files can only be imported from other .server.js files. ' +
63
+ 'That way nobody accidentally sends these to the client by indirectly importing it.',
64
+ );
65
+ }
66
+ }
67
return defaultResolve(specifier, context, defaultResolve);
68
}
69
packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js
+25
@@ -9,6 +9,9 @@
9
10
const url = require('url');
11
12
+// $FlowFixMe
13
+const Module = require('module');
14
+
15
module.exports = function register() {
16
(require: any).extensions['.client.js'] = function(module, path) {
17
module.exports = {
@@ -16,4 +19,26 @@ module.exports = function register() {
19
name: url.pathToFileURL(path).href,
20
};
21
};
22
+
23
+ const originalResolveFilename = Module._resolveFilename;
24
+
25
+ Module._resolveFilename = function(request, parent, isMain, options) {
26
+ // We intentionally check the request here instead of the resolved file.
27
+ // This allows package exports to configure non-server aliases that resolve to server files
28
+ // depending on environment. It's probably a bad idea to export a server file as "main" though.
29
+ if (request.endsWith('.server.js')) {
30
+ if (
31
+ parent &&
32
+ parent.filename &&
33
+ !parent.filename.endsWith('.server.js')
34
+ ) {
35
+ throw new Error(
36
+ `Cannot import "${request}" from "${parent.filename}". ` +
37
+ 'By react-server convention, .server.js files can only be imported from other .server.js files. ' +
38
+ 'That way nobody accidentally sends these to the client by indirectly importing it.',
39
+ );
40
+ }
41
+ }
42
+ return originalResolveFilename.apply(this, arguments);
43
+ };
44
};
scripts/rollup/bundles.js
+1
-1
@@ -310,7 +310,7 @@ const bundles = [
310
moduleType: RENDERER_UTILS,
311
entry: 'react-transport-dom-webpack/node-register',
312
global: 'ReactFlightWebpackNodeRegister',
313
- externals: ['url'],
313
+ externals: ['url', 'module'],
314
},
315
316
/******* React Transport DOM Server Relay *******/