diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e14d00b..d52f97d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: npm ci --ignore-scripts + - run: npm run check - run: npm run build - name: Publish package to NPM if: startsWith(github.ref, 'refs/tags/v') diff --git a/.gitignore b/.gitignore index f06235c..4241c17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +*~ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bef1c52..a4cf36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -404,8 +404,6 @@ release date when you use `npm version` (see `README.md`). [0.15.0]: https://github.com/webxdc/webxdc-dev/tree/v0.15.0 [unreleased]: https://github.com/webxdc/webxdc-dev/compare/v0.15.1...HEAD [0.15.1]: https://github.com/webxdc/webxdc-dev/tree/v0.15.1 - - [Unreleased]: https://github.com/webxdc/webxdc-dev/compare/v0.18.0...HEAD [0.18.0]: https://github.com/webxdc/webxdc-dev/compare/v0.17.0...v0.18.0 [0.17.0]: https://github.com/webxdc/webxdc-dev/compare/v0.16.0...v0.17.0 diff --git a/README.md b/README.md index d78db53..9939cc5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # webxdc-dev [![CI](https://github.com/webxdc/webxdc-dev/actions/workflows/ci.yml/badge.svg)](https://github.com/webxdc/webxdc-dev/actions/workflows/ci.yml) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) webxdc-dev is a development server for [webxdc apps](https://webxdc.org). Easily test your app's behavior as if it was shared in a real chat with @@ -8,7 +9,6 @@ multiple people. ![example screenshot](https://raw.githubusercontent.com/webxdc/webxdc-dev/main/screenshot.png) - In contrast to [hello](https://github.com/webxdc/hello), each "user"'s app instance gets its own isolated state (e.g. `localStorage`), since each is served from a separate port (therefore a separate origin). @@ -80,7 +80,7 @@ to see full information. You can also filter messages. There is also a "chat" tab which you can use to see `info` contained in updates as well as any `summary` text contained in an update. -The sidebar can be closed with `Close Messages` button within the sidebar and +The sidebar can be closed with `Close Messages` button within the sidebar and expanded by clicking on `Open Messages` within the devices tab. The sidebars width can also be adjusted by moving the separating line between devices and sidebar. diff --git a/TODO.md b/TODO.md index 5668d0d..f039eac 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,7 @@ ## Node version - Can I warn when the node version is too old upon installation. - + ## Messages - Perhaps default message filter should see connect & sent at the same time? diff --git a/backend/app.ts b/backend/app.ts index 7349a42..a0e1365 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -29,7 +29,7 @@ export function createFrontend( appInfo: AppInfo, instances: Instances, injectFrontend: InjectExpress, - getIndexHtml: () => string + getIndexHtml: () => string, ): expressWs.Application { const expressApp = express(); const wsInstance = expressWs(expressApp); @@ -57,9 +57,7 @@ export function createFrontend( res.send(appInfo.icon.buffer); }); app.get<{}, Instance[]>("/instances", (req, res) => { - res.json( - instances.list() - ); + res.json(instances.list()); }); app.post<{}, Instance>("/instances", (req, res) => { const instance = instances.add(); @@ -71,9 +69,9 @@ export function createFrontend( color: instance.color, }); }); - app.delete<{id: string}, Instance[]>("/instances/:id", (req, res) => { + app.delete<{ id: string }, Instance[]>("/instances/:id", (req, res) => { instances.delete(parseInt(req.params.id)); - res.json(instances.list()) + res.json(instances.list()); }); app.post<{}, { status: string }>("/clear", (req, res) => { @@ -126,7 +124,7 @@ export function createPeer(options: PeerOptions): expressWs.Instance { wsInstance.app.use((req, res, next) => { const contentSecurityPolicy = getContentSecurityPolicy( location, - options.instanceUrl + options.instanceUrl, ); res.setHeader("Content-Security-Policy", contentSecurityPolicy); next(); @@ -144,7 +142,7 @@ export function createPeer(options: PeerOptions): expressWs.Instance { createProxyMiddleware(filter, { target: location.url, ws: false, - }) + }), ); } else { // serve webxdc project from directory @@ -155,7 +153,7 @@ export function createPeer(options: PeerOptions): expressWs.Instance { function getContentSecurityPolicy( location: Location, - instanceUrl: string + instanceUrl: string, ): string { const connectSrcUrls = []; @@ -180,7 +178,7 @@ function getContentSecurityPolicy( return policy.replace( /connect-src (.*?);/, - `connect-src $1 ${connectSrcUrls.join(" ")} ;` + `connect-src $1 ${connectSrcUrls.join(" ")} ;`, ); } diff --git a/backend/appInfo.test.ts b/backend/appInfo.test.ts index d53067e..7c1807b 100644 --- a/backend/appInfo.test.ts +++ b/backend/appInfo.test.ts @@ -17,7 +17,7 @@ test("minimal directory app info", async () => { test("directory app info with manifest", async () => { const location = getLocation( - path.resolve(__dirname, "fixtures", "withManifest") + path.resolve(__dirname, "fixtures", "withManifest"), ); const appInfo = await getAppInfo(location); expect(appInfo.location).toEqual(location); @@ -31,7 +31,7 @@ test("directory app info with manifest", async () => { test("directory app info with manifest but no name entry", async () => { const location = getLocation( - path.resolve(__dirname, "fixtures", "withManifestWithoutName") + path.resolve(__dirname, "fixtures", "withManifestWithoutName"), ); const appInfo = await getAppInfo(location); expect(appInfo.location).toEqual(location); @@ -64,7 +64,7 @@ test("directory app info with manifest but no name entry", async () => { test("directory app info with jpg icon", async () => { const location = getLocation( - path.resolve(__dirname, "fixtures", "withJpgIcon") + path.resolve(__dirname, "fixtures", "withJpgIcon"), ); const appInfo = await getAppInfo(location); expect(appInfo.location).toEqual(location); @@ -83,7 +83,7 @@ test("directory app info with jpg icon", async () => { test("directory app info with png icon", async () => { const location = getLocation( - path.resolve(__dirname, "fixtures", "withPngIcon") + path.resolve(__dirname, "fixtures", "withPngIcon"), ); const appInfo = await getAppInfo(location); expect(appInfo.location).toEqual(location); @@ -210,7 +210,7 @@ test("url app info with broken manifest", async () => { } catch (e) { if (e instanceof AppInfoError) { expect(e.message).toEqual( - "Invalid manifest.toml, please check the format" + "Invalid manifest.toml, please check the format", ); } else { throw e; diff --git a/backend/appInfo.ts b/backend/appInfo.ts index c683686..75987b8 100644 --- a/backend/appInfo.ts +++ b/backend/appInfo.ts @@ -49,7 +49,7 @@ export async function getAppInfo(location: Location): Promise { export async function getAppInfoUrl( location: UrlLocation, - fetch: typeof nodeFetch + fetch: typeof nodeFetch, ): Promise { return { location, @@ -65,7 +65,7 @@ export function getToolVersion(): string { async function getManifestInfoFromUrl( url: string, - fetch: typeof nodeFetch + fetch: typeof nodeFetch, ): Promise { if (!url.endsWith("/")) { url = url + "/"; @@ -89,7 +89,7 @@ async function getManifestInfoFromUrl( async function getIconInfoFromUrl( url: string, - fetch: typeof nodeFetch + fetch: typeof nodeFetch, ): Promise { if (!url.endsWith("/")) { url = url + "/"; @@ -113,7 +113,7 @@ async function getIconInfoFromUrl( function getManifestInfoFromDir( dir: string, - fallbackName: string + fallbackName: string, ): ManifestInfo { const tomlBuffer = readFileBuffer(path.join(dir, "manifest.toml")); if (tomlBuffer === null) { @@ -165,7 +165,7 @@ function readFileBuffer(location: string): Buffer | null { async function readUrlBuffer( url: string, - fetch: typeof nodeFetch + fetch: typeof nodeFetch, ): Promise { const response = await fetch(url); if (!response.ok) { diff --git a/backend/fixtures/minimal/index.html b/backend/fixtures/minimal/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/minimal/index.html +++ b/backend/fixtures/minimal/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/fixtures/notXdcDir.xdc/index.html b/backend/fixtures/notXdcDir.xdc/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/notXdcDir.xdc/index.html +++ b/backend/fixtures/notXdcDir.xdc/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/fixtures/withJpgIcon/index.html b/backend/fixtures/withJpgIcon/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/withJpgIcon/index.html +++ b/backend/fixtures/withJpgIcon/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/fixtures/withManifest/index.html b/backend/fixtures/withManifest/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/withManifest/index.html +++ b/backend/fixtures/withManifest/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/fixtures/withManifestWithoutName/index.html b/backend/fixtures/withManifestWithoutName/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/withManifestWithoutName/index.html +++ b/backend/fixtures/withManifestWithoutName/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/fixtures/withPngIcon/index.html b/backend/fixtures/withPngIcon/index.html index 50a010e..a2728bd 100644 --- a/backend/fixtures/withPngIcon/index.html +++ b/backend/fixtures/withPngIcon/index.html @@ -1,3 +1,5 @@ - Hello, world! - \ No newline at end of file + + Hello, world! + + diff --git a/backend/instance.ts b/backend/instance.ts index 0741e6a..cb35791 100644 --- a/backend/instance.ts +++ b/backend/instance.ts @@ -7,7 +7,7 @@ import { Location } from "./location"; import { createPeer, InjectExpress } from "./app"; import { AppInfo } from "./appInfo"; import { getColorForId } from "./color"; -import { Instance as FrontendInstance } from '../types/instance'; +import { Instance as FrontendInstance } from "../types/instance"; export type Options = { basePort: number; @@ -39,7 +39,7 @@ class Instance { public app: expressWs.Application, public port: number, public url: string, - public webXdc: WebXdcMulti + public webXdc: WebXdcMulti, ) { this.id = port.toString(); this.color = getColorForId(this.id); @@ -52,7 +52,7 @@ class Instance { } close() { - this.server.close() + this.server.close(); } } @@ -107,7 +107,7 @@ export class Instances { app, port, instanceUrl, - this.processor.createClient(port.toString()) + this.processor.createClient(port.toString()), ); const wss = wsInstance.getWss(); @@ -117,7 +117,7 @@ export class Instances { ws.on("message", (msg: string) => { if (typeof msg !== "string") { console.error( - "webxdc: Don't know how to handle unexpected non-string data" + "webxdc: Don't know how to handle unexpected non-string data", ); return; } @@ -133,7 +133,7 @@ export class Instances { JSON.stringify({ type: "updates", updates: updates.map(([update]) => update), - }) + }), ); }, parsed.serial, @@ -152,7 +152,7 @@ export class Instances { name: this.appInfo.manifest.name, color: instance.color, }, - }) + }), ); } else { throw new Error(`Unknown message: ${JSON.stringify(parsed)}`); @@ -166,7 +166,9 @@ export class Instances { delete(id: number) { let instance = this.instances.get(id); if (instance == null) { - throw new Error(`Instance with id ${id} can't be deleted because it does not exist`); + throw new Error( + `Instance with id ${id} can't be deleted because it does not exist`, + ); } instance.close(); this.processor.removeClient(instance.id); @@ -187,13 +189,13 @@ export class Instances { this._onMessage = onMessage; } - list(): FrontendInstance[]{ + list(): FrontendInstance[] { return Array.from(this.instances.values()).map((instance) => ({ id: instance.id, port: instance.port, url: instance.url, color: instance.color, - })) + })); } } @@ -215,7 +217,7 @@ function isSendUpdateMessage(value: any): value is SendUpdateMessage { } function isSetUpdateListenerMessage( - value: any + value: any, ): value is SetUpdateListenerMessage { return value.type === "setUpdateListener"; } diff --git a/backend/location.ts b/backend/location.ts index f1a21eb..a431140 100644 --- a/backend/location.ts +++ b/backend/location.ts @@ -45,7 +45,7 @@ export function getLocation(location: string): Location { } if (!hasIndexHtml(path)) { throw new LocationError( - `Invalid xdc file (no index.html file inside): ${location}` + `Invalid xdc file (no index.html file inside): ${location}`, ); } return { @@ -65,7 +65,7 @@ export function getLocation(location: string): Location { if (!hasIndexHtml(location)) { throw new LocationError( - `Invalid xdc dir (no index.html file): ${location}` + `Invalid xdc dir (no index.html file): ${location}`, ); } return { diff --git a/backend/message.test.ts b/backend/message.test.ts index 164d85f..f6a1ec0 100644 --- a/backend/message.test.ts +++ b/backend/message.test.ts @@ -327,7 +327,7 @@ test("clear single client", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); // we always clear on first connection with a new processor expect(client0Cleared).toMatchObject(["cleared"]); @@ -343,7 +343,7 @@ test("clear single client", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared", "cleared"]); }); @@ -362,7 +362,7 @@ test("clear multiple clients", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); client1.connect( @@ -371,7 +371,7 @@ test("clear multiple clients", () => { () => { client1Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared"]); @@ -389,7 +389,7 @@ test("clear multiple clients", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared", "cleared"]); }); @@ -407,7 +407,7 @@ test("clear client that is created later", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared"]); @@ -422,7 +422,7 @@ test("clear client that is created later", () => { () => { client1Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared", "cleared"]); @@ -444,7 +444,7 @@ test("clear multiple clients, multiple times", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); client1.connect( @@ -453,7 +453,7 @@ test("clear multiple clients, multiple times", () => { () => { client1Cleared.push("cleared"); return true; - } + }, ); expect(client0Cleared).toMatchObject(["cleared"]); @@ -482,7 +482,7 @@ test("connect with clear means we get no catchup if no new updates", () => { () => { client0Heard.push("cleared"); return true; - } + }, ); client0.sendUpdate({ payload: "Hello" }, "update"); @@ -511,7 +511,7 @@ test("connect with clear means we get no catchup if no new updates", () => { () => { client1Heard.push("cleared"); return true; - } + }, ); expect(client0Heard).toMatchObject([ @@ -540,7 +540,7 @@ test("connect with clear means catchup only with updates after clear", () => { () => { client0Heard.push("cleared"); return true; - } + }, ); client0.sendUpdate({ payload: "Hello" }, "update"); @@ -572,7 +572,7 @@ test("connect with clear means catchup only with updates after clear", () => { () => { client1Heard.push("cleared"); return true; - } + }, ); expect(client0Heard).toMatchObject([ @@ -699,7 +699,7 @@ test("clear other client but was disconnected", () => { () => { client0Cleared.push("cleared"); return true; - } + }, ); client1.connect( @@ -708,7 +708,7 @@ test("clear other client but was disconnected", () => { () => { // we never got the clear return false; - } + }, ); expect(client0Cleared).toMatchObject(["cleared"]); diff --git a/backend/message.ts b/backend/message.ts index b6464fb..c41bcab 100644 --- a/backend/message.ts +++ b/backend/message.ts @@ -8,7 +8,7 @@ import type { Message } from "../types/message"; import { getColorForId } from "./color"; type UpdateListenerMulti = ( - updates: [ReceivedUpdate, string][] + updates: [ReceivedUpdate, string][], ) => boolean; type ClearListener = () => boolean; @@ -42,7 +42,10 @@ class Client implements WebXdcMulti { updateSerial: number | null = null; deleteListener: DeleteListener | null = null; - constructor(public processor: Processor, public id: string) {} + constructor( + public processor: Processor, + public id: string, + ) {} sendUpdate(update: Update, descr: string): void { this.processor.distribute(this.id, update, descr); @@ -124,10 +127,10 @@ class Client implements WebXdcMulti { // sends a message to the all clients to shut down delete() { - if ( this.deleteListener == null ) { + if (this.deleteListener == null) { return; } - this.deleteListener() + this.deleteListener(); } } @@ -186,7 +189,10 @@ class Processor implements IProcessor { updateListener( this.updates .slice(serial) - .map(([update, descr]) => [{ ...update, max_serial: maxSerial }, descr]) + .map(([update, descr]) => [ + { ...update, max_serial: maxSerial }, + descr, + ]), ); } } diff --git a/backend/program.ts b/backend/program.ts index 171a34e..10e07ee 100644 --- a/backend/program.ts +++ b/backend/program.ts @@ -25,28 +25,28 @@ export function createProgram(inject: Inject): Command { .command("run") .argument( "", - "URL with dev server, path to .xdc file, or path to webxdc dist directory" + "URL with dev server, path to .xdc file, or path to webxdc dist directory", ) .option( "-p, --port ", "start port for webxdc-dev UI, instance ports are incremented by one each", parsePort, - 7000 + 7000, ) .option("--no-csp", "run instances without CSP applied") .option( "-v, --verbose", "Print all messages sent and received by instances", - false + false, ) .description( - "Run webxdc-dev simulator with webxdc from dev server URL, .xdc file or dist directory" + "Run webxdc-dev simulator with webxdc from dev server URL, .xdc file or dist directory", ) .action((location, options) => { run( location, { basePort: options.port, csp: options.csp, verbose: options.verbose }, - inject + inject, ); }); return program; diff --git a/backend/run.ts b/backend/run.ts index 7996df2..2c06417 100644 --- a/backend/run.ts +++ b/backend/run.ts @@ -25,7 +25,7 @@ function actualRun(appInfo: AppInfo, options: Options, inject: Inject): void { appInfo, instances, injectFrontend, - getIndexHtml + getIndexHtml, ); frontend.listen(options.basePort, () => { diff --git a/backend/unpack.ts b/backend/unpack.ts index 30ad430..3365c83 100644 --- a/backend/unpack.ts +++ b/backend/unpack.ts @@ -17,7 +17,7 @@ export function withTempDir(fn: (tmpDir: string) => void) { fs.rmSync(tmpDir, { recursive: true }); } catch (e) { console.error( - `An error has occurred while removing the temp dir at ${tmpDir}. Error: ${e}` + `An error has occurred while removing the temp dir at ${tmpDir}. Error: ${e}`, ); } } diff --git a/frontend/App.tsx b/frontend/App.tsx index 3f12fa0..4180395 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -54,7 +54,12 @@ const App: Component = () => { Main Info - + diff --git a/frontend/InstanceHeader.tsx b/frontend/InstanceHeader.tsx index b19657f..ebd7d1b 100644 --- a/frontend/InstanceHeader.tsx +++ b/frontend/InstanceHeader.tsx @@ -1,5 +1,12 @@ import { Component, Show, createMemo, JSX, Accessor } from "solid-js"; -import { Flex, Text, Badge, Tooltip, IconButton, notificationService } from "@hope-ui/solid"; +import { + Flex, + Text, + Badge, + Tooltip, + IconButton, + notificationService, +} from "@hope-ui/solid"; import { IoRefreshOutline, IoStop, IoPlay } from "solid-icons/io"; import { FiExternalLink, FiTrash } from "solid-icons/fi"; @@ -20,14 +27,15 @@ const InstanceHeader: Component<{ }); const handleRemoveInstance = async (id: string) => { - let new_instances = await (await fetch(`/instances/${id}`, { method: "DELETE" })).json() + let new_instances = await ( + await fetch(`/instances/${id}`, { method: "DELETE" }) + ).json(); mutateInstances(new_instances); notificationService.show({ title: `Deleted instance ${id}`, }); }; - const receivedCount = createMemo(() => { return received(props.instance.id); }); @@ -39,7 +47,6 @@ const InstanceHeader: Component<{ return ( - - + - + - - + + ); diff --git a/frontend/Main.tsx b/frontend/Main.tsx index fe246c9..55b62a1 100644 --- a/frontend/Main.tsx +++ b/frontend/Main.tsx @@ -1,11 +1,5 @@ import { Component, For, createSignal, Setter, Show } from "solid-js"; -import { - Flex, - Box, - createDisclosure, - Heading, - Button, -} from "@hope-ui/solid"; +import { Flex, Box, createDisclosure, Heading, Button } from "@hope-ui/solid"; import { instances } from "./store"; import InstancesButtons from "./InstancesButtons"; @@ -29,9 +23,11 @@ const Main: Component = () => { return ( <> { - - + + Devices { onOpenMessages={onOpen} /> - + {(instance: InstanceData) => ( - + )} - }> + } + > @@ -66,11 +66,14 @@ const Main: Component = () => { onOpenMessages={() => {}} /> - + {(instance: InstanceData) => ( - + )} @@ -78,8 +81,13 @@ const Main: Component = () => { - Messages - + + Messages + + diff --git a/frontend/RecordRow.tsx b/frontend/RecordRow.tsx index 3177833..15dfcf1 100644 --- a/frontend/RecordRow.tsx +++ b/frontend/RecordRow.tsx @@ -2,7 +2,7 @@ import { Component, JSX } from "solid-js"; import { Tr, Td } from "@hope-ui/solid"; const RecordRow: Component<{ label: string; children: JSX.Element }> = ( - props + props, ) => { return ( diff --git a/frontend/SplitView.tsx b/frontend/SplitView.tsx index 7c5f368..3bbefa2 100644 --- a/frontend/SplitView.tsx +++ b/frontend/SplitView.tsx @@ -1,71 +1,94 @@ - import { Box } from "@hope-ui/solid"; import { Accessor } from "solid-js"; -import { createMemo, ParentComponent, Component, JSX, createSignal, children } from "solid-js"; +import { + createMemo, + ParentComponent, + Component, + JSX, + createSignal, + children, +} from "solid-js"; import { ResolvedJSXElement } from "solid-js/types/reactive/signal"; let x = 0; let initialLeftWidth = 0; -const SplitView: ParentComponent<{ -}> = (props) => { - - // Somehow using `HtmlElement` here causes a type error - let leftSide: any; - let rightSide: any; - let resizer: any; - - - const resolvedChildren = children(() => props.children) as Accessor; - - let [isResizing, setIsResizing] = createSignal(false); - let [leftWidth, setLeftWidth] = createSignal(60); - let leftStyleGet = createMemo(() => { - return { - width: leftWidth() + "%", - "user-select": isResizing() ? "none" : "user-select", - "pointer-event": isResizing() ? "none" : "pointer-events" - } - }) - let rightStyleGet = createMemo(() => { - return { - "user-select": isResizing() ? "none" : "user-select", - "pointer-event": isResizing() ? "none" : "pointer-events" - } - }) - let resizerStyleGet = createMemo(() => { - return isResizing() ? {cursor: 'col-resize'} : undefined - }) - +const SplitView: ParentComponent<{}> = (props) => { + // Somehow using `HtmlElement` here causes a type error + let leftSide: any; + let rightSide: any; + let resizer: any; - function mouseMoveHandler(e: MouseEvent) { - const dx = e.clientX - x; - setLeftWidth(((initialLeftWidth + dx) * 100) / (resizer.parentNode as HTMLElement).getBoundingClientRect().width) - document.body.style.cursor = 'col-resize'; - }; + const resolvedChildren = children(() => props.children) as Accessor< + ResolvedJSXElement[] + >; - function mouseUpHandler () { - setIsResizing(false); - document.body.style.removeProperty('cursor'); - document.removeEventListener('mousemove', mouseMoveHandler); - document.removeEventListener('mouseup', mouseUpHandler); + let [isResizing, setIsResizing] = createSignal(false); + let [leftWidth, setLeftWidth] = createSignal(60); + let leftStyleGet = createMemo(() => { + return { + width: leftWidth() + "%", + "user-select": isResizing() ? "none" : "user-select", + "pointer-event": isResizing() ? "none" : "pointer-events", }; - - function mouseDownHandler(e: MouseEvent) { - x = e.clientX; - initialLeftWidth = leftSide.getBoundingClientRect().width; - setIsResizing(true); - document.addEventListener('mousemove', mouseMoveHandler); - document.addEventListener('mouseup', mouseUpHandler); + }); + let rightStyleGet = createMemo(() => { + return { + "user-select": isResizing() ? "none" : "user-select", + "pointer-event": isResizing() ? "none" : "pointer-events", }; + }); + let resizerStyleGet = createMemo(() => { + return isResizing() ? { cursor: "col-resize" } : undefined; + }); - return ( - -
{resolvedChildren()[0]}
-
-
{resolvedChildren()[1]}
-
+ function mouseMoveHandler(e: MouseEvent) { + const dx = e.clientX - x; + setLeftWidth( + ((initialLeftWidth + dx) * 100) / + (resizer.parentNode as HTMLElement).getBoundingClientRect().width, ); + document.body.style.cursor = "col-resize"; + } + + function mouseUpHandler() { + setIsResizing(false); + document.body.style.removeProperty("cursor"); + document.removeEventListener("mousemove", mouseMoveHandler); + document.removeEventListener("mouseup", mouseUpHandler); + } + + function mouseDownHandler(e: MouseEvent) { + x = e.clientX; + initialLeftWidth = leftSide.getBoundingClientRect().width; + setIsResizing(true); + document.addEventListener("mousemove", mouseMoveHandler); + document.addEventListener("mouseup", mouseUpHandler); + } + + return ( + +
+ {resolvedChildren()[0]} +
+
+
+ {resolvedChildren()[1]} +
+
+ ); }; export default SplitView; - diff --git a/frontend/index.html b/frontend/index.html index 37c146e..6af3a9d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + @@ -8,31 +8,31 @@ + diff --git a/frontend/index.tsx b/frontend/index.tsx index 10b0347..64914cf 100644 --- a/frontend/index.tsx +++ b/frontend/index.tsx @@ -25,5 +25,5 @@ render( ), - document.getElementById("root") as HTMLElement + document.getElementById("root") as HTMLElement, ); diff --git a/frontend/store.ts b/frontend/store.ts index adfda0f..5e5d7fb 100644 --- a/frontend/store.ts +++ b/frontend/store.ts @@ -41,7 +41,7 @@ export function addMessage(message: Message): void { setState( produce((s) => { s.push(message); - }) + }), ); } @@ -80,7 +80,7 @@ export function getMessages(search: Search): Message[] { (message) => (instanceId == null || message.instanceId === instanceId) && (type == null || message.type === type) && - (!info || (isUpdateMessage(message) && hasText(message.update.info))) + (!info || (isUpdateMessage(message) && hasText(message.update.info))), ); } diff --git a/package-lock.json b/package-lock.json index 86ef7b6..b54332f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "eslint-config-prettier": "^8.5.0", "html-webpack-plugin": "^5.5.0", "jest": "^28.1.1", - "prettier": "^2.7.1", + "prettier": "^3.3.3", "rimraf": "^3.0.2", "ts-node-dev": "^2.0.0", "typescript": "^4.7.3", @@ -9271,15 +9271,15 @@ } }, "node_modules/prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -17907,9 +17907,9 @@ "dev": true }, "prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true }, "pretty-error": { diff --git a/package.json b/package.json index c505208..fa79773 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ ], "scripts": { "dev": "ts-node-dev --project tsconfig-backend.json backend/dev-cli.ts", + "fix": "prettier --write .", + "check": "prettier --check .", "cli": "node dist/backend/cli.js", "test": "jest", "typecheck": "tsc --noEmit", @@ -60,7 +62,7 @@ "eslint-config-prettier": "^8.5.0", "html-webpack-plugin": "^5.5.0", "jest": "^28.1.1", - "prettier": "^2.7.1", + "prettier": "^3.3.3", "rimraf": "^3.0.2", "ts-node-dev": "^2.0.0", "typescript": "^4.7.3", diff --git a/sim/create.ts b/sim/create.ts index 1f65692..a48b9cf 100644 --- a/sim/create.ts +++ b/sim/create.ts @@ -44,7 +44,7 @@ type Log = (...args: any[]) => void; export function createWebXdc( transport: Transport, - log: Log = () => {} + log: Log = () => {}, ): WebXdc { let resolveUpdateListenerPromise: (() => void) | null = null; @@ -72,7 +72,7 @@ export function createWebXdc( transport.setInfo(message.info); } else if (isDeleteMessage(message)) { log("delete"); - window.top?.close() + window.top?.close(); } }); transport.onConnect(() => { @@ -85,109 +85,111 @@ export function createWebXdc( return promise; }, sendToChat: async (content) => { - if (!content.file && !content.text) { - alert("🚨 Error: either file or text need to be set. (or both)"); - return Promise.reject( - "Error from sendToChat: either file or text need to be set" - ); - } - - /** @type {(file: Blob) => Promise} */ - const blob_to_base64 = (file) => { - const data_start = ";base64,"; - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - /** @type {string} */ - //@ts-ignore - let data = reader.result; - resolve( - data.slice(data.indexOf(data_start) + data_start.length) - ); - }; - reader.onerror = () => reject(reader.error); - }); - }; + if (!content.file && !content.text) { + alert("🚨 Error: either file or text need to be set. (or both)"); + return Promise.reject( + "Error from sendToChat: either file or text need to be set", + ); + } + + /** @type {(file: Blob) => Promise} */ + const blob_to_base64 = (file) => { + const data_start = ";base64,"; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + /** @type {string} */ + //@ts-ignore + let data = reader.result; + resolve(data.slice(data.indexOf(data_start) + data_start.length)); + }; + reader.onerror = () => reject(reader.error); + }); + }; - let base64Content; - if (content.file) { - if (!content.file.name) { - return Promise.reject("file name is missing"); - } - if ( - Object.keys(content.file).filter((key) => - ["blob", "base64", "plainText"].includes(key) - ).length > 1 - ) { - return Promise.reject( - "you can only set one of `blob`, `base64` or `plainText`, not multiple ones" - ); - } - - // @ts-ignore - needed because typescript imagines that blob would not exist - if (content.file.blob instanceof Blob) { - // @ts-ignore - needed because typescript imagines that blob would not exist - base64Content = await blob_to_base64(content.file.blob); - // @ts-ignore - needed because typescript imagines that base64 would not exist - } else if (typeof content.file.base64 === "string") { - // @ts-ignore - needed because typescript imagines that base64 would not exist - base64Content = content.file.base64; - // @ts-ignore - needed because typescript imagines that plainText would not exist - } else if (typeof content.file.plainText === "string") { - base64Content = await blob_to_base64( - // @ts-ignore - needed because typescript imagines that plainText would not exist - new Blob([content.file.plainText]) - ); - } else { - return Promise.reject( - "data is not set or wrong format, set one of `blob`, `base64` or `plainText`, see webxdc documentation for sendToChat" - ); - } + let base64Content; + if (content.file) { + if (!content.file.name) { + return Promise.reject("file name is missing"); + } + if ( + Object.keys(content.file).filter((key) => + ["blob", "base64", "plainText"].includes(key), + ).length > 1 + ) { + return Promise.reject( + "you can only set one of `blob`, `base64` or `plainText`, not multiple ones", + ); } - const msg = `The app would now close and the user would select a chat to send this message:\nText: ${ -content.text ? `"${content.text}"` : "No Text" -}\nFile: ${ -content.file ? `${content.file.name} - ${base64Content.length} bytes` : "No File" -}`; - if (content.file) { - const confirmed = confirm(msg + '\n\nDownload the file in the browser instead?'); - if (confirmed) { - var element = document.createElement("a"); - element.setAttribute( - "href", - "data:application/octet-stream;base64," + base64Content - ); - element.setAttribute("download", content.file.name); - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - } + + // @ts-ignore - needed because typescript imagines that blob would not exist + if (content.file.blob instanceof Blob) { + // @ts-ignore - needed because typescript imagines that blob would not exist + base64Content = await blob_to_base64(content.file.blob); + // @ts-ignore - needed because typescript imagines that base64 would not exist + } else if (typeof content.file.base64 === "string") { + // @ts-ignore - needed because typescript imagines that base64 would not exist + base64Content = content.file.base64; + // @ts-ignore - needed because typescript imagines that plainText would not exist + } else if (typeof content.file.plainText === "string") { + base64Content = await blob_to_base64( + // @ts-ignore - needed because typescript imagines that plainText would not exist + new Blob([content.file.plainText]), + ); } else { - alert(msg); + return Promise.reject( + "data is not set or wrong format, set one of `blob`, `base64` or `plainText`, see webxdc documentation for sendToChat", + ); } + } + const msg = `The app would now close and the user would select a chat to send this message:\nText: ${ + content.text ? `"${content.text}"` : "No Text" + }\nFile: ${ + content.file + ? `${content.file.name} - ${base64Content.length} bytes` + : "No File" + }`; + if (content.file) { + const confirmed = confirm( + msg + "\n\nDownload the file in the browser instead?", + ); + if (confirmed) { + var element = document.createElement("a"); + element.setAttribute( + "href", + "data:application/octet-stream;base64," + base64Content, + ); + element.setAttribute("download", content.file.name); + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + } else { + alert(msg); + } }, importFiles: (filters) => { - var element = document.createElement("input"); - element.type = "file"; - element.accept = [ - ...(filters.extensions || []), - ...(filters.mimeTypes || []), - ].join(","); - element.multiple = filters.multiple || false; - const promise = new Promise((resolve, _reject) => { - element.onchange = (_ev) => { - console.log("element.files", element.files); - const files = Array.from(element.files || []);; - document.body.removeChild(element); - resolve(files); - }; - }); - element.style.display = "none"; - document.body.appendChild(element); - element.click(); - console.log(element); - return promise; + var element = document.createElement("input"); + element.type = "file"; + element.accept = [ + ...(filters.extensions || []), + ...(filters.mimeTypes || []), + ].join(","); + element.multiple = filters.multiple || false; + const promise = new Promise((resolve, _reject) => { + element.onchange = (_ev) => { + console.log("element.files", element.files); + const files = Array.from(element.files || []); + document.body.removeChild(element); + resolve(files); + }; + }); + element.style.display = "none"; + document.body.appendChild(element); + element.click(); + console.log(element); + return promise; }, selfAddr: transport.address(), selfName: transport.name(), diff --git a/sim/ui.ts b/sim/ui.ts index 7f9846d..57dd94d 100644 --- a/sim/ui.ts +++ b/sim/ui.ts @@ -3,9 +3,9 @@ import { DevServerTransport } from "./webxdc"; const consoleFunctionNames = ["debug", "log", "info", "warn", "error"] as const; function overwriteConsoleFunction( - functionName: typeof consoleFunctionNames[number], + functionName: (typeof consoleFunctionNames)[number], deviceIdentifier: string, - logStyle: string + logStyle: string, ) { const original = console[functionName]; const replacement = original.bind(null, `%c${deviceIdentifier}`, logStyle); @@ -15,7 +15,7 @@ function overwriteConsoleFunction( export async function overwriteConsole( deviceIdentifier: string, - transport: DevServerTransport + transport: DevServerTransport, ) { const info = await transport.getInfo(); const logStyle = `color:white;font-weight:bold;border-radius:4px;padding:2px;background: ${info.color}`; @@ -26,7 +26,7 @@ export async function overwriteConsole( export async function alterUi( instanceName: string, - transport: DevServerTransport + transport: DevServerTransport, ): Promise { const info = await transport.getInfo(); let title = document.getElementsByTagName("title")[0]; diff --git a/sim/webxdc.test.ts b/sim/webxdc.test.ts index 6d7170d..7a198d6 100644 --- a/sim/webxdc.test.ts +++ b/sim/webxdc.test.ts @@ -19,7 +19,7 @@ class FakeTransport implements Transport { public client: WebXdcMulti, address: string, name: string, - public onClear: () => void + public onClear: () => void, ) { this._address = address; this._name = name; @@ -46,7 +46,7 @@ class FakeTransport implements Transport { this.messageCallback({ type: "clear" }); } return true; - } + }, ); } else if (data.type === "requestInfo") { if (this.messageCallback != null) { diff --git a/sim/webxdc.ts b/sim/webxdc.ts index c951f02..1f152df 100644 --- a/sim/webxdc.ts +++ b/sim/webxdc.ts @@ -65,16 +65,18 @@ export class DevServerTransport implements Transport { window.sessionStorage.clear(); const databases = await window.indexedDB.databases(); - - await Promise.all(databases.map(result => { - return new Promise((resolve, reject) => { - const name = result?.name; - console.log(`Deleting indexedDB database: ${name}`); - const request = window.indexedDB.deleteDatabase(name); - request.onsuccess = ev => resolve(ev); - request.onerror = ev => reject(ev); - }); - })); + + await Promise.all( + databases.map((result) => { + return new Promise((resolve, reject) => { + const name = result?.name; + console.log(`Deleting indexedDB database: ${name}`); + const request = window.indexedDB.deleteDatabase(name); + request.onsuccess = (ev) => resolve(ev); + request.onerror = (ev) => reject(ev); + }); + }), + ); // we want to reload the window otherwise we won't take the // cleared localstorage into account diff --git a/types/webxdc.ts b/types/webxdc.ts index bf0af0b..72de897 100644 --- a/types/webxdc.ts +++ b/types/webxdc.ts @@ -59,20 +59,20 @@ export type SendUpdate = (update: Update, descr: string) => void; export type SendToChat = (message: SendOptions) => Promise; export type ImportFiles = (filter: { - /** - * mimetypes as in https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers - */ - mimeTypes?: string[]; - /** only show files with these extensions. - * All extensions need to start with a dot and have the format `.ext`. */ - extensions?: string[]; - /** false by default, whether to allow multiple files to be selected */ - multiple?: boolean; - }) => Promise; + /** + * mimetypes as in https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers + */ + mimeTypes?: string[]; + /** only show files with these extensions. + * All extensions need to start with a dot and have the format `.ext`. */ + extensions?: string[]; + /** false by default, whether to allow multiple files to be selected */ + multiple?: boolean; +}) => Promise; export type SetUpdateListener = ( listener: UpdateListener, - serial: number + serial: number, ) => Promise; export type WebXdc = {