@samitouri / QOS-React / commits / 922dd7ba50

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;