-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
293 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import * as Character from '../Character/Character.js' | ||
|
||
export const addSemicolon = (line) => { | ||
return line + Character.SemiColon | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export const Backslash = '\\' | ||
export const Dash = '-' | ||
export const Dot = '.' | ||
export const EmptyString = '' | ||
export const NewLine = '\n' | ||
export const OpenAngleBracket = '<' | ||
export const Slash = '/' | ||
export const Space = ' ' | ||
export const Tab = '\t' | ||
export const Underline = '_' | ||
export const T = 't' | ||
export const SemiColon = ';' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
export const commandMap = {} | ||
import * as WebViewServer from '../WebViewServer/WebViewServer.js' | ||
|
||
export const commandMap = { | ||
'WebViewServer.start': WebViewServer.start, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { VError } from '@lvce-editor/verror' | ||
import { createServer } from 'node:http' | ||
import * as Promises from '../Promises/Promises.js' | ||
|
||
export const createWebViewServer = async (port) => { | ||
try { | ||
const server = createServer() | ||
const { resolve, promise } = Promises.withResolvers() | ||
server.listen(port, resolve) | ||
await promise | ||
return { | ||
handler: undefined, | ||
setHandler(handleRequest) { | ||
if (this.handler) { | ||
return | ||
} | ||
this.handler = this.handler | ||
server.on('request', handleRequest) | ||
}, | ||
} | ||
} catch (error) { | ||
throw new VError(error, `Failed to start webview server`) | ||
} | ||
} |
110 changes: 110 additions & 0 deletions
110
src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { createReadStream } from 'node:fs' | ||
import { extname } from 'node:path' | ||
import { pipeline } from 'node:stream/promises' | ||
import { fileURLToPath } from 'node:url' | ||
import * as GetContentSecurityPolicy from '../GetContentSecurityPolicy/GetContentSecurityPolicy.js' | ||
import * as SetHeaders from '../SetHeaders/SetHeaders.js' | ||
import * as PreviewInjectedCode from '../PreviewInjectedCode/PreviewInjectedCode.js' | ||
import { readFile } from 'node:fs/promises' | ||
|
||
const getPathName = (request) => { | ||
const { pathname } = new URL(request.url || '', `https://${request.headers.host}`) | ||
return pathname | ||
} | ||
|
||
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 injectPreviewScript = (html) => { | ||
const injectedCode = `<script type="module" src="/preview-injected.js"></script>\n` | ||
const titleEndIndex = html.indexOf('</title>') | ||
const newHtml = html.slice(0, titleEndIndex + '</title>'.length) + '\n' + injectedCode + html.slice(titleEndIndex) | ||
return newHtml | ||
} | ||
|
||
const handleIndexHtml = async (response, filePath, frameAncestors) => { | ||
try { | ||
const csp = GetContentSecurityPolicy.getContentSecurityPolicy([`default-src 'none'`, `script-src 'self'`, `frame-ancestors ${frameAncestors}`]) | ||
const contentType = getContentType(filePath) | ||
const content = await readFile(filePath, 'utf8') | ||
SetHeaders.setHeaders(response, { | ||
'Cross-Origin-Resource-Policy': 'cross-origin', | ||
'Cross-Origin-Embedder-Policy': 'require-corp', | ||
'Content-Security-Policy': csp, | ||
'Content-Type': contentType, | ||
}) | ||
const newContent = injectPreviewScript(content) | ||
response.end(newContent) | ||
} catch (error) { | ||
console.error(`[preview-server] ${error}`) | ||
} | ||
} | ||
|
||
const handleOther = async (response, filePath) => { | ||
try { | ||
const contentType = getContentType(filePath) | ||
// TODO figure out which of these headers are actually needed | ||
SetHeaders.setHeaders(response, { | ||
'Cross-Origin-Resource-Policy': 'cross-origin', | ||
'Cross-Origin-Embedder-Policy': 'require-corp', | ||
'Access-Control-Allow-Origin': '*', | ||
'Content-Type': contentType, | ||
}) | ||
await pipeline(createReadStream(filePath), response) | ||
} catch (error) { | ||
console.error(error) | ||
response.end(`[preview-server] ${error}`) | ||
} | ||
} | ||
|
||
const handlePreviewInjected = (response) => { | ||
try { | ||
const injectedCode = PreviewInjectedCode.injectedCode | ||
const contentType = getContentType('/test/file.js') | ||
SetHeaders.setHeaders(response, { | ||
'Content-Type': contentType, | ||
}) | ||
response.end(injectedCode) | ||
} catch (error) { | ||
console.error(`[preview-server] ${error}`) | ||
} | ||
} | ||
|
||
export const createHandler = (frameAncestors, webViewRoot) => { | ||
const handleRequest = async (request, response) => { | ||
let pathName = getPathName(request) | ||
if (pathName === '/') { | ||
pathName += 'index.html' | ||
} | ||
const filePath = fileURLToPath(`file://${webViewRoot}${pathName}`) | ||
const isHtml = filePath.endsWith('index.html') | ||
if (isHtml) { | ||
return handleIndexHtml(response, filePath, frameAncestors) | ||
} | ||
if (filePath.endsWith('preview-injected.js')) { | ||
return handlePreviewInjected(response) | ||
} | ||
return handleOther(response, filePath) | ||
} | ||
return handleRequest | ||
} |
6 changes: 6 additions & 0 deletions
6
src/parts/GetContentSecurityPolicy/GetContentSecurityPolicy.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as Character from '../Character/Character.js' | ||
import * as AddSemiColon from '../AddSemiColon/AddSemiColon.js' | ||
|
||
export const getContentSecurityPolicy = (items) => { | ||
return items.map(AddSemiColon.addSemicolon).join(Character.Space) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
export const injectedCode = ` | ||
let commandMap = {} | ||
let port | ||
const callbacks = Object.create(null) | ||
const isJsonRpcResponse = message => { | ||
return 'result' in message || 'error' in message | ||
} | ||
const handleMessage = async (event) => { | ||
const message = event.data | ||
if(isJsonRpcResponse(message)){ | ||
const fn = callbacks[message.id] | ||
fn(message.result) | ||
return | ||
} | ||
const { method, params } = message | ||
const fn = commandMap[method] | ||
if(!fn){ | ||
throw new Error("command not found \${method}") | ||
} | ||
const result = await fn(...params) | ||
} | ||
const handleFirstMessage = (event) => { | ||
const message = event.data | ||
port = message.params[0] | ||
port.onmessage = handleMessage | ||
port.postMessage('ready') | ||
} | ||
window.addEventListener('message', handleFirstMessage, { | ||
once: true, | ||
}) | ||
const withResolvers = () => { | ||
let _resolve | ||
const promise = new Promise(resolve => { | ||
_resolve = resolve | ||
}) | ||
return { | ||
resolve: _resolve, | ||
promise | ||
} | ||
} | ||
const registerPromise = () => { | ||
const id = 1 | ||
const {resolve, promise} = withResolvers() | ||
callbacks[id] = { resolve } | ||
return { | ||
id, promise | ||
} | ||
} | ||
globalThis.lvceRpc = (value) => { | ||
commandMap = value | ||
return { | ||
async invoke(method, ...params){ | ||
const {id, promise } = registerPromise() | ||
port.postMessage({ | ||
jsonrpc: '2.0', | ||
id, | ||
method, | ||
params | ||
}) | ||
const response = await promise | ||
// TODO unwrap jsonrpc result | ||
return response | ||
} | ||
} | ||
} | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export const withResolvers = () => { | ||
/** | ||
* @type {any} | ||
*/ | ||
let _resolve | ||
/** | ||
* @type {any} | ||
*/ | ||
let _reject | ||
const promise = new Promise((resolve, reject) => { | ||
_resolve = resolve | ||
_reject = reject | ||
}) | ||
return { | ||
resolve: _resolve, | ||
reject: _reject, | ||
promise, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const setHeaders = (response, headers) => { | ||
for (const [key, value] of Object.entries(headers)) { | ||
response.setHeader(key, value) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as WebViewServer from '../WebViewServer/WebViewServer.js' | ||
|
||
export const name = 'WebViewServer' | ||
|
||
export const Commands = { | ||
start: WebViewServer.start, | ||
setHandler: WebViewServer.setHandler, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as CreateWebViewServer from '../CreateWebViewServer/CreateWebViewServer.js' | ||
import * as CreateWebViewServerHandler from '../CreateWebViewServerHandler/CreateWebViewServerHandler.js' | ||
|
||
const state = { | ||
/** | ||
* @type {any } | ||
*/ | ||
promise: undefined, | ||
} | ||
|
||
// TODO move webview preview | ||
// server into separate process | ||
|
||
export const start = async (port) => { | ||
if (!state.promise) { | ||
state.promise = CreateWebViewServer.createWebViewServer(port) | ||
} | ||
return state.promise | ||
} | ||
|
||
export const setHandler = async (frameAncestors, webViewRoot) => { | ||
const server = await state.promise | ||
const handler = CreateWebViewServerHandler.createHandler(frameAncestors, webViewRoot) | ||
server.setHandler(handler) | ||
} |