Skip to content

Commit 3129e28

Browse files
committed
[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 [9d4582d](9d4582d)
1 parent 2bd9065 commit 3129e28

7 files changed

+148
-23
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
b4cdd3e8922713f8c9817b004a0dc51be47bc5df
1+
9d4582dffdea5b4dcb6a6093ea848d15423c7701

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-classic-1747ba9c";
22+
var ReactVersion = "18.3.0-www-classic-dbbf9e79";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -1910,7 +1910,7 @@ var scriptIntegirty = stringToPrecomputedChunk('" integrity="');
19101910
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
19111911
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
19121912
/**
1913-
* This escaping function is designed to work with bootstrapScriptContent only.
1913+
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
19141914
* because we know we are escaping the entire script. We can avoid for instance
19151915
* escaping html comment string sequences that are valid javascript as well because
19161916
* if there are no sebsequent <script sequences the html parser will never enter
@@ -1920,7 +1920,7 @@ var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
19201920
* ensure that the script cannot be early terminated or never terminated state
19211921
*/
19221922

1923-
function escapeBootstrapScriptContent(scriptText) {
1923+
function escapeBootstrapAndImportMapScriptContent(scriptText) {
19241924
{
19251925
checkHtmlStringCoercion(scriptText);
19261926
}
@@ -1932,18 +1932,36 @@ var scriptRegex = /(<\/|<)(s)(cript)/gi;
19321932

19331933
var scriptReplacer = function (match, prefix, s, suffix) {
19341934
return "" + prefix + (s === "s" ? "\\u0073" : "\\u0053") + suffix;
1935-
}; // Allows us to keep track of what we've already written so we can refer back to it.
1935+
};
1936+
1937+
var importMapScriptStart = stringToPrecomputedChunk(
1938+
'<script type="importmap">'
1939+
);
1940+
var importMapScriptEnd = stringToPrecomputedChunk("</script>"); // Allows us to keep track of what we've already written so we can refer back to it.
19361941
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
19371942
// is set, the server will send instructions via data attributes (instead of inline scripts)
19381943

1939-
function createRenderState$1(resumableState, nonce) {
1944+
function createRenderState$1(resumableState, nonce, importMap) {
19401945
var inlineScriptWithNonce =
19411946
nonce === undefined
19421947
? startInlineScript
19431948
: stringToPrecomputedChunk(
19441949
'<script nonce="' + escapeTextForBrowser(nonce) + '">'
19451950
);
19461951
var idPrefix = resumableState.idPrefix;
1952+
var importMapChunks = [];
1953+
1954+
if (importMap !== undefined) {
1955+
var map = importMap;
1956+
importMapChunks.push(importMapScriptStart);
1957+
importMapChunks.push(
1958+
stringToChunk(
1959+
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
1960+
)
1961+
);
1962+
importMapChunks.push(importMapScriptEnd);
1963+
}
1964+
19471965
return {
19481966
placeholderPrefix: stringToPrecomputedChunk(idPrefix + "P:"),
19491967
segmentPrefix: stringToPrecomputedChunk(idPrefix + "S:"),
@@ -1953,6 +1971,7 @@ function createRenderState$1(resumableState, nonce) {
19531971
headChunks: null,
19541972
charsetChunks: [],
19551973
preconnectChunks: [],
1974+
importMapChunks: importMapChunks,
19561975
preloadChunks: [],
19571976
hoistableChunks: [],
19581977
nonce: nonce,
@@ -1983,7 +2002,9 @@ function createResumableState(
19832002
);
19842003
bootstrapChunks.push(
19852004
inlineScriptWithNonce,
1986-
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
2005+
stringToChunk(
2006+
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
2007+
),
19872008
endInlineScript
19882009
);
19892010
}
@@ -5852,6 +5873,13 @@ function writePreamble(
58525873
resumableState.highImagePreloads.clear(); // Flush unblocked stylesheets by precedence
58535874

58545875
resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
5876+
var importMapChunks = renderState.importMapChunks;
5877+
5878+
for (i = 0; i < importMapChunks.length; i++) {
5879+
writeChunk(destination, importMapChunks[i]);
5880+
}
5881+
5882+
importMapChunks.length = 0;
58555883
resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
58565884
resumableState.scripts.forEach(flushResourceInPreamble, destination);
58575885
resumableState.scripts.clear();
@@ -5912,7 +5940,11 @@ function writeHoistables(destination, resumableState, renderState) {
59125940
resumableState.highImagePreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this
59135941
// but we want to kick off preloading as soon as possible
59145942

5915-
resumableState.precedences.forEach(preloadLateStyles, destination); // bootstrap scripts should flush above script priority but these can only flush in the preamble
5943+
resumableState.precedences.forEach(preloadLateStyles, destination); // We only hoist importmaps that are configured through createResponse and that will
5944+
// always flush in the preamble. Generally we don't expect people to render them as
5945+
// tags when using React but if you do they are going to be treated like regular inline
5946+
// scripts and flush after other hoistables which is problematic
5947+
// bootstrap scripts should flush above script priority but these can only flush in the preamble
59165948
// so we elide the code here for performance
59175949

59185950
resumableState.scripts.forEach(flushResourceLate, destination);
@@ -7126,6 +7158,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
71267158
headChunks: renderState.headChunks,
71277159
charsetChunks: renderState.charsetChunks,
71287160
preconnectChunks: renderState.preconnectChunks,
7161+
importMapChunks: renderState.importMapChunks,
71297162
preloadChunks: renderState.preloadChunks,
71307163
hoistableChunks: renderState.hoistableChunks,
71317164
boundaryResources: renderState.boundaryResources,

compiled/facebook-www/ReactDOMServer-dev.modern.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-modern-d44f2eb8";
22+
var ReactVersion = "18.3.0-www-modern-451e1736";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -1910,7 +1910,7 @@ var scriptIntegirty = stringToPrecomputedChunk('" integrity="');
19101910
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
19111911
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
19121912
/**
1913-
* This escaping function is designed to work with bootstrapScriptContent only.
1913+
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
19141914
* because we know we are escaping the entire script. We can avoid for instance
19151915
* escaping html comment string sequences that are valid javascript as well because
19161916
* if there are no sebsequent <script sequences the html parser will never enter
@@ -1920,7 +1920,7 @@ var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
19201920
* ensure that the script cannot be early terminated or never terminated state
19211921
*/
19221922

1923-
function escapeBootstrapScriptContent(scriptText) {
1923+
function escapeBootstrapAndImportMapScriptContent(scriptText) {
19241924
{
19251925
checkHtmlStringCoercion(scriptText);
19261926
}
@@ -1932,18 +1932,36 @@ var scriptRegex = /(<\/|<)(s)(cript)/gi;
19321932

19331933
var scriptReplacer = function (match, prefix, s, suffix) {
19341934
return "" + prefix + (s === "s" ? "\\u0073" : "\\u0053") + suffix;
1935-
}; // Allows us to keep track of what we've already written so we can refer back to it.
1935+
};
1936+
1937+
var importMapScriptStart = stringToPrecomputedChunk(
1938+
'<script type="importmap">'
1939+
);
1940+
var importMapScriptEnd = stringToPrecomputedChunk("</script>"); // Allows us to keep track of what we've already written so we can refer back to it.
19361941
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
19371942
// is set, the server will send instructions via data attributes (instead of inline scripts)
19381943

1939-
function createRenderState$1(resumableState, nonce) {
1944+
function createRenderState$1(resumableState, nonce, importMap) {
19401945
var inlineScriptWithNonce =
19411946
nonce === undefined
19421947
? startInlineScript
19431948
: stringToPrecomputedChunk(
19441949
'<script nonce="' + escapeTextForBrowser(nonce) + '">'
19451950
);
19461951
var idPrefix = resumableState.idPrefix;
1952+
var importMapChunks = [];
1953+
1954+
if (importMap !== undefined) {
1955+
var map = importMap;
1956+
importMapChunks.push(importMapScriptStart);
1957+
importMapChunks.push(
1958+
stringToChunk(
1959+
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
1960+
)
1961+
);
1962+
importMapChunks.push(importMapScriptEnd);
1963+
}
1964+
19471965
return {
19481966
placeholderPrefix: stringToPrecomputedChunk(idPrefix + "P:"),
19491967
segmentPrefix: stringToPrecomputedChunk(idPrefix + "S:"),
@@ -1953,6 +1971,7 @@ function createRenderState$1(resumableState, nonce) {
19531971
headChunks: null,
19541972
charsetChunks: [],
19551973
preconnectChunks: [],
1974+
importMapChunks: importMapChunks,
19561975
preloadChunks: [],
19571976
hoistableChunks: [],
19581977
nonce: nonce,
@@ -1983,7 +2002,9 @@ function createResumableState(
19832002
);
19842003
bootstrapChunks.push(
19852004
inlineScriptWithNonce,
1986-
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
2005+
stringToChunk(
2006+
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
2007+
),
19872008
endInlineScript
19882009
);
19892010
}
@@ -5852,6 +5873,13 @@ function writePreamble(
58525873
resumableState.highImagePreloads.clear(); // Flush unblocked stylesheets by precedence
58535874

58545875
resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
5876+
var importMapChunks = renderState.importMapChunks;
5877+
5878+
for (i = 0; i < importMapChunks.length; i++) {
5879+
writeChunk(destination, importMapChunks[i]);
5880+
}
5881+
5882+
importMapChunks.length = 0;
58555883
resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
58565884
resumableState.scripts.forEach(flushResourceInPreamble, destination);
58575885
resumableState.scripts.clear();
@@ -5912,7 +5940,11 @@ function writeHoistables(destination, resumableState, renderState) {
59125940
resumableState.highImagePreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this
59135941
// but we want to kick off preloading as soon as possible
59145942

5915-
resumableState.precedences.forEach(preloadLateStyles, destination); // bootstrap scripts should flush above script priority but these can only flush in the preamble
5943+
resumableState.precedences.forEach(preloadLateStyles, destination); // We only hoist importmaps that are configured through createResponse and that will
5944+
// always flush in the preamble. Generally we don't expect people to render them as
5945+
// tags when using React but if you do they are going to be treated like regular inline
5946+
// scripts and flush after other hoistables which is problematic
5947+
// bootstrap scripts should flush above script priority but these can only flush in the preamble
59165948
// so we elide the code here for performance
59175949

59185950
resumableState.scripts.forEach(flushResourceLate, destination);
@@ -7126,6 +7158,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
71267158
headChunks: renderState.headChunks,
71277159
charsetChunks: renderState.charsetChunks,
71287160
preconnectChunks: renderState.preconnectChunks,
7161+
importMapChunks: renderState.importMapChunks,
71297162
preloadChunks: renderState.preloadChunks,
71307163
hoistableChunks: renderState.hoistableChunks,
71317164
boundaryResources: renderState.boundaryResources,

compiled/facebook-www/ReactDOMServer-prod.classic.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2357,6 +2357,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
23572357
headChunks: null,
23582358
charsetChunks: [],
23592359
preconnectChunks: [],
2360+
importMapChunks: [],
23602361
preloadChunks: [],
23612362
hoistableChunks: [],
23622363
boundaryResources: null,
@@ -4054,6 +4055,14 @@ function flushCompletedQueues(request, destination) {
40544055
flushAllStylesInPreamble,
40554056
destination
40564057
);
4058+
var importMapChunks = renderState.importMapChunks;
4059+
for (
4060+
_resumableState$exter = 0;
4061+
_resumableState$exter < importMapChunks.length;
4062+
_resumableState$exter++
4063+
)
4064+
destination.push(importMapChunks[_resumableState$exter]);
4065+
importMapChunks.length = 0;
40574066
resumableState.bootstrapScripts.forEach(
40584067
flushResourceInPreamble,
40594068
destination
@@ -4399,4 +4408,4 @@ exports.renderToString = function (children, options) {
43994408
'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'
44004409
);
44014410
};
4402-
exports.version = "18.3.0-www-classic-4f299cee";
4411+
exports.version = "18.3.0-www-classic-7f028090";

compiled/facebook-www/ReactDOMServer-prod.modern.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2357,6 +2357,7 @@ function createRenderState(resumableState, nonce, generateStaticMarkup) {
23572357
headChunks: null,
23582358
charsetChunks: [],
23592359
preconnectChunks: [],
2360+
importMapChunks: [],
23602361
preloadChunks: [],
23612362
hoistableChunks: [],
23622363
boundaryResources: null,
@@ -4037,6 +4038,14 @@ function flushCompletedQueues(request, destination) {
40374038
flushAllStylesInPreamble,
40384039
destination
40394040
);
4041+
var importMapChunks = renderState.importMapChunks;
4042+
for (
4043+
_resumableState$exter = 0;
4044+
_resumableState$exter < importMapChunks.length;
4045+
_resumableState$exter++
4046+
)
4047+
destination.push(importMapChunks[_resumableState$exter]);
4048+
importMapChunks.length = 0;
40404049
resumableState.bootstrapScripts.forEach(
40414050
flushResourceInPreamble,
40424051
destination
@@ -4382,4 +4391,4 @@ exports.renderToString = function (children, options) {
43824391
'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'
43834392
);
43844393
};
4385-
exports.version = "18.3.0-www-modern-e02b0940";
4394+
exports.version = "18.3.0-www-modern-972e36e6";

0 commit comments

Comments
 (0)