@lvce-editor/preview-process
Version:
1,808 lines (1,735 loc) • 46.6 kB
JavaScript
import { createServer } from 'node:http';
import { createHash } from 'node:crypto';
import path, { extname, dirname, join } from 'node:path';
import * as fs from 'node:fs';
import { createReadStream } from 'node:fs';
import * as nodeFs from 'node:fs/promises';
import { stat } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { pipeline } from 'node:stream/promises';
const normalizeLine = line => {
if (line.startsWith('Error: ')) {
return line.slice('Error: '.length);
}
if (line.startsWith('VError: ')) {
return line.slice('VError: '.length);
}
return line;
};
const getCombinedMessage = (error, message) => {
const stringifiedError = normalizeLine(`${error}`);
if (message) {
return `${message}: ${stringifiedError}`;
}
return stringifiedError;
};
const NewLine$2 = '\n';
const getNewLineIndex$1 = (string, startIndex = undefined) => {
return string.indexOf(NewLine$2, startIndex);
};
const mergeStacks = (parent, child) => {
if (!child) {
return parent;
}
const parentNewLineIndex = getNewLineIndex$1(parent);
const childNewLineIndex = getNewLineIndex$1(child);
if (childNewLineIndex === -1) {
return parent;
}
const parentFirstLine = parent.slice(0, parentNewLineIndex);
const childRest = child.slice(childNewLineIndex);
const childFirstLine = normalizeLine(child.slice(0, childNewLineIndex));
if (parentFirstLine.includes(childFirstLine)) {
return parentFirstLine + childRest;
}
return child;
};
class VError extends Error {
constructor(error, message) {
const combinedMessage = getCombinedMessage(error, message);
super(combinedMessage);
this.name = 'VError';
if (error instanceof Error) {
this.stack = mergeStacks(this.stack, error.stack);
}
if (error.codeFrame) {
// @ts-ignore
this.codeFrame = error.codeFrame;
}
if (error.code) {
// @ts-ignore
this.code = error.code;
}
}
}
const infos = Object.create(null);
const set$2 = (webViewId, info) => {
infos[webViewId] = info;
};
const get$2 = webViewId => {
return infos[webViewId];
};
const emptyInfo = {
contentSecurityPolicy: '',
iframeContent: '',
webViewId: '',
webViewRoot: '',
remotePathPrefix: '/remote'
};
const getInfoAndPath = requestUrl => {
const {
pathname
} = new URL(requestUrl, 'http://localhost');
if (pathname.startsWith('/remote')) {
return {
pathName: pathname,
info: emptyInfo
};
}
if (pathname.endsWith('/preview-injected.js')) {
return {
pathName: pathname,
info: emptyInfo
};
}
const parts = pathname.split('/');
if (parts.length < 2) {
return undefined;
}
const webViewId = parts[1];
const info = get$2(webViewId);
if (!info) {
return undefined;
}
if (pathname === `/${webViewId}` || pathname === `/${webViewId}/`) {
return {
info,
pathName: '/index.html'
};
}
return {
info,
pathName: '/' + parts.slice(2).join('/')
};
};
const findMatchingRoute = (path, routes) => {
return routes.find(route => typeof route.pattern === 'string' ? path === route.pattern : route.pattern.test(path));
};
const AcceptRanges = 'Accept-Ranges';
const ContentLength = 'Content-Length';
const ContentRange = 'Content-Range';
const ContentSecurityPolicy = 'Content-Security-Policy';
const ContentType = 'Content-Type';
const CrossOriginEmbedderPolicy = 'Cross-Origin-Embedder-Policy';
const ServerTiming = 'Server-Timing';
const CrossOriginResourcePolicy = 'Cross-Origin-Resource-Policy';
const Etag = 'ETag';
const IfNotMatch = 'if-none-match';
const Get = 'GET';
const Head = 'HEAD';
class HeadResponse extends Response {
constructor(status, headers) {
super(null, {
status,
headers
});
}
}
const MethodNotAllowed = 405;
const NotFound = 404;
const Ok = 200;
const PartialContent = 206;
const RangeNotSatisfiable = 416;
const BadRequest = 400;
const NotModified = 304;
const ServerError = 500;
class MethodNotAllowedResponse extends Response {
constructor() {
super('405 - Method Not Allowed', {
status: MethodNotAllowed,
headers: {
[CrossOriginResourcePolicy]: 'same-origin'
}
});
}
}
class NotFoundResponse extends Response {
constructor() {
super('not found', {
status: NotFound,
headers: {
[CrossOriginResourcePolicy]: 'same-origin'
}
});
}
}
class ServerErrorResponse extends Response {
constructor() {
super('Internal Server Error', {
status: ServerError,
headers: {
[CrossOriginResourcePolicy]: 'same-origin'
}
});
}
}
const value = 'require-corp';
const CrossOrigin = 'cross-origin';
const generateEtag = content => {
const hash = createHash('sha1');
hash.update(content);
return `W/"${hash.digest('hex')}"`;
};
const getContentSecurityPolicyDocument = contentSecurityPolicy => {
return contentSecurityPolicy;
};
const textMimeType = {
'.html': 'text/html',
'.js': 'text/javascript',
'.ts': 'text/javascript',
'.mjs': 'text/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.svg': 'image/svg+xml',
'.avif': 'image/avif',
'.woff': 'application/font-woff',
'.ttf': 'font/ttf',
'.png': 'image/png',
'.jpe': 'image/jpg',
'.ico': 'image/x-icon',
'.jpeg': 'image/jpg',
'.jpg': 'image/jpg',
'.webp': 'image/webp'
};
const getContentType = filePath => {
return textMimeType[extname(filePath)] || 'text/plain';
};
const matchesEtag = (request, etag) => {
return request.headers[IfNotMatch] === etag;
};
class NotModifiedResponse extends Response {
constructor(etag, extraHeaders = {}) {
super(null, {
status: NotModified,
headers: {
[CrossOriginResourcePolicy]: 'same-origin',
[Etag]: etag,
...extraHeaders
}
});
}
}
const handleIndexHtml = async (request, options) => {
try {
if (!options.iframeContent) {
throw new Error('iframe content is required');
}
const contentType = getContentType('/test/index.html');
const csp = getContentSecurityPolicyDocument(options.contentSecurityPolicy);
const headers = {
[CrossOriginResourcePolicy]: CrossOrigin,
[CrossOriginEmbedderPolicy]: value,
[ContentSecurityPolicy]: csp,
[ContentType]: contentType
};
if (options.etag) {
const etag = generateEtag(options.iframeContent);
if (matchesEtag(request, etag)) {
return new NotModifiedResponse(etag, headers);
}
// @ts-ignore
headers[Etag] = etag;
}
return new Response(options.iframeContent, {
headers
});
} catch (error) {
console.error(`[preview-server] ${error}`);
return new ServerErrorResponse();
}
};
const readFile = async url => {
const buffer = await nodeFs.readFile(url);
return buffer;
};
const getEtag = fileStat => {
return `W/"${[fileStat.ino, fileStat.size, fileStat.mtime.getTime()].join('-')}"`;
};
const getPathEtag = async absolutePath => {
let stats = await stat(absolutePath);
if (stats.isDirectory()) {
absolutePath += '/index.html';
stats = await stat(absolutePath);
}
const etag = getEtag(stats);
return etag;
};
class BadRequestResponse extends Response {
constructor(message = 'Bad Request') {
super(message, {
status: BadRequest,
headers: {
[CrossOriginResourcePolicy]: 'same-origin'
}
});
}
}
class RangeNotSatisfiableResponse extends Response {
constructor(totalSize) {
super(null, {
status: RangeNotSatisfiable,
headers: {
[ContentRange]: `bytes */${totalSize}`,
[CrossOriginResourcePolicy]: 'same-origin'
}
});
}
}
class RangeResponse extends Response {
constructor(readStream, start, end, totalSize) {
super(readStream, {
status: PartialContent,
headers: {
[ContentRange]: `bytes ${start}-${end}/${totalSize}`,
[ContentLength]: `${end - start + 1}`,
[AcceptRanges]: 'bytes'
}
});
}
}
const handleRangeRequest = async (filePath, range) => {
const stats = await stat(filePath);
const [x, y] = range.replace('bytes=', '').split('-');
const start = Number.parseInt(x, 10);
if (Number.isNaN(start)) {
return new BadRequestResponse('Invalid Range');
}
if (start >= stats.size) {
return new RangeNotSatisfiableResponse(stats.size);
}
const end = y ? Number.parseInt(y, 10) : stats.size - 1;
const finalEnd = end >= stats.size ? stats.size - 1 : end;
const readStream = createReadStream(filePath, {
start,
end: finalEnd
});
return new RangeResponse(readStream, start, finalEnd, stats.size);
};
const ENOENT = 'ENOENT';
const ERR_STREAM_PREMATURE_CLOSE = 'ERR_STREAM_PREMATURE_CLOSE';
const isEnoentError = error => {
// @ts-ignore
return error && error.code && error.code === ENOENT;
};
const isUriError = error => {
return Boolean(error && error instanceof URIError);
};
const resolveFilePath = (pathName, webViewRoot, remotePathPrefix = '/remote') => {
// TODO remove this, double slash should not be allowed
// TODO use path.resolve and verify that file path is in allowed roots
if (pathName.startsWith('/remote//')) {
const filePath = pathName.slice('/remote/'.length);
return fileURLToPath(`file://${filePath}`);
}
if (pathName.startsWith('/remote/')) {
const filePath = pathName.slice('/remote'.length);
return fileURLToPath(`file://${filePath}`);
}
if (webViewRoot.startsWith('file://')) {
return fileURLToPath(`${webViewRoot}${pathName}`);
}
const filePath = fileURLToPath(`file://${webViewRoot}${pathName}`);
return filePath;
};
class ContentResponse extends Response {
constructor(content, contentType, etag) {
const headers = {
[CrossOriginResourcePolicy]: 'same-origin',
[ContentType]: contentType
};
if (etag) {
headers[Etag] = etag;
}
super(content, {
status: Ok,
headers
});
}
}
const handleOther = async (requestOptions, handlerOptions) => {
try {
const filePath = resolveFilePath(requestOptions.path, handlerOptions.webViewRoot);
if (requestOptions.headers && requestOptions.headers.range) {
return await handleRangeRequest(filePath, requestOptions.headers.range);
}
const etag = handlerOptions.etag ? await getPathEtag(filePath) : undefined;
if (etag === null) {
return new NotFoundResponse();
}
if (etag && matchesEtag(requestOptions, etag)) {
return new NotModifiedResponse(etag);
}
const contentType = getContentType(filePath);
if (handlerOptions.stream) {
const readStream = createReadStream(filePath);
return new ContentResponse(readStream, contentType, etag);
}
const content = await readFile(filePath);
return new ContentResponse(content, contentType, etag);
} catch (error) {
if (isEnoentError(error)) {
return new NotFoundResponse();
}
if (isUriError(error)) {
return new BadRequestResponse();
}
console.error(`[preview-server] ${error}`);
return new ServerErrorResponse();
}
};
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');
const injectedCodePath = join(root, 'files', 'previewInjectedCode.js');
const getPreviewInjectedCode = () => {
const injectedCode = fs.readFileSync(injectedCodePath, 'utf8');
return injectedCode;
};
const handlePreviewInjected = async () => {
const injectedCode = getPreviewInjectedCode();
const contentType = getContentType('/test/file.js');
return new ContentResponse(injectedCode, contentType);
};
const routes = [{
pattern: /index\.html$/,
handler: handleIndexHtml
}, {
pattern: /preview-injected\.js$/,
handler: handlePreviewInjected
}, {
pattern: /.*/,
handler: handleOther
}];
const getResponse$2 = async (request, options) => {
const start = performance.now();
try {
if (request.method !== Get && request.method !== Head) {
return new MethodNotAllowedResponse();
}
const matchedRoute = findMatchingRoute(request.path, routes);
if (!matchedRoute) {
return new NotFoundResponse();
}
const response = await matchedRoute.handler(request, options);
if (request.method === Head) {
return new HeadResponse(response.status, response.headers);
}
// Add Server-Timing header
const duration = Math.round(performance.now() - start);
response.headers.set(ServerTiming, `total;dur=${duration}`);
return response;
} catch (error) {
console.error(`[preview-process] ${error}`);
return new ServerErrorResponse();
}
};
const isStreamPrematureCloseError = error => {
return error && error.code === ERR_STREAM_PREMATURE_CLOSE;
};
const sendResponse = async (response, result) => {
try {
response.statusCode = result.status;
for (const [key, value] of result.headers.entries()) {
response.setHeader(key, value);
}
if (!result.body) {
response.end();
return;
}
await pipeline(result.body, response);
} catch (error) {
if (isStreamPrematureCloseError(error)) {
return;
}
if (isEnoentError(error)) {
if (!response.headersSent) {
response.statusCode = NotFound;
response.end('Not Found');
}
return;
}
console.error(`[preview-process] ${error}`);
if (!response.headersSent) {
response.statusCode = ServerError;
response.end('Internal Server Error');
}
}
};
const handleRequest2 = async (request, response) => {
const infoAndPath = getInfoAndPath(request.url || '');
if (!infoAndPath) {
const result = new NotFoundResponse();
await sendResponse(response, result);
return;
}
const requestOptions = {
method: request.method || 'GET',
path: infoAndPath.pathName,
headers: request.headers
};
const handlerOptions = {
webViewRoot: infoAndPath.info.webViewRoot,
contentSecurityPolicy: infoAndPath.info.contentSecurityPolicy,
iframeContent: infoAndPath.info.iframeContent,
stream: false,
etag: true,
remotePathPrefix: '/remote'
};
const result = await getResponse$2(requestOptions, handlerOptions);
await sendResponse(response, result);
};
const servers = Object.create(null);
const set$1 = (id, server) => {
servers[id] = server;
};
const get$1 = id => {
const server = servers[id];
if (!server) {
throw new Error(`Server with id ${id} not found`);
}
return server;
};
const has = id => {
return id in servers;
};
const createWebViewServer = (id, useNewHandler) => {
try {
if (has(id)) {
return;
}
const server = createServer();
if (useNewHandler) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on('request', handleRequest2);
}
const webViewServer = {
handler: undefined,
setHandler(handleRequest) {
if (this.handler) {
return;
}
this.handler = handleRequest;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on('request', handleRequest);
},
on(event, listener) {
server.on(event, listener);
},
off(event, listener) {
server.off(event, listener);
},
listen(port, callback) {
server.listen(port, callback);
},
isListening() {
return server.listening;
}
};
set$1(id, webViewServer);
} catch (error) {
throw new VError(error, 'Failed to create webview server');
}
};
const isMessagePort = value => {
return value && value instanceof MessagePort;
};
const isMessagePortMain = value => {
return value && value.constructor && value.constructor.name === 'MessagePortMain';
};
const isOffscreenCanvas = value => {
return typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas;
};
const isInstanceOf = (value, constructorName) => {
return value?.constructor?.name === constructorName;
};
const isSocket = value => {
return isInstanceOf(value, 'Socket');
};
const transferrables = [isMessagePort, isMessagePortMain, isOffscreenCanvas, isSocket];
const isTransferrable = value => {
for (const fn of transferrables) {
if (fn(value)) {
return true;
}
}
return false;
};
const walkValue = (value, transferrables, isTransferrable) => {
if (!value) {
return;
}
if (isTransferrable(value)) {
transferrables.push(value);
return;
}
if (Array.isArray(value)) {
for (const item of value) {
walkValue(item, transferrables, isTransferrable);
}
return;
}
if (typeof value === 'object') {
for (const property of Object.values(value)) {
walkValue(property, transferrables, isTransferrable);
}
return;
}
};
const getTransferrables = value => {
const transferrables = [];
walkValue(value, transferrables, isTransferrable);
return transferrables;
};
const removeValues = (value, toRemove) => {
if (!value) {
return value;
}
if (Array.isArray(value)) {
const newItems = [];
for (const item of value) {
if (!toRemove.includes(item)) {
newItems.push(removeValues(item, toRemove));
}
}
return newItems;
}
if (typeof value === 'object') {
const newObject = Object.create(null);
for (const [key, property] of Object.entries(value)) {
if (!toRemove.includes(property)) {
newObject[key] = removeValues(property, toRemove);
}
}
return newObject;
}
return value;
};
// workaround for electron not supporting transferrable objects
// as parameters. If the transferrable object is a parameter, in electron
// only an empty objected is received in the main process
const fixElectronParameters = value => {
const transfer = getTransferrables(value);
const newValue = removeValues(value, transfer);
return {
newValue,
transfer
};
};
const getActualDataElectron = event => {
const {
data,
ports
} = event;
if (ports.length === 0) {
return data;
}
return {
...data,
params: [...ports, ...data.params]
};
};
const attachEvents = that => {
const handleMessage = (...args) => {
const data = that.getData(...args);
that.dispatchEvent(new MessageEvent('message', {
data
}));
};
that.onMessage(handleMessage);
const handleClose = event => {
that.dispatchEvent(new Event('close'));
};
that.onClose(handleClose);
};
class Ipc extends EventTarget {
constructor(rawIpc) {
super();
this._rawIpc = rawIpc;
attachEvents(this);
}
}
const E_INCOMPATIBLE_NATIVE_MODULE = 'E_INCOMPATIBLE_NATIVE_MODULE';
const E_MODULES_NOT_SUPPORTED_IN_ELECTRON = 'E_MODULES_NOT_SUPPORTED_IN_ELECTRON';
const ERR_MODULE_NOT_FOUND = 'ERR_MODULE_NOT_FOUND';
const NewLine$1 = '\n';
const joinLines$1 = lines => {
return lines.join(NewLine$1);
};
const RE_AT = /^\s+at/;
const RE_AT_PROMISE_INDEX = /^\s*at async Promise.all \(index \d+\)$/;
const isNormalStackLine = line => {
return RE_AT.test(line) && !RE_AT_PROMISE_INDEX.test(line);
};
const getDetails = lines => {
const index = lines.findIndex(isNormalStackLine);
if (index === -1) {
return {
actualMessage: joinLines$1(lines),
rest: []
};
}
let lastIndex = index - 1;
while (++lastIndex < lines.length) {
if (!isNormalStackLine(lines[lastIndex])) {
break;
}
}
return {
actualMessage: lines[index - 1],
rest: lines.slice(index, lastIndex)
};
};
const splitLines$1 = lines => {
return lines.split(NewLine$1);
};
const RE_MESSAGE_CODE_BLOCK_START = /^Error: The module '.*'$/;
const RE_MESSAGE_CODE_BLOCK_END = /^\s* at/;
const isMessageCodeBlockStartIndex = line => {
return RE_MESSAGE_CODE_BLOCK_START.test(line);
};
const isMessageCodeBlockEndIndex = line => {
return RE_MESSAGE_CODE_BLOCK_END.test(line);
};
const getMessageCodeBlock = stderr => {
const lines = splitLines$1(stderr);
const startIndex = lines.findIndex(isMessageCodeBlockStartIndex);
const endIndex = startIndex + lines.slice(startIndex).findIndex(isMessageCodeBlockEndIndex, startIndex);
const relevantLines = lines.slice(startIndex, endIndex);
const relevantMessage = relevantLines.join(' ').slice('Error: '.length);
return relevantMessage;
};
const isModuleNotFoundMessage = line => {
return line.includes('[ERR_MODULE_NOT_FOUND]');
};
const getModuleNotFoundError = stderr => {
const lines = splitLines$1(stderr);
const messageIndex = lines.findIndex(isModuleNotFoundMessage);
const message = lines[messageIndex];
return {
message,
code: ERR_MODULE_NOT_FOUND
};
};
const isModuleNotFoundError = stderr => {
if (!stderr) {
return false;
}
return stderr.includes('ERR_MODULE_NOT_FOUND');
};
const isModulesSyntaxError = stderr => {
if (!stderr) {
return false;
}
return stderr.includes('SyntaxError: Cannot use import statement outside a module');
};
const RE_NATIVE_MODULE_ERROR = /^innerError Error: Cannot find module '.*.node'/;
const RE_NATIVE_MODULE_ERROR_2 = /was compiled against a different Node.js version/;
const isUnhelpfulNativeModuleError = stderr => {
return RE_NATIVE_MODULE_ERROR.test(stderr) && RE_NATIVE_MODULE_ERROR_2.test(stderr);
};
const getNativeModuleErrorMessage = stderr => {
const message = getMessageCodeBlock(stderr);
return {
message: `Incompatible native node module: ${message}`,
code: E_INCOMPATIBLE_NATIVE_MODULE
};
};
const getModuleSyntaxError = () => {
return {
message: `ES Modules are not supported in electron`,
code: E_MODULES_NOT_SUPPORTED_IN_ELECTRON
};
};
const getHelpfulChildProcessError = (stdout, stderr) => {
if (isUnhelpfulNativeModuleError(stderr)) {
return getNativeModuleErrorMessage(stderr);
}
if (isModulesSyntaxError(stderr)) {
return getModuleSyntaxError();
}
if (isModuleNotFoundError(stderr)) {
return getModuleNotFoundError(stderr);
}
const lines = splitLines$1(stderr);
const {
actualMessage,
rest
} = getDetails(lines);
return {
message: actualMessage,
code: '',
stack: rest
};
};
class IpcError extends VError {
// @ts-ignore
constructor(betterMessage, stdout = '', stderr = '') {
if (stdout || stderr) {
// @ts-ignore
const {
message,
code,
stack
} = getHelpfulChildProcessError(stdout, stderr);
const cause = new Error(message);
// @ts-ignore
cause.code = code;
cause.stack = stack;
super(cause, betterMessage);
} else {
super(betterMessage);
}
// @ts-ignore
this.name = 'IpcError';
// @ts-ignore
this.stdout = stdout;
// @ts-ignore
this.stderr = stderr;
}
}
const listen$b = ({
messagePort
}) => {
if (!isMessagePortMain(messagePort)) {
throw new IpcError('port must be of type MessagePortMain');
}
return messagePort;
};
const signal$c = messagePort => {
messagePort.start();
};
class IpcChildWithElectronMessagePort extends Ipc {
getData = getActualDataElectron;
send(message) {
this._rawIpc.postMessage(message);
}
sendAndTransfer(message) {
const {
newValue,
transfer
} = fixElectronParameters(message);
this._rawIpc.postMessage(newValue, transfer);
}
dispose() {
this._rawIpc.close();
}
onMessage(callback) {
this._rawIpc.on('message', callback);
}
onClose(callback) {
this._rawIpc.on('close', callback);
}
}
const wrap$j = messagePort => {
return new IpcChildWithElectronMessagePort(messagePort);
};
const IpcChildWithElectronMessagePort$1 = {
__proto__: null,
listen: listen$b,
signal: signal$c,
wrap: wrap$j
};
// @ts-ignore
const getUtilityProcessPortData = event => {
const {
data,
ports
} = event;
if (ports.length === 0) {
return data;
}
return {
...data,
params: [...ports, ...data.params]
};
};
const readyMessage = 'ready';
const listen$a = () => {
// @ts-ignore
const {
parentPort
} = process;
if (!parentPort) {
throw new Error('parent port must be defined');
}
return parentPort;
};
const signal$b = parentPort => {
parentPort.postMessage(readyMessage);
};
class IpcChildWithElectronUtilityProcess extends Ipc {
getData(event) {
return getUtilityProcessPortData(event);
}
send(message) {
this._rawIpc.postMessage(message);
}
sendAndTransfer(message) {
const {
newValue,
transfer
} = fixElectronParameters(message);
this._rawIpc.postMessage(newValue, transfer);
}
dispose() {
this._rawIpc.close();
}
onClose(callback) {
this._rawIpc.on('close', callback);
}
onMessage(callback) {
this._rawIpc.on('message', callback);
}
}
const wrap$i = parentPort => {
return new IpcChildWithElectronUtilityProcess(parentPort);
};
const IpcChildWithElectronUtilityProcess$1 = {
__proto__: null,
listen: listen$a,
signal: signal$b,
wrap: wrap$i
};
const getActualData = (message, handle) => {
if (handle) {
return {
...message,
params: [handle, ...message.params]
};
}
return message;
};
const getTransferrablesNode = value => {
const transferrables = getTransferrables(value);
if (transferrables.length === 0) {
throw new Error(`no transferrables found`);
}
return transferrables[0];
};
const listen$5 = async () => {
if (!process.send) {
throw new Error('process.send must be defined');
}
return process;
};
const signal$7 = process => {
process.send(readyMessage);
};
class IpcChildWithNodeForkedProcess extends Ipc {
getData(message, handle) {
return getActualData(message, handle);
}
onClose(callback) {
this._rawIpc.on('close', callback);
}
send(message) {
this._rawIpc.send(message);
}
onMessage(callback) {
this._rawIpc.on('message', callback);
}
sendAndTransfer(message) {
const transfer = getTransferrablesNode(message);
this._rawIpc.send(message, transfer);
}
dispose() {
// ignore
}
}
const wrap$d = process => {
return new IpcChildWithNodeForkedProcess(process);
};
const IpcChildWithNodeForkedProcess$1 = {
__proto__: null,
listen: listen$5,
signal: signal$7,
wrap: wrap$d
};
const listen$3 = async () => {
const {
parentPort
} = await import('node:worker_threads');
if (!parentPort) {
throw new IpcError('parentPort is required');
}
return parentPort;
};
const signal$5 = parentPort => {
parentPort.postMessage(readyMessage);
};
class IpcChildWithNodeWorker extends Ipc {
getData(data) {
return data;
}
onClose(callback) {
this._rawIpc.on('close', callback);
}
send(message) {
this._rawIpc.postMessage(message);
}
onMessage(callback) {
this._rawIpc.on('message', callback);
}
sendAndTransfer(message) {
const transfer = getTransferrablesNode(message);
this._rawIpc.postMessage(message, transfer);
}
dispose() {
this._rawIpc.close();
}
}
const wrap$b = parentPort => {
return new IpcChildWithNodeWorker(parentPort);
};
const IpcChildWithNodeWorker$1 = {
__proto__: null,
listen: listen$3,
signal: signal$5,
wrap: wrap$b
};
const Two = '2.0';
const create$4$1 = (method, params) => {
return {
jsonrpc: Two,
method,
params
};
};
const callbacks = Object.create(null);
const set = (id, fn) => {
callbacks[id] = fn;
};
const get = id => {
return callbacks[id];
};
const remove = id => {
delete callbacks[id];
};
let id = 0;
const create$3 = () => {
return ++id;
};
const registerPromise = () => {
const id = create$3();
const {
resolve,
promise
} = Promise.withResolvers();
set(id, resolve);
return {
id,
promise
};
};
const create$2 = (method, params) => {
const {
id,
promise
} = registerPromise();
const message = {
jsonrpc: Two,
method,
params,
id
};
return {
message,
promise
};
};
class JsonRpcError extends Error {
constructor(message) {
super(message);
this.name = 'JsonRpcError';
}
}
const NewLine = '\n';
const DomException = 'DOMException';
const ReferenceError$1 = 'ReferenceError';
const SyntaxError$1 = 'SyntaxError';
const TypeError$1 = 'TypeError';
const getErrorConstructor = (message, type) => {
if (type) {
switch (type) {
case DomException:
return DOMException;
case TypeError$1:
return TypeError;
case SyntaxError$1:
return SyntaxError;
case ReferenceError$1:
return ReferenceError;
default:
return Error;
}
}
if (message.startsWith('TypeError: ')) {
return TypeError;
}
if (message.startsWith('SyntaxError: ')) {
return SyntaxError;
}
if (message.startsWith('ReferenceError: ')) {
return ReferenceError;
}
return Error;
};
const constructError = (message, type, name) => {
const ErrorConstructor = getErrorConstructor(message, type);
if (ErrorConstructor === DOMException && name) {
return new ErrorConstructor(message, name);
}
if (ErrorConstructor === Error) {
const error = new Error(message);
if (name && name !== 'VError') {
error.name = name;
}
return error;
}
return new ErrorConstructor(message);
};
const getNewLineIndex = (string, startIndex = undefined) => {
return string.indexOf(NewLine, startIndex);
};
const getParentStack = error => {
let parentStack = error.stack || error.data || error.message || '';
if (parentStack.startsWith(' at')) {
parentStack = error.message + NewLine + parentStack;
}
return parentStack;
};
const joinLines = lines => {
return lines.join(NewLine);
};
const MethodNotFound = -32601;
const Custom = -32001;
const splitLines = lines => {
return lines.split(NewLine);
};
const restoreJsonRpcError = error => {
if (error && error instanceof Error) {
return error;
}
const currentStack = joinLines(splitLines(new Error().stack || '').slice(1));
if (error && error.code && error.code === MethodNotFound) {
const restoredError = new JsonRpcError(error.message);
const parentStack = getParentStack(error);
restoredError.stack = parentStack + NewLine + currentStack;
return restoredError;
}
if (error && error.message) {
const restoredError = constructError(error.message, error.type, error.name);
if (error.data) {
if (error.data.stack && error.data.type && error.message) {
restoredError.stack = error.data.type + ': ' + error.message + NewLine + error.data.stack + NewLine + currentStack;
} else if (error.data.stack) {
restoredError.stack = error.data.stack;
}
if (error.data.codeFrame) {
// @ts-ignore
restoredError.codeFrame = error.data.codeFrame;
}
if (error.data.code) {
// @ts-ignore
restoredError.code = error.data.code;
}
if (error.data.type) {
// @ts-ignore
restoredError.name = error.data.type;
}
} else {
if (error.stack) {
const lowerStack = restoredError.stack || '';
// @ts-ignore
const indexNewLine = getNewLineIndex(lowerStack);
const parentStack = getParentStack(error);
// @ts-ignore
restoredError.stack = parentStack + lowerStack.slice(indexNewLine);
}
if (error.codeFrame) {
// @ts-ignore
restoredError.codeFrame = error.codeFrame;
}
}
return restoredError;
}
if (typeof error === 'string') {
return new Error(`JsonRpc Error: ${error}`);
}
return new Error(`JsonRpc Error: ${error}`);
};
const unwrapJsonRpcResult = responseMessage => {
if ('error' in responseMessage) {
const restoredError = restoreJsonRpcError(responseMessage.error);
throw restoredError;
}
if ('result' in responseMessage) {
return responseMessage.result;
}
throw new JsonRpcError('unexpected response message');
};
const warn = (...args) => {
console.warn(...args);
};
const resolve = (id, response) => {
const fn = get(id);
if (!fn) {
console.log(response);
warn(`callback ${id} may already be disposed`);
return;
}
fn(response);
remove(id);
};
const E_COMMAND_NOT_FOUND = 'E_COMMAND_NOT_FOUND';
const getErrorType = prettyError => {
if (prettyError && prettyError.type) {
return prettyError.type;
}
if (prettyError && prettyError.constructor && prettyError.constructor.name) {
return prettyError.constructor.name;
}
return undefined;
};
const getErrorProperty = (error, prettyError) => {
if (error && error.code === E_COMMAND_NOT_FOUND) {
return {
code: MethodNotFound,
message: error.message,
data: error.stack
};
}
return {
code: Custom,
message: prettyError.message,
data: {
stack: prettyError.stack,
codeFrame: prettyError.codeFrame,
type: getErrorType(prettyError),
code: prettyError.code,
name: prettyError.name
}
};
};
const create$1 = (message, error) => {
return {
jsonrpc: Two,
id: message.id,
error
};
};
const getErrorResponse = (message, error, preparePrettyError, logError) => {
const prettyError = preparePrettyError(error);
logError(error, prettyError);
const errorProperty = getErrorProperty(error, prettyError);
return create$1(message, errorProperty);
};
const create = (message, result) => {
return {
jsonrpc: Two,
id: message.id,
result: result ?? null
};
};
const getSuccessResponse = (message, result) => {
const resultProperty = result ?? null;
return create(message, resultProperty);
};
const getResponse$1 = async (message, ipc, execute, preparePrettyError, logError, requiresSocket) => {
try {
const result = requiresSocket(message.method) ? await execute(message.method, ipc, ...message.params) : await execute(message.method, ...message.params);
return getSuccessResponse(message, result);
} catch (error) {
return getErrorResponse(message, error, preparePrettyError, logError);
}
};
const defaultPreparePrettyError = error => {
return error;
};
const defaultLogError = () => {
// ignore
};
const defaultRequiresSocket = () => {
return false;
};
const defaultResolve = resolve;
// TODO maybe remove this in v6 or v7, only accept options object to simplify the code
const normalizeParams = args => {
if (args.length === 1) {
const options = args[0];
return {
ipc: options.ipc,
message: options.message,
execute: options.execute,
resolve: options.resolve || defaultResolve,
preparePrettyError: options.preparePrettyError || defaultPreparePrettyError,
logError: options.logError || defaultLogError,
requiresSocket: options.requiresSocket || defaultRequiresSocket
};
}
return {
ipc: args[0],
message: args[1],
execute: args[2],
resolve: args[3],
preparePrettyError: args[4],
logError: args[5],
requiresSocket: args[6]
};
};
const handleJsonRpcMessage = async (...args) => {
const options = normalizeParams(args);
const {
message,
ipc,
execute,
resolve,
preparePrettyError,
logError,
requiresSocket
} = options;
if ('id' in message) {
if ('method' in message) {
const response = await getResponse$1(message, ipc, execute, preparePrettyError, logError, requiresSocket);
try {
ipc.send(response);
} catch (error) {
const errorResponse = getErrorResponse(message, error, preparePrettyError, logError);
ipc.send(errorResponse);
}
return;
}
resolve(message.id, message);
return;
}
if ('method' in message) {
await getResponse$1(message, ipc, execute, preparePrettyError, logError, requiresSocket);
return;
}
throw new JsonRpcError('unexpected message');
};
const invokeHelper = async (ipc, method, params, useSendAndTransfer) => {
const {
message,
promise
} = create$2(method, params);
if (useSendAndTransfer && ipc.sendAndTransfer) {
ipc.sendAndTransfer(message);
} else {
ipc.send(message);
}
const responseMessage = await promise;
return unwrapJsonRpcResult(responseMessage);
};
const send = (transport, method, ...params) => {
const message = create$4$1(method, params);
transport.send(message);
};
const invoke = (ipc, method, ...params) => {
return invokeHelper(ipc, method, params, false);
};
const invokeAndTransfer = (ipc, method, ...params) => {
return invokeHelper(ipc, method, params, true);
};
const commands = Object.create(null);
const register = commandMap => {
Object.assign(commands, commandMap);
};
const getCommand = key => {
return commands[key];
};
const execute = (command, ...args) => {
const fn = getCommand(command);
if (!fn) {
throw new Error(`command not found ${command}`);
}
return fn(...args);
};
const createRpc = ipc => {
const rpc = {
// @ts-ignore
ipc,
/**
* @deprecated
*/
send(method, ...params) {
send(ipc, method, ...params);
},
invoke(method, ...params) {
return invoke(ipc, method, ...params);
},
invokeAndTransfer(method, ...params) {
return invokeAndTransfer(ipc, method, ...params);
},
async dispose() {
await ipc?.dispose();
}
};
return rpc;
};
const requiresSocket = () => {
return false;
};
const preparePrettyError = error => {
return error;
};
const logError = () => {
// handled by renderer worker
};
const handleMessage = event => {
const actualRequiresSocket = event?.target?.requiresSocket || requiresSocket;
const actualExecute = event?.target?.execute || execute;
return handleJsonRpcMessage(event.target, event.data, actualExecute, resolve, preparePrettyError, logError, actualRequiresSocket);
};
const handleIpc = ipc => {
if ('addEventListener' in ipc) {
ipc.addEventListener('message', handleMessage);
} else if ('on' in ipc) {
// deprecated
ipc.on('message', handleMessage);
}
};
const listen$2 = async (module, options) => {
const rawIpc = await module.listen(options);
if (module.signal) {
module.signal(rawIpc);
}
const ipc = module.wrap(rawIpc);
return ipc;
};
const create$c = async ({
commandMap,
messagePort
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithElectronMessagePort$1, {
messagePort
});
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const ElectronMessagePortRpcClient = {
__proto__: null,
create: create$c
};
const create$b = async ({
commandMap
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithElectronUtilityProcess$1);
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const ElectronUtilityProcessRpcClient = {
__proto__: null,
create: create$b
};
const create$7 = async ({
commandMap
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithNodeForkedProcess$1);
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const NodeForkedProcessRpcClient = {
__proto__: null,
create: create$7
};
const create$4 = async ({
commandMap
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithNodeWorker$1);
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const NodeWorkerRpcClient = {
__proto__: null,
create: create$4
};
const RE_URL_MATCH = /^([a-z-]+):\/\/([a-z-.]+)/;
const getProtocolMatch = url => {
// TODO maybe use URL to parse the url
const match = url.match(RE_URL_MATCH);
if (!match) {
throw new Error(`Failed to parse url`);
}
return {
protocol: match[1],
domain: match[2]
};
};
// TODO make scheme dynamic
const allowedProtocols = ['lvce-webview', 'lvce-oss-webview'];
const getInfo = url => {
const {
protocol,
domain
} = getProtocolMatch(url);
if (!allowedProtocols.includes(protocol)) {
throw new Error(`unsupported protocol`);
}
const item = get$2(domain);
if (!item) {
throw new Error(`webview info not found`);
}
return item;
};
const getPathName = request => {
const {
pathname
} = new URL(request.url || '', `https://${request.headers.host}`);
return pathname;
};
const getPathName2 = url => {
try {
const p = new URL(url).pathname;
return p;
} catch {
return '';
}
};
const SPECIAL_CASES = {
etag: 'ETag'
};
const toTitleCase = key => {
const lowerKey = key.toLowerCase();
if (SPECIAL_CASES[lowerKey]) {
return SPECIAL_CASES[lowerKey];
}
return key.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join('-');
};
const serializeHeaders = headers => {
const result = {};
for (const [key, value] of headers.entries()) {
const normalizedKey = toTitleCase(key);
result[normalizedKey] = value;
}
return result;
};
const serializeResponse = async response => {
const body = await response.arrayBuffer();
return {
body: Buffer.from(body),
init: {
status: response.status,
headers: serializeHeaders(response.headers)
}
};
};
const getResponse = async (method, url, headers) => {
const info = getInfo(url);
let pathName = getPathName2(url);
if (pathName === '/') {
pathName += 'index.html';
}
const requestOptions = {
method,
path: pathName,
headers: headers || {}
};
const handlerOptions = {
contentSecurityPolicy: info.contentSecurityPolicy,
iframeContent: info.iframeContent,
stream: false,
webViewRoot: info.webViewRoot,
etag: false,
remotePathPrefix: '/remote'
};
const jsResponse = await getResponse$2(requestOptions, handlerOptions);
const serializedResponse = await serializeResponse(jsResponse);
return serializedResponse;
};
const commandMap$1 = {
'WebViewProtocol.getResponse': getResponse
};
const handleElectronMessagePort = async messagePort => {
await ElectronMessagePortRpcClient.create({
messagePort,
commandMap: commandMap$1
});
};
const setInfo2 = options => {
set$2(options.webViewId, options);
};
const setInfo = (id, webViewId, webViewUri, contentSecurityPolicy, iframeContent) => {
// TODO set webviewroot and webviewUri
let webViewRoot = webViewUri;
if (webViewRoot.startsWith('file://')) {
webViewRoot = fileURLToPath(webViewRoot).toString();
}
set$2(webViewId, {
webViewId,
webViewRoot,
contentSecurityPolicy,
iframeContent
});
};
const createHandler = (webViewRoot, contentSecurityPolicy, iframeContent) => {
if (webViewRoot && webViewRoot.startsWith('file://')) {
webViewRoot = fileURLToPath(webViewRoot);
}
// TODO configuration can be added via setInfo. then the request handler doesn't need to be a closure,
// but instead can retrieve the info from infoState (matching by request protocol / request url)
const handleRequest = async (request, response) => {
let pathName = getPathName(request);
if (pathName === '/') {
pathName += 'index.html';
}
const requestOptions = {
method: request.method || 'GET',
path: pathName,
headers: request.headers
};
const handlerOptions = {
webViewRoot,
contentSecurityPolicy,
iframeContent,
stream: false,
etag: true,
remotePathPrefix: '/remote'
};
const result = await getResponse$2(requestOptions, handlerOptions);
await sendResponse(response, result);
};
return handleRequest;
};
const setWebViewServerHandler = (id, frameAncestors, webViewRoot, contentSecurityPolicy, iframeContent) => {
const server = get$1(id);
const handler = createHandler(webViewRoot, contentSecurityPolicy, iframeContent);
server.setHandler(handler);
};
const addListener = (emitter, type, callback) => {
emitter.on(type, callback);
};
const removeListener = (emitter, type, callback) => {
emitter.off(type, callback);
};
const getFirstEvent = (eventEmitter, eventMap) => {
const {
resolve,
promise
} = Promise.withResolvers();
const listenerMap = Object.create(null);
const cleanup = value => {
for (const event of Object.keys(eventMap)) {
removeListener(eventEmitter, event, listenerMap[event]);
}
resolve(value);
};
for (const [event, type] of Object.entries(eventMap)) {
const listener = event => {
cleanup({
type,
event
});
};
addListener(eventEmitter, event, listener);
listenerMap[event] = listener;
}
return promise;
};
const waitForServerToBeReady = async (server, port) => {
const responsePromise = getFirstEvent(server, {
error: 1,
listening: 2
});
server.listen(port, () => {});
const {
type,
event
} = await responsePromise;
if (type === 1) {
throw new Error(`Server error: ${event}`);
}
};
const startWebViewServer = async (id, port) => {
try {
const server = get$1(id);
if (server.isListening()) {
return;
}
await waitForServerToBeReady(server, port);
} catch (error) {
throw new VError(error, 'Failed to start webview server');
}
};
const commandMap = {
'HandleElectronMessagePort.handleElectronMessagePort': handleElectronMessagePort,
'WebViewServer.create': createWebViewServer,
'WebViewServer.setHandler': setWebViewServerHandler,
'WebViewServer.setInfo': setInfo,
'WebViewServer.setInfo2': setInfo2,
'WebViewServer.start': startWebViewServer
};
const NodeWorker = 1;
const NodeForkedProcess = 2;
const ElectronUtilityProcess = 3;
const ElectronMessagePort = 4;
const Auto = () => {
const {
argv
} = process;
if (argv.includes('--ipc-type=node-worker')) {
return NodeWorker;
}
if (argv.includes('--ipc-type=node-forked-process')) {
return NodeForkedProcess;
}
if (argv.includes('--ipc-type=electron-utility-process')) {
return ElectronUtilityProcess;
}
throw new Error('[preview-process] unknown ipc type');
};
const getModule = method => {
switch (method) {
case NodeForkedProcess:
return NodeForkedProcessRpcClient.create;
case NodeWorker:
return NodeWorkerRpcClient.create;
case ElectronUtilityProcess:
return ElectronUtilityProcessRpcClient.create;
case ElectronMessagePort:
return ElectronMessagePortRpcClient.create;
default:
throw new Error('unexpected ipc type');
}
};
const listen$1 = async ({
method,
...params
}) => {
const fn = getModule(method);
// @ts-ignore
const rpc = await fn(params);
// @ts-ignore
return rpc;
};
const listen = async () => {
await listen$1({
method: Auto(),
commandMap: commandMap
});
};
const main = async () => {
await listen();
};
main();