Skip to content

Commit

Permalink
[Float][Flight][Fizz][Fiber] Implement preloadModule and `preinitMo…
Browse files Browse the repository at this point in the history
…dule` (#27220)

Stacked on #27224

### Implements `ReactDOM.preloadModule()`
`preloadModule` is a function to preload modules of various types.
Similar to `preload` this is useful when you expect to use a Resource
soon but can't render that resource directly. At the moment the only
sensible module to preload is script modules along with some other `as`
variants such as `as="serviceworker"`. In the future when there is some
notion of how to preload style module script or json module scripts this
API will be extended to support those as well.

##### Arguments
1. `href: string` -> the href or src value you want to preload.
2. `options?: {...}` ->
2.1. `options.as?: string` -> the `as` property the modulepreload link
should render with. If not provided it will be omitted which will cause
the modulepreload to be treated like a script module
2.2. `options.crossOrigin?: string` -> modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3. `options.integrity?: string` -> an integrity hash for subresource
integrity APIs

##### Rendering
each preloaded module will emit a `<link rel="modulepreload" href="..."
/>`
if `as` is specified and is something other than `"script"` the as
attribute will also be included
if crossOrigin or integrity as specified their attributes will also be
included

During SSR these script tags will be emitted before content. If we have
not yet flushed the document head they will be emitted there after
things that block paint such as font preloads, img preloads, and
stylesheets.

On the client these link tags will be appended to the document.head.

### Implements `ReactDOM.preinitModule()`
`preinitModule` is a function to loading module scripts before they are
required. It has the same use cases as `preinit`.

During SSR you would use this to tell the browsers to start fetching
code that will be used without having to wait for bootstrapping to
initiate module fetches.

ON the client you would use this to start fetching a module script early
for an anticipated navigation or other event that is likely to depend on
this module script.

the `as` property for Float methods drew inspiration from the `as`
attribute of the `<link rel="preload" ... >` tag but it is used as a
sort of tag for the kind of thing being targetted by Float methods. For
`preinitModule` we currently only support `as: "script"` and this is
also the assumed default type so you current never need to specify this
`as` value. In the future `preinitModule` will support additional module
script types such as `style` or `json`. The support of these types will
correspond to [Module Import
Attributes](https://github.com/tc39/proposal-import-attributes).

##### Arguments
1. `href: string` -> the href or src value you want to preinitialize
2. `options?: {...}` ->
2.1 `options.as?: string` -> only supports `script` and this is the
default behavior. Until we support import attributes such as `json` and
`style` there will not be much reason to provide an `as` option.
2.2. `options.crossOrigin?: string`: modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3 `options.integrity?: string` -> an integrity hash for subresource
integrity APIs

##### Rendering
each preinitialized `script` module will emit a `<script type="module"
async="" src"...">` During SSR these will appear behind display blocking
resources such as font preloads, img preloads, and stylesheets. In the
browser these will be appende to the head.

Note that for other `as` types the rendered output will be slightly
different. `<script type="module">import "..." with {type: "json"
}</script>`. Since this tag is an inline script variants of React that
do not use inline scripts will simply omit these preinitialization tags
from the SSR output. This is not implemented in this PR but will appear
in a future update.

DiffTrain build for [e505316](e505316)
  • Loading branch information
gnoff committed Aug 17, 2023
1 parent 5cc1661 commit 0fe197b
Show file tree
Hide file tree
Showing 22 changed files with 2,337 additions and 66 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ac1a16c67e268fcb2c52e91717cbc918c7c24446
e50531692010bbda2a4627b07c7c810c3770a52a
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-modern-00d6344c";
var ReactVersion = "18.3.0-www-modern-cfb2f865";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/ReactART-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
return self;
}

var ReactVersion = "18.3.0-www-modern-2dcc215c";
var ReactVersion = "18.3.0-www-modern-dc24a33c";

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down
4 changes: 2 additions & 2 deletions compiled/facebook-www/ReactART-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -9759,7 +9759,7 @@ var slice = Array.prototype.slice,
return null;
},
bundleType: 0,
version: "18.3.0-www-modern-0328f77f",
version: "18.3.0-www-modern-1182ef25",
rendererPackageName: "react-art"
};
var internals$jscomp$inline_1283 = {
Expand Down Expand Up @@ -9790,7 +9790,7 @@ var internals$jscomp$inline_1283 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-modern-0328f77f"
reconcilerVersion: "18.3.0-www-modern-1182ef25"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1284 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
241 changes: 239 additions & 2 deletions compiled/facebook-www/ReactDOM-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -33944,7 +33944,7 @@ function createFiberRoot(
return root;
}

var ReactVersion = "18.3.0-www-classic-87900e0d";
var ReactVersion = "18.3.0-www-classic-40cd88c9";

function createPortal$1(
children,
Expand Down Expand Up @@ -43365,7 +43365,9 @@ var ReactDOMClientDispatcher = {
prefetchDNS: prefetchDNS$1,
preconnect: preconnect$1,
preload: preload$1,
preinit: preinit$1
preloadModule: preloadModule$1,
preinit: preinit$1,
preinitModule: preinitModule$1
}; // We expect this to get inlined. It is a function mostly to communicate the special nature of
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
Expand Down Expand Up @@ -43583,6 +43585,95 @@ function preload$1(href, options) {
}
}

function preloadModule$1(href, options) {
{
var encountered = "";

if (typeof href !== "string" || !href) {
encountered +=
" The `href` argument encountered was " +
getValueDescriptorExpectingObjectForWarning(href) +
".";
}

if (options !== undefined && typeof options !== "object") {
encountered +=
" The `options` argument encountered was " +
getValueDescriptorExpectingObjectForWarning(options) +
".";
} else if (options && "as" in options && typeof options.as !== "string") {
encountered +=
" The `as` option encountered was " +
getValueDescriptorExpectingObjectForWarning(options.as) +
".";
}

if (encountered) {
error(
'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
encountered
);
}
}

var ownerDocument = getDocumentForImperativeFloatMethods();

if (typeof href === "string" && href) {
var as = options && typeof options.as === "string" ? options.as : "script";
var preloadSelector =
'link[rel="modulepreload"][as="' +
escapeSelectorAttributeValueInsideDoubleQuotes(as) +
'"][href="' +
escapeSelectorAttributeValueInsideDoubleQuotes(href) +
'"]'; // Some preloads are keyed under their selector. This happens when the preload is for
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
// Here we figure out which key to use to determine if we have a preload already.

var key = preloadSelector;

switch (as) {
case "audioworklet":
case "paintworklet":
case "serviceworker":
case "sharedworker":
case "worker":
case "script": {
key = getScriptKey(href);
break;
}
}

if (!preloadPropsMap.has(key)) {
var preloadProps = preloadModulePropsFromPreloadModuleOptions(
href,
as,
options
);
preloadPropsMap.set(key, preloadProps);

if (null === ownerDocument.querySelector(preloadSelector)) {
switch (as) {
case "audioworklet":
case "paintworklet":
case "serviceworker":
case "sharedworker":
case "worker":
case "script": {
if (ownerDocument.querySelector(getScriptSelectorFromKey(key))) {
return;
}
}
}

var instance = ownerDocument.createElement("link");
setInitialProperties(instance, "link", preloadProps);
markNodeAsHoistable(instance);
ownerDocument.head.appendChild(instance);
}
}
}
}

function preloadPropsFromPreloadOptions(href, as, options) {
return {
rel: "preload",
Expand All @@ -43603,6 +43694,16 @@ function preloadPropsFromPreloadOptions(href, as, options) {
};
}

function preloadModulePropsFromPreloadModuleOptions(href, as, options) {
return {
rel: "modulepreload",
as: as !== "script" ? as : undefined,
href: href,
crossOrigin: options ? options.crossOrigin : undefined,
integrity: options ? options.integrity : undefined
};
}

function preinit$1(href, options) {
{
validatePreinitArguments(href, options);
Expand Down Expand Up @@ -43730,6 +43831,112 @@ function preinit$1(href, options) {
}
}

function preinitModule$1(href, options) {
{
var encountered = "";

if (typeof href !== "string" || !href) {
encountered +=
" The `href` argument encountered was " +
getValueDescriptorExpectingObjectForWarning(href) +
".";
}

if (options !== undefined && typeof options !== "object") {
encountered +=
" The `options` argument encountered was " +
getValueDescriptorExpectingObjectForWarning(options) +
".";
} else if (options && "as" in options && options.as !== "script") {
encountered +=
" The `as` option encountered was " +
getValueDescriptorExpectingEnumForWarning(options.as) +
".";
}

if (encountered) {
error(
"ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s",
encountered
);
} else {
var as =
options && typeof options.as === "string" ? options.as : "script";

switch (as) {
case "script": {
break;
}
// We have an invalid as type and need to warn

default: {
var typeOfAs = getValueDescriptorExpectingEnumForWarning(as);

error(
'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
" module types will be supported, aligning with the import-attributes proposal. Learn more here:" +
" (https://github.com/tc39/proposal-import-attributes)",
typeOfAs,
href
);
}
}
}
}

var ownerDocument = getDocumentForImperativeFloatMethods();

if (typeof href === "string" && href) {
var _as = options && typeof options.as === "string" ? options.as : "script";

switch (_as) {
case "script": {
var src = href;
var scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
var key = getScriptKey(src); // Check if this resource already exists

var resource = scripts.get(key);

if (resource) {
// We can early return. The resource exists and there is nothing
// more to do
return;
} // Attempt to hydrate instance from DOM

var instance = ownerDocument.querySelector(
getScriptSelectorFromKey(key)
);

if (!instance) {
// Construct a new instance and insert it
var scriptProps = modulePropsFromPreinitModuleOptions(src, options); // Adopt certain preload props

var preloadProps = preloadPropsMap.get(key);

if (preloadProps) {
adoptPreloadPropsForScript(scriptProps, preloadProps);
}

instance = ownerDocument.createElement("script");
markNodeAsHoistable(instance);
setInitialProperties(instance, "link", scriptProps);
ownerDocument.head.appendChild(instance);
} // Construct a Resource and cache it

resource = {
type: "script",
instance: instance,
count: 1,
state: null
};
scripts.set(key, resource);
return;
}
}
}
}

function stylesheetPropsFromPreinitOptions(href, precedence, options) {
return {
rel: "stylesheet",
Expand All @@ -43750,6 +43957,16 @@ function scriptPropsFromPreinitOptions(src, options) {
nonce: options.nonce,
fetchPriority: options.fetchPriority
};
}

function modulePropsFromPreinitModuleOptions(src, options) {
return {
src: src,
async: true,
type: "module",
crossOrigin: options ? options.crossOrigin : undefined,
integrity: options ? options.integrity : undefined
};
} // This function is called in begin work and we should always have a currentDocument set

function getResource(type, currentProps, pendingProps) {
Expand Down Expand Up @@ -46657,6 +46874,15 @@ function preload(href, options) {
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
function preloadModule(href, options) {
var dispatcher = Dispatcher.current;

if (dispatcher) {
dispatcher.preloadModule(href, options);
} // We don't error because preload needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
function preinit(href, options) {
var dispatcher = Dispatcher.current;

Expand All @@ -46666,6 +46892,15 @@ function preinit(href, options) {
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
function preinitModule(href, options) {
var dispatcher = Dispatcher.current;

if (dispatcher) {
dispatcher.preinitModule(href, options);
} // We don't error because preinit needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}

{
if (
Expand Down Expand Up @@ -46813,7 +47048,9 @@ exports.hydrateRoot = hydrateRoot;
exports.preconnect = preconnect;
exports.prefetchDNS = prefetchDNS;
exports.preinit = preinit;
exports.preinitModule = preinitModule;
exports.preload = preload;
exports.preloadModule = preloadModule;
exports.render = render;
exports.unmountComponentAtNode = unmountComponentAtNode;
exports.unstable_batchedUpdates = batchedUpdates$1;
Expand Down
Loading

0 comments on commit 0fe197b

Please sign in to comment.