diff --git a/.gitignore b/.gitignore index 1305422..00e30f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# miscellaneous +.DS_Store + # node node_modules package-lock.json @@ -7,4 +10,7 @@ pnpm-lock.yaml # gluon build -chrome_data \ No newline at end of file +gluon_data + +# vscode +.vscode diff --git a/README.md b/README.md index 296ba76..15d02f1 100644 --- a/README.md +++ b/README.md @@ -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) +

+ +Gluon +

-**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). + + +[![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) + + + +**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 - +- **Cross-platform** - Gluon works on Windows, Linux, and macOS (WIP) + ![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) diff --git a/assets/logo.png b/assets/logo.png index 5e9e20f..9ac8aef 100644 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/assets/logo_square.png b/assets/logo_square.png new file mode 100644 index 0000000..90e9a14 Binary files /dev/null and b/assets/logo_square.png differ diff --git a/changelog.md b/changelog.md index 82cab0c..9bf0c6e 100644 --- a/changelog.md +++ b/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 diff --git a/gluon.d.ts b/gluon.d.ts index 4c48abe..96b4762 100644 --- a/gluon.d.ts +++ b/gluon.d.ts @@ -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 + ) => Promise, + + /** Promise for waiting until the page has loaded. */ + loaded: Promise }; 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 }; @@ -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'; diff --git a/gluworld/index.js b/gluworld/index.js index 9ee8652..0a1b5f9 100644 --- a/gluworld/index.js +++ b/gluworld/index.js @@ -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 ] }); diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..ae4288f --- /dev/null +++ b/mod.ts @@ -0,0 +1,2 @@ +// @deno-types="./gluon.d.ts" +export * from "./src/index.js" \ No newline at end of file diff --git a/package.json b/package.json index 21c7bff..6bbb08e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/roadmap.md b/roadmap.md index fc50bca..263cfca 100644 --- a/roadmap.md +++ b/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 diff --git a/src/api/idle.js b/src/api/idle.js index 620c920..f7ef7bb 100644 --- a/src/api/idle.js +++ b/src/api/idle.js @@ -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, diff --git a/src/browser/firefox.js b/src/browser/firefox.js index 70a3d56..124f582 100644 --- a/src/browser/firefox.js +++ b/src/browser/firefox.js @@ -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); diff --git a/src/index.js b/src/index.js index 4add195..27562f7 100644 --- a/src/index.js +++ b/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; }; diff --git a/src/launcher/inject.js b/src/launcher/inject.js index da9020e..2851ae8 100644 --- a/src/launcher/inject.js +++ b/src/launcher/inject.js @@ -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; }; \ No newline at end of file diff --git a/src/lib/cdp.js b/src/lib/cdp.js index 7894ff4..ad1fe9e 100644 --- a/src/lib/cdp.js +++ b/src/lib/cdp.js @@ -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); diff --git a/src/lib/ipc.js b/src/lib/ipc.js index d96a35a..ed1c8a2 100644 --- a/src/lib/ipc.js +++ b/src/lib/ipc.js @@ -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 + ]; }; \ No newline at end of file