-
Notifications
You must be signed in to change notification settings - Fork 46.4k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; |
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; |
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'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -47,21 +49,27 @@ | |
".": "./index.js", | ||
"./client": "./client.js", | ||
"./server": { | ||
"workerd": "./server.edge.js", | ||
"edge-light": "./server.edge.js", | ||
"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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wouldn't it make sense to also include the For context - we've shipped the What is different in the content of the files that you return here for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. “worker” doesn’t have AsyncLocalStorage. The other two would include |
||
"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", | ||
|
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, | ||
); | ||
} |
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}; |
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}; |
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'; |
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'); | ||
} |
There was a problem hiding this comment.
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 theworkerd
condition is used here if it points to the same file? What about other conditions likeedge-routine
,netlify
, etc?There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 resolvesbrowser
but notworker