@dark-engine/platform-server
Version:
Dark renderer for server
171 lines (170 loc) • 5.33 kB
JavaScript
import { Readable } from 'node:stream';
import {
ROOT,
Fiber,
CREATE_EFFECT_TAG,
STATE_SCRIPT_TYPE,
TaskPriority,
platform,
flatten,
TagVirtualNode,
createReplacer,
unmountRoot,
setRootId,
getRootId,
$$scope,
nextTick,
dummyFn,
falseFn,
scheduler,
} from '@dark-engine/core';
import { detectIsMetatagsBox } from '@dark-engine/platform-browser';
import { createNativeElement, commit, finishCommit, createChunk, createNativeChildrenNodes } from '../dom';
import { TagNativeElement } from '../native-element';
import { DOCTYPE } from '../constants';
const spawn = nextTick;
let nextRootId = -1;
let isInjected = false;
function inject() {
platform.createElement = createNativeElement;
platform.raf = dummyFn;
platform.caf = dummyFn;
platform.spawn = spawn;
platform.commit = commit;
platform.finishCommit = finishCommit;
platform.detectIsDynamic = falseFn;
isInjected = true;
}
function scheduleRender(options) {
!isInjected && inject();
const { element, onCompleted, onError, onStart } = options;
const rootId = getNextRootId();
const callback = () => {
setRootId(rootId);
const $scope = $$scope();
const fiber = new Fiber().mutate({
el: new TagNativeElement(ROOT),
inst: new TagVirtualNode(ROOT, {}, flatten([element || createReplacer()])),
tag: 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);
});
};
scheduler.schedule(callback, { priority: TaskPriority.NORMAL, forceAsync: true });
}
function renderToReadableStream(element, options, fromStream) {
const { bootstrapScripts = [], bootstrapModules = [], chunkSize = 500, awaitMetatags = false } = options || {};
const stream = new Readable({ encoding: 'utf-8', read() {} });
let canSendChunks = true;
let hasMetatags = false;
let content = '';
let stash = '';
const onStart = () => {
const emitter = $$scope().getEmitter();
emitter.on('box', box => {
if (!hasMetatags && 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 = 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 = getRootId();
if (content) {
stream.push(content);
content = '';
}
stream.push(withState());
stream.push(null);
unmountRoot(rootId);
};
const onError = err => {
const rootId = getRootId();
stream.emit('error', new Error(err));
stream.push(null);
unmountRoot(rootId);
};
scheduleRender({ element, onStart, onCompleted, onError });
return stream;
}
function renderToString(element) {
return convertStreamToPromise(renderToReadableStream(element));
}
function renderToStream(element, options) {
const stream = renderToReadableStream(element, options, true);
stream.push(DOCTYPE);
return stream;
}
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);
});
}
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 = $$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="${STATE_SCRIPT_TYPE}">"${encoded}"</script>`;
return $content;
}
const createMetadata = vNodes =>
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>';
export { renderToString, renderToStream, convertStreamToPromise, inject };
//# sourceMappingURL=render.js.map