@lvce-editor/shared-process 0.33.3 → 0.33.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/shared-process",
3
- "version": "0.33.3",
3
+ "version": "0.33.5",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -17,9 +17,9 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "dependencies": {
20
- "@lvce-editor/assert": "^1.2.0",
21
- "@lvce-editor/extension-host-helper-process": "0.33.3",
22
- "@lvce-editor/ipc": "^10.0.4",
20
+ "@lvce-editor/assert": "^1.3.0",
21
+ "@lvce-editor/extension-host-helper-process": "0.33.5",
22
+ "@lvce-editor/ipc": "^10.2.1",
23
23
  "@lvce-editor/json-rpc": "^3.0.0",
24
24
  "@lvce-editor/jsonc-parser": "^1.4.0",
25
25
  "@lvce-editor/pretty-error": "^1.6.0",
@@ -35,6 +35,7 @@
35
35
  "@lvce-editor/typescript-compile-process": "^1.2.0",
36
36
  "@lvce-editor/pty-host": "^2.0.0",
37
37
  "@lvce-editor/embeds-process": "^1.5.0",
38
+ "@lvce-editor/preview-process": "^2.7.0",
38
39
  "open": "^10.1.0",
39
40
  "tail": "^2.2.6",
40
41
  "tmp-promise": "^3.0.3",
@@ -1,22 +1,35 @@
1
1
  import * as GetContentSecurityPolicy from '../GetContentSecurityPolicy/GetContentSecurityPolicy.js';
2
+ import * as GetGitpodPreviewUrl from '../GetGitpodPreviewUrl/GetGitpodPreviewUrl.js';
2
3
  import * as IsElectron from '../IsElectron/IsElectron.js';
3
4
  import * as IsGitpod from '../IsGitpod/IsGitpod.js';
4
- const getGitpodPreviewUrl = (port) => {
5
- const workspaceId = process.env.GITPOD_WORKSPACE_ID;
6
- const cluster = process.env.GITPOD_WORKSPACE_CLUSTER_HOST;
7
- const url = `https://${port}-${workspaceId}.${cluster}`;
8
- return url;
5
+ import * as Scheme from '../Scheme/Scheme.js';
6
+ const getFrameSrc = () => {
7
+ // TODO make ports configurable
8
+ if (IsGitpod.isGitpod) {
9
+ return [`frame-src 'self' ${GetGitpodPreviewUrl.getGitpodPreviewUrl(3001)} ${GetGitpodPreviewUrl.getGitpodPreviewUrl(3002)}`];
10
+ }
11
+ if (IsElectron.isElectron) {
12
+ return [`frame-src ${Scheme.WebView}:`];
13
+ }
14
+ return [`frame-src 'self' http://localhost:3001 http://localhost:3002`];
15
+ };
16
+ const getManifestSrc = () => {
17
+ if (IsElectron.isElectron) {
18
+ return [];
19
+ }
20
+ return [`manifest-src 'self'`];
21
+ };
22
+ const getFrameAncestors = () => {
23
+ return [`frame-ancestors 'none'`];
9
24
  };
10
25
  export const value = GetContentSecurityPolicy.getContentSecurityPolicy([
11
26
  `default-src 'none'`,
12
27
  `font-src 'self'`,
13
- `img-src 'self' https: data:`,
28
+ `img-src 'self' https: data:`, // TODO maybe disallow https and data images
14
29
  `media-src 'self'`,
15
30
  `script-src 'self'`,
16
31
  `style-src 'self'`,
17
- `frame-ancestors 'none' `,
18
- IsGitpod.isGitpod
19
- ? `frame-src 'self' ${getGitpodPreviewUrl(3001)} ${getGitpodPreviewUrl(3002)}`
20
- : `frame-src 'self' http://localhost:3001 http://localhost:3002`,
21
- ...(IsElectron.isElectron ? [] : [`manifest-src 'self'`]),
32
+ ...getFrameAncestors(),
33
+ ...getFrameSrc(),
34
+ ...getManifestSrc(),
22
35
  ]);
@@ -1,4 +1,4 @@
1
- import { stat } from 'fs/promises';
1
+ import { stat } from 'node:fs/promises';
2
2
  import * as GetElectronFileResponseAbsolutePath from '../GetElectronFileResponseAbsolutePath/GetElectronFileResponseAbsolutePath.js';
3
3
  import * as GetElectronFileResponseContent from '../GetElectronFileResponseContent/GetElectronFileResponseContent.js';
4
4
  import * as GetElectronFileResponseRelativePath from '../GetElectronFileResponseRelativePath/GetElectronFileResponseRelativePath.js';
@@ -7,27 +7,35 @@ import * as GetHeaders from '../GetHeaders/GetHeaders.js';
7
7
  import * as GetNotFoundResponse from '../GetNotFoundResponse/GetNotFoundResponse.js';
8
8
  import * as GetNotModifiedResponse from '../GetNotModifiedResponse/GetNotModifiedResponse.js';
9
9
  import * as GetServerErrorResponse from '../GetServerErrorResponse/GetServerErrorResponse.js';
10
+ import * as HttpHeader from '../HttpHeader/HttpHeader.js';
10
11
  import * as HttpStatusCode from '../HttpStatusCode/HttpStatusCode.js';
11
12
  import * as IsEnoentError from '../IsEnoentError/IsEnoentError.js';
12
13
  import * as Logger from '../Logger/Logger.js';
14
+ // TODO maybe handle app responses and webview responses separately
15
+ // maybe send webview requests directly to preview process
13
16
  export const getElectronFileResponse = async (url, request) => {
14
17
  try {
15
18
  const pathName = GetElectronFileResponseRelativePath.getElectronFileResponseRelativePath(url);
16
- const absolutePath = GetElectronFileResponseAbsolutePath.getElectronFileResponseAbsolutePath(pathName);
19
+ let absolutePath = GetElectronFileResponseAbsolutePath.getElectronFileResponseAbsolutePath(pathName);
17
20
  let etag;
21
+ // TODO when is there no request?
18
22
  if (request) {
19
- const stats = await stat(absolutePath);
23
+ let stats = await stat(absolutePath);
24
+ if (stats.isDirectory()) {
25
+ absolutePath += '/index.html';
26
+ stats = await stat(absolutePath);
27
+ }
20
28
  etag = GetEtag.getEtag(stats);
21
- if (request.headers['if-none-match'] === etag) {
29
+ if (request.headers[HttpHeader.IfNotMatch] === etag) {
22
30
  const headers = GetHeaders.getHeaders(absolutePath, pathName);
23
31
  return GetNotModifiedResponse.getNotModifiedResponse(headers);
24
32
  }
25
33
  }
26
34
  const content = await GetElectronFileResponseContent.getElectronFileResponseContent(request, absolutePath, url);
27
35
  const headers = GetHeaders.getHeaders(absolutePath, pathName);
28
- headers['Cache-Control'] = 'public, max-age=0, must-revalidate';
36
+ headers[HttpHeader.CacheControl] = 'public, max-age=0, must-revalidate';
29
37
  if (etag) {
30
- headers['Etag'] = etag;
38
+ headers[HttpHeader.Etag] = etag;
31
39
  }
32
40
  return {
33
41
  body: content,
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import * as Path from '../Path/Path.js';
5
5
  import * as Platform from '../Platform/Platform.js';
6
6
  import * as Root from '../Root/Root.js';
7
+ // TODO clean up this code
7
8
  export const getElectronFileResponseAbsolutePath = (pathName) => {
8
9
  // TODO remove if/else in prod (use replacement)
9
10
  if (pathName === `/` || pathName.startsWith(`/?`)) {
@@ -30,5 +31,9 @@ export const getElectronFileResponseAbsolutePath = (pathName) => {
30
31
  const processExplorerThemeCss = join(tmpdir(), 'process-explorer-theme.css');
31
32
  return processExplorerThemeCss;
32
33
  }
34
+ // TODO move this to preview process
35
+ if (pathName.startsWith('/home')) {
36
+ return pathName;
37
+ }
33
38
  return Path.join(Root.root, 'static', pathName);
34
39
  };
@@ -1,12 +1,30 @@
1
1
  import * as Platform from '../Platform/Platform.js';
2
+ import * as Scheme from '../Scheme/Scheme.js';
2
3
  const { scheme } = Platform;
3
4
  const prefix = `${scheme}://-`;
4
5
  const prefixLength = prefix.length;
6
+ const webViewPrefix = `${Scheme.WebView}://-`;
7
+ const webViewPrefixLength = webViewPrefix.length;
5
8
  export const getElectronFileResponseRelativePath = (requestUrl) => {
9
+ if (requestUrl.startsWith(prefix)) {
10
+ // TODO only support reading from app root and extensions
11
+ // maybe add a separate protocol for extensions (e.g. lvce-remote)
12
+ const decoded = decodeURI(requestUrl);
13
+ const pathName = decoded.slice(prefixLength);
14
+ return pathName;
15
+ }
16
+ if (requestUrl.startsWith(webViewPrefix)) {
17
+ // TODO group this by extension so that
18
+ // extension-a can only access resources in extension-a folder and
19
+ // extension-b can only access resources in extension-b folder
20
+ const decoded = decodeURI(requestUrl);
21
+ const pathName = decoded.slice(webViewPrefixLength);
22
+ return pathName;
23
+ }
6
24
  if (requestUrl.startsWith('/')) {
25
+ // TODO this should not be supported anymore and could probably be removed
7
26
  return requestUrl;
8
27
  }
9
- const decoded = decodeURI(requestUrl);
10
- const pathName = decoded.slice(prefixLength);
11
- return pathName;
28
+ console.log({ requestUrl });
29
+ return '';
12
30
  };
@@ -0,0 +1,6 @@
1
+ export const getGitpodPreviewUrl = (port) => {
2
+ const workspaceId = process.env.GITPOD_WORKSPACE_ID;
3
+ const cluster = process.env.GITPOD_WORKSPACE_CLUSTER_HOST;
4
+ const url = `https://${port}-${workspaceId}.${cluster}`;
5
+ return url;
6
+ };
@@ -0,0 +1,23 @@
1
+ import * as HandleIpc from '../HandleIpc/HandleIpc.js';
2
+ import * as IpcParent from '../IpcParent/IpcParent.js';
3
+ import * as IpcParentType from '../IpcParentType/IpcParentType.js';
4
+ import * as IsElectron from '../IsElectron/IsElectron.js';
5
+ import * as PreviewProcessPath from '../PreviewProcessPath/PreviewProcessPath.js';
6
+ const getMethod = (isElectron) => {
7
+ if (isElectron) {
8
+ return IpcParentType.ElectronUtilityProcess;
9
+ }
10
+ return IpcParentType.NodeForkedProcess;
11
+ };
12
+ export const launchPreviewProcess = async () => {
13
+ const method = getMethod(IsElectron.isElectron);
14
+ const previewProcess = await IpcParent.create({
15
+ method,
16
+ path: PreviewProcessPath.previewProcessPath,
17
+ argv: [],
18
+ stdio: 'inherit',
19
+ name: 'Preview Process',
20
+ });
21
+ HandleIpc.handleIpc(previewProcess);
22
+ return previewProcess;
23
+ };
@@ -279,8 +279,10 @@ export const getModuleId = (commandId) => {
279
279
  return ModuleId.FileWatcher;
280
280
  case 'Transferrable.transfer':
281
281
  return ModuleId.Transferrable;
282
+ case 'WebViewServer.create':
282
283
  case 'WebViewServer.start':
283
284
  case 'WebViewServer.setHandler':
285
+ case 'WebViewServer.registerProtocol':
284
286
  return ModuleId.WebViewServer;
285
287
  default:
286
288
  throw new CommandNotFoundError(commandId);
@@ -41,9 +41,9 @@ export const getAppImageName = () => {
41
41
  export const getSetupName = () => {
42
42
  return 'Lvce-Setup';
43
43
  };
44
- export const version = '0.33.3';
45
- export const commit = '3581a11';
46
- export const date = '2024-08-26T21:23:31.000Z';
44
+ export const version = '0.33.5';
45
+ export const commit = 'aee872d';
46
+ export const date = '2024-09-01T07:12:00.000Z';
47
47
  export const getVersion = () => {
48
48
  return version;
49
49
  };
@@ -0,0 +1,22 @@
1
+ import * as JsonRpc from '../JsonRpc/JsonRpc.js';
2
+ import * as LaunchPreviewProcess from '../LaunchPreviewProcess/LaunchPreviewProcess.js';
3
+ export const state = {
4
+ /**
5
+ * @type {any}
6
+ */
7
+ ipc: undefined,
8
+ };
9
+ export const getOrCreate = async () => {
10
+ if (!state.ipc) {
11
+ state.ipc = LaunchPreviewProcess.launchPreviewProcess();
12
+ }
13
+ return state.ipc;
14
+ };
15
+ export const invoke = async (method, ...params) => {
16
+ const ipc = await getOrCreate();
17
+ return JsonRpc.invoke(ipc, method, ...params);
18
+ };
19
+ export const invokeAndTransfer = async (method, ...params) => {
20
+ const ipc = await getOrCreate();
21
+ return JsonRpc.invokeAndTransfer(ipc, method, ...params);
22
+ };
@@ -0,0 +1,2 @@
1
+ import * as ResolveBin from '../ResolveBin/ResolveBin.js';
2
+ export const previewProcessPath = ResolveBin.resolveBin('@lvce-editor/preview-process');
@@ -0,0 +1 @@
1
+ export const WebView = 'lvce-oss-webview';
@@ -2,5 +2,7 @@ import * as WebViewServer from '../WebViewServer/WebViewServer.js';
2
2
  export const name = 'WebViewServer';
3
3
  export const Commands = {
4
4
  start: WebViewServer.start,
5
+ create: WebViewServer.create,
5
6
  setHandler: WebViewServer.setHandler,
7
+ registerProtocol: WebViewServer.registerProtocol,
6
8
  };
@@ -1,21 +1,26 @@
1
- import * as CreateWebViewServer from '../CreateWebViewServer/CreateWebViewServer.js';
2
- import * as CreateWebViewServerHandler from '../CreateWebViewServerHandler/CreateWebViewServerHandler.js';
3
- const state = {
4
- /**
5
- * @type {any }
6
- */
7
- promise: undefined,
1
+ import * as PreviewProcess from '../PreviewProcess/PreviewProcess.js';
2
+ import * as ParentIpc from '../ParentIpc/ParentIpc.js';
3
+ import * as GetPortTuple from '../GetPortTuple/GetPortTuple.js';
4
+ export const registerProtocol = async () => {
5
+ // TODO only do this once
6
+ // TODO send messageport to proview process
7
+ // TODO send other messageport to parent process
8
+ console.time('get-ports');
9
+ const { port1, port2 } = await GetPortTuple.getPortTuple();
10
+ console.timeEnd('get-ports');
11
+ console.time('register-protocol');
12
+ await ParentIpc.invokeAndTransfer('ElectronSession.registerWebviewProtocol', port1);
13
+ console.timeEnd('register-protocol');
14
+ console.time('handle-port');
15
+ await PreviewProcess.invokeAndTransfer('HandleElectronMessagePort.handleElectronMessagePort', port2);
16
+ console.timeEnd('handle-port');
8
17
  };
9
- // TODO move webview preview
10
- // server into separate process
11
- export const start = async (port) => {
12
- if (!state.promise) {
13
- state.promise = CreateWebViewServer.createWebViewServer(port);
14
- }
15
- return state.promise;
18
+ export const create = async (previewId) => {
19
+ await PreviewProcess.invoke('WebViewServer.create', previewId);
16
20
  };
17
- export const setHandler = async (frameAncestors, webViewRoot) => {
18
- const server = await state.promise;
19
- const handler = CreateWebViewServerHandler.createHandler(frameAncestors, webViewRoot);
20
- server.setHandler(handler);
21
+ export const start = async (previewId, port) => {
22
+ await PreviewProcess.invoke('WebViewServer.start', previewId, port);
23
+ };
24
+ export const setHandler = async (previewId, frameAncestors, webViewRoot) => {
25
+ await PreviewProcess.invoke('WebViewServer.setHandler', previewId, frameAncestors, webViewRoot);
21
26
  };
@@ -1,24 +0,0 @@
1
- import { VError } from '@lvce-editor/verror';
2
- import { createServer } from 'node:http';
3
- import * as Promises from '../Promises/Promises.js';
4
- export const createWebViewServer = async (port) => {
5
- try {
6
- const server = createServer();
7
- const { resolve, promise } = Promises.withResolvers();
8
- server.listen(port, resolve);
9
- await promise;
10
- return {
11
- handler: undefined,
12
- setHandler(handleRequest) {
13
- if (this.handler) {
14
- return;
15
- }
16
- this.handler = this.handler;
17
- server.on('request', handleRequest);
18
- },
19
- };
20
- }
21
- catch (error) {
22
- throw new VError(error, `Failed to start webview server`);
23
- }
24
- };
@@ -1,105 +0,0 @@
1
- import { createReadStream } from 'node:fs';
2
- import { extname } from 'node:path';
3
- import { pipeline } from 'node:stream/promises';
4
- import { fileURLToPath } from 'node:url';
5
- import * as GetContentSecurityPolicy from '../GetContentSecurityPolicy/GetContentSecurityPolicy.js';
6
- import * as SetHeaders from '../SetHeaders/SetHeaders.js';
7
- import * as PreviewInjectedCode from '../PreviewInjectedCode/PreviewInjectedCode.js';
8
- import { readFile } from 'node:fs/promises';
9
- const getPathName = (request) => {
10
- const { pathname } = new URL(request.url || '', `https://${request.headers.host}`);
11
- return pathname;
12
- };
13
- const textMimeType = {
14
- '.html': 'text/html',
15
- '.js': 'text/javascript',
16
- '.ts': 'text/javascript',
17
- '.mjs': 'text/javascript',
18
- '.json': 'application/json',
19
- '.css': 'text/css',
20
- '.svg': 'image/svg+xml',
21
- '.avif': 'image/avif',
22
- '.woff': 'application/font-woff',
23
- '.ttf': 'font/ttf',
24
- '.png': 'image/png',
25
- '.jpe': 'image/jpg',
26
- '.ico': 'image/x-icon',
27
- '.jpeg': 'image/jpg',
28
- '.jpg': 'image/jpg',
29
- '.webp': 'image/webp',
30
- };
31
- const getContentType = (filePath) => {
32
- return textMimeType[extname(filePath)] || 'text/plain';
33
- };
34
- const injectPreviewScript = (html) => {
35
- const injectedCode = `<script type="module" src="/preview-injected.js"></script>\n`;
36
- const titleEndIndex = html.indexOf('</title>');
37
- const newHtml = html.slice(0, titleEndIndex + '</title>'.length) + '\n' + injectedCode + html.slice(titleEndIndex);
38
- return newHtml;
39
- };
40
- const handleIndexHtml = async (response, filePath, frameAncestors) => {
41
- try {
42
- const csp = GetContentSecurityPolicy.getContentSecurityPolicy([`default-src 'none'`, `script-src 'self'`, `frame-ancestors ${frameAncestors}`]);
43
- const contentType = getContentType(filePath);
44
- const content = await readFile(filePath, 'utf8');
45
- SetHeaders.setHeaders(response, {
46
- 'Cross-Origin-Resource-Policy': 'cross-origin',
47
- 'Cross-Origin-Embedder-Policy': 'require-corp',
48
- 'Content-Security-Policy': csp,
49
- 'Content-Type': contentType,
50
- });
51
- const newContent = injectPreviewScript(content);
52
- response.end(newContent);
53
- }
54
- catch (error) {
55
- console.error(`[preview-server] ${error}`);
56
- }
57
- };
58
- const handleOther = async (response, filePath) => {
59
- try {
60
- const contentType = getContentType(filePath);
61
- // TODO figure out which of these headers are actually needed
62
- SetHeaders.setHeaders(response, {
63
- 'Cross-Origin-Resource-Policy': 'cross-origin',
64
- 'Cross-Origin-Embedder-Policy': 'require-corp',
65
- 'Access-Control-Allow-Origin': '*',
66
- 'Content-Type': contentType,
67
- });
68
- await pipeline(createReadStream(filePath), response);
69
- }
70
- catch (error) {
71
- console.error(error);
72
- response.end(`[preview-server] ${error}`);
73
- }
74
- };
75
- const handlePreviewInjected = (response) => {
76
- try {
77
- const injectedCode = PreviewInjectedCode.injectedCode;
78
- const contentType = getContentType('/test/file.js');
79
- SetHeaders.setHeaders(response, {
80
- 'Content-Type': contentType,
81
- });
82
- response.end(injectedCode);
83
- }
84
- catch (error) {
85
- console.error(`[preview-server] ${error}`);
86
- }
87
- };
88
- export const createHandler = (frameAncestors, webViewRoot) => {
89
- const handleRequest = async (request, response) => {
90
- let pathName = getPathName(request);
91
- if (pathName === '/') {
92
- pathName += 'index.html';
93
- }
94
- const filePath = fileURLToPath(`file://${webViewRoot}${pathName}`);
95
- const isHtml = filePath.endsWith('index.html');
96
- if (isHtml) {
97
- return handleIndexHtml(response, filePath, frameAncestors);
98
- }
99
- if (filePath.endsWith('preview-injected.js')) {
100
- return handlePreviewInjected(response);
101
- }
102
- return handleOther(response, filePath);
103
- };
104
- return handleRequest;
105
- };
@@ -1,74 +0,0 @@
1
- export const injectedCode = `
2
- let commandMap = {}
3
- let port
4
- const callbacks = Object.create(null)
5
-
6
-
7
- const isJsonRpcResponse = message => {
8
- return 'result' in message || 'error' in message
9
- }
10
-
11
- const handleMessage = async (event) => {
12
- const message = event.data
13
- if(isJsonRpcResponse(message)){
14
- const fn = callbacks[message.id]
15
- fn(message.result)
16
- return
17
- }
18
- const { method, params } = message
19
- const fn = commandMap[method]
20
- if(!fn){
21
- throw new Error("command not found \${method}")
22
- }
23
- const result = await fn(...params)
24
- }
25
-
26
- const handleFirstMessage = (event) => {
27
- const message = event.data
28
- port = message.params[0]
29
- port.onmessage = handleMessage
30
- port.postMessage('ready')
31
- }
32
-
33
- window.addEventListener('message', handleFirstMessage, {
34
- once: true,
35
- })
36
-
37
- const withResolvers = () => {
38
- let _resolve
39
- const promise = new Promise(resolve => {
40
- _resolve = resolve
41
- })
42
- return {
43
- resolve: _resolve,
44
- promise
45
- }
46
- }
47
-
48
- const registerPromise = () => {
49
- const id = 1
50
- const {resolve, promise} = withResolvers()
51
- callbacks[id] = { resolve }
52
- return {
53
- id, promise
54
- }
55
- }
56
-
57
- globalThis.lvceRpc = (value) => {
58
- commandMap = value
59
- return {
60
- async invoke(method, ...params){
61
- const {id, promise } = registerPromise()
62
- port.postMessage({
63
- jsonrpc: '2.0',
64
- id,
65
- method,
66
- params
67
- })
68
- const response = await promise
69
- // TODO unwrap jsonrpc result
70
- return response
71
- }
72
- }
73
- }
74
- `;