UNPKG

@lvce-editor/preview-process

Version:
1,808 lines (1,735 loc) 46.6 kB
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();