Skip to content

Commit

Permalink
Merge branch 'main' into jestbump
Browse files Browse the repository at this point in the history
  • Loading branch information
ymqy authored Feb 7, 2023
2 parents 52d5f01 + 1445acf commit 9a949a8
Show file tree
Hide file tree
Showing 30 changed files with 411 additions and 237 deletions.
195 changes: 67 additions & 128 deletions .circleci/config.yml

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions fixtures/flight/server/handler.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ module.exports = function (req, res) {
const App = m.default.default || m.default;
res.setHeader('Access-Control-Allow-Origin', '*');
const moduleMap = JSON.parse(data);
const {pipe} = renderToPipeableStream(React.createElement(App), {
clientManifest: moduleMap,
});
const {pipe} = renderToPipeableStream(
React.createElement(App),
moduleMap
);
pipe(res);
}
);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^3.0.1",
"filesize": "^6.0.1",
"flow-bin": "^0.196.3",
"flow-remove-types": "^2.196.1",
"flow-bin": "^0.199.0",
"flow-remove-types": "^2.198.2",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"google-closure-compiler": "^20200517.0.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,12 @@ export function parseModelString(
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
}
case '@': {
// Promise
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'S': {
return Symbol.for(value.substring(2));
}
Expand Down
23 changes: 13 additions & 10 deletions packages/react-reconciler/src/ReactFiberThenable.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export function trackUsedThenable<T>(
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop, noop);
} else {
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = 'pending';
Expand All @@ -107,17 +110,17 @@ export function trackUsedThenable<T>(
}
},
);
}

