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:
parent
a86305eb41
commit
c4c0436634
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
|
||||
|
24
README.md
24
README.md
@ -1,17 +1,29 @@
|
||||
# Gluon (experimental Deno edition)
|
||||
[](https://choosealicense.com/licenses/mit/l) [](https://github.com/sponsors/CanadaHonk) [](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">
|
||||
|
||||
[](https://choosealicense.com/licenses/mit)
|
||||
[](https://www.npmjs.com/package/@gluon-framework/gluon)
|
||||
[](https://github.com/sponsors/CanadaHonk)
|
||||
[](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 -->
|
||||
|
||||

|
||||
|
||||
|
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 31 KiB |
BIN
assets/logo_square.png
Normal file
BIN
assets/logo_square.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
11
changelog.md
11
changelog.md
@ -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
41
gluon.d.ts
vendored
@ -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';
|
||||
|
@ -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
2
mod.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// @deno-types="./gluon.d.ts"
|
||||
export * from "./src/index.js"
|
@ -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",
|
||||
|
10
roadmap.md
10
roadmap.md
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
100
src/index.js
100
src/index.js
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
];
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user