first pass

cordova
Valerie 2022-07-20 16:04:09 -04:00
parent d1de401ce4
commit e651446cc2
27 changed files with 3986 additions and 582 deletions

View File

@ -1,7 +1,7 @@
import {resolve, sep} from 'path';
export default {
'*.{js,ts,vue}': 'eslint --cache --fix',
'*.{js,ts,tsx}': 'eslint --cache --fix',
/**
* Run typechecking if any type-sensitive files was staged

3815
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,22 +13,25 @@
},
"main": "packages/main/dist/index.cjs",
"scripts": {
"build": "npm run build:main && npm run build:preload && npm run build:renderer",
"build": "npm run build:main && npm run build:preload && npm run build:renderer && npm run build:server",
"build:main": "cd ./packages/main && vite build",
"build:preload": "cd ./packages/preload && vite build",
"build:renderer": "cd ./packages/renderer && vite build",
"build:server": "cd ./packages/server && vite build",
"compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js --dir --config.asar=false",
"test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e",
"test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e && npm run test:server",
"test:e2e": "vitest run",
"test:main": "vitest run -r packages/main --passWithNoTests",
"test:preload": "vitest run -r packages/preload --passWithNoTests",
"test:renderer": "vitest run -r packages/renderer --passWithNoTests",
"test:server": "vitest run -r packages/server --passWithNoTests",
"watch": "node scripts/watch.js",
"lint": "eslint . --ext js,ts,vue",
"typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
"typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
"typecheck:renderer": "vue-tsc --noEmit -p packages/renderer/tsconfig.json",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer",
"typecheck:server": "tsc --noEmit -p packages/server/tsconfig.json",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer && npm run typecheck:server",
"postinstall": "cross-env ELECTRON_RUN_AS_NODE=1 npx --no-install electron ./scripts/update-electron-vendors.js "
},
"devDependencies": {
@ -52,7 +55,21 @@
"vue-tsc": "0.38.8"
},
"dependencies": {
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"@vitejs/plugin-react": "^2.0.0",
"electron-updater": "5.0.5",
"vue": "3.2.37"
"eslint-plugin-react": "^7.30.1",
"express": "^4.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-time-ago": "^7.2.1",
"react-timeago": "^7.1.0",
"uuid": "^8.3.2",
"vue": "3.2.37",
"ws": "^8.8.1"
}
}

View File

@ -48,12 +48,13 @@ app.whenReady()
if (import.meta.env.DEV) {
app.whenReady()
.then(() => import('electron-devtools-installer'))
.then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
loadExtensionOptions: {
allowFileAccess: true,
},
}))
.catch(e => console.error('Failed install extension:', e));
.catch(e => console.error('Failed importing electron-devtools-installer:', e));
// .then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
// loadExtensionOptions: {
// allowFileAccess: true,
// },
// }))
// .catch(e => console.error('Failed install extension:', e));
}
/**

View File

@ -6,6 +6,7 @@ async function createWindow() {
const browserWindow = new BrowserWindow({
show: false, // Use 'ready-to-show' event to show window
webPreferences: {
sandbox: false,
webviewTag: false, // The webview tag is not recommended. Consider alternatives like iframe or Electron's BrowserView. https://www.electronjs.org/docs/latest/api/webview-tag#warning
preload: join(__dirname, '../../preload/dist/index.cjs'),
},

View File

@ -5,7 +5,8 @@
},
"extends": [
/** @see https://eslint.vuejs.org/rules/ */
"plugin:vue/vue3-recommended"
"eslint:recommended"
// "plugin:react/recommended"
],
"parserOptions": {
"parser": "@typescript-eslint/parser",

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script src="./src/index.ts" type="module"></script>
</body>
<head>
<meta charset="UTF-8">
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:"> -->
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Vite App</title>
</head>
<body style="margin: 0px; overflow: hidden; color: #f8f8f2; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif">
<div id="app" style="width: 100vw; height: 100vh"></div>
<script src="./src/index.tsx" type="module"></script>
</body>
</html>

View File

@ -1,58 +0,0 @@
<script lang="ts" setup>
import ReactiveCounter from '/@/components/ReactiveCounter.vue';
import ReactiveHash from '/@/components/ReactiveHash.vue';
import ElectronVersions from '/@/components/ElectronVersions.vue';
</script>
<template>
<img
alt="Vue logo"
src="../assets/logo.svg"
width="150"
>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a
href="https://github.com/cawa-93/vite-electron-builder"
target="_blank"
>vite-electron-builder documentation</a>.
</p>
<fieldset>
<legend>Test Vue Reactivity</legend>
<reactive-counter />
</fieldset>
<fieldset>
<legend>Test Node.js API</legend>
<reactive-hash />
</fieldset>
<fieldset>
<legend>Environment</legend>
<electron-versions />
</fieldset>
<p>
Edit
<code>packages/renderer/src/App.vue</code> to test hot module replacement.
</p>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin: 60px auto;
max-width: 700px;
}
fieldset {
margin: 2rem;
padding: 1rem;
}
</style>

