@samitouri / QOS-React-2 / commits / d1ced9fd58

[Float] support all links as Resources (#25515)

stacked on https://github.com/facebook/react/pull/25514 This PR adds support for any type of Link as long as it has a string rel and href and does not include an onLoad or onError property. The semantics for generic link resources matches other head resources, they will be inserted and removed as their ref counts go positive and back to zero. Keys are based on rel, href, sizes, and media. on the server preconnect and prefetch-dns are privileged and will emit near the start of the stream.

Josh Story committed Oct 21, 2022 at 22:38 UTC d1ced9fd58a1ba91febacba8ceaf2e17640fb942
5 files changed +371 -50
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+88 -15
@@ -13,7 +13,7 @@ import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
13 const {Dispatcher} = ReactDOMSharedInternals;
14 import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
15 import {
16 - validateUnmatchedLinkResourceProps,
16 + warnOnMissingHrefAndRel,
17 validatePreloadResourceDifference,
18 validateURLKeyedUpdatedProps,
19 validateStyleResourceDifference,
@@ -54,7 +54,7 @@ type StyleProps = {
54 'data-precedence': string,
55 [string]: mixed,
56 };
57 -export type StyleResource = {
57 +type StyleResource = {
58 type: 'style',
59
60 // Ref count for resource
@@ -79,7 +79,7 @@ type ScriptProps = {
79 src: string,
80 [string]: mixed,
81 };
82 -export type ScriptResource = {
82 +type ScriptResource = {
83 type: 'script',
84 src: string,
85 props: ScriptProps,
@@ -88,12 +88,10 @@ export type ScriptResource = {
88 root: FloatRoot,
89 };
90
91 -export type HeadResource = TitleResource | MetaResource;
92 -
91 type TitleProps = {
92 [string]: mixed,
93 };
96 -export type TitleResource = {
94 +type TitleResource = {
95 type: 'title',
96 props: TitleProps,
97
@@ -105,7 +103,7 @@ export type TitleResource = {
103 type MetaProps = {
104 [string]: mixed,
105 };
108 -export type MetaResource = {
106 +type MetaResource = {
107 type: 'meta',
108 matcher: string,
109 property: ?string,
@@ -117,8 +115,23 @@ export type MetaResource = {
115 root: Document,
116 };
117
118 +type LinkProps = {
119 + href: string,
120 + rel: string,
121 + [string]: mixed,
122 +};
123 +type LinkResource = {
124 + type: 'link',
125 + props: LinkProps,
126 +
127 + count: number,
128 + instance: ?Element,
129 + root: Document,
130 +};
131 +
132 type Props = {[string]: mixed};
133
134 +type HeadResource = TitleResource | MetaResource | LinkResource;
135 type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
136
137 export type RootResources = {
@@ -617,8 +630,30 @@ export function getResource(
630 return null;
631 }
632 default: {
633 + const {href, sizes, media} = pendingProps;
634 + if (typeof rel === 'string' && typeof href === 'string') {
635 + const sizeKey =
636 + '::sizes:' + (typeof sizes === 'string' ? sizes : '');
637 + const mediaKey =
638 + '::media:' + (typeof media === 'string' ? media : '');
639 + const key = 'rel:' + rel + '::href:' + href + sizeKey + mediaKey;
640 + const headRoot = getDocumentFromRoot(resourceRoot);
641 + const headResources = getResourcesFromRoot(headRoot).head;
642 + let resource = headResources.get(key);
643 + if (!resource) {
644 + resource = {
645 + type: 'link',
646 + props: Object.assign({}, pendingProps),
647 + count: 0,
648 + instance: null,
649 + root: headRoot,
650 + };
651 + headResources.set(key, resource);
652 + }
653 + return resource;
654 + }
655 if (__DEV__) {
621 - validateUnmatchedLinkResourceProps(pendingProps, currentProps);
656 + warnOnMissingHrefAndRel(pendingProps, currentProps);
657 }
658 return null;
659 }
@@ -710,6 +745,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
745 export function acquireResource(resource: Resource): Instance {
746 switch (resource.type) {
747 case 'title':
748 + case 'link':
749 case 'meta': {
750 return acquireHeadResource(resource);
751 }
@@ -732,6 +768,7 @@ export function acquireResource(resource: Resource): Instance {
768
769 export function releaseResource(resource: Resource): void {
770 switch (resource.type) {
771 + case 'link':
772 case 'title':
773 case 'meta': {
774 return releaseHeadResource(resource);
@@ -1050,6 +1087,41 @@ function acquireHeadResource(resource: HeadResource): Instance {
1087 insertResourceInstanceBefore(root, instance, insertBefore);
1088 break;
1089 }
1090 + case 'link': {
1091 + const linkProps: LinkProps = (props: any);
1092 + const limitedEscapedRel = escapeSelectorAttributeValueInsideDoubleQuotes(
1093 + linkProps.rel,
1094 + );
1095 + const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
1096 + linkProps.href,
1097 + );
1098 + let selector = `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`;
1099 + if (typeof linkProps.sizes === 'string') {
1100 + const limitedEscapedSizes = escapeSelectorAttributeValueInsideDoubleQuotes(
1101 + linkProps.sizes,
1102 + );
1103 + selector += `[sizes="${limitedEscapedSizes}"]`;
1104 + }
1105 + if (typeof linkProps.media === 'string') {
1106 + const limitedEscapedMedia = escapeSelectorAttributeValueInsideDoubleQuotes(
1107 + linkProps.media,
1108 + );
1109 + selector += `[media="${limitedEscapedMedia}"]`;
1110 + }
1111 + const existingEl = root.querySelector(selector);
1112 + if (existingEl) {
1113 + instance = resource.instance = existingEl;
1114 + markNodeAsResource(instance);
1115 + return instance;
1116 + }
1117 + instance = resource.instance = createResourceInstance(
1118 + type,
1119 + props,
1120 + root,
1121 + );
1122 + insertResourceInstanceBefore(root, instance, null);
1123 + return instance;
1124 + }
1125 default: {
1126 throw new Error(
1127 `acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
@@ -1265,26 +1337,27 @@ export function isHostResourceType(type: string, props: Props): boolean {
1337 return true;
1338 }
1339 case 'link': {
1340 + const {onLoad, onError} = props;
1341 + if (onLoad || onError) {
1342 + return false;
1343 + }
1344 switch (props.rel) {
1345 case 'stylesheet': {
1346 if (__DEV__) {
1347 validateLinkPropsForStyleResource(props);
1348 }
1273 - const {href, precedence, onLoad, onError, disabled} = props;
1349 + const {href, precedence, disabled} = props;
1350 return (
1351 typeof href === 'string' &&
1352 typeof precedence === 'string' &&
1277 - !onLoad &&
1278 - !onError &&
1353 disabled == null
1354 );
1355 }
1282 - case 'preload': {
1283 - const {href, onLoad, onError} = props;
1284 - return !onLoad && !onError && typeof href === 'string';
1356 + default: {
1357 + const {rel, href} = props;
1358 + return typeof href === 'string' && typeof rel === 'string';
1359 }
1360 }
1287 - return false;
1361 }
1362 case 'script': {
1363 // We don't validate because it is valid to use async with onLoad/onError unlike combining
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+46 -4
@@ -89,8 +89,20 @@ type MetaResource = {
89 flushed: boolean,
90 };
91
92 +type LinkProps = {
93 + href: string,
94 + rel: string,
95 + [string]: mixed,
96 +};
97 +type LinkResource = {
98 + type: 'link',
99 + props: LinkProps,
100 +
101 + flushed: boolean,
102 +};
103 +
104 export type Resource = PreloadResource | StyleResource | ScriptResource;
93 -export type HeadResource = TitleResource | MetaResource;
105 +export type HeadResource = TitleResource | MetaResource | LinkResource;
106
107 export type Resources = {
108 // Request local cache
@@ -101,6 +113,7 @@ export type Resources = {
113
114 // Flushing queues for Resource dependencies
115 charset: null | MetaResource,
116 + preconnects: Set<LinkResource>,
117 fontPreloads: Set<PreloadResource>,
118 // usedImagePreloads: Set<PreloadResource>,
119 precedences: Map<string, Set<StyleResource>>,
@@ -131,6 +144,7 @@ export function createResources(): Resources {
144
145 // cleared on flush
146 charset: null,
147 + preconnects: new Set(),
148 fontPreloads: new Set(),
149 // usedImagePreloads: new Set(),
150 precedences: new Map(),
@@ -697,10 +711,11 @@ export function resourcesFromLink(props: Props): boolean {
711 const resources = currentResources;
712
713 const {rel, href} = props;
700 - if (!href || typeof href !== 'string') {
714 + if (!href || typeof href !== 'string' || !rel || typeof rel !== 'string') {
715 return false;
716 }
717
718 + let key = '';
719 switch (rel) {
720 case 'stylesheet': {
721 const {onLoad, onError, precedence, disabled} = props;
@@ -813,10 +828,37 @@ export function resourcesFromLink(props: Props): boolean {
828 return true;
829 }
830 }
816 - return false;
831 + break;
832 }
833 }
819 - return false;
834 + if (props.onLoad || props.onError) {
835 + return false;
836 + }
837 +
838 + const sizes = typeof props.sizes === 'string' ? props.sizes : '';
839 + const media = typeof props.media === 'string' ? props.media : '';
840 + key =
841 + 'rel:' + rel + '::href:' + href + '::sizes:' + sizes + '::media:' + media;
842 + let resource = resources.headsMap.get(key);
843 + if (!resource) {
844 + resource = {
845 + type: 'link',
846 + props: Object.assign({}, props),
847 + flushed: false,
848 + };
849 + resources.headsMap.set(key, resource);
850 + switch (rel) {
851 + case 'preconnect':
852 + case 'dns-prefetch': {
853 + resources.preconnects.add(resource);
854 + break;
855 + }
856 + default: {
857 + resources.headResources.add(resource);
858 + }
859 + }
860 + }
861 + return true;
862 }
863
864 // Construct a resource from link props.
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+24
@@ -2340,6 +2340,7 @@ export function writeInitialResources(
2340
2341 const {
2342 charset,
2343 + preconnects,
2344 fontPreloads,
2345 precedences,
2346 usedStylePreloads,
@@ -2356,6 +2357,13 @@ export function writeInitialResources(
2357 resources.charset = null;
2358 }
2359
2360 + preconnects.forEach(r => {
2361 + // font preload Resources should not already be flushed so we elide this check
2362 + pushLinkImpl(target, r.props, responseState);
2363 + r.flushed = true;
2364 + });
2365 + preconnects.clear();
2366 +
2367 fontPreloads.forEach(r => {
2368 // font preload Resources should not already be flushed so we elide this check
2369 pushLinkImpl(target, r.props, responseState);
@@ -2418,6 +2426,10 @@ export function writeInitialResources(
2426 pushSelfClosing(target, r.props, 'meta', responseState);
2427 break;
2428 }
2429 + case 'link': {
2430 + pushLinkImpl(target, r.props, responseState);
2431 + break;
2432 + }
2433 }
2434 r.flushed = true;
2435 });
@@ -2450,6 +2462,7 @@ export function writeImmediateResources(
2462
2463 const {
2464 charset,
2465 + preconnects,
2466 fontPreloads,
2467 usedStylePreloads,
2468 scripts,
@@ -2465,6 +2478,13 @@ export function writeImmediateResources(
2478 resources.charset = null;
2479 }
2480
2481 + preconnects.forEach(r => {
2482 + // font preload Resources should not already be flushed so we elide this check
2483 + pushLinkImpl(target, r.props, responseState);
2484 + r.flushed = true;
2485 + });
2486 + preconnects.clear();
2487 +
2488 fontPreloads.forEach(r => {
2489 // font preload Resources should not already be flushed so we elide this check
2490 pushLinkImpl(target, r.props, responseState);
@@ -2507,6 +2527,10 @@ export function writeImmediateResources(
2527 pushSelfClosing(target, r.props, 'meta', responseState);
2528 break;
2529 }
2530 + case 'link': {
2531 + pushLinkImpl(target, r.props, responseState);
2532 + break;
2533 + }
2534 }
2535 r.flushed = true;
2536 });
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
+40 -22
@@ -11,7 +11,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
11
12 type Props = {[string]: mixed};
13
14 -export function validateUnmatchedLinkResourceProps(
14 +export function warnOnMissingHrefAndRel(
15 pendingProps: Props,
16 currentProps: ?Props,
17 ) {
@@ -24,34 +24,52 @@ export function validateUnmatchedLinkResourceProps(
24 const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
25 currentProps.rel,
26 );
27 - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
27 + const pendingRel = getValueDescriptorExpectingEnumForWarning(
28 pendingProps.rel,
29 );
30 - const pendingHrefStatement =
31 - typeof pendingProps.href === 'string'
32 - ? ` and the updated href is "${pendingProps.href}"`
33 - : '';
34 - console.error(
35 - 'A <link> previously rendered as a %s but was updated with a rel type that is not' +
36 - ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
37 - ' props however in some limited circumstances it can be valid when changing the href.' +
38 - ' When React encounters props that invalidate the Resource it is the same as not rendering' +
39 - ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
40 - ' rel for this instance was %s. The updated rel is %s%s.',
41 - originalResourceName,
42 - originalRelStatement,
43 - pendingRelStatement,
44 - pendingHrefStatement,
30 + const pendingHref = getValueDescriptorExpectingEnumForWarning(
31 + pendingProps.href,
32 );
33 + if (typeof pendingProps.rel !== 'string') {
34 + console.error(
35 + 'A <link> previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
36 + ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
37 + ' do not render the <link> anymore.',
38 + originalResourceName,
39 + originalRelStatement,
40 + pendingRel,
41 + );
42 + } else if (typeof pendingProps.href !== 'string') {
43 + console.error(
44 + 'A <link> previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
45 + ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
46 + ' do not render the <link> anymore.',
47 + originalResourceName,
48 + pendingHref,
49 + );
50 + }
51 } else {
47 - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
52 + const pendingRel = getValueDescriptorExpectingEnumForWarning(
53 pendingProps.rel,
54 );
50 - console.error(
51 - 'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
52 - ' This is a bug in React.',
53 - pendingRelStatement,
55 + const pendingHref = getValueDescriptorExpectingEnumForWarning(
56 + pendingProps.href,
57 );
58 + if (typeof pendingProps.rel !== 'string') {
59 + console.error(
60 + 'A <link> is rendering with an invalid rel: %s. When a link' +
61 + ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
62 + ' do not render the <link> anymore.',
63 + pendingRel,
64 + );
65 + } else if (typeof pendingProps.href !== 'string') {
66 + console.error(
67 + 'A <link> is rendering with an invalid href: %s. When a link' +
68 + ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
69 + ' do not render the <link> anymore.',
70 + pendingHref,
71 + );
72 + }
73 }
74 }
75 }
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+173 -9
@@ -389,29 +389,29 @@ describe('ReactDOMFloat', () => {
389 root.render(
390 <div>
391 <link rel="stylesheet" href="foo" precedence="foo" />
392 + <link rel="stylesheet" href="bar" precedence="foo" />
393 </div>,
394 );
395 expect(Scheduler).toFlushWithoutYielding();
396 root.render(
397 <div>
397 - <link rel="author" href="bar" />
398 + <link rel={() => {}} href="bar" />
399 + <link rel="foo" href={() => {}} />
400 </div>,
401 );
402 expect(() => {
403 expect(Scheduler).toFlushWithoutYielding();
402 - }).toErrorDev(
403 - 'Warning: A <link> previously rendered as a Resource with href "foo" but was updated with a rel type that is not' +
404 - ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
405 - ' props however in some limited circumstances it can be valid when changing the href.' +
406 - ' When React encounters props that invalidate the Resource it is the same as not rendering' +
407 - ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
408 - ' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
409 - );
404 + }).toErrorDev([
405 + 'Warning: A <link> previously rendered as a Resource with href "foo" with rel ""stylesheet"" but was updated with an invalid rel: something with type "function". When a link does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
406 + 'Warning: A <link> previously rendered as a Resource with href "bar" but was updated with an invalid href prop: something with type "function". When a link does not have a valid href prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
407 + ]);
408 expect(getMeaningfulChildren(document)).toEqual(
409 <html>
410 <head>
411 <link rel="stylesheet" href="foo" data-precedence="foo" />
412 + <link rel="stylesheet" href="bar" data-precedence="foo" />
413 <link rel="preload" as="style" href="foo" />
414 + <link rel="preload" as="style" href="bar" />
415 </head>
416 <body>
417 <div id="container">
@@ -941,6 +941,170 @@ describe('ReactDOMFloat', () => {
941 });
942
943 describe('head resources', () => {
944 + // @gate enableFloat
945 + it('supports preconnects, prefetc-dns, and arbitrary other link types', async () => {
946 + await actIntoEmptyDocument(() => {
947 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
948 + <html>
949 + <head />
950 + <body>
951 + <div>hello world</div>
952 + </body>
953 + <link rel="foo" href="bar" />
954 + <link rel="preload" href="bar" />
955 + <link rel="preload" href="bar" as="style" />
956 + <link rel="stylesheet" href="bar" precedence="default" />
957 + <link rel="preconnect" href="bar" />
958 + <link rel="dns-prefetch" href="bar" />
959 + <link rel="icon" href="bar" />
960 + <link rel="icon" href="bar" sizes="1x1" />
961 + <link rel="icon" href="bar" media="foo" />
962 + <link rel="shortcut icon" href="bar" />
963 + <link rel="apple-touch-icon" href="bar" />
964 + </html>,
965 + );
966 + pipe(writable);
967 + });
968 + // "preconnect" and "dns-prefetch" get hoisted to the front.
969 + // All other generic links (not styles, or typed preloads)
970 + // get emitted after styles and other higher priority Resources
971 + // Sizes and Media are part of generic link keys
972 + expect(getMeaningfulChildren(document)).toEqual(
973 + <html>
974 + <head>
975 + <link rel="preconnect" href="bar" />
976 + <link rel="dns-prefetch" href="bar" />
977 + <link rel="stylesheet" href="bar" data-precedence="default" />
978 + <link rel="foo" href="bar" />
979 + <link rel="preload" href="bar" />
980 + <link rel="icon" href="bar" />
981 + <link rel="icon" href="bar" sizes="1x1" />
982 + <link rel="icon" href="bar" media="foo" />
983 + <link rel="shortcut icon" href="bar" />
984 + <link rel="apple-touch-icon" href="bar" />
985 + </head>
986 + <body>
987 + <div>hello world</div>
988 + </body>
989 + </html>,
990 + );
991 +
992 + const root = ReactDOMClient.hydrateRoot(
993 + document,
994 + <html>
995 + <head />
996 + <body>
997 + <div>hello world</div>
998 + </body>
999 + <link rel="foo" href="bar" />
1000 + <link rel="preload" href="bar" />
1001 + <link rel="preload" href="bar" as="style" />
1002 + <link rel="stylesheet" href="bar" precedence="default" />
1003 + <link rel="preconnect" href="bar" />
1004 + <link rel="dns-prefetch" href="bar" />
1005 + <link rel="icon" href="bar" />
1006 + <link rel="icon" href="bar" sizes="1x1" />
1007 + <link rel="icon" href="bar" media="foo" />
1008 + <link rel="shortcut icon" href="bar" />
1009 + <link rel="apple-touch-icon" href="bar" />
1010 + </html>,
1011 + );
1012 + expect(Scheduler).toFlushWithoutYielding();
1013 + expect(getMeaningfulChildren(document)).toEqual(
1014 + <html>
1015 + <head>
1016 + <link rel="preconnect" href="bar" />
1017 + <link rel="dns-prefetch" href="bar" />
1018 + <link rel="stylesheet" href="bar" data-precedence="default" />
1019 + <link rel="foo" href="bar" />
1020 + <link rel="preload" href="bar" />
1021 + <link rel="icon" href="bar" />
1022 + <link rel="icon" href="bar" sizes="1x1" />
1023 + <link rel="icon" href="bar" media="foo" />
1024 + <link rel="shortcut icon" href="bar" />
1025 + <link rel="apple-touch-icon" href="bar" />
1026 + </head>
1027 + <body>
1028 + <div>hello world</div>
1029 + </body>
1030 + </html>,
1031 + );
1032 +
1033 + root.render(
1034 + <html>
1035 + <head />
1036 + <body>
1037 + <div>hello world</div>
1038 + </body>
1039 + </html>,
1040 + );
1041 + expect(Scheduler).toFlushWithoutYielding();
1042 + expect(getMeaningfulChildren(document)).toEqual(
1043 + <html>
1044 + <head>
1045 + <link rel="stylesheet" href="bar" data-precedence="default" />
1046 + <link rel="preload" href="bar" />
1047 + </head>
1048 + <body>
1049 + <div>hello world</div>
1050 + </body>
1051 + </html>,
1052 + );
1053 + });
1054 + // @gate enableFloat
1055 + it('can render icons and apple-touch-icons as resources', async () => {
1056 + await actIntoEmptyDocument(() => {
1057 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1058 + <>
1059 + <html>
1060 + <head />
1061 + <body>
1062 + <link rel="icon" href="foo" />
1063 + <div>hello world</div>
1064 + </body>
1065 + </html>
1066 + <link rel="apple-touch-icon" href="foo" />
1067 + </>,
1068 + );
1069 + pipe(writable);
1070 + });
1071 + expect(getMeaningfulChildren(document)).toEqual(
1072 + <html>
1073 + <head>
1074 + <link rel="icon" href="foo" />
1075 + <link rel="apple-touch-icon" href="foo" />
1076 + </head>
1077 + <body>
1078 + <div>hello world</div>
1079 + </body>
1080 + </html>,
1081 + );
1082 +
1083 + ReactDOMClient.hydrateRoot(
1084 + document,
1085 + <html>
1086 + <link rel="apple-touch-icon" href="foo" />
1087 + <head />
1088 + <body>
1089 + <link rel="icon" href="foo" />
1090 + <div>hello world</div>
1091 + </body>
1092 + </html>,
1093 + );
1094 + expect(Scheduler).toFlushWithoutYielding();
1095 + expect(getMeaningfulChildren(document)).toEqual(
1096 + <html>
1097 + <head>
1098 + <link rel="icon" href="foo" />
1099 + <link rel="apple-touch-icon" href="foo" />
1100 + </head>
1101 + <body>
1102 + <div>hello world</div>
1103 + </body>
1104 + </html>,
1105 + );
1106 + });
1107 +
1108 // @gate enableFloat
1109 it('can hydrate the right instances for deeply nested structured metas', async () => {
1110 await actIntoEmptyDocument(() => {