@bleedingdev/modern-js-runtime-extensions 3.8.3-ultramodern.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cjs/helmetCompat.js +419 -0
- package/dist/cjs/html.js +45 -0
- package/dist/cjs/index.js +83 -0
- package/dist/cjs/navigationWarmup.js +59 -0
- package/dist/cjs/node.js +143 -0
- package/dist/cjs/rendererHead.js +263 -0
- package/dist/cjs/rscHtmlStream.js +128 -0
- package/dist/cjs/safeFailure.js +125 -0
- package/dist/cjs/shellStream.js +76 -0
- package/dist/esm/helmetCompat.mjs +357 -0
- package/dist/esm/html.mjs +4 -0
- package/dist/esm/index.mjs +4 -0
- package/dist/esm/navigationWarmup.mjs +21 -0
- package/dist/esm/node.mjs +61 -0
- package/dist/esm/rendererHead.mjs +162 -0
- package/dist/esm/rscHtmlStream.mjs +86 -0
- package/dist/esm/safeFailure.mjs +78 -0
- package/dist/esm/shellStream.mjs +38 -0
- package/dist/esm-node/helmetCompat.mjs +358 -0
- package/dist/esm-node/html.mjs +5 -0
- package/dist/esm-node/index.mjs +5 -0
- package/dist/esm-node/navigationWarmup.mjs +22 -0
- package/dist/esm-node/node.mjs +62 -0
- package/dist/esm-node/rendererHead.mjs +163 -0
- package/dist/esm-node/rscHtmlStream.mjs +87 -0
- package/dist/esm-node/safeFailure.mjs +79 -0
- package/dist/esm-node/shellStream.mjs +39 -0
- package/dist/types/helmetCompat.d.ts +75 -0
- package/dist/types/html.d.ts +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/navigationWarmup.d.ts +8 -0
- package/dist/types/node.d.ts +11 -0
- package/dist/types/rendererHead.d.ts +17 -0
- package/dist/types/rscHtmlStream.d.ts +4 -0
- package/dist/types/safeFailure.d.ts +17 -0
- package/dist/types/shellStream.d.ts +1 -0
- package/package.json +90 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +7 -0
- package/src/helmetCompat.ts +628 -0
- package/src/html.ts +11 -0
- package/src/index.ts +4 -0
- package/src/navigationWarmup.ts +39 -0
- package/src/node.ts +93 -0
- package/src/rendererHead.ts +236 -0
- package/src/rscHtmlStream.ts +142 -0
- package/src/safeFailure.ts +137 -0
- package/src/shellStream.ts +53 -0
- package/tests/rendererHead.test.ts +299 -0
- package/tests/rscHtmlStream.test.ts +153 -0
- package/tests/safeFailure.test.ts +154 -0
- package/tests/shellStream.test.ts +85 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
+
var define = (defs, kind)=>{
|
|
6
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
[kind]: defs[key]
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
define(getters, "get");
|
|
12
|
+
define(values, "value");
|
|
13
|
+
};
|
|
14
|
+
})();
|
|
15
|
+
(()=>{
|
|
16
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
+
})();
|
|
18
|
+
(()=>{
|
|
19
|
+
__webpack_require__.r = (exports1)=>{
|
|
20
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
+
value: 'Module'
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
+
value: true
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
})();
|
|
28
|
+
var __webpack_exports__ = {};
|
|
29
|
+
__webpack_require__.r(__webpack_exports__);
|
|
30
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
createConservingWebShellStream: ()=>createConservingWebShellStream
|
|
32
|
+
});
|
|
33
|
+
const external_rendererHead_js_namespaceObject = require("./rendererHead.js");
|
|
34
|
+
const MISSING_SHELL_MARKER_ERROR = 'React SSR stream ended before the shell marker';
|
|
35
|
+
const createConservingWebShellStream = (source, context, marker, onMissingMarker)=>{
|
|
36
|
+
const decoder = new TextDecoder();
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const headProcessor = (0, external_rendererHead_js_namespaceObject.createHeadChunkProcessor)(context);
|
|
39
|
+
let markerCarry = '';
|
|
40
|
+
let markerFound = false;
|
|
41
|
+
const observeMarker = (chunk)=>{
|
|
42
|
+
if (markerFound) return;
|
|
43
|
+
const candidate = markerCarry + chunk;
|
|
44
|
+
markerFound = candidate.includes(marker);
|
|
45
|
+
markerCarry = markerFound ? '' : candidate.slice(-(marker.length - 1));
|
|
46
|
+
};
|
|
47
|
+
return source.pipeThrough(new TransformStream({
|
|
48
|
+
transform (chunk, controller) {
|
|
49
|
+
const decoded = decoder.decode(chunk, {
|
|
50
|
+
stream: true
|
|
51
|
+
});
|
|
52
|
+
observeMarker(decoded);
|
|
53
|
+
const output = headProcessor.push(decoded);
|
|
54
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
55
|
+
},
|
|
56
|
+
flush (controller) {
|
|
57
|
+
const decoded = decoder.decode();
|
|
58
|
+
observeMarker(decoded);
|
|
59
|
+
if (!markerFound) {
|
|
60
|
+
(0, external_rendererHead_js_namespaceObject.abortHeadRender)(context);
|
|
61
|
+
const error = new Error(MISSING_SHELL_MARKER_ERROR);
|
|
62
|
+
onMissingMarker?.(error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const output = headProcessor.finish(decoded);
|
|
66
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
exports.createConservingWebShellStream = __webpack_exports__.createConservingWebShellStream;
|
|
71
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
72
|
+
"createConservingWebShellStream"
|
|
73
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
74
|
+
Object.defineProperty(exports, '__esModule', {
|
|
75
|
+
value: true
|
|
76
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
const CONTENT_PROPERTIES = new Set([
|
|
2
|
+
'innerHTML',
|
|
3
|
+
'cssText'
|
|
4
|
+
]);
|
|
5
|
+
const HTML_ATTRIBUTE_NAMES = {
|
|
6
|
+
charSet: 'charset',
|
|
7
|
+
className: 'class',
|
|
8
|
+
contentEditable: 'contenteditable',
|
|
9
|
+
httpEquiv: 'http-equiv',
|
|
10
|
+
hrefLang: 'hreflang',
|
|
11
|
+
itemProp: 'itemprop',
|
|
12
|
+
tabIndex: 'tabindex'
|
|
13
|
+
};
|
|
14
|
+
const REACT_ATTRIBUTE_NAMES = {
|
|
15
|
+
accesskey: 'accessKey',
|
|
16
|
+
charset: 'charSet',
|
|
17
|
+
class: 'className',
|
|
18
|
+
contenteditable: 'contentEditable',
|
|
19
|
+
contextmenu: 'contextMenu',
|
|
20
|
+
'http-equiv': 'httpEquiv',
|
|
21
|
+
hreflang: 'hrefLang',
|
|
22
|
+
itemprop: 'itemProp',
|
|
23
|
+
tabindex: 'tabIndex'
|
|
24
|
+
};
|
|
25
|
+
const SEO_PRIORITY_VALUES = {
|
|
26
|
+
link: {
|
|
27
|
+
rel: [
|
|
28
|
+
'amphtml',
|
|
29
|
+
'canonical',
|
|
30
|
+
'alternate'
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
meta: {
|
|
34
|
+
charset: [
|
|
35
|
+
''
|
|
36
|
+
],
|
|
37
|
+
name: [
|
|
38
|
+
'generator',
|
|
39
|
+
'robots',
|
|
40
|
+
"description"
|
|
41
|
+
],
|
|
42
|
+
property: [
|
|
43
|
+
'og:type',
|
|
44
|
+
'og:title',
|
|
45
|
+
'og:url',
|
|
46
|
+
'og:image',
|
|
47
|
+
'og:image:alt',
|
|
48
|
+
"og:description",
|
|
49
|
+
'twitter:url',
|
|
50
|
+
'twitter:title',
|
|
51
|
+
"twitter:description",
|
|
52
|
+
'twitter:image',
|
|
53
|
+
'twitter:image:alt',
|
|
54
|
+
'twitter:card',
|
|
55
|
+
'twitter:site'
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
script: {
|
|
59
|
+
type: [
|
|
60
|
+
'application/ld+json'
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const TAG_PRIMARY_ATTRIBUTES = {
|
|
65
|
+
link: [
|
|
66
|
+
'rel',
|
|
67
|
+
'href'
|
|
68
|
+
],
|
|
69
|
+
meta: [
|
|
70
|
+
'name',
|
|
71
|
+
'charset',
|
|
72
|
+
'http-equiv',
|
|
73
|
+
'property',
|
|
74
|
+
'itemprop'
|
|
75
|
+
],
|
|
76
|
+
noscript: [
|
|
77
|
+
'innerHTML'
|
|
78
|
+
],
|
|
79
|
+
script: [
|
|
80
|
+
'src',
|
|
81
|
+
'innerHTML'
|
|
82
|
+
],
|
|
83
|
+
style: [
|
|
84
|
+
'cssText'
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
const priorityRecords = new WeakSet();
|
|
88
|
+
const priorityTags = new WeakSet();
|
|
89
|
+
const immediateRecords = new WeakMap();
|
|
90
|
+
const createElementFactory = (react)=>react.createElement;
|
|
91
|
+
const renderHeadMarker = (react, marker)=>marker ? createElementFactory(react)('template', marker) : null;
|
|
92
|
+
const toReactAttributes = (tag)=>{
|
|
93
|
+
const props = {};
|
|
94
|
+
for (const [attribute, value] of Object.entries(tag))if (!CONTENT_PROPERTIES.has(attribute)) props[REACT_ATTRIBUTE_NAMES[attribute] ?? attribute] = value;
|
|
95
|
+
return props;
|
|
96
|
+
};
|
|
97
|
+
const toReactProps = (tag, key)=>{
|
|
98
|
+
const props = {
|
|
99
|
+
...toReactAttributes(tag),
|
|
100
|
+
key,
|
|
101
|
+
'data-rh': true
|
|
102
|
+
};
|
|
103
|
+
if ('innerHTML' in tag || 'cssText' in tag) props.dangerouslySetInnerHTML = {
|
|
104
|
+
__html: tag.innerHTML ?? tag.cssText ?? ''
|
|
105
|
+
};
|
|
106
|
+
return props;
|
|
107
|
+
};
|
|
108
|
+
const escapeHtml = (value)=>String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
109
|
+
const toHtmlAttributeName = (name)=>HTML_ATTRIBUTE_NAMES[name] ?? name;
|
|
110
|
+
const attributesToString = (tag, includeHelmetAttribute = false)=>{
|
|
111
|
+
const attributes = includeHelmetAttribute ? [
|
|
112
|
+
'data-rh="true"'
|
|
113
|
+
] : [];
|
|
114
|
+
for (const [name, value] of Object.entries(tag)){
|
|
115
|
+
if (CONTENT_PROPERTIES.has(name) || false === value || null == value) continue;
|
|
116
|
+
const attribute = toHtmlAttributeName(name);
|
|
117
|
+
attributes.push(true === value ? attribute : `${attribute}="${escapeHtml(value)}"`);
|
|
118
|
+
}
|
|
119
|
+
return attributes.join(' ');
|
|
120
|
+
};
|
|
121
|
+
const tagsToString = (tagName, tags)=>tags.map((tag)=>{
|
|
122
|
+
const attributes = attributesToString(tag, true);
|
|
123
|
+
if (("script" === tagName || "noscript" === tagName) && 'string' == typeof tag.innerHTML) return `<${tagName} ${attributes}>${tag.innerHTML}</${tagName}>`;
|
|
124
|
+
if ('style' === tagName && 'string' == typeof tag.cssText) return `<style ${attributes}>${tag.cssText}</style>`;
|
|
125
|
+
return `<${tagName} ${attributes}>`;
|
|
126
|
+
}).join('');
|
|
127
|
+
const markPriorityHeadRecord = (record, prioritize)=>{
|
|
128
|
+
if (prioritize) priorityRecords.add(record);
|
|
129
|
+
};
|
|
130
|
+
const isPriorityHeadTag = (tag)=>priorityTags.has(tag);
|
|
131
|
+
const headTagsToComponents = (react, tagName, tags)=>{
|
|
132
|
+
const createElement = createElementFactory(react);
|
|
133
|
+
return tags.filter((tag)=>!priorityTags.has(tag)).map((tag, index)=>createElement(tagName, toReactProps(tag, index)));
|
|
134
|
+
};
|
|
135
|
+
const headTitleToComponent = (react, title, attributes)=>{
|
|
136
|
+
if (!title) return [];
|
|
137
|
+
const createElement = createElementFactory(react);
|
|
138
|
+
return [
|
|
139
|
+
createElement('title', toReactProps(attributes, title), title)
|
|
140
|
+
];
|
|
141
|
+
};
|
|
142
|
+
const normalizedEntries = (tag)=>Object.entries(tag).map(([name, value])=>[
|
|
143
|
+
toHtmlAttributeName(name).toLowerCase(),
|
|
144
|
+
String(value).toLowerCase()
|
|
145
|
+
]);
|
|
146
|
+
const isSeoPriorityTag = (tagName, tag)=>{
|
|
147
|
+
const values = SEO_PRIORITY_VALUES[tagName];
|
|
148
|
+
return normalizedEntries(tag).some(([name, value])=>{
|
|
149
|
+
const allowed = values[name];
|
|
150
|
+
return void 0 !== allowed && ('charset' === name || allowed.includes(value));
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const createPriorityHeadDatum = (react, records, dedupe)=>{
|
|
154
|
+
if (!records.some((record)=>'object' == typeof record && null !== record && priorityRecords.has(record))) return {
|
|
155
|
+
toComponent: ()=>[],
|
|
156
|
+
toString: ()=>''
|
|
157
|
+
};
|
|
158
|
+
const groups = [
|
|
159
|
+
'meta',
|
|
160
|
+
'link',
|
|
161
|
+
"script"
|
|
162
|
+
].map((tagName)=>({
|
|
163
|
+
tagName,
|
|
164
|
+
tags: dedupe(tagName, records).filter((tag)=>isSeoPriorityTag(tagName, tag))
|
|
165
|
+
}));
|
|
166
|
+
for (const { tags } of groups)for (const tag of tags)priorityTags.add(tag);
|
|
167
|
+
const createElement = createElementFactory(react);
|
|
168
|
+
return {
|
|
169
|
+
toComponent: ()=>groups.flatMap(({ tagName, tags })=>tags.map((tag, index)=>createElement(tagName, toReactProps(tag, `${tagName}-${index}`)))),
|
|
170
|
+
toString: ()=>groups.map(({ tagName, tags })=>tagsToString(tagName, tags)).join('')
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const createDatum = (react, tagName, tags)=>({
|
|
174
|
+
toComponent: ()=>headTagsToComponents(react, tagName, tags),
|
|
175
|
+
toString: ()=>tagsToString(tagName, tags.filter((tag)=>!priorityTags.has(tag)))
|
|
176
|
+
});
|
|
177
|
+
const createAttributeDatum = (attributes)=>({
|
|
178
|
+
toComponent: ()=>toReactAttributes(attributes),
|
|
179
|
+
toString: ()=>attributesToString(attributes)
|
|
180
|
+
});
|
|
181
|
+
const createTitleDatum = (react, title, attributes)=>({
|
|
182
|
+
toComponent: ()=>headTitleToComponent(react, title, attributes),
|
|
183
|
+
toString: ()=>{
|
|
184
|
+
if (!title) return '';
|
|
185
|
+
const serialized = attributesToString(attributes, true);
|
|
186
|
+
return `<title ${serialized}>${escapeHtml(title)}</title>`;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const normalizeHelmetTitle = (title)=>{
|
|
190
|
+
if ('string' == typeof title) return title;
|
|
191
|
+
if (Array.isArray(title)) return title.map((part)=>String(part)).join('');
|
|
192
|
+
};
|
|
193
|
+
const mergeAttributes = (current, next)=>({
|
|
194
|
+
...current,
|
|
195
|
+
...next ?? {}
|
|
196
|
+
});
|
|
197
|
+
const isHeadElement = (value)=>'object' == typeof value && null !== value && 'type' in value && 'props' in value && 'object' == typeof value.props && null !== value.props;
|
|
198
|
+
const collectChildren = (react, children, draft)=>{
|
|
199
|
+
react.Children.forEach(children, (child)=>{
|
|
200
|
+
if (!react.isValidElement(child) || !isHeadElement(child)) return;
|
|
201
|
+
const element = child;
|
|
202
|
+
if (element.type === react.Fragment) return void collectChildren(react, element.props.children, draft);
|
|
203
|
+
if ('string' != typeof element.type) return;
|
|
204
|
+
const { children: nestedChildren, ...props } = element.props;
|
|
205
|
+
if ('title' === element.type) {
|
|
206
|
+
draft.title = react.Children.toArray(nestedChildren).join('');
|
|
207
|
+
draft.titleAttributes = mergeAttributes(draft.titleAttributes, props);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if ('html' === element.type) {
|
|
211
|
+
draft.htmlAttributes = mergeAttributes(draft.htmlAttributes ?? {}, props);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if ('body' === element.type) {
|
|
215
|
+
draft.bodyAttributes = mergeAttributes(draft.bodyAttributes ?? {}, props);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if ('base' === element.type || 'link' === element.type || 'meta' === element.type || "noscript" === element.type || "script" === element.type || 'style' === element.type) {
|
|
219
|
+
const tag = {
|
|
220
|
+
...props
|
|
221
|
+
};
|
|
222
|
+
if (("script" === element.type || 'style' === element.type || "noscript" === element.type) && 'string' == typeof nestedChildren) tag['style' === element.type ? 'cssText' : 'innerHTML'] = nestedChildren;
|
|
223
|
+
draft[element.type].push(tag);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
const createHelmetRecord = (reactValue, props)=>{
|
|
228
|
+
const draft = {
|
|
229
|
+
base: void 0 === props.base ? [] : [
|
|
230
|
+
{
|
|
231
|
+
...props.base
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
bodyAttributes: void 0 === props.bodyAttributes ? void 0 : {
|
|
235
|
+
...props.bodyAttributes
|
|
236
|
+
},
|
|
237
|
+
htmlAttributes: void 0 === props.htmlAttributes ? void 0 : {
|
|
238
|
+
...props.htmlAttributes
|
|
239
|
+
},
|
|
240
|
+
link: (props.link ?? []).map((tag)=>({
|
|
241
|
+
...tag
|
|
242
|
+
})),
|
|
243
|
+
meta: (props.meta ?? []).map((tag)=>({
|
|
244
|
+
...tag
|
|
245
|
+
})),
|
|
246
|
+
noscript: (props.noscript ?? []).map((tag)=>({
|
|
247
|
+
...tag
|
|
248
|
+
})),
|
|
249
|
+
script: (props.script ?? []).map((tag)=>({
|
|
250
|
+
...tag
|
|
251
|
+
})),
|
|
252
|
+
style: (props.style ?? []).map((tag)=>({
|
|
253
|
+
...tag
|
|
254
|
+
})),
|
|
255
|
+
title: normalizeHelmetTitle(props.title),
|
|
256
|
+
titleAttributes: {
|
|
257
|
+
...props.titleAttributes ?? {}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
collectChildren(reactValue, props.children, draft);
|
|
261
|
+
const record = {
|
|
262
|
+
...draft,
|
|
263
|
+
defaultTitle: props.defaultTitle,
|
|
264
|
+
prioritizeSeoTags: true === props.prioritizeSeoTags,
|
|
265
|
+
titleTemplate: props.titleTemplate
|
|
266
|
+
};
|
|
267
|
+
markPriorityHeadRecord(record, record.prioritizeSeoTags);
|
|
268
|
+
return record;
|
|
269
|
+
};
|
|
270
|
+
const getInnermostDefined = (records, pick)=>{
|
|
271
|
+
for(let index = records.length - 1; index >= 0; index -= 1){
|
|
272
|
+
const value = pick(records[index]);
|
|
273
|
+
if (void 0 !== value) return value;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const deriveTitle = (records)=>{
|
|
277
|
+
const title = getInnermostDefined(records, (record)=>record.title);
|
|
278
|
+
const template = getInnermostDefined(records, (record)=>record.titleTemplate);
|
|
279
|
+
if (template && title) return template.replaceAll('%s', title);
|
|
280
|
+
return title || getInnermostDefined(records, (record)=>record.defaultTitle);
|
|
281
|
+
};
|
|
282
|
+
const mergeRecordAttributes = (records, pick)=>records.reduce((merged, record)=>mergeAttributes(merged, pick(record)), {});
|
|
283
|
+
const deriveBaseTags = (records)=>{
|
|
284
|
+
for(let recordIndex = records.length - 1; recordIndex >= 0; recordIndex--){
|
|
285
|
+
const tags = records[recordIndex].base;
|
|
286
|
+
for(let tagIndex = tags.length - 1; tagIndex >= 0; tagIndex--)if (tags[tagIndex].href) return [
|
|
287
|
+
tags[tagIndex]
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
291
|
+
};
|
|
292
|
+
const getPrimaryAttribute = (tagName, tag)=>{
|
|
293
|
+
const candidates = TAG_PRIMARY_ATTRIBUTES[tagName];
|
|
294
|
+
let selectedKey;
|
|
295
|
+
let selectedValue;
|
|
296
|
+
for (const [key, value] of Object.entries(tag)){
|
|
297
|
+
const normalizedKey = CONTENT_PROPERTIES.has(key) ? key : toHtmlAttributeName(key).toLowerCase();
|
|
298
|
+
if (candidates.includes(normalizedKey)) {
|
|
299
|
+
if ('rel' !== selectedKey || 'canonical' !== String(selectedValue).toLowerCase()) {
|
|
300
|
+
if ('rel' !== normalizedKey || 'stylesheet' !== String(value).toLowerCase()) {
|
|
301
|
+
selectedKey = normalizedKey;
|
|
302
|
+
selectedValue = value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (void 0 === selectedKey || !selectedValue) return;
|
|
308
|
+
return {
|
|
309
|
+
key: selectedKey,
|
|
310
|
+
value: String(selectedValue).toLowerCase()
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
const dedupeTags = (tagName, records)=>{
|
|
314
|
+
const approvedValues = new Map();
|
|
315
|
+
const approved = [];
|
|
316
|
+
for(let index = records.length - 1; index >= 0; index -= 1){
|
|
317
|
+
const instanceValues = new Map();
|
|
318
|
+
const kept = [];
|
|
319
|
+
for (const tag of records[index][tagName]){
|
|
320
|
+
const primary = getPrimaryAttribute(tagName, tag);
|
|
321
|
+
if (void 0 === primary || approvedValues.get(primary.key)?.has(primary.value)) continue;
|
|
322
|
+
const seen = instanceValues.get(primary.key) ?? new Set();
|
|
323
|
+
seen.add(primary.value);
|
|
324
|
+
instanceValues.set(primary.key, seen);
|
|
325
|
+
kept.push(tag);
|
|
326
|
+
}
|
|
327
|
+
approved.unshift(...kept);
|
|
328
|
+
for (const [key, values] of instanceValues){
|
|
329
|
+
const target = approvedValues.get(key);
|
|
330
|
+
if (void 0 === target) approvedValues.set(key, values);
|
|
331
|
+
else for (const value of values)target.add(value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return approved;
|
|
335
|
+
};
|
|
336
|
+
const deriveHelmetServerState = (react, records)=>{
|
|
337
|
+
const priority = createPriorityHeadDatum(react, records, dedupeTags);
|
|
338
|
+
return {
|
|
339
|
+
base: createDatum(react, 'base', deriveBaseTags(records)),
|
|
340
|
+
bodyAttributes: createAttributeDatum(mergeRecordAttributes(records, (record)=>record.bodyAttributes)),
|
|
341
|
+
htmlAttributes: createAttributeDatum(mergeRecordAttributes(records, (record)=>record.htmlAttributes)),
|
|
342
|
+
link: createDatum(react, 'link', dedupeTags('link', records)),
|
|
343
|
+
meta: createDatum(react, 'meta', dedupeTags('meta', records)),
|
|
344
|
+
noscript: createDatum(react, "noscript", dedupeTags("noscript", records)),
|
|
345
|
+
priority,
|
|
346
|
+
script: createDatum(react, "script", dedupeTags("script", records)),
|
|
347
|
+
style: createDatum(react, 'style', dedupeTags('style', records)),
|
|
348
|
+
title: createTitleDatum(react, deriveTitle(records), mergeRecordAttributes(records, (record)=>record.titleAttributes))
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
const collectImmediateHelmetState = (react, context, record)=>{
|
|
352
|
+
const records = immediateRecords.get(context) ?? [];
|
|
353
|
+
if (!immediateRecords.has(context)) immediateRecords.set(context, records);
|
|
354
|
+
records.push(record);
|
|
355
|
+
return deriveHelmetServerState(react, records);
|
|
356
|
+
};
|
|
357
|
+
export { collectImmediateHelmetState, createHelmetRecord, createPriorityHeadDatum, deriveHelmetServerState, headTagsToComponents, headTitleToComponent, isPriorityHeadTag, markPriorityHeadRecord, renderHeadMarker };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const SAFE_HTML_ATTRIBUTE_NAME = /^[^\u0000-\u0020"'<>/=]+$/u;
|
|
2
|
+
const isSafeHtmlAttributeName = (name)=>SAFE_HTML_ATTRIBUTE_NAME.test(name);
|
|
3
|
+
const escapeHtmlAttribute = (value)=>String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
4
|
+
export { escapeHtmlAttribute, isSafeHtmlAttributeName };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const realmNamespaces = new WeakMap();
|
|
2
|
+
let nextRealmNamespace = 0;
|
|
3
|
+
function getNavigationWarmupCacheKey(runtimeContext, chunkLoader, publicPath, warmupKey) {
|
|
4
|
+
let loaderNamespaces = realmNamespaces.get(runtimeContext);
|
|
5
|
+
if (void 0 === loaderNamespaces) {
|
|
6
|
+
loaderNamespaces = new WeakMap();
|
|
7
|
+
realmNamespaces.set(runtimeContext, loaderNamespaces);
|
|
8
|
+
}
|
|
9
|
+
let publicPathNamespaces = loaderNamespaces.get(chunkLoader);
|
|
10
|
+
if (void 0 === publicPathNamespaces) {
|
|
11
|
+
publicPathNamespaces = new Map();
|
|
12
|
+
loaderNamespaces.set(chunkLoader, publicPathNamespaces);
|
|
13
|
+
}
|
|
14
|
+
let namespace = publicPathNamespaces.get(publicPath);
|
|
15
|
+
if (void 0 === namespace) {
|
|
16
|
+
namespace = `realm-${++nextRealmNamespace}`;
|
|
17
|
+
publicPathNamespaces.set(publicPath, namespace);
|
|
18
|
+
}
|
|
19
|
+
return `${namespace}:${warmupKey}`;
|
|
20
|
+
}
|
|
21
|
+
export { getNavigationWarmupCacheKey };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Transform, pipeline } from "node:stream";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { abortHeadRender, createHeadChunkProcessor } from "./rendererHead.mjs";
|
|
4
|
+
export * from "./rendererHead.mjs";
|
|
5
|
+
const toError = (reason)=>reason instanceof Error ? reason : new Error(String(reason));
|
|
6
|
+
const createNodeHeadMarkerStripper = (context, terminalMarker)=>{
|
|
7
|
+
const decoder = new StringDecoder('utf8');
|
|
8
|
+
const processor = createHeadChunkProcessor(context);
|
|
9
|
+
let terminalCarry = '';
|
|
10
|
+
let sawTerminalMarker = void 0 === terminalMarker;
|
|
11
|
+
const observeTerminalMarker = (output)=>{
|
|
12
|
+
if (!sawTerminalMarker && void 0 !== terminalMarker) {
|
|
13
|
+
const observed = terminalCarry + output;
|
|
14
|
+
sawTerminalMarker = observed.includes(terminalMarker);
|
|
15
|
+
terminalCarry = observed.slice(Math.max(0, observed.length - terminalMarker.length + 1));
|
|
16
|
+
}
|
|
17
|
+
return output;
|
|
18
|
+
};
|
|
19
|
+
return new Transform({
|
|
20
|
+
transform (chunk, _encoding, callback) {
|
|
21
|
+
try {
|
|
22
|
+
callback(null, observeTerminalMarker(processor.push(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
callback(toError(error));
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
flush (callback) {
|
|
28
|
+
try {
|
|
29
|
+
const output = observeTerminalMarker(processor.finish(decoder.end()));
|
|
30
|
+
callback(null, sawTerminalMarker ? output : `${output}${terminalMarker}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
callback(toError(error));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
destroy (error, callback) {
|
|
36
|
+
try {
|
|
37
|
+
abortHeadRender(context);
|
|
38
|
+
callback(error);
|
|
39
|
+
} catch (abortError) {
|
|
40
|
+
callback(error ?? toError(abortError));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const createOnceErrorReporter = (onError)=>{
|
|
46
|
+
let reported = false;
|
|
47
|
+
return (error)=>{
|
|
48
|
+
if (!reported) {
|
|
49
|
+
reported = true;
|
|
50
|
+
onError?.(error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const pipeNodeHeadStream = (options)=>{
|
|
55
|
+
const { source, destination, context, terminalMarker, onError } = options;
|
|
56
|
+
const stripper = createNodeHeadMarkerStripper(context, terminalMarker);
|
|
57
|
+
pipeline(source, stripper, destination, (error)=>{
|
|
58
|
+
if (null !== error) onError?.(error);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
export { createNodeHeadMarkerStripper, createOnceErrorReporter, pipeNodeHeadStream };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export * from "./helmetCompat.mjs";
|
|
2
|
+
const MARKER_ATTRIBUTE = 'data-modern-helmet';
|
|
3
|
+
const MARKER_PREFIX = `<template ${MARKER_ATTRIBUTE}="`;
|
|
4
|
+
const MARKER_SUFFIX = '"></template>';
|
|
5
|
+
const RECORD_ID_WIDTH = 12;
|
|
6
|
+
const MARKER_LENGTH = MARKER_PREFIX.length + 1 + 36 + RECORD_ID_WIDTH + MARKER_SUFFIX.length;
|
|
7
|
+
const HEAD_STATE = Symbol('modern.rendererHead');
|
|
8
|
+
const getState = (context)=>{
|
|
9
|
+
const headContext = context;
|
|
10
|
+
let state = headContext[HEAD_STATE];
|
|
11
|
+
if (void 0 === state) {
|
|
12
|
+
state = {
|
|
13
|
+
publishedRecords: []
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(headContext, HEAD_STATE, {
|
|
16
|
+
value: state
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return state;
|
|
20
|
+
};
|
|
21
|
+
const formatId = (value)=>value.toString(36).padStart(RECORD_ID_WIDTH, '0');
|
|
22
|
+
const createToken = (transaction)=>{
|
|
23
|
+
const token = `h${transaction.nonce}${formatId(transaction.nextRecordId)}`;
|
|
24
|
+
transaction.nextRecordId += 1;
|
|
25
|
+
return token;
|
|
26
|
+
};
|
|
27
|
+
const beginHeadRender = (context)=>{
|
|
28
|
+
const state = getState(context);
|
|
29
|
+
state.transaction = {
|
|
30
|
+
recordsByToken: new Map(),
|
|
31
|
+
committedRecords: [],
|
|
32
|
+
committedTokens: new Set(),
|
|
33
|
+
nextRecordId: 0,
|
|
34
|
+
nonce: globalThis.crypto.randomUUID(),
|
|
35
|
+
previousRecords: state.publishedRecords,
|
|
36
|
+
sealed: false
|
|
37
|
+
};
|
|
38
|
+
state.publish?.([]);
|
|
39
|
+
};
|
|
40
|
+
const collectHeadRecord = (context, createRecord, publish)=>{
|
|
41
|
+
const state = getState(context);
|
|
42
|
+
const transaction = state.transaction;
|
|
43
|
+
if (void 0 === transaction) return;
|
|
44
|
+
const hadPublisher = void 0 !== state.publish;
|
|
45
|
+
state.publish = (records)=>publish(records);
|
|
46
|
+
if (!hadPublisher) state.publish([]);
|
|
47
|
+
if (transaction.sealed) return null;
|
|
48
|
+
const token = createToken(transaction);
|
|
49
|
+
transaction.recordsByToken.set(token, createRecord());
|
|
50
|
+
return {
|
|
51
|
+
[MARKER_ATTRIBUTE]: token
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const collectHeadState = (context, createRecord, derive, target)=>collectHeadRecord(context, createRecord, (records)=>{
|
|
55
|
+
target.helmet = derive(records);
|
|
56
|
+
});
|
|
57
|
+
const consumeMarkers = (context, html)=>{
|
|
58
|
+
const transaction = getState(context).transaction;
|
|
59
|
+
if (void 0 === transaction) return html;
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
let stripped = '';
|
|
62
|
+
while(cursor < html.length){
|
|
63
|
+
const start = html.indexOf(MARKER_PREFIX, cursor);
|
|
64
|
+
if (-1 === start) {
|
|
65
|
+
stripped += html.slice(cursor);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const tokenStart = start + MARKER_PREFIX.length;
|
|
69
|
+
const end = html.indexOf(MARKER_SUFFIX, tokenStart);
|
|
70
|
+
if (-1 === end) {
|
|
71
|
+
stripped += html.slice(cursor);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const token = html.slice(tokenStart, end);
|
|
75
|
+
if (transaction.recordsByToken.has(token)) {
|
|
76
|
+
stripped += html.slice(cursor, start);
|
|
77
|
+
if (!transaction.sealed && !transaction.committedTokens.has(token)) {
|
|
78
|
+
transaction.committedTokens.add(token);
|
|
79
|
+
transaction.committedRecords.push(transaction.recordsByToken.get(token));
|
|
80
|
+
}
|
|
81
|
+
} else stripped += html.slice(cursor, end + MARKER_SUFFIX.length);
|
|
82
|
+
cursor = end + MARKER_SUFFIX.length;
|
|
83
|
+
}
|
|
84
|
+
return stripped;
|
|
85
|
+
};
|
|
86
|
+
const rendererHead_publish = (context)=>{
|
|
87
|
+
const state = getState(context);
|
|
88
|
+
const records = state.transaction?.committedRecords ?? [];
|
|
89
|
+
state.publishedRecords = records;
|
|
90
|
+
state.publish?.(records);
|
|
91
|
+
};
|
|
92
|
+
const publishHeadRender = (context)=>{
|
|
93
|
+
const transaction = getState(context).transaction;
|
|
94
|
+
if (void 0 !== transaction) {
|
|
95
|
+
transaction.sealed = true;
|
|
96
|
+
rendererHead_publish(context);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const finishHeadRender = (context)=>{
|
|
100
|
+
const state = getState(context);
|
|
101
|
+
if (void 0 !== state.transaction) {
|
|
102
|
+
if (!state.transaction.sealed) rendererHead_publish(context);
|
|
103
|
+
state.transaction = void 0;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const completeHeadRender = (context, html)=>{
|
|
107
|
+
const stripped = consumeMarkers(context, html);
|
|
108
|
+
finishHeadRender(context);
|
|
109
|
+
return stripped;
|
|
110
|
+
};
|
|
111
|
+
const abortHeadRender = (context)=>{
|
|
112
|
+
const state = getState(context);
|
|
113
|
+
const transaction = state.transaction;
|
|
114
|
+
if (void 0 !== transaction) {
|
|
115
|
+
state.publishedRecords = transaction.previousRecords;
|
|
116
|
+
state.publish?.(transaction.previousRecords);
|
|
117
|
+
state.transaction = void 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const findCarryStart = (input)=>{
|
|
121
|
+
const windowStart = Math.max(0, input.length - MARKER_LENGTH + 1);
|
|
122
|
+
const markerStart = input.lastIndexOf(MARKER_PREFIX);
|
|
123
|
+
if (markerStart >= windowStart && input.length - markerStart < MARKER_LENGTH) return markerStart;
|
|
124
|
+
const maxPrefixLength = Math.min(MARKER_PREFIX.length - 1, input.length);
|
|
125
|
+
for(let length = maxPrefixLength; length > 0; length -= 1)if (MARKER_PREFIX.startsWith(input.slice(-length))) return input.length - length;
|
|
126
|
+
return input.length;
|
|
127
|
+
};
|
|
128
|
+
const createHeadChunkProcessor = (context)=>{
|
|
129
|
+
let carry = '';
|
|
130
|
+
return {
|
|
131
|
+
push (chunk) {
|
|
132
|
+
const input = carry + chunk;
|
|
133
|
+
const carryStart = findCarryStart(input);
|
|
134
|
+
const output = consumeMarkers(context, input.slice(0, carryStart));
|
|
135
|
+
carry = input.slice(carryStart);
|
|
136
|
+
return output;
|
|
137
|
+
},
|
|
138
|
+
finish (chunk = '') {
|
|
139
|
+
const output = completeHeadRender(context, carry + chunk);
|
|
140
|
+
carry = '';
|
|
141
|
+
return output;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const createWebHeadMarkerStripper = (context)=>{
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
const encoder = new TextEncoder();
|
|
148
|
+
const processor = createHeadChunkProcessor(context);
|
|
149
|
+
return new TransformStream({
|
|
150
|
+
transform (chunk, controller) {
|
|
151
|
+
const output = processor.push(decoder.decode(chunk, {
|
|
152
|
+
stream: true
|
|
153
|
+
}));
|
|
154
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
155
|
+
},
|
|
156
|
+
flush (controller) {
|
|
157
|
+
const output = processor.finish(decoder.decode());
|
|
158
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
export { abortHeadRender, beginHeadRender, collectHeadRecord, collectHeadState, completeHeadRender, createHeadChunkProcessor, createWebHeadMarkerStripper, finishHeadRender, publishHeadRender };
|