View File

@ -1,21 +0,0 @@
<script lang="ts" setup>
import {versions} from '#preload';
</script>
<template>
<ul id="process-versions">
<li
v-for="(version, lib) in versions"
:key="lib"
>
<strong>{{ lib }}</strong>: v{{ version }}
</li>
</ul>
<code>packages/renderer/src/components/ElectronVersions.vue</code>
</template>
<style scoped>
ul {
list-style: none;
}
</style>

View File

@ -1,13 +0,0 @@
<script lang="ts" setup>
import {ref} from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++">
count is: {{ count }}
</button>
<br><br>
<code>packages/renderer/src/components/ReactiveCounter.vue</code>
</template>

View File

@ -1,31 +0,0 @@
<script lang="ts" setup>
import {computed, ref} from 'vue';
import {sha256sum} from '#preload';
const rawString = ref('Hello World');
/**
* window.nodeCrypto was exposed from {@link module:preload}
*/
const hashedString = computed(() => sha256sum(rawString.value));
</script>
<template>
<label>
Raw value
<input
v-model="rawString"
type="text"
>
</label>
<br>
<label>
Hashed by node:crypto
<input
v-model="hashedString"
type="text"
readonly
>
</label>
<br><br>
<code>packages/renderer/src/components/ReactiveHash.vue</code>
</template>

View File

@ -1,5 +0,0 @@
import {createApp} from 'vue';
import App from '/@/App.vue';
createApp(App).mount('#app');

View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Chat from './pages/Chat';
ReactDOM.render(
(
<>
<Chat
></Chat>
</>
),
document.getElementById('app'),
);

View File

@ -0,0 +1,83 @@
let socket: WebSocket | null = null;
let connectionAttempts = 0;
const url = 'wss://dev.valnet.xyz';
let routers: any[] = [];
const connect = async () => {
try {
connectionAttempts ++;
console.log('attempting api connection...');
socket = new WebSocket(url);
} catch (e) {
if(connectionAttempts === 1)
connect();
else {
const seconds = 2 ** connectionAttempts;
console.log(`waiting ${seconds} seconds before reconnecting`);
setTimeout(connect, 1000 * seconds);
}
return;
}
socket.addEventListener('open', () => {
if(socket === null) return;
connectionAttempts = 0;
// socket.send('Hello Server!');
console.log('API Connected');
});
socket.addEventListener('message', (event) => {
console.log('API Broadcasted', event.data);
const {action, data} = JSON.parse(event.data);
for(const router of routers) {
// debugger;
router(action, data);
}
});
socket.addEventListener('close', () => {
socket = null;
console.log('API Closed');
connect();
});
};
connect();
export function send(action: string, data?: any) {
if(socket === null) return;
const message = JSON.stringify({ action, data });
socket.send(message);
}
export function router(routes: any) {
for(const routeName in routes) {
const route = routes[routeName];
if(typeof route === 'object') {
for(const suffix in route) {
const combinedRouteName = routeName + ':' + suffix;
routes[combinedRouteName] = route[suffix];
}
delete routes[routeName];
}
}
return function(route: string, data: any) {
if(route in routes) {
routes[route](data);
} else {
console.warn(`route <${route}> not found`);
}
};
}
export function registerRouter(router: any) {
routers.push(router);
}
export function unregisterRouter(router: any) {
routers = routers.filter(r => r !== router);
}