// Check one more time in case the thenable resolved synchronously
switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
throw rejectedThenable.reason;
}
// Check one more time in case the thenable resolved synchronously.
switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
throw rejectedThenable.reason;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ type Options = {

function renderToReadableStream(
model: ReactModel,
webpackMaps: BundlerConfig,
webpackMap: BundlerConfig,
options?: Options,
): ReadableStream {
const request = createRequest(
model,
webpackMaps,
webpackMap,
options ? options.onError : undefined,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ type PipeableStream = {

function renderToPipeableStream(
model: ReactModel,
webpackMaps: BundlerConfig,
webpackMap: BundlerConfig,
options?: Options,
): PipeableStream {
const request = createRequest(
model,
webpackMaps,
webpackMap,
options ? options.onError : undefined,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ type WebpackMap = {
},
};

export type BundlerConfig = {
clientManifest: WebpackMap,
};
export type BundlerConfig = WebpackMap;

// eslint-disable-next-line no-unused-vars
export type ClientReference<T> = {
Expand Down Expand Up @@ -56,7 +54,7 @@ export function resolveModuleMetaData<T>(
clientReference: ClientReference<T>,
): ModuleMetaData {
const resolvedModuleData =
config.clientManifest[clientReference.filepath][clientReference.name];
config[clientReference.filepath][clientReference.name];
if (clientReference.async) {
return {
id: resolvedModuleData.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ module.exports = function register() {
// reference.
case 'defaultProps':
return undefined;
case 'getDefaultProps':
return undefined;
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined;
Expand Down Expand Up @@ -91,8 +89,6 @@ module.exports = function register() {
// reference.
case 'defaultProps':
return undefined;
case 'getDefaultProps':
return undefined;
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined;
Expand All @@ -106,9 +102,9 @@ module.exports = function register() {
target.default = Object.defineProperties(
(function () {
throw new Error(
`Attempted to call the default export of ${moduleId} from the server` +
`Attempted to call the default export of ${moduleId} from the server ` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a` +
`the server, it can only be rendered as a Component or passed to props of a ` +
`Client Component.`,
);
}: any),
Expand All @@ -132,24 +128,13 @@ module.exports = function register() {
// we should resolve that with a client reference that unwraps the Promise on
// the client.

const innerModuleId = target.filepath;
const clientReference: Function = Object.defineProperties(
(function () {
throw new Error(
`Attempted to call the module exports of ${innerModuleId} from the server` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a` +
`Client Component.`,
);
}: any),
{
// Represents the whole object instead of a particular import.
name: {value: '*'},
$$typeof: {value: CLIENT_REFERENCE},
filepath: {value: target.filepath},
async: {value: true},
},
);
const clientReference = Object.defineProperties(({}: any), {
// Represents the whole Module object instead of a particular import.
name: {value: '*'},
$$typeof: {value: CLIENT_REFERENCE},
filepath: {value: target.filepath},
async: {value: true},
});
const proxy = new Proxy(clientReference, proxyHandlers);

// Treat this as a resolved Promise for React's use()
Expand Down Expand Up @@ -221,23 +206,13 @@ module.exports = function register() {
// $FlowFixMe[prop-missing] found when upgrading Flow
Module._extensions['.client.js'] = function (module, path) {
const moduleId: string = (url.pathToFileURL(path).href: any);
const clientReference: Function = Object.defineProperties(
(function () {
throw new Error(
`Attempted to call the module exports of ${moduleId} from the server` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a` +
`Client Component.`,
);
}: any),
{
// Represents the whole object instead of a particular import.
name: {value: '*'},
$$typeof: {value: CLIENT_REFERENCE},
filepath: {value: moduleId},
async: {value: false},
},
);
const clientReference = Object.defineProperties(({}: any), {
// Represents the whole Module object instead of a particular import.
name: {value: '*'},
$$typeof: {value: CLIENT_REFERENCE},
filepath: {value: moduleId},
async: {value: false},
});
// $FlowFixMe[incompatible-call] found when upgrading Flow
module.exports = new Proxy(clientReference, proxyHandlers);
};
Expand Down
138 changes: 136 additions & 2 deletions packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,94 @@ describe('ReactFlightDOM', () => {
expect(container.innerHTML).toBe('<p>@div</p>');
});

// @gate enableUseHook
it('should be able to esm compat test module references', async () => {
const ESMCompatModule = {
__esModule: true,
default: function ({greeting}) {
return greeting + ' World';
},
hi: 'Hello',
};

function Print({response}) {
return <p>{use(response)}</p>;
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

function interopWebpack(obj) {
// Basically what Webpack's ESM interop feature testing does.
if (typeof obj === 'object' && obj.__esModule) {
return obj;
}
return Object.assign({default: obj}, obj);
}

const {default: Component, hi} = interopWebpack(
clientExports(ESMCompatModule),
);

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<Component greeting={hi} />,
webpackMap,
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Hello World</p>');
});

// @gate enableUseHook
it('should be able to render a named component export', async () => {
const Module = {
Component: function ({greeting}) {
return greeting + ' World';
},
};

function Print({response}) {
return <p>{use(response)}</p>;
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

const {Component} = clientExports(Module);

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<Component greeting={'Hello'} />,
webpackMap,
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Hello World</p>');
});

// @gate enableUseHook
it('should unwrap async module references', async () => {
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
Expand Down Expand Up @@ -848,8 +936,8 @@ describe('ReactFlightDOM', () => {
});

// We simulate a bug in the Webpack bundler which causes an error on the server.
for (const id in webpackMap.clientManifest) {
Object.defineProperty(webpackMap.clientManifest, id, {
for (const id in webpackMap) {
Object.defineProperty(webpackMap, id, {
get: () => {
throw new Error('bug in the bundler');
},
Expand Down Expand Up @@ -905,4 +993,50 @@ describe('ReactFlightDOM', () => {

expect(reportedErrors).toEqual(['bug in the bundler']);
});

// @gate enableUseHook
it('should pass a Promise through props and be able use() it on the client', async () => {
async function getData() {
return 'async hello';
}

function Component({data}) {
const text = use(data);
return <p>{text}</p>;
}

const ClientComponent = clientExports(Component);

function ServerComponent() {
const data = getData(); // no await here
return <ClientComponent data={data} />;
}

function Print({response}) {
return use(response);
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<ServerComponent />,
webpackMap,
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>async hello</p>');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -473,14 +473,12 @@ describe('ReactFlightDOMBrowser', () => {
const ClientComponentOnTheServer = clientExports(ClientComponent);

// In the SSR bundle this module won't exist. We simulate this by deleting it.
const clientId =
webpackMap.clientManifest[ClientComponentOnTheClient.filepath]['*'].id;
const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
delete webpackModules[clientId];

// Instead, we have to provide a translation from the client meta data to the SSR
// meta data.
const ssrMetaData =
webpackMap.clientManifest[ClientComponentOnTheServer.filepath]['*'];
const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath]['*'];
const translationMap = {
[clientId]: {
'*': ssrMetaData,
Expand Down
Loading

0 comments on commit 9a949a8

Please sign in to comment.