diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 4a3c2d157dc01..0eb1c51f72622 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -15,6 +15,7 @@ import type { PreloadModuleOptions, PreinitOptions, PreinitModuleOptions, + ImportMap, } from 'react-dom/src/shared/ReactDOMTypes'; import { @@ -139,6 +140,7 @@ export type RenderState = { // Hoistable chunks charsetChunks: Array, preconnectChunks: Array, + importMapChunks: Array, preloadChunks: Array, hoistableChunks: Array, @@ -205,7 +207,7 @@ const scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="'); const endAsyncScript = stringToPrecomputedChunk('" async="">'); /** - * This escaping function is designed to work with bootstrapScriptContent only. + * This escaping function is designed to work with bootstrapScriptContent and importMap only. * because we know we are escaping the entire script. We can avoid for instance * escaping html comment string sequences that are valid javascript as well because * if there are no sebsequent '); * While untrusted script content should be made safe before using this api it will * ensure that the script cannot be early terminated or never terminated state */ -function escapeBootstrapScriptContent(scriptText: string) { +function escapeBootstrapAndImportMapScriptContent(scriptText: string) { if (__DEV__) { checkHtmlStringCoercion(scriptText); } @@ -237,12 +239,19 @@ export type ExternalRuntimeScript = { src: string, chunks: Array, }; + +const importMapScriptStart = stringToPrecomputedChunk( + ''); + // Allows us to keep track of what we've already written so we can refer back to it. // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag // is set, the server will send instructions via data attributes (instead of inline scripts) export function createRenderState( resumableState: ResumableState, nonce: string | void, + importMap: ImportMap | void, ): RenderState { const inlineScriptWithNonce = nonce === undefined @@ -251,6 +260,17 @@ export function createRenderState( '', + ); + }); + describe('error escaping', () => { it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => { window.__outlet = {}; @@ -3949,7 +3976,7 @@ describe('ReactDOMFizzServer', () => { ]); }); - describe('bootstrapScriptContent escaping', () => { + describe('bootstrapScriptContent and importMap escaping', () => { it('the "S" in " { window.__test_outlet = ''; const stringWithScriptsInIt = @@ -4005,6 +4032,24 @@ describe('ReactDOMFizzServer', () => { }); expect(window.__test_outlet).toBe(1); }); + + it('escapes in importMaps', async () => { + window.__test_outlet_key = ''; + window.__test_outlet_value = ''; + const jsonWithScriptsInIt = { + "keypos, + 'hello world', + ]); + }); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 1ce1150423e4e..0de26739b5696 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -10,6 +10,7 @@ import type {PostponedState} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -38,6 +39,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type ResumeOptions = { @@ -101,7 +103,11 @@ function renderToReadableStream( const request = createRequest( children, resumableState, - createRenderState(resumableState, options ? options.nonce : undefined), + createRenderState( + resumableState, + options ? options.nonce : undefined, + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, @@ -171,6 +177,7 @@ function resume( createRenderState( postponedState.resumableState, options ? options.nonce : undefined, + undefined, // importMap ), postponedState.rootFormatContext, postponedState.progressiveChunkSize, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index e3800b7debc59..997934e1a3d1a 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -9,6 +9,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -37,6 +38,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; // TODO: Move to sub-classing ReadableStream. @@ -93,7 +95,11 @@ function renderToReadableStream( const request = createRequest( children, resumableState, - createRenderState(resumableState, options ? options.nonce : undefined), + createRenderState( + resumableState, + options ? options.nonce : undefined, + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index 1ce1150423e4e..0de26739b5696 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -10,6 +10,7 @@ import type {PostponedState} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -38,6 +39,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type ResumeOptions = { @@ -101,7 +103,11 @@ function renderToReadableStream( const request = createRequest( children, resumableState, - createRenderState(resumableState, options ? options.nonce : undefined), + createRenderState( + resumableState, + options ? options.nonce : undefined, + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, @@ -171,6 +177,7 @@ function resume( createRenderState( postponedState.resumableState, options ? options.nonce : undefined, + undefined, // importMap ), postponedState.rootFormatContext, postponedState.progressiveChunkSize, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index ca4853a1533f7..89332ef5dc7de 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -12,6 +12,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {Writable} from 'stream'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -51,6 +52,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type ResumeOptions = { @@ -81,7 +83,11 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { return createRequest( children, resumableState, - createRenderState(resumableState, options ? options.nonce : undefined), + createRenderState( + resumableState, + options ? options.nonce : undefined, + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, @@ -140,6 +146,7 @@ function resumeRequestImpl( createRenderState( postponedState.resumableState, options ? options.nonce : undefined, + undefined, // importMap ), postponedState.rootFormatContext, postponedState.progressiveChunkSize, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index de603b20ac8e9..0e03530b2d885 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -10,6 +10,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -38,6 +39,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type StaticResult = { @@ -81,7 +83,11 @@ function prerender( const request = createRequest( children, resources, - createRenderState(resources, undefined), + createRenderState( + resources, + undefined, // nonce + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index de603b20ac8e9..0e03530b2d885 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -10,6 +10,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -38,6 +39,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type StaticResult = { @@ -81,7 +83,11 @@ function prerender( const request = createRequest( children, resources, - createRenderState(resources, undefined), + createRenderState( + resources, + undefined, // nonce + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index d22e1ea2d06f2..d538eb9f81ce9 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -10,6 +10,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type {ImportMap} from '../shared/ReactDOMTypes'; import {Writable, Readable} from 'stream'; @@ -40,6 +41,7 @@ type Options = { onError?: (error: mixed) => ?string, onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, + importMap?: ImportMap, }; type StaticResult = { @@ -95,7 +97,11 @@ function prerenderToNodeStream( const request = createRequest( children, resumableState, - createRenderState(resumableState, undefined), + createRenderState( + resumableState, + undefined, // nonce + options ? options.importMap : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js index 9b8bf37c04dee..3e8c7d1a79d22 100644 --- a/packages/react-dom/src/shared/ReactDOMTypes.js +++ b/packages/react-dom/src/shared/ReactDOMTypes.js @@ -47,3 +47,14 @@ export type HostDispatcher = { preinit: (href: string, options: PreinitOptions) => void, preinitModule: (href: string, options?: ?PreinitModuleOptions) => void, }; + +export type ImportMap = { + imports?: { + [specifier: string]: string, + }, + scopes?: { + [scope: string]: { + [specifier: string]: string, + }, + }, +}; diff --git a/packages/react-dom/src/test-utils/FizzTestUtils.js b/packages/react-dom/src/test-utils/FizzTestUtils.js index a10bec7fa0dee..545743a6445b7 100644 --- a/packages/react-dom/src/test-utils/FizzTestUtils.js +++ b/packages/react-dom/src/test-utils/FizzTestUtils.js @@ -104,9 +104,9 @@ async function executeScript(script: Element) { const newScript = ownerDocument.createElement('script'); newScript.textContent = script.textContent; // make sure to add nonce back to script if it exists - const scriptNonce = script.getAttribute('nonce'); - if (scriptNonce) { - newScript.setAttribute('nonce', scriptNonce); + for (let i = 0; i < script.attributes.length; i++) { + const attribute = script.attributes[i]; + newScript.setAttribute(attribute.name, attribute.value); } parent.insertBefore(newScript, script);