@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,86 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const closingTagsPattern = /<\/body>\s*<\/html>\s*$/i;
|
|
3
|
+
const closingTagsPrefixPattern = /(?:<\/body>\s*(?:<\/html>\s*|<\/h(?:t(?:m(?:l>?)?)?)?|<\/?|<)?|<\/b(?:o(?:d(?:y>?)?)?)?|<\/?|<[^>]*)$/i;
|
|
4
|
+
const RSC_HTML_CLOSING_TAIL_LIMIT = 8192;
|
|
5
|
+
function injectRSCPayload(rscStream, { injectClosingTags = true } = {}) {
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let retainedTail = '';
|
|
8
|
+
let committedClosingBody = false;
|
|
9
|
+
let committedHtmlClose = false;
|
|
10
|
+
let committedProbe = '';
|
|
11
|
+
let flightPromise;
|
|
12
|
+
const startFlight = (controller)=>{
|
|
13
|
+
if (!flightPromise) {
|
|
14
|
+
flightPromise = writeRSCStream(rscStream, controller);
|
|
15
|
+
flightPromise.catch((error)=>controller.error(error));
|
|
16
|
+
}
|
|
17
|
+
return flightPromise;
|
|
18
|
+
};
|
|
19
|
+
const enqueueHtml = (value, controller)=>{
|
|
20
|
+
if (value) controller.enqueue(encoder.encode(value));
|
|
21
|
+
};
|
|
22
|
+
const processHtml = async (value, controller)=>{
|
|
23
|
+
if (!value) return;
|
|
24
|
+
if (committedClosingBody) {
|
|
25
|
+
const probe = committedProbe + value;
|
|
26
|
+
committedHtmlClose ||= /<\/html>/i.test(probe);
|
|
27
|
+
committedProbe = probe.slice(-6);
|
|
28
|
+
enqueueHtml(value, controller);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const html = retainedTail + value;
|
|
32
|
+
retainedTail = '';
|
|
33
|
+
const suffix = html.match(closingTagsPrefixPattern)?.[0] ?? '';
|
|
34
|
+
const prefix = suffix ? html.slice(0, -suffix.length) : html;
|
|
35
|
+
enqueueHtml(prefix, controller);
|
|
36
|
+
if (!suffix) return void startFlight(controller);
|
|
37
|
+
if (suffix.length <= RSC_HTML_CLOSING_TAIL_LIMIT) {
|
|
38
|
+
retainedTail = suffix;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await startFlight(controller);
|
|
42
|
+
if (/<\/body>/i.test(suffix)) {
|
|
43
|
+
committedClosingBody = true;
|
|
44
|
+
committedHtmlClose = /<\/html>/i.test(suffix);
|
|
45
|
+
committedProbe = suffix.slice(-6);
|
|
46
|
+
}
|
|
47
|
+
enqueueHtml(suffix, controller);
|
|
48
|
+
};
|
|
49
|
+
return new TransformStream({
|
|
50
|
+
async transform (chunk, controller) {
|
|
51
|
+
await processHtml(decoder.decode(chunk, {
|
|
52
|
+
stream: true
|
|
53
|
+
}), controller);
|
|
54
|
+
},
|
|
55
|
+
async flush (controller) {
|
|
56
|
+
await processHtml(decoder.decode(), controller);
|
|
57
|
+
await startFlight(controller);
|
|
58
|
+
const retainedClosingTags = closingTagsPattern.test(retainedTail);
|
|
59
|
+
enqueueHtml(retainedTail, controller);
|
|
60
|
+
if (!injectClosingTags) return;
|
|
61
|
+
if (committedClosingBody) {
|
|
62
|
+
if (!committedHtmlClose) enqueueHtml('</html>', controller);
|
|
63
|
+
} else if (!retainedClosingTags) enqueueHtml('</body></html>', controller);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function writeRSCStream(rscStream, controller) {
|
|
68
|
+
const decoder = new TextDecoder('utf-8', {
|
|
69
|
+
fatal: true
|
|
70
|
+
});
|
|
71
|
+
for await (const chunk of rscStream)try {
|
|
72
|
+
writeChunk(JSON.stringify(decoder.decode(chunk, {
|
|
73
|
+
stream: true
|
|
74
|
+
})), controller);
|
|
75
|
+
} catch {
|
|
76
|
+
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
77
|
+
writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller);
|
|
78
|
+
}
|
|
79
|
+
const remaining = decoder.decode();
|
|
80
|
+
if (remaining) writeChunk(JSON.stringify(remaining), controller);
|
|
81
|
+
}
|
|
82
|
+
function writeChunk(chunk, controller) {
|
|
83
|
+
const script = `(self.__FLIGHT_DATA||=[]).push(${chunk})`;
|
|
84
|
+
controller.enqueue(encoder.encode(`<script>${script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1')}</script>`));
|
|
85
|
+
}
|
|
86
|
+
export { RSC_HTML_CLOSING_TAIL_LIMIT, injectRSCPayload };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const SAFE_FAILURE_MESSAGES = {
|
|
2
|
+
500: 'Internal Server Error',
|
|
3
|
+
503: 'Service Unavailable'
|
|
4
|
+
};
|
|
5
|
+
const SAFE_FAILURE_CODES = {
|
|
6
|
+
500: 'INTERNAL_SERVER_ERROR',
|
|
7
|
+
503: 'SERVICE_UNAVAILABLE'
|
|
8
|
+
};
|
|
9
|
+
const readErrorProperty = (error, key)=>{
|
|
10
|
+
if ('object' != typeof error || null === error) return;
|
|
11
|
+
try {
|
|
12
|
+
return error[key];
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const normalizeFailureStatus = (value)=>'number' == typeof value && Number.isInteger(value) && value >= 400 && value <= 599 ? value : void 0;
|
|
18
|
+
const normalizeDelaySeconds = (value)=>{
|
|
19
|
+
if ('number' == typeof value) {
|
|
20
|
+
const seconds = Math.ceil(value);
|
|
21
|
+
return Number.isFinite(value) && value >= 0 && Number.isSafeInteger(seconds) ? String(seconds) : void 0;
|
|
22
|
+
}
|
|
23
|
+
if ('string' == typeof value) {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (!/^\d+$/u.test(trimmed)) return;
|
|
26
|
+
const seconds = Number(trimmed);
|
|
27
|
+
return Number.isSafeInteger(seconds) ? String(seconds) : void 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const normalizeHttpDate = (value)=>{
|
|
31
|
+
try {
|
|
32
|
+
if (value instanceof Date) {
|
|
33
|
+
const timestamp = Date.prototype.getTime.call(value);
|
|
34
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toUTCString() : void 0;
|
|
35
|
+
}
|
|
36
|
+
if ('string' == typeof value) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
const timestamp = Date.parse(trimmed);
|
|
39
|
+
if (Number.isFinite(timestamp)) {
|
|
40
|
+
const canonical = new Date(timestamp).toUTCString();
|
|
41
|
+
return canonical === trimmed ? canonical : void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
};
|
|
46
|
+
const normalizeRetryAfter = (value)=>normalizeDelaySeconds(value) ?? normalizeHttpDate(value);
|
|
47
|
+
const getSafeFailureStatus = (error)=>normalizeFailureStatus(readErrorProperty(error, 'status')) ?? normalizeFailureStatus(readErrorProperty(error, 'statusCode')) ?? 500;
|
|
48
|
+
const createSafeFailureHttpResult = (error)=>{
|
|
49
|
+
const status = getSafeFailureStatus(error);
|
|
50
|
+
const retryAfterMs = readErrorProperty(error, 'retryAfterMs');
|
|
51
|
+
const retryAfter = 503 === status ? normalizeRetryAfter(readErrorProperty(error, 'retryAfter')) ?? normalizeRetryAfter(readErrorProperty(error, 'retryAfterSeconds')) ?? normalizeRetryAfter('number' == typeof retryAfterMs ? retryAfterMs / 1000 : void 0) : void 0;
|
|
52
|
+
return {
|
|
53
|
+
status,
|
|
54
|
+
body: {
|
|
55
|
+
success: false,
|
|
56
|
+
error: {
|
|
57
|
+
code: SAFE_FAILURE_CODES[status] ?? 'REQUEST_FAILED',
|
|
58
|
+
message: SAFE_FAILURE_MESSAGES[status] ?? 'Request failed',
|
|
59
|
+
status
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/json; charset=utf-8',
|
|
64
|
+
...void 0 === retryAfter ? {} : {
|
|
65
|
+
'Retry-After': retryAfter
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const createSafeJsonFailureResponse = (error)=>{
|
|
71
|
+
const result = createSafeFailureHttpResult(error);
|
|
72
|
+
return new Response(JSON.stringify(result.body), {
|
|
73
|
+
status: result.status,
|
|
74
|
+
headers: result.headers
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const createSafeFailureResponse = createSafeJsonFailureResponse;
|
|
78
|
+
export { createSafeFailureHttpResult, createSafeFailureResponse, createSafeJsonFailureResponse, getSafeFailureStatus };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { abortHeadRender, createHeadChunkProcessor } from "./rendererHead.mjs";
|
|
2
|
+
const MISSING_SHELL_MARKER_ERROR = 'React SSR stream ended before the shell marker';
|
|
3
|
+
const createConservingWebShellStream = (source, context, marker, onMissingMarker)=>{
|
|
4
|
+
const decoder = new TextDecoder();
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const headProcessor = createHeadChunkProcessor(context);
|
|
7
|
+
let markerCarry = '';
|
|
8
|
+
let markerFound = false;
|
|
9
|
+
const observeMarker = (chunk)=>{
|
|
10
|
+
if (markerFound) return;
|
|
11
|
+
const candidate = markerCarry + chunk;
|
|
12
|
+
markerFound = candidate.includes(marker);
|
|
13
|
+
markerCarry = markerFound ? '' : candidate.slice(-(marker.length - 1));
|
|
14
|
+
};
|
|
15
|
+
return source.pipeThrough(new TransformStream({
|
|
16
|
+
transform (chunk, controller) {
|
|
17
|
+
const decoded = decoder.decode(chunk, {
|
|
18
|
+
stream: true
|
|
19
|
+
});
|
|
20
|
+
observeMarker(decoded);
|
|
21
|
+
const output = headProcessor.push(decoded);
|
|
22
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
23
|
+
},
|
|
24
|
+
flush (controller) {
|
|
25
|
+
const decoded = decoder.decode();
|
|
26
|
+
observeMarker(decoded);
|
|
27
|
+
if (!markerFound) {
|
|
28
|
+
abortHeadRender(context);
|
|
29
|
+
const error = new Error(MISSING_SHELL_MARKER_ERROR);
|
|
30
|
+
onMissingMarker?.(error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const output = headProcessor.finish(decoded);
|
|
34
|
+
if (output) controller.enqueue(encoder.encode(output));
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
export { createConservingWebShellStream };
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const CONTENT_PROPERTIES = new Set([
|
|
3
|
+
'innerHTML',
|
|
4
|
+
'cssText'
|
|
5
|
+
]);
|
|
6
|
+
const HTML_ATTRIBUTE_NAMES = {
|
|
7
|
+
charSet: 'charset',
|
|
8
|
+
className: 'class',
|
|
9
|
+
contentEditable: 'contenteditable',
|
|
10
|
+
httpEquiv: 'http-equiv',
|
|
11
|
+
hrefLang: 'hreflang',
|
|
12
|
+
itemProp: 'itemprop',
|
|
13
|
+
tabIndex: 'tabindex'
|
|
14
|
+
};
|
|
15
|
+
const REACT_ATTRIBUTE_NAMES = {
|
|
16
|
+
accesskey: 'accessKey',
|
|
17
|
+
charset: 'charSet',
|
|
18
|
+
class: 'className',
|
|
19
|
+
contenteditable: 'contentEditable',
|
|
20
|
+
contextmenu: 'contextMenu',
|
|
21
|
+
'http-equiv': 'httpEquiv',
|
|
22
|
+
hreflang: 'hrefLang',
|
|
23
|
+
itemprop: 'itemProp',
|
|
24
|
+
tabindex: 'tabIndex'
|
|
25
|
+
};
|
|
26
|
+
const SEO_PRIORITY_VALUES = {
|
|
27
|
+
link: {
|
|
28
|
+
rel: [
|
|
29
|
+
'amphtml',
|
|
30
|
+
'canonical',
|
|
31
|
+
'alternate'
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
meta: {
|
|
35
|
+
charset: [
|
|
36
|
+
''
|
|
37
|
+
],
|
|
38
|
+
name: [
|
|
39
|
+
'generator',
|
|
40
|
+
'robots',
|
|
41
|
+
"description"
|
|
42
|
+
],
|
|
43
|
+
property: [
|
|
44
|
+
'og:type',
|
|
45
|
+
'og:title',
|
|
46
|
+
'og:url',
|
|
47
|
+
'og:image',
|
|
48
|
+
'og:image:alt',
|
|
49
|
+
"og:description",
|
|
50
|
+
'twitter:url',
|
|
51
|
+
'twitter:title',
|
|
52
|
+
"twitter:description",
|
|
53
|
+
'twitter:image',
|
|
54
|
+
'twitter:image:alt',
|
|
55
|
+
'twitter:card',
|
|
56
|
+
'twitter:site'
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
script: {
|
|
60
|
+
type: [
|
|
61
|
+
'application/ld+json'
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const TAG_PRIMARY_ATTRIBUTES = {
|
|
66
|
+
link: [
|
|
67
|
+
'rel',
|
|
68
|
+
'href'
|
|
69
|
+
],
|
|
70
|
+
meta: [
|
|
71
|
+
'name',
|
|
72
|
+
'charset',
|
|
73
|
+
'http-equiv',
|
|
74
|
+
'property',
|
|
75
|
+
'itemprop'
|
|
76
|
+
],
|
|
77
|
+
noscript: [
|
|
78
|
+
'innerHTML'
|
|
79
|
+
],
|
|
80
|
+
script: [
|
|
81
|
+
'src',
|
|
82
|
+
'innerHTML'
|
|
83
|
+
],
|
|
84
|
+
style: [
|
|
85
|
+
'cssText'
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
const priorityRecords = new WeakSet();
|
|
89
|
+
const priorityTags = new WeakSet();
|
|
90
|
+
const immediateRecords = new WeakMap();
|
|
91
|
+
const createElementFactory = (react)=>react.createElement;
|
|
92
|
+
const renderHeadMarker = (react, marker)=>marker ? createElementFactory(react)('template', marker) : null;
|
|
93
|
+
const toReactAttributes = (tag)=>{
|
|
94
|
+
const props = {};
|
|
95
|
+
for (const [attribute, value] of Object.entries(tag))if (!CONTENT_PROPERTIES.has(attribute)) props[REACT_ATTRIBUTE_NAMES[attribute] ?? attribute] = value;
|
|
96
|
+
return props;
|
|
97
|
+
};
|
|
98
|
+
const toReactProps = (tag, key)=>{
|
|
99
|
+
const props = {
|
|
100
|
+
...toReactAttributes(tag),
|
|
101
|
+
key,
|
|
102
|
+
'data-rh': true
|
|
103
|
+
};
|
|
104
|
+
if ('innerHTML' in tag || 'cssText' in tag) props.dangerouslySetInnerHTML = {
|
|
105
|
+
__html: tag.innerHTML ?? tag.cssText ?? ''
|
|
106
|
+
};
|
|
107
|
+
return props;
|
|
108
|
+
};
|
|
109
|
+
const escapeHtml = (value)=>String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
110
|
+
const toHtmlAttributeName = (name)=>HTML_ATTRIBUTE_NAMES[name] ?? name;
|
|
111
|
+
const attributesToString = (tag, includeHelmetAttribute = false)=>{
|
|
112
|
+
const attributes = includeHelmetAttribute ? [
|
|
113
|
+
'data-rh="true"'
|
|
114
|
+
] : [];
|
|
115
|
+
for (const [name, value] of Object.entries(tag)){
|
|
116
|
+
if (CONTENT_PROPERTIES.has(name) || false === value || null == value) continue;
|
|
117
|
+
const attribute = toHtmlAttributeName(name);
|
|
118
|
+
attributes.push(true === value ? attribute : `${attribute}="${escapeHtml(value)}"`);
|
|
119
|
+
}
|
|
120
|
+
return attributes.join(' ');
|
|
121
|
+
};
|
|
122
|
+
const tagsToString = (tagName, tags)=>tags.map((tag)=>{
|
|
123
|
+
const attributes = attributesToString(tag, true);
|
|
124
|
+
if (("script" === tagName || "noscript" === tagName) && 'string' == typeof tag.innerHTML) return `<${tagName} ${attributes}>${tag.innerHTML}</${tagName}>`;
|
|
125
|
+
if ('style' === tagName && 'string' == typeof tag.cssText) return `<style ${attributes}>${tag.cssText}</style>`;
|
|
126
|
+
return `<${tagName} ${attributes}>`;
|
|
127
|
+
}).join('');
|
|
128
|
+
const markPriorityHeadRecord = (record, prioritize)=>{
|
|
129
|
+
if (prioritize) priorityRecords.add(record);
|
|
130
|
+
};
|
|
131
|
+
const isPriorityHeadTag = (tag)=>priorityTags.has(tag);
|
|
132
|
+
const headTagsToComponents = (react, tagName, tags)=>{
|
|
133
|
+
const createElement = createElementFactory(react);
|
|
134
|
+
return tags.filter((tag)=>!priorityTags.has(tag)).map((tag, index)=>createElement(tagName, toReactProps(tag, index)));
|
|
135
|
+
};
|
|
136
|
+
const headTitleToComponent = (react, title, attributes)=>{
|
|
137
|
+
if (!title) return [];
|
|
138
|
+
const createElement = createElementFactory(react);
|
|
139
|
+
return [
|
|
140
|
+
createElement('title', toReactProps(attributes, title), title)
|
|
141
|
+
];
|
|
142
|
+
};
|
|
143
|
+
const normalizedEntries = (tag)=>Object.entries(tag).map(([name, value])=>[
|
|
144
|
+
toHtmlAttributeName(name).toLowerCase(),
|
|
145
|
+
String(value).toLowerCase()
|
|
146
|
+
]);
|
|
147
|
+
const isSeoPriorityTag = (tagName, tag)=>{
|
|
148
|
+
const values = SEO_PRIORITY_VALUES[tagName];
|
|
149
|
+
return normalizedEntries(tag).some(([name, value])=>{
|
|
150
|
+
const allowed = values[name];
|
|
151
|
+
return void 0 !== allowed && ('charset' === name || allowed.includes(value));
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const createPriorityHeadDatum = (react, records, dedupe)=>{
|
|
155
|
+
if (!records.some((record)=>'object' == typeof record && null !== record && priorityRecords.has(record))) return {
|
|
156
|
+
toComponent: ()=>[],
|
|
157
|
+
toString: ()=>''
|
|
158
|
+
};
|
|
159
|
+
const groups = [
|
|
160
|
+
'meta',
|
|
161
|
+
'link',
|
|
162
|
+
"script"
|
|
163
|
+
].map((tagName)=>({
|
|
164
|
+
tagName,
|
|
165
|
+
tags: dedupe(tagName, records).filter((tag)=>isSeoPriorityTag(tagName, tag))
|
|
166
|
+
}));
|
|
167
|
+
for (const { tags } of groups)for (const tag of tags)priorityTags.add(tag);
|
|
168
|
+
const createElement = createElementFactory(react);
|
|
169
|
+
return {
|
|
170
|
+
toComponent: ()=>groups.flatMap(({ tagName, tags })=>tags.map((tag, index)=>createElement(tagName, toReactProps(tag, `${tagName}-${index}`)))),
|
|
171
|
+
toString: ()=>groups.map(({ tagName, tags })=>tagsToString(tagName, tags)).join('')
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const createDatum = (react, tagName, tags)=>({
|
|
175
|
+
toComponent: ()=>headTagsToComponents(react, tagName, tags),
|
|
176
|
+
toString: ()=>tagsToString(tagName, tags.filter((tag)=>!priorityTags.has(tag)))
|
|
177
|
+
});
|
|
178
|
+
const createAttributeDatum = (attributes)=>({
|
|
179
|
+
toComponent: ()=>toReactAttributes(attributes),
|
|
180
|
+
toString: ()=>attributesToString(attributes)
|
|
181
|
+
});
|
|
182
|
+
const createTitleDatum = (react, title, attributes)=>({
|
|
183
|
+
toComponent: ()=>headTitleToComponent(react, title, attributes),
|
|
184
|
+
toString: ()=>{
|
|
185
|
+
if (!title) return '';
|
|
186
|
+
const serialized = attributesToString(attributes, true);
|
|
187
|
+
return `<title ${serialized}>${escapeHtml(title)}</title>`;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const normalizeHelmetTitle = (title)=>{
|
|
191
|
+
if ('string' == typeof title) return title;
|
|
192
|
+
if (Array.isArray(title)) return title.map((part)=>String(part)).join('');
|
|
193
|
+
};
|
|
194
|
+
const mergeAttributes = (current, next)=>({
|
|
195
|
+
...current,
|
|
196
|
+
...next ?? {}
|
|
197
|
+
});
|
|
198
|
+
const isHeadElement = (value)=>'object' == typeof value && null !== value && 'type' in value && 'props' in value && 'object' == typeof value.props && null !== value.props;
|
|
199
|
+
const collectChildren = (react, children, draft)=>{
|
|
200
|
+
react.Children.forEach(children, (child)=>{
|
|
201
|
+
if (!react.isValidElement(child) || !isHeadElement(child)) return;
|
|
202
|
+
const element = child;
|
|
203
|
+
if (element.type === react.Fragment) return void collectChildren(react, element.props.children, draft);
|
|
204
|
+
if ('string' != typeof element.type) return;
|
|
205
|
+
const { children: nestedChildren, ...props } = element.props;
|
|
206
|
+
if ('title' === element.type) {
|
|
207
|
+
draft.title = react.Children.toArray(nestedChildren).join('');
|
|
208
|
+
draft.titleAttributes = mergeAttributes(draft.titleAttributes, props);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if ('html' === element.type) {
|
|
212
|
+
draft.htmlAttributes = mergeAttributes(draft.htmlAttributes ?? {}, props);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if ('body' === element.type) {
|
|
216
|
+
draft.bodyAttributes = mergeAttributes(draft.bodyAttributes ?? {}, props);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if ('base' === element.type || 'link' === element.type || 'meta' === element.type || "noscript" === element.type || "script" === element.type || 'style' === element.type) {
|
|
220
|
+
const tag = {
|
|
221
|
+
...props
|
|
222
|
+
};
|
|
223
|
+
if (("script" === element.type || 'style' === element.type || "noscript" === element.type) && 'string' == typeof nestedChildren) tag['style' === element.type ? 'cssText' : 'innerHTML'] = nestedChildren;
|
|
224
|
+
draft[element.type].push(tag);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const createHelmetRecord = (reactValue, props)=>{
|
|
229
|
+
const draft = {
|
|
230
|
+
base: void 0 === props.base ? [] : [
|
|
231
|
+
{
|
|
232
|
+
...props.base
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
bodyAttributes: void 0 === props.bodyAttributes ? void 0 : {
|
|
236
|
+
...props.bodyAttributes
|
|
237
|
+
},
|
|
238
|
+
htmlAttributes: void 0 === props.htmlAttributes ? void 0 : {
|
|
239
|
+
...props.htmlAttributes
|
|
240
|
+
},
|
|
241
|
+
link: (props.link ?? []).map((tag)=>({
|
|
242
|
+
...tag
|
|
243
|
+
})),
|
|
244
|
+
meta: (props.meta ?? []).map((tag)=>({
|
|
245
|
+
...tag
|
|
246
|
+
})),
|
|
247
|
+
noscript: (props.noscript ?? []).map((tag)=>({
|
|
248
|
+
...tag
|
|
249
|
+
})),
|
|
250
|
+
script: (props.script ?? []).map((tag)=>({
|
|
251
|
+
...tag
|
|
252
|
+
})),
|
|
253
|
+
style: (props.style ?? []).map((tag)=>({
|
|
254
|
+
...tag
|
|
255
|
+
})),
|
|
256
|
+
title: normalizeHelmetTitle(props.title),
|
|
257
|
+
titleAttributes: {
|
|
258
|
+
...props.titleAttributes ?? {}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
collectChildren(reactValue, props.children, draft);
|
|
262
|
+
const record = {
|
|
263
|
+
...draft,
|
|
264
|
+
defaultTitle: props.defaultTitle,
|
|
265
|
+
prioritizeSeoTags: true === props.prioritizeSeoTags,
|
|
266
|
+
titleTemplate: props.titleTemplate
|
|
267
|
+
};
|
|
268
|
+
markPriorityHeadRecord(record, record.prioritizeSeoTags);
|
|
269
|
+
return record;
|
|
270
|
+
};
|
|
271
|
+
const getInnermostDefined = (records, pick)=>{
|
|
272
|
+
for(let index = records.length - 1; index >= 0; index -= 1){
|
|
273
|
+
const value = pick(records[index]);
|
|
274
|
+
if (void 0 !== value) return value;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const deriveTitle = (records)=>{
|
|
278
|
+
const title = getInnermostDefined(records, (record)=>record.title);
|
|
279
|
+
const template = getInnermostDefined(records, (record)=>record.titleTemplate);
|
|
280
|
+
if (template && title) return template.replaceAll('%s', title);
|
|
281
|
+
return title || getInnermostDefined(records, (record)=>record.defaultTitle);
|
|
282
|
+
};
|
|
283
|
+
const mergeRecordAttributes = (records, pick)=>records.reduce((merged, record)=>mergeAttributes(merged, pick(record)), {});
|
|
284
|
+
const deriveBaseTags = (records)=>{
|
|
285
|
+
for(let recordIndex = records.length - 1; recordIndex >= 0; recordIndex--){
|
|
286
|
+
const tags = records[recordIndex].base;
|
|
287
|
+
for(let tagIndex = tags.length - 1; tagIndex >= 0; tagIndex--)if (tags[tagIndex].href) return [
|
|
288
|
+
tags[tagIndex]
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
return [];
|
|
292
|
+
};
|
|
293
|
+
const getPrimaryAttribute = (tagName, tag)=>{
|
|
294
|
+
const candidates = TAG_PRIMARY_ATTRIBUTES[tagName];
|
|
295
|
+
let selectedKey;
|
|
296
|
+
let selectedValue;
|
|
297
|
+
for (const [key, value] of Object.entries(tag)){
|
|
298
|
+
const normalizedKey = CONTENT_PROPERTIES.has(key) ? key : toHtmlAttributeName(key).toLowerCase();
|
|
299
|
+
if (candidates.includes(normalizedKey)) {
|
|
300
|
+
if ('rel' !== selectedKey || 'canonical' !== String(selectedValue).toLowerCase()) {
|
|
301
|
+
if ('rel' !== normalizedKey || 'stylesheet' !== String(value).toLowerCase()) {
|
|
302
|
+
selectedKey = normalizedKey;
|
|
303
|
+
selectedValue = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (void 0 === selectedKey || !selectedValue) return;
|
|
309
|
+
return {
|
|
310
|
+
key: selectedKey,
|
|
311
|
+
value: String(selectedValue).toLowerCase()
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
const dedupeTags = (tagName, records)=>{
|
|
315
|
+
const approvedValues = new Map();
|
|
316
|
+
const approved = [];
|
|
317
|
+
for(let index = records.length - 1; index >= 0; index -= 1){
|
|
318
|
+
const instanceValues = new Map();
|
|
319
|
+
const kept = [];
|
|
320
|
+
for (const tag of records[index][tagName]){
|
|
321
|
+
const primary = getPrimaryAttribute(tagName, tag);
|
|
322
|
+
if (void 0 === primary || approvedValues.get(primary.key)?.has(primary.value)) continue;
|
|
323
|
+
const seen = instanceValues.get(primary.key) ?? new Set();
|
|
324
|
+
seen.add(primary.value);
|
|
325
|
+
instanceValues.set(primary.key, seen);
|
|
326
|
+
kept.push(tag);
|
|
327
|
+
}
|
|
328
|
+
approved.unshift(...kept);
|
|
329
|
+
for (const [key, values] of instanceValues){
|
|
330
|
+
const target = approvedValues.get(key);
|
|
331
|
+
if (void 0 === target) approvedValues.set(key, values);
|
|
332
|
+
else for (const value of values)target.add(value);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return approved;
|
|
336
|
+
};
|
|
337
|
+
const deriveHelmetServerState = (react, records)=>{
|
|
338
|
+
const priority = createPriorityHeadDatum(react, records, dedupeTags);
|
|
339
|
+
return {
|
|
340
|
+
base: createDatum(react, 'base', deriveBaseTags(records)),
|
|
341
|
+
bodyAttributes: createAttributeDatum(mergeRecordAttributes(records, (record)=>record.bodyAttributes)),
|
|
342
|
+
htmlAttributes: createAttributeDatum(mergeRecordAttributes(records, (record)=>record.htmlAttributes)),
|
|
343
|
+
link: createDatum(react, 'link', dedupeTags('link', records)),
|
|
344
|
+
meta: createDatum(react, 'meta', dedupeTags('meta', records)),
|
|
345
|
+
noscript: createDatum(react, "noscript", dedupeTags("noscript", records)),
|
|
346
|
+
priority,
|
|
347
|
+
script: createDatum(react, "script", dedupeTags("script", records)),
|
|
348
|
+
style: createDatum(react, 'style', dedupeTags('style', records)),
|
|
349
|
+
title: createTitleDatum(react, deriveTitle(records), mergeRecordAttributes(records, (record)=>record.titleAttributes))
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
const collectImmediateHelmetState = (react, context, record)=>{
|
|
353
|
+
const records = immediateRecords.get(context) ?? [];
|
|
354
|
+
if (!immediateRecords.has(context)) immediateRecords.set(context, records);
|
|
355
|
+
records.push(record);
|
|
356
|
+
return deriveHelmetServerState(react, records);
|
|
357
|
+
};
|
|
358
|
+
export { collectImmediateHelmetState, createHelmetRecord, createPriorityHeadDatum, deriveHelmetServerState, headTagsToComponents, headTitleToComponent, isPriorityHeadTag, markPriorityHeadRecord, renderHeadMarker };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const SAFE_HTML_ATTRIBUTE_NAME = /^[^\u0000-\u0020"'<>/=]+$/u;
|
|
3
|
+
const isSafeHtmlAttributeName = (name)=>SAFE_HTML_ATTRIBUTE_NAME.test(name);
|
|
4
|
+
const escapeHtmlAttribute = (value)=>String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
5
|
+
export { escapeHtmlAttribute, isSafeHtmlAttributeName };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const realmNamespaces = new WeakMap();
|
|
3
|
+
let nextRealmNamespace = 0;
|
|
4
|
+
function getNavigationWarmupCacheKey(runtimeContext, chunkLoader, publicPath, warmupKey) {
|
|
5
|
+
let loaderNamespaces = realmNamespaces.get(runtimeContext);
|
|
6
|
+
if (void 0 === loaderNamespaces) {
|
|
7
|
+
loaderNamespaces = new WeakMap();
|
|
8
|
+
realmNamespaces.set(runtimeContext, loaderNamespaces);
|
|
9
|
+
}
|
|
10
|
+
let publicPathNamespaces = loaderNamespaces.get(chunkLoader);
|
|
11
|
+
if (void 0 === publicPathNamespaces) {
|
|
12
|
+
publicPathNamespaces = new Map();
|
|
13
|
+
loaderNamespaces.set(chunkLoader, publicPathNamespaces);
|
|
14
|
+
}
|
|
15
|
+
let namespace = publicPathNamespaces.get(publicPath);
|
|
16
|
+
if (void 0 === namespace) {
|
|
17
|
+
namespace = `realm-${++nextRealmNamespace}`;
|
|
18
|
+
publicPathNamespaces.set(publicPath, namespace);
|
|
19
|
+
}
|
|
20
|
+
return `${namespace}:${warmupKey}`;
|
|
21
|
+
}
|
|
22
|
+
export { getNavigationWarmupCacheKey };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { Transform, pipeline } from "node:stream";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import { abortHeadRender, createHeadChunkProcessor } from "./rendererHead.mjs";
|
|
5
|
+
export * from "./rendererHead.mjs";
|
|
6
|
+
const toError = (reason)=>reason instanceof Error ? reason : new Error(String(reason));
|
|
7
|
+
const createNodeHeadMarkerStripper = (context, terminalMarker)=>{
|
|
8
|
+
const decoder = new StringDecoder('utf8');
|
|
9
|
+
const processor = createHeadChunkProcessor(context);
|
|
10
|
+
let terminalCarry = '';
|
|
11
|
+
let sawTerminalMarker = void 0 === terminalMarker;
|
|
12
|
+
const observeTerminalMarker = (output)=>{
|
|
13
|
+
if (!sawTerminalMarker && void 0 !== terminalMarker) {
|
|
14
|
+
const observed = terminalCarry + output;
|
|
15
|
+
sawTerminalMarker = observed.includes(terminalMarker);
|
|
16
|
+
terminalCarry = observed.slice(Math.max(0, observed.length - terminalMarker.length + 1));
|
|
17
|
+
}
|
|
18
|
+
return output;
|
|
19
|
+
};
|
|
20
|
+
return new Transform({
|
|
21
|
+
transform (chunk, _encoding, callback) {
|
|
22
|
+
try {
|
|
23
|
+
callback(null, observeTerminalMarker(processor.push(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
callback(toError(error));
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
flush (callback) {
|
|
29
|
+
try {
|
|
30
|
+
const output = observeTerminalMarker(processor.finish(decoder.end()));
|
|
31
|
+
callback(null, sawTerminalMarker ? output : `${output}${terminalMarker}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
callback(toError(error));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
destroy (error, callback) {
|
|
37
|
+
try {
|
|
38
|
+
abortHeadRender(context);
|
|
39
|
+
callback(error);
|
|
40
|
+
} catch (abortError) {
|
|
41
|
+
callback(error ?? toError(abortError));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const createOnceErrorReporter = (onError)=>{
|
|
47
|
+
let reported = false;
|
|
48
|
+
return (error)=>{
|
|
49
|
+
if (!reported) {
|
|
50
|
+
reported = true;
|
|
51
|
+
onError?.(error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const pipeNodeHeadStream = (options)=>{
|
|
56
|
+
const { source, destination, context, terminalMarker, onError } = options;
|
|
57
|
+
const stripper = createNodeHeadMarkerStripper(context, terminalMarker);
|
|
58
|
+
pipeline(source, stripper, destination, (error)=>{
|
|
59
|
+
if (null !== error) onError?.(error);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
export { createNodeHeadMarkerStripper, createOnceErrorReporter, pipeNodeHeadStream };
|