UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

246 lines (245 loc) 11.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.escapeInject = escapeInject; exports.dangerouslySkipEscape = dangerouslySkipEscape; exports.renderDocumentHtml = renderDocumentHtml; exports.isDocumentHtml = isDocumentHtml; exports.getHtmlString = getHtmlString; const utils_js_1 = require("../utils.js"); const injectAssets_js_1 = require("./injectAssets.js"); const stream_js_1 = require("./stream.js"); const react_streaming_js_1 = require("./stream/react-streaming.js"); const picocolors_1 = __importDefault(require("@brillout/picocolors")); function isDocumentHtml(something) { if (isTemplateWrapped(something) || isEscapedString(something) || (0, stream_js_1.isStream)(something)) { (0, utils_js_1.checkType)(something); return true; } return false; } async function renderDocumentHtml(documentHtml, pageContext, onErrorWhileStreaming, injectFilter) { if (isEscapedString(documentHtml)) { (0, utils_js_1.objectAssign)(pageContext, { _isStream: false }); let htmlString = getEscapedString(documentHtml); htmlString = await (0, injectAssets_js_1.injectHtmlTagsToString)([htmlString], pageContext, injectFilter); return htmlString; } if ((0, stream_js_1.isStream)(documentHtml)) { (0, utils_js_1.objectAssign)(pageContext, { _isStream: true }); const stream = documentHtml; const streamWrapper = await renderHtmlStream(stream, null, pageContext, onErrorWhileStreaming, injectFilter); return streamWrapper; } if (isTemplateWrapped(documentHtml)) { const templateContent = documentHtml._template; const render = renderTemplate(templateContent, pageContext); if (!('htmlStream' in render)) { (0, utils_js_1.objectAssign)(pageContext, { _isStream: false }); const { htmlPartsAll } = render; const htmlString = await (0, injectAssets_js_1.injectHtmlTagsToString)(htmlPartsAll, pageContext, injectFilter); return htmlString; } else { (0, utils_js_1.objectAssign)(pageContext, { _isStream: true }); const { htmlStream } = render; const streamWrapper = await renderHtmlStream(htmlStream, { htmlPartsBegin: render.htmlPartsBegin, htmlPartsEnd: render.htmlPartsEnd, }, pageContext, onErrorWhileStreaming, injectFilter); return streamWrapper; } } (0, utils_js_1.checkType)(documentHtml); (0, utils_js_1.assert)(false); } async function renderHtmlStream(streamOriginal, injectString, pageContext, onErrorWhileStreaming, injectFilter) { const processStreamOptions = { onErrorWhileStreaming, enableEagerStreaming: pageContext.enableEagerStreaming, }; if (injectString) { let streamFromReactStreamingPackage = null; if ((0, react_streaming_js_1.isStreamFromReactStreamingPackage)(streamOriginal) && !streamOriginal.disabled) { streamFromReactStreamingPackage = streamOriginal; } const { injectAtStreamBegin, injectAtStreamAfterFirstChunk, injectAtStreamEnd } = (0, injectAssets_js_1.injectHtmlTagsToStream)(pageContext, streamFromReactStreamingPackage, injectFilter); processStreamOptions.injectStringAtBegin = async () => { return await injectAtStreamBegin(injectString.htmlPartsBegin); }; processStreamOptions.injectStringAtEnd = async () => { return await injectAtStreamEnd(injectString.htmlPartsEnd); }; processStreamOptions.injectStringAfterFirstChunk = () => { return injectAtStreamAfterFirstChunk(); }; } let makeClosableAgain = () => { }; if ((0, react_streaming_js_1.isStreamFromReactStreamingPackage)(streamOriginal)) { // Make sure Vike injects its HTML fragments, such as `<script id="vike_pageContext" type="application/json">`, before the stream is closed (if React/Vue finishes its stream before the promise below resolves). makeClosableAgain = streamOriginal.doNotClose(); } try { const streamWrapper = await (0, stream_js_1.processStream)(streamOriginal, processStreamOptions); return streamWrapper; } finally { makeClosableAgain(); } } function isTemplateWrapped(something) { return (0, utils_js_1.hasProp)(something, '_template'); } function isEscapedString(something) { const result = (0, utils_js_1.hasProp)(something, '_escaped'); if (result) { (0, utils_js_1.assert)((0, utils_js_1.hasProp)(something, '_escaped', 'string')); (0, utils_js_1.checkType)(something); } return result; } function getEscapedString(escapedString) { let htmlString; (0, utils_js_1.assert)((0, utils_js_1.hasProp)(escapedString, '_escaped')); htmlString = escapedString._escaped; (0, utils_js_1.assert)(typeof htmlString === 'string'); return htmlString; } function escapeInject(templateStrings, ...templateVariables) { (0, utils_js_1.assertUsage)(templateStrings.length === templateVariables.length + 1 && templateStrings.every((str) => typeof str === 'string'), `You're using ${picocolors_1.default.cyan('escapeInject')} as a function, but ${picocolors_1.default.cyan('escapeInject')} is a string template tag, see https://vike.dev/escapeInject`, { showStackTrace: true }); return { _template: { templateStrings, templateVariables: templateVariables, }, }; } function dangerouslySkipEscape(alreadyEscapedString) { return _dangerouslySkipEscape(alreadyEscapedString); } function _dangerouslySkipEscape(arg) { if ((0, utils_js_1.hasProp)(arg, '_escaped')) { (0, utils_js_1.assert)(isEscapedString(arg)); return arg; } (0, utils_js_1.assertUsage)(!(0, utils_js_1.isPromise)(arg), `[dangerouslySkipEscape(${picocolors_1.default.cyan('str')})] Argument ${picocolors_1.default.cyan('str')} is a promise. It should be a string instead (or a stream). Make sure to ${picocolors_1.default.cyan('await str')}.`, { showStackTrace: true }); if (typeof arg === 'string') { return { _escaped: arg }; } (0, utils_js_1.assertWarning)(false, `[dangerouslySkipEscape(${picocolors_1.default.cyan('str')})] Argument ${picocolors_1.default.cyan('str')} should be a string but we got ${picocolors_1.default.cyan(`typeof str === "${typeof arg}"`)}.`, { onlyOnce: false, showStackTrace: true, }); return { _escaped: String(arg) }; } function renderTemplate(templateContent, pageContext) { const htmlPartsBegin = []; const htmlPartsEnd = []; let htmlStream = null; const addHtmlPart = (htmlPart) => { if (htmlStream === null) { htmlPartsBegin.push(htmlPart); } else { htmlPartsEnd.push(htmlPart); } }; const setStream = (stream) => { const { hookName, hookFilePath } = pageContext._renderHook; (0, utils_js_1.assertUsage)(!htmlStream, `Injecting two streams in ${picocolors_1.default.cyan('escapeInject')} template tag of ${hookName}() hook defined by ${hookFilePath}. Inject only one stream instead.`); htmlStream = stream; }; const { templateStrings, templateVariables } = templateContent; for (let i = 0; i < templateVariables.length; i++) { addHtmlPart(templateStrings[i]); let templateVar = templateVariables[i]; // Process `dangerouslySkipEscape()` if (isEscapedString(templateVar)) { const htmlString = getEscapedString(templateVar); // User used `dangerouslySkipEscape()` so we assume the string to be safe addHtmlPart(htmlString); continue; } // Process `escapeInject` fragments if (isTemplateWrapped(templateVar)) { const templateContentInner = templateVar._template; const result = renderTemplate(templateContentInner, pageContext); if (!('htmlStream' in result)) { result.htmlPartsAll.forEach(addHtmlPart); } else { result.htmlPartsBegin.forEach(addHtmlPart); setStream(result.htmlStream); result.htmlPartsEnd.forEach(addHtmlPart); } continue; } if ((0, stream_js_1.isStream)(templateVar)) { setStream(templateVar); continue; } const getErrMsg = (msg) => { const { hookName, hookFilePath } = pageContext._renderHook; const nth = (i === 0 && '1st') || (i === 1 && '2nd') || (i === 2 && '3rd') || `${i}-th`; return [ `The ${nth} HTML variable is ${msg}`, `The HTML was provided by the ${hookName}() hook at ${hookFilePath}.`, ] .filter(Boolean) .join(' '); }; (0, utils_js_1.assertUsage)(!(0, utils_js_1.isPromise)(templateVar), getErrMsg(`a promise, did you forget to ${picocolors_1.default.cyan('await')} the promise?`)); if (templateVar === undefined || templateVar === null) { const msgVal = picocolors_1.default.cyan(String(templateVar)); const msgEmptyString = picocolors_1.default.cyan("''"); const msg = `${msgVal} which will be converted to an empty string. Pass the empty string ${msgEmptyString} instead of ${msgVal} to remove this warning.`; (0, utils_js_1.assertWarning)(false, getErrMsg(msg), { onlyOnce: false }); templateVar = ''; } { const varType = typeof templateVar; if (varType !== 'string') { const msgType = picocolors_1.default.cyan(`typeof htmlVariable === "${varType}"`); const msg = `${msgType} but a string or stream (https://vike.dev/streaming) is expected instead.`; (0, utils_js_1.assertUsage)(false, getErrMsg(msg)); } } { const { _isProduction: isProduction } = pageContext._globalContext; if ((0, utils_js_1.isHtml)(templateVar) && // We don't show this warning in production because it's expected that some users may (un)willingly do some XSS injection: we avoid flooding the production logs. !isProduction) { const msgVal = picocolors_1.default.cyan(String(templateVar)); const msg = `${msgVal} which seems to be HTML code. Did you forget to wrap the value with dangerouslySkipEscape()?`; (0, utils_js_1.assertWarning)(false, getErrMsg(msg), { onlyOnce: false }); } } // Escape untrusted template variable addHtmlPart((0, utils_js_1.escapeHtml)(templateVar)); } (0, utils_js_1.assert)(templateStrings.length === templateVariables.length + 1); addHtmlPart(templateStrings[templateStrings.length - 1]); if (htmlStream === null) { (0, utils_js_1.assert)(htmlPartsEnd.length === 0); return { htmlPartsAll: htmlPartsBegin, }; } return { htmlStream, htmlPartsBegin, htmlPartsEnd, }; } async function getHtmlString(htmlRender) { if (typeof htmlRender === 'string') { return htmlRender; } if ((0, stream_js_1.isStream)(htmlRender)) { return (0, stream_js_1.streamToString)(htmlRender); } (0, utils_js_1.checkType)(htmlRender); (0, utils_js_1.assert)(false); }