update Deno branch to 0.10.0 (#31)

* cdp: add internal close api

* inject: add Window.close() api

* typedef: add Window.close()

* roadmap: add close api for 0.8.0

* release: 0.8.0

* readme: update project age

* paths: add more linux browsers

* readme: update screenshot

* readme: move trying gluon to earlier up

* inject: add useSessionId to Window.cdp.send()

* typedef: add useSessionId to CDPApi.send()

* idle: rewrite to use CDP to get processes instead of exec

* index: remove now unneeded parameters for idle creation

* chore: bump version to 0.9.0-alpha.0

* inject: pass IPC browser engine

* api: add Window.versions

* typedef: add future BrowserEngine and update Browser values

* typedef: add Window.versions

* index: rename internal variable

* idle: v2 - added sleep, utils, documenting, and more

* typedef: update for IdleApi v2

* roadmap: update

* chore: bump version to 0.9.0-alpha.2

* readme: add more feature specific statuses

* readme: make specific feature statuses into a table

* cdp: return protocol errors

* idle: move to new api dir

* controls: new Window.controls API

* darwin: full support (#4)

* meta: add pnpm-lock.yaml to .gitignore for my own sanity

* darwin: preliminary support

* chore: disable menubar key in firefox

* Add support new browsers Mac OS (#20)

Co-authored-by: a.artamonov <a.artamonov@sftpro.ru>

* roadmap: tweak

* roadmap: update done

* roadmap: remove overall

* typedef: change some voids to Promise<void>s

* typedef: add Window.controls

* release: 0.9.0

* add .vscode to gitignore

* assets: new logo

* readme: tweak features

* readme: fancy header

* readme: tweak description

* paths>win: add alternate appdata paths

* paths: add new browsers, refactor windows to auto append env

* readme: update npm link

* readme: fix license badge link

* paths: add brave

* chore: bump version to 0.10.0-alpha.2

* index: redo browser finding logging

* index: rewrite data path gen (#6)

* Added `.DS_Store` in .gitignore (#28)

* `.DS_Store` Removed

* Update .gitignore

* roadmap: update

* roadmap: add intended release date for 0.10

* launcher/inject: get browser info first, and always await

* roadmap: update

* cdp: make logging available via optional cli flag

* chore: bump version to 0.10.0-alpha.3

* chore: update alpha version in package

* inject: make eval return just result

* inject: add window.loaded promise

* ipc: add expose APIs

* roadmap: update

* gitignore: ignore gluon_data

* inject: rename Window.window -> Window.page

* typedef: rename Window.window -> page

* typedef: add PageApi.loaded

* typedef: add new IPC expose APIs

* idle: minor cleanup

* fix: hopefully remove all references to nodejs `process`

* idle: add close handler (also move API creation into inject)

* chore: bump version to alpha.5

* launcher: run Window.close on process close too

* fix: firefox was still refering to node's `process`

* gluworld: update to newest version from examples repo, fully convert to Deno

* typedefs: fix errors that deno lsp was complaining about

* release: v0.10.0

* deno: re-export src/index.ts in mod.ts to fit typical deno usage

Co-authored-by: CanadaHonk <oj@oojmed.com>
Co-authored-by: Beef <beefers@riseup.net>
Co-authored-by: Alexander Artamonov <47431914+artamonovtech@users.noreply.github.com>
Co-authored-by: a.artamonov <a.artamonov@sftpro.ru>
Co-authored-by: Mantresh Khurana <120998049+mantreshkhurana@users.noreply.github.com>
This commit is contained in:
Drake 2023-01-12 08:37:37 -08:00 committed by GitHub
parent a86305eb41
commit c4c0436634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 292 additions and 109 deletions

8
.gitignore vendored
View File

@ -1,3 +1,6 @@
# miscellaneous
.DS_Store
# node
node_modules
package-lock.json
@ -7,4 +10,7 @@ pnpm-lock.yaml
# gluon
build
chrome_data
gluon_data
# vscode
.vscode

View File

@ -1,17 +1,29 @@
# 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)
<h1 align="center">
<sub><img src="assets/logo.png" height="38" width="38"></sub>
Gluon
</h1>
**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).
<span align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://choosealicense.com/licenses/mit)
[![NPM version](https://img.shields.io/npm/v/@gluon-framework/gluon)](https://www.npmjs.com/package/@gluon-framework/gluon)
[![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)
</span>
**Gluon is a new 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).
## 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 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 Deno backend
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Node backend
- **Also supports Deno** (experimental) - Deno is also being worked as an option (developer choice) in replacement of NodeJS for the backend (you're here right now)
- **Fast build times** - Gluon has build times under 1 second on most machines for small projects
- **Actively developed** and **listening to feedback** - new updates are coming around weekly, 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 Deno or Chromium/etc to use them, it just uses normal versions -->
- **Cross-platform** - Gluon works on Windows, Linux, and macOS (WIP)
<!-- - **No forks needed** - Gluon doesn't need forks of Node or Chromium/etc to use them, it just uses normal versions -->
![Gluworld Screenshot showing Chrome Canary and Firefox Nightly being used at once using Deno.](https://user-images.githubusercontent.com/19228318/210020320-ff62e67f-0eb5-4e9e-989b-3ba4edc0fe35.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/logo_square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,13 +1,8 @@
# Gluon Changelog
## v0.9.0 [2023-01-03]
- New `Window.versions` API with browser version info
- New `Window.controls` API to manage window state (minimize/maximize/etc)
- New additions and improvements to `Window.idle`:
- `Window.idle.sleep()` now performs a light version of hibernation
- Now uses CDP commands instead of native to detect processes
- Added new `useSessionId` option to `Window.cdp.send()`, allowing to send browser-level CDP commands instead of just to target
- Added initial Mac support
## [v0.10.0 - 2023-01-05](https://gluonjs.org/blog/gluon-v0.10/)
## [v0.9.0 - 2023-01-03](https://gluonjs.org/blog/gluon-v0.9/)
## v0.8.0 [2022-12-30]
- Rewrote browser detection to support more setups

41
gluon.d.ts vendored
View File

@ -1,4 +1,4 @@
type WindowApi = {
type PageApi = {
/**
* Evaluate a string or function in the web context.
* @returns Return value of expression given.
@ -6,7 +6,10 @@ type WindowApi = {
eval: (
/** String or function to evaluate. */
expression: string|Function
) => Promise<any>
) => Promise<any>,
/** Promise for waiting until the page has loaded. */
loaded: Promise<void>
};
type IPCApi = {
@ -34,6 +37,26 @@ type IPCApi = {
* @returns Optionally with what to reply with, otherwise null by default.
*/
callback: (data: any) => any
): void,
/**
* Expose a Node function to the web context, acts as a wrapper around IPC events.
* Can be ran in window with Gluon.ipc[key](...args)
*/
expose(
/** Key name to expose to. */
key: string,
/** Handler function which is called from the web context. */
handler: Function
): void,
/**
* Unexpose (remove) a Node function previously exposed using expose().
*/
unexpose(
/** Key name to unexpose (remove). */
key: string
): void
};
@ -51,7 +74,7 @@ type CDPApi = {
params?: Object,
/** Send session ID with the command (default true). */
useSessionId?: Boolean = true
useSessionId?: Boolean
): Promise<any>
};
@ -134,8 +157,8 @@ type ControlsApi = {
}
type Window = {
/** API for accessing the window itself. */
window: WindowApi,
/** API for the page of the window. */
page: PageApi,
/** API for IPC. */
ipc: IPCApi,
@ -161,7 +184,13 @@ type Window = {
/** A browser that Gluon supports. */
type Browser = 'chrome'|'chrome_canary'|'chromium'|'chromium_snapshot'|'edge'|'firefox'|'firefox_nightly';
type Browser =
'chrome'|'chrome_beta'|'chrome_dev'|'chrome_canary'|
'chromium'|'chromium_snapshot'|
'edge'|'edge_beta'|'edge_dev'|'edge_canary'|
'firefox'|'firefox_nightly'|
'thorium'|
'librewolf';
/** A browser engine that Gluon supports. */
type BrowserEngine = 'chromium'|'firefox';

View File

@ -1,15 +1,28 @@
import * as Gluon from '../src/index.js';
import * as Gluon from '../mod.ts';
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 __dirname = new URL(".", import.meta.url).pathname;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dirSize = async dir => {
const files = Array.from(await Deno.readDir(dir, { withFileTypes: true }));
const paths = files.map(async file => {
const path = `${dir}/${file.name}`;
if (file.isDirectory()) return await dirSize(path);
if (file.isFile()) return (await Deno.stat(path)).size;
return 0;
});
return (await Promise.all(paths)).flat(Infinity).reduce((acc, x) => acc + x, 0);
};
(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, {
const browsers = Deno.args.slice(1).filter(x => !x.startsWith('-'));
if (browsers.length > 0) { // use argv as browsers to use
for (const forceBrowser of browsers) {
await Gluon.open(new URL(`file://${__dirname}/index.html`).href, {
windowSize: [ 800, 500 ],
forceBrowser
});
@ -18,7 +31,7 @@ const __dirname = dirname(__filename);
return;
}
const Browser = await Gluon.open(pathToFileURL(join(__dirname, 'index.html')).href, {
const Browser = await Gluon.open(new URL(`file://${__dirname}/index.html`).href, {
windowSize: [ 800, 500 ]
});

2
mod.ts Normal file
View File

@ -0,0 +1,2 @@
// @deno-types="./gluon.d.ts"
export * from "./src/index.js"

View File

@ -1,6 +1,6 @@
{
"name": "@gluon-framework/gluon",
"version": "0.9.0",
"version": "0.10.0",
"description": "Make websites into desktop apps with system installed browsers and NodeJS.",
"main": "src/index.js",
"types": "gluon.d.ts",

View File

@ -3,9 +3,15 @@
> **Note** |
> Want more info on what some of these mean/are? Ask in [our Discord](https://discord.gg/RFtUCA8fST)!
## v0.11.0
- [ ] Resources/Injection API
## v0.10.0
- [ ] Resources Window API
- [ ] On/await load Window API
- [X] Rewrite data path generation
- [X] Rewrite browser path generation for Windows and add more browsers
- [X] Clean up logging, minor internal cleanup/simplifying/rewriting
- [X] IPC v2 (Expose API)
- [X] Await page load API
## v0.9.0
- [X] Browser version info API

View File

@ -1,10 +1,10 @@
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
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 }) => {
if (browserType !== 'chromium') { // current implementation is for chromium-based only
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserType})`);
export default async (CDP, { browserEngine, closeHandlers }) => {
if (browserEngine !== 'chromium') { // current implementation is for chromium-based only
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserEngine})`);
return {
hibernate: warning,
@ -44,7 +44,7 @@ export default async (CDP, { browserType }) => {
let wakeUrl, hibernating = false;
const hibernate = async () => { // hibernate - crashing chromium internally to save max memory. users will see a crash/gone wrong page but we hopefully "reload" quick enough once visible again for not much notice.
if (hibernating) return;
if (process.platform !== 'win32') return sleep(); // sleep instead - full hibernation is windows only for now due to needing to do native things
if (Deno.build.os !== 'windows') return sleep(); // sleep instead - full hibernation is windows only for now due to needing to do native things
hibernating = true;
@ -129,6 +129,7 @@ export default async (CDP, { browserType }) => {
log('stopped auto idle');
};
let lastScreenshot, takingScreenshot = false;
const screenshotInterval = setInterval(async () => {
if (takingScreenshot) return;
@ -140,12 +141,15 @@ export default async (CDP, { browserType }) => {
getScreenshot().then(x => lastScreenshot = x);
closeHandlers.push(() => {
clearInterval(screenshotInterval);
stopAuto();
});
log(`idle API active (window id: ${windowId})`);
if (autoEnabled) startAuto();
const setWindowState = async state => await CDP.send('Browser.setWindowBounds', { windowId, bounds: { windowState: state }});
return {
hibernate,
sleep,

View File

@ -19,7 +19,7 @@ user_pref('privacy.resistFingerprinting', true);
user_pref('fission.bfcacheInParent', false);
user_pref('fission.webContentIsolationStrategy', 0);
user_pref('ui.key.menuAccessKeyFocuses', false);
${process.platform === 'darwin' ? `user_pref('browser.tabs.inTitlebar', 0);` : `` }
${Deno.build.os === 'darwin' ? `user_pref('browser.tabs.inTitlebar', 0);` : `` }
`);
// user_pref('privacy.resistFingerprinting', false);

View File

@ -3,7 +3,7 @@ window.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args
Deno.version = { // have to do this because... Deno
...Deno.version,
gluon: '0.9.0-deno-dev'
gluon: '0.10.0-deno'
};
import { join, dirname, delimiter, sep } from 'https://deno.land/std@0.170.0/node/path.ts';
@ -13,45 +13,91 @@ 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';
import IdleAPI from './api/idle.js';
import ControlsAPI from './api/controls.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const browserPaths = ({
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'),
windows: Deno.build.os === 'windows' && { // windows paths are automatically prepended with program files, program files (x86), and local appdata if a string, see below
chrome: join('Google', 'Chrome', 'Application', 'chrome.exe'),
chrome_beta: join('Google', 'Chrome Beta', 'Application', 'chrome.exe'),
chrome_dev: join('Google', 'Chrome Dev', 'Application', 'chrome.exe'),
chrome_canary: join('Google', 'Chrome SxS', 'Application', 'chrome.exe'),
firefox: join(Deno.env.get('PROGRAMFILES'), 'Mozilla Firefox', 'firefox.exe'),
firefox_nightly: join(Deno.env.get('PROGRAMFILES'), 'Firefox Nightly', 'firefox.exe'),
chromium: join('Chromium', 'Application', 'chrome.exe'),
edge: join('Microsoft', 'Edge', 'Application', 'msedge.exe'),
edge_beta: join('Microsoft', 'Edge Beta', 'Application', 'msedge.exe'),
edge_dev: join('Microsoft', 'Edge Dev', 'Application', 'msedge.exe'),
edge_canary: join('Microsoft', 'Edge SxS', 'Application', 'msedge.exe'),
thorium: join('Thorium', 'Application', 'thorium.exe'),
brave: join('BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
firefox: join('Mozilla Firefox', 'firefox.exe'),
firefox_developer: join('Firefox Developer Edition', 'firefox.exe'),
firefox_nightly: join('Firefox Nightly', 'firefox.exe'),
librewolf: join('LibreWolf', 'librewolf.exe'),
},
linux: { // these should be in path so just use the name of the binary
chrome: [ 'chrome', 'google-chrome', 'chrome-browser', 'google-chrome-stable' ],
chrome_canary: [ 'chrome-canary', 'google-chrome-canary', 'google-chrome-unstable', 'chrome-unstable' ],
chrome_beta: [ 'chrome-beta', 'google-chrome-beta', 'chrome-beta-browser', 'chrome-browser-beta' ],
chrome_dev: [ 'chrome-unstable', 'google-chrome-unstable', 'chrome-unstable-browser', 'chrome-browser-unstable' ],
chrome_canary: [ 'chrome-canary', 'google-chrome-canary', 'chrome-canary-browser', 'chrome-browser-canary' ],
chromium: [ 'chromium', 'chromium-browser' ],
chromium_snapshot: [ 'chromium-snapshot', 'chromium-snapshot-bin' ],
firefox: 'firefox',
firefox_nightly: 'firefox-nightly'
edge: [ 'microsoft-edge', 'microsoft-edge-stable', 'microsoft-edge-browser' ],
edge_beta: [ 'microsoft-edge-beta', 'microsoft-edge-browser-beta', 'microsoft-edge-beta-browser' ],
edge_dev: [ 'microsoft-edge-dev', 'microsoft-edge-browser-dev', 'microsoft-edge-dev-browser' ],
edge_canary: [ 'microsoft-edge-canary', 'microsoft-edge-browser-canary', 'microsoft-edge-canary-browser' ],
thorium: [ 'thorium', 'thorium-browser' ],
brave: [ 'brave', 'brave-browser' ],
firefox: [ 'firefox', 'firefox-browser' ],
firefox_nightly: [ 'firefox-nightly', 'firefox-nightly-browser', 'firefox-browser-nightly' ],
librewolf: [ 'librewolf', 'librewolf-browser' ]
},
darwin: {
chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
chrome_beta: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome Beta',
chrome_dev: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome Dev',
chrome_canary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
edge_beta: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge Beta',
edge_dev: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge Dev',
edge_canary: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge Canary',
brave: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
firefox_nightly: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox'
}
})[Deno.build.os];
if (Deno.build.os === 'windows') { // windows: automatically generate env-based paths if not arrays
for (const browser in browserPaths) {
if (!Array.isArray(browserPaths[browser])) {
const basePath = browserPaths[browser];
browserPaths[browser] = [
join(Deno.env.get('PROGRAMFILES'), basePath),
join(Deno.env.get('LOCALAPPDATA'), basePath),
join(Deno.env.get('PROGRAMFILES(x86)'), basePath)
];
}
}
}
let _binariesInPath; // cache as to avoid excessive reads
const getBinariesInPath = async () => {
if (_binariesInPath) return _binariesInPath;
@ -72,7 +118,7 @@ const exists = async path => {
const getBrowserPath = async browser => {
for (const path of Array.isArray(browserPaths[browser]) ? browserPaths[browser] : [ browserPaths[browser] ]) {
log('checking if ' + browser + ' exists:', path, await exists(path));
// log('checking if ' + browser + ' exists:', path, await exists(path));
if (await exists(path)) return path;
}
@ -97,21 +143,28 @@ const findBrowserPath = async (forceBrowser) => {
};
const getFriendlyName = whichBrowser => whichBrowser[0].toUpperCase() + whichBrowser.slice(1).replace(/[a-z]_[a-z]/g, _ => _[0] + ' ' + _[2].toUpperCase());
const getDataPath = () => join(__dirname, '..', 'chrome_data');
const ranJsDir = !Deno.args[0] ? __dirname : (Deno.args[0].endsWith('.js') ? dirname(Deno.args[0]) : Deno.args[0]);
const getDataPath = browser => join(ranJsDir, 'gluon_data', browser);
const getBrowserType = name => { // todo: not need this
if (name.startsWith('firefox') ||
[ 'librewolf' ].includes(name)) return 'firefox';
return 'chromium';
};
const startBrowser = async (url, { windowSize, forceBrowser }) => {
const dataPath = getDataPath();
const [ browserPath, browserName ] = await findBrowserPath(forceBrowser);
const browserFriendlyName = getFriendlyName(browserName);
log('browser path:', browserPath);
log('data path:', dataPath);
if (!browserPath) return log('failed to find a good browser install');
const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium';
const dataPath = getDataPath(browserName);
const browserType = getBrowserType(browserName);
log('found browser', browserName, `(${browserType} based)`, 'at path:', browserPath);
log('data path:', dataPath);
const Window = await (browserType === 'firefox' ? Firefox : Chromium)({
browserName: browserFriendlyName,
@ -122,9 +175,6 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => {
windowSize
});
Window.idle = await IdleAPI(Window.cdp, { browserType });
Window.controls = await ControlsAPI(Window.cdp);
return Window;
};

View File

@ -1,34 +1,33 @@
import IPCApi from '../lib/ipc.js';
import IdleApi from '../api/idle.js';
import ControlsApi from '../api/controls.js';
export default async (CDP, proc, injectionType = 'browser', { browserName } = { browserName: 'unknown' }) => {
let pageLoadCallback = () => {}, onWindowMessage = () => {};
let pageLoadCallback, pageLoadPromise = new Promise(res => pageLoadCallback = res);
let frameLoadCallback = () => {}, onWindowMessage = () => {};
CDP.onMessage(msg => {
if (msg.method === 'Runtime.bindingCalled' && msg.params.name === '_gluonSend') onWindowMessage(JSON.parse(msg.params.payload));
if (msg.method === 'Page.frameStoppedLoading') pageLoadCallback(msg.params);
if (msg.method === 'Page.frameStoppedLoading') frameLoadCallback(msg.params);
if (msg.method === 'Page.loadEventFired') pageLoadCallback();
if (msg.method === 'Runtime.executionContextCreated') injectIPC(); // ensure IPC injection again
});
const browserInfo = await CDP.sendMessage('Browser.getVersion');
log('browser:', browserInfo.product);
let browserInfo, sessionId;
const browserEngine = browserInfo.jsVersion.startsWith('1.') ? 'firefox' : 'chromium';
let sessionId;
if (injectionType === 'browser') { // connected to browser itself, need to get and attach to a target
CDP.sendMessage('Browser.getVersion').then(x => { // get browser info async as we have time while attaching
browserInfo = x;
log('browser:', x.product);
});
const target = (await CDP.sendMessage('Target.getTargets')).targetInfos[0];
sessionId = (await CDP.sendMessage('Target.attachToTarget', {
targetId: target.targetId,
flatten: true
})).sessionId;
} else { // already attached to target
browserInfo = await CDP.sendMessage('Browser.getVersion');
log('browser:', browserInfo.product);
}
const browserEngine = browserInfo.jsVersion.startsWith('1.') ? 'firefox' : 'chromium';
CDP.sendMessage('Runtime.enable', {}, sessionId); // enable runtime API
@ -36,11 +35,9 @@ export default async (CDP, proc, injectionType = 'browser', { browserName } = {
name: '_gluonSend'
}, sessionId);
const evalInWindow = async func => {
return await CDP.sendMessage(`Runtime.evaluate`, {
expression: typeof func === 'string' ? func : `(${func.toString()})()`
}, sessionId);
};
const evalInWindow = async func => (await CDP.sendMessage(`Runtime.evaluate`, {
expression: typeof func === 'string' ? func : `(${func.toString()})()`
}, sessionId)).result.value;
const [ ipcMessageCallback, injectIPC, IPC ] = await IPCApi({
@ -50,22 +47,35 @@ export default async (CDP, proc, injectionType = 'browser', { browserName } = {
}, {
evalInWindow,
evalOnNewDocument: source => CDP.sendMessage('Page.addScriptToEvaluateOnNewDocument', { source }, sessionId),
pageLoadPromise: new Promise(res => pageLoadCallback = res)
pageLoadPromise: new Promise(res => frameLoadCallback = res)
});
onWindowMessage = ipcMessageCallback;
log('finished setup');
evalInWindow('document.readyState').then(readyState => { // check if already loaded, if so trigger page load promise
if (readyState === 'complete' || readyState === 'ready') pageLoadCallback();
});
const generateVersionInfo = (name, version) => ({
name,
version,
major: parseInt(version.split('.')[0])
});
return {
window: {
const versions = {
product: generateVersionInfo(browserName, browserInfo.product.split('/')[1]),
engine: generateVersionInfo(browserEngine, browserInfo.product.split('/')[1]),
jsEngine: generateVersionInfo(browserEngine === 'chromium' ? 'v8' : 'spidermonkey', browserInfo.jsVersion)
};
const closeHandlers = [];
const Window = {
page: {
eval: evalInWindow,
loaded: pageLoadPromise
},
ipc: IPC,
@ -75,14 +85,19 @@ export default async (CDP, proc, injectionType = 'browser', { browserName } = {
},
close: () => {
for (const handler of closeHandlers) handler(); // extra api handlers which need to be closed
CDP.close();
proc.kill();
},
versions: {
product: generateVersionInfo(browserName, browserInfo.product.split('/')[1]),
engine: generateVersionInfo(browserEngine, browserInfo.product.split('/')[1]),
jsEngine: generateVersionInfo(browserEngine === 'chromium' ? 'v8' : 'spidermonkey', browserInfo.jsVersion)
}
versions
};
proc.on('close', Window.close);
Window.idle = await IdleApi(Window.cdp, { browserEngine, closeHandlers });
Window.controls = await ControlsApi(Window.cdp);
return Window;
};

View File

@ -1,5 +1,7 @@
import { get } from 'https://deno.land/std@0.170.0/node/http.ts';
const logCDP = Deno.args.includes('--cdp-logging');
export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
let messageCallbacks = [], onReply = {};
const onMessage = msg => {
@ -7,7 +9,7 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
msg = JSON.parse(msg);
// log('received', msg);
if (logCDP) log('received', msg);
if (onReply[msg.id]) {
onReply[msg.id](msg);
delete onReply[msg.id];
@ -37,7 +39,7 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
_send(JSON.stringify(msg));
// log('sent', msg);
if (logCDP) log('sent', msg);
const reply = await new Promise(res => {
onReply[id] = msg => res(msg);

View File

@ -127,21 +127,70 @@ delete window._gluonSend;
sendToWindow('pong', null, id); // send simple pong to confirm
};
let API = {
on: (type, cb) => {
if (!ipcListeners[type]) ipcListeners[type] = [];
ipcListeners[type].push(cb);
},
removeListener: (type, cb) => {
if (!ipcListeners[type]) return false;
ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1);
if (ipcListeners[type].length === 0) delete ipcListeners[type]; // clean up - remove type from listeners if 0 listeners left
},
send: sendToWindow,
};
// Expose API
const makeExposeKey = key => 'exposed ' + key;
const expose = (key, func) => {
if (typeof func !== 'function') return new Error('Invalid arguments (expected key and function)');
const exposeKey = makeExposeKey(key);
API.on(exposeKey, args => func(args)); // handle IPC events
evalInWindow(`Gluon.ipc['${key}'] = (...args) => Gluon.ipc.send('${exposeKey}', args)`); // add wrapper func to window
};
const unexpose = key => {
const exposeKey = makeExposeKey(key);
const existed = API.removeListener(exposeKey); // returns false if type isn't registered/active
if (!existed) return;
evalInWindow(`delete Gluon.ipc['${key}']`); // remove wrapper func from window
};
API.expose = (...args) => {
if (args.length === 1) { // given object to expose
for (const key in args[0]) expose(key, args[0][key]); // expose all keys given
return;
}
if (args.length === 2) return expose(args[0], args[1]);
return new Error('Invalid arguments (expected object or key and function)');
};
API.unexpose = unexpose;
API = new Proxy(API, { // setter and deleter API
set(obj, key, value) {
expose(key, value);
},
deleteProperty(obj, key) {
unexpose(key);
}
});
return [
onWindowMessage,
() => evalInWindow(injection),
{
on: (type, cb) => {
if (!ipcListeners[type]) ipcListeners[type] = [];
ipcListeners[type].push(cb);
},
removeListener: (type, cb) => {
if (!ipcListeners[type]) return false;
ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1);
},
send: sendToWindow,
} ];
API
];
};