Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Edge Server Builds for workerd / edge-light #26116

Merged
merged 4 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
18 changes: 18 additions & 0 deletions packages/react-dom/npm/server.edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

var b;
var l;
if (process.env.NODE_ENV === 'production') {
b = require('./cjs/react-dom-server.edge.production.min.js');
l = require('./cjs/react-dom-server-legacy.browser.production.min.js');
} else {
b = require('./cjs/react-dom-server.edge.development.js');
l = require('./cjs/react-dom-server-legacy.browser.development.js');
}

exports.version = b.version;
exports.renderToReadableStream = b.renderToReadableStream;
exports.renderToNodeStream = b.renderToNodeStream;
exports.renderToStaticNodeStream = b.renderToStaticNodeStream;
exports.renderToString = l.renderToString;
exports.renderToStaticMarkup = l.renderToStaticMarkup;
7 changes: 7 additions & 0 deletions packages/react-dom/npm/static.edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-static.edge.production.min.js');
} else {
module.exports = require('./cjs/react-dom-static.edge.development.js');
}
8 changes: 8 additions & 0 deletions packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
"profiling.js",
"server.js",
"server.browser.js",
"server.edge.js",
"server.node.js",
"server.bun.js",
"static.js",
"static.browser.js",
"static.edge.js",
"static.node.js",
"server-rendering-stub.js",
"test-utils.js",
Expand All @@ -47,21 +49,27 @@
".": "./index.js",
"./client": "./client.js",
"./server": {
"workerd": "./server.edge.js",
"edge-light": "./server.edge.js",
Comment on lines +52 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't edge-light supposed to be a "catch-all" for those environments discussed by WinterCG? If yes, why the workerd condition is used here if it points to the same file? What about other conditions like edge-routine, netlify, etc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edge-light is specifically Vercel's environment. These are the only ones we've tested and evaluated the other ones if they work as expected. Afaik, the other ones don't have AsyncLocalStorage yet so they'd use the browser build.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any condition that acts as a catch-all? I understand that it might not work for your use case here but some other libraries might just want to ship something that works in any worker-like environment. The browser condition doesn't work well for that right now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I've looked into that before. Mainly the thing that sucks is that there's no distinction between "serviceworker" and "worker"-like on the server isn't the same a "worker" on the client. E.g. these APIs can't really be used in actual web workers but can be used in "serviceworker". So I've been waiting on further clarification on that before moving further.

I think the notion of these environments just being "a simple worker" is getting to be outdated as more features are added. I suspect it'll be more like a special "wintercg worker" concept or something that's a superset that's useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely not always possible to use a single condition to catch them all - but as long as you only need some subset of the functionality, then it makes sense to reuse the condition.

Do you have any opinions about Next adding the support for worker condition as well? It just looks so off to me that it resolves browser but not worker

"bun": "./server.bun.js",
"deno": "./server.browser.js",
"worker": "./server.browser.js",
"browser": "./server.browser.js",
"default": "./server.node.js"
},
"./server.browser": "./server.browser.js",
"./server.edge": "./server.edge.js",
"./server.node": "./server.node.js",
"./static": {
"workerd": "./static.edge.js",
"edge-light": "./static.edge.js",
"deno": "./static.browser.js",
"worker": "./static.browser.js",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently abuse the browser builds for Web streams derived environments. We already have a special build for Bun but we should also have one for other "edge" runtimes so that we can maximally take advantage of the APIs that exist on each platform.

Wouldn't it make sense to also include the worker condition (that is defined here) when Next.js bundles for the edge-light runtime? The worker condition is IMHO closer to edge-light than the browser condition is to it. So it's a little bit of a weird situation that browser is automatically picked up but the worker is not.

For context - we've shipped the worker condition in Emotion in hope that it would get picked up by tools. There were no strong signals that other conditions would have to be needed and this seemed like a good "catch all" at the time.

What is different in the content of the files that you return here for the edge-light and worker?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“worker” doesn’t have AsyncLocalStorage. The other two would include require(“node:async_hooks”) which would fail on worker builds.

"browser": "./static.browser.js",
"default": "./static.node.js"
},
"./static.browser": "./static.browser.js",
"./static.edge": "./static.edge.js",
"./static.node": "./static.node.js",
"./server-rendering-stub": "./server-rendering-stub.js",
"./profiling": "./profiling.js",
Expand Down
47 changes: 47 additions & 0 deletions packages/react-dom/server.edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// This file is only used for tests.
// It lazily loads the implementation so that we get the correct set of host configs.