View File

@ -0,0 +1,192 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import TimeAgo from 'react-timeago';
import { v4 } from 'uuid';
import { registerRouter, router, send, unregisterRouter } from '../lib/api';
const firstLineIndent = '10px';
const multiLineIndent = '16px';
const rightMessagePagging = '16px';
interface Message {
text: string;
from: string;
timestamp: number;
// nid: number;
uid: string;
}
function createMessage(from: string, text: string, t = 0): Message {
return {
text,
from,
timestamp: Date.now() - t * 1000,
uid: v4(),
};
}
const mockMessages: Message[] = [
// createMessage('Bob', 'Hey', 55),
// createMessage('Alice', 'Hello', 50),
// createMessage('Bob', 'What up', 45),
// createMessage('Alice', 'nm UUU', 40),
// createMessage('Bob', 'Hey', 35),
// createMessage('Alice', 'Hello', 30),
// createMessage('Bob', 'What up', 25),
// createMessage('Alice', 'nm UUU', 20),
// createMessage('Bob', 'Hey', 15),
// createMessage('Alice', 'Hello', 10),
// createMessage('Bob', 'What up', 5),
// createMessage('Alice', 'This is what a really long message could possibly look like, if a person decided to write a really long essay. This is what a really long message could possibly look like, if a person decided to write a really long essay. This is what a really long message could possibly look like, if a person decided to write a really long essay. This is what a really long message could possibly look like, if a person decided to write a really long essay.'),
];
export default () => {
const [messages, setMessages] = useState<Message[]>(mockMessages);
const textBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const actions = router({
message(data: Message) {
setMessages([...messages, data]);
},
});
registerRouter(actions);
return () => {
unregisterRouter(actions);
};
}, [messages]);
const sendMessage = useCallback(() => {
if(textBoxRef.current === null) return;
send('message', createMessage('Anonymous', textBoxRef.current.innerText));
textBoxRef.current.innerText = '';
}, []);
const keyDown = useCallback((evt: any) => {
console.log(evt);
if(evt.key === 'Enter') {
sendMessage();
}
}, [sendMessage]);
return (
<>
<div
style={{
background: '#282a36',
height: '100%',
width: '100%',
display: 'grid',
gridTemplateColumns: '1fr 64px',
gridTemplateRows: '1fr 64px',
gridTemplateAreas: '"content content" "message send"',
}}
>
<div style={{
// borderBottom: '1px solid #bd93f9',
gridArea: 'content',
position: 'relative',
}}>
<div style={{
position: 'absolute',
bottom: '0px',
width: '100%',
}}>
{messages.map(message => (
<div style={{
display: 'grid',
gridTemplateColumns: '128px 1fr',
width: '100%',
padding: '1px 0px',
}}>
<span style={{
fontStyle: 'italic',
color: '#596793',
textAlign: 'right',
userSelect: 'none',
marginRight: '16px',
}}>
<TimeAgo
date={message.timestamp}
formatter={(t, u) => u === 'second' ? 'Just Now' : ('' + t + u[0])}
></TimeAgo>
</span>
<span style={{
}}>
<div style={{
fontWeight: 'bold',
float: 'left',
paddingRight: firstLineIndent,
// marginRight: '16px',
// height: '100%'
// borderBottom: '1px solid white'
}}>
{message.from}
</div>
<div style={{
marginRight: rightMessagePagging,
paddingLeft: multiLineIndent,
boxSizing: 'border-box',
position: 'relative',
}}>
{message.text}
</div>
</span>
</div>
))}
</div>
</div>
<div onClick={() => {
textBoxRef.current?.focus();
}}style={{
margin: '8px',
marginRight: '3px',
borderRadius: '8px',
background: '#343746',
gridArea: 'message',
display: 'grid',
placeItems: 'center center',
padding: '0px 16px',
cursor: 'text',
overflow: 'auto',
}}>
<div
ref={textBoxRef}
onKeyDown={keyDown}
className="input"
role="textbox"
contentEditable
style={{
background: 'inherit',
outline: 'none',
boxSizing: 'border-box',
borderRadius: '8px',
width: '100%',
resize: 'none',
}}
></div>
</div>
<div style={{
width: '100%',
height: '100%',
padding: '8px',
boxSizing: 'border-box',
}}>
<div onClick={sendMessage} style={{
background: '#bd93f9',
width: '100%',
height: '100%',
// borderRadius: '50%',
borderRadius: '8px',
cursor: 'pointer',
display: 'grid',
placeItems: 'center center',
fontSize: '32px',
}}>
</div>
</div>
</div>
</>
);
};

