@dark-engine/platform-server
Version:
Dark renderer for server
159 lines (158 loc) • 5.67 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
exports.inject = exports.convertStreamToPromise = exports.renderToStream = exports.renderToString = void 0;
const node_stream_1 = require('node:stream');
const core_1 = require('@dark-engine/core');
const platform_browser_1 = require('@dark-engine/platform-browser');
const dom_1 = require('../dom');
const native_element_1 = require('../native-element');
const constants_1 = require('../constants');
const spawn = core_1.nextTick;
let nextRootId = -1;
let isInjected = false;
function inject() {
core_1.platform.createElement = dom_1.createNativeElement;
core_1.platform.raf = core_1.dummyFn;
core_1.platform.caf = core_1.dummyFn;
core_1.platform.spawn = spawn;
core_1.platform.commit = dom_1.commit;
core_1.platform.finishCommit = dom_1.finishCommit;
core_1.platform.detectIsDynamic = core_1.falseFn;
isInjected = true;
}
exports.inject = inject;
function scheduleRender(options) {
!isInjected && inject();
const { element, onCompleted, onError, onStart } = options;
const rootId = getNextRootId();
const callback = () => {
(0, core_1.setRootId)(rootId);
const $scope = (0, core_1.$$scope)();
const fiber = new core_1.Fiber().mutate({
el: new native_element_1.TagNativeElement(core_1.ROOT),
inst: new core_1.TagVirtualNode(core_1.ROOT, {}, (0, core_1.flatten)([element || (0, core_1.createReplacer)()])),
tag: core_1.CREATE_EFFECT_TAG,
});
const emitter = $scope.getEmitter();
$scope.setIsStream(true);
$scope.resetMount();
$scope.setWorkInProgress(fiber);
$scope.setUnitOfWork(fiber);
onStart();
emitter.on('finish', () => {
emitter.kill();
onCompleted();
});
emitter.on('error', err => {
emitter.kill();
onError(err);
});
};
core_1.scheduler.schedule(callback, { priority: core_1.TaskPriority.NORMAL, forceAsync: true });
}
function renderToReadableStream(element, options, fromStream) {
const { bootstrapScripts = [], bootstrapModules = [], chunkSize = 500, awaitMetatags = false } = options || {};
const stream = new node_stream_1.Readable({ encoding: 'utf-8', read() {} });
let canSendChunks = true;
let hasMetatags = false;
let content = '';
let stash = '';
const onStart = () => {
const emitter = (0, core_1.$$scope)().getEmitter();
emitter.on('box', box => {
if (!hasMetatags && (0, platform_browser_1.detectIsMetatagsBox)(box)) {
const data = createMetadata(box.vNodes);
hasMetatags = true;
if (awaitMetatags) {
canSendChunks = true;
content += data + stash;
stash = '';
} else if (!fromStream) {
content = content.replace(HEAD_CLOSED_CHUNK, data + HEAD_CLOSED_CHUNK);
}
}
});
emitter.on('chunk', fiber => {
const chunk = (0, dom_1.createChunk)(fiber);
if (chunk === HEAD_CLOSED_CHUNK && awaitMetatags && !hasMetatags) {
canSendChunks = false;
}
if (canSendChunks) {
if (chunk === BODY_CLOSED_CHUNK && (bootstrapScripts.length > 0 || bootstrapModules.length > 0)) {
content += addScripts(bootstrapScripts, false);
content += addScripts(bootstrapModules, true);
}
content += chunk;
if (content.length >= chunkSize) {
stream.push(content);
content = '';
}
} else {
stash += chunk;
}
});
};
const onCompleted = () => {
const rootId = (0, core_1.getRootId)();
if (content) {
stream.push(content);
content = '';
}
stream.push(withState());
stream.push(null);
(0, core_1.unmountRoot)(rootId);
};
const onError = err => {
const rootId = (0, core_1.getRootId)();
stream.emit('error', new Error(err));
stream.push(null);
(0, core_1.unmountRoot)(rootId);
};
scheduleRender({ element, onStart, onCompleted, onError });
return stream;
}
function renderToString(element) {
return convertStreamToPromise(renderToReadableStream(element));
}
exports.renderToString = renderToString;
function renderToStream(element, options) {
const stream = renderToReadableStream(element, options, true);
stream.push(constants_1.DOCTYPE);
return stream;
}
exports.renderToStream = renderToStream;
function convertStreamToPromise(stream) {
return new Promise((resolve, reject) => {
let data = '';
stream.on('data', chunk => (data += chunk));
stream.on('end', () => resolve(data));
stream.on('error', reject);
});
}
exports.convertStreamToPromise = convertStreamToPromise;
function addScripts(scripts, isModule) {
if (scripts.length === 0) return '';
let content = '';
scripts.forEach(x => (content += isModule ? createModule(x) : createScript(x)));
return content;
}
function withState(content = '') {
const $scope = (0, core_1.$$scope)();
const state = $scope.getResources();
const resources = {};
if (state.size === 0) return content;
state.forEach((value, key) => (resources[key] = value));
const encoded = Buffer.from(JSON.stringify(resources)).toString('base64');
const $content = `${content}<script type="${core_1.STATE_SCRIPT_TYPE}">"${encoded}"</script>`;
return $content;
}
const createMetadata = vNodes =>
(0, dom_1.createNativeChildrenNodes)(vNodes)
.map(x => x.renderToString())
.join('');
const createModule = src => `<script type="module" src="${src}" defer></script>`;
const createScript = src => `<script src="${src}" defer></script>`;
const getNextRootId = () => ++nextRootId;
const HEAD_CLOSED_CHUNK = '</head>';
const BODY_CLOSED_CHUNK = '</body>';
//# sourceMappingURL=render.js.map