From d93041a3a7e9d121959a8e755214aae84d4ba757 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 26 Aug 2022 00:15:00 +0200 Subject: [PATCH 1/8] Handle async module for client components --- package.json | 4 +- .../loaders/next-flight-client-loader.ts | 177 - .../next-flight-client-loader/index.ts | 24 + .../next-flight-client-loader/module-proxy.ts | 99 + ...bpack-writer.browser.development.server.js | 260 +- ...ck-writer.browser.production.min.server.js | 75 +- packages/next/package.json | 2 +- pnpm-lock.yaml | 16182 ++++++++++++---- test/e2e/app-dir/rsc-basic.test.ts | 39 +- .../app/external-imports/page.server.js | 8 +- .../non-isomorphic-text/browser.js | 3 + .../non-isomorphic-text/index.mjs | 5 + .../non-isomorphic-text/package.json | 2 +- .../random-module-instance/index.js | 0 .../random-module-instance/package.json | 0 .../non-isomorphic-text/browser.js | 1 - .../non-isomorphic-text/index.js | 1 - 17 files changed, 12235 insertions(+), 4647 deletions(-) delete mode 100644 packages/next/build/webpack/loaders/next-flight-client-loader.ts create mode 100644 packages/next/build/webpack/loaders/next-flight-client-loader/index.ts create mode 100644 packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts create mode 100644 test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/browser.js create mode 100644 test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/index.mjs rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/non-isomorphic-text/package.json (75%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/random-module-instance/index.js (100%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/random-module-instance/package.json (100%) delete mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/browser.js delete mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/index.js diff --git a/package.json b/package.json index 60f97bfdcc1605d..103dbed18d5171d 100644 --- a/package.json +++ b/package.json @@ -177,8 +177,8 @@ "react-17": "npm:react@17.0.2", "react-dom": "18.2.0", "react-dom-17": "npm:react-dom@17.0.2", - "react-dom-exp": "npm:react-dom@0.0.0-experimental-6ef466c68-20220816", - "react-exp": "npm:react@0.0.0-experimental-6ef466c68-20220816", + "react-dom-exp": "npm:react-dom@0.0.0-experimental-c8b778b7f-20220825", + "react-exp": "npm:react@0.0.0-experimental-c8b778b7f-20220825", "react-ssr-prepass": "1.0.8", "react-virtualized": "9.22.3", "relay-compiler": "13.0.2", diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts deleted file mode 100644 index cede5eae3ee0edc..000000000000000 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { promisify } from 'util' - -import { parse } from '../../swc' -import { buildExports, isNextBuiltinClientComponent } from './utils' - -function addExportNames(names: string[], node: any) { - if (!node) return - switch (node.type) { - case 'Identifier': - names.push(node.value) - return - case 'ObjectPattern': - for (let i = 0; i < node.properties.length; i++) - addExportNames(names, node.properties[i]) - return - case 'ArrayPattern': - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i] - if (element) addExportNames(names, element) - } - return - case 'Property': - addExportNames(names, node.value) - return - case 'AssignmentPattern': - addExportNames(names, node.left) - return - case 'RestElement': - addExportNames(names, node.argument) - return - case 'ParenthesizedExpression': - addExportNames(names, node.expression) - return - default: - return - } -} - -async function collectExports( - resourcePath: string, - transformedSource: string, - { - resolve, - loadModule, - }: { - resolve: (request: string) => Promise - loadModule: (request: string) => Promise - } -) { - const names: string[] = [] - - // Next.js built-in client components - if (isNextBuiltinClientComponent(resourcePath)) { - names.push('default') - } - - const { body } = await parse(transformedSource, { - filename: resourcePath, - isModule: 'unknown', - }) - - for (let i = 0; i < body.length; i++) { - const node = body[i] - - switch (node.type) { - case 'ExportDefaultExpression': - case 'ExportDefaultDeclaration': - names.push('default') - break - case 'ExportNamedDeclaration': - if (node.declaration) { - if (node.declaration.type === 'VariableDeclaration') { - const declarations = node.declaration.declarations - for (let j = 0; j < declarations.length; j++) { - addExportNames(names, declarations[j].id) - } - } else { - addExportNames(names, node.declaration.id) - } - } - if (node.specifiers) { - const specifiers = node.specifiers - for (let j = 0; j < specifiers.length; j++) { - addExportNames(names, specifiers[j].exported) - } - } - break - case 'ExportDeclaration': - if (node.declaration?.identifier) { - addExportNames(names, node.declaration.identifier) - } - break - case 'ExpressionStatement': { - const { - expression: { left }, - } = node - // exports.xxx = xxx - if ( - left?.object && - left.type === 'MemberExpression' && - left.object.type === 'Identifier' && - left.object.value === 'exports' - ) { - addExportNames(names, left.property) - } - break - } - case 'ExportAllDeclaration': - if (node.exported) { - addExportNames(names, node.exported) - break - } - - const reexportedFromResourcePath = await resolve(node.source.value) - const reexportedFromResourceSource = await loadModule( - reexportedFromResourcePath - ) - - names.push( - ...(await collectExports( - reexportedFromResourcePath, - reexportedFromResourceSource, - { resolve, loadModule } - )) - ) - continue - default: - break - } - } - - return names -} - -export default async function transformSource( - this: any, - source: string -): Promise { - const { resourcePath, resolve, loadModule, context } = this - - const transformedSource = source - if (typeof transformedSource !== 'string') { - throw new Error('Expected source to have been transformed to a string.') - } - - const names = await collectExports(resourcePath, transformedSource, { - resolve: (...args) => promisify(resolve)(context, ...args), - loadModule: promisify(loadModule), - }) - - const moduleRefDef = - "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n" - - const isNextClientBuiltIn = isNextBuiltinClientComponent(resourcePath) - - const clientRefsExports = names.reduce((res: any, name) => { - const moduleRef = - '{ $$typeof: MODULE_REFERENCE, filepath: ' + - JSON.stringify(resourcePath) + - ', name: ' + - JSON.stringify(name === 'default' && isNextClientBuiltIn ? '' : name) + - ' };\n' - res[name] = moduleRef - return res - }, {}) - - // still generate module references in ESM - const output = moduleRefDef + buildExports(clientRefsExports, true) - return output -} diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts new file mode 100644 index 000000000000000..d475e1376fb846c --- /dev/null +++ b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default async function transformSource( + this: any, + source: string +): Promise { + const { resourcePath } = this + + const transformedSource = source + if (typeof transformedSource !== 'string') { + throw new Error('Expected source to have been transformed to a string.') + } + + const output = ` +const { createProxy } = require("next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy")\n +module.exports = createProxy(${JSON.stringify(resourcePath)}) +` + return output +} diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts b/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts new file mode 100644 index 000000000000000..e6cf4ac2ad8c38a --- /dev/null +++ b/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Modified from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js + +const MODULE_REFERENCE = Symbol.for('react.module.reference') +const PROMISE_PROTOTYPE = Promise.prototype + +const proxyHandlers: ProxyHandler = { + get: function (target: any, name: string, receiver: any) { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + // These names are a little too common. We should probably have a way to + // have the Flight runtime extract the inner target instead. + return target.$$typeof + case 'filepath': + return target.filepath + case 'name': + return target.name + case 'async': + return target.async + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined + case '__esModule': + // Something is conditionally checking which export to use. We'll pretend to be + // an ESM compat module but then we'll check again on the client. + target.default = { + $$typeof: MODULE_REFERENCE, + filepath: target.filepath, + // This a placeholder value that tells the client to conditionally use the + // whole object or just the default export. + name: '', + async: target.async, + } + return true + case 'then': + if (!target.async) { + // If this module is expected to return a Promise (such as an AsyncModule) then + // we should resolve that with a client reference that unwraps the Promise on + // the client. + const then = function then( + resolve: (res: any) => void, + reject: (err: any) => void + ) { + const moduleReference: Record = { + $$typeof: MODULE_REFERENCE, + filepath: target.filepath, + name: '*', // Represents the whole object instead of a particular import. + async: true, + } + return Promise.resolve( + resolve(new Proxy(moduleReference, proxyHandlers)) + ) + } + // If this is not used as a Promise but is treated as a reference to a `.then` + // export then we should treat it as a reference to that name. + then.$$typeof = MODULE_REFERENCE + then.filepath = target.filepath + // then.name is conveniently already "then" which is the export name we need. + // This will break if it's minified though. + return then + } + } + let cachedReference = target[name] + if (!cachedReference) { + cachedReference = target[name] = { + $$typeof: MODULE_REFERENCE, + filepath: target.filepath, + name: name, + async: target.async, + } + } + return cachedReference + }, + getPrototypeOf(target: object) { + // Pretend to be a Promise in case anyone asks. + return PROMISE_PROTOTYPE + }, + set: function () { + throw new Error('Cannot assign to a client module from a server module.') + }, +} + +export function createProxy(moduleId: string) { + const moduleReference = { + $$typeof: MODULE_REFERENCE, + filepath: moduleId, + name: '*', // Represents the whole object instead of a particular import. + async: false, + } + return new Proxy(moduleReference, proxyHandlers) +} diff --git a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js index b1dc9903bcb313c..75a92df9b1958b9 100644 --- a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js +++ b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js @@ -166,6 +166,11 @@ function processModelChunk(request, id, model) { var row = serializeRowHeader('J', id) + json + '\n'; return stringToChunk(row); } +function processReferenceChunk(request, id, reference) { + var json = stringify(reference); + var row = serializeRowHeader('J', id) + json + '\n'; + return stringToChunk(row); +} function processModuleChunk(request, id, moduleMetaData) { var json = stringify(moduleMetaData); var row = serializeRowHeader('M', id) + json + '\n'; @@ -502,6 +507,7 @@ var startInlineScript = stringToPrecomputedChunk(''); var startScriptSrc = stringToPrecomputedChunk(''); var textSeparator = stringToPrecomputedChunk(''); @@ -537,10 +543,12 @@ var startPendingSuspenseBoundary1 = stringToPrecomputedChunk(''); var startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(''); var endSuspenseBoundary = stringToPrecomputedChunk(''); -var clientRenderedSuspenseBoundaryError1 = stringToPrecomputedChunk(''); +var clientRenderedSuspenseBoundaryError1 = stringToPrecomputedChunk(''); var endSegmentHTML = stringToPrecomputedChunk(''); @@ -570,7 +578,7 @@ var endSegmentColGroup = stringToPrecomputedChunk(''); // const SUSPENSE_PENDING_START_DATA = '$?'; // const SUSPENSE_FALLBACK_START_DATA = '$!'; // -// function clientRenderBoundary(suspenseBoundaryID, errorHash, errorMsg, errorComponentStack) { +// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) { // // Find the fallback's first element. // const suspenseIdNode = document.getElementById(suspenseBoundaryID); // if (!suspenseIdNode) { @@ -584,9 +592,9 @@ var endSegmentColGroup = stringToPrecomputedChunk(''); // suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; // // assign error metadata to first sibling // let dataset = suspenseIdNode.dataset; -// if (errorHash) dataset.hash = errorHash; +// if (errorDigest) dataset.dgst = errorDigest; // if (errorMsg) dataset.msg = errorMsg; -// if (errorComponentStack) dataset.stack = errorComponentStack; +// if (errorComponentStack) dataset.stck = errorComponentStack; // // Tell React to retry it if the parent already hydrated. // if (suspenseNode._reactRetry) { // suspenseNode._reactRetry(); @@ -670,7 +678,7 @@ var endSegmentColGroup = stringToPrecomputedChunk(''); var completeSegmentFunction = 'function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)}'; var completeBoundaryFunction = 'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}'; -var clientRenderFunction = 'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.hash=c),d&&(a.msg=d),e&&(a.stack=e),b._reactRetry&&b._reactRetry())}'; +var clientRenderFunction = 'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())}'; var completeSegmentScript1Full = stringToPrecomputedChunk(completeSegmentFunction + ';$RS("'); var completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("'); var completeSegmentScript2 = stringToPrecomputedChunk('","'); @@ -936,6 +944,9 @@ var Dispatcher = { useSyncExternalStore: unsupportedHook, useCacheRefresh: function () { return unsupportedRefresh; + }, + useMemoCache: function (size) { + return new Array(size); } }; @@ -977,6 +988,10 @@ function getOrCreateServerContext(globalName) { return ContextRegistry[globalName]; } +var PENDING = 0; +var COMPLETED = 1; +var ABORTED = 3; +var ERRORED = 4; var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function defaultErrorHandler(error) { @@ -987,7 +1002,8 @@ var OPEN = 0; var CLOSING = 1; var CLOSED = 2; function createRequest(model, bundlerConfig, onError, context, identifierPrefix) { - var pingedSegments = []; + var abortSet = new Set(); + var pingedTasks = []; var request = { status: OPEN, fatalError: null, @@ -996,7 +1012,8 @@ function createRequest(model, bundlerConfig, onError, context, identifierPrefix) cache: new Map(), nextChunkId: 0, pendingChunks: 0, - pingedSegments: pingedSegments, + abortableTasks: abortSet, + pingedTasks: pingedTasks, completedModuleChunks: [], completedJSONChunks: [], completedErrorChunks: [], @@ -1012,8 +1029,8 @@ function createRequest(model, bundlerConfig, onError, context, identifierPrefix) }; request.pendingChunks++; var rootContext = createRootContext(context); - var rootSegment = createSegment(request, model, rootContext); - pingedSegments.push(rootSegment); + var rootTask = createTask(request, model, rootContext, abortSet); + pingedTasks.push(rootTask); return request; } @@ -1032,7 +1049,12 @@ function attemptResolveElement(type, key, ref, props) { } if (typeof type === 'function') { - // This is a server-side component. + if (isModuleReference(type)) { + // This is a reference to a client component. + return [REACT_ELEMENT_TYPE, type, key, props]; + } // This is a server-side component. + + return type(props); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. @@ -1106,28 +1128,30 @@ function attemptResolveElement(type, key, ref, props) { throw new Error("Unsupported server component type: " + describeValueForErrorMessage(type)); } -function pingSegment(request, segment) { - var pingedSegments = request.pingedSegments; - pingedSegments.push(segment); +function pingTask(request, task) { + var pingedTasks = request.pingedTasks; + pingedTasks.push(task); - if (pingedSegments.length === 1) { + if (pingedTasks.length === 1) { scheduleWork(function () { return performWork(request); }); } } -function createSegment(request, model, context) { +function createTask(request, model, context, abortSet) { var id = request.nextChunkId++; - var segment = { + var task = { id: id, + status: PENDING, model: model, context: context, ping: function () { - return pingSegment(request, segment); + return pingTask(request, task); } }; - return segment; + abortSet.add(task); + return task; } function serializeByValueID(id) { @@ -1138,6 +1162,49 @@ function serializeByRefID(id) { return '@' + id.toString(16); } +function serializeModuleReference(request, parent, key, moduleReference) { + var moduleKey = getModuleKey(moduleReference); + var writtenModules = request.writtenModules; + var existingId = writtenModules.get(moduleKey); + + if (existingId !== undefined) { + if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { + // If we're encoding the "type" of an element, we can refer + // to that by a lazy reference instead of directly since React + // knows how to deal with lazy values. This lets us suspend + // on this component rather than its parent until the code has + // loaded. + return serializeByRefID(existingId); + } + + return serializeByValueID(existingId); + } + + try { + var moduleMetaData = resolveModuleMetaData(request.bundlerConfig, moduleReference); + request.pendingChunks++; + var moduleId = request.nextChunkId++; + emitModuleChunk(request, moduleId, moduleMetaData); + writtenModules.set(moduleKey, moduleId); + + if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { + // If we're encoding the "type" of an element, we can refer + // to that by a lazy reference instead of directly since React + // knows how to deal with lazy values. This lets us suspend + // on this component rather than its parent until the code has + // loaded. + return serializeByRefID(moduleId); + } + + return serializeByValueID(moduleId); + } catch (x) { + request.pendingChunks++; + var errorId = request.nextChunkId++; + emitErrorChunk(request, errorId, x); + return serializeByValueID(errorId); + } +} + function escapeStringValue(value) { if (value[0] === '$' || value[0] === '@') { // We need to escape $ or @ prefixed strings since we use those to encode @@ -1362,12 +1429,12 @@ function resolveModelToJSON(request, parent, key, value) { } } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { - // Something suspended, we'll need to create a new segment and resolve it later. + // Something suspended, we'll need to create a new task and resolve it later. request.pendingChunks++; - var newSegment = createSegment(request, value, getActiveContext()); - var ping = newSegment.ping; + var newTask = createTask(request, value, getActiveContext(), request.abortableTasks); + var ping = newTask.ping; x.then(ping, ping); - return serializeByRefID(newSegment.id); + return serializeByRefID(newTask.id); } else { logRecoverableError(request, x); // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client @@ -1387,49 +1454,7 @@ function resolveModelToJSON(request, parent, key, value) { if (typeof value === 'object') { if (isModuleReference(value)) { - var moduleReference = value; - var moduleKey = getModuleKey(moduleReference); - var writtenModules = request.writtenModules; - var existingId = writtenModules.get(moduleKey); - - if (existingId !== undefined) { - if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { - // If we're encoding the "type" of an element, we can refer - // to that by a lazy reference instead of directly since React - // knows how to deal with lazy values. This lets us suspend - // on this component rather than its parent until the code has - // loaded. - return serializeByRefID(existingId); - } - - return serializeByValueID(existingId); - } - - try { - var moduleMetaData = resolveModuleMetaData(request.bundlerConfig, moduleReference); - request.pendingChunks++; - var moduleId = request.nextChunkId++; - emitModuleChunk(request, moduleId, moduleMetaData); - writtenModules.set(moduleKey, moduleId); - - if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { - // If we're encoding the "type" of an element, we can refer - // to that by a lazy reference instead of directly since React - // knows how to deal with lazy values. This lets us suspend - // on this component rather than its parent until the code has - // loaded. - return serializeByRefID(moduleId); - } - - return serializeByValueID(moduleId); - } catch (x) { - request.pendingChunks++; - - var _errorId = request.nextChunkId++; - - emitErrorChunk(request, _errorId, x); - return serializeByValueID(_errorId); - } + return serializeModuleReference(request, parent, key, value); } else if (value.$$typeof === REACT_PROVIDER_TYPE) { var providerKey = value._context._globalName; var writtenProviders = request.writtenProviders; @@ -1483,6 +1508,10 @@ function resolveModelToJSON(request, parent, key, value) { } if (typeof value === 'function') { + if (isModuleReference(value)) { + return serializeModuleReference(request, parent, key, value); + } + if (/^on[A-Z]/.test(key)) { throw new Error('Event handlers cannot be passed to client component props. ' + ("Remove " + describeKeyForErrorMessage(key) + " from these props if possible: " + describeObjectForErrorMessage(parent) + "\n") + 'If you need interactivity, consider converting part of this to a client component.'); } else { @@ -1492,11 +1521,10 @@ function resolveModelToJSON(request, parent, key, value) { if (typeof value === 'symbol') { var writtenSymbols = request.writtenSymbols; + var existingId = writtenSymbols.get(value); - var _existingId = writtenSymbols.get(value); - - if (_existingId !== undefined) { - return serializeByValueID(_existingId); + if (existingId !== undefined) { + return serializeByValueID(existingId); } var name = value.description; @@ -1575,34 +1603,43 @@ function emitProviderChunk(request, id, contextName) { request.completedJSONChunks.push(processedChunk); } -function retrySegment(request, segment) { - switchContext(segment.context); +function retryTask(request, task) { + if (task.status !== PENDING) { + // We completed this by other means before we had a chance to retry it. + return; + } + + switchContext(task.context); try { - var _value3 = segment.model; + var _value3 = task.model; while (typeof _value3 === 'object' && _value3 !== null && _value3.$$typeof === REACT_ELEMENT_TYPE) { // TODO: Concatenate keys of parents onto children. var element = _value3; // Attempt to render the server component. - // Doing this here lets us reuse this same segment if the next component + // Doing this here lets us reuse this same task if the next component // also suspends. - segment.model = _value3; + task.model = _value3; _value3 = attemptResolveElement(element.type, element.key, element.ref, element.props); } - var processedChunk = processModelChunk(request, segment.id, _value3); + var processedChunk = processModelChunk(request, task.id, _value3); request.completedJSONChunks.push(processedChunk); + request.abortableTasks.delete(task); + task.status = COMPLETED; } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended again, let's pick it back up later. - var ping = segment.ping; + var ping = task.ping; x.then(ping, ping); return; } else { + request.abortableTasks.delete(task); + task.status = ERRORED; logRecoverableError(request, x); // This errored, we need to serialize this error to the - emitErrorChunk(request, segment.id, x); + emitErrorChunk(request, task.id, x); } } } @@ -1615,12 +1652,12 @@ function performWork(request) { prepareToUseHooksForRequest(request); try { - var pingedSegments = request.pingedSegments; - request.pingedSegments = []; + var pingedTasks = request.pingedTasks; + request.pingedTasks = []; - for (var i = 0; i < pingedSegments.length; i++) { - var segment = pingedSegments[i]; - retrySegment(request, segment); + for (var i = 0; i < pingedTasks.length; i++) { + var task = pingedTasks[i]; + retryTask(request, task); } if (request.destination !== null) { @@ -1636,6 +1673,15 @@ function performWork(request) { } } +function abortTask(task, request, errorId) { + task.status = ABORTED; // Instead of emitting an error per task.id, we emit a model that only + // has a single value referencing the error. + + var ref = serializeByValueID(errorId); + var processedChunk = processReferenceChunk(request, task.id, ref); + request.completedJSONChunks.push(processedChunk); +} + function flushCompletedChunks(request, destination) { beginWriting(); @@ -1735,6 +1781,34 @@ function startFlowing(request, destination) { logRecoverableError(request, error); fatalError(request, error); } +} // This is called to early terminate a request. It creates an error at all pending tasks. + +function abort(request, reason) { + try { + var abortableTasks = request.abortableTasks; + + if (abortableTasks.size > 0) { + // We have tasks to abort. We'll emit one error row and then emit a reference + // to that row from every row that's still remaining. + var _error = reason === undefined ? new Error('The render was aborted by the server without a reason.') : reason; + + logRecoverableError(request, _error); + request.pendingChunks++; + var errorId = request.nextChunkId++; + emitErrorChunk(request, errorId, _error); + abortableTasks.forEach(function (task) { + return abortTask(task, request, errorId); + }); + abortableTasks.clear(); + } + + if (request.destination !== null) { + flushCompletedChunks(request, request.destination); + } + } catch (error) { + logRecoverableError(request, error); + fatalError(request, error); + } } function importServerContexts(contexts) { @@ -1760,6 +1834,22 @@ function importServerContexts(contexts) { function renderToReadableStream(model, webpackMap, options) { var request = createRequest(model, webpackMap, options ? options.onError : undefined, options ? options.context : undefined, options ? options.identifierPrefix : undefined); + + if (options && options.signal) { + var signal = options.signal; + + if (signal.aborted) { + abort(request, signal.reason); + } else { + var listener = function () { + abort(request, signal.reason); + signal.removeEventListener('abort', listener); + }; + + signal.addEventListener('abort', listener); + } + } + var stream = new ReadableStream({ type: 'bytes', start: function (controller) { diff --git a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js index 1a013d84b6ea5b1..1df8f931cb3f951 100644 --- a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js +++ b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js @@ -7,39 +7,42 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -'use strict';var e=require("react"),k=null,m=0;function n(a,b){if(0!==b.length)if(512