View File

@ -1,21 +0,0 @@
import {mount} from '@vue/test-utils';
import {expect, test, vi} from 'vitest';
import ElectronVersions from '../src/components/ElectronVersions.vue';
vi.mock('#preload', () => {
return {
versions: {lib1: 1, lib2: 2},
};
});
test('ElectronVersions component', async () => {
expect(ElectronVersions).toBeTruthy();
const wrapper = mount(ElectronVersions);
const lis = wrapper.findAll<HTMLElement>('li');
expect(lis.length).toBe(2);
expect(lis[0].text()).toBe('lib1: v1');
expect(lis[1].text()).toBe('lib2: v2');
});

View File

@ -1,14 +0,0 @@
import {mount} from '@vue/test-utils';
import {expect, test} from 'vitest';
import ReactiveCounter from '../src/components/ReactiveCounter.vue';
test('ReactiveHash component', async () => {
expect(ReactiveCounter).toBeTruthy();
const wrapper = mount(ReactiveCounter);
const button = wrapper.get('button');
expect(button.text()).toBe('count is: 0');
await button.trigger('click');
expect(button.text()).toBe('count is: 1');
});

View File

@ -1,23 +0,0 @@
import {mount} from '@vue/test-utils';
import {expect, test, vi} from 'vitest';
import ReactiveHash from '../src/components/ReactiveHash.vue';
vi.mock('#preload', () => {
return {
sha256sum: vi.fn((s: string) => `${s}:HASHED`),
};
});
test('ReactiveHash component', async () => {
expect(ReactiveHash).toBeTruthy();
const wrapper = mount(ReactiveHash);
const dataInput = wrapper.get<HTMLInputElement>('input:not([readonly])');
const hashInput = wrapper.get<HTMLInputElement>('input[readonly]');
const dataToHashed = Math.random().toString(36).slice(2, 7);
await dataInput.setValue(dataToHashed);
expect(hashInput.element.value).toBe(`${dataToHashed}:HASHED`);
});

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"impl"
"module": "esnext",
"target": "esnext",
"sourceMap": false,
@ -7,7 +8,6 @@
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"jsx": "preserve",
"types" : ["node"],
"baseUrl": ".",
@ -19,11 +19,12 @@
"./src/*"
]
},
"lib": ["ESNext", "dom", "dom.iterable"]
"lib": ["ESNext", "dom", "dom.iterable"],
"jsx": "preserve",
"allowSyntheticDefaultImports": true
},
"include": [
"src/**/*.vue",
"src/**/*.ts",
"src/**/*.tsx",
"types/**/*.d.ts",

View File

@ -1,6 +0,0 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -2,7 +2,7 @@
import {chrome} from '../../.electron-vendors.cache.json';
import {join} from 'path';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
import {renderer} from 'unplugin-auto-expose';
const PACKAGE_ROOT = __dirname;
@ -40,7 +40,7 @@ const config = {
environment: 'happy-dom',
},
plugins: [
vue(),
react(),
renderer.vite({
preloadEntry: join(PACKAGE_ROOT, '../preload/src/index.ts'),
}),

View File

@ -0,0 +1,42 @@
import { WebSocketServer } from 'ws';
import router from './routers/root';
const wss = new WebSocketServer({
port: 3000,
}, () => {
console.log('ws chat server started on dev.valnet.xyz');
});
wss.on('connection', (ws) => {
ws.on('message', (str) => {
try {
const message = JSON.parse(str.toString());
if(typeof message.action !== 'string') {
console.warn('invalid JSON message');
return;
}
const {action, data} = message;
try {
router(action, data);
} catch(e) {
console.warn(`error in action ${action}`);
console.error(e);
}
} catch (e) {
console.warn('JSON parse failed on message');
console.error(e);
}
});
});
export function send(client: any, action: string, data?: any) {
client.send(JSON.stringify({action, data}));
}
export function broadcast(action: string, data?: any) {
for(const client of wss.clients) {
send(client, action, data);
}
}
export default wss;

View File

@ -0,0 +1,19 @@
export default function router(routes: any) {
for(const routeName in routes) {
const route = routes[routeName];
if(typeof route === 'object') {
for(const suffix in route) {
const combinedRouteName = routeName + ':' + suffix;
routes[combinedRouteName] = route[suffix];
}
delete routes[routeName];
}
}
return function(route: any, data: any) {
if(route in routes) {
routes[route](data);
} else {
console.warn(`route <${route}> not found`);
}
};
}

View File

@ -0,0 +1,11 @@
import router from '../router';
import { broadcast } from '../index';
export default router({
up() {
console.log(Date.now());
},
message(data: string) {
broadcast('message', data);
},
});

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"types" : ["node"],
"baseUrl": ".",
"paths": {
"/@/*": [
"./src/*"
]
},
},
"include": [
"src/**/*.ts",
"../../types/**/*.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts"
]
}

