Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Junhui Tong committed Jul 12, 2024
0 parents commit 4623ad6
Show file tree
Hide file tree
Showing 13 changed files with 625 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"deno.enable": true,
"deno.disablePaths": [
"frontend"
]
}
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Deno Web app

This is a template for a web app that uses Deno as backend and TypeScript as frontend.

This template also implement a feature that enumerates all windows on Windows OS and shows them in the UI to demonstrate typed-communications between frontend and backend.
![screenshot](doc/screenshot.png)
To run it, clone the repo and run: `run.bat` on Windows, or invoking tsc and deno manually on other platforms.

(pre-requisite: deno, tsc)

## Usage

You define API interfaces between the web client and the backend script in `api.ts`:

```typescript
export type API = {
checkResult: (a:number, b:number, res:number) => string,
getWindows: () => {title:string, className:string}[]
}
export const api: Promisify<API> = {
checkResult: (a, b, res) => fetchAPI('checkResult', [a, b, res]),
getWindows: () => fetchAPI('getWindows', []),
}
```

Then you implement the API in `api_impl.ts`.

Now you can use the API as normal function in the web client, e.g.:

```typescript
import {api} from '../api.js'
...
const windows = await api.getWindows()
for (const w of windows) {
const div = document.createElement('div');
div.innerHTML = `<b>${w.title}</b> (${w.className})`;
app.appendChild(div);
}
document.body.appendChild(app);
```
30 changes: 30 additions & 0 deletions api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const api = {
checkResult: async function (_a:number, _b:number, _res:number) {
return await callAPI('checkResult', arguments) as string
},
getWindows: async function () {
return await callAPI('getWindows', arguments) as {title:string, className:string}[]
},
launchExcel: async function () {
return await callAPI('launchExcel', arguments) as string
},
getActiveExcelRow: async function () {
return await callAPI('getActiveExcelRow', arguments) as {
headings: string[],
data: string[]
}
},
closeBackend: async function () {
return await callAPI('closeBackend', arguments) as string
}
}

export type BackendAPI = Omit<typeof api, 'closeBackend'>

async function callAPI(cmd:string, ...args:any[]){
const proto = window.location.protocol
const host = window.location.hostname
const port = 8080
const resp = await fetch(`${proto}//${host}:${port}/api?cmd=${cmd}&args=${encodeURIComponent(JSON.stringify(args))}`)
return await resp.json()
}
143 changes: 143 additions & 0 deletions architecture.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions backend/api_impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as wui from "https://win32.deno.dev/0.4.1/UI.WindowsAndMessaging"
import { BackendAPI } from '../api.ts'

let scriptProcess: Deno.ChildProcess

function launchScript() {
const cmd = new Deno.Command('cscript.exe', {
args: ['//nologo', 'excel.js'],
stdout: 'inherit',
stderr: 'inherit',
})
const p = cmd.spawn()
return p
}

export const apiImpl: BackendAPI = {
checkResult: async (a: number, b: number, res: number) => {
if ((a + b) == res) {
return `Correct: ${a} + ${b} = ${res}`;
}
else {
return `Incorrect: ${a} + ${b} != ${res}`;
}
},
getWindows: async () => {
const windows: { title: string, className: string }[] = []
const cb = new Deno.UnsafeCallback({
parameters: ['pointer', 'pointer'],
result: 'bool'
}, (w, lparam) => {
const buf = new Uint8Array(100)
const buffer = new Uint16Array(1000)
wui.GetWindowTextW(w, buffer, 1000)
const title = new TextDecoder('utf-16le').decode(buffer).split('\0')[0]
wui.GetClassNameW(w, buffer, 1000)
const className = new TextDecoder('utf-16le').decode(buffer).split('\0')[0]
const tid = wui.GetWindowThreadProcessId(w, buf)
const pp = Deno.UnsafePointer.of(buf)
const pid = new Deno.UnsafePointerView(pp!).getInt32()
const info = { title, className }
console.log(w, info, title, className, tid, pid);
windows.push(info)
return true;
})
wui.EnumWindows(cb.pointer, null)
await new Promise(resolve => setTimeout(resolve, 1000))
return windows
},
launchExcel: async () => {
if (scriptProcess) {
try {
scriptProcess.kill()
} catch (_e) {
// ignore
}
scriptProcess.kill();
}
scriptProcess = launchScript()
return ''
},
getActiveExcelRow: async () => {
// read output from script
const s = Deno.readTextFileSync('excelrow.txt')
const [hs, vs] = s.split('_@@RS@@_')
return {
headings: hs.split('_@@HS@@_'),
data: vs.split('_@@VS@@_')
}
}
}
Binary file added doc/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions dwa/dwa_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { typeByExtension } from "https://deno.land/std/media_types/mod.ts";
import { extname } from "https://deno.land/std/path/mod.ts";

export function startDenoWebApp(root: string, port: number, apiImpl: {[key: string]: Function}) {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Content-Length, X-Requested-With",
};
const handlerCORS = async (req: Request) => {
const response = await handler(req);
response.headers.set("Access-Control-Allow-Origin", "*");
return response;
}
const handler = async (req: Request) => {
let path = new URL(req.url).pathname;

// API
if (path == "/api") {
const cmd = new URL(req.url).searchParams.get("cmd") || ''
const args = JSON.parse(decodeURI(new URL(req.url).searchParams.get("args") || "[]"))
console.log('handling api', cmd, args)
if (cmd in apiImpl) {
const func = apiImpl[cmd as keyof typeof apiImpl]
const result = await func.apply(apiImpl, args)
return new Response(JSON.stringify(result), { status: 200 });
} else {
if (cmd === 'closeBackend') {
setTimeout(() => {
console.log('backend closed')
Deno.exit()
}, 1000)
return new Response(JSON.stringify('OK'), { status: 200 });
}
}
return new Response(`invalid command ${cmd}`, { status: 404 });
}

if(path == "/"){
path = `/index.html`;
}
try {
console.log('serving', root + path)
const file = await Deno.open(root + path);
return new Response(file.readable, {
headers: {
"content-type" : typeByExtension(extname(path)) || "text/plain"
}
});
} catch(ex){
if(ex.code === "ENOENT"){
return new Response("Not Found", { status: 404 });
}
return new Response("Internal Server Error", { status: 500 });
}
};

Deno.serve({ port }, handlerCORS);
}

Loading

0 comments on commit 4623ad6

Please sign in to comment.