Repository: GoogleChromeLabs/carlo
Branch: master
Commit: 8f2cbfedf381
Files: 47
Total size: 161.7 KB
Directory structure:
gitextract_4gq2v2ts/
├── .gitignore
├── .npmignore
├── API.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples/
│ ├── photobooth/
│ │ ├── README.md
│ │ ├── main.js
│ │ ├── package.json
│ │ └── www/
│ │ └── index.html
│ ├── systeminfo/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── main.js
│ │ ├── package.json
│ │ ├── test.js
│ │ └── www/
│ │ └── index.html
│ ├── terminal/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── main.js
│ │ ├── package.json
│ │ ├── worker.js
│ │ └── www/
│ │ └── index.html
│ └── windows/
│ ├── README.md
│ ├── main.html
│ ├── main.js
│ └── package.json
├── index.js
├── lib/
│ ├── carlo.js
│ ├── color.js
│ ├── features/
│ │ ├── file_info.js
│ │ └── shortcuts.js
│ ├── find_chrome.js
│ └── http_request.js
├── package.json
├── rpc/
│ ├── index.js
│ ├── rpc.js
│ ├── rpc.md
│ ├── rpc_process.js
│ └── test.js
└── test/
├── app.spec.js
├── color.spec.js
├── folder/
│ ├── index.html
│ └── redirect.html
├── headful.js
├── http/
│ └── index.html
└── test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
.eslintrc.js
.eslintignore
.profile
.vscode
node_modules
package-lock.json
rpc/node_modules
rpc/package-lock.json
lib/.local-data
================================================
FILE: .npmignore
================================================
# repeats from .gitignore
.DS_Store
.eslintrc.js
.eslintignore
.profile
.vscode
node_modules
package-lock.json
rpc/node_modules
rpc/package-lock.json
lib/.local-data
.npmignore
examples
CONTRIBUTING.md
API.md
rpc/test.js
test
================================================
FILE: API.md
================================================
## API v0.9
> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is validated, it will be bumped to v1.0 and preserved for backwards compatibility.
##### Node side API
- [carlo.enterTestMode()](#carloentertestmode)
- [carlo.launch([options])](#carlolaunchoptions)
- [class: App](#class-app)
* [event: 'exit'](#event-exit)
* [event: 'window'](#event-window)
* [App.browserForTest()](#appbrowserfortest)
* [App.createWindow(options)](#appcreatewindowoptions)
* [App.evaluate(pageFunction[, ...args])](#appevaluatepagefunction-args)
* [App.exit()](#appexit)
* [App.exposeFunction(name, carloFunction)](#appexposefunctionname-carlofunction)
* [App.load(uri[, ...params])](#apploaduri-params)
* [App.mainWindow()](#appmainwindow)
* [App.serveFolder(folder[, prefix])](#appservefolderfolder-prefix)
* [App.serveHandler(handler)](#appservehandlerhandler)
* [App.serveOrigin(base[, prefix])](#appserveoriginbase-prefix)
* [App.setIcon(image)](#appseticonimage)
* [App.windows()](#appwindows)
- [class: HttpRequest](#class-httprequest)
* [HttpRequest.abort()](#httprequestabort)
* [HttpRequest.continue()](#httprequestcontinue)
* [HttpRequest.fail()](#httprequestfail)
* [HttpRequest.fulfill(options)](#httprequestfulfilloptions)
* [HttpRequest.headers()](#httprequestheaders)
* [HttpRequest.method()](#httprequestmethod)
* [HttpRequest.url()](#httprequesturl)
- [class: Window](#class-window)
* [event: 'close'](#event-close)
* [Window.bounds()](#windowbounds)
* [Window.bringToFront()](#windowbringtofront)
* [Window.close()](#windowclose)
* [Window.evaluate(pageFunction[, ...args])](#windowevaluatepagefunction-args)
* [Window.exposeFunction(name, carloFunction)](#windowexposefunctionname-carlofunction)
* [Window.fullscreen()](#windowfullscreen)
* [Window.load(uri[, ...params])](#windowloaduri-params)
* [Window.maximize()](#windowmaximize)
* [Window.minimize()](#windowminimize)
* [Window.pageForTest()](#windowpagefortest)
* [Window.paramsForReuse()](#windowparamsforreuse)
* [Window.serveFolder(folder[, prefix])](#windowservefolderfolder-prefix)
* [Window.serveHandler(handler)](#windowservehandlerhandler)
* [Window.serveOrigin(base[, prefix])](#windowserveoriginbase-prefix)
* [Window.setBounds(bounds)](#windowsetboundsbounds)
##### Web side API
- [carlo.fileInfo(file)](#carlofileinfofile)
- [carlo.loadParams()](#carloloadparams)
#### carlo.enterTestMode()
Enters headless test mode. In the test mode, Puppeteer browser and pages are available via
[App.browserForTest()](#appbrowserfortest) and [Window.pageForTest()](#windowpagefortest) respectively.
Please refer to the Puppeteer [documentation](https://pptr.dev) for details on headless testing.
#### carlo.launch([options])
- `options` <[Object]> Set of configurable options to set on the app. Can have the following fields:
- `width` <[number]> App window width in pixels.
- `height` <[number]> App window height in pixels.
- `top`: <[number]> App window top offset in pixels.
- `left` <[number]> App window left offset in pixels.
- `bgcolor` <[string]> Background color using hex notation, defaults to `'#ffffff'`.
- `channel` <[Array]<[string]>> Browser to be used, defaults to `['stable']`:
- `'stable'` only uses locally installed stable channel Chrome.
- `'canary'` only uses Chrome SxS aka Canary.
- `'chromium'` downloads local version of Chromium compatible with the Puppeteer used.
- `'rXXXXXX'` a specific Chromium revision is used.
- `icon` <[Buffer]|[string]> Application icon to be used in the system dock. Either buffer containing PNG or a path to the PNG file on the file system. This feature is only available in Chrome M72+. One can use `'canary'` channel to see it in action before M72 hits stable.
- `paramsForReuse` <\*> Optional parameters to share between Carlo instances. See [Window.paramsForReuse](#windowparamsforreuse) for details.
- `title` <[string]> Application title.
- `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). This folder is created upon the first app launch and contains user settings and Web storage data. Defaults to `'.profile'`.
- `executablePath` <[string]> Path to a Chromium or Chrome executable to run instead of the automatically located Chrome. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). Carlo is only guaranteed to work with the latest Chrome stable version.
- `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](https://peter.sh/experiments/chromium-command-line-switches/).
- `return`: <[Promise]<[App]>> Promise which resolves to the app instance.
Launches the browser.
### class: App
#### event: 'exit'
Emitted when the last window closes.
#### event: 'window'
- <[Window]>
Emitted when the new window opens. This can happen in the following situations:
- [App.createWindow](#appcreatewindowoptions) was called.
- [carlo.launch](#carlolaunchoptions) was called from the same or another instance of the Node app.
- [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) was called from within the web page.
#### App.browserForTest()
- `return`: <[Browser]> Puppeteer browser object for testing.
#### App.createWindow([options])
- `options` <[Object]> Set of configurable options to set on the app. Can have the following fields:
- `width` <[number]> Window width in pixels, defaults to app width.
- `height` <[number]> Window height in pixels, defaults to app height.
- `top` <[number]> Window top in pixels, defaults to app top.
- `left` <[number]> Window left in pixels, defaults to app left.
- `bgcolor` <[string]> Background color using hex notation, defaults to app `bgcolor`.
- `return`: <[Promise]<[Window]>> Promise which resolves to the window instance.
Creates a new app window.
#### App.evaluate(pageFunction[, ...args])
Shortcut to the main window's [Window.evaluate(pageFunction[, ...args])](#windowevaluatepagefunction-args).
#### App.exit()
- `return`: <[Promise]>
Closes the browser window.
#### App.exposeFunction(name, carloFunction)
- `name` <[string]> Name of the function on the window object.
- `carloFunction` <[function]> Callback function which will be called in Carlo's context.
- `return`: <[Promise]>
The method adds a function called `name` on the pages' `window` object.
When called, the function executes `carloFunction` in Node.js and returns a [Promise] which resolves to the return value of `carloFunction`.
If the `carloFunction` returns a [Promise], it will be awaited.
> **NOTE** Functions installed via `App.exposeFunction` survive navigations.
An example of adding an `md5` function into the page:
`main.js`
```js
const carlo = require('carlo');
const crypto = require('crypto');
carlo.launch().then(async app => {
app.on('exit', () => process.exit());
app.serveFolder(__dirname);
await app.exposeFunction('md5', text => // <-- expose function
crypto.createHash('md5').update(text).digest('hex')
);
await app.load('index.html');
});
```
`index.html`
```html
<script>
md5('digest').then(result => document.body.textContent = result);
</script>
```
#### App.load(uri[, ...params])
Shortcut to the main window's [Window.load(uri[, ...params])](#windowloaduri-params).
#### App.mainWindow()
- `return`: <[Window]> Returns main window.
Running app guarantees to have main window. If current main window closes, a next open window
becomes the main one.
#### App.serveFolder(folder[, prefix])
- `folder` <[string]> Folder with web content to make available to Chrome.
- `prefix` <[string]> Prefix of the URL path to serve from the given folder.
Makes the content of the given folder available to the Chrome web app.
An example of adding a local `www` folder along with the `node_modules`:
`main.js`
```js
const carlo = require('carlo');
carlo.launch().then(async app => {
app.on('exit', () => process.exit());
app.serveFolder(`${__dirname}/www`);
app.serveFolder(`${__dirname}/node_modules`, 'node_modules');
await app.load('index.html');
});
```
***www***/`index.html`
```html
<style>body { white-space: pre; }</style>
<script>
fetch('node_modules/carlo/package.json')
.then(response => response.text())
.then(text => document.body.textContent = text);
</script>
```
#### App.serveHandler(handler)
- `handler` <[Function]> Network handler callback accepting the [HttpRequest](#class-httprequest) parameter.
An example serving primitive `index.html`:
```js
const carlo = require('carlo');
carlo.launch().then(async app => {
app.on('exit', () => process.exit());
app.serveHandler(request => {
if (request.url().endsWith('/index.html'))
request.fulfill({body: Buffer.from('<html>Hello World</hmtl>')});
else
request.continue(); // <-- user needs to resolve each request, otherwise it'll time out.
});
await app.load('index.html'); // <-- loads index.html served above.
});
```
Handler function is called with every network request in this app. It can abort, continue or fulfill each request. Only single app-wide handler can be registered.
#### App.serveOrigin(base[, prefix])
- `base` <[string]> Base to serve web content from.
- `prefix` <[string]> Prefix of the URL path to serve from the given folder.
Fetches Carlo content from the specified origin instead of reading it from the file system, eg `http://localhost:8080`. This mode can be used for the fast development mode available in web frameworks.
An example of adding the local `http://localhost:8080` origin:
```js
const carlo = require('carlo');
carlo.launch().then(async app => {
app.on('exit', () => process.exit());
app.serveOrigin('http://localhost:8080'); // <-- fetch from the local server
app.serveFolder(__dirname); // <-- won't be used
await app.load('index.html');
});
```
#### App.setIcon(image)
- `image`: <[Buffer]|[string]> Either buffer containing PNG or a path to the PNG file on the file system.
Specifies image to be used as an app icon in the system dock.
> This feature is only available in Chrome M72+. One can use `'canary'` channel to see it in action before M72 hits stable.
#### App.windows()
- `return`: <[Array]<[Window]>> Returns all currently opened windows.
Running app guarantees to have at least one open window.
### class: HttpRequest
Handlers registered via [App.serveHandler](#appservehandlerhandler) and [Window.serveHandler](#windowservehandlerhandler) receive parameter of this upon every network request.
#### HttpRequest.abort()
- `return`: <[Promise]>
Aborts request. If request is a navigation request, navigation is aborted as well.
#### HttpRequest.continue()
Proceeds with the default behavior for this request. Either serves it from the filesystem or defers to the browser.
#### HttpRequest.fail()
- `return`: <[Promise]>
Marks the request as failed. If request is a navigation request, navigation is still committed, but to a location that fails to be fetched.
#### HttpRequest.fulfill(options)
- `options`: <[Object]>
- `status` <[number]> HTTP status code (200, 304, etc), defaults to 200.
- `headers` <[Object]> HTTP response headers.
- `body` <[Buffer]> Response body.
- `return`: <[Promise]>
Fulfills the network request with the given data. `'Content-Length'` header is generated in case it is not listed in the headers.
#### HttpRequest.headers()
- `return`: <[Object]> HTTP headers
Network request headers.
#### HttpRequest.method()
- `return`: <[string]> HTTP method
HTTP method of this network request (GET, POST, etc).
#### HttpRequest.url()
- `return`: <[string]> HTTP URL
Network request URL.
### class: Window
#### event: 'close'
Emitted when the window closes.
#### Window.bounds()
- `return`: <[Promise]<[Object]>>
- `top` <[number]> Top offset in pixels.
- `left` <[number]> Left offset in pixels.
- `width` <[number]> Width in pixels.
- `height` <[number]> Height in pixels.
Returns window bounds.
#### Window.bringToFront()
- `return`: <[Promise]>
Brings this window to front.
#### Window.close()
- `return`: <[Promise]>
Closes this window.
#### Window.evaluate(pageFunction[, ...args])
- `pageFunction` <[function]|[string]> Function to be evaluated in the page context.
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`.
- `return`: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`.
If the function passed to the `Window.evaluate` returns a [Promise], then `Window.evaluate` would wait for the promise to resolve and return its value.
If the function passed to the `Window.evaluate` returns a non-[Serializable] value, then `Window.evaluate` resolves to `undefined`.
```js
const result = await window.evaluate(() => navigator.userAgent);
console.log(result); // prints "<UA>" in Node console
```
Passing arguments to `pageFunction`:
```js
const result = await window.evaluate(x => {
return Promise.resolve(8 * x);
}, 7);
console.log(result); // prints "56" in Node console
```
A string can also be passed in instead of a function:
```js
console.log(await window.evaluate('1 + 2')); // prints "3"
const x = 10;
console.log(await window.evaluate(`1 + ${x}`)); // prints "11"
```
#### Window.exposeFunction(name, carloFunction)
- `name` <[string]> Name of the function on the window object.
- `carloFunction` <[function]> Callback function which will be called in Carlo's context.
- `return`: <[Promise]>
Same as [App.exposeFunction](#appexposefunctionname-carlofunction), but only applies to
the current window.
> **NOTE** Functions installed via `Window.exposeFunction` survive navigations.
#### Window.fullscreen()
- `return`: <[Promise]>
Turns the window into the full screen mode. Behavior is platform-specific.
#### Window.load(uri[, ...params])
- `uri` <[string]> Path to the resource relative to the folder passed into [`serveFolder()`].
- `params` <\*> Optional parameters to pass to the web application. Parameters can be
primitive types, <[Array]>, <[Object]> or <[rpc]> `handles`.
- `return`: <[Promise]> Resolves upon DOMContentLoaded event in the web page.
Navigates the corresponding web page to the given `uri`, makes given `params` available in the web page via [carlo.loadParams()](#carloloadparams).
`main.js`
```js
const carlo = require('carlo');
const { rpc } = require('carlo/rpc');
carlo.launch().then(async app => {
app.serveFolder(__dirname);
app.on('exit', () => process.exit());
await app.load('index.html', rpc.handle(new Backend));
});
class Backend {
hello(name) {
console.log(`Hello ${name}`);
return 'Backend is happy';
}
setFrontend(frontend) {
// Node world can now use frontend RPC handle.
this.frontend_ = frontend;
}
}
```
`index.html`
```html
<script>
class Frontend {}
async function load(backend) {
// Web world can now use backend RPC handle.
console.log(await backend.hello('from frontend'));
await backend.setFrontend(rpc.handle(new Frontend));
}
</script>
<body>Open console</body>
```
#### Window.maximize()
- `return`: <[Promise]>
Maximizes the window. Behavior is platform-specific.
#### Window.minimize()
- `return`: <[Promise]>
Minimizes the window. Behavior is platform-specific.
#### Window.pageForTest()
- `return`: <[Page]> Puppeteer page object for testing.
#### Window.paramsForReuse()
- `return`: <\*> parameters.
Returns the `options.paramsForReuse` value passed into the [carlo.launch](#carlolaunchoptions).
These parameters are useful when Carlo app is started multiple times:
- First time the Carlo app is started, it successfully calls `carlo.launch` and opens the main window.
- Second time the Carlo app is started, `carlo.launch` fails with the 'browser is already running' exception.
- Despite the fact that second call to `carlo.launch` failed, a new window is created in the first Carlo app. This window contains `paramsForReuse` value that was specified in the second (failed) `carlo.launch` call.
This way app can pass initialization parameters such as command line, etc. to the singleton Carlo that owns the browser.
#### Window.serveFolder(folder[, prefix])
- `folder` <[string]> Folder with web content to make available to Chrome.
- `prefix` <[string]> Prefix of the URL path to serve from the given folder.
Same as [App.serveFolder(folder[, prefix])](#appservefolderfolder-prefix), but
only applies to current window.
#### Window.serveHandler(handler)
- `handler` <[Function]> Network handler callback accepting the [HttpRequest](#class-httprequest) parameter.
Same as [App.serveHandler(handler)](#appservehandlerhandler), but only applies to the current window requests.
Only single window-level handler can be installed in window.
#### Window.serveOrigin(base[, prefix])
- `base` <[string]> Base to serve web content from.
- `prefix` <[string]> Prefix of the URL path to serve from the given folder.
Same as [App.serveOrigin(base[, prefix])](#appserveoriginbase-prefix), but
only applies to current window.
#### Window.setBounds(bounds)
- `bounds` <[Object]> Window bounds:
- `top` <[number]> Top offset in pixels.
- `left` <[number]> Left offset in pixels.
- `width` <[number]> Width in pixels.
- `height` <[number]> Height in pixels.
- `return`: <[Promise]>
Sets window bounds. Parameters `top`, `left`, `width` and `height` are all optional. Dimension or
the offset is only applied when specified.
#### carlo.fileInfo(file)
- `file` <[File]> to get additional information for.
- `return`: <[Promise]<[Object]>>
- `path` absolute path to the given file.
> Available in Chrome M73+.
Returns additional information about the file, otherwise not available to the web.
#### carlo.loadParams()
- `return`: <[Promise]<[Array]>> parameters passed into [window.load()](#windowloaduri-params).
This method is available in the Web world and returns parameters passed into the [window.load()](#windowloaduri-params). This is how Carlo passes initial set of <[rpc]> handles to Node objects into the web world.
[`serveFolder()`]: #windowservefolderfolder-prefix
[App]: #class-app
[Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array"
[Browser]: https://pptr.dev/#?show=api-class-browser "Browser"
[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer"
[File]: https://developer.mozilla.org/en-US/docs/Web/API/File "File"
[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object"
[Page]: https://pptr.dev/#?show=api-class-page "Page"
[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise"
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable"
[Window]: #class-window
[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean"
[function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function "Function"
[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number"
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "Origin"
[rpc]: https://github.com/GoogleChromeLabs/carlo/blob/master/rpc/rpc.md "rpc"
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google.com/conduct/).
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Carlo - headful Node app framework
### ❗Carlo is [no longer maintained](https://github.com/GoogleChromeLabs/carlo/issues/163#issuecomment-592238093).
-----------------------
> Carlo provides Node applications with [Google Chrome](https://www.google.com/chrome/) rendering capabilities, communicates with the locally-installed browser instance using the [Puppeteer](https://github.com/GoogleChrome/puppeteer/) project, and implements a remote call infrastructure for communication between Node and the browser.
###### [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) | [FAQ](#faq) | [Contributing](https://github.com/GoogleChromeLabs/carlo/blob/master/CONTRIBUTING.md)

<!-- [START usecases] -->
###### What can I do?
With Carlo, users can create hybrid applications that use Web stack for rendering and Node for capabilities:
- For Node applications, the web rendering stack lets users visualize the dynamic state of the app.
- For Web applications, additional system capabilities are accessible from Node.
- The application can be bundled into a single executable using [pkg](https://github.com/zeit/pkg).
###### How does it work?
- Carlo locates Google Chrome installed locally.
- Launches Chrome and establishes a connection over the process pipe.
- Exposes a high-level API for rendering in Chrome with the Node environment.
<!-- [END usecases] -->
<!-- [START getstarted] -->
## Usage
Install Carlo
#### npm
```bash
npm i carlo
# yarn add carlo
```
> Carlo requires at least Node v7.6.0.
**Example** - Display local environment
Save file as **example.js**
```js
const carlo = require('carlo');
(async () => {
// Launch the browser.
const app = await carlo.launch();
// Terminate Node.js process on app window closing.
app.on('exit', () => process.exit());
// Tell carlo where your web files are located.
app.serveFolder(__dirname);
// Expose 'env' function in the web environment.
await app.exposeFunction('env', _ => process.env);
// Navigate to the main page of your app.
await app.load('example.html');
})();
```
Save file as **example.html**
```html
<script>
async function run() {
// Call the function that was exposed in Node.
const data = await env();
for (const type in data) {
const div = document.createElement('div');
div.textContent = `${type}: ${data[type]}`;
document.body.appendChild(div);
}
}
</script>
<body onload="run()">
```
Run your application:
```bash
node example.js
```
Check out [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) and [terminal](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/terminal) examples with richer UI and RPC-based communication between the Web and Node in the [examples](https://github.com/GoogleChromeLabs/carlo/tree/master/examples) folder.
<!-- [END getstarted] -->
## API
Check out the [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) to get familiar with Carlo.
## Testing
Carlo uses [Puppeteer](https://pptr.dev/) project for testing. Carlo application and all Carlo windows have
corresponding Puppeteer objects exposed for testing. Please refer to the [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) and the [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) project for more details.
## Contributing to Carlo
Look at the [contributing guide](https://github.com/GoogleChromeLabs/carlo/blob/master/CONTRIBUTING.md) to get an overview of Carlo's development.
<!-- [START faq] -->
## FAQ
#### Q: What was the motivation behind this project when we already have Electron and NW.js? How does this project differ from these platforms, how does it achieve something that is not possible/harder with Electron or NW.js?
- One of the motivations of this project is to demonstrate how browsers that are installed locally can be used with Node out of the box.
- Node v8 and Chrome v8 engines are decoupled in Carlo, providing a maintainable model with the ability to independently update underlying components. Carlo gives the user control over bundling and is more about productivity than branding.
#### Q: Can a Node app using Carlo be packaged as a Desktop app?
The [pkg](https://github.com/zeit/pkg) project can be used to package a Node app as a Desktop app. Carlo does not provide branding configurability such as application icons or customizable menus, instead, Carlo focuses on productivity and Web/Node interoperability. Check out the [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) example and call `pkg package.json` to see how it works.
#### Q: What happens if the user does not have Chrome installed?
Carlo prints an error message when Chrome can not be located.
#### Q: What is the minimum Chrome version that Carlo supports?
Chrome Stable channel, versions 70.* are supported.
<!-- [END faq] -->
================================================
FILE: examples/photobooth/README.md
================================================
### Usage
> This example requires Chrome 72 (Chrome Canary) to function.
Install dependencies
```bash
npm i
```
Run application
```bash
npm start
```
Optionally package as executable
```bash
pkg package.json
```
================================================
FILE: examples/photobooth/main.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const carlo = require('carlo');
const fs = require('fs');
const path = require('path');
const os = require('os');
(async () => {
let app;
try {
app = await carlo.launch(
{
bgcolor: '#e6e8ec',
width: 800,
height: 648 + 24,
icon: path.join(__dirname, '/app_icon.png'),
channel: ['canary', 'stable'],
localDataDir: path.join(os.homedir(), '.carlophotobooth'),
});
} catch(e) {
// New window is opened in the running instance.
console.log('Reusing the running instance');
return;
}
app.on('exit', () => process.exit());
// New windows are opened when this app is started again from command line.
app.on('window', window => window.load('index.html'));
app.serveFolder(path.join(__dirname, '/www'));
await app.exposeFunction('saveImage', saveImage);
await app.load('index.html');
})();
function saveImage(base64) {
var buffer = Buffer.from(base64, 'base64')
if (!fs.existsSync('pictures'))
fs.mkdirSync('pictures');
const fileName = path.join('pictures', new Date().toISOString().replace(/:/g,'-') + '.jpeg');
fs.writeFileSync(fileName, buffer);
}
================================================
FILE: examples/photobooth/package.json
================================================
{
"name": "photobooth-app",
"version": "0.9.0",
"description": "Photo Booth App",
"main": "main.js",
"scripts": {
"bundle": "pkg package.json",
"start": "node main.js"
},
"bin": {
"photobooth-app": "./main.js"
},
"pkg": {
"scripts": "*.js",
"assets": "www/**/*"
},
"keywords": [],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"carlo": "^0.9.0",
"systeminformation": "^3.45.9"
},
"devDependencies": {
"pkg": "^4.3.4"
}
}
================================================
FILE: examples/photobooth/www/index.html
================================================
<!--
Copyright 2018 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="shortcut icon" href="favicon.ico" sizes="256x256" />
<title>PhotoBooth App</title>
<style>
body {
background-color: #e6e8ec;
display: flex;
flex-direction: column;
margin: 0;
}
canvas {
display: none;
}
.buttons {
display: flex;
justify-content: center;
color: #aaa;
flex-basis: 48px;
}
#button {
cursor: hand;
background-color: black;
-webkit-mask-image: url(camera.svg);
transform: scale(2, 2);
width: 24px;
height: 24px;
margin-top: 12px;
}
#button:hover {
background: red;
}
#button:active {
opacity: 0.4;
}
#video {
flex: auto;
transform: scaleX(-1);
}
.flashit {
animation: flash linear 100ms;
}
@keyframes flash {
0% { opacity: 1; }
50% { opacity: .1; }
100% { opacity: 1; }
}
</style>
<script>
async function run(){
const video = document.getElementById('video');
const button = document.getElementById('button');
video.srcObject = await navigator.mediaDevices.getUserMedia({ video: true });
button.addEventListener('click', () => captureScreenshot(), false);
}
function captureScreenshot() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
saveImage(canvas.toDataURL('image/jpeg').substr('data:image/jpeg;base64,'.length));
video.classList.add('flashit');
setTimeout(() => video.classList.remove('flashit'), 1000);
}
</script>
<body onload="run()" tabIndex="0">
<video autoplay id="video"></video>
<canvas id="canvas"></canvas>
<div class="buttons">
<div id="button"/>
</div>
</body>
</html>
================================================
FILE: examples/systeminfo/.gitignore
================================================
node_modules
package-lock.json
.profile
.DS_Store
.vscode
.idea
================================================
FILE: examples/systeminfo/README.md
================================================
### Usage
Install dependencies
```bash
npm i
```
Run application
```bash
npm start
```
Optionally package as executable
```bash
pkg package.json
```
================================================
FILE: examples/systeminfo/app.js
================================================
/**
* Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const carlo = require('carlo');
const os = require('os');
const path = require('path');
const si = require('systeminformation');
async function run() {
let app;
try {
app = await carlo.launch(
{
bgcolor: '#2b2e3b',
title: 'Systeminfo App',
width: 1000,
height: 500,
channel: ['canary', 'stable'],
icon: path.join(__dirname, '/app_icon.png'),
args: process.env.DEV === 'true' ? ['--auto-open-devtools-for-tabs'] : [],
localDataDir: path.join(os.homedir(), '.carlosysteminfo'),
});
} catch(e) {
// New window is opened in the running instance.
console.log('Reusing the running instance');
return;
}
app.on('exit', () => process.exit());
// New windows are opened when this app is started again from command line.
app.on('window', window => window.load('index.html'));
app.serveFolder(path.join(__dirname, 'www'));
await app.exposeFunction('systeminfo', systeminfo);
await app.load('index.html');
return app;
}
async function systeminfo() {
const info = {};
await Promise.all([
si.battery().then(r => info.battery = r),
si.cpu().then(r => info.cpu = r),
si.osInfo().then(r => info.osInfo = r),
]);
return info;
}
module.exports = { run };
================================================
FILE: examples/systeminfo/main.js
================================================
/**
* Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
require('./app.js').run();
================================================
FILE: examples/systeminfo/package.json
================================================
{
"name": "systeminfo-app",
"version": "0.9.0",
"description": "System info example",
"main": "main.js",
"scripts": {
"bundle": "pkg package.json",
"start": "node main.js",
"test": "node test.js"
},
"bin": {
"systeminfo-app": "./main.js"
},
"pkg": {
"scripts": "*.js",
"assets": "www/**/*"
},
"keywords": [],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"carlo": "^0.9.0",
"systeminformation": "^3.45.9"
},
"devDependencies": {
"pkg": "^4.3.4",
"@pptr/testrunner": "^0.5.0"
}
}
================================================
FILE: examples/systeminfo/test.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');
require('carlo').enterTestMode();
const { run } = require('./app');
// Runner holds and runs all the tests
const runner = new TestRunner({
parallel: 1, // run 2 parallel threads
timeout: 3000, // setup timeout of 1 second per test
});
// Simple expect-like matchers
const {expect} = new Matchers();
// Extract jasmine-like DSL into the global namespace
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('test', () => {
it('test columns', async(state, test) => {
const app = await run();
const page = app.mainWindow().pageForTest();
await page.waitForSelector('.header');
const columns = await page.$$eval('.header', nodes => nodes.map(n => n.textContent));
expect(columns.sort().join(',')).toBe('battery,cpu,osInfo');
});
});
// Reporter subscribes to TestRunner events and displays information in terminal
new Reporter(runner);
// Run all tests.
runner.run();
================================================
FILE: examples/systeminfo/www/index.html
================================================
<!--
Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<link rel="shortcut icon" href="favicon.ico" sizes="256x256" />
<style>
body {
color: #ddd;
display: flex;
justify-content: center;
background-color: #2b2e3b;
opacity: 0;
transition: opacity 2s;
font-family: Roboto;
overflow: hidden;
}
.content {
display: flex;
flex-direction: column;
flex: auto;
justify-content: center;
}
.heading {
font-size: 36px;
text-align: center;
margin: 25px 0;
}
#grids {
margin-top: 30px;
color: #ddd;
display: grid;
grid-template-columns: 33% 33% 33%;
grid-gap: 40px;
margin: 25px;
overflow: hidden;
}
.grid-placeholder {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 10px;
}
.grid {
flex: auto;
display: grid;
grid-template-columns: 1fr 4fr;
grid-gap: 4px;
}
.blur {
/**filter: blur(7px);*/
}
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header {
font-weight: bold;
grid-column: span 2;
border-bottom: 1px solid #999;
}
/* roboto-regular - latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('fonts/roboto-v18-latin-regular.woff2') format('woff2');
}
</style>
<title>Systeminfo App</title>
<script>
async function onload() {
const data = await systeminfo();
const grids = document.getElementById('grids');
const blur = new Set(['serial', 'uuid', 'sku', 'hostname']);
const keys = Object.keys(data).sort();
for (const type of keys) {
const info = data[type];
const placeholder = createChild(grids, 'div', 'grid-placeholder');
const grid = createChild(placeholder, 'div', 'grid');
createChild(grid, 'div', 'header').textContent = type;
const infos = Object.keys(info).sort();
for (const key of infos) {
if (typeof info[key] === 'object') continue;
createChild(grid, 'div').textContent = key;
const value = createChild(grid, 'div', 'value');
value.textContent = info[key];
if (blur.has(key))
value.classList.add('blur');
}
}
document.body.style.opacity = 1;
}
function createChild(parent, tag, className) {
const elem = document.createElement(tag);
if (className)
elem.className = className;
parent.appendChild(elem);
return elem;
}
</script>
<body onload="onload()">
<div class="content">
<div class="heading">Welcome to Carlo!</div>
<div id="grids"></div>
</div>
</body>
</html>
================================================
FILE: examples/terminal/.gitignore
================================================
node_modules
package-lock.json
.profile
.DS_Store
.vscode
.idea
================================================
FILE: examples/terminal/README.md
================================================
### Usage
Install dependencies
```bash
npm i
```
Run application
```bash
npm start
```
================================================
FILE: examples/terminal/main.js
================================================
#!/usr/bin/env node
/**
* Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const carlo = require('carlo');
const path = require('path');
const { rpc, rpc_process } = require('carlo/rpc');
class TerminalApp {
constructor() {
this.lastTop_ = 50;
this.lastLeft_ = 50;
this.launch_();
this.handle_ = rpc.handle(this);
}
async launch_() {
try {
this.app_ = await carlo.launch({
bgcolor: '#2b2e3b',
title: 'Terminal App',
width: 800,
height: 800,
channel: ['canary', 'stable'],
icon: path.join(__dirname, '/app_icon.png'),
top: this.lastTop_,
left: this.lastLeft_ });
} catch (e) {
console.log('Reusing the running instance');
return;
}
this.app_.on('exit', () => process.exit());
this.app_.serveFolder(path.join(__dirname, 'www'));
this.app_.serveFolder(path.join(__dirname, 'node_modules'), 'node_modules');
this.app_.on('window', win => this.initUI_(win));
this.initUI_(this.app_.mainWindow());
}
async newWindow() {
this.lastTop_ = (this.lastTop_ + 50) % 200;
this.lastLeft_ += 50;
const options = { top: this.lastTop_, left: this.lastLeft_ };
this.app_.createWindow(options);
}
async initUI_(win) {
const term = await rpc_process.spawn('worker.js');
win.load('index.html', this.handle_, term);
}
}
new TerminalApp();
================================================
FILE: examples/terminal/package.json
================================================
{
"name": "xterm-app",
"version": "0.9.0",
"description": "Terminal example",
"main": "main.js",
"scripts": {
"start": "node main.js"
},
"bin": {
"xterm-app": "./main.js"
},
"keywords": [],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"carlo": "^0.9.0",
"ndb-node-pty-prebuilt": "^0.8.0",
"xterm": "~3.8.1"
}
}
================================================
FILE: examples/terminal/worker.js
================================================
#!/usr/bin/env node
/**
* Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const EventEmitter = require('events');
const os = require('os');
const pty = require('ndb-node-pty-prebuilt');
const { rpc, rpc_process } = require('carlo/rpc');
class Terminal extends EventEmitter {
constructor() {
super();
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
this.term_ = pty.spawn(shell, [], {
name: 'xterm-color',
cwd: process.env.PWD,
env: process.env
});
this.term_.on('data', data => this.emit('data', data));
}
on(event, func) {
// EventEmitter returns heavy object that we don't want to
// send over the wire.
super.on(event, func);
}
resize(cols, rows) {
this.term_.resize(cols, rows);
}
write(data) {
this.term_.write(data);
}
dispose() {
process.kill(this._term.pid);
}
}
rpc_process.init(() => rpc.handle(new Terminal));
================================================
FILE: examples/terminal/www/index.html
================================================
<!--
Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<link rel="shortcut icon" href="favicon.ico" sizes="256x256" />
<link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
<title>Terminal App</title>
<style>
body, #terminal {
display: flex;
flex: auto;
}
</style>
<script src="node_modules/xterm/dist/xterm.js"></script>
<script src="node_modules/xterm/dist/addons/fit/fit.js"></script>
<script>
Terminal.applyAddon(fit);
async function run() {
const [app, term] = await carlo.loadParams();
// Create ui control.
const termUI = new Terminal({cursorBlink: true});
termUI.open(document.getElementById('terminal'));
window.onresize = () => termUI.fit();
// Wire them together.
termUI.on('data', data => term.write(data));
term.on('data', rpc.handle(data => termUI.write(data)));
termUI.on('resize', size => term.resize(size.cols, size.rows));
// Init.
termUI.fit();
termUI.focus();
document.addEventListener('keydown', event => {
if (event.keyCode === 78 && (event.metaKey || event.ctrlKey)) { // Ctrl+N
app.newWindow();
event.preventDefault();
}
});
}
</script>
<body onload="run()">
<div id="terminal"></div>
</body>
</html>
================================================
FILE: examples/windows/README.md
================================================
### Usage
Install dependencies
```bash
npm i
```
Run application
```bash
npm start
```
================================================
FILE: examples/windows/main.html
================================================
<!--
Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
<title>Main</title>
<script>
async function run() {
const [backend] = await carlo.loadParams();
const alexaTop5 = [
'https://google.com', 'https://youtube.com',
'https://facebook.com', 'https://baidu.com',
'https://wikipedia.org'];
for (const url of alexaTop5) {
const button = document.createElement('button');
button.textContent = url;
button.onclick = () => backend.showMyWindow(url);
document.body.appendChild(button);
document.body.appendChild(document.createElement('br'));
}
}
</script>
</head>
<body onload="run()"></body>
================================================
FILE: examples/windows/main.js
================================================
#!/usr/bin/env node
/**
* Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const carlo = require('carlo');
const { rpc } = require('carlo/rpc');
class Backend {
constructor(app) {
this.app_ = app;
this.windows_ = new Map();
}
showMyWindow(url) {
let windowPromise = this.windows_.get(url);
if (!windowPromise) {
windowPromise = this.createWindow_(url);
this.windows_.set(url, windowPromise);
}
windowPromise.then(w => w.bringToFront());
}
async createWindow_(url) {
const window = await this.app_.createWindow({width: 800, height: 600, top: 200, left: 10});
window.on('close', () => this.windows_.delete(url));
window.load(url);
return window;
}
}
(async() => {
const app = await carlo.launch(
{title: 'Main', width: 300, height: 100, top: 10, left: 10 });
app.on('exit', () => process.exit());
const mainWindow = app.mainWindow();
mainWindow.on('close', () => process.exit());
mainWindow.serveFolder(__dirname);
mainWindow.load('main.html', rpc.handle(new Backend(app)));
})();
================================================
FILE: examples/windows/package.json
================================================
{
"name": "windows-app",
"version": "0.9.0",
"description": "Multiple windows example",
"main": "main.js",
"scripts": {
"start": "node main.js"
},
"bin": {
"windows-app": "./main.js"
},
"keywords": [],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"carlo": "^0.9.0"
}
}
================================================
FILE: index.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = require('./lib/carlo');
================================================
FILE: lib/carlo.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const path = require('path');
const puppeteer = require('puppeteer-core');
const findChrome = require('./find_chrome');
const {rpc} = require('../rpc');
const debugApp = require('debug')('carlo:app');
const debugServer = require('debug')('carlo:server');
const {Color} = require('./color');
const {HttpRequest} = require('./http_request');
const fs = require('fs');
const util = require('util');
const {URL} = require('url');
const EventEmitter = require('events');
const fsReadFile = util.promisify(fs.readFile);
let testMode = false;
class App extends EventEmitter {
/**
* @param {!Puppeteer.Browser} browser Puppeteer browser
* @param {!Object} options
*/
constructor(browser, options) {
super();
this.browser_ = browser;
this.options_ = options;
this.windows_ = new Map();
this.exposedFunctions_ = [];
this.pendingWindows_ = new Map();
this.windowSeq_ = 0;
this.www_ = [];
}
async init_() {
debugApp('Configuring browser');
let page;
await Promise.all([
this.browser_.target().createCDPSession().then(session => {
this.session_ = session;
if (this.options_.icon)
this.setIcon(this.options_.icon);
}),
this.browser_.defaultBrowserContext().
overridePermissions('https://domain', [
'geolocation',
'midi',
'notifications',
'camera',
'microphone',
'clipboard-read',
'clipboard-write']),
this.browser_.pages().then(pages => page = pages[0])
]);
this.browser_.on('targetcreated', this.targetCreated_.bind(this));
// Simulate the pageCreated sequence.
let callback;
const result = new Promise(f => callback = f);
this.pendingWindows_.set('', { options: this.options_, callback });
this.pageCreated_(page);
return result;
}
/**
* Close the app windows.
*/
async exit() {
debugApp('app.exit...');
if (this.exited_)
return;
this.exited_ = true;
await this.browser_.close();
this.emit(App.Events.Exit);
}
/**
* @return {!<Window>} main window.
*/
mainWindow() {
for (const window of this.windows_.values())
return window;
}
/**
* @param {!Object=} options
* @return {!Promise<Window>}
*/
async createWindow(options = {}) {
options = Object.assign({}, this.options_, options);
const seq = String(++this.windowSeq_);
if (!this.windows_.size)
throw new Error('Needs at least one window to create more.');
const params = [];
for (const prop of ['top', 'left', 'width', 'height']) {
if (typeof options[prop] === 'number')
params.push(`${prop}=${options[prop]}`);
}
for (const page of this.windows_.keys()) {
page.evaluate(`window.open('about:blank?seq=${seq}', '', '${params.join(',')}')`);
break;
}
return new Promise(callback => {
this.pendingWindows_.set(seq, { options, callback });
});
}
/**
* @return {!Array<!Window>}
*/
windows() {
return Array.from(this.windows_.values());
}
/**
* @param {string} name
* @param {function} func
* @return {!Promise}
*/
exposeFunction(name, func) {
this.exposedFunctions_.push({name, func});
return Promise.all(this.windows().map(window => window.exposeFunction(name, func)));
}
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
evaluate(pageFunction, ...args) {
return this.mainWindow().evaluate(pageFunction, ...args);
}
/**
* @param {string=} folder Folder with the web content.
* @param {string=} prefix Only serve folder for requests with given prefix.
*/
serveFolder(folder = '', prefix = '') {
this.www_.push({folder, prefix: wrapPrefix(prefix)});
}
/**
* Serves pages from given origin, eg `http://localhost:8080`.
* This can be used for the fast development mode available in web frameworks.
*
* @param {string} base
* @param {string=} prefix Only serve folder for requests with given prefix.
*/
serveOrigin(base, prefix = '') {
this.www_.push({baseURL: new URL(base + '/'), prefix: wrapPrefix(prefix)});
}
/**
* Calls given handler for each request and allows called to handle it.
*
* @param {function(!Request)} handler to be used for each request.
*/
serveHandler(handler) {
this.httpHandler_ = handler;
}
/**
* @param {string=} uri
* @param {...*} params
* @return {!Promise<*>}
*/
async load(uri = '', ...params) {
return this.mainWindow().load(uri, ...params);
}
/**
* Set the application icon shown in the OS dock / task swicher.
* @param {string|!Buffer} dockIcon
*/
async setIcon(icon) {
const buffer = typeof icon === 'string' ? await fsReadFile(icon) : icon;
this.session_.send('Browser.setDockTile',
{ image: buffer.toString('base64') }).catch(e => {});
}
/**
* Puppeteer browser object for test.
* @return {!Puppeteer.Browser}
*/
browserForTest() {
return this.browser_;
}
async targetCreated_(target) {
const page = await target.page();
if (!page)
return;
this.pageCreated_(page);
}
/**
* @param {!Puppeteer.Page} page
*/
async pageCreated_(page) {
const url = page.url();
debugApp('Page created at', url);
const seq = url.startsWith('about:blank?seq=') ? url.substr('about:blank?seq='.length) : '';
const params = this.pendingWindows_.get(seq);
const { callback, options } = params || { options: this.options_ };
this.pendingWindows_.delete(seq);
const window = new Window(this, page, options);
await window.init_();
this.windows_.set(page, window);
if (callback)
callback(window);
this.emit(App.Events.Window, window);
}
/**
* @param {!Window}
*/
windowClosed_(window) {
debugApp('window closed', window.loadURI_);
this.windows_.delete(window.page_);
if (!this.windows_.size)
this.exit();
}
}
App.Events = {
Exit: 'exit',
Window: 'window'
};
class Window extends EventEmitter {
/**
* @param {!App} app
* @param {!Puppeteer.Page} page Puppeteer page
* @param {!Object} options
*/
constructor(app, page, options) {
super();
this.app_ = app;
this.options_ = Object.assign({}, app.options_, options);
this.www_ = [];
this.page_ = page;
this.page_.on('close', this.closed_.bind(this));
this.page_.on('domcontentloaded', this.domContentLoaded_.bind(this));
this.hostHandle_ = rpc.handle(new HostWindow(this));
}
async init_() {
debugApp('Configuring window');
const targetId = this.page_.target()._targetInfo.targetId;
const bgcolor = Color.parse(this.options_.bgcolor);
const bgcolorRGBA = bgcolor.canonicalRGBA();
this.session_ = await this.page_.target().createCDPSession();
await Promise.all([
this.session_.send('Runtime.evaluate', { expression: 'self.paramsForReuse', returnByValue: true }).
then(response => { this.paramsForReuse_ = response.result.value; }),
this.session_.send('Emulation.setDefaultBackgroundColorOverride',
{color: {r: bgcolorRGBA[0], g: bgcolorRGBA[1],
b: bgcolorRGBA[2], a: bgcolorRGBA[3] * 255}}),
this.app_.session_.send('Browser.getWindowForTarget', { targetId })
.then(this.initBounds_.bind(this)),
this.configureRpcOnce_(),
...this.app_.exposedFunctions_.map(({name, func}) => this.exposeFunction(name, func))
]);
}
/**
* @param {string} name
* @param {function} func
* @return {!Promise}
*/
exposeFunction(name, func) {
debugApp('Exposing function', name);
return this.page_.exposeFunction(name, func);
}
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
evaluate(pageFunction, ...args) {
return this.page_.evaluate(pageFunction, ...args);
}
/**
* @param {string=} www Folder with the web content.
* @param {string=} prefix Only serve folder for requests with given prefix.
*/
serveFolder(folder = '', prefix = '') {
this.www_.push({folder, prefix: wrapPrefix(prefix)});
}
/**
* Serves pages from given origin, eg `http://localhost:8080`.
* This can be used for the fast development mode available in web frameworks.
*
* @param {string} base
* @param {string=} prefix Only serve folder for requests with given prefix.
*/
serveOrigin(base, prefix = '') {
this.www_.push({baseURL: new URL(base + '/'), prefix: wrapPrefix(prefix)});
}
/**
* Calls given handler for each request and allows called to handle it.
*
* @param {function(!Request)} handler to be used for each request.
*/
serveHandler(handler) {
this.httpHandler_ = handler;
}
/**
* @param {string=} uri
* @param {...*} params
* @return {!Promise}
*/
async load(uri = '', ...params) {
debugApp('Load page', uri);
this.loadURI_ = uri;
this.loadParams_ = params;
await this.initializeInterception_();
debugApp('Navigating the page to', this.loadURI_);
const result = new Promise(f => this.domContentLoadedCallback_ = f);
// Await here to process exceptions.
await this.page_.goto(new URL(this.loadURI_, 'https://domain/').toString(), {timeout: 0, waitFor: 'domcontentloaded'});
// Available in Chrome M73+.
this.session_.send('Page.resetNavigationHistory').catch(e => {});
// Make sure domContentLoaded callback is processed before we return.
// That indirection is here to handle debug-related reloads we did not call for.
return result;
}
initBounds_(result) {
this.windowId_ = result.windowId;
return this.setBounds({ top: this.options_.top,
left: this.options_.left,
width: this.options_.width,
height: this.options_.height });
}
/**
* Puppeteer page object for test.
* @return {!Puppeteer.Page}
*/
pageForTest() {
return this.page_;
}
/**
* Returns value specified in the carlo.launch(options.paramsForReuse). This is handy
* when Carlo is reused across app runs. First Carlo app successfully starts the browser.
* Second carlo attempts to start the browser, but browser profile is already in use.
* Yet, new window is being opened in the first Carlo app. This new window returns
* options.paramsForReuse passed into the second Carlo. This was single app knows what to
* do with the additional windows.
*
* @return {*}
*/
paramsForReuse() {
return this.paramsForReuse_;
}
async configureRpcOnce_() {
await this.page_.exposeFunction('receivedFromChild', data => this.receivedFromChild_(data));
const rpcFile = (await fsReadFile(__dirname + '/../rpc/rpc.js')).toString();
const features = [ require('./features/shortcuts.js'),
require('./features/file_info.js') ];
await this.page_.evaluateOnNewDocument((rpcFile, features) => {
const module = { exports: {} };
eval(rpcFile);
self.rpc = module.exports;
self.carlo = {};
let argvCallback;
const argvPromise = new Promise(f => argvCallback = f);
self.carlo.loadParams = () => argvPromise;
function transport(receivedFromParent) {
self.receivedFromParent = receivedFromParent;
return receivedFromChild;
}
self.rpc.initWorld(transport, async(loadParams, win) => {
argvCallback(loadParams);
if (document.readyState === 'loading')
await new Promise(f => document.addEventListener('DOMContentLoaded', f));
for (const feature of features)
eval(`(${feature})`)(win);
});
}, rpcFile, features.map(f => f.toString()));
}
async domContentLoaded_() {
debugApp('Creating rpc world for page...');
const transport = receivedFromChild => {
this.receivedFromChild_ = receivedFromChild;
return data => {
const json = JSON.stringify(data);
if (this.session_._connection)
this.session_.send('Runtime.evaluate', {expression: `self.receivedFromParent(${json})`});
};
};
if (this._lastWebWorldId)
rpc.disposeWorld(this._lastWebWorldId);
const { worldId } = await rpc.createWorld(transport, this.loadParams_, this.hostHandle_);
debugApp('World created', worldId);
this._lastWebWorldId = worldId;
this.domContentLoadedCallback_();
}
async initializeInterception_() {
debugApp('Initializing network interception...');
if (this.interceptionInitialized_)
return;
if (this.www_.length + this.app_.www_.length === 0 && !this.httpHandler_ && !this.app_.httpHandler_)
return;
this.interceptionInitialized_ = true;
this.session_.on('Network.requestIntercepted', this.requestIntercepted_.bind(this));
return this.session_.send('Network.setRequestInterception', {patterns: [{urlPattern: '*'}]});
}
/**
* @param {!Object} request Intercepted request.
*/
async requestIntercepted_(payload) {
debugServer('intercepted:', payload.request.url);
const handlers = [];
if (this.httpHandler_)
handlers.push(this.httpHandler_);
if (this.app_.httpHandler_)
handlers.push(this.app_.httpHandler_);
handlers.push(this.handleRequest_.bind(this));
new HttpRequest(this.session_, payload, handlers);
}
/**
* @param {!HttpRequest} request Intercepted request.
*/
async handleRequest_(request) {
const url = new URL(request.url());
debugServer('request url:', url.toString());
if (url.hostname !== 'domain') {
request.deferToBrowser();
return;
}
const urlpathname = url.pathname;
for (const {prefix, folder, baseURL} of this.app_.www_.concat(this.www_)) {
debugServer('prefix:', prefix);
if (!urlpathname.startsWith(prefix))
continue;
const pathname = urlpathname.substr(prefix.length);
debugServer('pathname:', pathname);
if (baseURL) {
request.deferToBrowser({ url: String(new URL(pathname, baseURL)) });
return;
}
const fileName = path.join(folder, pathname);
if (!fs.existsSync(fileName))
continue;
const headers = { 'content-type': contentType(request, fileName) };
const body = await fsReadFile(fileName);
request.fulfill({ headers, body});
return;
}
request.deferToBrowser();
}
/**
* @return {{left: number, top: number, width: number, height: number}}
*/
async bounds() {
const { bounds } = await this.app_.session_.send('Browser.getWindowBounds', { windowId: this.windowId_ });
return { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height };
}
/**
* @param {{left: (number|undefined), top: (number|undefined), width: (number|undefined), height: (number|undefined)}} bounds
*/
async setBounds(bounds) {
await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });
}
async fullscreen() {
const bounds = { windowState: 'fullscreen' };
await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });
}
async minimize() {
const bounds = { windowState: 'minimized' };
await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });
}
async maximize() {
const bounds = { windowState: 'maximized' };
await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });
}
bringToFront() {
return this.page_.bringToFront();
}
close() {
return this.page_.close();
}
closed_() {
rpc.dispose(this.hostHandle_);
this.app_.windowClosed_(this);
this.emit(Window.Events.Close);
}
/**
* @return {boolean}
*/
isClosed() {
return this.page_.isClosed();
}
}
Window.Events = {
Close: 'close',
};
const imageContentTypes = new Map([
['jpeg', 'image/jpeg'], ['jpg', 'image/jpeg'], ['svg', 'image/svg+xml'], ['gif', 'image/gif'], ['webp', 'image/webp'],
['png', 'image/png'], ['ico', 'image/ico'], ['tiff', 'image/tiff'], ['tif', 'image/tiff'], ['bmp', 'image/bmp']
]);
const fontContentTypes = new Map([
['ttf', 'font/opentype'], ['otf', 'font/opentype'], ['ttc', 'font/opentype'], ['woff', 'application/font-woff']
]);
/**
* @param {!HttpRequest} request
* @param {!string} fileName
*/
function contentType(request, fileName) {
const dotIndex = fileName.lastIndexOf('.');
const extension = fileName.substr(dotIndex + 1);
switch (request.resourceType()) {
case 'Document': return 'text/html';
case 'Script': return 'text/javascript';
case 'Stylesheet': return 'text/css';
case 'Image':
return imageContentTypes.get(extension) || 'image/png';
case 'Font':
return fontContentTypes.get(extension) || 'application/font-woff';
}
}
/**
* @param {!Object=} options
* @return {!App}
*/
async function launch(options = {}) {
debugApp('Launching Carlo', options);
options = Object.assign(options);
if (!options.bgcolor)
options.bgcolor = '#ffffff';
options.localDataDir = options.localDataDir || path.join(__dirname, '.local-data');
const { executablePath, type } = await findChrome(options);
if (!executablePath) {
console.error('Could not find Chrome installation, please make sure Chrome browser is installed from https://www.google.com/chrome/.');
process.exit(0);
return;
}
const targetPage = `
<title>${encodeURIComponent(options.title || '')}</title>
<style>html{background:${encodeURIComponent(options.bgcolor)};}</style>
<script>self.paramsForReuse = ${JSON.stringify(options.paramsForReuse || undefined)};</script>`;
const args = [
`--app=data:text/html,${targetPage}`,
`--enable-features=NetworkService,NetworkServiceInProcess`,
];
if (options.args)
args.push(...options.args);
if (typeof options.width === 'number' && typeof options.height === 'number')
args.push(`--window-size=${options.width},${options.height}`);
if (typeof options.left === 'number' && typeof options.top === 'number')
args.push(`--window-position=${options.left},${options.top}`);
try {
const browser = await puppeteer.launch({
executablePath,
pipe: true,
defaultViewport: null,
headless: testMode,
userDataDir: options.userDataDir || path.join(options.localDataDir, `profile-${type}`),
args });
const app = new App(browser, options);
await app.init_();
return app;
} catch (e) {
if (e.toString().includes('Target closed'))
throw new Error('Could not start the browser or the browser was already running with the given profile.');
else
throw e;
}
}
class HostWindow {
/**
* @param {!Window} win
*/
constructor(win) {
this.window_ = win;
}
closeBrowser() {
// Allow rpc response to land.
setTimeout(() => this.window_.app_.exit(), 0);
}
async fileInfo(expression) {
const { result } = await this.window_.session_.send('Runtime.evaluate', { expression });
return this.window_.session_.send('DOM.getFileInfo', { objectId: result.objectId });
}
}
function enterTestMode() {
testMode = true;
}
function wrapPrefix(prefix) {
if (!prefix.startsWith('/')) prefix = '/' + prefix;
if (!prefix.endsWith('/')) prefix += '/';
return prefix;
}
module.exports = { launch, enterTestMode };
================================================
FILE: lib/color.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
class Color {
/**
* @param {!Array.<number>} rgba
* @param {!Color.Format} format
* @param {string=} originalText
*/
constructor(rgba, format, originalText) {
this._rgba = rgba;
this._originalText = originalText || null;
this._originalTextIsValid = !!this._originalText;
this._format = format;
if (typeof this._rgba[3] === 'undefined')
this._rgba[3] = 1;
for (let i = 0; i < 4; ++i) {
if (this._rgba[i] < 0) {
this._rgba[i] = 0;
this._originalTextIsValid = false;
}
if (this._rgba[i] > 1) {
this._rgba[i] = 1;
this._originalTextIsValid = false;
}
}
}
/**
* @param {string} text
* @return {?Color}
*/
static parse(text) {
const value = text.toLowerCase().replace(/\s+/g, '');
const simple = /^(?:#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))$/i;
let match = value.match(simple);
if (match) {
if (match[1]) { // hex
let hex = match[1].toLowerCase();
let format;
if (hex.length === 3) {
format = Color.Format.ShortHEX;
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
} else if (hex.length === 4) {
format = Color.Format.ShortHEXA;
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) +
hex.charAt(3) + hex.charAt(3);
} else if (hex.length === 6) {
format = Color.Format.HEX;
} else {
format = Color.Format.HEXA;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
let a = 1;
if (hex.length === 8)
a = parseInt(hex.substring(6, 8), 16) / 255;
return new Color([r / 255, g / 255, b / 255, a], format, text);
}
return null;
}
// rgb/rgba(), hsl/hsla()
match = text.toLowerCase().match(/^\s*(?:(rgba?)|(hsla?))\((.*)\)\s*$/);
if (match) {
const components = match[3].trim();
let values = components.split(/\s*,\s*/);
if (values.length === 1) {
values = components.split(/\s+/);
if (values[3] === '/') {
values.splice(3, 1);
if (values.length !== 4)
return null;
} else if ((values.length > 2 && values[2].indexOf('/') !== -1) || (values.length > 3 && values[3].indexOf('/') !== -1)) {
const alpha = values.slice(2, 4).join('');
values = values.slice(0, 2).concat(alpha.split(/\//)).concat(values.slice(4));
} else if (values.length >= 4) {
return null;
}
}
if (values.length !== 3 && values.length !== 4 || values.indexOf('') > -1)
return null;
const hasAlpha = (values[3] !== undefined);
if (match[1]) { // rgb/rgba
const rgba = [
Color._parseRgbNumeric(values[0]), Color._parseRgbNumeric(values[1]),
Color._parseRgbNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
];
if (rgba.indexOf(null) > -1)
return null;
return new Color(rgba, hasAlpha ? Color.Format.RGBA : Color.Format.RGB, text);
}
if (match[2]) { // hsl/hsla
const hsla = [
Color._parseHueNumeric(values[0]), Color._parseSatLightNumeric(values[1]),
Color._parseSatLightNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
];
if (hsla.indexOf(null) > -1)
return null;
const rgba = [];
Color.hsl2rgb(hsla, rgba);
return new Color(rgba, hasAlpha ? Color.Format.HSLA : Color.Format.HSL, text);
}
}
return null;
}
/**
* @param {string} value
* return {number}
*/
static _parsePercentOrNumber(value) {
if (isNaN(value.replace('%', '')))
return null;
const parsed = parseFloat(value);
if (value.indexOf('%') !== -1) {
if (value.indexOf('%') !== value.length - 1)
return null;
return parsed / 100;
}
return parsed;
}
/**
* @param {string} value
* return {number}
*/
static _parseRgbNumeric(value) {
const parsed = Color._parsePercentOrNumber(value);
if (parsed === null)
return null;
if (value.indexOf('%') !== -1)
return parsed;
return parsed / 255;
}
/**
* @param {string} value
* return {number}
*/
static _parseHueNumeric(value) {
const angle = value.replace(/(deg|g?rad|turn)$/, '');
if (isNaN(angle) || value.match(/\s+(deg|g?rad|turn)/))
return null;
const number = parseFloat(angle);
if (value.indexOf('turn') !== -1)
return number % 1;
else if (value.indexOf('grad') !== -1)
return (number / 400) % 1;
else if (value.indexOf('rad') !== -1)
return (number / (2 * Math.PI)) % 1;
return (number / 360) % 1;
}
/**
* @param {string} value
* return {number}
*/
static _parseSatLightNumeric(value) {
if (value.indexOf('%') !== value.length - 1 || isNaN(value.replace('%', '')))
return null;
const parsed = parseFloat(value);
return Math.min(1, parsed / 100);
}
/**
* @param {string} value
* return {number}
*/
static _parseAlphaNumeric(value) {
return Color._parsePercentOrNumber(value);
}
/**
* @param {!Array.<number>} hsl
* @param {!Array.<number>} out_rgb
*/
static hsl2rgb(hsl, out_rgb) {
const h = hsl[0];
let s = hsl[1];
const l = hsl[2];
function hue2rgb(p, q, h) {
if (h < 0)
h += 1;
else if (h > 1)
h -= 1;
if ((h * 6) < 1)
return p + (q - p) * h * 6;
else if ((h * 2) < 1)
return q;
else if ((h * 3) < 2)
return p + (q - p) * ((2 / 3) - h) * 6;
else
return p;
}
if (s < 0)
s = 0;
let q;
if (l <= 0.5)
q = l * (1 + s);
else
q = l + s - (l * s);
const p = 2 * l - q;
const tr = h + (1 / 3);
const tg = h;
const tb = h - (1 / 3);
out_rgb[0] = hue2rgb(p, q, tr);
out_rgb[1] = hue2rgb(p, q, tg);
out_rgb[2] = hue2rgb(p, q, tb);
out_rgb[3] = hsl[3];
}
/**
* @return {!Color.Format}
*/
format() {
return this._format;
}
/**
* @return {!Array.<number>} HSLA with components within [0..1]
*/
hsla() {
if (this._hsla)
return this._hsla;
const r = this._rgba[0];
const g = this._rgba[1];
const b = this._rgba[2];
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
const add = max + min;
let h;
if (min === max)
h = 0;
else if (r === max)
h = ((1 / 6 * (g - b) / diff) + 1) % 1;
else if (g === max)
h = (1 / 6 * (b - r) / diff) + 1 / 3;
else
h = (1 / 6 * (r - g) / diff) + 2 / 3;
const l = 0.5 * add;
let s;
if (l === 0)
s = 0;
else if (l === 1)
s = 0;
else if (l <= 0.5)
s = diff / add;
else
s = diff / (2 - add);
this._hsla = [h, s, l, this._rgba[3]];
return this._hsla;
}
/**
* @return {boolean}
*/
hasAlpha() {
return this._rgba[3] !== 1;
}
/**
* @return {!Color.Format}
*/
detectHEXFormat() {
let canBeShort = true;
for (let i = 0; i < 4; ++i) {
const c = Math.round(this._rgba[i] * 255);
if (c % 17) {
canBeShort = false;
break;
}
}
const hasAlpha = this.hasAlpha();
const cf = Color.Format;
if (canBeShort)
return hasAlpha ? cf.ShortHEXA : cf.ShortHEX;
return hasAlpha ? cf.HEXA : cf.HEX;
}
/**
* @return {?string}
*/
asString(format) {
if (format === this._format && this._originalTextIsValid)
return this._originalText;
if (!format)
format = this._format;
/**
* @param {number} value
* @return {number}
*/
function toRgbValue(value) {
return Math.round(value * 255);
}
/**
* @param {number} value
* @return {string}
*/
function toHexValue(value) {
const hex = Math.round(value * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
/**
* @param {number} value
* @return {string}
*/
function toShortHexValue(value) {
return (Math.round(value * 255) / 17).toString(16);
}
switch (format) {
case Color.Format.Original:
return this._originalText;
case Color.Format.RGB:
if (this.hasAlpha())
return null;
return `rgb(${toRgbValue(this._rgba[0])}, ${toRgbValue(this._rgba[1])}, ${toRgbValue(this._rgba[2])})`;
case Color.Format.RGBA:
return `rgba(${toRgbValue(this._rgba[0])}, ${toRgbValue(this._rgba[1])}, ${toRgbValue(this._rgba[2])}, ${this._rgba[3]})`;
case Color.Format.HSL:
if (this.hasAlpha())
return null;
const hsl = this.hsla();
return `hsl(${Math.round(hsl[0] * 360)}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%)`;
case Color.Format.HSLA:
const hsla = this.hsla();
return `hsla(${Math.round(hsla[0] * 360)}, ${Math.round(hsla[1] * 100)}%, ${Math.round(hsla[2] * 100)}%, ${hsla[3]})`;
case Color.Format.HEXA:
return `#${toHexValue(this._rgba[0])}${toHexValue(this._rgba[1])}${toHexValue(this._rgba[2])}${toHexValue(this._rgba[3])}`.toLowerCase();
case Color.Format.HEX:
if (this.hasAlpha())
return null;
return `#${toHexValue(this._rgba[0])}${toHexValue(this._rgba[1])}${toHexValue(this._rgba[2])}`.toLowerCase();
case Color.Format.ShortHEXA:
const hexFormat = this.detectHEXFormat();
if (hexFormat !== Color.Format.ShortHEXA && hexFormat !== Color.Format.ShortHEX)
return null;
return `#${toShortHexValue(this._rgba[0])}${toShortHexValue(this._rgba[1])}${toShortHexValue(this._rgba[2])}${toShortHexValue(this._rgba[3])}`.toLowerCase();
case Color.Format.ShortHEX:
if (this.hasAlpha())
return null;
if (this.detectHEXFormat() !== Color.Format.ShortHEX)
return null;
return `#${toShortHexValue(this._rgba[0])}${toShortHexValue(this._rgba[1])}${toShortHexValue(this._rgba[2])}`.toLowerCase();
}
return this._originalText;
}
/**
* @return {!Array<number>}
*/
rgba() {
return this._rgba.slice();
}
/**
* @return {!Array.<number>}
*/
canonicalRGBA() {
const rgba = new Array(4);
for (let i = 0; i < 3; ++i)
rgba[i] = Math.round(this._rgba[i] * 255);
rgba[3] = this._rgba[3];
return rgba;
}
}
/** @type {!RegExp} */
Color.Regex = /((?:rgb|hsl)a?\([^)]+\)|#[0-9a-fA-F]{8}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3,4}|\b[a-zA-Z]+\b(?!-))/g;
/**
* @enum {string}
*/
Color.Format = {
Original: 'original',
HEX: 'hex',
ShortHEX: 'shorthex',
HEXA: 'hexa',
ShortHEXA: 'shorthexa',
RGB: 'rgb',
RGBA: 'rgba',
HSL: 'hsl',
HSLA: 'hsla'
};
module.exports = { Color };
================================================
FILE: lib/features/file_info.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = function install(hostWindow) {
let lastFileId = 0;
self.carlo.fileInfo = async(file) => {
const fileId = ++lastFileId;
self.carlo.fileInfo.files_.set(fileId, file);
const result = await hostWindow.fileInfo(`self.carlo.fileInfo.files_.get(${fileId})`);
self.carlo.fileInfo.files_.delete(fileId);
return result;
};
self.carlo.fileInfo.files_ = new Map();
};
================================================
FILE: lib/features/shortcuts.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = function install(hostWindow) {
const ctrlOrCmdCodes = new Set(
['KeyD', 'KeyE', 'KeyD', 'KeyG', 'KeyN', 'KeyO', 'KeyP', 'KeyQ', 'KeyR', 'KeyS',
'KeyT', 'KeyW', 'KeyY', 'Tab', 'PageUp', 'PageDown', 'F4']);
const cmdCodes = new Set(['BracketLeft', 'BracketRight', 'Comma']);
const cmdOptionCodes = new Set(['ArrowLeft', 'ArrowRight', 'KeyB']);
const ctrlShiftCodes = new Set(['KeyQ', 'KeyW']);
const altCodes = new Set(['Home', 'ArrowLeft', 'ArrowRight', 'F4']);
function preventDefaultShortcuts(event) {
let prevent = false;
if (navigator.userAgent.match(/Mac OS X/)) {
if (event.metaKey) {
if (event.keyCode > 48 && event.keyCode <= 57) // 1-9
prevent = true;
if (ctrlOrCmdCodes.has(event.code) || cmdCodes.has(event.code))
prevent = true;
if (event.shiftKey && cmdOptionCodes.has(event.code))
prevent = true;
if (event.code === 'ArrowLeft' || event.code === 'ArrowRight') {
if (!event.contentEditable && event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'TEXTAREA')
prevent = true;
}
}
} else {
if (event.code === 'F4')
prevent = true;
if (event.ctrlKey) {
if (event.keyCode > 48 && event.keyCode <= 57) // 1-9
prevent = true;
if (ctrlOrCmdCodes.has(event.code))
prevent = true;
if (event.shiftKey && ctrlShiftCodes.has(event.code))
prevent = true;
}
if (event.altKey && altCodes.has(event.code))
prevent = true;
}
if (prevent)
event.preventDefault();
}
document.addEventListener('keydown', preventDefaultShortcuts, false);
document.addEventListener('keydown', event => {
if ((event.key === 'q' || event.key === 'Q') && (event.metaKey || event.ctrlKey)) {
hostWindow.closeBrowser();
event.preventDefault();
}
});
};
================================================
FILE: lib/find_chrome.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const execSync = require('child_process').execSync;
const execFileSync = require('child_process').execFileSync;
const puppeteer = require('puppeteer-core');
const newLineRegex = /\r?\n/;
function darwin(canary) {
const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework' +
'/Versions/A/Frameworks/LaunchServices.framework' +
'/Versions/A/Support/lsregister';
const grepexpr = canary ? 'google chrome canary' : 'google chrome';
const result =
execSync(`${LSREGISTER} -dump | grep -i \'${grepexpr}\\?.app$\' | awk \'{$1=""; print $0}\'`);
const installations = new Set();
const paths = result.toString().split(newLineRegex).filter(a => a).map(a => a.trim());
paths.unshift(canary ? '/Applications/Google Chrome Canary.app' : '/Applications/Google Chrome.app');
for (const p of paths) {
if (p.startsWith('/Volumes'))
continue;
const inst = path.join(p, canary ? '/Contents/MacOS/Google Chrome Canary' : '/Contents/MacOS/Google Chrome');
if (canAccess(inst))
return inst;
}
}
/**
* Look for linux executables in 3 ways
* 1. Look into CHROME_PATH env variable
* 2. Look into the directories where .desktop are saved on gnome based distro's
* 3. Look for google-chrome-stable & google-chrome executables by using the which command
*/
function linux(canary) {
let installations = [];
// Look into the directories where .desktop are saved on gnome based distro's
const desktopInstallationFolders = [
path.join(require('os').homedir(), '.local/share/applications/'),
'/usr/share/applications/',
];
desktopInstallationFolders.forEach(folder => {
installations = installations.concat(findChromeExecutables(folder));
});
// Look for google-chrome(-stable) & chromium(-browser) executables by using the which command
const executables = [
'google-chrome-stable',
'google-chrome',
'chromium-browser',
'chromium',
];
executables.forEach(executable => {
try {
const chromePath =
execFileSync('which', [executable], {stdio: 'pipe'}).toString().split(newLineRegex)[0];
if (canAccess(chromePath))
installations.push(chromePath);
} catch (e) {
// Not installed.
}
});
if (!installations.length)
throw new Error('The environment variable CHROME_PATH must be set to executable of a build of Chromium version 54.0 or later.');
const priorities = [
{regex: /chrome-wrapper$/, weight: 51},
{regex: /google-chrome-stable$/, weight: 50},
{regex: /google-chrome$/, weight: 49},
{regex: /chromium-browser$/, weight: 48},
{regex: /chromium$/, weight: 47},
];
if (process.env.CHROME_PATH)
priorities.unshift({regex: new RegExp(`${process.env.CHROME_PATH}`), weight: 101});
return sort(uniq(installations.filter(Boolean)), priorities)[0];
}
function win32(canary) {
const suffix = canary ?
`${path.sep}Google${path.sep}Chrome SxS${path.sep}Application${path.sep}chrome.exe` :
`${path.sep}Google${path.sep}Chrome${path.sep}Application${path.sep}chrome.exe`;
const prefixes = [
process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']
].filter(Boolean);
let result;
prefixes.forEach(prefix => {
const chromePath = path.join(prefix, suffix);
if (canAccess(chromePath))
result = chromePath;
});
return result;
}
function sort(installations, priorities) {
const defaultPriority = 10;
return installations
// assign priorities
.map(inst => {
for (const pair of priorities) {
if (pair.regex.test(inst))
return {path: inst, weight: pair.weight};
}
return {path: inst, weight: defaultPriority};
})
// sort based on priorities
.sort((a, b) => (b.weight - a.weight))
// remove priority flag
.map(pair => pair.path);
}
function canAccess(file) {
if (!file)
return false;
try {
fs.accessSync(file);
return true;
} catch (e) {
return false;
}
}
function uniq(arr) {
return Array.from(new Set(arr));
}
function findChromeExecutables(folder) {
const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space
const chromeExecRegex = '^Exec=\/.*\/(google-chrome|chrome|chromium)-.*';
const installations = [];
if (canAccess(folder)) {
// Output of the grep & print looks like:
// /opt/google/chrome/google-chrome --profile-directory
// /home/user/Downloads/chrome-linux/chrome-wrapper %U
let execPaths;
// Some systems do not support grep -R so fallback to -r.
// See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
try {
execPaths = execSync(`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`);
} catch (e) {
execPaths = execSync(`grep -Er "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`);
}
execPaths = execPaths.toString()
.split(newLineRegex)
.map(execPath => execPath.replace(argumentsRegex, '$1'));
execPaths.forEach(execPath => canAccess(execPath) && installations.push(execPath));
}
return installations;
}
/**
* @return {!Promise<?string>}
*/
async function downloadChromium(options, targetRevision) {
const browserFetcher = puppeteer.createBrowserFetcher({ path: options.localDataDir });
const revision = targetRevision || require('puppeteer-core/package.json').puppeteer.chromium_revision;
const revisionInfo = browserFetcher.revisionInfo(revision);
// Do nothing if the revision is already downloaded.
if (revisionInfo.local)
return revisionInfo;
// Override current environment proxy settings with npm configuration, if any.
try {
console.log(`Downloading Chromium r${revision}...`);
const newRevisionInfo = await browserFetcher.download(revisionInfo.revision);
console.log('Chromium downloaded to ' + newRevisionInfo.folderPath);
let localRevisions = await browserFetcher.localRevisions();
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
// Remove previous chromium revisions.
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
await Promise.all(cleanupOldVersions);
return newRevisionInfo;
} catch (error) {
console.error(`ERROR: Failed to download Chromium r${revision}!`);
console.error(error);
return null;
}
}
async function findChrome(options) {
if (options.executablePath)
return { executablePath: options.executablePath, type: 'user' };
const config = new Set(options.channel || ['stable']);
let executablePath;
// Always prefer canary.
if (config.has('canary') || config.has('*')) {
if (process.platform === 'linux')
executablePath = linux(true);
else if (process.platform === 'win32')
executablePath = win32(true);
else if (process.platform === 'darwin')
executablePath = darwin(true);
if (executablePath)
return { executablePath, type: 'canary' };
}
// Then pick stable.
if (config.has('stable') || config.has('*')) {
if (process.platform === 'linux')
executablePath = linux();
else if (process.platform === 'win32')
executablePath = win32();
else if (process.platform === 'darwin')
executablePath = darwin();
if (executablePath)
return { executablePath, type: 'stable' };
}
// always prefer puppeteer revision of chromium
if (config.has('chromium') || config.has('*')) {
const revisionInfo = await downloadChromium(options);
return { executablePath: revisionInfo.executablePath, type: revisionInfo.revision };
}
for (const item of config) {
if (!item.startsWith('r'))
continue;
const revisionInfo = await downloadChromium(options, item.substring(1));
return { executablePath: revisionInfo.executablePath, type: revisionInfo.revision };
}
return {};
}
module.exports = findChrome;
================================================
FILE: lib/http_request.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const debugServer = require('debug')('carlo:server');
const statusTexts = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'209': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
};
/**
* Intercepted request instance that can be resolved to the client's liking.
*/
class HttpRequest {
/**
* @param {!CDPSession} session
* @param {!Object} params
*/
constructor(session, params, handlers) {
this.session_ = session;
this.params_ = params;
this.handlers_ = handlers;
this.done_ = false;
this.callNextHandler_();
}
/**
* @return {string}
*/
url() {
return this.params_.request.url;
}
/**
* @return {string}
*/
method() {
return this.params_.request.method;
}
/**
* @return {!Object<string, string>} HTTP request headers.
*/
headers() {
return this.params_.request.headers || {};
}
/**
* @return {string}
*/
resourceType() {
return this.params_.resourceType;
}
/**
* Aborts the request.
*/
abort() {
debugServer('abort', this.url());
return this.resolve_({errorReason: 'Aborted'});
}
/**
* Fails the request.
*/
fail() {
debugServer('fail', this.url());
return this.resolve_({errorReason: 'Failed'});
}
/**
* Falls through to the next handler.
*/
continue() {
debugServer('continue', this.url());
return this.callNextHandler_();
}
/**
* Continues the request with the provided overrides to the url, method or
* headers.
*
* @param {{url: (string|undefined), method: (string|undefined),
* headers: (!Object<string, string>|undefined)}|undefined} overrides
* Overrides to apply to the request before it hits network.
*/
deferToBrowser(overrides) {
debugServer('deferToBrowser', this.url());
const params = {};
if (overrides && overrides.url) params.url = overrides.url;
if (overrides && overrides.method) params.method = overrides.method;
if (overrides && overrides.headers) params.headers = overrides.headers;
return this.resolve_(params);
}
/**
* Fulfills the request with the given data.
*
* @param {{status: number|undefined,
* headers: !Object<string,string>|undefined,
* body: !Buffer|undefined}} options
*/
fulfill({status, headers, body}) {
debugServer('fulfill', this.url());
status = status || 200;
const responseHeaders = {};
if (headers) {
for (const header of Object.keys(headers))
responseHeaders[header.toLowerCase()] = headers[header];
}
if (body && !('content-length' in responseHeaders))
responseHeaders['content-length'] = Buffer.byteLength(body);
const statusText = statusTexts[status] || '';
const statusLine = `HTTP/1.1 ${status} ${statusText}`;
const CRLF = '\r\n';
let text = statusLine + CRLF;
for (const header of Object.keys(responseHeaders))
text += header + ': ' + responseHeaders[header] + CRLF;
text += CRLF;
let responseBuffer = Buffer.from(text, 'utf8');
if (body)
responseBuffer = Buffer.concat([responseBuffer, body]);
return this.resolve_({
interceptionId: this.interceptionId_,
rawResponse: responseBuffer.toString('base64')
});
}
callNextHandler_() {
debugServer('next handler', this.url());
const handler = this.handlers_.shift();
if (handler) {
handler(this);
return;
}
this.resolve_({});
}
/**
* Aborts the request.
* @param {!Object} params
*/
async resolve_(params) {
debugServer('resolve', this.url());
if (this.done_) throw new Error('Already resolved given request');
params.interceptionId = this.params_.interceptionId;
this.done_ = true;
return this.session_.send('Network.continueInterceptedRequest', params);
}
}
module.exports = { HttpRequest };
================================================
FILE: package.json
================================================
{
"name": "carlo",
"version": "0.9.46",
"description": "Carlo is a framework for rendering Node data structures using Chrome browser.",
"repository": "github:GoogleChromeLabs/carlo",
"engines": {
"node": ">=7.6.0"
},
"main": "index.js",
"directories": {
"lib": "lib"
},
"scripts": {
"lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .)",
"test": "node rpc/test.js && node test/test.js",
"headful-test": "node test/headful.js"
},
"keywords": [],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0",
"puppeteer-core": "~1.12.0"
},
"devDependencies": {
"eslint": "^5.8.0",
"@pptr/testrunner": "^0.5.0",
"@pptr/testserver": "^0.5.0"
}
}
================================================
FILE: rpc/index.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = {
rpc: require('./rpc'),
rpc_process: require('./rpc_process')
};
================================================
FILE: rpc/rpc.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/** @typedef { !Array<string> } Address */
/** @typedef {{ name: string, isFunc: boolean }} Descriptor */
/** @typedef {function(function(data)): function(data)} Transport */
const handleSymbol = Symbol('handle');
/**
* Handle to the object. This handle has methods matching the methods of the
* target object. Calling these methods calls them remotely over the low level
* messaging transprot. Return values are delivered to the caller.
*/
class Handle {
/**
* @param {string} localAddress Address of this handle.
* @param {string} address Address of the primary handle this handle refers
* to. Primary handle is the one that lives in the same world
* as the actual object it refers to.
* @param {!Object} descriptor Target object spec descriptor (list of methods, etc.)
* @param {!Rpc} rpc
*/
constructor(localAddress, address, descriptor, rpc) {
this.localAddress_ = localAddress;
this.address_ = address;
this.descriptor_ = descriptor;
this.rpc_ = rpc;
this.object_ = null;
const target = {};
target[handleSymbol] = this;
this.proxy_ = new Proxy(target, { get: Handle.proxyHandler_ });
}
/**
* We always return proxies to the user to encapsulate handle and marshall
* calls automatically.
*/
static proxyHandler_(target, methodName, receiver) {
const handle = target[handleSymbol];
if (methodName === handleSymbol)
return handle;
if (typeof methodName !== 'string')
return;
if (methodName === 'then')
return target[methodName];
return handle.callMethod_.bind(handle, methodName);
}
/**
* Calls method on the target object.
*
* @param {string} method Method to call on the target object.
* @param {!Array<*>} args Call arguments. These can be either primitive
* types, other handles or JSON structures.
* @return {!Promise<*>} result, also primitive, JSON or handle.
*/
async callMethod_(method, ...args) {
const message = {
m: method,
p: this.rpc_.wrap_(args)
};
const response = await this.rpc_.sendCommand_(this.address_, this.localAddress_, message);
return this.rpc_.unwrap_(response);
}
/**
* Dispatches external message on this handle.
* @param {string} message
* @return {!Promise<*>} result, also primitive, JSON or handle.
*/
async dispatchMessage_(message) {
if (this.descriptor_.isFunc) {
const result = await this.object_(...this.rpc_.unwrap_(message.p));
return this.rpc_.wrap_(result);
}
if (message.m.startsWith('_') || message.m.endsWith('_'))
throw new Error(`Private members are not exposed over RPC: '${message.m}'`);
if (!(message.m in this.object_))
throw new Error(`There is no member '${message.m}' in '${this.descriptor_.name}'`);
const value = this.object_[message.m];
if (typeof value !== 'function') {
if (message.p.length)
throw new Error(`'${message.m}' is not a function, can't pass args '${message.p}'`);
return this.rpc_.wrap_(value);
}
const result = await this.object_[message.m](...this.rpc_.unwrap_(message.p));
return this.rpc_.wrap_(result);
}
/**
* Returns the proxy to this handle that is passed to the userland.
*/
proxy() {
return this.proxy_;
}
}
/**
* Main Rpc object. Keeps all the book keeping and performs message routing
* between handles beloning to different worlds. Each 'world' has a singleton
* 'rpc' instance.
*/
class Rpc {
constructor() {
this.lastHandleId_ = 0;
this.lastWorldId_ = 0;
this.worlds_ = new Map();
this.idToHandle_ = new Map();
this.lastMessageId_ = 0;
this.callbacks_ = new Map();
this.worldId_ = '.';
this.sendToParent_ = null;
this.cookieResponseCallbacks_ = new Map();
this.debug_ = false;
}
/**
* Each singleton rpc object has the world's parameters that parent world sent
* to them.
*
* @return {*}
*/
params() {
return this.worldParams_;
}
/**
* Called in the parent world.
* Creates a child world with the given root handle.
*
* @param {!Transport} transport
* - receives function that should be called upon messages from
* the world and
* - returns function that should be used to send messages to the
* world
* @param {...*} args Params to pass to the child world.
* @return {!Promise<{worldId:string, *}>} returns the handles / parameters that child
* world returned during the initialization.
*/
createWorld(transport, ...args) {
const worldId = this.worldId_ + '/' + (++this.lastWorldId_);
const sendToChild = transport(this.routeMessage_.bind(this, false));
this.worlds_.set(worldId, sendToChild);
sendToChild({cookie: true, args: this.wrap_(args), worldId });
return new Promise(f => this.cookieResponseCallbacks_.set(worldId, f));
}
/**
* Called in the parent world.
* Disposes a child world with the given id.
*
* @param {string} worldId The world to dispose.
*/
disposeWorld(worldId) {
if (!this.worlds_.has(worldId))
throw new Error('No world with given id exists');
this.worlds_.delete(worldId);
}
/**
* Called in the child world to initialize it.
* @param {!Transport} transport.
* @param {function(...*):!Promise<*>} initializer
*/
initWorld(transport, initializer) {
this.sendToParent_ = transport(this.routeMessage_.bind(this, true));
return new Promise(f => this.cookieCallback_ = f)
.then(args => initializer ? initializer(...args) : undefined)
.then(response => this.sendToParent_(
{cookieResponse: true, worldId: this.worldId_, r: this.wrap_(response)}));
}
/**
* Creates a handle to the object.
* @param {!Object} object Object to create handle for
* @return {!Object}
*/
handle(object) {
if (!object)
throw new Error('Can only create handles for objects');
if (typeof object === 'object' && handleSymbol in object)
throw new Error('Can not return handle to handle.');
const descriptor = this.describe_(object);
const address = [this.worldId_, descriptor.name + '#' + (++this.lastHandleId_)];
const handle = new Handle(address, address, descriptor, this);
handle.object_ = object;
this.idToHandle_.set(address[1], handle);
return handle.proxy();
}
/**
* Returns the object this handle points to. Only works on the local
* handles, otherwise returns null.
*
* @param {*} handle Primary object handle.
* @return {?Object}
*/
object(proxy) {
return proxy[handleSymbol].object_ || null;
}
/**
* Disposes a handle to the object.
* @param {*} handle Primary object handle.
*/
dispose(proxy) {
const handle = proxy[handleSymbol];
if (!handle.object_)
throw new Error('Can only dipose handle that was explicitly created with rpc.handle()');
this.idToHandle_.delete(handle.address_[1]);
}
/**
* Builds object descriptor.
* @return {!Descriptor}
*/
describe_(o) {
if (typeof o === 'function')
return { isFunc: true };
return { name: o.constructor.name };
}
/**
* Wraps call argument as a protocol structures.
* @param {*} param
* @param {number=} maxDepth
* @return {*}
*/
wrap_(param, maxDepth = 1000) {
if (!maxDepth)
throw new Error('Object reference chain is too long');
maxDepth--;
if (!param)
return param;
if (param[handleSymbol]) {
const handle = param[handleSymbol];
return { __rpc_a__: handle.address_, descriptor: handle.descriptor_ };
}
if (param instanceof Array)
return param.map(item => this.wrap_(item, maxDepth));
if (typeof param === 'object') {
const result = {};
for (const key in param)
result[key] = this.wrap_(param[key], maxDepth);
return result;
}
return param;
}
/**
* Unwraps call argument from the protocol structures.
* @param {!Object} param
* @return {*}
*/
unwrap_(param) {
if (!param)
return param;
if (param.__rpc_a__) {
const handle = this.createHandle_(param.__rpc_a__, param.descriptor);
if (handle.descriptor_.isFunc)
return (...args) => handle.callMethod_('call', ...args);
return handle.proxy();
}
if (param instanceof Array)
return param.map(item => this.unwrap_(item));
if (typeof param === 'object') {
const result = {};
for (const key in param)
result[key] = this.unwrap_(param[key]);
return result;
}
return param;
}
/**
* Unwraps descriptor and creates a local world handle that will be associated
* with the primary handle at given address.
*
* @param {!Address} address Address of the primary wrapper.
* @param {!Descriptor} address Address of the primary wrapper.
* @return {!Handle}
*/
createHandle_(address, descriptor) {
if (address[0] === this.worldId_) {
const existing = this.idToHandle_.get(address[1]);
if (existing)
return existing;
}
const localAddress = [this.worldId_, descriptor.name + '#' + (++this.lastHandleId_)];
return new Handle(localAddress, address, descriptor, this);
}
/**
* Sends message to the target handle and receive the response.
*
* @param {!Object} payload
* @return {!Promise<!Object>}
*/
sendCommand_(to, from, message) {
const payload = { to, from, message, id: ++this.lastMessageId_ };
if (this.debug_)
console.log('\nSEND', payload);
const result = new Promise((fulfill, reject) =>
this.callbacks_.set(payload.id, {fulfill, reject}));
this.routeMessage_(false, payload);
return result;
}
/**
* Routes message between the worlds.
*
* @param {!Object} payload
*/
routeMessage_(fromParent, payload) {
if (this.debug_)
console.log(`\nROUTE[${this.worldId_}]`, payload);
if (payload.cookie) {
this.worldId_ = payload.worldId;
this.cookieCallback_(this.unwrap_(payload.args));
this.cookieCallback_ = null;
return;
}
// If this is a cookie request, the world is being initialized.
if (payload.cookieResponse) {
const callback = this.cookieResponseCallbacks_.get(payload.worldId);
this.cookieResponseCallbacks_.delete(payload.worldId);
callback({ result: this.unwrap_(payload.r), worldId: payload.worldId });
return;
}
if (!fromParent && !this.isActiveWorld_(payload.from[0])) {
// Dispatching from the disposed world.
if (this.debug_)
console.log(`DROP ON THE FLOOR`);
return;
}
if (payload.to[0] === this.worldId_) {
if (this.debug_)
console.log(`ROUTED TO SELF`);
this.dispatchMessageLocally_(payload);
return;
}
for (const [worldId, worldSend] of this.worlds_) {
if (payload.to[0].startsWith(worldId)) {
if (this.debug_)
console.log(`ROUTED TO CHILD ${worldId}`);
worldSend(payload);
return;
}
}
if (payload.to[0].startsWith(this.worldId_)) {
// Sending to the disposed world.
if (this.debug_)
console.log(`DROP ON THE FLOOR`);
return;
}
if (this.debug_)
console.log(`ROUTED TO PARENT`);
this.sendToParent_(payload);
}
/**
* @param {!Address} address
* @return {boolean}
*/
isActiveWorld_(worldId) {
if (this.worldId_ === worldId)
return true;
for (const wid of this.worlds_.keys()) {
if (worldId.startsWith(wid))
return true;
}
return false;
}
/**
* Message is routed from other worlds and hits rpc here.
*
* @param {!Object} payload
*/
async dispatchMessageLocally_(payload) {
if (this.debug_)
console.log('\nDISPATCH', payload);
// Dispatch the response.
if (typeof payload.rid === 'number') {
const {fulfill, reject} = this.callbacks_.get(payload.rid);
this.callbacks_.delete(payload.rid);
if (payload.e)
reject(new Error(payload.e));
else
fulfill(payload.r);
return;
}
const message = { from: payload.to, rid: payload.id, to: payload.from };
const handle = this.idToHandle_.get(payload.to[1]);
if (!handle) {
message.e = 'Object has been diposed.';
} else {
try {
message.r = await handle.dispatchMessage_(payload.message);
} catch (e) {
message.e = e.toString() + '\n' + e.stack;
}
}
this.routeMessage_(false, message);
}
}
module.exports = new Rpc();
================================================
FILE: rpc/rpc.md
================================================
## RPC API
> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is validated, it will be bumped to v1.0 and preserved for backwards compatibility.
### Handles
In Carlo's RPC system one can obtain a `handle` to a local `object` and pass it between the execution
contexts. Execution contexts can be Chrome, Node, child processes or any other JavaScript
execution environment, local or remote.

Calling a method on the `handle` results in calling it on the actual `object`:
```js
class Foo {
hello(name) { console.log(`hello ${name}`); }
}
const foo = rpc.handle(new Foo()); // <-- Obtained handle to object.
await foo.hello('world'); // <-- Prints 'hello world'.
```
> By default, `handle` has access to all the *public* methods of the object.
Public methods are the ones not starting or ending with `_`.
All handle operations are async, notice how synchronous `hello` method became async when accessed
via the handle. The world where `handle` is created can access the actual `object`. When handle is no longer needed, the world that created it can dispose it:
```js
const handle = rpc.handle(object);
const object = rpc.object(handle);
rpc.dispose(handle);
```
Properties of the target object are similarly accessible via the handle:
```js
const foo = rpc.handle({ myValue: 'value' }); // <-- Obtained handle to object.
await foo.myValue(); // <-- Returns 'value'.
```
Handles are passed between the worlds as arguments of the calls on other handles:
`World 1`
```js
class Parent {
constructor() {
this.children = [];
}
addChild(child) {
this.children.push(child);
return this.children.length - 1;
}
}
```
`World 2`
```js
class Child {}
async function main(parent) { // parent is a handle to the object from World 1.
const child = rpc.handle(new Child);
// Call method on parent remotely, pass handle to child into it.
const ordinal = await parent.addChild(child);
console.log(`Added child #${ordinal}`);
}
```
### Example
Following is an end-to-end example of the RPC application that demonstrates the variety of remote
operations that can be performed on handles:
`family.js`
```js
const rpc = require('rpc');
class Parent {
constructor() {
this.children = [];
}
addChild(child) {
const ordinal = this.children.length;
console.log(`Adding child #${ordinal}`);
child.setOrdinal(ordinal);
// Go over the children and make siblings aware of each other.
for (const c of this.children) {
c.setSibling(child);
child.setSibling(c);
}
this.children.push(child);
return ordinal;
}
}
class Child {
constructor() {
// Obtain handle to self that is used in RPC.
this.handle_ = rpc.handle(this);
}
setOrdinal(ordinal) { this.ordinal_ = ordinal; }
ordinal() { return this.ordinal_; }
async setSibling(sibling) {
// Say hello to another sibling when it is reported.
const o = await sibling.ordinal();
console.log(`I am #${this.ordinal_} and I have a sibling #${o}`);
await sibling.hiSibling(this.handle_);
}
async hiSibling(sibling) {
const o = await sibling.ordinal();
console.log(`I am #${this.ordinal_} and my sibling #${o} is saying hello`);
}
dispose() {
rpc.dispose(this.handle_);
}
}
module.exports = { Parent, Child };
```
`main.js` runs in the main process.
```js
const rpc = require('rpc');
const rpc_process = require('rpc_process');
const { Parent } = require('./family');
(async () => {
// Create parent object in the main process, obtain the handle to it.
const parent = rpc.handle(new Parent());
// Create a child process and load worker.js there. Pass parent object
// into that new child world, assign return value to a child.
const child1 = await rpc_process.spawn(__dirname + '/worker.js', parent);
parent.addChild(child1);
// Do it again.
const child2 = await rpc_process.spawn(__dirname + '/worker.js', parent);
parent.addChild(child2);
})();
```
`worker.js` runs in a child process.
```js
const rpc = require('rpc');
const rpc_process = require('rpc_process');
const { Child } = require('./family');
rpc_process.init(parent => {
// Note that parent is available in this context and we can call
// parent.addChild(rpc.handle(new Child)) here.
// But we prefer to simply return the handle to the newly created child
// into the parent world for the sake of this demo.
return rpc.handle(new Child());
});
```
================================================
FILE: rpc/rpc_process.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const child_process = require('child_process');
const rpc = require('./rpc');
async function spawn(fileName, ...args) {
const child = child_process.fork(fileName, [], {
detached: true, stdio: [0, 1, 2, 'ipc']
});
const transport = receivedFromChild => {
child.on('message', receivedFromChild);
return child.send.bind(child);
};
const { result } = await rpc.createWorld(transport, ...args);
return result;
}
function init(initializer) {
const transport = receivedFromParent => {
process.on('message', receivedFromParent);
return process.send.bind(process);
};
rpc.initWorld(transport, initializer);
}
module.exports = { spawn, init };
================================================
FILE: rpc/test.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');
const rpc = require('./rpc');
// Runner holds and runs all the tests
const runner = new TestRunner({
parallel: 1, // run 2 parallel threads
timeout: 1000, // setup timeout of 1 second per test
});
// Simple expect-like matchers
const {expect} = new Matchers();
// Extract jasmine-like DSL into the global namespace
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
async function createChildWorld(rpc, initializer, ...args) {
let sendToParent;
let sendToChild;
function transport1(receivedFromChild) {
sendToParent = receivedFromChild;
return data => setTimeout(() => sendToChild(data), 0);
}
function transport2(receivedFromParent) {
sendToChild = receivedFromParent;
return data => setTimeout(() => sendToParent(data), 0);
}
const childRpc = new rpc.constructor();
childRpc.initWorld(transport2, p => initializer(p, childRpc));
await rpc.createWorld(transport1, ...args);
return childRpc;
}
describe('rpc', () => {
it('call method', async(state, test) => {
class Foo {
sum(a, b) { return a + b; }
}
const foo = rpc.handle(new Foo());
expect(await foo.sum(1, 3)).toBe(4);
});
it('call method with object', async(state, test) => {
class Foo {
sum(a, b) { return { value: a.value + b.value }; }
}
const foo = rpc.handle(new Foo());
const result = await foo.sum({value: 1}, {value: 3});
expect(result.value).toBe(4);
});
it('call method with array', async(state, test) => {
class Foo {
sum(arr) { return arr.reduce((a, c) => a + c, 0); }
}
const foo = rpc.handle(new Foo());
const result = await foo.sum([1, 2, 3, 4, 5]);
expect(result).toBe(15);
});
it('call method with objects with handles', async(state, test) => {
class Foo {
async call(val) { return await val.a[0].name(); }
name() { return 'name'; }
}
const foo = rpc.handle(new Foo());
const result = await foo.call({a: [foo]});
expect(result).toBe('name');
});
it('call method with object with recursive link', async(state, test) => {
class Foo {
async call(val) { return await val.a[0].name(); }
name() { return 'name'; }
}
const foo = rpc.handle(new Foo());
const a = {};
a.a = a;
try {
await foo.call({a});
} catch (e) {
expect(e.message).toBe('Object reference chain is too long');
}
});
it('call method that does not exist', async(state, test) => {
class Foo {
}
const foo = rpc.handle(new Foo());
try {
await foo.sum(1, 3);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('There is no member');
}
});
it('call private method', async(state, test) => {
const foo = rpc.handle({});
try {
await foo._sum(1, 3);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('Private members are not exposed over RPC');
}
});
it('call method exception', async(state, test) => {
class Foo {
sum(a, b) { return b + c; }
}
const foo = rpc.handle(new Foo());
try {
await foo.sum(1, 3);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('c is not defined');
}
});
it('call nested exception', async(state, test) => {
class Foo {
sum(a, b) { return rpc.handle(this).doSum(a, b); }
doSum(a, b) { return b + c; }
}
const foo = rpc.handle(new Foo());
try {
await foo.sum(1, 3);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('c is not defined');
}
});
it('handle to function', async(state, test) => {
class Foo {
call(callback) { return callback(); }
}
const foo = rpc.handle(new Foo());
let calls = 0;
await foo.call(rpc.handle(() => ++calls));
expect(calls).toBe(1);
});
it('handle to function exception', async(state, test) => {
class Foo {
call(callback) { return callback(); }
}
const foo = rpc.handle(new Foo());
const calls = 0;
try {
await foo.call(rpc.handle(() => ++calls));
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('Assignment to constant');
}
});
it('access property', async(state, test) => {
const foo = rpc.handle({ value: 'Hello wold' });
expect(await foo.value()).toBe('Hello wold');
});
it('access property with params', async(state, test) => {
const foo = rpc.handle({ value: 'Hello wold' });
try {
expect(await foo.value(1)).toBe('Hello wold');
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('is not a function');
}
});
it('materialize handle', async(state, test) => {
const object = {};
const handle = rpc.handle(object);
expect(rpc.object(handle) === object).toBeTruthy();
});
it('access disposed handle', async(state, test) => {
class Foo {
sum(a, b) { return b + c; }
}
const foo = rpc.handle(new Foo());
rpc.dispose(foo);
try {
await foo.sum(1, 2);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('Object has been diposed');
}
});
it('dedupe implicit handles in the same world', async(state, test) => {
let foo2;
class Foo { foo(f) { foo2 = f; }}
const foo = rpc.handle(new Foo());
await foo.foo(foo);
expect(foo === foo2).toBeTruthy();
});
it('handle to handle should throw', async(state, test) => {
const handle = rpc.handle({});
try {
rpc.handle(handle);
expect(true).toBeFalsy();
} catch (e) {
expect(e.toString()).toContain('Can not return handle to handle');
}
});
it('parent / child communication', async(state, test) => {
const messages = [];
class Root { hello(message) { messages.push(message); } }
const root = rpc.handle(new Root());
await createChildWorld(rpc, p => p.hello('one'), root);
await createChildWorld(rpc, p => p.hello('two'), root);
expect(messages.join(',')).toBe('one,two');
});
it('parent / grand child communication', async(state, test) => {
const messages = [];
class Root { hello(message) { messages.push(message); } }
const root = rpc.handle(new Root());
await createChildWorld(rpc, async(p, r) => {
await createChildWorld(r, p => p.hello('one'), p);
}, root);
expect(messages.join(',')).toBe('one');
});
it('child / child communication', async(state, test) => {
const messages = [];
class Parent {
constructor() { this.children_ = []; }
addChild(child) {
this.children_.forEach(c => { c.setSibling(child); child.setSibling(c); });
this.children_.push(child);
}
}
class Child {
constructor() {}
setSibling(sibling) {
sibling.helloSibling('hello');
}
helloSibling(message) {
messages.push(message);
}
}
const parent = rpc.handle(new Parent());
await createChildWorld(rpc, (p, r) => p.addChild(r.handle(new Child())), parent);
await createChildWorld(rpc, (p, r) => p.addChild(r.handle(new Child())), parent);
await new Promise(f => setTimeout(f, 0));
await new Promise(f => setTimeout(f, 0));
expect(messages.join(',')).toBe('hello,hello');
});
it('dispose world', async(state, test) => {
const messages = [];
class Root { hello(message) { messages.push(message); } }
const root = rpc.handle(new Root());
let childRoot;
const childRpc = await createChildWorld(rpc, r => childRoot = r, root);
childRoot.hello('hello');
await new Promise(f => setTimeout(f, 0));
rpc.disposeWorld(childRpc.worldId_);
childRoot.hello('hello');
await new Promise(f => setTimeout(f, 0));
expect(messages.join(',')).toBe('hello');
});
it('dispose world half way', async(state, test) => {
const messages = [];
let go;
class Root {
hello(message) { messages.push(message); return new Promise(f => go = f); }
}
const root = rpc.handle(new Root());
let childRoot;
const childRpc = await createChildWorld(rpc, r => childRoot = r, root);
childRoot.hello('hello').then(() => messages.push('should-not-happen'));
await new Promise(f => setTimeout(f, 0));
rpc.disposeWorld(childRpc.worldId_);
go();
await new Promise(f => setTimeout(f, 0));
await new Promise(f => setTimeout(f, 0));
expect(messages.join(',')).toBe('hello');
});
});
// Reporter subscribes to TestRunner events and displays information in terminal
new Reporter(runner);
// Run all tests.
runner.run();
================================================
FILE: test/app.spec.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const carlo = require('../lib/carlo');
const {rpc} = require('../rpc');
let app;
function staticHandler(data) {
return request => {
for (const entry of data) {
const url = new URL(request.url());
if (url.pathname === entry[0]) {
request.fulfill({ body: Buffer.from(entry[1]), headers: entry[2]});
return;
}
}
request.continue();
};
}
afterEach(async({server, httpsServer}) => {
try { await app.exit(); } catch (e) {}
});
describe('app basics', () => {
it('evaluate', async() => {
app = await carlo.launch();
const ua = await app.evaluate('navigator.userAgent');
expect(ua).toContain('HeadlessChrome');
});
it('exposeFunction', async() => {
app = await carlo.launch();
await app.exposeFunction('foobar', () => 42);
const result = await app.evaluate('foobar()');
expect(result).toBe(42);
});
it('app load', async() => {
app = await carlo.launch();
await app.load('data:text/plain,hello');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello');
});
it('mainWindow accessor', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'));
await app.load('index.html');
expect(app.mainWindow().pageForTest().url()).toBe('https://domain/index.html');
});
it('createWindow creates window', async() => {
app = await carlo.launch();
let window = await app.createWindow();
expect(window.pageForTest().url()).toBe('about:blank?seq=1');
window = await app.createWindow();
expect(window.pageForTest().url()).toBe('about:blank?seq=2');
});
it('exit event is emitted', async() => {
app = await carlo.launch();
let callback;
const onexit = new Promise(f => callback = f);
app.on('exit', callback);
await app.mainWindow().close();
await onexit;
});
it('window event is emitted', async() => {
app = await carlo.launch();
const windows = [];
app.on('window', window => windows.push(window));
const window1 = await app.createWindow();
const window2 = await app.createWindow();
expect(window1).toBe(windows[0]);
expect(window2).toBe(windows[1]);
});
it('window exposeFunction', async() => {
app = await carlo.launch();
await app.exposeFunction('appFunc', () => 'app');
const w1 = await app.createWindow();
await w1.exposeFunction('windowFunc', () => 'window');
const result1 = await w1.evaluate(async() => (await appFunc()) + (await windowFunc()));
expect(result1).toBe('appwindow');
const w2 = await app.createWindow();
const result2 = await w2.evaluate(async() => (await appFunc()) + self.windowFunc);
expect(result2).toBe('appundefined');
});
});
describe('http serve', () => {
it('serveFolder works', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'));
await app.load('index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello file');
});
it('serveFolder prefix is respected works', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'), 'prefix');
await app.load('prefix/index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello file');
});
it('serveOrigin works', async({server}) => {
app = await carlo.launch();
app.serveOrigin(server.PREFIX);
await app.load('index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello http');
});
it('serveOrigin prefix is respected', async({server}) => {
app = await carlo.launch();
app.serveOrigin(server.PREFIX, 'prefix');
await app.load('prefix/index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello http');
});
it('HttpRequest params', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'));
const log = [];
app.serveHandler(request => {
log.push({url: request.url(), method: request.method(), ua: ('User-Agent' in request.headers()) });
request.continue();
});
await app.load('index.html');
expect(JSON.stringify(log)).toBe('[{"url":"https://domain/index.html","method":"GET","ua":true}]');
});
it('serveHandler can fulfill', async() => {
app = await carlo.launch();
app.serveHandler(request => {
if (!request.url().endsWith('index.html')) {
request.continue();
return;
}
request.fulfill({ body: Buffer.from('hello handler') });
});
await app.load('index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello handler');
});
it('serveHandler can continue to file', async() => {
app = await carlo.launch();
app.serveHandler(request => request.continue());
app.serveFolder(path.join(__dirname, 'folder'));
await app.load('index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello file');
});
it('serveHandler can continue to http', async({server}) => {
app = await carlo.launch();
app.serveOrigin(server.PREFIX);
app.serveHandler(request => request.continue());
await app.load('index.html');
const result = await app.evaluate('document.body.textContent');
expect(result).toBe('hello http');
});
xit('serveHandler can abort', async() => {
app = await carlo.launch();
app.serveHandler(request => request.abort());
try {
await app.load('index.html');
expect(false).toBeTruthy();
} catch (e) {
expect(e.toString()).toContain('domain/index.html');
}
});
it('window serveFolder', async() => {
app = await carlo.launch();
const w1 = await app.createWindow();
await w1.serveFolder(path.join(__dirname, 'folder'));
await w1.load('index.html');
const result1 = await w1.evaluate('document.body.textContent');
expect(result1).toBe('hello file');
const w2 = await app.createWindow();
try {
await w2.load('index.html');
expect(false).toBeTruthy();
} catch (e) {
expect(e.toString()).toContain('domain/index.html');
}
});
it('navigation history is empty', async() => {
app = await carlo.launch({ channel: ['canary'] });
app.serveFolder(path.join(__dirname, 'folder'));
await app.load('index.html?1');
await app.load('index.html?2');
await app.load('index.html?3');
expect(await app.evaluate('history.length')).toBe(1);
});
it('fail navigation', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'));
app.serveHandler(async request => {
request.url() === 'https://domain/index.html' ? request.fail() : request.continue();
});
await app.load('redirect.html');
expect(await app.evaluate(`window.location.href`)).toBe('chrome-error://chromewebdata/');
});
it('abort navigation', async() => {
app = await carlo.launch();
app.serveFolder(path.join(__dirname, 'folder'));
app.serveHandler(async request => {
request.url() === 'https://domain/index.html' ? request.abort() : request.continue();
});
await app.load('redirect.html');
expect(await app.evaluate(`window.location.href`)).toBe('https://domain/redirect.html');
});
});
describe('features', () => {
it('carlo.fileInfo', async() => {
const files = [[
'/index.html', `
<script>
async function check() {
const input = document.getElementById('file');
const info = await self.carlo.fileInfo(input.files[0]);
checkFileInfo(info);
}
</script>
<body><input type="file" id="file"></body>`
]];
app = await carlo.launch();
app.serveHandler(staticHandler(files));
let callback;
const result = new Promise(f => callback = f);
app.exposeFunction('checkFileInfo', callback);
await app.load('index.html');
const page = app.mainWindow().pageForTest();
const element = await page.evaluateHandle(`document.getElementById('file')`);
await element.uploadFile(__filename);
app.evaluate('check()');
const info = await result;
expect(info.path).toBe(__filename);
});
});
describe('rpc', () => {
it('load params are accessible', async() => {
const files = [[
'/index.html',
`<script>async function run() {
const [a, b] = await carlo.loadParams();
b.print(await a.val());
}
</script>
<body onload='run()'></body>`
]];
app = await carlo.launch();
app.serveHandler(staticHandler(files));
let callback;
const result = new Promise(f => callback = f);
await app.load('index.html',
rpc.handle({ val: 42 }),
rpc.handle({ print: v => callback(v) }));
expect(await result).toBe(42);
// Allow b.print to dispatch.
await new Promise(f => setTimeout(f, 0));
});
it('load params are accessible after reload', async() => {
const files = [[
'/index.html',
`<script>async function run() {
if (!window.location.search) {
setTimeout(() => {
window.location.href += '?reload';
}, 0);
return;
}
const [a, b] = await carlo.loadParams();
b.print(await a.val());
}
</script>
<body onload='run()'></body>`
]];
app = await carlo.launch();
app.serveHandler(staticHandler(files));
let callback;
const result = new Promise(f => callback = f);
await app.load('index.html',
rpc.handle({ val: 42 }),
rpc.handle({ print: v => callback(v) }));
expect(await result).toBe(42);
// Allow b.print to dispatch.
await new Promise(f => setTimeout(f, 0));
});
});
};
================================================
FILE: test/color.spec.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {Color} = require('../lib/color');
describe('color', () => {
it('rgb1', async(state, test) => {
color = Color.parse('rgb(94, 126, 91)');
expect(color.asString(Color.Format.RGB)).toBe('rgb(94, 126, 91)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsl(115, 16%, 43%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5bff');
expect(color.asString(Color.Format.HEX)).toBe('#5e7e5b');
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('rgb(94, 126, 91)');
});
it('rgb2', async(state, test) => {
color = Color.parse('rgba(94 126 91)');
expect(color.asString(Color.Format.RGB)).toBe('rgba(94 126 91)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsl(115, 16%, 43%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5bff');
expect(color.asString(Color.Format.HEX)).toBe('#5e7e5b');
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('rgb(94, 126, 91)');
});
it('rgb3', async(state, test) => {
color = Color.parse('rgba(94, 126, 91, 0.5)');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 0.5)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 0.5)');
expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5b80');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('rgba(94, 126, 91, 0.5)');
});
it('rgb4', async(state, test) => {
color = Color.parse('rgb(94 126 91 / 50%)');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgb(94 126 91 / 50%)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 0.5)');
expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5b80');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('rgba(94, 126, 91, 0.5)');
});
it('hsl1', async(state, test) => {
color = Color.parse('hsl(212, 55%, 32%)');
expect(color.asString(Color.Format.RGB)).toBe('rgb(37, 79, 126)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsl(212, 55%, 32%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#254f7eff');
expect(color.asString(Color.Format.HEX)).toBe('#254f7e');
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('hsl(212, 55%, 32%)');
});
it('hsl2', async(state, test) => {
color = Color.parse('hsla(212 55% 32%)');
expect(color.asString(Color.Format.RGB)).toBe('rgb(37, 79, 126)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsla(212 55% 32%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#254f7eff');
expect(color.asString(Color.Format.HEX)).toBe('#254f7e');
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('hsl(212, 55%, 32%)');
});
it('hsl3', async(state, test) => {
color = Color.parse('hsla(212, 55%, 32%, 0.5)');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 0.5)');
expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');
});
it('hsl4', async(state, test) => {
color = Color.parse('hsla(212 55% 32% / 50%)');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(212 55% 32% / 50%)');
expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');
});
it('hsl5', async(state, test) => {
color = Color.parse('hsla(212deg 55% 32% / 50%)');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(212deg 55% 32% / 50%)');
expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');
});
it('hex1', async(state, test) => {
color = Color.parse('#12345678');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(18, 52, 86, 0.47058823529411764)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(210, 65%, 20%, 0.47058823529411764)');
expect(color.asString(Color.Format.HEXA)).toBe('#12345678');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe(null);
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('#12345678');
});
it('hex2', async(state, test) => {
color = Color.parse('#00FFFF');
expect(color.asString(Color.Format.RGB)).toBe('rgb(0, 255, 255)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(0, 255, 255, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsl(180, 100%, 50%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(180, 100%, 50%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#00ffffff');
expect(color.asString(Color.Format.HEX)).toBe('#00FFFF');
expect(color.asString(Color.Format.ShortHEXA)).toBe('#0fff');
expect(color.asString(Color.Format.ShortHEX)).toBe('#0ff');
expect(color.asString()).toBe('#00ffff');
});
it('hex3', async(state, test) => {
color = Color.parse('#1234');
expect(color.asString(Color.Format.RGB)).toBe(null);
expect(color.asString(Color.Format.RGBA)).toBe('rgba(17, 34, 51, 0.26666666666666666)');
expect(color.asString(Color.Format.HSL)).toBe(null);
expect(color.asString(Color.Format.HSLA)).toBe('hsla(210, 50%, 13%, 0.26666666666666666)');
expect(color.asString(Color.Format.HEXA)).toBe('#11223344');
expect(color.asString(Color.Format.HEX)).toBe(null);
expect(color.asString(Color.Format.ShortHEXA)).toBe('#1234');
expect(color.asString(Color.Format.ShortHEX)).toBe(null);
expect(color.asString()).toBe('#1234');
});
it('hex4', async(state, test) => {
color = Color.parse('#0FF');
expect(color.asString(Color.Format.RGB)).toBe('rgb(0, 255, 255)');
expect(color.asString(Color.Format.RGBA)).toBe('rgba(0, 255, 255, 1)');
expect(color.asString(Color.Format.HSL)).toBe('hsl(180, 100%, 50%)');
expect(color.asString(Color.Format.HSLA)).toBe('hsla(180, 100%, 50%, 1)');
expect(color.asString(Color.Format.HEXA)).toBe('#00ffffff');
expect(color.asString(Color.Format.HEX)).toBe('#00ffff');
expect(color.asString(Color.Format.ShortHEXA)).toBe('#0fff');
expect(color.asString(Color.Format.ShortHEX)).toBe('#0FF');
expect(color.asString()).toBe('#0ff');
});
});
};
================================================
FILE: test/folder/index.html
================================================
hello file
================================================
FILE: test/folder/redirect.html
================================================
<script>
window.location = 'https://domain/index.html';
</script>
================================================
FILE: test/headful.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');
const path = require('path');
const carlo = require('../lib/carlo');
// Runner holds and runs all the tests
const testRunner = new TestRunner({
parallel: 1, // run 2 parallel threads
timeout: 3000, // setup timeout of 1 second per test
});
const {expect} = new Matchers();
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
describe('app reuse', () => {
fit('load returns value', async() => {
app = await carlo.launch();
let callback;
const windowPromise = new Promise(f => callback = f);
app.on('window', callback);
try {
await carlo.launch({paramsForReuse: {val: 42}});
expect(false).toBeTruthy();
} catch (e) {
expect(e.toString()).toContain('already running');
}
const window = await windowPromise;
expect(JSON.stringify(window.paramsForReuse())).toBe('{"val":42}');
});
});
// Reporter subscribes to TestRunner events and displays information in terminal
new Reporter(testRunner);
// Run all tests.
testRunner.run();
================================================
FILE: test/http/index.html
================================================
hello http
================================================
FILE: test/test.js
================================================
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');
const {TestServer} = require('@pptr/testserver');
const path = require('path');
const carlo = require('../lib/carlo');
carlo.enterTestMode();
// Runner holds and runs all the tests
const testRunner = new TestRunner({
parallel: 1, // run 2 parallel threads
timeout: 3000, // setup timeout of 1 second per test
});
const {expect} = new Matchers();
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
beforeAll(async state => {
const assetsPath = path.join(__dirname, 'http');
const port = 8907 + state.parallelIndex * 2;
state.server = await TestServer.create(assetsPath, port);
state.server.PORT = port;
state.server.PREFIX = `http://localhost:${port}`;
const httpsPort = port + 1;
state.httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);
state.httpsServer.PORT = httpsPort;
state.httpsServer.PREFIX = `https://localhost:${httpsPort}`;
});
afterAll(async({server, httpsServer}) => {
await Promise.all([
server.stop(),
httpsServer.stop(),
]);
});
beforeEach(async({server, httpsServer}) => {
server.reset();
httpsServer.reset();
});
require('./app.spec.js').addTests({testRunner, expect});
require('./color.spec.js').addTests({testRunner, expect});
// Reporter subscribes to TestRunner events and displays information in terminal
new Reporter(testRunner);
// Run all tests.
testRunner.run();
gitextract_4gq2v2ts/
├── .gitignore
├── .npmignore
├── API.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples/
│ ├── photobooth/
│ │ ├── README.md
│ │ ├── main.js
│ │ ├── package.json
│ │ └── www/
│ │ └── index.html
│ ├── systeminfo/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── main.js
│ │ ├── package.json
│ │ ├── test.js
│ │ └── www/
│ │ └── index.html
│ ├── terminal/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── main.js
│ │ ├── package.json
│ │ ├── worker.js
│ │ └── www/
│ │ └── index.html
│ └── windows/
│ ├── README.md
│ ├── main.html
│ ├── main.js
│ └── package.json
├── index.js
├── lib/
│ ├── carlo.js
│ ├── color.js
│ ├── features/
│ │ ├── file_info.js
│ │ └── shortcuts.js
│ ├── find_chrome.js
│ └── http_request.js
├── package.json
├── rpc/
│ ├── index.js
│ ├── rpc.js
│ ├── rpc.md
│ ├── rpc_process.js
│ └── test.js
└── test/
├── app.spec.js
├── color.spec.js
├── folder/
│ ├── index.html
│ └── redirect.html
├── headful.js
├── http/
│ └── index.html
└── test.js
SYMBOL INDEX (177 symbols across 14 files)
FILE: examples/photobooth/main.js
function saveImage (line 49) | function saveImage(base64) {
FILE: examples/systeminfo/app.js
function run (line 24) | async function run() {
function systeminfo (line 52) | async function systeminfo() {
FILE: examples/terminal/main.js
class TerminalApp (line 25) | class TerminalApp {
method constructor (line 26) | constructor() {
method launch_ (line 33) | async launch_() {
method newWindow (line 55) | async newWindow() {
method initUI_ (line 62) | async initUI_(win) {
FILE: examples/terminal/worker.js
class Terminal (line 26) | class Terminal extends EventEmitter {
method constructor (line 27) | constructor() {
method on (line 38) | on(event, func) {
method resize (line 44) | resize(cols, rows) {
method write (line 48) | write(data) {
method dispose (line 52) | dispose() {
FILE: examples/windows/main.js
class Backend (line 24) | class Backend {
method constructor (line 25) | constructor(app) {
method showMyWindow (line 30) | showMyWindow(url) {
method createWindow_ (line 39) | async createWindow_(url) {
FILE: lib/carlo.js
class App (line 36) | class App extends EventEmitter {
method constructor (line 41) | constructor(browser, options) {
method init_ (line 52) | async init_() {
method exit (line 86) | async exit() {
method mainWindow (line 98) | mainWindow() {
method createWindow (line 107) | async createWindow(options = {}) {
method windows (line 132) | windows() {
method exposeFunction (line 141) | exposeFunction(name, func) {
method evaluate (line 151) | evaluate(pageFunction, ...args) {
method serveFolder (line 159) | serveFolder(folder = '', prefix = '') {
method serveOrigin (line 170) | serveOrigin(base, prefix = '') {
method serveHandler (line 179) | serveHandler(handler) {
method load (line 188) | async load(uri = '', ...params) {
method setIcon (line 196) | async setIcon(icon) {
method browserForTest (line 206) | browserForTest() {
method targetCreated_ (line 210) | async targetCreated_(target) {
method pageCreated_ (line 220) | async pageCreated_(page) {
method windowClosed_ (line 238) | windowClosed_(window) {
class Window (line 251) | class Window extends EventEmitter {
method constructor (line 257) | constructor(app, page, options) {
method init_ (line 268) | async init_() {
method exposeFunction (line 293) | exposeFunction(name, func) {
method evaluate (line 303) | evaluate(pageFunction, ...args) {
method serveFolder (line 311) | serveFolder(folder = '', prefix = '') {
method serveOrigin (line 322) | serveOrigin(base, prefix = '') {
method serveHandler (line 331) | serveHandler(handler) {
method load (line 340) | async load(uri = '', ...params) {
method initBounds_ (line 357) | initBounds_(result) {
method pageForTest (line 369) | pageForTest() {
method paramsForReuse (line 383) | paramsForReuse() {
method configureRpcOnce_ (line 387) | async configureRpcOnce_() {
method domContentLoaded_ (line 420) | async domContentLoaded_() {
method initializeInterception_ (line 439) | async initializeInterception_() {
method requestIntercepted_ (line 453) | async requestIntercepted_(payload) {
method handleRequest_ (line 467) | async handleRequest_(request) {
method bounds (line 503) | async bounds() {
method setBounds (line 511) | async setBounds(bounds) {
method fullscreen (line 515) | async fullscreen() {
method minimize (line 520) | async minimize() {
method maximize (line 525) | async maximize() {
method bringToFront (line 530) | bringToFront() {
method close (line 534) | close() {
method closed_ (line 538) | closed_() {
method isClosed (line 547) | isClosed() {
function contentType (line 569) | function contentType(request, fileName) {
function launch (line 587) | async function launch(options = {}) {
class HostWindow (line 637) | class HostWindow {
method constructor (line 641) | constructor(win) {
method closeBrowser (line 645) | closeBrowser() {
method fileInfo (line 650) | async fileInfo(expression) {
function enterTestMode (line 656) | function enterTestMode() {
function wrapPrefix (line 660) | function wrapPrefix(prefix) {
FILE: lib/color.js
class Color (line 19) | class Color {
method constructor (line 25) | constructor(rgba, format, originalText) {
method parse (line 49) | static parse(text) {
method _parsePercentOrNumber (line 134) | static _parsePercentOrNumber(value) {
method _parseRgbNumeric (line 151) | static _parseRgbNumeric(value) {
method _parseHueNumeric (line 165) | static _parseHueNumeric(value) {
method _parseSatLightNumeric (line 184) | static _parseSatLightNumeric(value) {
method _parseAlphaNumeric (line 195) | static _parseAlphaNumeric(value) {
method hsl2rgb (line 203) | static hsl2rgb(hsl, out_rgb) {
method format (line 248) | format() {
method hsla (line 255) | hsla() {
method hasAlpha (line 295) | hasAlpha() {
method detectHEXFormat (line 302) | detectHEXFormat() {
method asString (line 322) | asString(format) {
method rgba (line 396) | rgba() {
method canonicalRGBA (line 403) | canonicalRGBA() {
FILE: lib/features/shortcuts.js
function preventDefaultShortcuts (line 26) | function preventDefaultShortcuts(event) {
FILE: lib/find_chrome.js
function darwin (line 27) | function darwin(canary) {
function linux (line 53) | function linux(canary) {
function win32 (line 100) | function win32(canary) {
function sort (line 117) | function sort(installations, priorities) {
function canAccess (line 134) | function canAccess(file) {
function uniq (line 146) | function uniq(arr) {
function findChromeExecutables (line 150) | function findChromeExecutables(folder) {
function downloadChromium (line 182) | async function downloadChromium(options, targetRevision) {
function findChrome (line 209) | async function findChrome(options) {
FILE: lib/http_request.js
class HttpRequest (line 87) | class HttpRequest {
method constructor (line 92) | constructor(session, params, handlers) {
method url (line 103) | url() {
method method (line 110) | method() {
method headers (line 117) | headers() {
method resourceType (line 124) | resourceType() {
method abort (line 131) | abort() {
method fail (line 140) | fail() {
method continue (line 148) | continue() {
method deferToBrowser (line 161) | deferToBrowser(overrides) {
method fulfill (line 177) | fulfill({status, headers, body}) {
method callNextHandler_ (line 206) | callNextHandler_() {
method resolve_ (line 220) | async resolve_(params) {
FILE: rpc/rpc.js
class Handle (line 30) | class Handle {
method constructor (line 39) | constructor(localAddress, address, descriptor, rpc) {
method proxyHandler_ (line 55) | static proxyHandler_(target, methodName, receiver) {
method callMethod_ (line 74) | async callMethod_(method, ...args) {
method dispatchMessage_ (line 88) | async dispatchMessage_(message) {
method proxy (line 112) | proxy() {
class Rpc (line 122) | class Rpc {
method constructor (line 123) | constructor() {
method params (line 143) | params() {
method createWorld (line 160) | createWorld(transport, ...args) {
method disposeWorld (line 174) | disposeWorld(worldId) {
method initWorld (line 185) | initWorld(transport, initializer) {
method handle (line 198) | handle(object) {
method object (line 218) | object(proxy) {
method dispose (line 226) | dispose(proxy) {
method describe_ (line 237) | describe_(o) {
method wrap_ (line 249) | wrap_(param, maxDepth = 1000) {
method unwrap_ (line 279) | unwrap_(param) {
method createHandle_ (line 310) | createHandle_(address, descriptor) {
method sendCommand_ (line 327) | sendCommand_(to, from, message) {
method routeMessage_ (line 342) | routeMessage_(fromParent, payload) {
method isActiveWorld_ (line 400) | isActiveWorld_(worldId) {
method dispatchMessageLocally_ (line 415) | async dispatchMessageLocally_(payload) {
FILE: rpc/rpc_process.js
function spawn (line 22) | async function spawn(fileName, ...args) {
function init (line 35) | function init(initializer) {
FILE: rpc/test.js
function createChildWorld (line 33) | async function createChildWorld(rpc, initializer, ...args) {
class Foo (line 52) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 59) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 67) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 75) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 84) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 98) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 118) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 130) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 143) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 152) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 183) | class Foo {
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Foo (line 197) | class Foo { foo(f) { foo2 = f; }}
method sum (line 53) | sum(a, b) { return a + b; }
method sum (line 60) | sum(a, b) { return { value: a.value + b.value }; }
method sum (line 68) | sum(arr) { return arr.reduce((a, c) => a + c, 0); }
method call (line 76) | async call(val) { return await val.a[0].name(); }
method name (line 77) | name() { return 'name'; }
method call (line 85) | async call(val) { return await val.a[0].name(); }
method name (line 86) | name() { return 'name'; }
method sum (line 119) | sum(a, b) { return b + c; }
method sum (line 131) | sum(a, b) { return rpc.handle(this).doSum(a, b); }
method doSum (line 132) | doSum(a, b) { return b + c; }
method call (line 144) | call(callback) { return callback(); }
method call (line 153) | call(callback) { return callback(); }
method sum (line 184) | sum(a, b) { return b + c; }
method foo (line 197) | foo(f) { foo2 = f; }
class Root (line 213) | class Root { hello(message) { messages.push(message); } }
method hello (line 213) | hello(message) { messages.push(message); }
method hello (line 221) | hello(message) { messages.push(message); }
method hello (line 255) | hello(message) { messages.push(message); }
method hello (line 273) | hello(message) { messages.push(message); return new Promise(f => go = ...
class Root (line 221) | class Root { hello(message) { messages.push(message); } }
method hello (line 213) | hello(message) { messages.push(message); }
method hello (line 221) | hello(message) { messages.push(message); }
method hello (line 255) | hello(message) { messages.push(message); }
method hello (line 273) | hello(message) { messages.push(message); return new Promise(f => go = ...
class Parent (line 230) | class Parent {
method constructor (line 231) | constructor() { this.children_ = []; }
method addChild (line 232) | addChild(child) {
class Child (line 237) | class Child {
method constructor (line 238) | constructor() {}
method setSibling (line 239) | setSibling(sibling) {
method helloSibling (line 242) | helloSibling(message) {
class Root (line 255) | class Root { hello(message) { messages.push(message); } }
method hello (line 213) | hello(message) { messages.push(message); }
method hello (line 221) | hello(message) { messages.push(message); }
method hello (line 255) | hello(message) { messages.push(message); }
method hello (line 273) | hello(message) { messages.push(message); return new Promise(f => go = ...
class Root (line 272) | class Root {
method hello (line 213) | hello(message) { messages.push(message); }
method hello (line 221) | hello(message) { messages.push(message); }
method hello (line 255) | hello(message) { messages.push(message); }
method hello (line 273) | hello(message) { messages.push(message); return new Promise(f => go = ...
FILE: test/app.spec.js
function staticHandler (line 29) | function staticHandler(data) {
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
{
"path": ".gitignore",
"chars": 139,
"preview": ".DS_Store\n.eslintrc.js\n.eslintignore\n.profile\n.vscode\nnode_modules\npackage-lock.json\nrpc/node_modules\nrpc/package-lock.j"
},
{
"path": ".npmignore",
"chars": 227,
"preview": "# repeats from .gitignore\n.DS_Store\n.eslintrc.js\n.eslintignore\n.profile\n.vscode\nnode_modules\npackage-lock.json\nrpc/node_"
},
{
"path": "API.md",
"chars": 19626,
"preview": "## API v0.9\n\n> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is vali"
},
{
"path": "CONTRIBUTING.md",
"chars": 1101,
"preview": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guid"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 5053,
"preview": "# Carlo - headful Node app framework\n\n### ❗Carlo is [no longer maintained](https://github.com/GoogleChromeLabs/carlo/iss"
},
{
"path": "examples/photobooth/README.md",
"chars": 219,
"preview": "### Usage\n\n> This example requires Chrome 72 (Chrome Canary) to function.\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun "
},
{
"path": "examples/photobooth/main.js",
"chars": 1785,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "examples/photobooth/package.json",
"chars": 518,
"preview": "{\n \"name\": \"photobooth-app\",\n \"version\": \"0.9.0\",\n \"description\": \"Photo Booth App\",\n \"main\": \"main.js\",\n \"scripts\""
},
{
"path": "examples/photobooth/www/index.html",
"chars": 2347,
"preview": "<!--\n Copyright 2018 Google Inc. All rights reserved.\n\n Licensed under the Apache License, Version 2.0 (the \"License\")"
},
{
"path": "examples/systeminfo/.gitignore",
"chars": 63,
"preview": "node_modules\npackage-lock.json\n.profile\n.DS_Store\n.vscode\n.idea"
},
{
"path": "examples/systeminfo/README.md",
"chars": 155,
"preview": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n\nOptionally package as execut"
},
{
"path": "examples/systeminfo/app.js",
"chars": 1945,
"preview": "/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Versi"
},
{
"path": "examples/systeminfo/main.js",
"chars": 679,
"preview": "/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Versi"
},
{
"path": "examples/systeminfo/package.json",
"chars": 584,
"preview": "{\n \"name\": \"systeminfo-app\",\n \"version\": \"0.9.0\",\n \"description\": \"System info example\",\n \"main\": \"main.js\",\n \"scri"
},
{
"path": "examples/systeminfo/test.js",
"chars": 1683,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "examples/systeminfo/www/index.html",
"chars": 3029,
"preview": "<!--\n Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n Licensed under the Apache License, Version "
},
{
"path": "examples/terminal/.gitignore",
"chars": 63,
"preview": "node_modules\npackage-lock.json\n.profile\n.DS_Store\n.vscode\n.idea"
},
{
"path": "examples/terminal/README.md",
"chars": 91,
"preview": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n"
},
{
"path": "examples/terminal/main.js",
"chars": 1989,
"preview": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the "
},
{
"path": "examples/terminal/package.json",
"chars": 389,
"preview": "{\n \"name\": \"xterm-app\",\n \"version\": \"0.9.0\",\n \"description\": \"Terminal example\",\n \"main\": \"main.js\",\n \"scripts\": {\n"
},
{
"path": "examples/terminal/worker.js",
"chars": 1529,
"preview": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the "
},
{
"path": "examples/terminal/www/index.html",
"chars": 1776,
"preview": "<!--\n Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n Licensed under the Apache License, Version "
},
{
"path": "examples/windows/README.md",
"chars": 91,
"preview": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n"
},
{
"path": "examples/windows/main.html",
"chars": 1207,
"preview": "<!--\n Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n Licensed under the Apache License, Version "
},
{
"path": "examples/windows/main.js",
"chars": 1661,
"preview": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the "
},
{
"path": "examples/windows/package.json",
"chars": 339,
"preview": "{\n \"name\": \"windows-app\",\n \"version\": \"0.9.0\",\n \"description\": \"Multiple windows example\",\n \"main\": \"main.js\",\n \"sc"
},
{
"path": "index.js",
"chars": 674,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/carlo.js",
"chars": 20052,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/color.js",
"chars": 11689,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/features/file_info.js",
"chars": 1020,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/features/shortcuts.js",
"chars": 2545,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/find_chrome.js",
"chars": 8628,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "lib/http_request.js",
"chars": 6079,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "package.json",
"chars": 774,
"preview": "{\n \"name\": \"carlo\",\n \"version\": \"0.9.46\",\n \"description\": \"Carlo is a framework for rendering Node data structures us"
},
{
"path": "rpc/index.js",
"chars": 720,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "rpc/rpc.js",
"chars": 13266,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "rpc/rpc.md",
"chars": 4577,
"preview": "## RPC API\n\n> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is valid"
},
{
"path": "rpc/rpc_process.js",
"chars": 1309,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "rpc/test.js",
"chars": 9372,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "test/app.spec.js",
"chars": 11298,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "test/color.spec.js",
"chars": 10037,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "test/folder/index.html",
"chars": 10,
"preview": "hello file"
},
{
"path": "test/folder/redirect.html",
"chars": 68,
"preview": "<script>\n window.location = 'https://domain/index.html';\n</script>\n"
},
{
"path": "test/headful.js",
"chars": 1781,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
},
{
"path": "test/http/index.html",
"chars": 10,
"preview": "hello http"
},
{
"path": "test/test.js",
"chars": 2055,
"preview": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'Licens"
}
]
About this extraction
This page contains the full source code of the GoogleChromeLabs/carlo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (161.7 KB), approximately 42.9k tokens, and a symbol index with 177 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.