import ReactVersion from 'shared/ReactVersion';
export {ReactVersion as version};

export function renderToReadableStream() {
return require('./src/server/ReactDOMFizzServerEdge').renderToReadableStream.apply(
this,
arguments,
);
}

export function renderToNodeStream() {
return require('./src/server/ReactDOMFizzServerEdge').renderToNodeStream.apply(
this,
arguments,
);
}

export function renderToStaticNodeStream() {
return require('./src/server/ReactDOMFizzServerEdge').renderToStaticNodeStream.apply(
this,
arguments,
);
}

export function renderToString() {
return require('./src/server/ReactDOMLegacyServerBrowser').renderToString.apply(
this,
arguments,
);
}

export function renderToStaticMarkup() {
return require('./src/server/ReactDOMLegacyServerBrowser').renderToStaticMarkup.apply(
this,
arguments,
);
}
116 changes: 116 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerEdge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactNodeList} from 'shared/ReactTypes';
import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';

import ReactVersion from 'shared/ReactVersion';

import {
createRequest,
startWork,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

import {
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

// TODO: Move to sub-classing ReadableStream.
type ReactDOMServerReadableStream = ReadableStream & {
allReady: Promise<void>,
};

function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): Promise<ReactDOMServerReadableStream> {
return new Promise((resolve, reject) => {
let onFatalError;
let onAllReady;
const allReady = new Promise((res, rej) => {
onAllReady = res;
onFatalError = rej;
});

function onShellReady() {
const stream: ReactDOMServerReadableStream = (new ReadableStream(
{
type: 'bytes',
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
abort(request);
},
},
// $FlowFixMe size() methods are not allowed on byte streams.
{highWaterMark: 0},
): any);
// TODO: Move to sub-classing ReadableStream.
stream.allReady = allReady;
resolve(stream);
}
function onShellError(error: mixed) {
// If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
// However, `allReady` will be rejected by `onFatalError` as well.
// So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
allReady.catch(() => {});
reject(error);
}
const request = createRequest(
children,
createResponseState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
onAllReady,
onShellReady,
onShellError,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}

export {renderToReadableStream, ReactVersion as version};
101 changes: 101 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactNodeList} from 'shared/ReactTypes';
import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';

import ReactVersion from 'shared/ReactVersion';

import {
createRequest,
startWork,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

import {
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

type StaticResult = {
prelude: ReadableStream,
};

function prerender(
children: ReactNodeList,
options?: Options,
): Promise<StaticResult> {
return new Promise((resolve, reject) => {
const onFatalError = reject;

function onAllReady() {
const stream = new ReadableStream(
{
type: 'bytes',
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
},
// $FlowFixMe size() methods are not allowed on byte streams.
{highWaterMark: 0},
);

const result = {
prelude: stream,
};
resolve(result);
}
const request = createRequest(
children,
createResponseState(
options ? options.identifierPrefix : undefined,
undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
onAllReady,
undefined,
undefined,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}

export {prerender, ReactVersion as version};
10 changes: 10 additions & 0 deletions packages/react-dom/static.edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export {prerender, version} from './src/server/ReactDOMFizzStaticEdge';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export * from 'react-dom-bindings/src/client/ReactDOMHostConfig';
7 changes: 7 additions & 0 deletions packages/react-server-dom-webpack/npm/server.edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-server-dom-webpack-server.edge.production.min.js');
} else {
module.exports = require('./cjs/react-server-dom-webpack-server.edge.development.js');
}
Loading