initial deno prototype

This commit is contained in:
CanadaHonk 2022-12-29 23:19:10 +00:00
parent b23351ae4a
commit b2597e0d7c
11 changed files with 222 additions and 60 deletions

View File

@ -1,17 +1,17 @@
# Gluon
# Gluon (experimental Deno edition)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://choosealicense.com/licenses/mit/l) [![GitHub Sponsors](https://img.shields.io/github/sponsors/CanadaHonk?label=Sponsors&logo=github)](https://github.com/sponsors/CanadaHonk) [![Discord](https://img.shields.io/discord/1051940602704564244.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RFtUCA8fST)
**Gluon is a framework for creating "desktop apps" from websites**, using **system installed browsers** *(not webviews)* and NodeJS, differing a lot from other existing active projects - opening up innovation and allowing some major advantages. Instead of other similar frameworks bundling a browser like Chromium or using webviews (like Edge Webview2 on Windows), **Gluon just uses system installed browsers** like Chrome, Edge, Firefox, etc. Gluon supports Chromium ***and Firefox*** based browsers as the frontend, while Gluon's backend uses NodeJS to be versatile and easy to develop (also allowing easy learning from other popular frameworks like Electron by using the same-ish stack).
**Gluon is a framework for creating "desktop apps" from websites**, using **system installed browsers** *(not webviews)* and Deno, differing a lot from other existing active projects - opening up innovation and allowing some major advantages. Instead of other similar frameworks bundling a browser like Chromium or using webviews (like Edge Webview2 on Windows), **Gluon just uses system installed browsers** like Chrome, Edge, Firefox, etc. Gluon supports Chromium ***and Firefox*** based browsers as the frontend, while Gluon's backend uses Deno to be versatile and easy to develop (also allowing easy learning from other popular frameworks like Electron by using the same-ish stack).
## Features
- **Uses normal system installed browsers** - allows user choice by **supporting most Chromium *and Firefox*** based browsers, no webviews
- **Tiny bundle sizes** - <1MB using system Node, <10MB when bundling it
- **Tiny bundle sizes** - <1MB using system Deno
- **Chromium *and Firefox* supported as browser engine**, unlike any other active framework
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Node backend
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Deno backend
- **Fast build times** - Gluon has build times under 1 second on most machines for small projects
- **Actively developed** and **listening to feedback** - already on 5th major revision in ~1 week of development, quickly adding unplanned requested features if liked by the community (like Firefox support!)
- **Lower memory usage** - compared to most other frameworks Gluon should have a slightly lower average memory usage by using browser flags to squeeze out more performance
<!-- - **No forks needed** - Gluon doesn't need forks of Node or Chromium/etc to use them, it just uses normal versions -->
<!-- - **No forks needed** - Gluon doesn't need forks of Deno or Chromium/etc to use them, it just uses normal versions -->
![Gluworld Screenshot showing Chrome Canary and Firefox Nightly being used at once.](https://user-images.githubusercontent.com/19228318/207103757-fd5c9428-a927-4986-8c6c-02d9961ad422.png)
@ -23,12 +23,12 @@ Gluon is currently **barely 1 week old**, so is still in an **early and experime
### Specific feature statuses
- Using Chromium based browsers: Stable
- Using Firefox based browsers: Experimental
- Web-Node IPC: Stable
- Web-Deno IPC: Stable
<br>
## Ecosystem
- [Gluon](https://github.com/gluon-framework/gluon): the Gluon framework (NodeJS)
- [Gluon](https://github.com/gluon-framework/gluon): the Gluon framework (Deno)
- [Glugun](https://github.com/gluon-framework/glugun): builds Gluon apps into releasable builds with optional bundling (soon)
### Apps
@ -39,27 +39,23 @@ Gluon is currently **barely 1 week old**, so is still in an **early and experime
<br>
## Trying Gluon
1. Clone [the Gluon examples repo](https://github.com/gluon-framework/examples)
2. Inside of `gluworld`, run `npm install`
3. Now do `node .` to run it!
1. Clone [the `deno` branch of this repo](https://github.com/gluon-framework/gluon/tree/deno) (`git clone --branch deno https://github.com/gluon-framework/gluon.git`)
2. `deno run -A gluworld/index.js`
<details>
<summary>Shell example</summary>
```sh
$ git clone https://github.com/gluon-framework/examples.git
$ cd examples
$ cd gluworld
$ npm install
...
$ node .
$ git clone --branch deno https://github.com/gluon-framework/gluon.git
$ cd gluon
$ deno run -A gluworld/index.js
```
</details>
<br>
## IPC API
<!-- ## IPC API
Gluon has an easy to use, but powerful asynchronous IPC API. Example:
```js
// In your website's JS
@ -68,7 +64,7 @@ console.log(reply); // { give: 'back', different: 'stuff' }
```
```js
// In your Node backend
// In your Deno backend
import * as Gluon from '@gluon-framework/gluon';
const Window = await Gluon.open(...);
@ -77,14 +73,14 @@ Window.ipc.on('my type', data => { // { more: 'data' }
});
```
<br>
<br> -->
## Comparisons
### Internals
| Part | Gluon | Electron | Tauri | Neutralinojs |
| ---- | ----- | -------- | ------------ | ----- |
| Frontend | System installed Chromium *or Firefox* | Self-contained Chromium | System installed webview | System installed webview |
| Backend | System installed *or bundled* Node.JS | Self-contained Node.JS | Native (Rust) | Native (Any) |
| Backend | System installed *or bundled* Deno | Self-contained Node.JS | Native (Rust) | Native (Any) |
| IPC | Window object | Preload | Window object | Window object |
| Status | Early in development | Production ready | Usable | Usable |
| Ecosystem | Integrated | Distributed | Integrated | Integrated |
@ -97,7 +93,7 @@ Basic (plain HTML) Hello World demo, measured on up to date Windows 10, on my ma
| ---- | ----- | -------- | ------------ | ----- |
| Build Size | <1MB[^system][^gluon][^1] | ~220MB | ~1.8MB[^system] | ~2.6MB[^system] |
| Memory Usage | ~80MB[^gluon] | ~100MB | ~90MB | ~90MB |
| Backend[^2] Memory Usage | ~13MB[^gluon] (Node) | ~22MB (Node) | ~3MB (Native) | ~3MB (Native) |
| Backend[^2] Memory Usage | ~13MB[^gluon] (Deno) | ~22MB (Node) | ~3MB (Native) | ~3MB (Native) |
| Build Time | ~0.7s[^3] | ~20s[^4] | ~120s[^5] | ~2s[^3][^6] |
*Extra info: All HTML/CSS/JS is unminified (including Gluon). Built in release configuration. All binaries were left as compiled with common size optimizations enabled for that language, no stripping/packing done.*
@ -105,7 +101,7 @@ Basic (plain HTML) Hello World demo, measured on up to date Windows 10, on my ma
[^system]: Does not include system installed components.
[^gluon]: Using Chrome as system browser. Early/WIP data, may change in future.
[^1]: *How is Gluon so small?* Since NodeJS is expected as a system installed component, it is "just" bundled and minified Node code.
[^1]: *How is Gluon so small?* Since Deno is expected as a system installed component, it is "just" bundled and minified Deno code.
[^2]: Backend like non-Web (not Chromium/WebView2/etc).
[^3]: Includes Node.JS spinup time.
[^4]: Built for win32 zip (not Squirrel) as a fairer comparison.

136
gluworld/index.html Normal file
View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://raw.githubusercontent.com/OpenAsar/gluon/main/assets/logo.png">
<title>Gluworld</title>
</head>
<body>
<h1 id="main">
Gluon <code id="gluon_version"></code> <br>
<span id="built_with">built with <code id="builder"></code> <br></span>
running on <code id="product"></code>
</h1>
<div id="versions">
<h2>
<span id="engine_name"></span> <br>
<code id="browser_version"></code> <br>
<p><span id="js_engine_name"></span> <code id="browser_v8"></code></p>
</h2>
<h2 id="build">
Build Size <br>
<code id="build_size"></code>
</h2>
<h2>
Deno <br>
<code id="deno_version"></code> <br>
<p>V8 <code id="deno_v8"></code></p>
</h2>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700;800;900');
html, body {
background: #101418;
color: #fff;
font-family: Inter, sans-serif;
margin: 0;
width: 100%;
height: 100%;
}
body {
box-sizing: border-box;
padding: 5vw 5vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
}
h1, h2, code {
text-align: center;
margin: 0;
}
h1 {
font-weight: 900;
font-size: 34px;
}
h1 > code, h2 > p > code {
margin-left: 8px;
}
h2 {
font-weight: 800;
font-size: 28px;
}
h2 > p, h2 > p > code {
font-weight: 600;
font-size: 18px;
color: #bbb;
}
code {
font-size: 100%;
font-family: Fira Code;
color: #ddd;
}
#versions {
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
}
#build, #built_with {
display: none;
}
</style>
<script> (async () => {
await new Promise(res => {
const check = () => {
if (!window.Gluon) return setTimeout(check, 100);
res();
};
check();
});
browser_version.textContent = Gluon.versions.browser;
deno_version.textContent = `${Gluon.versions.embedded.deno ? 'Embed' : 'System'} ${Gluon.versions.deno}`;
gluon_version.textContent = Gluon.versions.gluon;
builder.textContent = Gluon.versions.builder;
product.textContent = Gluon.versions.product;
deno_v8.textContent = Gluon.versions.js.deno;
browser_v8.textContent = Gluon.versions.js.browser;
if (Gluon.versions.builder !== 'nothing') {
build.style.display = 'block';
built_with.style.display = 'block';
// main.innerHTML = main.innerHTML.replace(`built with <code id="builder">nothing</code> <br>`, '');
}
engine_name.textContent = Gluon.versions.browserType === 'firefox' ? 'Firefox' : 'Chromium';
js_engine_name.textContent = Gluon.versions.browserType === 'firefox' ? 'SpiderMonkey' : 'V8';
Gluon.ipc.on('build size', size => {
const kb = size / 1024;
const mb = kb / 1024;
build_size.textContent = mb > 0.1 ? `${mb.toFixed(2)}MB` : `${kb.toFixed(0)}KB`;
});
})();
</script>
</body>
</html>

27
gluworld/index.js Normal file
View File

@ -0,0 +1,27 @@
import * as Gluon from '../src/index.js';
import { fileURLToPath, pathToFileURL } from 'https://deno.land/std@0.170.0/node/url.ts';
import { join, dirname } from 'https://deno.land/std@0.170.0/node/path.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
(async () => {
if (Deno.args.length > 0) { // use argv as browsers to use
for (const forceBrowser of Deno.args) {
await Gluon.open(pathToFileURL(join(__dirname, 'index.html')).href, {
windowSize: [ 800, 500 ],
forceBrowser
});
}
return;
}
const Browser = await Gluon.open(pathToFileURL(join(__dirname, 'index.html')).href, {
windowSize: [ 800, 500 ]
});
// const buildSize = await dirSize(__dirname);
// Chromium.ipc.send('build size', buildSize);
})();

1
gluworld/package.json Normal file
View File

@ -0,0 +1 @@
{"type":"module","dependencies":{"@gluon-framework/gluon":"^0.7.0"}}

View File

@ -10,9 +10,8 @@ const presets = { // Presets from OpenAsar
export default async ({ browserName, browserPath, dataPath }, { url, windowSize }) => {
return await StartBrowser(browserPath, [
`--app=${url}`,
`--remote-debugging-pipe`,
`--user-data-dir=${dataPath}`,
windowSize ? `--window-size=${windowSize.join(',')}` : '',
...`--new-window --disable-extensions --disable-default-apps --disable-breakpad --disable-crashpad --disable-background-networking --disable-domain-reliability --disable-component-update --disable-sync --disable-features=AutofillServerCommunication ${presets.perf}`.split(' ')
], 'stdio', { browserName });
], 'websocket', { browserName });
};

View File

@ -1,5 +1,5 @@
import { mkdir, writeFile } from 'fs/promises';
import { join } from 'path';
import { mkdir, writeFile } from 'https://deno.land/std@0.170.0/node/fs/promises.ts';
import { join } from 'https://deno.land/std@0.170.0/node/path.ts';
import StartBrowser from '../launcher/start.js';

View File

@ -1,11 +1,15 @@
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
global.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args);
window.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args);
process.versions.gluon = '0.8.0-dev';
Deno.version = { // have to do this because... Deno
...Deno.version,
gluon: '0.8.0-deno-dev'
};
import { join, dirname, delimiter, sep } from 'path';
import { access, readdir } from 'fs/promises';
import { fileURLToPath } from 'url';
import { join, dirname, delimiter, sep } from 'https://deno.land/std@0.170.0/node/path.ts';
import { access, readdir } from 'https://deno.land/std@0.170.0/node/fs/promises.ts';
import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
import Chromium from './browser/chromium.js';
import Firefox from './browser/firefox.js';
@ -16,13 +20,13 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const browserPaths = ({
win32: process.platform === 'win32' && {
chrome: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'),
chrome_canary: join(process.env.LOCALAPPDATA, 'Google', 'Chrome SxS', 'Application', 'chrome.exe'),
edge: join(process.env['PROGRAMFILES(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
windows: Deno.build.os === 'windows' && {
chrome: join(Deno.env.get('PROGRAMFILES'), 'Google', 'Chrome', 'Application', 'chrome.exe'),
chrome_canary: join(Deno.env.get('LOCALAPPDATA'), 'Google', 'Chrome SxS', 'Application', 'chrome.exe'),
edge: join(Deno.env.get('PROGRAMFILES(x86)'), 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
firefox: join(process.env.PROGRAMFILES, 'Mozilla Firefox', 'firefox.exe'),
firefox_nightly: join(process.env.PROGRAMFILES, 'Firefox Nightly', 'firefox.exe'),
firefox: join(Deno.env.get('PROGRAMFILES'), 'Mozilla Firefox', 'firefox.exe'),
firefox_nightly: join(Deno.env.get('PROGRAMFILES'), 'Firefox Nightly', 'firefox.exe'),
},
linux: { // these should be in path so just use the name of the binary
@ -31,13 +35,13 @@ const browserPaths = ({
chromium: [ 'chromium', 'chromium-browser' ],
firefox: 'firefox',
}
})[process.platform];
})[Deno.build.os];
let _binariesInPath; // cache as to avoid excessive reads
const getBinariesInPath = async () => {
if (_binariesInPath) return _binariesInPath;
return _binariesInPath = (await Promise.all(process.env.PATH
return _binariesInPath = (await Promise.all(Deno.env.get('PATH')
.replaceAll('"', '')
.split(delimiter)
.filter(Boolean)
@ -65,7 +69,7 @@ const findBrowserPath = async (forceBrowser) => {
if (forceBrowser) return [ await getBrowserPath(forceBrowser), forceBrowser ];
for (const x in browserPaths) {
if (process.argv.includes('--' + x) || process.argv.includes('--' + x.split('_')[0])) return [ await getBrowserPath(x), x ];
if (Deno.args.includes('--' + x) || Deno.args.includes('--' + x.split('_')[0])) return [ await getBrowserPath(x), x ];
}
for (const x in browserPaths) {

View File

@ -1,4 +1,4 @@
import { spawn } from 'child_process';
import { spawn } from 'https://deno.land/std@0.170.0/node/child_process.ts';
import ConnectCDP from '../lib/cdp.js';
import InjectInto from './inject.js';
@ -16,9 +16,6 @@ export default async (browserPath, args, transport, extra) => {
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']
});
proc.stdout.pipe(proc.stdout);
proc.stderr.pipe(proc.stderr);
log(`connecting to CDP over ${transport === 'stdio' ? 'stdio pipe' : `websocket (${port})`}...`);
let CDP;
@ -28,6 +25,7 @@ export default async (browserPath, args, transport, extra) => {
break;
case 'stdio':
console.log('pain', proc.stdio);
const { 3: pipeWrite, 4: pipeRead } = proc.stdio;
CDP = await ConnectCDP({ pipe: { pipeWrite, pipeRead } });
break;

View File

@ -1,5 +1,4 @@
import WebSocket from 'ws';
import { get } from 'http';
import { get } from 'https://deno.land/std@0.170.0/node/http.ts';
export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
let messageCallbacks = [], onReply = {};
@ -46,7 +45,8 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
const continualTrying = func => new Promise(resolve => {
const attempt = async () => {
try {
process.stdout.write('.');
console.log('a');
// process.stdout.write('.');
resolve(await func());
} catch (e) { // fail, wait 100ms and try again
await new Promise(res => setTimeout(res, 200));
@ -75,12 +75,13 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
log('got target', target);
// await new Promise(res => setTimeout(res, 2000));
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise(resolve => ws.on('open', resolve));
await new Promise(resolve => ws.onopen = resolve);
_send = data => ws.send(data);
ws.on('message', data => onMessage(data));
ws.onmessage = ({ data }) => onMessage(data);
} else {
let pending = '';
pipeRead.on('data', buf => {

View File

@ -1,6 +1,6 @@
import { exec } from 'child_process';
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
const getProcesses = async containing => process.platform !== 'win32' ? Promise.resolve([]) : new Promise(resolve => exec(`wmic process get Commandline,ProcessID /format:csv`, (e, out) => {
const getProcesses = async containing => Deno.build.os !== 'windows' ? Promise.resolve([]) : new Promise(resolve => exec(`wmic process get Commandline,ProcessID /format:csv`, (e, out) => {
resolve(out.toString().split('\r\n').slice(2).map(x => {
const parsed = x.trim().split(',').slice(1).reverse();
@ -11,7 +11,7 @@ const getProcesses = async containing => process.platform !== 'win32' ? Promise.
}).filter(x => x[1] && x[1].includes(containing)));
}));
const killProcesses = async pids => process.platform !== 'win32' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
const killProcesses = async pids => Deno.build.os !== 'windows' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
export default async (CDP, { browserType, dataPath }) => {
if (browserType !== 'chromium') { // current implementation is for chromium-based only
@ -57,7 +57,7 @@ export default async (CDP, { browserType, dataPath }) => {
const { windowId } = await CDP.send('Browser.getWindowForTarget');
let autoEnabled = process.argv.includes('--force-auto-idle'), autoOptions = {
let autoEnabled = Deno.args.includes('--force-auto-idle'), autoOptions = {
timeMinimizedToHibernate: 5
};

View File

@ -4,20 +4,20 @@ if (window.Gluon) return;
let onIPCReply = {}, ipcListeners = {};
window.Gluon = {
versions: {
gluon: '${process.versions.gluon}',
gluon: '${Deno.version.gluon}',
builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}',
node: '${process.versions.node}',
browser: '${browserInfo.product.split('/')[1]}',
deno: '${Deno.version.deno}',
browser: '${browserInfo?.product.split('/')[1]}',
browserType: '${browserName.startsWith('Firefox') ? 'firefox' : 'chromium'}',
product: '${browserName}',
js: {
node: '${process.versions.v8}',
browser: '${browserInfo.jsVersion}'
deno: '${Deno.version.v8}',
browser: '${browserInfo?.jsVersion}'
},
embedded: {
node: ${'EMBEDDED_NODE' === 'true' ? 'true' : 'false'},
deno: ${'EMBEDDED_DENO' === 'true' ? 'true' : 'false'},
browser: false
}
},