View File

@ -0,0 +1,41 @@
import {node} from '../../.electron-vendors.cache.json';
import {join} from 'path';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
ssr: true,
sourcemap: 'inline',
target: `node${node}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
},
rollupOptions: {
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
brotliSize: false,
},
};
export default config;

View File

@ -3,7 +3,7 @@
const {createServer, build, createLogger} = require('vite');
const electronPath = require('electron');
const {spawn} = require('child_process');
const node = 'node' + (process.platform === 'win32' ? '.exe' : '');
/** @type 'production' | 'development'' */
const mode = process.env.MODE = process.env.MODE || 'development';
@ -23,6 +23,58 @@ const stderrFilterPatterns = [
/ExtensionLoadWarning/,
];
const setupServerPackageWatcher = () => {
const logger = createLogger(logLevel, {
prefix: '[srvr]',
});
let spawnProcess = null;
const processDied = () => {
logger.error('Server has died.', {timestamp: true});
spawnProcess = null;
};
return build({
mode,
logLevel,
build: {
watch: {},
},
configFile: 'packages/server/vite.config.js',
plugins: [{
name: 'reload-server-on-server-package-change',
writeBundle() {
/** Kill electron ff process already exist */
if (spawnProcess !== null) {
spawnProcess.off('exit', processDied);
spawnProcess.kill('SIGINT');
spawnProcess = null;
}
/** Spawn new electron process */
spawnProcess = spawn(node, ['./index.cjs'], {
cwd: './packages/server/dist',
});
/** Proxy all logs */
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
/** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
spawnProcess.stderr.on('data', d => {
const data = d.toString().trim();
if (!data) return;
const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
if (mayIgnore) return;
logger.error(data, {timestamp: true});
});
/** Stops the watch script when the application has been quit */
spawnProcess.on('exit', processDied);
},
}],
});
};
/**
* Setup watcher for `main` package
@ -30,6 +82,7 @@ const stderrFilterPatterns = [
* @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
* Needs to set up `VITE_DEV_SERVER_URL` environment variable from {@link import('vite').ViteDevServer.resolvedUrls}
*/
const setupMainPackageWatcher = ({resolvedUrls}) => {
process.env.VITE_DEV_SERVER_URL = resolvedUrls.local[0];
@ -142,6 +195,7 @@ const setupPreloadPackageWatcher = ({ws}) =>
* See {@link setupMainPackageWatcher} JSDoc
*/
await setupMainPackageWatcher(rendererWatchServer);
await setupServerPackageWatcher();
} catch (e) {
console.error(e);
process.exit(1);