[Float][Fizz][Static] add importMap option to Fizz and Static server renderers (#27260)

Import maps need to be emitted before any scripts or preloads so the
browser can properly locate these resources.

Unlike most scripts, importmaps are singletons meaning you can only have
one per document and they must appear before any modules are loaded or
preloaded. In the future there may be a way to dynamically add more
mappings however the proposed API for this seems likely to be a
javascript API and not an html tag.

Given the unique constraints here this PR implements React's support of
importMaps as the following

1. an `importMap` option accepting a plain object mapping module
specifier to path is accepted in any API that renders a preamble (head
content). Notably this precludes resume rendering because in resume
cases the preamble should have already been produced as part of the
prerender step.
2. the importMap is stringified and emitted as a `<script
type="importmap">...</script>` in the preamble.
3. the importMap is escaped identically to how bootstrapScriptContent is
escaped, notably, isntances of `</script>` are escaped to avoid breaking
out of the script context

Users can still render importmap tags however with Float enabled this is
rather pointless as most modules will be hoisted above the importmap
that is rendered. In practice this means the only functional way to use
import maps with React is to use this config API.

DiffTrain build for [9d4582dffd](https://github.com/facebook/react/commit/9d4582dffdea5b4dcb6a6093ea848d15423c7701)
This commit is contained in:
gnoff
2023-08-24 20:53:38 +00:00
parent 2bd9065dd7
commit 3129e28505
7 changed files with 148 additions and 23 deletions
+1 -1
View File
@@ -1 +1 @@
b4cdd3e8922713f8c9817b004a0dc51be47bc5df
9d4582dffdea5b4dcb6a6093ea848d15423c7701
@@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");
var ReactVersion = "18.3.0-www-classic-1747ba9c";
var ReactVersion = "18.3.0-www-classic-dbbf9e79";
// This refers to a WWW module.
var warningWWW = require("warning");
@@ -1910,7 +1910,7 @@ var scriptIntegirty = stringToPrecomputedChunk('" integrity="');
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
/**
* 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 <script sequences the html parser will never enter
@@ -1920,7 +1920,7 @@ var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
* ensure that the script cannot be early terminated or never terminated state
*/
function escapeBootstrapScriptContent(scriptText) {
function escapeBootstrapAndImportMapScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
@@ -1932,11 +1932,16 @@ var scriptRegex = /(<\/|<)(s)(cript)/gi;
var scriptReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\u0073" : "\\u0053") + suffix;
}; // Allows us to keep track of what we've already written so we can refer back to it.
};
var importMapScriptStart = stringToPrecomputedChunk(
'<script type="importmap">'
);
var importMapScriptEnd = stringToPrecomputedChunk("</script>"); // 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)
function createRenderState$1(resumableState, nonce) {
function createRenderState$1(resumableState, nonce, importMap) {
var inlineScriptWithNonce =
nonce === undefined
? startInlineScript
@@ -1944,6 +1949,19 @@ function createRenderState$1(resumableState, nonce) {
'<script nonce="' + escapeTextForBrowser(nonce) + '">'
);
var idPrefix = resumableState.idPrefix;
var importMapChunks = [];
if (importMap !== undefined) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
);
importMapChunks.push(importMapScriptEnd);
}
return {
placeholderPrefix: stringToPrecomputedChunk(idPrefix + "P:"),
segmentPrefix: stringToPrecomputedChunk(idPrefix + "S:"),
@@ -1953,6 +1971,7 @@ function createRenderState$1(resumableState, nonce) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: importMapChunks,
preloadChunks: [],
hoistableChunks: [],
nonce: nonce,
@@ -1983,7 +2002,9 @@ function createResumableState(
);
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
),
endInlineScript
);
}
@@ -5852,6 +5873,13 @@ function writePreamble(
resumableState.highImagePreloads.clear(); // Flush unblocked stylesheets by precedence
resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
var importMapChunks = renderState.importMapChunks;
for (i = 0; i < importMapChunks.length; i++) {
writeChunk(destination, importMapChunks[i]);
}
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.clear();
@@ -5912,7 +5940,11 @@ function writeHoistables(destination, resumableState, renderState) {
resumableState.highImagePreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this
// but we want to kick off preloading as soon as possible
resumableState.precedences.forEach(preloadLateStyles, destination); // bootstrap scripts should flush above script priority but these can only flush in the preamble
resumableState.precedences.forEach(preloadLateStyles, destination); // We only hoist importmaps that are configured through createResponse and that will
// always flush in the preamble. Generally we don't expect people to render them as
// tags when using React but if you do they are going to be treated like regular inline
// scripts and flush after other hoistables which is problematic
// bootstrap scripts should flush above script priority but these can only flush in the preamble
// so we elide the code here for performance
resumableState.scripts.forEach(flushResourceLate, destination);
@@ -7126,6 +7158,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
headChunks: renderState.headChunks,
charsetChunks: renderState.charsetChunks,
preconnectChunks: renderState.preconnectChunks,
importMapChunks: renderState.importMapChunks,
preloadChunks: renderState.preloadChunks,
hoistableChunks: renderState.hoistableChunks,
boundaryResources: renderState.boundaryResources,
@@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");
var ReactVersion = "18.3.0-www-modern-d44f2eb8";
var ReactVersion = "18.3.0-www-modern-451e1736";
// This refers to a WWW module.
var warningWWW = require("warning");
@@ -1910,7 +1910,7 @@ var scriptIntegirty = stringToPrecomputedChunk('" integrity="');
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
/**
* 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 <script sequences the html parser will never enter
@@ -1920,7 +1920,7 @@ var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
* ensure that the script cannot be early terminated or never terminated state
*/
function escapeBootstrapScriptContent(scriptText) {
function escapeBootstrapAndImportMapScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
@@ -1932,11 +1932,16 @@ var scriptRegex = /(<\/|<)(s)(cript)/gi;
var scriptReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\u0073" : "\\u0053") + suffix;
}; // Allows us to keep track of what we've already written so we can refer back to it.
};
var importMapScriptStart = stringToPrecomputedChunk(
'<script type="importmap">'
);
var importMapScriptEnd = stringToPrecomputedChunk("</script>"); // 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)
function createRenderState$1(resumableState, nonce) {
function createRenderState$1(resumableState, nonce, importMap) {
var inlineScriptWithNonce =
nonce === undefined
? startInlineScript
@@ -1944,6 +1949,19 @@ function createRenderState$1(resumableState, nonce) {
'<script nonce="' + escapeTextForBrowser(nonce) + '">'
);
var idPrefix = resumableState.idPrefix;
var importMapChunks = [];
if (importMap !== undefined) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
);
importMapChunks.push(importMapScriptEnd);
}
return {
placeholderPrefix: stringToPrecomputedChunk(idPrefix + "P:"),
segmentPrefix: stringToPrecomputedChunk(idPrefix + "S:"),
@@ -1953,6 +1971,7 @@ function createRenderState$1(resumableState, nonce) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: importMapChunks,
preloadChunks: [],
hoistableChunks: [],
nonce: nonce,
@@ -1983,7 +2002,9 @@ function createResumableState(
);
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
),
endInlineScript
);
}
@@ -5852,6 +5873,13 @@ function writePreamble(
resumableState.highImagePreloads.clear(); // Flush unblocked stylesheets by precedence
resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
var importMapChunks = renderState.importMapChunks;
for (i = 0; i < importMapChunks.length; i++) {
writeChunk(destination, importMapChunks[i]);
}
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.clear();
@@ -5912,7 +5940,11 @@ function writeHoistables(destination, resumableState, renderState) {
resumableState.highImagePreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this
// but we want to kick off preloading as soon as possible
resumableState.precedences.forEach(preloadLateStyles, destination); // bootstrap scripts should flush above script priority but these can only flush in the preamble
resumableState.precedences.forEach(preloadLateStyles, destination); // We only hoist importmaps that are configured through createResponse and that will
// always flush in the preamble. Generally we don't expect people to render them as
// tags when using React but if you do they are going to be treated like regular inline
// scripts and flush after other hoistables which is problematic
// bootstrap scripts should flush above script priority but these can only flush in the preamble
// so we elide the code here for performance
resumableState.scripts.forEach(flushResourceLate, destination);
@@ -7126,6 +7158,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
headChunks: renderState.headChunks,
charsetChunks: renderState.charsetChunks,
preconnectChunks: renderState.preconnectChunks,
importMapChunks: renderState.importMapChunks,
preloadChunks: renderState.preloadChunks,
hoistableChunks: renderState.hoistableChunks,
boundaryResources: renderState.boundaryResources,
@@ -2357,6 +2357,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: [],
preloadChunks: [],
hoistableChunks: [],
boundaryResources: null,
@@ -4054,6 +4055,14 @@ function flushCompletedQueues(request, destination) {
flushAllStylesInPreamble,
destination
);
var importMapChunks = renderState.importMapChunks;
for (
_resumableState$exter = 0;
_resumableState$exter < importMapChunks.length;
_resumableState$exter++
)
destination.push(importMapChunks[_resumableState$exter]);
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(
flushResourceInPreamble,
destination
@@ -4399,4 +4408,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "18.3.0-www-classic-4f299cee";
exports.version = "18.3.0-www-classic-7f028090";
@@ -2357,6 +2357,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: [],
preloadChunks: [],
hoistableChunks: [],
boundaryResources: null,
@@ -4037,6 +4038,14 @@ function flushCompletedQueues(request, destination) {
flushAllStylesInPreamble,
destination
);
var importMapChunks = renderState.importMapChunks;
for (
_resumableState$exter = 0;
_resumableState$exter < importMapChunks.length;
_resumableState$exter++
)
destination.push(importMapChunks[_resumableState$exter]);
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(
flushResourceInPreamble,
destination
@@ -4382,4 +4391,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "18.3.0-www-modern-e02b0940";
exports.version = "18.3.0-www-modern-972e36e6";
@@ -1907,7 +1907,7 @@ var scriptIntegirty = stringToPrecomputedChunk('" integrity="');
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
/**
* 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 <script sequences the html parser will never enter
@@ -1917,7 +1917,7 @@ var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
* ensure that the script cannot be early terminated or never terminated state
*/
function escapeBootstrapScriptContent(scriptText) {
function escapeBootstrapAndImportMapScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
@@ -1929,11 +1929,16 @@ var scriptRegex = /(<\/|<)(s)(cript)/gi;
var scriptReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\u0073" : "\\u0053") + suffix;
}; // Allows us to keep track of what we've already written so we can refer back to it.
};
var importMapScriptStart = stringToPrecomputedChunk(
'<script type="importmap">'
);
var importMapScriptEnd = stringToPrecomputedChunk("</script>"); // 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)
function createRenderState(resumableState, nonce) {
function createRenderState(resumableState, nonce, importMap) {
var inlineScriptWithNonce =
nonce === undefined
? startInlineScript
@@ -1941,6 +1946,19 @@ function createRenderState(resumableState, nonce) {
'<script nonce="' + escapeTextForBrowser(nonce) + '">'
);
var idPrefix = resumableState.idPrefix;
var importMapChunks = [];
if (importMap !== undefined) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
);
importMapChunks.push(importMapScriptEnd);
}
return {
placeholderPrefix: stringToPrecomputedChunk(idPrefix + "P:"),
segmentPrefix: stringToPrecomputedChunk(idPrefix + "S:"),
@@ -1950,6 +1968,7 @@ function createRenderState(resumableState, nonce) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: importMapChunks,
preloadChunks: [],
hoistableChunks: [],
nonce: nonce,
@@ -1980,7 +1999,9 @@ function createResumableState(
);
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
),
endInlineScript
);
}
@@ -5846,6 +5867,13 @@ function writePreamble(
resumableState.highImagePreloads.clear(); // Flush unblocked stylesheets by precedence
resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
var importMapChunks = renderState.importMapChunks;
for (i = 0; i < importMapChunks.length; i++) {
writeChunk(destination, importMapChunks[i]);
}
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.forEach(flushResourceInPreamble, destination);
resumableState.scripts.clear();
@@ -5906,7 +5934,11 @@ function writeHoistables(destination, resumableState, renderState) {
resumableState.highImagePreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this
// but we want to kick off preloading as soon as possible
resumableState.precedences.forEach(preloadLateStyles, destination); // bootstrap scripts should flush above script priority but these can only flush in the preamble
resumableState.precedences.forEach(preloadLateStyles, destination); // We only hoist importmaps that are configured through createResponse and that will
// always flush in the preamble. Generally we don't expect people to render them as
// tags when using React but if you do they are going to be treated like regular inline
// scripts and flush after other hoistables which is problematic
// bootstrap scripts should flush above script priority but these can only flush in the preamble
// so we elide the code here for performance
resumableState.scripts.forEach(flushResourceLate, destination);
@@ -3739,6 +3739,14 @@ function flushCompletedQueues(request, destination) {
flushAllStylesInPreamble,
destination
);
var importMapChunks = renderState.importMapChunks;
for (
_resumableState$exter = 0;
_resumableState$exter < importMapChunks.length;
_resumableState$exter++
)
writeChunk(destination, importMapChunks[_resumableState$exter]);
importMapChunks.length = 0;
resumableState.bootstrapScripts.forEach(
flushResourceInPreamble,
destination
@@ -4309,6 +4317,7 @@ exports.renderToStream = function (children, options) {
headChunks: null,
charsetChunks: [],
preconnectChunks: [],
importMapChunks: [],
preloadChunks: [],
hoistableChunks: [],
nonce: void 0,