Revert the outer module object to an object (#26093)
This is because Webpack has a `typeof ... === 'object'` before its esm compat test. This is unfortunate because it means we can't have a nice error in CJS when someone does this: ``` const fn = require('client-fn'); fn(); ``` I also fixed some checks in the validator that read off the client ref. It shouldn't do those checks against a client ref, since those now throw.
Sebastian Markbåge committed
Feb 2, 2023 at 15:30 UTC
922dd7ba50f69f1f969c5809eb57104768716c89
4 files changed
+122
-45
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
+14
-39
@@ -33,8 +33,6 @@ module.exports = function register() {
33
// reference.
34
case 'defaultProps':
35
return undefined;
36
- case 'getDefaultProps':
37
- return undefined;
36
// Avoid this attempting to be serialized.
37
case 'toJSON':
38
return undefined;
@@ -91,8 +89,6 @@ module.exports = function register() {
89
// reference.
90
case 'defaultProps':
91
return undefined;
94
- case 'getDefaultProps':
95
- return undefined;
92
// Avoid this attempting to be serialized.
93
case 'toJSON':
94
return undefined;
@@ -132,24 +128,13 @@ module.exports = function register() {
128
// we should resolve that with a client reference that unwraps the Promise on
129
// the client.
130
135
- const innerModuleId = target.filepath;
136
- const clientReference: Function = Object.defineProperties(
137
- (function () {
138
- throw new Error(
139
- `Attempted to call the module exports of ${innerModuleId} from the server` +
140
- `but it's on the client. It's not possible to invoke a client function from ` +
141
- `the server, it can only be rendered as a Component or passed to props of a` +
142
- `Client Component.`,
143
- );
144
- }: any),
145
- {
146
- // Represents the whole object instead of a particular import.
147
- name: {value: '*'},
148
- $$typeof: {value: CLIENT_REFERENCE},
149
- filepath: {value: target.filepath},
150
- async: {value: true},
151
- },
152
- );
131
+ const clientReference = Object.defineProperties(({}: any), {
132
+ // Represents the whole Module object instead of a particular import.
133
+ name: {value: '*'},
134
+ $$typeof: {value: CLIENT_REFERENCE},
135
+ filepath: {value: target.filepath},
136
+ async: {value: true},
137
+ });
138
const proxy = new Proxy(clientReference, proxyHandlers);
139
140
// Treat this as a resolved Promise for React's use()
@@ -221,23 +206,13 @@ module.exports = function register() {
206
// $FlowFixMe[prop-missing] found when upgrading Flow
207
Module._extensions['.client.js'] = function (module, path) {
208
const moduleId: string = (url.pathToFileURL(path).href: any);
224
- const clientReference: Function = Object.defineProperties(
225
- (function () {
226
- throw new Error(
227
- `Attempted to call the module exports of ${moduleId} from the server` +
228
- `but it's on the client. It's not possible to invoke a client function from ` +
229
- `the server, it can only be rendered as a Component or passed to props of a` +
230
- `Client Component.`,
231
- );
232
- }: any),
233
- {
234
- // Represents the whole object instead of a particular import.
235
- name: {value: '*'},
236
- $$typeof: {value: CLIENT_REFERENCE},
237
- filepath: {value: moduleId},
238
- async: {value: false},
239
- },
240
- );
209
+ const clientReference = Object.defineProperties(({}: any), {
210
+ // Represents the whole Module object instead of a particular import.
211
+ name: {value: '*'},
212
+ $$typeof: {value: CLIENT_REFERENCE},
213
+ filepath: {value: moduleId},
214
+ async: {value: false},
215
+ });
216
// $FlowFixMe[incompatible-call] found when upgrading Flow
217
module.exports = new Proxy(clientReference, proxyHandlers);
218
};
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+88
@@ -246,6 +246,94 @@ describe('ReactFlightDOM', () => {
246
expect(container.innerHTML).toBe('<p>@div</p>');
247
});
248
249
+ // @gate enableUseHook
250
+ it('should be able to esm compat test module references', async () => {
251
+ const ESMCompatModule = {
252
+ __esModule: true,
253
+ default: function ({greeting}) {
254
+ return greeting + ' World';
255
+ },
256
+ hi: 'Hello',
257
+ };
258
+
259
+ function Print({response}) {
260
+ return <p>{use(response)}</p>;
261
+ }
262
+
263
+ function App({response}) {
264
+ return (
265
+ <Suspense fallback={<h1>Loading...</h1>}>
266
+ <Print response={response} />
267
+ </Suspense>
268
+ );
269
+ }
270
+
271
+ function interopWebpack(obj) {
272
+ // Basically what Webpack's ESM interop feature testing does.
273
+ if (typeof obj === 'object' && obj.__esModule) {
274
+ return obj;
275
+ }
276
+ return Object.assign({default: obj}, obj);
277
+ }
278
+
279
+ const {default: Component, hi} = interopWebpack(
280
+ clientExports(ESMCompatModule),
281
+ );
282
+
283
+ const {writable, readable} = getTestStream();
284
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
285
+ <Component greeting={hi} />,
286
+ webpackMap,
287
+ );
288
+ pipe(writable);
289
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
290
+
291
+ const container = document.createElement('div');
292
+ const root = ReactDOMClient.createRoot(container);
293
+ await act(async () => {
294
+ root.render(<App response={response} />);
295
+ });
296
+ expect(container.innerHTML).toBe('<p>Hello World</p>');
297
+ });
298
+
299
+ // @gate enableUseHook
300
+ it('should be able to render a named component export', async () => {
301
+ const Module = {
302
+ Component: function ({greeting}) {
303
+ return greeting + ' World';
304
+ },
305
+ };
306
+
307
+ function Print({response}) {
308
+ return <p>{use(response)}</p>;
309
+ }
310
+
311
+ function App({response}) {
312
+ return (
313
+ <Suspense fallback={<h1>Loading...</h1>}>
314
+ <Print response={response} />
315
+ </Suspense>
316
+ );
317
+ }
318
+
319
+ const {Component} = clientExports(Module);
320
+
321
+ const {writable, readable} = getTestStream();
322
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
323
+ <Component greeting={'Hello'} />,
324
+ webpackMap,
325
+ );
326
+ pipe(writable);
327
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
328
+
329
+ const container = document.createElement('div');
330
+ const root = ReactDOMClient.createRoot(container);
331
+ await act(async () => {
332
+ root.render(<App response={response} />);
333
+ });
334
+ expect(container.innerHTML).toBe('<p>Hello World</p>');
335
+ });
336
+
337
// @gate enableUseHook
338
it('should unwrap async module references', async () => {
339
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
packages/react/src/ReactElementValidator.js
+10
-3
@@ -35,6 +35,8 @@ import {setExtraStackFrame} from './ReactDebugCurrentFrame';
35
import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
36
import hasOwnProperty from 'shared/hasOwnProperty';
37
38
+const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
39
+
40
function setCurrentlyValidatingElement(element) {
41
if (__DEV__) {
42
if (element) {
@@ -165,10 +167,12 @@ function validateExplicitKey(element, parentType) {
167
* @param {*} parentType node's parent's type.
168
*/
169
function validateChildKeys(node, parentType) {
168
- if (typeof node !== 'object') {
170
+ if (typeof node !== 'object' || !node) {
171
return;
172
}
171
- if (isArray(node)) {
173
+ if (node.$$typeof === REACT_CLIENT_REFERENCE) {
174
+ // This is a reference to a client component so it's unknown.
175
+ } else if (isArray(node)) {
176
for (let i = 0; i < node.length; i++) {
177
const child = node[i];
178
if (isValidElement(child)) {
@@ -180,7 +184,7 @@ function validateChildKeys(node, parentType) {
184
if (node._store) {
185
node._store.validated = true;
186
}
183
- } else if (node) {
187
+ } else {
188
const iteratorFn = getIteratorFn(node);
189
if (typeof iteratorFn === 'function') {
190
// Entry iterators used to provide implicit keys,
@@ -210,6 +214,9 @@ function validatePropTypes(element) {
214
if (type === null || type === undefined || typeof type === 'string') {
215
return;
216
}
217
+ if (type.$$typeof === REACT_CLIENT_REFERENCE) {
218
+ return;
219
+ }
220
let propTypes;
221
if (typeof type === 'function') {
222
propTypes = type.propTypes;
packages/react/src/jsx/ReactJSXElementValidator.js
+10
-3
@@ -32,6 +32,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
32
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
33
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
34
35
+const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
36
+
37
function setCurrentlyValidatingElement(element) {
38
if (__DEV__) {
39
if (element) {
@@ -179,10 +181,12 @@ function validateExplicitKey(element, parentType) {
181
*/
182
function validateChildKeys(node, parentType) {
183
if (__DEV__) {
182
- if (typeof node !== 'object') {
184
+ if (typeof node !== 'object' || !node) {
185
return;
186
}
185
- if (isArray(node)) {
187
+ if (node.$$typeof === REACT_CLIENT_REFERENCE) {
188
+ // This is a reference to a client component so it's unknown.
189
+ } else if (isArray(node)) {
190
for (let i = 0; i < node.length; i++) {
191
const child = node[i];
192
if (isValidElement(child)) {
@@ -194,7 +198,7 @@ function validateChildKeys(node, parentType) {
198
if (node._store) {
199
node._store.validated = true;
200
}
197
- } else if (node) {
201
+ } else {
202
const iteratorFn = getIteratorFn(node);
203
if (typeof iteratorFn === 'function') {
204
// Entry iterators used to provide implicit keys,
@@ -225,6 +229,9 @@ function validatePropTypes(element) {
229
if (type === null || type === undefined || typeof type === 'string') {
230
return;
231
}
232
+ if (type.$$typeof === REACT_CLIENT_REFERENCE) {
233
+ return;
234
+ }
235
let propTypes;
236
if (typeof type === 'function') {
237
propTypes = type.propTypes;