You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
17 KiB
JavaScript
437 lines
17 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
0 && (module.exports = {
|
|
cloneTransformStream: null,
|
|
chainStreams: null,
|
|
streamFromString: null,
|
|
streamToString: null,
|
|
createBufferedTransformStream: null,
|
|
renderToInitialFizzStream: null,
|
|
createRootLayoutValidatorStream: null,
|
|
continueFizzStream: null,
|
|
continuePostponedFizzStream: null
|
|
});
|
|
function _export(target, all) {
|
|
for(var name in all)Object.defineProperty(target, name, {
|
|
enumerable: true,
|
|
get: all[name]
|
|
});
|
|
}
|
|
_export(exports, {
|
|
cloneTransformStream: function() {
|
|
return cloneTransformStream;
|
|
},
|
|
chainStreams: function() {
|
|
return chainStreams;
|
|
},
|
|
streamFromString: function() {
|
|
return streamFromString;
|
|
},
|
|
streamToString: function() {
|
|
return streamToString;
|
|
},
|
|
createBufferedTransformStream: function() {
|
|
return createBufferedTransformStream;
|
|
},
|
|
renderToInitialFizzStream: function() {
|
|
return renderToInitialFizzStream;
|
|
},
|
|
createRootLayoutValidatorStream: function() {
|
|
return createRootLayoutValidatorStream;
|
|
},
|
|
continueFizzStream: function() {
|
|
return continueFizzStream;
|
|
},
|
|
continuePostponedFizzStream: function() {
|
|
return continuePostponedFizzStream;
|
|
}
|
|
});
|
|
const _tracer = require("../lib/trace/tracer");
|
|
const _constants = require("../lib/trace/constants");
|
|
const _encodedecode = require("./encode-decode");
|
|
const _detachedpromise = require("../../lib/detached-promise");
|
|
const _scheduler = require("../../lib/scheduler");
|
|
function cloneTransformStream(source) {
|
|
const sourceReader = source.readable.getReader();
|
|
const clone = new TransformStream({
|
|
async start (controller) {
|
|
while(true){
|
|
const { done, value } = await sourceReader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
controller.enqueue(value);
|
|
}
|
|
},
|
|
// skip all piped chunks
|
|
transform () {}
|
|
});
|
|
return clone;
|
|
}
|
|
function chainStreams(...streams) {
|
|
const { readable, writable } = new TransformStream();
|
|
let promise = Promise.resolve();
|
|
for(let i = 0; i < streams.length; ++i){
|
|
promise = promise.then(()=>streams[i].pipeTo(writable, {
|
|
preventClose: i + 1 < streams.length
|
|
}));
|
|
}
|
|
// Catch any errors from the streams and ignore them, they will be handled
|
|
// by whatever is consuming the readable stream.
|
|
promise.catch(()=>{});
|
|
return readable;
|
|
}
|
|
function streamFromString(str) {
|
|
const encoder = new TextEncoder();
|
|
return new ReadableStream({
|
|
start (controller) {
|
|
controller.enqueue(encoder.encode(str));
|
|
controller.close();
|
|
}
|
|
});
|
|
}
|
|
async function streamToString(stream) {
|
|
let buffer = "";
|
|
await stream// Decode the streamed chunks to turn them into strings.
|
|
.pipeThrough((0, _encodedecode.createDecodeTransformStream)()).pipeTo(new WritableStream({
|
|
write (chunk) {
|
|
buffer += chunk;
|
|
}
|
|
}));
|
|
return buffer;
|
|
}
|
|
function createBufferedTransformStream() {
|
|
let buffer = new Uint8Array();
|
|
let pending;
|
|
const flush = (controller)=>{
|
|
// If we already have a pending flush, then return early.
|
|
if (pending) return;
|
|
const detached = new _detachedpromise.DetachedPromise();
|
|
pending = detached;
|
|
(0, _scheduler.scheduleImmediate)(()=>{
|
|
try {
|
|
controller.enqueue(buffer);
|
|
buffer = new Uint8Array();
|
|
} catch {
|
|
// If an error occurs while enqueuing it can't be due to this
|
|
// transformers fault. It's likely due to the controller being
|
|
// errored due to the stream being cancelled.
|
|
} finally{
|
|
pending = undefined;
|
|
detached.resolve();
|
|
}
|
|
});
|
|
};
|
|
return new TransformStream({
|
|
transform (chunk, controller) {
|
|
// Combine the previous buffer with the new chunk.
|
|
const combined = new Uint8Array(buffer.length + chunk.byteLength);
|
|
combined.set(buffer);
|
|
combined.set(chunk, buffer.length);
|
|
buffer = combined;
|
|
// Flush the buffer to the controller.
|
|
flush(controller);
|
|
},
|
|
flush () {
|
|
if (!pending) return;
|
|
return pending.promise;
|
|
}
|
|
});
|
|
}
|
|
function createInsertedHTMLStream(getServerInsertedHTML) {
|
|
const encoder = new TextEncoder();
|
|
return new TransformStream({
|
|
transform: async (chunk, controller)=>{
|
|
const html = await getServerInsertedHTML();
|
|
if (html) {
|
|
controller.enqueue(encoder.encode(html));
|
|
}
|
|
controller.enqueue(chunk);
|
|
}
|
|
});
|
|
}
|
|
function renderToInitialFizzStream({ ReactDOMServer, element, streamOptions }) {
|
|
return (0, _tracer.getTracer)().trace(_constants.AppRenderSpan.renderToReadableStream, async ()=>ReactDOMServer.renderToReadableStream(element, streamOptions));
|
|
}
|
|
function createHeadInsertionTransformStream(insert) {
|
|
let inserted = false;
|
|
let freezing = false;
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
return new TransformStream({
|
|
async transform (chunk, controller) {
|
|
// While react is flushing chunks, we don't apply insertions
|
|
if (freezing) {
|
|
controller.enqueue(chunk);
|
|
return;
|
|
}
|
|
const insertion = await insert();
|
|
if (inserted) {
|
|
controller.enqueue(encoder.encode(insertion));
|
|
controller.enqueue(chunk);
|
|
freezing = true;
|
|
} else {
|
|
const content = decoder.decode(chunk);
|
|
const index = content.indexOf("</head>");
|
|
if (index !== -1) {
|
|
const insertedHeadContent = content.slice(0, index) + insertion + content.slice(index);
|
|
controller.enqueue(encoder.encode(insertedHeadContent));
|
|
freezing = true;
|
|
inserted = true;
|
|
}
|
|
}
|
|
if (!inserted) {
|
|
controller.enqueue(chunk);
|
|
} else {
|
|
(0, _scheduler.scheduleImmediate)(()=>{
|
|
freezing = false;
|
|
});
|
|
}
|
|
},
|
|
async flush (controller) {
|
|
// Check before closing if there's anything remaining to insert.
|
|
const insertion = await insert();
|
|
if (insertion) {
|
|
controller.enqueue(encoder.encode(insertion));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// Suffix after main body content - scripts before </body>,
|
|
// but wait for the major chunks to be enqueued.
|
|
function createDeferredSuffixStream(suffix) {
|
|
let flushed = false;
|
|
let pending;
|
|
const encoder = new TextEncoder();
|
|
const flush = (controller)=>{
|
|
const detached = new _detachedpromise.DetachedPromise();
|
|
pending = detached;
|
|
(0, _scheduler.scheduleImmediate)(()=>{
|
|
try {
|
|
controller.enqueue(encoder.encode(suffix));
|
|
} catch {
|
|
// If an error occurs while enqueuing it can't be due to this
|
|
// transformers fault. It's likely due to the controller being
|
|
// errored due to the stream being cancelled.
|
|
} finally{
|
|
pending = undefined;
|
|
detached.resolve();
|
|
}
|
|
});
|
|
};
|
|
return new TransformStream({
|
|
transform (chunk, controller) {
|
|
controller.enqueue(chunk);
|
|
// If we've already flushed, we're done.
|
|
if (flushed) return;
|
|
// Schedule the flush to happen.
|
|
flushed = true;
|
|
flush(controller);
|
|
},
|
|
flush (controller) {
|
|
if (pending) return pending.promise;
|
|
if (flushed) return;
|
|
// Flush now.
|
|
controller.enqueue(encoder.encode(suffix));
|
|
}
|
|
});
|
|
}
|
|
// Merge two streams into one. Ensure the final transform stream is closed
|
|
// when both are finished.
|
|
function createMergedTransformStream(stream) {
|
|
let started = false;
|
|
let pending = null;
|
|
const start = (controller)=>{
|
|
const reader = stream.getReader();
|
|
// NOTE: streaming flush
|
|
// We are buffering here for the inlined data stream because the
|
|
// "shell" stream might be chunkenized again by the underlying stream
|
|
// implementation, e.g. with a specific high-water mark. To ensure it's
|
|
// the safe timing to pipe the data stream, this extra tick is
|
|
// necessary.
|
|
const detached = new _detachedpromise.DetachedPromise();
|
|
pending = detached;
|
|
// We use `setTimeout/setImmediate` here to ensure that it's inserted after
|
|
// flushing the shell. Note that this implementation might get stale if impl
|
|
// details of Fizz change in the future.
|
|
(0, _scheduler.scheduleImmediate)(async ()=>{
|
|
try {
|
|
while(true){
|
|
const { done, value } = await reader.read();
|
|
if (done) return;
|
|
controller.enqueue(value);
|
|
}
|
|
} catch (err) {
|
|
controller.error(err);
|
|
} finally{
|
|
detached.resolve();
|
|
}
|
|
});
|
|
};
|
|
return new TransformStream({
|
|
transform (chunk, controller) {
|
|
controller.enqueue(chunk);
|
|
// Start the streaming if it hasn't already been started yet.
|
|
if (started) return;
|
|
started = true;
|
|
start(controller);
|
|
},
|
|
flush () {
|
|
// If the data stream promise is defined, then return it as its completion
|
|
// will be the completion of the stream.
|
|
if (!pending) return;
|
|
if (!started) return;
|
|
return pending.promise;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* This transform stream moves the suffix to the end of the stream, so results
|
|
* like `</body></html><script>...</script>` will be transformed to
|
|
* `<script>...</script></body></html>`.
|
|
*/ function createMoveSuffixStream(suffix) {
|
|
let foundSuffix = false;
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
return new TransformStream({
|
|
transform (chunk, controller) {
|
|
if (foundSuffix) {
|
|
return controller.enqueue(chunk);
|
|
}
|
|
const buf = decoder.decode(chunk);
|
|
const index = buf.indexOf(suffix);
|
|
if (index > -1) {
|
|
foundSuffix = true;
|
|
// If the whole chunk is the suffix, then don't write anything, it will
|
|
// be written in the flush.
|
|
if (buf.length === suffix.length) {
|
|
return;
|
|
}
|
|
// Write out the part before the suffix.
|
|
const before = buf.slice(0, index);
|
|
chunk = encoder.encode(before);
|
|
controller.enqueue(chunk);
|
|
// In the case where the suffix is in the middle of the chunk, we need
|
|
// to split the chunk into two parts.
|
|
if (buf.length > suffix.length + index) {
|
|
// Write out the part after the suffix.
|
|
const after = buf.slice(index + suffix.length);
|
|
chunk = encoder.encode(after);
|
|
controller.enqueue(chunk);
|
|
}
|
|
} else {
|
|
controller.enqueue(chunk);
|
|
}
|
|
},
|
|
flush (controller) {
|
|
// Even if we didn't find the suffix, the HTML is not valid if we don't
|
|
// add it, so insert it at the end.
|
|
controller.enqueue(encoder.encode(suffix));
|
|
}
|
|
});
|
|
}
|
|
function createRootLayoutValidatorStream(assetPrefix = "", getTree) {
|
|
let foundHtml = false;
|
|
let foundBody = false;
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
let content = "";
|
|
return new TransformStream({
|
|
async transform (chunk, controller) {
|
|
// Peek into the streamed chunk to see if the tags are present.
|
|
if (!foundHtml || !foundBody) {
|
|
content += decoder.decode(chunk, {
|
|
stream: true
|
|
});
|
|
if (!foundHtml && content.includes("<html")) {
|
|
foundHtml = true;
|
|
}
|
|
if (!foundBody && content.includes("<body")) {
|
|
foundBody = true;
|
|
}
|
|
}
|
|
controller.enqueue(chunk);
|
|
},
|
|
flush (controller) {
|
|
// Flush the decoder.
|
|
if (!foundHtml || !foundBody) {
|
|
content += decoder.decode();
|
|
if (!foundHtml && content.includes("<html")) {
|
|
foundHtml = true;
|
|
}
|
|
if (!foundBody && content.includes("<body")) {
|
|
foundBody = true;
|
|
}
|
|
}
|
|
// If html or body tag is missing, we need to inject a script to notify
|
|
// the client.
|
|
const missingTags = [];
|
|
if (!foundHtml) missingTags.push("html");
|
|
if (!foundBody) missingTags.push("body");
|
|
if (missingTags.length > 0) {
|
|
controller.enqueue(encoder.encode(`<script>self.__next_root_layout_missing_tags_error=${JSON.stringify({
|
|
missingTags,
|
|
assetPrefix: assetPrefix ?? "",
|
|
tree: getTree()
|
|
})}</script>`));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function chainTransformers(readable, transformers) {
|
|
let stream = readable;
|
|
for (const transformer of transformers){
|
|
if (!transformer) continue;
|
|
stream = stream.pipeThrough(transformer);
|
|
}
|
|
return stream;
|
|
}
|
|
async function continueFizzStream(renderStream, { suffix, inlinedDataStream, isStaticGeneration, getServerInsertedHTML, serverInsertedHTMLToHead, validateRootLayout }) {
|
|
const closeTag = "</body></html>";
|
|
// Suffix itself might contain close tags at the end, so we need to split it.
|
|
const suffixUnclosed = suffix ? suffix.split(closeTag, 1)[0] : null;
|
|
// If we're generating static HTML and there's an `allReady` promise on the
|
|
// stream, we need to wait for it to resolve before continuing.
|
|
if (isStaticGeneration && "allReady" in renderStream) {
|
|
await renderStream.allReady;
|
|
}
|
|
return chainTransformers(renderStream, [
|
|
// Buffer everything to avoid flushing too frequently
|
|
createBufferedTransformStream(),
|
|
// Insert generated tags to head
|
|
getServerInsertedHTML && !serverInsertedHTMLToHead ? createInsertedHTMLStream(getServerInsertedHTML) : null,
|
|
// Insert suffix content
|
|
suffixUnclosed != null && suffixUnclosed.length > 0 ? createDeferredSuffixStream(suffixUnclosed) : null,
|
|
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
|
|
inlinedDataStream ? createMergedTransformStream(inlinedDataStream) : null,
|
|
// Close tags should always be deferred to the end
|
|
createMoveSuffixStream(closeTag),
|
|
// Special head insertions
|
|
// TODO-APP: Insert server side html to end of head in app layout rendering, to avoid
|
|
// hydration errors. Remove this once it's ready to be handled by react itself.
|
|
getServerInsertedHTML && serverInsertedHTMLToHead ? createHeadInsertionTransformStream(getServerInsertedHTML) : null,
|
|
validateRootLayout ? createRootLayoutValidatorStream(validateRootLayout.assetPrefix, validateRootLayout.getTree) : null
|
|
]);
|
|
}
|
|
async function continuePostponedFizzStream(renderStream, { inlinedDataStream, isStaticGeneration, getServerInsertedHTML, serverInsertedHTMLToHead }) {
|
|
const closeTag = "</body></html>";
|
|
// If we're generating static HTML and there's an `allReady` promise on the
|
|
// stream, we need to wait for it to resolve before continuing.
|
|
if (isStaticGeneration && "allReady" in renderStream) {
|
|
await renderStream.allReady;
|
|
}
|
|
return chainTransformers(renderStream, [
|
|
// Buffer everything to avoid flushing too frequently
|
|
createBufferedTransformStream(),
|
|
// Insert generated tags to head
|
|
getServerInsertedHTML && !serverInsertedHTMLToHead ? createInsertedHTMLStream(getServerInsertedHTML) : null,
|
|
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
|
|
inlinedDataStream ? createMergedTransformStream(inlinedDataStream) : null,
|
|
// Close tags should always be deferred to the end
|
|
createMoveSuffixStream(closeTag)
|
|
]);
|
|
}
|
|
|
|
//# sourceMappingURL=node-web-streams-helper.js.map
|