[Float][Fizz][Fiber]: Refactor <style> Resource implementation to group on flush (#26280)
There is a problem with <style> as resource. For css-in-js libs there may be an very large number of these hoistables being created. The number of style tags can grow quickly and to help reduce the prevalence of this FIzz now aggregates all style tags for a given precedence into a single tag. The client can 'hydrate' against these compound tags but currently on the client insertions are done individually. additionally drops the implementation where style tags are embedding in a template for one where `media="not all"` is set. The idea is to have the browser construct the underlying stylesheet eagerly which does not happen if the tag is embedded in a template Key Decision: One choice made in this PR is that we flush style tags eagerly even if a boundary is blocked that is the only thing that depends on that style rule. The reason we are starting with this implementation is that it allows a very condensed representation of the style resources. If we tracked which rules were used in which boundaries we would need a style resource for every rendered <style> tag. This could be problematic for css-in-js libs that might render hundreds or thousands of style tags. The tradeoff here is we slightly delay content reveal in some cases (we send extra bytes) but we have fewer DOM tags and faster SSR runtime
This commit is contained in:
parent
5c633a48f9
commit
1f1f8eb559
|
@ -663,7 +663,7 @@ function styleTagPropsFromRawProps(
|
|||
function getStyleKey(href: string) {
|
||||
const limitedEscapedHref =
|
||||
escapeSelectorAttributeValueInsideDoubleQuotes(href);
|
||||
return `href="${limitedEscapedHref}"`;
|
||||
return `href~="${limitedEscapedHref}"`;
|
||||
}
|
||||
|
||||
function getStyleTagSelectorFromKey(key: string) {
|
||||
|
|
|
@ -153,6 +153,11 @@ export type ResponseState = {
|
|||
preloadChunks: Array<Chunk | PrecomputedChunk>,
|
||||
hoistableChunks: Array<Chunk | PrecomputedChunk>,
|
||||
|
||||
// Module-global-like reference for flushing/hoisting state of style resources
|
||||
// We need to track whether the current request has flushed any style resources
|
||||
// without sending an instruction to hoist them. we do that here
|
||||
stylesToHoist: boolean,
|
||||
|
||||
// We allow the legacy renderer to extend this object.
|
||||
|
||||
...
|
||||
|
@ -300,6 +305,7 @@ export function createResponseState(
|
|||
preconnectChunks: [],
|
||||
preloadChunks: [],
|
||||
hoistableChunks: [],
|
||||
stylesToHoist: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1421,7 +1427,7 @@ function pushLink(
|
|||
resource = {
|
||||
type: 'stylesheet',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: resources.boundaryResources ? Blocked : NoState,
|
||||
state: NoState,
|
||||
props: resourceProps,
|
||||
};
|
||||
resources.stylesMap.set(key, resource);
|
||||
|
@ -1432,6 +1438,25 @@ function pushLink(
|
|||
if (!precedenceSet) {
|
||||
precedenceSet = new Set();
|
||||
resources.precedences.set(precedence, precedenceSet);
|
||||
const emptyStyleResource = {
|
||||
type: 'style',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: NoState,
|
||||
props: {
|
||||
precedence,
|
||||
hrefs: ([]: Array<string>),
|
||||
},
|
||||
};
|
||||
precedenceSet.add(emptyStyleResource);
|
||||
if (__DEV__) {
|
||||
if (resources.stylePrecedences.has(precedence)) {
|
||||
console.error(
|
||||
'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
|
||||
precedence,
|
||||
);
|
||||
}
|
||||
}
|
||||
resources.stylePrecedences.set(precedence, emptyStyleResource);
|
||||
}
|
||||
precedenceSet.add(resource);
|
||||
}
|
||||
|
@ -1556,30 +1581,49 @@ function pushStyle(
|
|||
return pushStyleImpl(target, props);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (href.includes(' ')) {
|
||||
console.error(
|
||||
'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "%s".',
|
||||
href,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const key = getResourceKey('style', href);
|
||||
let resource = resources.stylesMap.get(key);
|
||||
if (!resource) {
|
||||
resource = {
|
||||
type: 'style',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: resources.boundaryResources ? Blocked : NoState,
|
||||
props: styleTagPropsFromRawProps(props),
|
||||
};
|
||||
resources.stylesMap.set(key, resource);
|
||||
if (__DEV__) {
|
||||
markAsRenderedResourceDEV(resource, props);
|
||||
}
|
||||
pushStyleImpl(resource.chunks, resource.props);
|
||||
|
||||
let precedenceSet = resources.precedences.get(precedence);
|
||||
if (!precedenceSet) {
|
||||
precedenceSet = new Set();
|
||||
resource = resources.stylePrecedences.get(precedence);
|
||||
if (!resource) {
|
||||
resource = {
|
||||
type: 'style',
|
||||
chunks: [],
|
||||
state: NoState,
|
||||
props: {
|
||||
precedence,
|
||||
hrefs: [href],
|
||||
},
|
||||
};
|
||||
resources.stylePrecedences.set(precedence, resource);
|
||||
const precedenceSet: Set<StyleResource> = new Set();
|
||||
precedenceSet.add(resource);
|
||||
if (__DEV__) {
|
||||
if (resources.precedences.has(precedence)) {
|
||||
console.error(
|
||||
'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.',
|
||||
precedence,
|
||||
);
|
||||
}
|
||||
}
|
||||
resources.precedences.set(precedence, precedenceSet);
|
||||
} else {
|
||||
resource.props.hrefs.push(href);
|
||||
}
|
||||
precedenceSet.add(resource);
|
||||
resources.stylesMap.set(key, resource);
|
||||
if (resources.boundaryResources) {
|
||||
resources.boundaryResources.add(resource);
|
||||
}
|
||||
pushStyleContents(resource.chunks, props);
|
||||
}
|
||||
|
||||
if (textEmbedded) {
|
||||
|
@ -1640,6 +1684,47 @@ function pushStyleImpl(
|
|||
return null;
|
||||
}
|
||||
|
||||
function pushStyleContents(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
): void {
|
||||
let children = null;
|
||||
let innerHTML = null;
|
||||
for (const propKey in props) {
|
||||
if (hasOwnProperty.call(props, propKey)) {
|
||||
const propValue = props[propKey];
|
||||
if (propValue == null) {
|
||||
continue;
|
||||
}
|
||||
switch (propKey) {
|
||||
case 'children':
|
||||
children = propValue;
|
||||
break;
|
||||
case 'dangerouslySetInnerHTML':
|
||||
innerHTML = propValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const child = Array.isArray(children)
|
||||
? children.length < 2
|
||||
? children[0]
|
||||
: null
|
||||
: children;
|
||||
if (
|
||||
typeof child !== 'function' &&
|
||||
typeof child !== 'symbol' &&
|
||||
child !== null &&
|
||||
child !== undefined
|
||||
) {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
target.push(stringToChunk(escapeTextForBrowser('' + child)));
|
||||
}
|
||||
pushInnerHTML(target, innerHTML, children);
|
||||
return;
|
||||
}
|
||||
|
||||
function pushSelfClosing(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
|
@ -2931,6 +3016,7 @@ const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
|
|||
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
|
||||
styleInsertionFunction + '$RR("',
|
||||
);
|
||||
|
||||
const completeBoundaryWithStylesScript1Partial =
|
||||
stringToPrecomputedChunk('$RR("');
|
||||
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
|
||||
|
@ -2955,16 +3041,21 @@ export function writeCompletedBoundaryInstruction(
|
|||
contentSegmentID: number,
|
||||
boundaryResources: BoundaryResources,
|
||||
): boolean {
|
||||
let hasStyleDependencies;
|
||||
let requiresStyleInsertion;
|
||||
if (enableFloat) {
|
||||
hasStyleDependencies = hasStyleResourceDependencies(boundaryResources);
|
||||
requiresStyleInsertion = responseState.stylesToHoist;
|
||||
// If necessary stylesheets will be flushed with this instruction.
|
||||
// Any style tags not yet hoisted in the Document will also be hoisted.
|
||||
// We reset this state since after this instruction executes all styles
|
||||
// up to this point will have been hoisted
|
||||
responseState.stylesToHoist = false;
|
||||
}
|
||||
const scriptFormat =
|
||||
!enableFizzExternalRuntime ||
|
||||
responseState.streamingFormat === ScriptStreamingFormat;
|
||||
if (scriptFormat) {
|
||||
writeChunk(destination, responseState.startInlineScript);
|
||||
if (enableFloat && hasStyleDependencies) {
|
||||
if (enableFloat && requiresStyleInsertion) {
|
||||
if (
|
||||
(responseState.instructions & SentCompleteBoundaryFunction) ===
|
||||
NothingSent
|
||||
|
@ -2996,7 +3087,7 @@ export function writeCompletedBoundaryInstruction(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if (enableFloat && hasStyleDependencies) {
|
||||
if (enableFloat && requiresStyleInsertion) {
|
||||
writeChunk(destination, completeBoundaryWithStylesData1);
|
||||
} else {
|
||||
writeChunk(destination, completeBoundaryData1);
|
||||
|
@ -3019,7 +3110,7 @@ export function writeCompletedBoundaryInstruction(
|
|||
}
|
||||
writeChunk(destination, responseState.segmentPrefix);
|
||||
writeChunk(destination, formattedContentID);
|
||||
if (enableFloat && hasStyleDependencies) {
|
||||
if (enableFloat && requiresStyleInsertion) {
|
||||
// Script and data writers must format this differently:
|
||||
// - script writer emits an array literal, whose string elements are
|
||||
// escaped for javascript e.g. ["A", "B"]
|
||||
|
@ -3214,51 +3305,85 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
|
|||
});
|
||||
}
|
||||
|
||||
const styleTagTemplateOpen = stringToPrecomputedChunk(
|
||||
'<template data-precedence="">',
|
||||
const lateStyleTagResourceOpen1 = stringToPrecomputedChunk(
|
||||
'<style media="not all" data-precedence="',
|
||||
);
|
||||
const styleTagTemplateClose = stringToPrecomputedChunk('</template>');
|
||||
const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
|
||||
const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('">');
|
||||
const lateStyleTagTemplateClose = stringToPrecomputedChunk('</style>');
|
||||
|
||||
// Tracks whether we wrote any late style tags. We use this to determine
|
||||
// whether we need to emit a closing template tag after flushing late style tags
|
||||
let didWrite = false;
|
||||
// Tracks whether the boundary currently flushing is flushign style tags or has any
|
||||
// stylesheet dependencies not flushed in the Preamble.
|
||||
let currentlyRenderingBoundaryHasStylesToHoist = false;
|
||||
|
||||
// Acts as a return value for the forEach execution of style tag flushing.
|
||||
let destinationHasCapacity = true;
|
||||
|
||||
function flushStyleTagsLateForBoundary(
|
||||
this: Destination,
|
||||
resource: StyleResource,
|
||||
) {
|
||||
if (resource.type === 'style' && (resource.state & Flushed) === NoState) {
|
||||
if (didWrite === false) {
|
||||
// we are going to write so we need to emit the open tag
|
||||
didWrite = true;
|
||||
writeChunk(this, styleTagTemplateOpen);
|
||||
}
|
||||
// This <style> tag can be flushed now
|
||||
if (
|
||||
resource.type === 'stylesheet' &&
|
||||
(resource.state & FlushedInPreamble) === NoState
|
||||
) {
|
||||
currentlyRenderingBoundaryHasStylesToHoist = true;
|
||||
} else if (resource.type === 'style') {
|
||||
const chunks = resource.chunks;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
const hrefs = resource.props.hrefs;
|
||||
let i = 0;
|
||||
if (chunks.length) {
|
||||
writeChunk(this, lateStyleTagResourceOpen1);
|
||||
writeChunk(
|
||||
this,
|
||||
stringToChunk(escapeTextForBrowser(resource.props.precedence)),
|
||||
);
|
||||
if (hrefs.length) {
|
||||
writeChunk(this, lateStyleTagResourceOpen2);
|
||||
for (; i < hrefs.length - 1; i++) {
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
|
||||
writeChunk(this, spaceSeparator);
|
||||
}
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
|
||||
}
|
||||
writeChunk(this, lateStyleTagResourceOpen3);
|
||||
for (i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
}
|
||||
destinationHasCapacity = writeChunkAndReturn(
|
||||
this,
|
||||
lateStyleTagTemplateClose,
|
||||
);
|
||||
|
||||
// We wrote style tags for this boundary and we may need to emit a script
|
||||
// to hoist them.
|
||||
currentlyRenderingBoundaryHasStylesToHoist = true;
|
||||
|
||||
// style resources can flush continuously since more rules may be written into
|
||||
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
|
||||
// and hrefs
|
||||
chunks.length = 0;
|
||||
hrefs.length = 0;
|
||||
}
|
||||
resource.state |= FlushedLate;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeResourcesForBoundary(
|
||||
destination: Destination,
|
||||
boundaryResources: BoundaryResources,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
didWrite = false;
|
||||
boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
|
||||
if (didWrite) {
|
||||
return writeChunkAndReturn(destination, styleTagTemplateClose);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Reset these on each invocation, they are only safe to read in this function
|
||||
currentlyRenderingBoundaryHasStylesToHoist = false;
|
||||
destinationHasCapacity = true;
|
||||
|
||||
const precedencePlaceholderStart = stringToPrecomputedChunk(
|
||||
'<style data-precedence="',
|
||||
);
|
||||
const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
|
||||
// Flush each Boundary resource
|
||||
boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
|
||||
if (currentlyRenderingBoundaryHasStylesToHoist) {
|
||||
responseState.stylesToHoist = true;
|
||||
}
|
||||
return destinationHasCapacity;
|
||||
}
|
||||
|
||||
function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
|
||||
if ((resource.state & (Flushed | Blocked)) === NoState) {
|
||||
|
@ -3271,7 +3396,7 @@ function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
|
|||
}
|
||||
|
||||
function flushResourceLate<T: Resource>(this: Destination, resource: T) {
|
||||
if ((resource.state & Flushed) === NoState) {
|
||||
if ((resource.state & (Flushed | Blocked)) === NoState) {
|
||||
const chunks = resource.chunks;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
|
@ -3280,9 +3405,16 @@ function flushResourceLate<T: Resource>(this: Destination, resource: T) {
|
|||
}
|
||||
}
|
||||
|
||||
let didFlush = false;
|
||||
// This must always be read after flushing stylesheet styles. we know we will encounter a style resource
|
||||
// per precedence and it will be set before ready so we cast this to avoid an extra check at runtime
|
||||
let precedenceStyleTagResource: StyleTagResource = (null: any);
|
||||
|
||||
function flushUnblockedStyle(
|
||||
// This flags let's us opt out of flushing a placeholder style tag to emit the precedence in the right order.
|
||||
// If a stylesheet was flushed then we have the precedence order preserved and only need to emit <style> tags
|
||||
// if there are actual chunks to flush
|
||||
let didFlushPrecedence = false;
|
||||
|
||||
function flushStyleInPreamble(
|
||||
this: Destination,
|
||||
resource: StyleResource,
|
||||
key: mixed,
|
||||
|
@ -3294,81 +3426,70 @@ function flushUnblockedStyle(
|
|||
// Set on flush but to ensure correct semantics we don't emit
|
||||
// anything if we are in this state.
|
||||
set.delete(resource);
|
||||
} else if (resource.state & Blocked) {
|
||||
// We can't flush but we can preload. We will do this in a second pass
|
||||
} else {
|
||||
didFlush = true;
|
||||
// We can emit this style or stylesheet as is.
|
||||
|
||||
if (resource.type === 'stylesheet') {
|
||||
// We still need to encode stylesheet chunks
|
||||
// because unlike most Hoistables and Resources we do not eagerly encode
|
||||
// them during render. This is because if we flush late we have to send a
|
||||
// different encoding and we don't want to encode multiple times
|
||||
pushLinkImpl(chunks, resource.props);
|
||||
if (resource.type === 'style') {
|
||||
precedenceStyleTagResource = resource;
|
||||
return;
|
||||
}
|
||||
|
||||
// We still need to encode stylesheet chunks
|
||||
// because unlike most Hoistables and Resources we do not eagerly encode
|
||||
// them during render. This is because if we flush late we have to send a
|
||||
// different encoding and we don't want to encode multiple times
|
||||
pushLinkImpl(chunks, resource.props);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
}
|
||||
resource.state |= FlushedInPreamble;
|
||||
set.delete(resource);
|
||||
didFlushPrecedence = true;
|
||||
}
|
||||
}
|
||||
|
||||
function flushUnblockedStyles(
|
||||
const styleTagResourceOpen1 = stringToPrecomputedChunk(
|
||||
'<style data-precedence="',
|
||||
);
|
||||
const styleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
|
||||
const spaceSeparator = stringToPrecomputedChunk(' ');
|
||||
const styleTagResourceOpen3 = stringToPrecomputedChunk('">');
|
||||
|
||||
const styleTagResourceClose = stringToPrecomputedChunk('</style>');
|
||||
|
||||
function flushAllStylesInPreamble(
|
||||
this: Destination,
|
||||
set: Set<StyleResource>,
|
||||
precedence: string,
|
||||
) {
|
||||
didFlush = false;
|
||||
set.forEach(flushUnblockedStyle, this);
|
||||
if (!didFlush) {
|
||||
// if we did not flush anything for this precedence slot we emit
|
||||
// an empty <style data-precedence="..." /> tag to ensure the
|
||||
// precedence remains in the correct order
|
||||
writeChunk(this, precedencePlaceholderStart);
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
|
||||
writeChunk(this, precedencePlaceholderEnd);
|
||||
}
|
||||
}
|
||||
|
||||
function preloadBlockedStyle(this: Destination, resource: StyleResource) {
|
||||
// The only Resources that should remain are Blocked resources
|
||||
if (__DEV__) {
|
||||
if ((resource.state & Blocked) === NoState) {
|
||||
console.error(
|
||||
'React encountered a Stylesheet Resource that was not Blocked when it was expected to be. This is a bug in React.',
|
||||
);
|
||||
} else if (resource.state & PreloadFlushed) {
|
||||
console.error(
|
||||
'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (resource.type === 'style') {
|
||||
// <style> tags do not need to be preloaded
|
||||
return;
|
||||
}
|
||||
const chunks = resource.chunks;
|
||||
const preloadProps = preloadAsStylePropsFromProps(
|
||||
resource.props.href,
|
||||
resource.props,
|
||||
);
|
||||
pushLinkImpl(chunks, preloadProps);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
}
|
||||
resource.state |= PreloadFlushed;
|
||||
chunks.length = 0;
|
||||
}
|
||||
|
||||
function preloadBlockedStyles(
|
||||
this: Destination,
|
||||
set: Set<StyleResource>,
|
||||
precedence: string,
|
||||
) {
|
||||
set.forEach(preloadBlockedStyle, this);
|
||||
didFlushPrecedence = false;
|
||||
set.forEach(flushStyleInPreamble, this);
|
||||
set.clear();
|
||||
|
||||
const chunks = precedenceStyleTagResource.chunks;
|
||||
const hrefs = precedenceStyleTagResource.props.hrefs;
|
||||
if (didFlushPrecedence === false || chunks.length) {
|
||||
writeChunk(this, styleTagResourceOpen1);
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
|
||||
let i = 0;
|
||||
if (hrefs.length) {
|
||||
writeChunk(this, styleTagResourceOpen2);
|
||||
for (; i < hrefs.length - 1; i++) {
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
|
||||
writeChunk(this, spaceSeparator);
|
||||
}
|
||||
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
|
||||
}
|
||||
writeChunk(this, styleTagResourceOpen3);
|
||||
for (i = 0; i < chunks.length; i++) {
|
||||
writeChunk(this, chunks[i]);
|
||||
}
|
||||
writeChunk(this, styleTagResourceClose);
|
||||
|
||||
// style resources can flush continuously since more rules may be written into
|
||||
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
|
||||
// and hrefs
|
||||
chunks.length = 0;
|
||||
hrefs.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function preloadLateStyle(this: Destination, resource: StyleResource) {
|
||||
|
@ -3480,10 +3601,7 @@ export function writePreamble(
|
|||
resources.fontPreloads.clear();
|
||||
|
||||
// Flush unblocked stylesheets by precedence
|
||||
resources.precedences.forEach(flushUnblockedStyles, destination);
|
||||
|
||||
// Flush preloads for Blocked stylesheets
|
||||
resources.precedences.forEach(preloadBlockedStyles, destination);
|
||||
resources.precedences.forEach(flushAllStylesInPreamble, destination);
|
||||
|
||||
resources.usedStylesheets.forEach(resource => {
|
||||
const key = getResourceKey(resource.props.as, resource.props.href);
|
||||
|
@ -3643,25 +3761,6 @@ export function writePostamble(
|
|||
}
|
||||
}
|
||||
|
||||
function hasStyleResourceDependencies(
|
||||
boundaryResources: BoundaryResources,
|
||||
): boolean {
|
||||
const iter = boundaryResources.values();
|
||||
// At the moment boundaries only accumulate style resources
|
||||
// so we assume the type is correct and don't check it
|
||||
while (true) {
|
||||
const {value: resource} = iter.next();
|
||||
if (!resource) break;
|
||||
|
||||
// If every style Resource flushed in the shell we do not need to send
|
||||
// any dependencies
|
||||
if ((resource.state & FlushedInPreamble) === NoState) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
|
||||
const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
|
||||
const arrayInterstitial = stringToPrecomputedChunk(',');
|
||||
|
@ -3678,7 +3777,9 @@ function writeStyleResourceDependenciesInJS(
|
|||
|
||||
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
|
||||
boundaryResources.forEach(resource => {
|
||||
if (resource.state & FlushedInPreamble) {
|
||||
if (resource.type === 'style') {
|
||||
// Style dependencies don't require coordinated reveal and can be omitted
|
||||
} else if (resource.state & FlushedInPreamble) {
|
||||
// We can elide this dependency because it was flushed in the shell and
|
||||
// should be ready before content is shown on the client
|
||||
} else if (resource.state & Flushed) {
|
||||
|
@ -3688,9 +3789,7 @@ function writeStyleResourceDependenciesInJS(
|
|||
writeChunk(destination, nextArrayOpenBrackChunk);
|
||||
writeStyleResourceDependencyHrefOnlyInJS(
|
||||
destination,
|
||||
resource.type === 'style'
|
||||
? resource.props['data-href']
|
||||
: resource.props.href,
|
||||
resource.props.href,
|
||||
);
|
||||
writeChunk(destination, arrayCloseBracket);
|
||||
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
|
||||
|
@ -3875,7 +3974,9 @@ function writeStyleResourceDependenciesInAttr(
|
|||
|
||||
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
|
||||
boundaryResources.forEach(resource => {
|
||||
if (resource.state & FlushedInPreamble) {
|
||||
if (resource.type === 'style') {
|
||||
// Style dependencies don't require coordinated reveal and can be omitted
|
||||
} else if (resource.state & FlushedInPreamble) {
|
||||
// We can elide this dependency because it was flushed in the shell and
|
||||
// should be ready before content is shown on the client
|
||||
} else if (resource.state & Flushed) {
|
||||
|
@ -3885,9 +3986,7 @@ function writeStyleResourceDependenciesInAttr(
|
|||
writeChunk(destination, nextArrayOpenBrackChunk);
|
||||
writeStyleResourceDependencyHrefOnlyInAttr(
|
||||
destination,
|
||||
resource.type === 'style'
|
||||
? resource.props['data-href']
|
||||
: resource.props.href,
|
||||
resource.props.href,
|
||||
);
|
||||
writeChunk(destination, arrayCloseBracket);
|
||||
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
|
||||
|
@ -4135,9 +4234,8 @@ type StylesheetProps = {
|
|||
type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
|
||||
|
||||
type StyleTagProps = {
|
||||
'data-href': string,
|
||||
'data-precedence': string,
|
||||
[string]: mixed,
|
||||
hrefs: Array<string>,
|
||||
precedence: string,
|
||||
};
|
||||
type StyleTagResource = TResource<'style', StyleTagProps>;
|
||||
|
||||
|
@ -4168,6 +4266,7 @@ export type Resources = {
|
|||
fontPreloads: Set<PreloadResource>,
|
||||
// usedImagePreloads: Set<PreloadResource>,
|
||||
precedences: Map<string, Set<StyleResource>>,
|
||||
stylePrecedences: Map<string, StyleTagResource>,
|
||||
usedStylesheets: Set<PreloadResource>,
|
||||
scripts: Set<ScriptResource>,
|
||||
usedScripts: Set<PreloadResource>,
|
||||
|
@ -4195,6 +4294,7 @@ export function createResources(): Resources {
|
|||
fontPreloads: new Set(),
|
||||
// usedImagePreloads: new Set(),
|
||||
precedences: new Map(),
|
||||
stylePrecedences: new Map(),
|
||||
usedStylesheets: new Set(),
|
||||
scripts: new Set(),
|
||||
usedScripts: new Set(),
|
||||
|
@ -4615,6 +4715,25 @@ function preinitImpl(
|
|||
if (!precedenceSet) {
|
||||
precedenceSet = new Set();
|
||||
resources.precedences.set(precedence, precedenceSet);
|
||||
const emptyStyleResource = {
|
||||
type: 'style',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: NoState,
|
||||
props: {
|
||||
precedence,
|
||||
hrefs: ([]: Array<string>),
|
||||
},
|
||||
};
|
||||
precedenceSet.add(emptyStyleResource);
|
||||
if (__DEV__) {
|
||||
if (resources.stylePrecedences.has(precedence)) {
|
||||
console.error(
|
||||
'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
|
||||
precedence,
|
||||
);
|
||||
}
|
||||
}
|
||||
resources.stylePrecedences.set(precedence, emptyStyleResource);
|
||||
}
|
||||
precedenceSet.add(resource);
|
||||
}
|
||||
|
@ -4769,16 +4888,6 @@ function adoptPreloadPropsForStylesheetProps(
|
|||
resourceProps.integrity = preloadProps.integrity;
|
||||
}
|
||||
|
||||
function styleTagPropsFromRawProps(rawProps: any): StyleTagProps {
|
||||
return {
|
||||
...rawProps,
|
||||
'data-precedence': rawProps.precedence,
|
||||
precedence: null,
|
||||
'data-href': rawProps.href,
|
||||
href: null,
|
||||
};
|
||||
}
|
||||
|
||||
function scriptPropsFromPreinitOptions(
|
||||
src: string,
|
||||
options: PreinitOptions,
|
||||
|
@ -4801,10 +4910,7 @@ function adoptPreloadPropsForScriptProps(
|
|||
resourceProps.integrity = preloadProps.integrity;
|
||||
}
|
||||
|
||||
function hoistStylesheetResource(
|
||||
this: BoundaryResources,
|
||||
resource: StyleResource,
|
||||
) {
|
||||
function hoistStyleResource(this: BoundaryResources, resource: StyleResource) {
|
||||
this.add(resource);
|
||||
}
|
||||
|
||||
|
@ -4814,23 +4920,10 @@ export function hoistResources(
|
|||
): void {
|
||||
const currentBoundaryResources = resources.boundaryResources;
|
||||
if (currentBoundaryResources) {
|
||||
source.forEach(hoistStylesheetResource, currentBoundaryResources);
|
||||
source.clear();
|
||||
source.forEach(hoistStyleResource, currentBoundaryResources);
|
||||
}
|
||||
}
|
||||
|
||||
function unblockStylesheet(resource: StyleResource) {
|
||||
resource.state &= ~Blocked;
|
||||
}
|
||||
|
||||
export function hoistResourcesToRoot(
|
||||
resources: Resources,
|
||||
boundaryResources: BoundaryResources,
|
||||
): void {
|
||||
boundaryResources.forEach(unblockStylesheet);
|
||||
boundaryResources.clear();
|
||||
}
|
||||
|
||||
function markAsRenderedResourceDEV(
|
||||
resource: Resource,
|
||||
originalProps: any,
|
||||
|
|
|
@ -52,6 +52,7 @@ export type ResponseState = {
|
|||
preconnectChunks: Array<Chunk | PrecomputedChunk>,
|
||||
preloadChunks: Array<Chunk | PrecomputedChunk>,
|
||||
hoistableChunks: Array<Chunk | PrecomputedChunk>,
|
||||
stylesToHoist: boolean,
|
||||
// This is an extra field for the legacy renderer
|
||||
generateStaticMarkup: boolean,
|
||||
};
|
||||
|
@ -88,6 +89,7 @@ export function createResponseState(
|
|||
preconnectChunks: responseState.preconnectChunks,
|
||||
preloadChunks: responseState.preloadChunks,
|
||||
hoistableChunks: responseState.hoistableChunks,
|
||||
stylesToHoist: responseState.stylesToHoist,
|
||||
|
||||
// This is an extra field for the legacy renderer
|
||||
generateStaticMarkup,
|
||||
|
@ -134,7 +136,6 @@ export {
|
|||
writeHoistables,
|
||||
writePostamble,
|
||||
hoistResources,
|
||||
hoistResourcesToRoot,
|
||||
setCurrentlyRenderingBoundaryResourcesTarget,
|
||||
prepareToRender,
|
||||
cleanupAfterRender,
|
||||
|
|
|
@ -20,74 +20,69 @@ const resourceMap = new Map();
|
|||
export function completeBoundaryWithStyles(
|
||||
suspenseBoundaryID,
|
||||
contentID,
|
||||
styles,
|
||||
stylesheetDescriptors,
|
||||
) {
|
||||
const stylesToHoist = new Map();
|
||||
const precedences = new Map();
|
||||
const thisDocument = document;
|
||||
let lastResource, node;
|
||||
|
||||
let nodes = thisDocument.querySelectorAll('template[data-precedence]');
|
||||
for (let i = 0; (node = nodes[i++]); ) {
|
||||
let child = node.content.firstChild;
|
||||
for (; child; child = child.nextSibling) {
|
||||
stylesToHoist.set(child.getAttribute('data-href'), child);
|
||||
}
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
// Seed the precedence list with existing resources
|
||||
nodes = thisDocument.querySelectorAll(
|
||||
// Seed the precedence list with existing resources and collect hoistable style tags
|
||||
const nodes = thisDocument.querySelectorAll(
|
||||
'link[data-precedence],style[data-precedence]',
|
||||
);
|
||||
const styleTagsToHoist = [];
|
||||
for (let i = 0; (node = nodes[i++]); ) {
|
||||
// We populate the resourceMap from found nodes so we can incorporate any
|
||||
// resources the client runtime adds when the two runtimes are running concurrently
|
||||
resourceMap.set(
|
||||
node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
|
||||
node,
|
||||
);
|
||||
precedences.set(node.dataset['precedence'], (lastResource = node));
|
||||
if (node.getAttribute('media') === 'not all') {
|
||||
styleTagsToHoist.push(node);
|
||||
} else {
|
||||
if (node.tagName === 'LINK') {
|
||||
resourceMap.set(node.getAttribute('href'), node);
|
||||
}
|
||||
precedences.set(node.dataset['precedence'], (lastResource = node));
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
const dependencies = [];
|
||||
let style, href, precedence, attr, loadingState, resourceEl, media;
|
||||
let href, precedence, attr, loadingState, resourceEl, media;
|
||||
|
||||
function setStatus(s) {
|
||||
this['s'] = s;
|
||||
}
|
||||
|
||||
while ((style = styles[i++])) {
|
||||
let j = 0;
|
||||
href = style[j++];
|
||||
// Sheets Mode
|
||||
let sheetMode = true;
|
||||
while (true) {
|
||||
if (sheetMode) {
|
||||
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
|
||||
// dependency if they already existed
|
||||
const stylesheetDescriptor = stylesheetDescriptors[i++];
|
||||
if (!stylesheetDescriptor) {
|
||||
// enter <style> Mode
|
||||
sheetMode = false;
|
||||
i = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((resourceEl = resourceMap.get(href))) {
|
||||
// We have an already known resource. It could be a <style>, a <link> created
|
||||
// by this runtime (which will have a loadingState) or a <link> created by
|
||||
// the client runtime (which will also have a loadingState). We look for a
|
||||
// loadingState and test whether it is not loaded yet and whether the media matches
|
||||
// before using it as a dependency. If it is a <style> there will be no loadingState
|
||||
// and we can avoid tracking it as a dependency because these tags don't load
|
||||
} else {
|
||||
// We haven't already processed this href so we need to hoist an element. It will
|
||||
// either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
|
||||
// or we will need to create a <link>
|
||||
if ((resourceEl = stylesToHoist.get(href))) {
|
||||
// We have a <style> which needs to be hoisted to the correct precedence
|
||||
// We set it in the resourceMap so we can bail out on future passes
|
||||
// if this is depended on more than once
|
||||
precedence = resourceEl.getAttribute('data-precedence');
|
||||
let avoidInsert = false;
|
||||
let j = 0;
|
||||
href = stylesheetDescriptor[j++];
|
||||
|
||||
if ((resourceEl = resourceMap.get(href))) {
|
||||
// We have an already inserted stylesheet.
|
||||
loadingState = resourceEl['_p'];
|
||||
avoidInsert = true;
|
||||
} else {
|
||||
// If we got this far we are depending on a <link> which is not yet in the document.
|
||||
// We haven't already processed this href so we need to construct a stylesheet and hoist it
|
||||
// We construct it here and attach a loadingState. We also check whether it matches
|
||||
// media before we include it in the dependency array.
|
||||
resourceEl = thisDocument.createElement('link');
|
||||
resourceEl.href = href;
|
||||
resourceEl.rel = 'stylesheet';
|
||||
resourceEl.dataset['precedence'] = precedence = style[j++];
|
||||
while ((attr = style[j++])) {
|
||||
resourceEl.setAttribute(attr, style[j++]);
|
||||
resourceEl.dataset['precedence'] = precedence =
|
||||
stylesheetDescriptor[j++];
|
||||
while ((attr = stylesheetDescriptor[j++])) {
|
||||
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
|
||||
}
|
||||
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
|
||||
resourceEl.onload = re;
|
||||
|
@ -97,42 +92,47 @@ export function completeBoundaryWithStyles(
|
|||
setStatus.bind(loadingState, LOADED),
|
||||
setStatus.bind(loadingState, ERRORED),
|
||||
);
|
||||
// Save this resource element so we can bailout if it is used again
|
||||
resourceMap.set(href, resourceEl);
|
||||
}
|
||||
media = resourceEl.getAttribute('media');
|
||||
if (
|
||||
loadingState &&
|
||||
loadingState['s'] !== 'l' &&
|
||||
(!media || window['matchMedia'](media).matches)
|
||||
) {
|
||||
dependencies.push(loadingState);
|
||||
}
|
||||
if (avoidInsert) {
|
||||
// We have a link that is already in the document. We don't want to fall through to the insert path
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
|
||||
resourceEl = styleTagsToHoist[i++];
|
||||
if (!resourceEl) {
|
||||
// we are done with all style tags
|
||||
break;
|
||||
}
|
||||
// Save this resource element so we can bailout if it is used again
|
||||
resourceMap.set(href, resourceEl);
|
||||
|
||||
// The prior style resource is the last one placed at a given
|
||||
// precedence or the last resource itself which may be null.
|
||||
// We grab this value and then update the last resource for this
|
||||
// precedence to be the inserted element, updating the lastResource
|
||||
// pointer if needed.
|
||||
const prior = precedences.get(precedence) || lastResource;
|
||||
if (prior === lastResource) {
|
||||
lastResource = resourceEl;
|
||||
}
|
||||
precedences.set(precedence, resourceEl);
|
||||
|
||||
// Finally, we insert the newly constructed instance at an appropriate location
|
||||
// in the Document.
|
||||
if (prior) {
|
||||
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
|
||||
} else {
|
||||
const head = thisDocument.head;
|
||||
head.insertBefore(resourceEl, head.firstChild);
|
||||
}
|
||||
precedence = resourceEl.getAttribute('data-precedence');
|
||||
resourceEl.removeAttribute('media');
|
||||
}
|
||||
|
||||
// If we are a <link> we will have a loadingState and we can use this
|
||||
// combined with matchMedia to decide if we need to await this dependency
|
||||
// loading. <style> tags won't have a loadingState so they are never awaited
|
||||
loadingState = resourceEl['_p'];
|
||||
media = resourceEl.getAttribute('media');
|
||||
if (
|
||||
loadingState &&
|
||||
loadingState['s'] !== 'l' &&
|
||||
(!media || window['matchMedia'](media).matches)
|
||||
) {
|
||||
dependencies.push(loadingState);
|
||||
// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
|
||||
const prior = precedences.get(precedence) || lastResource;
|
||||
if (prior === lastResource) {
|
||||
lastResource = resourceEl;
|
||||
}
|
||||
precedences.set(precedence, resourceEl);
|
||||
|
||||
// Finally, we insert the newly constructed instance at an appropriate location
|
||||
// in the Document.
|
||||
if (prior) {
|
||||
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
|
||||
} else {
|
||||
const head = thisDocument.head;
|
||||
head.insertBefore(resourceEl, head.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,6 @@ export const clientRenderBoundary =
|
|||
export const completeBoundary =
|
||||
'$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
|
||||
export const completeBoundaryWithStyles =
|
||||
'$RM=new Map;\n$RR=function(p,q,w){function r(l){this.s=l}for(var t=$RC,m=$RM,u=new Map,n=new Map,g=document,h,e,f=g.querySelectorAll("template[data-precedence]"),c=0;e=f[c++];){for(var b=e.content.firstChild;b;b=b.nextSibling)u.set(b.getAttribute("data-href"),b);e.parentNode.removeChild(e)}f=g.querySelectorAll("link[data-precedence],style[data-precedence]");for(c=0;e=f[c++];)m.set(e.getAttribute("STYLE"===e.nodeName?"data-href":"href"),e),n.set(e.dataset.precedence,h=e);e=0;f=[];for(var d,\nv,a;d=w[e++];){var k=0;b=d[k++];if(!(a=m.get(b))){if(a=u.get(b))c=a.getAttribute("data-precedence");else{a=g.createElement("link");a.href=b;a.rel="stylesheet";for(a.dataset.precedence=c=d[k++];v=d[k++];)a.setAttribute(v,d[k++]);d=a._p=new Promise(function(l,x){a.onload=l;a.onerror=x});d.then(r.bind(d,"l"),r.bind(d,"e"))}m.set(b,a);b=n.get(c)||h;b===h&&(h=a);n.set(c,a);b?b.parentNode.insertBefore(a,b.nextSibling):(c=g.head,c.insertBefore(a,c.firstChild))}d=a._p;c=a.getAttribute("media");!d||"l"===\nd.s||c&&!matchMedia(c).matches||f.push(d)}Promise.all(f).then(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};';
|
||||
'$RM=new Map;\n$RR=function(t,u,y){function v(n){this.s=n}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var f=y[b++];if(!f){k=!1;b=0;continue}var c=!1,m=0;var e=f[m++];if(a=p.get(e)){var d=a._p;c=!0}else{a=r.createElement("link");a.href=e;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];d=f[m++];)a.setAttribute(d,f[m++]);d=a._p=new Promise(function(n,z){a.onload=n;a.onerror=z});d.then(v.bind(d,"l"),v.bind(d,"e"));p.set(e,a)}e=a.getAttribute("media");!d||"l"===d.s||e&&!matchMedia(e).matches||h.push(d);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
|
||||
export const completeSegment =
|
||||
'$RS=function(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)};';
|
||||
|
|
|
@ -20,77 +20,72 @@ export {clientRenderBoundary, completeBoundary, completeSegment};
|
|||
export function completeBoundaryWithStyles(
|
||||
suspenseBoundaryID,
|
||||
contentID,
|
||||
styles,
|
||||
stylesheetDescriptors,
|
||||
) {
|
||||
const completeBoundaryImpl = window['$RC'];
|
||||
const resourceMap = window['$RM'];
|
||||
|
||||
const stylesToHoist = new Map();
|
||||
const precedences = new Map();
|
||||
const thisDocument = document;
|
||||
let lastResource, node;
|
||||
|
||||
let nodes = thisDocument.querySelectorAll('template[data-precedence]');
|
||||
for (let i = 0; (node = nodes[i++]); ) {
|
||||
let child = node.content.firstChild;
|
||||
for (; child; child = child.nextSibling) {
|
||||
stylesToHoist.set(child.getAttribute('data-href'), child);
|
||||
}
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
// Seed the precedence list with existing resources
|
||||
nodes = thisDocument.querySelectorAll(
|
||||
// Seed the precedence list with existing resources and collect hoistable style tags
|
||||
const nodes = thisDocument.querySelectorAll(
|
||||
'link[data-precedence],style[data-precedence]',
|
||||
);
|
||||
const styleTagsToHoist = [];
|
||||
for (let i = 0; (node = nodes[i++]); ) {
|
||||
// We populate the resourceMap from found nodes so we can incorporate any
|
||||
// resources the client runtime adds when the two runtimes are running concurrently
|
||||
resourceMap.set(
|
||||
node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
|
||||
node,
|
||||
);
|
||||
precedences.set(node.dataset['precedence'], (lastResource = node));
|
||||
if (node.getAttribute('media') === 'not all') {
|
||||
styleTagsToHoist.push(node);
|
||||
} else {
|
||||
if (node.tagName === 'LINK') {
|
||||
resourceMap.set(node.getAttribute('href'), node);
|
||||
}
|
||||
precedences.set(node.dataset['precedence'], (lastResource = node));
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
const dependencies = [];
|
||||
let style, href, precedence, attr, loadingState, resourceEl, media;
|
||||
let href, precedence, attr, loadingState, resourceEl, media;
|
||||
|
||||
function setStatus(s) {
|
||||
this['s'] = s;
|
||||
}
|
||||
|
||||
while ((style = styles[i++])) {
|
||||
let j = 0;
|
||||
href = style[j++];
|
||||
// Sheets Mode
|
||||
let sheetMode = true;
|
||||
while (true) {
|
||||
if (sheetMode) {
|
||||
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
|
||||
// dependency if they already existed
|
||||
const stylesheetDescriptor = stylesheetDescriptors[i++];
|
||||
if (!stylesheetDescriptor) {
|
||||
// enter <style> Mode
|
||||
sheetMode = false;
|
||||
i = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((resourceEl = resourceMap.get(href))) {
|
||||
// We have an already known resource. It could be a <style>, a <link> created
|
||||
// by this runtime (which will have a loadingState) or a <link> created by
|
||||
// the client runtime (which will also have a loadingState). We look for a
|
||||
// loadingState and test whether it is not loaded yet and whether the media matches
|
||||
// before using it as a dependency. If it is a <style> there will be no loadingState
|
||||
// and we can avoid tracking it as a dependency because these tags don't load
|
||||
} else {
|
||||
// We haven't already processed this href so we need to hoist an element. It will
|
||||
// either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
|
||||
// or we will need to create a <link>
|
||||
if ((resourceEl = stylesToHoist.get(href))) {
|
||||
// We have a <style> which needs to be hoisted to the correct precedence
|
||||
// We set it in the resourceMap so we can bail out on future passes
|
||||
// if this is depended on more than once
|
||||
precedence = resourceEl.getAttribute('data-precedence');
|
||||
let avoidInsert = false;
|
||||
let j = 0;
|
||||
href = stylesheetDescriptor[j++];
|
||||
|
||||
if ((resourceEl = resourceMap.get(href))) {
|
||||
// We have an already inserted stylesheet.
|
||||
loadingState = resourceEl['_p'];
|
||||
avoidInsert = true;
|
||||
} else {
|
||||
// If we got this far we are depending on a <link> which is not yet in the document.
|
||||
// We haven't already processed this href so we need to construct a stylesheet and hoist it
|
||||
// We construct it here and attach a loadingState. We also check whether it matches
|
||||
// media before we include it in the dependency array.
|
||||
resourceEl = thisDocument.createElement('link');
|
||||
resourceEl.href = href;
|
||||
resourceEl.rel = 'stylesheet';
|
||||
resourceEl.dataset['precedence'] = precedence = style[j++];
|
||||
while ((attr = style[j++])) {
|
||||
resourceEl.setAttribute(attr, style[j++]);
|
||||
resourceEl.dataset['precedence'] = precedence =
|
||||
stylesheetDescriptor[j++];
|
||||
while ((attr = stylesheetDescriptor[j++])) {
|
||||
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
|
||||
}
|
||||
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
|
||||
resourceEl.onload = re;
|
||||
|
@ -100,42 +95,47 @@ export function completeBoundaryWithStyles(
|
|||
setStatus.bind(loadingState, LOADED),
|
||||
setStatus.bind(loadingState, ERRORED),
|
||||
);
|
||||
// Save this resource element so we can bailout if it is used again
|
||||
resourceMap.set(href, resourceEl);
|
||||
}
|
||||
media = resourceEl.getAttribute('media');
|
||||
if (
|
||||
loadingState &&
|
||||
loadingState['s'] !== 'l' &&
|
||||
(!media || window['matchMedia'](media).matches)
|
||||
) {
|
||||
dependencies.push(loadingState);
|
||||
}
|
||||
if (avoidInsert) {
|
||||
// We have a link that is already in the document. We don't want to fall through to the insert path
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
|
||||
resourceEl = styleTagsToHoist[i++];
|
||||
if (!resourceEl) {
|
||||
// we are done with all style tags
|
||||
break;
|
||||
}
|
||||
// Save this resource element so we can bailout if it is used again
|
||||
resourceMap.set(href, resourceEl);
|
||||
|
||||
// The prior style resource is the last one placed at a given
|
||||
// precedence or the last resource itself which may be null.
|
||||
// We grab this value and then update the last resource for this
|
||||
// precedence to be the inserted element, updating the lastResource
|
||||
// pointer if needed.
|
||||
const prior = precedences.get(precedence) || lastResource;
|
||||
if (prior === lastResource) {
|
||||
lastResource = resourceEl;
|
||||
}
|
||||
precedences.set(precedence, resourceEl);
|
||||
|
||||
// Finally, we insert the newly constructed instance at an appropriate location
|
||||
// in the Document.
|
||||
if (prior) {
|
||||
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
|
||||
} else {
|
||||
const head = thisDocument.head;
|
||||
head.insertBefore(resourceEl, head.firstChild);
|
||||
}
|
||||
precedence = resourceEl.getAttribute('data-precedence');
|
||||
resourceEl.removeAttribute('media');
|
||||
}
|
||||
|
||||
// If we are a <link> we will have a loadingState and we can use this
|
||||
// combined with matchMedia to decide if we need to await this dependency
|
||||
// loading. <style> tags won't have a loadingState so they are never awaited
|
||||
loadingState = resourceEl['_p'];
|
||||
media = resourceEl.getAttribute('media');
|
||||
if (
|
||||
loadingState &&
|
||||
loadingState['s'] !== 'l' &&
|
||||
(!media || window['matchMedia'](media).matches)
|
||||
) {
|
||||
dependencies.push(loadingState);
|
||||
// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
|
||||
const prior = precedences.get(precedence) || lastResource;
|
||||
if (prior === lastResource) {
|
||||
lastResource = resourceEl;
|
||||
}
|
||||
precedences.set(precedence, resourceEl);
|
||||
|
||||
// Finally, we insert the newly constructed instance at an appropriate location
|
||||
// in the Document.
|
||||
if (prior) {
|
||||
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
|
||||
} else {
|
||||
const head = thisDocument.head;
|
||||
head.insertBefore(resourceEl, head.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1281,8 +1281,8 @@ body {
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="initial" data-precedence="one" />
|
||||
<link rel="stylesheet" href="foo" data-precedence="one" />
|
||||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>loading foo bar...</div>
|
||||
|
@ -1304,7 +1304,6 @@ body {
|
|||
<link rel="stylesheet" href="foo" data-precedence="one" />
|
||||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>loading foo bar...</div>
|
||||
|
@ -1329,7 +1328,6 @@ body {
|
|||
<link rel="stylesheet" href="foo" data-precedence="one" />
|
||||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>loading foo bar...</div>
|
||||
|
@ -1354,7 +1352,6 @@ body {
|
|||
<link rel="stylesheet" href="foo" data-precedence="one" />
|
||||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
@ -1379,7 +1376,6 @@ body {
|
|||
<link rel="stylesheet" href="foo" data-precedence="one" />
|
||||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
@ -1407,7 +1403,6 @@ body {
|
|||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="stylesheet" href="baz" data-precedence="two" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
@ -1449,7 +1444,6 @@ body {
|
|||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="stylesheet" href="baz" data-precedence="two" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
@ -1482,7 +1476,6 @@ body {
|
|||
<link rel="stylesheet" href="preset" data-precedence="preset" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="default" />
|
||||
<link rel="stylesheet" href="baz" data-precedence="two" />
|
||||
<link rel="preload" href="foo" as="style" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
@ -3499,6 +3492,7 @@ body {
|
|||
<div>1</div>,
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
|
||||
function App() {
|
||||
|
@ -4157,19 +4151,14 @@ background-color: green;
|
|||
resolveText('first');
|
||||
});
|
||||
|
||||
const styleTemplates = document.querySelectorAll(
|
||||
'template[data-precedence]',
|
||||
);
|
||||
expect(styleTemplates.length).toBe(1);
|
||||
expect(getMeaningfulChildren(styleTemplates[0].content)).toEqual(
|
||||
<style data-href="foo" data-precedence="default">
|
||||
{css}
|
||||
</style>,
|
||||
);
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body />
|
||||
<body>
|
||||
<style data-href="foo" data-precedence="default" media="not all">
|
||||
{css}
|
||||
</style>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
|
@ -4194,6 +4183,266 @@ background-color: green;
|
|||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can hoist styles flushed early even when no other style dependencies are flushed on completion', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<Suspense fallback="loading...">
|
||||
<BlockedOn value="first">
|
||||
<style href="foo" precedence="default">
|
||||
some css
|
||||
</style>
|
||||
<div>first</div>
|
||||
<BlockedOn value="second">
|
||||
<div>second</div>
|
||||
</BlockedOn>
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>loading...</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// When we resolve first we flush the style tag because it is ready but we aren't yet ready to
|
||||
// flush the entire boundary and reveal it.
|
||||
await act(() => {
|
||||
resolveText('first');
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
loading...
|
||||
<style data-href="foo" data-precedence="default" media="not all">
|
||||
some css
|
||||
</style>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// When we resolve second we flush the rest of the boundary segments and reveal the boundary. The style tag
|
||||
// is hoisted during this reveal process even though no other styles flushed during this tick
|
||||
await act(() => {
|
||||
resolveText('second');
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<style data-href="foo" data-precedence="default">
|
||||
some css
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>first</div>
|
||||
<div>second</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can emit multiple style rules into a single style tag for a given precedence', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<style href="1" precedence="default">
|
||||
1
|
||||
</style>
|
||||
<style href="2" precedence="foo">
|
||||
foo2
|
||||
</style>
|
||||
<style href="3" precedence="default">
|
||||
3
|
||||
</style>
|
||||
<style href="4" precedence="default">
|
||||
4
|
||||
</style>
|
||||
<style href="5" precedence="foo">
|
||||
foo5
|
||||
</style>
|
||||
<div>initial</div>
|
||||
<Suspense fallback="loading...">
|
||||
<BlockedOn value="first">
|
||||
<style href="6" precedence="default">
|
||||
6
|
||||
</style>
|
||||
<style href="7" precedence="foo">
|
||||
foo7
|
||||
</style>
|
||||
<style href="8" precedence="default">
|
||||
8
|
||||
</style>
|
||||
<style href="9" precedence="default">
|
||||
9
|
||||
</style>
|
||||
<style href="10" precedence="foo">
|
||||
foo10
|
||||
</style>
|
||||
<div>first</div>
|
||||
<BlockedOn value="second">
|
||||
<style href="11" precedence="default">
|
||||
11
|
||||
</style>
|
||||
<style href="12" precedence="foo">
|
||||
foo12
|
||||
</style>
|
||||
<style href="13" precedence="default">
|
||||
13
|
||||
</style>
|
||||
<style href="14" precedence="default">
|
||||
14
|
||||
</style>
|
||||
<style href="15" precedence="foo">
|
||||
foo15
|
||||
</style>
|
||||
<div>second</div>
|
||||
</BlockedOn>
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<style data-href="1 3 4" data-precedence="default">
|
||||
134
|
||||
</style>
|
||||
<style data-href="2 5" data-precedence="foo">
|
||||
foo2foo5
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>initial</div>loading...
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// When we resolve first we flush the style tag because it is ready but we aren't yet ready to
|
||||
// flush the entire boundary and reveal it.
|
||||
await act(() => {
|
||||
resolveText('first');
|
||||
});
|
||||
await act(() => {
|
||||
resolveText('second');
|
||||
});
|
||||
|
||||
// Some sets of styles were ready before the entire boundary and they got emitted as early as they were
|
||||
// ready. The remaining styles were ready when the boundary finished and they got grouped as well
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<style data-href="1 3 4" data-precedence="default">
|
||||
134
|
||||
</style>
|
||||
<style data-href="6 8 9" data-precedence="default">
|
||||
689
|
||||
</style>
|
||||
<style data-href="11 13 14" data-precedence="default">
|
||||
111314
|
||||
</style>
|
||||
<style data-href="2 5" data-precedence="foo">
|
||||
foo2foo5
|
||||
</style>
|
||||
<style data-href="7 10" data-precedence="foo">
|
||||
foo7foo10
|
||||
</style>
|
||||
<style data-href="12 15" data-precedence="foo">
|
||||
foo12foo15
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>initial</div>
|
||||
<div>first</div>
|
||||
<div>second</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// Client inserted style tags are not grouped together but can hydrate against a grouped set
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<html>
|
||||
<body>
|
||||
<style href="1" precedence="default">
|
||||
1
|
||||
</style>
|
||||
<style href="2" precedence="foo">
|
||||
foo2
|
||||
</style>
|
||||
<style href="16" precedence="default">
|
||||
16
|
||||
</style>
|
||||
<style href="17" precedence="default">
|
||||
17
|
||||
</style>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<style data-href="1 3 4" data-precedence="default">
|
||||
134
|
||||
</style>
|
||||
<style data-href="6 8 9" data-precedence="default">
|
||||
689
|
||||
</style>
|
||||
<style data-href="11 13 14" data-precedence="default">
|
||||
111314
|
||||
</style>
|
||||
<style data-href="16" data-precedence="default">
|
||||
16
|
||||
</style>
|
||||
<style data-href="17" data-precedence="default">
|
||||
17
|
||||
</style>
|
||||
<style data-href="2 5" data-precedence="foo">
|
||||
foo2foo5
|
||||
</style>
|
||||
<style data-href="7 10" data-precedence="foo">
|
||||
foo7foo10
|
||||
</style>
|
||||
<style data-href="12 15" data-precedence="foo">
|
||||
foo12foo15
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>initial</div>
|
||||
<div>first</div>
|
||||
<div>second</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('warns if you render a <style> with an href with a space on the server', async () => {
|
||||
await expect(async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<style href="foo bar" precedence="default">
|
||||
style
|
||||
</style>
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
}).toErrorDev(
|
||||
'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "foo bar".',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Script Resources', () => {
|
||||
|
|
|
@ -339,11 +339,6 @@ export function hoistResources(
|
|||
boundaryResources: BoundaryResources,
|
||||
) {}
|
||||
|
||||
export function hoistResourcesToRoot(
|
||||
resources: Resources,
|
||||
boundaryResources: BoundaryResources,
|
||||
) {}
|
||||
|
||||
export function prepareToRender(resources: Resources) {}
|
||||
export function cleanupAfterRender(previousDispatcher: mixed) {}
|
||||
export function createResources() {}
|
||||
|
@ -356,6 +351,7 @@ export function setCurrentlyRenderingBoundaryResourcesTarget(
|
|||
export function writeResourcesForBoundary(
|
||||
destination: Destination,
|
||||
boundaryResources: BoundaryResources,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,6 @@ import {
|
|||
writeHoistables,
|
||||
writePostamble,
|
||||
hoistResources,
|
||||
hoistResourcesToRoot,
|
||||
prepareToRender,
|
||||
cleanupAfterRender,
|
||||
setCurrentlyRenderingBoundaryResourcesTarget,
|
||||
|
@ -594,11 +593,6 @@ function renderSuspenseBoundary(
|
|||
contentRootSegment.textEmbedded,
|
||||
);
|
||||
contentRootSegment.status = COMPLETED;
|
||||
if (enableFloat) {
|
||||
if (newBoundary.pendingTasks === 0) {
|
||||
hoistCompletedBoundaryResources(request, newBoundary);
|
||||
}
|
||||
}
|
||||
queueCompletedSegment(newBoundary, contentRootSegment);
|
||||
if (newBoundary.pendingTasks === 0) {
|
||||
// This must have been the last segment we were waiting on. This boundary is now complete.
|
||||
|
@ -652,19 +646,6 @@ function renderSuspenseBoundary(
|
|||
popComponentStackInDEV(task);
|
||||
}
|
||||
|
||||
function hoistCompletedBoundaryResources(
|
||||
request: Request,
|
||||
completedBoundary: SuspenseBoundary,
|
||||
): void {
|
||||
if (request.completedRootSegment !== null || request.pendingRootTasks > 0) {
|
||||
// The Shell has not flushed yet. we can hoist Resources for this boundary
|
||||
// all the way to the Root.
|
||||
hoistResourcesToRoot(request.resources, completedBoundary.resources);
|
||||
}
|
||||
// We don't hoist if the root already flushed because late resources will be hoisted
|
||||
// as boundaries flush
|
||||
}
|
||||
|
||||
function renderBackupSuspenseBoundary(
|
||||
request: Request,
|
||||
task: Task,
|
||||
|
@ -1802,9 +1783,6 @@ function finishedTask(
|
|||
queueCompletedSegment(boundary, segment);
|
||||
}
|
||||
}
|
||||
if (enableFloat) {
|
||||
hoistCompletedBoundaryResources(request, boundary);
|
||||
}
|
||||
if (boundary.parentFlushed) {
|
||||
// The segment might be part of a segment that didn't flush yet, but if the boundary's
|
||||
// parent flushed, we need to schedule the boundary to be emitted.
|
||||
|
@ -2177,7 +2155,11 @@ function flushCompletedBoundary(
|
|||
completedSegments.length = 0;
|
||||
|
||||
if (enableFloat) {
|
||||
writeResourcesForBoundary(destination, boundary.resources);
|
||||
writeResourcesForBoundary(
|
||||
destination,
|
||||
boundary.resources,
|
||||
request.responseState,
|
||||
);
|
||||
}
|
||||
|
||||
return writeCompletedBoundaryInstruction(
|
||||
|
@ -2221,7 +2203,11 @@ function flushPartialBoundary(
|
|||
// if there is no backpressure. Later before we complete the boundary we
|
||||
// will write resources regardless of backpressure before we emit the
|
||||
// completion instruction
|
||||
return writeResourcesForBoundary(destination, boundary.resources);
|
||||
return writeResourcesForBoundary(
|
||||
destination,
|
||||
boundary.resources,
|
||||
request.responseState,
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ export const writePreamble = $$$hostConfig.writePreamble;
|
|||
export const writeHoistables = $$$hostConfig.writeHoistables;
|
||||
export const writePostamble = $$$hostConfig.writePostamble;
|
||||
export const hoistResources = $$$hostConfig.hoistResources;
|
||||
export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
|
||||
export const createResources = $$$hostConfig.createResources;
|
||||
export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
|
||||
export const setCurrentlyRenderingBoundaryResourcesTarget =
|
||||
|
|
|
@ -113,7 +113,7 @@ export const enableUseEffectEventHook = __EXPERIMENTAL__;
|
|||
// Test in www before enabling in open source.
|
||||
// Enables DOM-server to stream its instruction set as data-attributes
|
||||
// (handled with an MutationObserver) instead of inline-scripts
|
||||
export const enableFizzExternalRuntime = false;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chopping Block
|
||||
|
|
Loading…
Reference in New Issue