Full Code of GoogleChromeLabs/gulliver for AI

main a8569469212f cached
158 files
839.8 KB
315.3k tokens
387 symbols
1 requests
Download .txt
Showing preview only (887K chars total). Download the full file or copy to clipboard to get everything.
Repository: GoogleChromeLabs/gulliver
Branch: main
Commit: a8569469212f
Files: 158
Total size: 839.8 KB

Directory structure:
gitextract_v5m2kdy5/

├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gcloudignore
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── FAQ.md
├── LICENSE
├── README.md
├── app.js
├── app.yaml
├── config/
│   ├── .gitignore
│   ├── config.example.json
│   └── config.js
├── controllers/
│   ├── api/
│   │   ├── favorite-pwa.js
│   │   ├── index.js
│   │   ├── lighthouse.js
│   │   ├── notifications.js
│   │   └── pwa.js
│   ├── app.js
│   ├── cache.js
│   ├── index.js
│   ├── pwa.js
│   ├── sw.js
│   └── tasks.js
├── cron.yaml
├── firebase-messaging-sw-generator.js
├── firebase-messaging-sw.tmpl
├── index.yaml
├── lib/
│   ├── asset-hashing.js
│   ├── color.js
│   ├── data-cache.js
│   ├── data-fetcher.js
│   ├── event-bus.js
│   ├── favorite-pwa.js
│   ├── images.js
│   ├── lighthouse.js
│   ├── manifest.js
│   ├── metadata.js
│   ├── model-datastore.js
│   ├── notifications.js
│   ├── promise-sequential.js
│   ├── pwa-index.js
│   ├── pwa.js
│   ├── search.js
│   ├── tasks.js
│   ├── verify-id-token.js
│   └── web-performance.js
├── lighthouse_machine/
│   ├── .dockerignore
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── Dockerfile
│   ├── LICENSE
│   ├── README.md
│   ├── app.yaml
│   ├── chromeuser-script.sh
│   ├── cpu_monitor.js
│   ├── entrypoint.sh
│   ├── etc/
│   │   └── xvfb
│   ├── package.json
│   └── server.js
├── middlewares/
│   └── index.js
├── models/
│   ├── favorite-pwa.js
│   ├── lighthouse.js
│   ├── manifest.js
│   ├── pwa.js
│   ├── task.js
│   └── user.js
├── package.json
├── public/
│   ├── .well-known/
│   │   └── assetlinks.json
│   ├── css/
│   │   └── style.css
│   ├── favicons/
│   │   └── browserconfig.xml
│   ├── google3915c2aaf77f961f.html
│   ├── humans.txt
│   ├── js/
│   │   ├── analytics.js
│   │   ├── chart.js
│   │   ├── event-target.js
│   │   ├── gapi.es6.js
│   │   ├── gulliver-config.js
│   │   ├── gulliver.es6.js
│   │   ├── loader.js
│   │   ├── messaging.js
│   │   ├── offline-support.js
│   │   ├── pwa-form.js
│   │   ├── routing/
│   │   │   ├── route.js
│   │   │   ├── router.js
│   │   │   └── transitions.js
│   │   ├── search-input.js
│   │   ├── shell.js
│   │   ├── signin.js
│   │   ├── ui/
│   │   │   ├── notification-checkbox.js
│   │   │   ├── share-button.js
│   │   │   └── signin-button.js
│   │   └── util/
│   │       └── requestIdleCallback.js
│   ├── manifest.json
│   ├── robots.txt
│   └── sw.js
├── rollup-config/
│   ├── gulliver.js
│   ├── lighthouse-chart.js
│   └── pwa-form.js
├── test/
│   ├── app/
│   │   ├── controllers/
│   │   │   ├── api/
│   │   │   │   ├── favorite-pwa.js
│   │   │   │   ├── lighthouse.js
│   │   │   │   └── pwa.js
│   │   │   ├── cache.js
│   │   │   └── tasks.js
│   │   ├── lib/
│   │   │   ├── asset-hashing.js
│   │   │   ├── color.js
│   │   │   ├── data-fetcher.js
│   │   │   ├── favorite-pwa.js
│   │   │   ├── images.js
│   │   │   ├── lighthouse-example.json
│   │   │   ├── lighthouse.js
│   │   │   ├── manifest.js
│   │   │   ├── model-datastore.js
│   │   │   ├── notifications.js
│   │   │   ├── promise-sequential.js
│   │   │   ├── pwa.js
│   │   │   ├── search.js
│   │   │   └── tasks.js
│   │   ├── manifests/
│   │   │   ├── icon-url-with-parameter.json
│   │   │   ├── inline-image-large-content.json
│   │   │   ├── invalid-theme-color.json
│   │   │   └── no-icon-array.json
│   │   ├── models/
│   │   │   └── pwa.js
│   │   └── views/
│   │       └── helpers/
│   │           └── index.js
│   └── client/
│       └── js/
│           └── event-target.js
├── third_party/
│   ├── Color.js
│   ├── README.md
│   ├── install.sh
│   └── manifest-parser.js
├── tsconfig.json
└── views/
    ├── 404.hbs
    ├── app/
    │   ├── offline.hbs
    │   └── shell.hbs
    ├── helpers/
    │   └── index.js
    ├── includes/
    │   ├── chevron_left.hbs
    │   ├── chevron_right.hbs
    │   ├── footer.hbs
    │   ├── head.hbs
    │   ├── header.hbs
    │   ├── hourglass.hbs
    │   ├── icon_log_in.hbs
    │   ├── icon_log_out.hbs
    │   ├── icon_search.hbs
    │   ├── icon_share.hbs
    │   ├── lighthouse.hbs
    │   ├── metadata.hbs
    │   ├── notifications_active.hbs
    │   ├── notifications_off.hbs
    │   ├── pagespeedinsight.hbs
    │   ├── pwadetails.hbs
    │   ├── score.hbs
    │   └── webpagetest.hbs
    └── pwas/
        ├── form.hbs
        ├── list.hbs
        ├── view-rss.hbs
        └── view.hbs

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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.
 */
{
  "env": {
    "test": {
      "presets": ["es2015"]
    },
    "default": {
      "presets": [
        [
          "es2015",
          {
            "modules": false
          }
        ]
      ],
      "plugins": ["external-helpers"]
    }
  }
}


================================================
FILE: .eslintignore
================================================
/coverage
/third_party
/public/js/gulliver.js
/public/js/lighthouse-chart.js
/public/js/pwa-form.js
/lighthouse_machine


================================================
FILE: .eslintrc.json
================================================
{
  "extends": "google",
  // http://eslint.org/docs/rules/
  "rules": {
    "max-len": [2, 100, {
      "ignoreComments": true,
      "ignoreUrls": true,
      "tabWidth": 2
    }],
    "no-implicit-coercion": [2, {
      "boolean": false,
      "number": true,
      "string": true
    }],
    "no-unused-expressions": [2, {
      "allowShortCircuit": true,
      "allowTernary": false
    }],
    "no-unused-vars": [2, {
      "vars": "all",
      "args": "after-used",
      "argsIgnorePattern": "(^reject$|^_$)",
      "varsIgnorePattern": "(^_$)"
    }],
    "quotes": [2, "single"],
    "require-jsdoc": 0,
    "valid-jsdoc": 0,
    "prefer-arrow-callback": 1,
    "no-var": 1
  },
  // http://eslint.org/docs/user-guide/configuring#specifying-environments
  "env": {
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 2017
  }
}


================================================
FILE: .gcloudignore
================================================
node_modules/
lighthouse_machine/


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
npm-debug.log
/coverage
.jshintrc
.idea/
key.json
public/js/gulliver.js
public/js/gulliver.js.map
public/firebase-messaging-sw.js



================================================
FILE: .travis.yml
================================================
language: node_js
sudo: required
dist: trusty
node_js:
  - "8"
before_script:
  - npm install
script:
  - npm test
env:
  # evade checks in config.js
  - CLIENT_ID=placeholder CLIENT_SECRET=placeholder GCLOUD_PROJECT=placeholder CLOUD_BUCKET=placeholder FIREBASE_AUTH=placeholder API_TOKENS="abcdefghijk"
  

================================================
FILE: CONTRIBUTING.md
================================================
Want to contribute? Great! First, read this page (including the small print at the end).

### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement]
(https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.

### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.

### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement]
(https://cla.developers.google.com/about/google-corporate).


================================================
FILE: FAQ.md
================================================
# PWA Directory FAQ

### What is PWA Directory?
Is an open source directory of Progressive Web Apps driven by user submissions. 

### What are the goals of this project?
Its goals are to help developers discover new PWAs, build a good example of a Server-Side Rendered PWA and share what we learn during the developing process.

### Is this a Google product?
No, it was built by the Google Developer Relations team as an example for the Web developer community.

### How does it rank PWAs?
We use [Lighthouse](https://github.com/GoogleChrome/lighthouse), that runs a set of checks validating the existence of the features, 
capabilities, and performance that should characterize a PWA.

### Why is my Lighthouse score different from the Lighthouse Chrome Extension?
It is important to highlight that we use a version of Lighthouse built on [Headless Chromium](https://chromium.googlesource.com/chromium/src/+/master/headless/README.md) which enables it to run as a server app, for that reason our Lighthouse score and report may deviate from the standard Lighthouse Chrome extension.

### Why are you using Server Side Rendering?
We found that there are not that many examples of PWAs using Server Side Rendering and that many developers would benefit from one.

### What technologies did you use?
*Backend*
 - [Node.js](https://nodejs.org/en/) 
 - [Express.js](http://expressjs.com/)
 - [Handlebars](http://handlebarsjs.com/)
 - [Google App Engine Node.js Flexible Environment](https://cloud.google.com/appengine/docs/flexible/nodejs/)

*Frontend*
 - JavaScript (vanilla)
 - [Service Worker Precache](https://github.com/GoogleChrome/sw-precache)
 - [Service Worker Toolbox](https://github.com/GoogleChrome/sw-toolbox)

*Storage*
 - [Google Cloud Datastore](https://cloud.google.com/datastore/) for general data
 - [Google Cloud Storage](https://cloud.google.com/storage/) for images only

### Why are you using Javascript without a framework?
There is a good variety of JS frameworks out there and we love them, however we did not want to add extra overhead to developers that have not used the framework of our choice.

### What do you plan for the near future?
We started with a basic example that we want to improve over time, our plan is to release a series of posts explaining in detail the discrete progressive enhancements from this basic Website to a high performing PWA.

Beyond that, we want to track the evolution of all the PWAs submitted over time by running Lighthouse weekly, include newer metrics and features that will help developers test and build better PWAs.

###Why didn’t you just collaborate with other existing PWA directories?
We wanted to start from scratch with a Server Side rendered solution and progressively add PWA functionalities to learn more about the process and document all the steps.

### How do I request features or submit bugs?

Please submit them directly in our [GitHub issues section](https://github.com/GoogleChrome/gulliver/issues).


================================================
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 2015, Google Inc.

   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
================================================
```diff
! This project has been deprecated.
```

# Gulliver

[Gulliver](https://pwa-directory.appspot.com/) is a directory of [Progressive Web Apps](https://infrequently.org/2016/09/what-exactly-makes-something-a-progressive-web-app/).

## Contents

In Gulliver's landing page you can browse the set of currently registered PWAs as depicted in the following landing page snapshot:

![Screenshot](img/gulliver-landing-page.png)

If you click on a particular PWA, Gulliver takes you to a detail page showing the results of an evaluation done on that specific PWA using the  [Lighthouse PWA Analyzer](https://www.youtube.com/watch?v=KiV2p46rWjU) tool (Details page #1), and a view of the associated [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) file  for the application (Details Page #2):

Details Page #1            |  Details Page #2
:-------------------------:|:-------------------------:
![](img/gulliver-details-one.png)  |  ![](img/gulliver-details-two.png)

Gulliver itself has been implemented as a PWA; therefore it is designed to work well on any kind of device, including desktop web browsers (see landing page), and on mobile devices (see details page).

## FAQ

[Visit our FAQ Page](https://github.com/GoogleChrome/gulliver/blob/master/FAQ.md)

## Requirements

Gulliver was built using the [ExpressJS](https://expressjs.com/) web framework for Node.js, and uses the [Google Cloud Platform](https://cloud.google.com/) (GCP) for computing and storage services.

The following components are required to run the project (tested on macOS):

1. [NodeJS](https://nodejs.org/) (LTS version ~6.11.0). A JavaScript runtime built on Chrome's V8 JavaScript engine. (How to verify? Run `node --version`.) If you have a later version, install the LTS version with `nvm`.

1. [Google Cloud SDK](https://cloud.google.com/sdk/). A set of tools for the Google Cloud Platform (GCP) that you can use to access the Google Compute Engine and the Google Cloud Storage, which are two components of GCP used by Gulliver. (How to verify? Run `gcloud --version`.)

1. [Memcached](https://memcached.org/). A distributed memory object caching system. (How to verify? Run `memcached` (the command should appear to hang), and then `telnet localhost 11211` in a separate terminal. In the `telnet` window, typing `version` it should report the `memcached` version. If you don't have it, see [these instructions](https://cloud.google.com/appengine/docs/flexible/nodejs/using-redislabs-memcache#testing_memcached_locally) to install memcached.)

In addition, you will need to set up a GCP project, and configure OAuth:

1. Create a [Google Cloud Platform](https://console.cloud.google.com/) project. A GCP project forms the basis of accessing the GCP. Then, run `gcloud init` to configure `gcloud` locally, if you get the error "Could not load the default credentials" run `gcloud auth login`.

1. Get the OAuth *client id* and *client secret* associated with this project. (How to verify? There's no automatic way, but see [Creating a Google API Console project and client ID](https://developers.google.com/identity/sign-in/web/devconsole-project) for how to create one. Make sure you list `http://localhost:8080` as one of the `Authorized JavaScript origins`.)

Finally (and optionally), you need a Firebase project, and the Firebase Cloud Messaging "Server key" and "Sender ID":

1. Create a [Firebase](https://console.firebase.google.com/) project.

1. Get Firebase Cloud Messaging "Server key" and "Sender ID" associated with this project. Select "Project settings" and then "Cloud Messaging". The URL should be of the form <https://console.firebase.google.com/project/$FIREBASE_PROJECT/settings/cloudmessaging>. (How to verify? There's no automatic way, but the "Server key" should be a long string of >100 characters, and the "Sender ID" a >10 digit number.)

## Running Gulliver

1. Clone the GitHub repository: `git clone https://github.com/GoogleChrome/gulliver.git`

1. Switch into the project directory: `cd gulliver`

1. Create indexes for the [Google Cloud Datastore](https://cloud.google.com/datastore/docs/concepts/overview): `gcloud datastore create-indexes index.yaml`

1. (Optional) Deploy cron jobs for scheduled PWA updates: `gcloud app deploy cron.yaml`

1. Install **Memcached** and run it on `localhost:11211`. Check these [installation instructions](https://cloud.google.com/appengine/docs/flexible/nodejs/caching-application-data) for guidance.

1. Run **`npm install`** to install dependencies.

1. Configure your project either via a config file or environment variables (which override the corresponding keys in the config file). To create a config file, copy the [sample config](config/config.example.json) and adjust the values accordingly:

```
$ cp config/config.example.json config/config.json
$ vim config/config.json
```

1. Start Gulliver via `npm start`.

1. Gulliver should now be running at `http://localhost:8080`.

## Running Tests

To verify that everything is working properly you can run the project's tests:

1. `npm test` to run lint + tests + coverage report.
2. `npm run mocha` to run all the tests only.
3. `npm run coverage` to run tests + coverage report.

## Lighthouse PWA Analyzer

Gulliver reports an evaluation of the "progressiveness" of each registered PWA. This evaluation is done by Lighthouse, which is a tool that runs a set of checks validating the existence of the features, capabilities, and performance that should characterize a PWA. You can learn more about Lighthouse in the [GitHub repository](https://github.com/GoogleChrome/lighthouse), or in this [video](https://www.youtube.com/watch?v=KiV2p46rWjU).

## References

To find out more about what PWAs are and how to go about incorporating the principles of PWAs into the development of your applications, check the following references which provide introductory information and references:

+ [Progressive Web Apps](https://developers.google.com/web/#progressive-web-apps): Documentation entry point. Here you will find several resources to get started developing PWAs

+ [Progressive Web Apps: Escaping Tabs without Losing our Soul](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/):
Introductory article with historical perspective

+ [Getting Started with Progressive Web Apps](https://addyosmani.com/blog/getting-started-with-progressive-web-apps/): Sound introduction on the fundamental elements behind the development of PWAs

+ [The Building Blocks of PWAs](https://www.smashingmagazine.com/2016/09/the-building-blocks-of-progressive-web-apps/): Interesting overall view of PWAs.

## License

See [LICENSE](./LICENSE) for more.

## Disclaimer

This is not a Google product.


================================================
FILE: app.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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';

// http-parser-js addresses issues such as corrupt HTTP headers
// http://stackoverflow.com/questions/36628420/nodejs-request-hpe-invalid-header-token
process.binding('http_parser').HTTPParser = require('http-parser-js').HTTPParser;

const path = require('path');
const express = require('express');
const enforce = require('express-sslify');
const compression = require('compression');
const config = require('./config/config');
const asset = require('./lib/asset-hashing').asset;
const hbs = require('hbs');
const helpers = require('./views/helpers');
const app = express();
const bodyParser = require('body-parser');
const serveStatic = require('serve-static');
const minifyHTML = require('express-minify-html');
const libPwaIndex = require('./lib/pwa-index');

const CACHE_CONTROL_SHORT_EXPIRES = 60 * 10; // 10 minutes.
const CACHE_CONTROL_EXPIRES = 60 * 60 * 24; // 1 day.
const CACHE_CONTROL_NEVER_EXPIRE = 31536000;
const ENVIRONMENT_PRODUCTION = 'production';

if (app.get('env') === ENVIRONMENT_PRODUCTION) {
  app.use((req, res, next) => {
    if (req.path.startsWith('/tasks/')) {
      next();
    } else {
      enforce.HTTPS({trustProtoHeader: true})(req, res, next); // eslint-disable-line new-cap
    }
  });
}

app.use(compression());

app.disable('x-powered-by');
app.disable('etag');
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
app.set('trust proxy', true);
hbs.registerPartials(path.join(__dirname, '/views/includes/'));
helpers.registerHelpers(hbs);

// Make variables available to *all* templates
hbs.localsAsTemplateData(app);
app.locals.configstring = JSON.stringify({
  /* eslint-disable camelcase */
  client_id: config.get('CLIENT_ID'),
  ga_id: config.get('GOOGLE_ANALYTICS'),
  firebase_msg_sender_id: config.get('FIREBASE_MSG_SENDER_ID')
  /* eslint-enable camelcase */
});

app.use(bodyParser.urlencoded({extended: true}));

if (app.get('env') === ENVIRONMENT_PRODUCTION) {
  app.use(minifyHTML({
    override: true,
    exception_url: false, // eslint-disable-line camelcase
    htmlMinifier: {
      removeComments: true,
      collapseWhitespace: true,
      collapseBooleanAttributes: true,
      removeAttributeQuotes: true,
      removeEmptyAttributes: true,
      minifyJS: true
    }
  }));
}

// Static files
const staticFilesMiddleware = serveStatic(path.resolve('./public'));
app.use((req, res, next) => {
  const path = req.url;
  req.url = asset.decode(path);
  let mime = serveStatic.mime.lookup(req.url);
  if (mime.match('image*') || req.url.includes('manifest.json')) {
    res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);
  } else if (req.url === path) {
    res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_SHORT_EXPIRES);
  } else {
    // versioned assets don't expire
    res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_NEVER_EXPIRE);
  }
  staticFilesMiddleware(req, res, next);
});

// Make node_modules/{{module}} available at /{{module}}
['sw-toolbox', 'sw-offline-google-analytics'].forEach(module => {
  app.use(
   '/' + module,
   express.static('node_modules/' + module)
 );
});

// Middlewares
app.use(require('./middlewares'));

// Controllers
app.use(require('./controllers'));

// If no route has matched, return 404
app.use((req, res) => {
  res.status(404).render('404.hbs',
    {nonce1: req.nonce1, nonce2: req.nonce2, contentOnly: req.query.contentOnly || false});
});

// Basic error handler
app.use((err, req, res, _) => {
  console.error(err);
  if (err.status === 404) {
    res.status(404).render('404.hbs', {nonce1: req.nonce1, nonce2: req.nonce2});
  } else {
    // If our routes specified a specific response, then send that. Otherwise,
    // send a generic message so as not to leak anything.
    res.status(500).send(err || 'Something broke!');
  }
});

if (module === require.main) {
  // Start the server
  const server = app.listen(config.get('PORT'), () => {
    const port = server.address().port;
    console.log('App listening on port %s', port);
  });

  // Index all PWAs
  libPwaIndex.indexAllPwas();
}

module.exports = app;


================================================
FILE: app.yaml
================================================
#	Copyright 2015-2016, Google, Inc.
# 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.
#
runtime: nodejs
env: flexible

instance_class: B4_1G
manual_scaling:
  instances: 2

handlers:
 - url: /.*
   script: IGNORED
   secure: always

network:
  instance_tag: default-service


================================================
FILE: config/.gitignore
================================================
config.json


================================================
FILE: config/config.example.json
================================================
{
  "//": "See README.md for more information about what to put here",
  "GCLOUD_PROJECT": "run `gcloud config get-value project`",
  "CLOUD_BUCKET": "see https://console.cloud.google.com/storage/browser?project=$GCLOUD_PROJECT",
  "CLIENT_ID": "see https://console.cloud.google.com/apis/credentials?project=$GCLOUD_PROJECT",
  "CLIENT_SECRET": "see https://console.cloud.google.com/apis/credentials?project=$GCLOUD_PROJECT",
  "WEBPERFORMANCE_SERVER": "your Web Performance server URL (optional)",
  "WEBPERFORMANCE_SERVER_API_KEY": "your Key for the Web Performance Service",
  "GOOGLE_ANALYTICS": "your Google Analytics tracking code (optional)",
  "CANONICAL_ROOT": "your website root address. Can be http://localhost:8080 in development",
  "FIREBASE_AUTH": "the 'Server key' (optional); see https://console.firebase.google.com/project/$FIREBASE_PROJECT/settings/cloudmessaging",
  "FIREBASE_MSG_SENDER_ID": "the 'Sender ID' (optional); see https://console.firebase.google.com/project/$FIREBASE_PROJECT/settings/cloudmessaging"
}


================================================
FILE: config/config.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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';

// Hierarchical node.js configuration with command-line arguments, environment
// variables, and files.
const nconf = require('nconf');
const path = require('path');

nconf
  // 1. Command-line arguments
  .argv()
  // 2. Environment variables
  .env([
    'CLOUD_BUCKET',
    'GCLOUD_PROJECT',
    'PORT',
    'CLIENT_ID',
    'CLIENT_SECRET',
    'WEBPERFORMANCE_SERVER',
    'WEBPERFORMANCE_SERVER_API_KEY',
    'GOOGLE_ANALYTICS',
    'FIREBASE_AUTH',
    'CANONICAL_ROOT',
    'FIREBASE_MSG_SENDER_ID',
    'API_TOKENS'
  ])
  // 3. Config file
  .file({file: path.join(__dirname, 'config.json')})
  // 4. Defaults
  .defaults({
    PORT: 8080 // Port used by HTTP server
  });

// Check for required settings
checkConfig('GCLOUD_PROJECT');
checkConfig('CLOUD_BUCKET');
checkConfig('CLIENT_ID');
checkConfig('CLIENT_SECRET');

function checkConfig(setting) {
  // If setting undefined, throw error
  if (!nconf.get(setting)) {
    throw new Error(`You must set the ${setting} environment variable or add it to ` +
      'config/config.json!');
  }
  // If setting includes a space, throw error
  if (nconf.get(setting).match(/\s/)) {
    throw new Error(`The ${setting} environment variable is suspicious ("${nconf.get(setting)}")`);
  }
}

module.exports = nconf;


================================================
FILE: controllers/api/favorite-pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap

const verifyIdToken = require('../../lib/verify-id-token');
const libFavoritePwa = require('../../lib/favorite-pwa');
const FavoritePwa = require('../../models/favorite-pwa');
const User = require('../../models/user');

/**
 * GET /favorite-pwa/
 *
 * Returns all Favorite PWAs for a user
 */
router.get('/', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  const idToken = req.get('Authorization');
  if (!idToken) {
    res.status(401);
    res.json('401 Unauthorized');
    return;
  }

  return verifyIdToken.verifyIdToken(idToken)
    .then(googleLogin => {
      const user = new User(googleLogin);
      return libFavoritePwa.findByUserId(user.id);
    })
    .then(favoritePwas => {
      if (favoritePwas) {
        res.json(favoritePwas);
      } else {
        res.status(404);
        res.json('not found');
      }
    })
    .catch(err => {
      console.error(err);
      res.status(500);
      res.json('Server error while loading Favorite PWAs');
    });
});

/**
 * GET /favorite-pwa/:pwaId
 *
 * Returns a Favorite PWA for a pwaId and user
 */
router.get('/:pwaId', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  const pwaId = req.params.pwaId;
  const idToken = req.get('Authorization');
  if (!idToken) {
    res.status(401);
    res.json('401 Unauthorized');
    return;
  }

  return verifyIdToken.verifyIdToken(idToken)
    .then(googleLogin => {
      const user = new User(googleLogin);
      return libFavoritePwa.findFavoritePwa(pwaId, user.id);
    })
    .then(favoritePwas => {
      if (favoritePwas) {
        res.json(favoritePwas);
      } else {
        res.status(404);
        res.json('not found');
      }
    })
    .catch(err => {
      console.error(err);
      res.status(500);
      res.json('Server error while loading Favorite PWAs');
    });
});

/**
 * POST /favorite-pwa/
 *
 * Create a Favorite PWA.
 */
router.post('/', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  const idToken = req.body.idToken;
  const pwaId = req.body.pwaId;

  return verifyIdToken.verifyIdToken(idToken)
    .then(googleLogin => {
      const user = new User(googleLogin);
      return libFavoritePwa.save(new FavoritePwa(pwaId, user.id));
    })
    .then(favoritePwa => {
      if (favoritePwa) {
        res.json(favoritePwa);
      } else {
        res.status(404);
        res.json('not found');
      }
    })
    .catch(err => {
      console.error(err);
      res.status(500);
      res.json('Error creating Favorite PWA');
    });
});

/**
 * DELETE /favorite-pwa/
 *
 * Delete a Favorite PWA.
 */
router.delete('/', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  const idToken = req.body.idToken;
  const pwaId = req.body.pwaId;

  return verifyIdToken.verifyIdToken(idToken)
    .then(googleLogin => {
      const user = new User(googleLogin);
      return libFavoritePwa.findFavoritePwa(pwaId, user.id);
    })
    .then(favoritePwa => {
      if (favoritePwa) {
        libFavoritePwa.delete(favoritePwa.id)
          .then(_ => {
            res.status(200);
            res.json(favoritePwa);
          })
          .catch(err => {
            console.error(err);
            res.status(500);
            res.json('Error deleting favorite PWA');
          });
      } else {
        res.status(404);
        res.json('not found');
      }
    })
    .catch(err => {
      console.error(err);
      res.status(500);
      res.json('Error deleting favorite PWA');
    });
});

module.exports = router;


================================================
FILE: controllers/api/index.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap

// Includes APIs for Lighthouse (/api/lighthouse)
router.use('/lighthouse', require('./lighthouse'));

// Includes APIs for Notifications (/api/notifications)
router.use('/notifications', require('./notifications'));

// Includes APIs for FavoritePwas (/api/favoritepwa)
router.use('/favorite-pwa', require('./favorite-pwa'));

// Includes APIs for PWAs (/api/pwa)
router.use('/pwa', require('./pwa'));

module.exports = router;


================================================
FILE: controllers/api/lighthouse.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const lighthouseLib = require('../../lib/lighthouse');
const router = express.Router(); // eslint-disable-line new-cap
const CACHE_CONTROL_EXPIRES = 60 * 60 * 24; // 1 day.

/**
 * GET /api/lighthouse-graph/:pwaId
 *
 * Returns the Lighthouse Graph information for a PWA
 * it uses the Google Charts JSON format:
 *  https://developers.google.com/chart/interactive/docs/reference#dataparam
 */
router.get('/graph/:pwaId', (req, res) => {
  res.setHeader('Content-Type', 'application/json');

  lighthouseLib.getLighthouseGraphByPwaId(req.params.pwaId)
    .then(lighthouseGraph => {
      if (lighthouseGraph) {
        res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);
        res.json(lighthouseGraph);
      } else {
        res.status(404);
        res.json('not found');
      }
    })
    .catch(err => {
      res.status(500);
      res.json(err);
    });
});

module.exports = router;


================================================
FILE: controllers/api/notifications.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const bodyParser = require('body-parser');
const router = express.Router(); // eslint-disable-line new-cap
const notificationsLib = require('../../lib/notifications');
const jsonParser = bodyParser.json();

router.get('/topics/', (req, res) => {
  const token = req.query.token;
  notificationsLib.list(token)
    .then(subscriptions => {
      res.json({
        subscriptions: subscriptions
      });
    })
    .catch(err => {
      res.status(500);
      res.json(err);
    });
});

router.post('/subscribe/:topic/', jsonParser, (req, res) => {
  const token = req.body.token;
  const topic = req.params.topic;
  notificationsLib.subscribe(token, topic)
    .then(_ => {
      res.json({success: true});
    })
    .catch(err => {
      res.status(500);
      res.json(err);
    });
});

router.post('/unsubscribe/:topic/', jsonParser, (req, res) => {
  const token = req.body.token.trim();
  const topic = req.params.topic;
  notificationsLib.unsubscribe(token, topic)
    .then(_ => {
      res.json({success: true});
    })
    .catch(err => {
      res.status(500);
      res.json(err);
    });
});

module.exports = router;



================================================
FILE: controllers/api/pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
require('express-csv');
const pwaLib = require('../../lib/pwa');
const libMetadata = require('../../lib/metadata');
const router = express.Router(); // eslint-disable-line new-cap
const verifyIdToken = require('../../lib/verify-id-token');
const bodyParser = require('body-parser');
const Pwa = require('../../models/pwa');
const color = require('../../lib/color');
const CACHE_CONTROL_EXPIRES = 60 * 60 * 1; // 1 hour
const RSS = require('rss');
const {URL} = require('url');

function getDate(date) {
  return new Date(date).toISOString().split('T')[0];
}

class CsvWriter {
  write(result, pwas) {
    const csv = [];
    pwas.forEach(pwa => {
      const created = getDate(pwa.created);
      const updated = getDate(pwa.updated);
      const csvLine = [];
      csvLine.push(pwa.id);
      csvLine.push(pwa.absoluteStartUrl);
      csvLine.push(pwa.manifestUrl);
      csvLine.push(pwa.lighthouseScore);
      csvLine.push(created);
      csvLine.push(updated);
      csv.push(csvLine);
    });
    result.setHeader('Content-Type', 'text/csv');
    csv.unshift(
      ['id', 'absoluteStartUrl', 'manifestUrl', 'lighthouseScore', 'created', 'updated']);
    result.csv(csv);
  }
}

class JsonWriter {
  write(result, pwas) {
    const pwaList = [];
    pwas.forEach(dbPwa => {
      const created = getDate(dbPwa.created);
      const updated = getDate(dbPwa.updated);
      const pwa = {};
      pwa.id = dbPwa.id;
      pwa.absoluteStartUrl = dbPwa.absoluteStartUrl;
      pwa.manifestUrl = dbPwa.manifestUrl;
      pwa.lighthouseScore = dbPwa.lighthouseScore;
      pwa.webPageTest = dbPwa.webPageTest;
      pwa.pageSpeed = dbPwa.pageSpeed;
      pwa.created = created;
      pwa.updated = updated;
      pwaList.push(pwa);
    });
    result.setHeader('Content-Type', 'application/json');
    result.json(pwaList);
  }
}

function render(res, view, options) {
  return new Promise((resolve, reject) => {
    res.render(view, options, (err, html) => {
      if (err) {
        console.log(err);
        reject(err);
      }
      resolve(html);
    });
  });
}

function renderOnePwaRss(pwa, req, res) {
  const url = req.originalUrl;
  const contentOnly = false || req.query.contentOnly;
  let arg = Object.assign(libMetadata.fromRequest(req, url), {
    pwa: pwa,
    title: 'PWA Directory: ' + pwa.name,
    description: 'PWA Directory: ' + pwa.name + ' - ' + pwa.description,
    backlink: true,
    contentOnly: contentOnly
  });
  return render(res, 'pwas/view-rss.hbs', arg);
}

async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
}

class RssWriter {
  write(req, res, pwas) {
    const feed = new RSS({
      /* eslint-disable camelcase */
      title: 'PWA Directory',
      description: 'A Directory of Progressive Web Apps',
      feed_url: 'https://pwa-directory.appspot.com/api/pwa/?format=rss',
      site_url: 'https://pwa-directory.appspot.com/',
      image_url: 'https://pwa-directory.appspot.com/favicons/android-chrome-144x144.png',
      pubDate: new Date(),
      custom_namespaces: {
        rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
        l: 'http://purl.org/rss/1.0/modules/link/',
        media: 'http://search.yahoo.com/mrss/',
        content: 'http://purl.org/rss/1.0/modules/content/'
      }
    });

    const start = async _ => {
      await asyncForEach(pwas, async pwa => {
        let html = await renderOnePwaRss(pwa, req, res);

        const customElements = [];
        customElements.push({'content:encoded': html});
        customElements.push({'l:link': {_attr: {'l:rel': 'http://purl.org/rss/1.0/modules/link/#alternate',
          'l:type': 'application/json',
          'rdf:resource': 'https://pwa-directory.appspot.com/api/pwa/' + pwa.id}}});
        if (pwa.iconUrl128) {
          customElements.push({'media:thumbnail': {_attr: {url: pwa.iconUrl128,
            height: '128', width: '128'}}});
        }

        feed.item({
          title: pwa.displayName,
          url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id,
          description: html,
          guid: pwa.id,
          date: pwa.created,
          custom_elements: customElements
        });
      });
      res.setHeader('Content-Type', 'application/rss+xml');
      res.status(200).send(feed.xml());
    };
    start();
    /* eslint-enable camelcase */
  }
}

const csvWriter = new CsvWriter();
const jsonWriter = new JsonWriter();
const rssWriter = new RssWriter();

/**
 * GET /api/pwa
 *
  * Returns all PWAs as JSON, ?format=csv for CSV or ?format=rss for RSS feed
 */
router.get('/:id*?', (req, res) => {
  let format = req.query.format || 'json';
  let sort = req.query.sort || 'newest';
  let skip = parseInt(req.query.skip, 10);
  let limit = parseInt(req.query.limit, 10) || 100;
  res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);

  let queryPromise = req.params.id ? pwaLib.find(req.params.id) : pwaLib.list(skip, limit, sort);
  queryPromise
  .then(result => {
    result = result.pwas ? result : {pwas: [result]};
    switch (format) {
      case 'csv': {
        csvWriter.write(res, result.pwas);
        break;
      }
      case 'rss': {
        rssWriter.write(req, res, result.pwas);
        break;
      }
      default: {
        jsonWriter.write(res, result.pwas);
      }
    }
  })
  .catch(err => {
    console.log(err);
    let code = err.code || 500;
    res.status(code);
    res.json(err);
  });
});

router.post('/add', bodyParser.json(), (req, res) => {
  const idToken = req.body.idToken;

  if (!idToken) {
    res.status(400).send({error: 'user not logged in'});
    return;
  }

  const manifestUrl = req.body.manifestUrl;
  if (!manifestUrl) {
    res.status(400).send({error: 'no manifest provided'});
    return;
  }

  try {
    const url = new URL(manifestUrl);
    (async () => {
      try {
        const pwa = new Pwa(url.toString());
        const user = await verifyIdToken.verifyIdToken(idToken);
        pwa.setUser(user);
        const savedPwa = await pwaLib.createOrUpdatePwa(pwa);
        res.json({
          id: savedPwa.id,
          name: savedPwa.name,
          backgroundColor: savedPwa.backgroundColor,
          foregroundColor: color.bestContrastRatio('#FFFFFF', '#000000', savedPwa.backgroundColor)
        });
      } catch (e) {
        const message = e.message || e;
        res.status(400).json({error: message});
      }
    })();
  } catch (e) {
    res.status(400).send({error: 'manifestUrl is not an URL'});
  }
});

module.exports = router;


================================================
FILE: controllers/app.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap

router.get('/shell', (req, res) => {
  res.render('app/shell.hbs');
});

router.get('/offline', (req, res, next) => { // eslint-disable-line no-unused-vars
  res.render('app/offline.hbs');
});

module.exports = router;


================================================
FILE: controllers/cache.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap
const libCache = require('../lib/data-cache');

const CACHE_LIFETIME = 60 * 60; // 1 hour

/**
 * GET *
 *
 * Serves cached HTML or
 * overrides res.send to be able to cache rendered HTML before sending.
 */
router.get('*', (req, res, next) => {
  const url = req.originalUrl;
  libCache.get(url)
    .then(cachedHtml => {
      console.log('From cache: ' + url);
      res.send(cachedHtml);
    })
    .catch(_ => {
      // Overrides res.send to be able to cache before sending.
      res.sendResponse = res.send;
      res.send = body => {
        libCache.set(url, body, CACHE_LIFETIME)
          .then(_ => {
            libCache.storeCachedUrls(url);
            console.log('Stored in cache: ' + url);
          })
          .catch(_ => {
            console.log('Error setting cache for: ' + url);
          });
        res.sendResponse(body);
      };
      next();
    });
});

module.exports = router;


================================================
FILE: controllers/index.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap
const config = require('../config/config');
const bodyParser = require('body-parser');

router.use(bodyParser.json());

// API
router.use('/api', require('./api'));

// Tasks
router.use('/tasks', require('./tasks'));

// PWAs
router.use('/pwas', require('./pwa'));

router.get('/', (req, res) => {
  req.url = '/pwas';
  router.handle(req, res);
});

router.get('/installable', (req, res) => {
  req.url = '/pwas/installable';
  router.handle(req, res);
});

// ServiceWorker
router.use('/js', require('./sw'));

// /.shell hosts app shell dependencies
router.use('/.app', require('./app'));

/**
 * This route is used to send config.json to firebase-messaging-sw.js
 */
router.get('/messaging-config.json', (req, res) => {
  // eslint-disable-next-line camelcase
  res.json({firebase_msg_sender_id: config.get('FIREBASE_MSG_SENDER_ID')});
});

module.exports = router;


================================================
FILE: controllers/pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const dataFetcher = require('../lib/data-fetcher');
const pwaLib = require('../lib/pwa');
const libPwaIndex = require('../lib/pwa-index');
const verifyIdToken = require('../lib/verify-id-token');
const lighthouseLib = require('../lib/lighthouse');
const Pwa = require('../models/pwa');
const router = express.Router(); // eslint-disable-line new-cap
const libMetadata = require('../lib/metadata');

const LIST_PAGE_SIZE = 32;
const DEFAULT_PAGE_NUMBER = 1;
const DEFAULT_SORT_ORDER = 'newest';
const DEFAULT_TAB = 'installable';
const DEFAULT_FILTER = {
  installable: true
};

/**
 * Setup the list template view state
 */
function setupListViewState(req) {
  const viewState = {};
  if (typeof req.query.query === 'undefined') {
    viewState.mainPage = true;
    viewState.backlink = false;
    viewState.search = false;
  } else {
    viewState.mainPage = true;
    viewState.backlink = true;
    viewState.search = true;
    viewState.searchQuery = req.query.query;
  }
  viewState.contentOnly = false || req.query.contentOnly;
  viewState.pageNumber = parseInt(req.query.page, 10) || DEFAULT_PAGE_NUMBER;
  viewState.sortOrder = req.query.sort || DEFAULT_SORT_ORDER;
  viewState.start = parseInt(req.query.start, 10) || (viewState.pageNumber - 1) * LIST_PAGE_SIZE;
  viewState.limit = parseInt(req.query.limit, 10) || LIST_PAGE_SIZE;
  viewState.end = viewState.pageNumber * LIST_PAGE_SIZE;
  return viewState;
}

/**
 * Setup the list template view arguments
 */
function setupListViewArguments(req, viewState, result) {
  return Object.assign(libMetadata.fromRequest(req), {
    title: 'PWA Directory',
    description: 'PWA Directory: A Directory of Progressive Web Apps',
    pwas: result.pwas,
    hasNextPage: result.hasMore,
    hasPreviousPage: viewState.pageNumber > 1,
    nextPageNumber: viewState.pageNumber + 1,
    previousPageNumber: (viewState.pageNumber === 2) ? false : viewState.pageNumber - 1,
    currentPageNumber: viewState.pageNumber,
    sortOrder: (viewState.sortOrder === DEFAULT_SORT_ORDER) ? false : viewState.sortOrder,
    currentTab: req.path.substring(1, req.path.length) || DEFAULT_TAB,
    startPwa: viewState.start + 1,
    mainPage: viewState.mainPage,
    search: viewState.search,
    backlink: viewState.backlink,
    searchQuery: viewState.searchQuery,
    contentOnly: viewState.contentOnly
  });
}

function listPwas(req, res, next, sortOrder, filters) {
  const viewState = setupListViewState(req);
  pwaLib.list(viewState.start, viewState.limit, sortOrder, filters)
    .then(result =>
      render(res, 'pwas/list.hbs', setupListViewArguments(req, viewState, result)))
    .then(html => res.send(html))
    .catch(err => {
      err.status = 500;
      next(err);
    });
}

/**
 * GET /
 *
 * Display a page of PWAs (up to LIST_PAGE_SIZE at a time)
 */
router.get('/', (req, res, next) => {
  listPwas(req, res, next, DEFAULT_SORT_ORDER, DEFAULT_FILTER);
});

/**
 * GET /newest
 *
 * Display a page of PWAs sorted by score.
 */
router.get('/newest', (req, res, next) => {
  listPwas(req, res, next, 'newest');
});

/**
 * GET /score
 *
 * Display a page of PWAs sorted by score.
 */
router.get('/score', (req, res, next) => {
  listPwas(req, res, next, 'score');
});

/**
 * GET /installable
 *
 * Display a page of installable PWAs.
 */
router.get('/installable', (req, res, next) => {
  const filters = {
    installable: true
  };
  listPwas(req, res, next, 'newest', filters);
});

/**
 * GET /pwas/search
 *
 * Display a search result page of PWAs
 */
router.get('/search', (req, res, next) => {
  const viewState = setupListViewState(req);
  libPwaIndex.searchPwas(viewState.searchQuery).then(result =>
    render(res, 'pwas/list.hbs', setupListViewArguments(req, viewState, result)))
  .then(html => res.send(html))
  .catch(err => {
    err.status = 500;
    next(err);
  });
});

/**
 * GET /pwas/add
 *
 * Display a form for creating a PWA.
 */
router.get('/add', async (req, res, next) => {
  try {
    const contentOnly = req.query.contentOnly || false;

    const url = req.query.url || '';
    let manifestUrl = req.query.manifestUrl || '';
    if (url !== '' && manifestUrl !== '') {
      const err = new Error('only one of url or manifestUrl may be set');
      err.status = 400;
      throw err;
    }
    if (url !== '') {
      manifestUrl = await dataFetcher.fetchLinkRelManifestUrl(url);
    }

    let arg = Object.assign(libMetadata.fromRequest(req), {
      title: 'PWA Directory - Submit a PWA',
      description: 'PWA Directory: Submit a Progressive Web Apps',
      pwa: {},
      action: 'Add',
      backlink: true,
      submit: true,
      contentOnly,
      manifestUrl
    });
    res.render('pwas/form.hbs', arg);
  } catch (err) {
    next(err);
  }
});

/**
 * POST /pwas/add
 *
 * Create a PWA.
 */
router.post('/add', (req, res, next) => {
  let manifestUrl = req.body.manifestUrl.trim();
  if (manifestUrl.startsWith('http://')) {
    manifestUrl = manifestUrl.replace('http://', 'https://');
  }
  const idToken = req.body.idToken;
  let pwa = new Pwa(manifestUrl);

  if (!manifestUrl || !idToken) {
    let arg = Object.assign(libMetadata.fromRequest(req), {
      pwa,
      backlink: true,
      error: (manifestUrl) ? 'user not logged in' : 'no manifest provided'
    });
    res.render('pwas/form.hbs', arg);
    return;
  }

  verifyIdToken.verifyIdToken(idToken)
    .then(user => {
      pwa.setUser(user);
      return pwaLib.createOrUpdatePwa(pwa);
    })
    .then(savedData => {
      res.redirect(req.baseUrl + '/' + savedData.id);
      return;
    })
    .catch(err => {
      if (typeof err === 'number') {
        switch (err) {
          case pwaLib.E_MANIFEST_INVALID_URL:
            err = `pwa.manifestUrl [${pwa.manifestUrl}] is not a valid URL`;
            break;
          case pwaLib.E_MISING_USER_INFORMATION:
            err = 'Missing user information';
            break;
          case pwaLib.E_MANIFEST_URL_MISSING:
            err = 'Missing manifestUrl';
            break;
          case pwaLib.E_NOT_A_PWA:
            err = 'pwa is not an instance of Pwa';
            break;
          default:
            return next(err);
        }
      }
      // Transform err from an array of strings (in a particular format) to a
      // comma-separated string.
      if (Array.isArray(err)) {
        const s = err.map(e => {
          const m = e.match(/^ERROR:\s+(.*)\.$/);
          return m ? m[1] : e; // if no match (format changed?), just return the string
        }).join(', ');
        err = s;
      }
      let arg = Object.assign(libMetadata.fromRequest(req), {
        pwa,
        backlink: true,
        error: err
      });
      res.render('pwas/form.hbs', arg);
      return;
    });
});

/**
 * GET /pwas/:id
 *
 * Display a PWA or redirects to the encodedStartUrl of the PWA.
 */
router.get('/:pwa', (req, res, next) => {
  renderOnePwa(req, res)
    .then(html => {
      res.send(html);
    })
    .catch(err => {
      err.status = 404;
      return next(err);
    });
  return;
});

/**
 * Generate the HTML with 'pwas/view.hbs' for one PWA
 */
function renderOnePwa(req, res) {
  const url = req.originalUrl;
  const pwaId = encodeURIComponent(req.params.pwa);  // we have foo/ here, need foo%2F
  const contentOnly = false || req.query.contentOnly;
  return pwaLib.find(pwaId)
    .then(pwa => {
      return lighthouseLib.findByPwaId(pwaId)
        .then(lighthouse => {
          if (lighthouse && lighthouse.lighthouseInfo &&
              Object.prototype.toString.call(lighthouse.lighthouseInfo) === '[object String]') {
            lighthouse.lighthouseInfo = JSON.parse(lighthouse.lighthouseInfo);
          }
          let arg = Object.assign(libMetadata.fromRequest(req, url), {
            pwa: pwa,
            lighthouse: lighthouse,
            rawManifestJson: JSON.parse(pwa.manifest.raw),
            title: 'PWA Directory: ' + pwa.name,
            description: 'PWA Directory: ' + pwa.name + ' - ' + pwa.description,
            backlink: true,
            contentOnly: contentOnly
          });
          return render(res, 'pwas/view.hbs', arg);
        });
    });
}

/**
 * res.render as a Promise
 */
function render(res, view, options) {
  return new Promise((resolve, reject) => {
    res.render(view, options, (err, html) => {
      if (err) {
        console.log(err);
        reject(err);
      }
      resolve(html);
    });
  });
}

/**
 * Errors on "/pwas/*" routes.
 */
router.use((err, req, res, next) => {
  // Format error and forward to generic error handler for logging and
  // responding to the request
  err.response = err.message;
  console.error(err);
  next(err);
});

module.exports = router;


================================================
FILE: controllers/sw.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const router = express.Router(); // eslint-disable-line new-cap
const asset = require('../lib/asset-hashing').asset;

const ASSETS = JSON.stringify([
  '/css/style.css',
  '/js/gulliver.js'
].map(assetPath => asset.encode(assetPath)));

const ASSETS_JS = `const ASSETS = ${ASSETS};`;

router.get('/sw-assets-precache.js', (req, res) => {
  res.setHeader('Content-Type', 'application/javascript');
  res.setHeader('Cache-Control', 'no-cache, max-age=0');
  res.send(ASSETS_JS);
});

module.exports = router;


================================================
FILE: controllers/tasks.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const pwaLib = require('../lib/pwa');
const tasksLib = require('../lib/tasks');
const Task = require('../models/task');
const router = express.Router(); // eslint-disable-line new-cap

const APP_ENGINE_CRON = 'X-Appengine-Cron';

/**
 * Checks for the presence of the 'X-Appengine-Cron' header on the request.
 * Only requests from the App Engine cron are allowed.
 */
function checkAppEngineCron(req, res, next) {
  if (!req.get(APP_ENGINE_CRON)) {
    return res.sendStatus(403);
  }
  return next();
}

/**
 * Creates a pwaLib.createOrUpdatePwa task for the given pwaId
 */
function createOrUpdatePwaTasks(pwaList) {
  const modulePath = require.resolve('../lib/pwa');
  pwaList.forEach(pwa => {
    tasksLib.push(new Task(pwa.id, modulePath, 'createOrUpdatePwa', 0));
  });
}

/**
 * GET /tasks/cron
 *
 * We use a GET from the cron job to launch a PWA update process
 * for all PWAs.
 *
 * Uses checkAppEngineCron to allow only request from cron job.
 */
router.get('/cron', checkAppEngineCron, (req, res, next) => {
  pwaLib.list(undefined, undefined, 'newest')
    .then(result => {
      // Create one update task for each PWA
      createOrUpdatePwaTasks(result.pwas);
      res.sendStatus(200);
    })
    .catch(err => {
      next(err);
    });
});

/**
 * GET /tasks/updateunscored
 *
 * We use a GET from the cron job to launch a PWA update process
 * for all PWAs.
 *
 * Uses checkAppEngineCron to allow only request from cron job.
 */
router.get('/updateunscored', checkAppEngineCron, (req, res, next) => {
  return pwaLib.list(undefined, undefined, 'newest')
    .then(result => {
      // Create one update task for each unscored PWA
      createOrUpdatePwaTasks(result.pwas.filter(pwa => !pwa.lighthouseScore));
      res.sendStatus(200);
    })
    .catch(err => {
      next(err);
    });
});

/**
 * GET /tasks/execute?tasks=1
 *
 * We use a GET from the cron job to execute each PWA update task
 * The tasks parameter is the number of tasks to execute per run
 *
 * Uses checkAppEngineCron to allow only request from cron job.
 */
router.get('/execute', checkAppEngineCron, (req, res) => {
  const tasksToExecute = req.query.tasks ? req.query.tasks : 1;
  // const tasksList = [];

  (async () => {
    const tasks = await tasksLib.getTasks(tasksToExecute);
    console.log(`Executing ${tasks.length} tasks`);

    for (let task of tasks) {
      try {
        console.log(`Will Execute Task: ${task.id}`);
        // Delete before executing, so we ensure that if the task breaks
        // something it is removed from the queue anyway.
        try {
          await tasksLib.deleteTask(task.id);
        } catch (err) {
          console.error(`Error deleting task: ${task.id}`);
        }
        await tasksLib.executePwaTask(task);
        console.log(`Executed Task: ${task.id}`);
      } catch (err) {
        console.error(`Failed to execute task: ${task.id}`);
      }
    }
    res.sendStatus(200);
  })();
});

/**
 * Errors on "/task/*" routes.
 */
router.use((err, req, res, next) => {
  // Format error and forward to generic error handler for logging and
  // responding to the request
  err.response = {
    message: err.message,
    internalCode: err.code
  };
  next(err);
});

module.exports = router;


================================================
FILE: cron.yaml
================================================
cron:
- description: (Node) Daily PWA info update job
  url: /tasks/cron
  schedule: every day 13:00

- description: (Node) Execute PWA update tasks
  url: /tasks/execute?tasks=30
  schedule: every 1 minutes

- description: (Node) Update unscored PWAs
  url: /tasks/updateunscored
  schedule: every 1 hours

- description: Update unscored PWAs
  url: /taskcreator/task?unscored=true
  schedule: every 1 hours
  target: web-performance

- description: UpdateManifestTask
  url: /taskcreator/task/UpdateManifestTask
  schedule: every day 16:00
  target: web-performance

- description: UpdateIconTask
  url: /taskcreator/task/UpdateIconTask
  schedule: every monday 01:00
  target: web-performance

- description: PageSpeedReportTask
  url: /taskcreator/task/PageSpeedReportTask
  schedule: every friday 01:00
  target: web-performance

- description: WebPageTestReportTask
  url: /taskcreator/task/WebPageTestReportTask
  schedule: every sunday 01:00
  target: web-performance


================================================
FILE: firebase-messaging-sw-generator.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 template = require('lodash.template');
const config = require('./config/config');

const firebaseMsgSenderId = config.get('FIREBASE_MSG_SENDER_ID');

fs.readFile('./firebase-messaging-sw.tmpl', 'utf8', (error, data) => {
  if (error) {
    console.error('Error reading template: ', error);
    return;
  }

  const firebaseMessagingSwFileContent = template(data)({
    firebaseMsgSenderId: firebaseMsgSenderId
  });

  fs.writeFile('./public/firebase-messaging-sw.js', firebaseMessagingSwFileContent, err => {
    if (err) {
      console.log('Error Writing firebase-messaging-sw: ', err);
    }
  });
});


================================================
FILE: firebase-messaging-sw.tmpl
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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.
 */

/* eslint-env serviceworker, browser */
/* global firebase */
importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-messaging.js');

firebase.initializeApp({
  messagingSenderId: '<%= firebaseMsgSenderId %>'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(_ => {
  return self.registration.showNotification();
});


================================================
FILE: index.yaml
================================================
indexes:
- kind: Lighthouse
  properties:
  - name: pwaId
    direction: asc
  - name: date
    direction: desc
- kind: PWA
  properties:
  - name: installable
  - name: lighthouseScore
    direction: desc
- kind: PWA
  properties:
  - name: installable
  - name: created
    direction: desc


================================================
FILE: lib/asset-hashing.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 revHash = require('rev-hash');

const CHECKSUM_LENGTH = 10;
const CHECKSUM_PATTERN = /^[0-9a-z]{10}$/;

class ChecksumProvider {

  constructor(root) {
    this.root_ = root;
  }

  get(assetPath) {
    const buffer = fs.readFileSync(path.join(this.root_, assetPath));
    return revHash(buffer);
  }

}

class AssetChecksum {

  constructor(checksumProvider) {
    this.checksumProvider_ = checksumProvider;
    this.checksumCache_ = {};
  }

  encode(assetPath) {
    if (!assetPath) {
      return assetPath;
    }
    let result = this.checksumCache_[assetPath];
    if (result) {
      return result;
    }
    const checksum = this.checksumProvider_.get(assetPath);
    const index = assetPath.lastIndexOf('.');
    if (index === -1) {
      return assetPath;
    }
    result = assetPath.substring(0, index) +
      '.' +
      checksum +
      assetPath.substring(index, assetPath.length);
    this.checksumCache_[assetPath] = result;
    return result;
  }

  decode(assetPath) {
    if (!assetPath) {
      return assetPath;
    }
    const fragments = assetPath.split('.');
    if (fragments.length <= 1) {
      return assetPath;
    }
    const checksumIndex = fragments.length - 2;
    if (!fragments[checksumIndex].match(CHECKSUM_PATTERN)) {
      return assetPath;
    }
    fragments.splice(checksumIndex, 1);
    return fragments.join('.');
  }

}

module.exports.asset = new AssetChecksum(new ChecksumProvider('public'));

// Exported for testing
module.exports.ChecksumProvider = ChecksumProvider;
module.exports.AssetChecksum = AssetChecksum;
module.exports.CHECKSUM_LENGTH = CHECKSUM_LENGTH;


================================================
FILE: lib/color.js
================================================
/**
 * Copyright 2015-2018, Google, Inc.
 * 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 parseColor = require('parse-color');

function bestContrastRatio(color1, color2, background) {
  return contrastRatio(color1, background) > contrastRatio(color2, background) ? color1 : color2;
}

/**
 * Calculates the contrast ratio, as described on https://www.w3.org/TR/WCAG20/#contrast-ratiodef
 *
 * @param {string} foreground the foreground color.
 * @param {string} background the background color, Defaults to #FFFFFF.
 * @returns {Number} the contrast ration.
 */
function contrastRatio(foreground, background = '#FFFFFF') {
  if (background.trim() === 'transparent') background = 'white';

  const bgLuminance = relativeLuminance(background);
  const fgLuminance = relativeLuminance(foreground);

  let darker;
  let lighter;
  if (fgLuminance > bgLuminance) {
    lighter = fgLuminance;
    darker = bgLuminance;
  } else {
    lighter = bgLuminance;
    darker = fgLuminance;
  }

  return (lighter + 0.05) / (darker + 0.05);
}

/**
 * Calculates the relative luminance, as described on https://www.w3.org/TR/WCAG20/#relativeluminancedef
 *
 * @param {string} color the foreground color.
 * @returns {Number} the relative luminance.
 */
function relativeLuminance(color) {
  let colorRed;
  let colorGreen;
  let colorBlue;

  try {
    [colorRed, colorGreen, colorBlue] = parseColor(color.trim()).rgb;
  } catch (error) {
    throw new Error('Error parsing Color with parseColor' + error);
  }
  let red = componentRelativeLuminance_(colorRed);
  let green = componentRelativeLuminance_(colorGreen);
  let blue = componentRelativeLuminance_(colorBlue);

  return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}

/**
 * Generates the luminance of a single color component.
 * @param {Number} component the value to have the luminance calculated
 * @returns {Number} the calculated luminance of the color component.
 */
function componentRelativeLuminance_(component) {
  let c = component / 255;
  return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}

module.exports = {
  contrastRatio: contrastRatio,
  relativeLuminance: relativeLuminance,
  bestContrastRatio: bestContrastRatio
};


================================================
FILE: lib/data-cache.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');
const Memcached = require('memcached');
const memcachedAddr = process.env.MEMCACHE_PORT_11211_TCP_ADDR ||
  config.get('MEMCACHED_SERVER') || 'localhost';
const memcachedPort = process.env.MEMCACHE_PORT_11211_TCP_PORT || '11211';
const memcached = new Memcached(memcachedAddr + ':' + memcachedPort, {timeout: 600, retries: 1});

const PAGELIST_URLS = 'PAGELIST_URLS';
const CACHE_LIFETIME = 60 * 60 * 6; // 6 hours

/**
 * Gets a value from memcached using.
 *
 * @param {object} a key.
 * @returns a Promise
 */
function get(key) {
  return new Promise((resolve, reject) => {
    memcached.get(key, (err, value) => {
      if (err) {
        return reject(err);
      }

      if (!value) {
        return reject('Not Found. Key: ' + key);
      }

      return resolve(value);
    });
  });
}

/**
 * Deletes a value from memcached.
 *
 * @param {object} a key.
 * @returns a Promise
 */
function del(key) {
  return new Promise((resolve, reject) => {
    memcached.del(key, (err, value) => {
      if (err) {
        return reject(err);
      }

      if (!value) {
        return reject('Not Found. Key: ' + key);
      }

      return resolve(value);
    });
  });
}

/**
 * Sets a value in Memcached.
 *
 * @param {object} the key.
 * @param {object} the value.
 * @param {Number} a lifetime.
 * @returns a Promise
 */
function set(key, value, lifetime) {
  return new Promise((resolve, reject) => {
    memcached.set(key, value, lifetime, err => {
      if (err) {
        return reject(err);
      }
      return resolve();
    });
  });
}

/**
 * Replaces a value in Memcached.
 *
 * @param {object} the key.
 * @param {object} the value.
 * @param {Number} a lifetime.
 * @returns a Promise
 */
function replace(key, value, lifetime) {
  return new Promise((resolve, reject) => {
    memcached.replace(key, value, lifetime, err => {
      if (err) {
        console.error(err);
        return reject(err);
      }
      return resolve();
    });
  });
}

function getMulti(keys) {
  return new Promise((resolve, reject) => {
    memcached.getMulti(keys, (err, data) => {
      if (err) {
        console.error(err);
        return reject(err);
      }
      return resolve(data);
    });
  });
}

/**
 * Flushes memcache
 */
function flush() {
  memcached.flush();
}

/**
 * Stores URLs in PAGELIST_URLS that need to be removed from cache.
 */
function storeCachedUrls(url) {
  // Stores list and search pages
  if (url.indexOf('/pwas/') === -1 || url.indexOf('/pwas/search') >= 0) {
    this.get(PAGELIST_URLS)
      .then(array => {
        let urlSet = new Set(array);
        urlSet.add(url);
        this.set(PAGELIST_URLS, Array.from(urlSet), CACHE_LIFETIME);
      })
      .catch(_ => {
        let urlSet = new Set();
        urlSet.add(url);
        this.set(PAGELIST_URLS, Array.from(urlSet), CACHE_LIFETIME);
      });
  }
}

/**
 * Flush URLs from PAGELIST_URLS list.
 */
function flushCacheUrls() {
  this.get(PAGELIST_URLS)
    .then(array => {
      array.forEach(url => {
        this.del(url);
      });
      this.del(PAGELIST_URLS);
    })
    .catch(err => {
      console.error('Error flushing cache URLs: ', err);
    });
}

module.exports = {
  get: get,
  del: del,
  set: set,
  replace: replace,
  flush: flush,
  getMulti: getMulti,
  storeCachedUrls: storeCachedUrls,
  flushCacheUrls: flushCacheUrls
};


================================================
FILE: lib/data-fetcher.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 URL = require('url');
const fetch = require('node-fetch');
const fs = require('fs');
const cheerio = require('cheerio');
const config = require('../config/config');

const FIREBASE_AUTH = config.get('FIREBASE_AUTH');
const USER_AGENT = ['Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36',
  '(KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36'].join(' ');

/**
 * Fetches the description from a webpage's metadata.
 *
 * @param {string} url of the page to get the description from
 * @return {Promise<string>} with the description or error
 */
function fetchMetadataDescription(url) {
  return fetchWithUA(url)
    .then(response => response.text())
    .then(html => cheerio.load(html))
    .then($ => {
      return $('meta[name="description"]').attr('content');
    });
}

/**
 * Fetches the manifest URL from a webpage's link rel header.
 *
 * @param {string} url of the page to get the manifest link from
 * @return {Promise<string>} with the URL or error
 */
function fetchLinkRelManifestUrl(pageUrl) {
  return fetchWithUA(pageUrl)
    .then(response => response.text())
    .then(html => cheerio.load(html))
    .then($ => $('link[rel="manifest"]').attr('href'))
    .then(newUrl => {
      if (!newUrl) {
        return Promise.reject(
          'this Web does not have a Web App Manifest (<link rel="manifest" href="...">)');
      }
      return URL.resolve(pageUrl, newUrl);
    });
}

/**
 * Fetches a URL using the USER_AGENT set on top of this file.
 * Uses spdy for http2 support
 *
 * @param {string} url to te be fetched
 * @return {Promise<Response>}
 */
function fetchWithUA(url) {
  const options = {
    method: 'GET',
    headers: {
      'user-agent': USER_AGENT
    },
    timeout: 1000
  };
  return fetch(url, options);
}

/**
 * Fetches a URL using the USER_AGENT set on top of this file and returns Json.
 *
 * @param {string} url to te be fetched
 * @return {Promise<Json>}
 */
function fetchJsonWithUA(url) {
  return fetchWithUA(url)
    .then(response => response.json());
}

/**
 * Reads a file and returns a promise instead of the fs' callback.
 *
 * @param {string} filename to te be read
 * @return {Promise<data>} with the content of the file
 */
function readFile(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, {encoding: 'utf-8'}, (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

function _firebaseOptions(payload) {
  if (payload) {
    return {
      method: 'POST',
      headers: {
        'Authorization': FIREBASE_AUTH,
        'content-type': 'application/json'
      },
      body: JSON.stringify(payload)
    };
  }

  return {
    method: 'GET',
    headers: {
      Authorization: FIREBASE_AUTH
    }
  };
}

function _handleFirebaseResponse(response) {
  // Request was successful. Resolve Promise with the JSON.
  if (response.status === 200) {
    return response.json();
  }

  // Request returned an error response. Reject with an error message.
  return response.text()
    .then(text => {
      return Promise.reject(
        'Request failed with response: ' + response.status + ' Message: ' + text);
    });
}

function firebaseFetch(url, payload) {
  const options = _firebaseOptions(payload);
  const res = fetch(url, options);
  res.then(r => {
    if (r.status !== 200) {
      r.text().then(msg => {
        // Add codebase-wide logging system
        console.warn(`firebaseFetch error: GET ${url} => ${r.status}: ${msg}`);
      });
    }
  });
  return res.then(_handleFirebaseResponse);
}

/**
 * POST to url
 *
 * @param {string} url to POST to
 * * @param {string} body of the POST
 * @return {Promise<Response>}
 */
function postJson(url, body) {
  return fetch(url, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(body)});
}

module.exports = {
  fetchMetadataDescription,
  fetchLinkRelManifestUrl,
  fetchWithUA,
  fetchJsonWithUA,
  firebaseFetch,
  _firebaseOptions,
  _handleFirebaseResponse,
  readFile,
  postJson
};


================================================
FILE: lib/event-bus.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 messageBus = new EventEmitter();
module.exports = messageBus;


================================================
FILE: lib/favorite-pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 db = require('../lib/model-datastore');
const FavoritePwa = require('../models/favorite-pwa');

const ENTITY_NAME = 'FavoritePwa';

/**
 * Saves a FavoritePwa object into the DB.
 *
 * @param {FavoritePwa} lighthouse
 * @return {Promise<FavoritePwa>}
 */
exports.save = function(favoritePwa) {
  return db.update(ENTITY_NAME, favoritePwa.id, favoritePwa)
    .catch(err => {
      console.log(err);
      return Promise.reject('Error saving the FavoritePwa');
    });
};

/**
 * Retrieves FavoritePwas for a given User.
 *
 * @param {number} userId
 * @return {Promise<Array<FavoritePwa>>}
 */
exports.findByUserId = function(userId) {
  console.log(userId);
  const query = db.createQuery(ENTITY_NAME).filter('userId', '=', userId);
  return db.runQuery(query).then(result => {
    if (!result || result.entities.length === 0) {
      return null;
    }
    let favoritePwas = result.entities.map(entry => {
      return new FavoritePwa(entry.pwaId, entry.userId);
    });
    return favoritePwas;
  });
};

/**
 * Retrieves a FavoritePwa for given User & PWA.
 *
 * @param {number} pwaId
 * @param {number} userId
 * @return {Promise<FavoritePwa>}
 */
exports.findFavoritePwa = function(pwaId, userId) {
  const query = db.createQuery(ENTITY_NAME).filter('pwaId', '=', parseInt(pwaId, 10))
      .filter('userId', '=', userId).limit(1);
  return db.runQuery(query).then(result => {
    if (!result || result.entities.length === 0) {
      return null;
    }
    return new FavoritePwa(result.entities[0].pwaId, result.entities[0].userId);
  });
};

/**
 * Deletes a FavoritePwa from DB.
 *
 * @param {number} key of the FavoritePwa
 * @return {Promise<>}
 */
exports.delete = function(key) {
  return db.delete(ENTITY_NAME, key);
};


================================================
FILE: lib/images.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');
const dataFetcher = require('../lib/data-fetcher');
const sharp = require('sharp');
const stream = require('stream');
const strongDataUri = require('strong-data-uri');
const url = require('url');
const mime = require('mime-types');

const cloudStorage = require('@google-cloud/storage');
const CLOUD_BUCKET = config.get('CLOUD_BUCKET');
const storage = cloudStorage({
  projectId: config.get('GCLOUD_PROJECT')
});
const bucket = storage.bucket(CLOUD_BUCKET);

const CACHE_CONTROL_EXPIRES = 60 * 60 * 24; // 1 day.

/**
 * Fetches and Saves an Image to Google Cloud Storage.
 *
 * @param {string} url image URL to retrieve
 * @param {string} destFile name of the destination file
 * @return {Promise<url[]>>} URLs for the new images in Google Cloud Storage
 */
function fetchAndSave(imageUrl, destFile) {
  const parsedUrl = url.parse(imageUrl);
  switch (parsedUrl.protocol) {
    case 'data:': {
      return this.dataUriAndSave(imageUrl);
    }
    case 'http:':
    case 'https:': {
      return dataFetcher.fetchWithUA(imageUrl)
        .then(response => {
          if (response.status !== 200) {
            return Promise.reject(new Error(
              'Bad Response (' + response.status + ') loading image: ' + response.url));
          }

          // Using mime.lookup insteand of `// response.headers.get('Content-Type');`, as some
          // publishers use an invalid value for the content-type.
          const contentType = mime.lookup(imageUrl);
          return this.saveImages(response.body, destFile, contentType);
        })
        .then(savedUrls => {
          return savedUrls;
        });
    }
    default: {
      return Promise.reject('Unsupported Protocol: ' + parsedUrl.protocol);
    }
  }
}

/**
 * Process a Data URI Image and Saves to Google Cloud Storage.
 *
 * @param {string} url Data URI image URL to process
 * @param {string} destFile name of the destination file
 * @return {Promise<url[]>} URLs for the new images in Google Cloud Storage
 */
function dataUriAndSave(url, destFile) {
  const buffer = strongDataUri.decode(url);
  const contentType = buffer.mimetype;
  const bufferStream = new stream.PassThrough();
  bufferStream.end(buffer);
  return this.saveImages(bufferStream, destFile, contentType);
}

/**
 * Saves the content from the stream to Google Cloud Storage
 * with 3 difference sizes, original, 128*128px and 64*64px
 *
 * @param {stream.Readable} stream
 * @param {string} destFile name of the destination file
 * @param {contentType} destFile image's mimetype
 * @return {Promise<url[]>} URLs for the new images in Google Cloud Storage
 */
function saveImages(readStream, destFile, contentType) {
  return Promise.all([
    this.saveImage(readStream, destFile, contentType),
    this.saveImage(readStream, destFile, contentType, 128),
    this.saveImage(readStream, destFile, contentType, 64)
  ])
  .catch(err => {
    console.log('Error Saving Images', err);
    return null;
  });
}

/**
 * Saves the content from the stream to Google Cloud Storage.
 *
 * @param {stream.Readable} stream
 * @param {string} destFile name of the destination file
 * @param {contentType} destFile image's mimetype
 * @param {int} size image's new size
 * @return {Promise<string>} full public URL of saved image in Google Cloud Storage
 */
function saveImage(readStream, destFile, contentType, size) {
  const destFilename = (size || 'original') + '_' + destFile;
  return new Promise((resolve, reject) => {
    const file = bucket.file(destFilename);
    const metadata = {
      contentType: contentType,
      cacheControl: 'public, max-age=' + CACHE_CONTROL_EXPIRES
    };

    const writeStream = file.createWriteStream({
      metadata: metadata,
      gzip: 'auto' // Enables Gzipping the content based on the contentType.
    });

    writeStream.on('error', err => {
      reject(err);
    });

    writeStream.on('finish', () => {
      resolve(getPublicUrl(destFilename));
    });

    if (size) {
      const transformer = sharp().resize(size);
      transformer.on('error', err => {
        reject(err);
      });
      readStream.pipe(transformer).pipe(writeStream);
    } else {
      readStream.pipe(writeStream);
    }
  });
}

/**
 * @private
 * Given a filename, returns the GCloud address for it.
 *
 * @param {string} filename the original filename.
 * @return {Promise<string>} the public URL of the file.
 */
function getPublicUrl(filename) {
  return 'https://storage.googleapis.com/' + CLOUD_BUCKET + '/' + filename;
}

module.exports = {
  fetchAndSave,
  saveImage,
  saveImages,
  dataUriAndSave
};


================================================
FILE: lib/lighthouse.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');
const pwaLib = require('../lib/pwa');
const libWebPerformance = require('../lib/web-performance');
const Lighthouse = require('../models/lighthouse');

const db = require('../lib/model-datastore');
const datastore = require('@google-cloud/datastore');
const ds = datastore({
  projectId: config.get('GCLOUD_PROJECT')
});

const ENTITY_NAME = 'Lighthouse';
const E_PWA_NOT_FOUND = exports.E_PWA_NOT_FOUND = 1;
const E_FETCHING_STORING_LIGHTHOUSE = exports.E_FETCHING_STORING_LIGHTHOUSE = 2;
const LIGTHOUSE_DATE_CHANGES = ['2016-12-01', '2017-03-01', '2017-05-05',
  '2017-05-25', '2017-06-20'];

/**
 * Saves a Lighthouse object into the DB.
 *
 * @param {Lighthouse} lighthouse
 * @return {Promise<Lighthouse>}
 */
exports.save = function(lighthouse) {
  return new Promise((resolve, reject) => {
    db.update(ENTITY_NAME, lighthouse.id, lighthouse)
      .then(result => {
        return resolve(result);
      })
      .catch(err => {
        console.log(err);
        return reject('Error saving the Lighthouse report');
      });
  });
};

/**
 * Retrieves the latest Lighthouse for a PWA.
 *
 * @param {number} pwaId
 * @return {Promise<Lighthouse>}
 */
exports.findByPwaId = function(pwaId) {
  return new Promise((resolve, reject) => {
    const query = ds.createQuery(ENTITY_NAME)
      .filter('pwaId', '=', parseInt(pwaId, 10)).order('date', {descending: true}).limit(1);
    ds.runQuery(query, (err, lighthouses) => {
      if (err) {
        return reject(err);
      }
      if (lighthouses.length === 0) {
        return resolve(null);
      }
      return resolve(lighthouses[0]);
    });
  });
};

/**
 * Retrieves the Lighthouse data for a PWA
 *
 * @param {number} pwaId
 * @return {Promise<Lighthouse[]>}
 */
exports.getLighthouseByPwaId = function(pwaId) {
  return new Promise((resolve, reject) => {
    // Gets the last 2 years of Ligthouses for a PWA
    const query = ds.createQuery(ENTITY_NAME)
      .filter('pwaId', '=', parseInt(pwaId, 10)).order('date', {descending: true}).limit(730);
    ds.runQuery(query, (err, lighthouses) => {
      if (err) {
        return reject(err);
      }
      return resolve(lighthouses);
    });
  });
};

/**
 * Retrieves the Lighthouse Grpah data for a PWA
 * in Google Charts JSON format
 *
 * @param {number} pwaId
 * @return {Promise<Json>}
 */
exports.getLighthouseGraphByPwaId = function(pwaId) {
  // Gets the last 2 years of Ligthouses for a PWA
  return this.getLighthouseByPwaId(pwaId)
    .then(lighthouses => {
      if (lighthouses.length === 0) {
        return null;
      }
      // Graph data uses the Google Charts JSON format:
      // https://developers.google.com/chart/interactive/docs/reference#dataparam
      let data = {};
      data.cols = [{label: 'Date', type: 'date'}, {label: 'Score', type: 'number'},
        {label: 'LH change', type: 'number'}];
      data.rows = [];
      lighthouses.forEach(lighthouse => {
        let lighthouseChange = null;
        const date = new Date(Date.parse(lighthouse.date));
        // Add dots over the line to anotate lighthouse changes
        if (LIGTHOUSE_DATE_CHANGES.indexOf(lighthouse.date) > -1) {
          lighthouseChange = lighthouse.totalScore;
        }
        data.rows.push(
          {c: [{v:
            'Date(' + date.getFullYear() + ',' + date.getMonth() + ',' + date.getDate() + ')'},
          {v: lighthouse.totalScore},
          {v: lighthouseChange}]});
      });
      return data;
    });
};

/**
 * Generates a Lighthouse report for a PWA by its id.
 *
 * @param {number} pwaId
 * @return {Promise<Lighthouse>}
 */
exports.fetchAndSave = function(pwaId) {
  return new Promise((resolve, reject) => {
    pwaLib.find(pwaId)
      .then(pwa => {
        libWebPerformance.getLighthouseReport(pwa)
          .then(lighthouseJson => {
            const reportData = lighthouseJson[0].rawData.value;
            const lighthouse = new Lighthouse(pwaId, pwa.absoluteStartUrl, reportData);
            this.save(lighthouse);
            return lighthouse;
          })
          .then(lighthouse => {
            return resolve(lighthouse);
          })
          .catch(err => {
            console.error(err);
            return reject(E_FETCHING_STORING_LIGHTHOUSE);
          });
      })
      .catch(err => {
        console.error(err);
        return reject(E_PWA_NOT_FOUND);
      });
  });
};

/**
 * Creates a new JSON with the main elemnts from a Lighthouse report.
 *
 * @param {string} lighthouseJson
 * @return {JSON}
 */
exports.processLighthouseJson = function(lighthouseJson) {
  let lighthouseInfo = {};
  lighthouseInfo.lighthouseVersion = lighthouseJson.lighthouseVersion;
  // Some PWAs do not have a LH 2.x report yet
  if (lighthouseInfo.lighthouseVersion.startsWith('1.')) {
    lighthouseJson.aggregations.forEach(aggregation => {
      let i = 0;
      let totalScore = 0;
      if (aggregation.name === 'Progressive Web App') {
        let scoreJson = [];
        aggregation.score.forEach(score => {
          scoreJson[i++] = {
            name: score.name,
            overall: Math.round(score.overall * 100),
            subItems: JSON.stringify(score.subItems)
          };
          totalScore += score.overall;
        });
        lighthouseInfo.aggregation = {
          name: aggregation.name,
          description: aggregation.description,
          scores: scoreJson
        };
        if (i > 0) {
          lighthouseInfo.totalScore = Math.round((totalScore / i) * 100);
        }
      }
    });
    let j = 0;
    lighthouseInfo.audits = [];
    for (let key in lighthouseJson.audits) {
      if (lighthouseJson.audits.hasOwnProperty(key)) {
        let audit = lighthouseJson.audits[key];
        lighthouseInfo.audits[j++] = {
          name: audit.name,
          description: audit.description,
          score: audit.score,
          displayValue: audit.displayValue,
          optimalValue: audit.optimalValue
        };
      }
    }
  } else {
    lighthouseInfo.totalScore = Math.round(lighthouseJson.score);
    lighthouseInfo.reportCategories = lighthouseJson.reportCategories;
  }
  return lighthouseInfo;
};


================================================
FILE: lib/manifest.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 dataFetcher = require('../lib/data-fetcher');
const Manifest = require('../models/manifest');

/**
 * Fetches the Manifest from the manifestUrl.
 *
 * @param {string} manifestUrl
 * @return {Promise<Manifest>}
 */
function fetchManifest(manifestUrl) {
  return dataFetcher.fetchJsonWithUA(manifestUrl)
    .then(json => new Manifest(manifestUrl, json));
}

/**
 * Wrapper for the manifest validator from lighthouse.
 *
 * @param {Manifest} manifest
 * @param {string} manifestUrl URL of manifest itself
 * @param {string} documentUrl URL of document that links to the manifest
 * @return string[] errors found in manifest
 */
function validateManifest(manifest, manifestUrl, documentUrl) {
  const parse = require('../third_party/manifest-parser.js');
  const res = parse(manifest, manifestUrl, documentUrl);
  // Lighthouse annotates the actual elements with validation errors; "flatten"
  // these here.
  function flatten(obj) {
    const debugString = obj.debugString ? [obj.debugString] : [];
    if (typeof obj.value !== 'object') {
      return debugString;
    }
    return Object.keys(obj.value).reduce((acc, k) => {
      return acc.concat(flatten(obj.value[k]));
    }, debugString);
  }
  return flatten(res);
}

module.exports = {
  fetchManifest,
  validateManifest
};


================================================
FILE: lib/metadata.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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';

/**
 * Generates the default metadata from a http request
 */
module.exports.fromRequest = function(req, newUrl) {
  const host = req.get('host');
  const url = newUrl || req.protocol + '://' + host + req.originalUrl;
  const timestamp = new Date().toISOString();
  const logo = req.protocol + '://' + host + '/favicons/android-chrome-512x512.png';
  const leader = req.protocol + '://' + host + '/img/pwa-directory-preview.png';
  const metadata = {
    url: url,
    host: host,
    datePublished: timestamp,
    dateModified: timestamp,
    logo: logo,
    logoWidth: '512',
    logoHeight: '512',
    leader: leader,
    leaderWidth: '2008',
    leaderHeight: '1386'
  };
  return metadata;
};


================================================
FILE: lib/model-datastore.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');

const datastore = require('@google-cloud/datastore');
const ds = datastore({
  projectId: config.get('GCLOUD_PROJECT')
});

const ENTITY_COUNT_KIND = 'counts';

/**
 * Translates from Datastore's entity format to
 * the format expected by the application.
 *
 * Datastore format:
 *   {
 *     key: [kind, id],
 *     data: {
 *       property: value
 *     }
 *   }
 *
 * Application format:
 *   {
 *     id: id,
 *     property: value
 *   }
 *
 * @param {Object} obj
 * @return {Object}
 */
function fromDatastore(obj) {
  obj.id = obj[datastore.KEY].id;
  return obj;
}

/**
 * Translates from the application's format to the datastore's
 * extended entity property format. It also handles marking any
 * specified properties as non-indexed. Does not translate the key.
 *
 * Application format:
 *   {
 *     id: id,
 *     property: value,
 *     unindexedProperty: value
 *   }
 *
 * Datastore extended format:
 *   [
 *     {
 *       name: property,
 *       value: value
 *     },
 *     {
 *       name: unindexedProperty,
 *       value: value,
 *       excludeFromIndexes: true
 *     }
 *   ]
 *
 * @param {Object} obj
 * @param {Array} nonIndexed
 * @return {Array<DBObject>}
 */
function toDatastore(obj, nonIndexed) {
  nonIndexed = nonIndexed || [];
  const results = [];
  Object.keys(obj).forEach(k => {
    if (obj[k] === undefined) {
      return;
    }

    let value;
    if (obj[k] instanceof Object) {
      if (nonIndexed.indexOf(k) === -1) {
        value = deepCopy(obj[k]);
      } else {
        // nonIndexed properties need to be stored as Strings
        value = JSON.stringify(obj[k]);
      }
    } else {
      value = obj[k];
    }

    results.push({
      name: k,
      value: value,
      excludeFromIndexes: nonIndexed.indexOf(k) !== -1
    });
  });
  return results;
}

function deepCopy(object) {
  if (!(object instanceof Object)) {
    return object;
  }

  if (object instanceof Date) {
    return object;
  }

  const clone = Object.assign({}, object);
  Object.keys(clone).forEach(k => {
    clone[k] = deepCopy(clone[k]);
  });

  return clone;
}

/**
 * Lists all Entities in the Datastore sorted alphabetically by title.
 * The ``limit`` argument determines the maximum amount of results to
 * return per page. The ``token`` argument allows requesting additional
 * pages. The callback is invoked with ``(err, Entities, nextPageToken)``.
 *
 * @param {string} kind
 * @param {number} offset
 * @param {number} limit
 * @param {object} {field:property name, config:sort direction}
 * @return {Promise<Array<Object>>}
 */
function list(kind, offset, limit, sort, filters) {
  return runQuery(createQuery(kind, offset, limit, sort, filters));
}

/**
 * Creates a DB query.
 *
 * @param {string} kind
 * @param {number} offset
 * @param {number} limit
 * @param {object} {field:property name, config:sort direction}
 * @return {query}
 */
function createQuery(kind, offset, limit, sort, filters) {
  const query = ds.createQuery([kind])
    .offset(offset || 0)
    .limit(limit);
  if (sort) {
    query.order(sort.field, sort.config);
  }
  if (filters) {
    for (let filter of filters) {
      query.filter(filter.property, filter.operator, filter.value);
    }
  }
  return query;
}

/**
 * Executes a DB query.
 *
 * @param {query} query
 * @return {Promise<Array<Object>>}
 */
function runQuery(query) {
  return new Promise((resolve, reject) => {
    ds.runQuery(query, (err, entities, nextQuery) => {
      if (err) {
        return reject(err);
      }
      const hasMore = nextQuery.moreResults !== 'NO_MORE_RESULTS';
      resolve({
        entities: entities.map(fromDatastore),
        hasMore: hasMore,
        endCursor: nextQuery.endCursor
      });
    });
  });
}

/**
 * Parse the Key to a number if possible
 * @param {object} key
 * @return {object : number}
 */
function parseKey(key) {
  return isNaN(key) ? key : parseInt(key, 10);
}

function startTransaction(transaction) {
  return new Promise((resolve, reject) => {
    transaction.run(err => {
      if (err) {
        return reject(err);
      }
      return resolve(transaction);
    });
  });
}

function commitTransaction(transaction) {
  return new Promise((resolve, reject) => {
    transaction.commit(err => {
      if (err) {
        return reject(err);
      }
      return resolve();
    });
  });
}

function rollbackTransaction(transaction) {
  return new Promise((resolve, reject) => {
    transaction.rollback(err => {
      if (err) {
        return reject(err);
      }
      return resolve();
    });
  });
}

function transactionGet(transaction, key) {
  return new Promise((resolve, reject) => {
    transaction.get(key, (err, entity) => {
      if (err) {
        return reject(err);
      }
      return resolve(entity);
    });
  });
}

function updateCount(transaction, kind, inc) {
  return new Promise((resolve, reject) => {
    const countKey = ds.key([ENTITY_COUNT_KIND, kind]);
    transaction.get(countKey, (err, countEntity) => {
      if (err) {
        return reject(err);
      }

      let count = 0;
      if (countEntity) {
        count = countEntity.count;
      }

      if (inc) {
        count++;
      } else {
        count--;
      }

      transaction.save({key: countKey, data: {count: count}});
      return resolve();
    });
  });
}

/**
 * Creates a new Entity or updates an existing Entity with new data. The provided
 * data is automatically translated into Datastore format. The Entity will be
 * queued for background processing.
 *
 * @param {string} kind
 * @param {string} id
 * @param {Object} data
 * @return {Promise<Object>}
 */
function update(kind, id, data) {
  return new Promise((resolve, reject) => {
    let key;
    if (id) {
      key = ds.key([kind, parseKey(id)]);
    } else {
      key = ds.key(kind);
    }

    const entity = {
      key: key,
      data: toDatastore(data, [
        'description', '_manifest', '_lighthouseJson', 'lighthouseInfo', 'webPageTest'])
    };

    ds.save(
      entity,
      err => {
        data.id = entity.key.id;
        if (err) {
          reject(err);
          return;
        }
        resolve(data);
      }
    );
  });
}

/**
 * Creates a new Entity or updates an existing Entity with new data. The provided
 * data is automatically translated into Datastore format. The Entity will be
 * queued for background processing.
 *
 * @param {string} kind
 * @param {string} id
 * @param {Object} data
 * @return {Promise<Object>}
 */
function updateWithCounts(kind, id, data) {
  let key;
  if (id) {
    key = ds.key([kind, parseKey(id)]);
  } else {
    key = ds.key(kind);
  }

  const entity = {
    key: key,
    data: toDatastore(data, [
      'description', '_manifest', '_lighthouseJson', 'lighthouseInfo', 'webPageTest'])
  };

  const transaction = ds.transaction();
  return startTransaction(transaction)
    .then(_ => {
      transaction.save(entity);
      if (!id) {
        return updateCount(transaction, kind, true);
      }
      return Promise.resolve();
    })
    .then(_ => {
      return commitTransaction(transaction);
    })
    .then(_ => {
      data.id = key.id;
      return data;
    })
    .catch(err => {
      console.error(err);
      return rollbackTransaction(transaction)
        .then(_ => {
          return Promise.reject(err);
        });
    });
}

function count(kind) {
  return read(ENTITY_COUNT_KIND, kind)
    .then(entity => {
      if (!entity) {
        return 0;
      }
      return entity.count || 0;
    })
    .catch(_ => {
      return 0;
    });
}

/**
 * Reads an Object of the specified kind and Id from the Datastore.
 *
 * @param {string} kind
 * @param {string} id
 * @return {Promise<Object>}
 */
function read(kind, id) {
  return new Promise((resolve, reject) => {
    const key = ds.key([kind, parseKey(id)]);
    ds.get(key, (err, entity) => {
      if (err) {
        return reject(err);
      }
      if (!entity) {
        return reject({
          code: 404,
          message: 'Not found'
        });
      }
      resolve(fromDatastore(entity));
    });
  });
}

/**
 * Deletes an Object with the specified kind and Id from the Datastore
 *
 * @param {string} kind
 * @param {string} id
 * @return {Promise<>}
 */
function _deleteWithCounts(kind, id) {
  const key = ds.key([kind, parseKey(id)]);
  const transaction = ds.transaction();
  return startTransaction(transaction)
    .then(_ => {
      return transactionGet(transaction, key);
    })
    .then(entity => {
      if (!entity) {
        return Promise.reject('Trying to delete entity that does not exist: ' + key.id);
      }
      transaction.delete(key);
      return updateCount(transaction, kind, false);
    })
    .then(_ => {
      return commitTransaction(transaction);
    })
    .catch(err => {
      return rollbackTransaction(transaction)
        .then(_ => {
          return Promise.reject(err);
        });
    });
}

/**
 * Deletes an Object with the specified kind and Id from the Datastore
 *
 * @param {string} kind
 * @param {string} id
 * @return {Promise<>}
 */
function _delete(kind, id) {
  return new Promise((resolve, reject) => {
    const key = ds.key([kind, parseKey(id)]);
    ds.delete(key, err => {
      if (err) {
        return reject(err);
      }
      resolve();
    });
  });
}

module.exports = {
  create: (kind, data) => {
    update(kind, null, data);
  },
  count: count,
  read: read,
  update: update,
  delete: _delete,
  updateWithCounts: updateWithCounts,
  deleteWithCounts: _deleteWithCounts,
  list: list,
  createQuery: createQuery,
  runQuery: runQuery
};


================================================
FILE: lib/notifications.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 dataFetcher = require('../lib/data-fetcher');

exports.list = function(token) {
  if (!token) {
    return Promise.reject(new Error('Missing token'));
  }

  const url = 'https://iid.googleapis.com/iid/info/' + token + '?details=true';
  return dataFetcher.firebaseFetch(url)
    .then(userDetails => {
      if (!userDetails || !userDetails.rel || !userDetails.rel.topics) {
        return Promise.resolve([]);
      }
      return Object.keys(userDetails.rel.topics);
    });
};

exports.subscribe = function(token, topic) {
  if (!token) {
    return Promise.reject(new Error('Missing token'));
  }

  if (!topic) {
    return Promise.reject(new Error('Missing topic'));
  }

  const url = 'https://iid.googleapis.com/iid/v1:batchAdd';
  const payload = {
    to: '/topics/' + topic,
    registration_tokens: [token] // eslint-disable-line camelcase
  };
  return dataFetcher.firebaseFetch(url, payload);
};

exports.unsubscribe = function(token, topic) {
  if (!token) {
    return Promise.reject(new Error('Missing token'));
  }

  if (!topic) {
    return Promise.reject(new Error('Missing topic'));
  }

  const url = 'https://iid.googleapis.com/iid/v1:batchRemove';
  const payload = {
    to: '/topics/' + topic,
    registration_tokens: [token] // eslint-disable-line camelcase
  };
  return dataFetcher.firebaseFetch(url, payload);
};

exports.sendPush = function(topic, notification) {
  if (!topic) {
    return Promise.reject(new Error('Missing topic'));
  }

  if (!notification) {
    return Promise.reject(new Error('Missing notification'));
  }

  // Require the notification to have a title, at minimum
  if (!notification.title) {
    return Promise.reject(new Error('Missing notification title'));
  }
  const url = 'https://fcm.googleapis.com/fcm/send';
  const payload = {
    to: '/topics/' + topic,
    notification: notification
  };
  return dataFetcher.firebaseFetch(url, payload);
};


================================================
FILE: lib/promise-sequential.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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';

/** Execute a list of Promise return functions serially
 * @param {list} a list of promise returning functions to execute serially
 * @return {Promise<result>} the result of the last promise in the list
 *  Example:
 *    promiseSequential.all([
 *      _ => this.function1(result),
 *      result => this.function2(result),
 *      result => this.function3(result)
 *    ]);
 */
exports.all = function(promiseList) {
  return promiseList.reduce((promiseFn, fn) => {
    return promiseFn.then(fn);
  }, Promise.resolve());
};


================================================
FILE: lib/pwa-index.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 libSearch = require('../lib/search');
const libPwa = require('../lib/pwa');
const Pwa = require('../models/pwa');
const db = require('../lib/model-datastore');
const ENTITY_NAME = 'PWA';

/**
 * Add all PWAs from the DB into the text search index.
 */
exports.indexAllPwas = _ => {
  let indexPage = (skip, limit) =>
    db.list(ENTITY_NAME, skip, limit)
      .then(result => {
        const pwas = result.entities.map(pwa => {
          return Object.assign(new Pwa(), pwa);
        });
        libSearch.addPwas(pwas);
        if (result.hasMore) {
          return indexPage(skip + limit, limit);
        }
        console.log('All PWAs indexed');
        return Promise.resolve();
      });
  return indexPage(0, 100);
};

/**
 * Search for PWAS using the text search index.
 *
 * @param {string} query
 * @return {resultPage} resultPage with an arrays of PWAs and hasMore boolean
 */
exports.searchPwas = string => {
  return libSearch.search(string).then(result => {
    let pwas = new Array(result.length);
    let find = (currentValue, index) => {
      return libPwa.find(currentValue.ref).then(pwa => {
        // Inserting at index to keep result order
        // because Promises run in parallel
        pwas[index] = pwa;
      });
    };
    return Promise.all(result.map(find)).then(_ => {
      // Returning all results without pagination for now
      const resultPage = {
        pwas: pwas,
        hasMore: false
      };
      return Promise.resolve(resultPage);
    });
  });
};

/**
 * Update PWA in the search index.
 *
 * @param {Pwa} pwa to update
 * @return {Promise<Pwa>}
 */
exports.updateSearchIndex = function(pwa) {
  if (pwa.isNew()) {
    libSearch.addPwa(pwa);
  } else {
    libSearch.updatePwa(pwa);
  }
  return Promise.resolve(pwa);
};


================================================
FILE: lib/pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 url = require('url');
const config = require('../config/config');

const libImages = require('../lib/images');
const dataFetcher = require('../lib/data-fetcher');
const libWebPerformance = require('../lib/web-performance');
const libManifest = require('../lib/manifest');
const notificationsLib = require('../lib/notifications');
const promiseSequential = require('../lib/promise-sequential');
const libPwa = require('./pwa');
const libPwaIndex = require('./pwa-index');
const libCache = require('../lib/data-cache');

const Pwa = require('../models/pwa');

const db = require('../lib/model-datastore');
const datastore = require('@google-cloud/datastore');
const ds = datastore({
  projectId: config.get('GCLOUD_PROJECT')
});

const DEFAULT_SORT_TYPE_KEY = 'score';
const ENTITY_NAME = 'PWA';
const E_MANIFEST_INVALID_URL = exports.E_MANIFEST_INVALID_URL = 2;
const E_MANIFEST_URL_MISSING = exports.E_MANIFEST_URL_MISSING = 3;
const E_MISSING_USER_INFORMATION = exports.E_MISSING_USER_INFORMATION = 4;
const E_NOT_A_PWA = exports.E_NOT_A_PWA = 5;
// Waiting time to fetch external info and send notification for new PWAs
const WAIT_TIME_NEW_PWAS = 10 * 60 * 1000; // 10 minutes

const SORT_TYPE_MAP = new Map([
  ['name', {name: 'name', field: 'manifest.name'}],
  ['newest', {name: 'newest', field: 'created', config: {descending: true}}],
  ['score', {name: 'score', field: 'lighthouseScore', config: {descending: true}}]
]);

/**
 * List of PWAs.
 *
 * @param {number} skip specifies the starting point for handling pagination
 * @param {number} limit number of results to return
 * @param {string} sort the field name to sort the results
 * @return {resultPage} resultPage with an arrays of PWAs and hasMore boolean
 */
exports.list = async (skip, limit, sort, filters) => {
  filters = filters || {};
  let sortType = SORT_TYPE_MAP.get(DEFAULT_SORT_TYPE_KEY);
  if (sort) {
    sortType = SORT_TYPE_MAP.get(sort) || sortType;
  }

  const queryFilters = [];
  if (filters.minLighthouseScore) {
    queryFilters.push({
      property: 'lighthouseScore',
      operator: '>=',
      value: filters.minLighthouseScore
    });
  }

  if (filters.installable) {
    queryFilters.push({
      property: 'installable',
      operator: '=',
      value: filters.installable
    });
  }

  const result = await db.list(ENTITY_NAME, skip, limit, sortType, queryFilters);
  const pwas = result.entities.map(pwa => {
    return Object.assign(new Pwa(), pwa);
  });

  const resultPage = {
    pwas: pwas,
    hasMore: result.hasMore
  };

  return resultPage;
};

/**
 * Saves a PWA to DB.
 *
 * @param {Pwa} the Pwa to be saved.
 * @return a Promise.
 */
exports.savePwa = function(pwa) {
  return db.updateWithCounts(ENTITY_NAME, pwa.id, pwa)
    .then(savedPwa => {
      return savedPwa;
    })
    .catch(err => {
      console.log('Error saving PWA err' + pwa.id);
      Promise.reject(err);
    });
};

/**
 * Finds a PWA by key.
 *
 * @param {number} key of the PWA
 * @return {Pwa} the PWA from DB
 */
exports.find = function(key) {
  return db.read(ENTITY_NAME, key)
    .then(pwa => {
      const pwaInstance = Object.assign(new Pwa(), pwa);
      return pwaInstance;
    });
};

/**
 * Finds a PWA by its manifest URL from DB.
 *
 * @param {string} manifestUrl of the PWA's manifest
 * @return {Pwa|null} the PWA from DB, or null if not found
 */
exports.findByManifestUrl = function(manifestUrl) {
  return new Promise((resolve, reject) => {
    const query = ds.createQuery(ENTITY_NAME).filter('manifestUrl', manifestUrl);
    ds.runQuery(query, (err, pwas) => {
      if (err) {
        return reject(err);
      }

      if (pwas.length === 0) {
        return resolve(null);
      }

      let pwa = Object.assign(new Pwa(), pwas[0]);
      return resolve(pwa);
    });
  });
};

/**
 * Finds a PWA by its encodedStartUrl from DB.
 *
 * @param {string} encodedStartUrl of the PWA's manifest
 * @return {Pwa|null} the PWA from DB, or null if not found
 */
exports.findByEncodedStartUrl = function(encodedStartUrl) {
  return new Promise((resolve, reject) => {
    const query = ds.createQuery(ENTITY_NAME).filter('encodedStartUrl', encodedStartUrl);
    ds.runQuery(query, (err, pwas) => {
      if (err) {
        return reject(err);
      }

      if (pwas.length === 0) {
        return resolve(null);
      }

      let pwa = Object.assign(new Pwa(), pwas[0]);
      return resolve(pwa);
    });
  });
};

/**
 * Creates or Updates a PWA.
 *
 * Steps:
 *  1) Validate Pwa
 *  2) Update Pwa's Manifest
 *  3) Save (to get the DB id for following steps)
 *  4) Update PWA's MetadataDescription
 *  5) Update PWA's Icon
 *  6) Save
 *  7) (in background):
 *    a) Submit PWA for WebPerformance info
 *    b) Get Pwa Performance info
 *    c) Delete modified PWAs from cache
 *
 * @param {Pwa} pwa to update
 * @return {Pwa} the updated PWA
 */
exports.createOrUpdatePwa = function(pwa) {
  return promiseSequential.all([
    _ => (pwa),
    this.validatePwa,
    this.updatePwaManifest,
    this.savePwa,
    this.updatePwaIcon,
    this.savePwa,
    this.removePwaFromCache,
    savedPwa => {
      // In background
      libPwaIndex.updateSearchIndex(savedPwa);
      this.submitWebPageUrlForWebPerformanceInformation(savedPwa);
      this.getPwaPerformanceInfo(savedPwa);
      return savedPwa;
    }
  ]);
};

/**
 * Get Pwa Performance info
 *
 * @param {Pwa} pwa to update
 * @return {Promise<Array>}
 */
exports.getPwaPerformanceInfo = function(pwa) {
  let timeout = pwa.isNew() ? WAIT_TIME_NEW_PWAS : 0;
  setTimeout(_ => {
    return promiseSequential.all([
      _ => (pwa),
      this.updatePwaMetadataDescription,
      this.updatePwaLighthouseInfo,
      this.updatePwaPageSpeedInformation,
      this.updatePwaWebPageTestInformation,
      this.savePwa,
      this.removePwaFromCache,
      this.sendNewAppNotification
    ]);
  }, timeout);
};

/**
 * Remove PWA from cache
 *
 * @param {Pwa} pwa to remove
 * @return {Promise<Pwa>}
 */
exports.removePwaFromCache = function(pwa) {
  if (pwa.isNew()) {
    libCache.flushCacheUrls();
  }
  // Delete modified PWA from cache
  const url = '/pwas/' + pwa.id;
  libCache.del(url)
    .catch(err => {
      console.error(`Error removing ${url} from memcached`, err.message);
    });
  libCache.del(url + '?contentOnly=true')
    .catch(err => {
      console.error(`Error removing ${url} from memcached`, err.message);
    });
  return Promise.resolve(pwa);
};

/**
 * Validates PWA's data
 *
 * @param {Pwa} pwa to validate
 * @return {Promise<Pwa>} Promise with validated PWA or rejects with error
 */
exports.validatePwa = function(pwa) {
  if (!pwa || !(pwa instanceof Pwa)) {
    return Promise.reject(E_NOT_A_PWA);
  }
  if (!pwa.manifestUrl) {
    return Promise.reject(E_MANIFEST_URL_MISSING);
  }
  const manifestUrl = url.format(pwa.manifestUrl);
  if (!(manifestUrl.startsWith('http://') ||
        manifestUrl.startsWith('https://'))) {
    return Promise.reject(E_MANIFEST_INVALID_URL);
  }
  if (!pwa.user || !pwa.user.id) {
    return Promise.reject(E_MISSING_USER_INFORMATION);
  }
  return Promise.resolve(pwa);
};

/**
 * Fetches the manifest for a PWA using it's manifest URL
 * or the webpage's link rel=manifest
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Manifest>} with the manifest for the PWA
 */
exports.fetchManifest = function(pwa) {
  return libManifest.fetchManifest(pwa.manifestUrl)
    .then(manifest => {
      return manifest;
    })
    .catch(_ => {
      // if there is not a manifest in the pwa.manifestUrl
      // we check if it is a webpage with a link rel=manifest to the manifest
      return dataFetcher.fetchLinkRelManifestUrl(pwa.manifestUrl)
        .then(newManifestUrl => {
          // remove hash from url
          pwa.manifestUrl = newManifestUrl.replace(/#.*/, '');
          return libManifest.fetchManifest(newManifestUrl);
        })
        .catch(err => {
          console.log('Error while fetching the PWA manifest ' + err);
          return Promise.reject(err);
        });
    });
};

/**
 * Update PWA's Manifest.
 *
 * @param {Pwa} Pwa to update
 * @return {Pwa} the updated PWA
 */
exports.updatePwaManifest = function(pwa) {
  return libPwa.fetchManifest(pwa)
    .then(manifest => {
      return libPwa.findByManifestUrl(pwa.manifestUrl)
        .then(existingPwa => {
          if (existingPwa) {
            pwa = existingPwa;
            pwa.updated = new Date();
          }
          const validationErrors = libPwa.validateManifest(pwa, manifest);
          if (validationErrors.length > 0) {
            return Promise.reject('Error while validating the manifest: ' + validationErrors);
          }
          pwa.manifest = manifest;

          // Creates a encodedStartUrl for human readable URLs
          pwa.generateEncodedStartUrl();

          return pwa;
        });
    });
};

/**
 * Validate PWA's Manifest.
 *
 * @param {Pwa} Pwa to validate
 * @param {Manifest} Manifest to validate
 * @returns {errors[]} Return errors in an array
 */
exports.validateManifest = function(pwa, manifest) {
  return libManifest.validateManifest(
    manifest.raw, pwa.manifestUrl, pwa.absoluteStartUrl);
};

/**
 * Sends a push notification for new PWAs using Firebase Cloud Messaging.
 *
 * @param {Pwa} pwa to send the notification for
 * @return {Promise<Pwa>} with the notified PWA
 */
exports.sendNewAppNotification = function(pwa) {
  if (!pwa.isNew()) {
    return Promise.resolve(pwa);
  }
  console.log('Sending Notification for ', pwa.id);
  const clickAction = config.get('CANONICAL_ROOT') + 'pwas/' + pwa.id + '?utm_source=push';
  return notificationsLib.sendPush('new-apps', {
    title: pwa.name + ' added to PWA Directory',
    body: pwa.description || '',
    icon: pwa.iconUrl64 || '',
    click_action: clickAction // eslint-disable-line camelcase
  })
  .then(_ => {
    return pwa;
  })
  .catch(err => {
    console.log('Error while sending PWA Notification ' + err);
    return pwa;
  });
};

/**
 * Deletes a PWA from DB.
 *
 * @param {number} key of the PWA
 * @return {Promise<>}
 */
exports.delete = function(key) {
  return db.delete(ENTITY_NAME, key);
};

/**
 * Updates the description from the webpage's metadata if not present in the Manifest.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.updatePwaMetadataDescription = function(pwa) {
  return dataFetcher.fetchMetadataDescription(pwa.absoluteStartUrl)
    .then(metaDescription => {
      pwa.metaDescription = metaDescription;
      console.log('Updated PWA MetadataDescription: ', pwa.id);
      return pwa;
    })
    .catch(err => {
      console.log('Error while updating PWA MetadataDescription ' + err);
      return pwa;
    });
};

/**
 * Updates the main icon of a PWA.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.updatePwaIcon = function(pwa) {
  const bestIconUrl = pwa.manifest.getBestIconUrl();
  if (!bestIconUrl) {
    console.log('bestIconUrl is null');
    return Promise.resolve(pwa);
  }
  const extension = path.extname(url.parse(bestIconUrl).pathname);
  const bucketFileName = pwa.id + extension;
  return libImages.fetchAndSave(bestIconUrl, bucketFileName)
    .then(savedUrls => {
      pwa.iconUrl = savedUrls[0];
      pwa.iconUrl128 = savedUrls[1];
      pwa.iconUrl64 = savedUrls[2];
      console.log('Updated PWA Icon/Image: ', pwa.id);
      return pwa;
    })
    .catch(err => {
      console.error('Error while updating PWA Icon/Image ' + err);
      return pwa;
    });
};

/**
 * Submit WebPageUrl to WebPerformance service,
 * that service runs daily WebPageTest, PageSpeed and Lighthouse.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.submitWebPageUrlForWebPerformanceInformation = function(pwa) {
  return libWebPerformance.submitWebPageUrl(pwa)
    .then(result => {
      if (result.status === 200) {
        console.log('Submited PWA for WebPerformance info: ', pwa.id);
      } else {
        console.log('Error while submiting PWA for WebPerformance information: ' +
          pwa.id + ' ' + JSON.stringify(result));
      }
      return pwa;
    })
    .catch(err => {
      console.log(
        'Error while submiting PWA for WebPerformance information: ' + pwa.id + ' ' + err);
      return pwa;
    });
};

/**
 * Updates the Lighthouse information.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.updatePwaLighthouseInfo = function(pwa) {
  return libWebPerformance.getLighthouseReport(pwa)
    .then(lighthouseJson => {
      // We are not using the full rawData or rawDataBlob report anymore
      delete lighthouseJson[0].rawData;
      delete lighthouseJson[0].rawDataBlob;
      pwa.lighthouseScore = Math.round(lighthouseJson[0].pwaScore);
      pwa.lighthouse = lighthouseJson[0];
      pwa.installable = lighthouseJson[0].installable;
      console.log('Updated PWA Lighthouse info for: ', pwa.id);
      return pwa;
    })
    .catch(err => {
      console.log('Error while updating PWA Lighthouse information ' + err);
      return pwa;
    });
};

/**
 * Update PageSpeed information.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.updatePwaPageSpeedInformation = function(pwa) {
  return libWebPerformance.getPageSpeedReport(pwa)
    .then(pageSpeedJson => {
      console.log('Updated PWA PageSpeed info: ', pwa.id);
      pwa.pageSpeed = pageSpeedJson[0];
      return pwa;
    })
    .catch(err => {
      console.log('Error while updating PageSpeed information: ' + pwa.id + ' ' + err);
      return pwa;
    });
};

/**
 * Update WebPageTest information.
 *
 * @param {Pwa} the PWA to update
 * @return {Promise<Pwa>} with the updated PWA
 */
exports.updatePwaWebPageTestInformation = function(pwa) {
  return libWebPerformance.getWebPageTestReport(pwa)
    .then(json => {
      console.log('Updated PWA WebPageTest info: ', pwa.id);
      let webPageTestJson = json[0];
      // remove rawFirstViewData to make the field smaller than 1500 bytes
      webPageTestJson.rawFirstViewData = null;
      pwa.webPageTest = webPageTestJson;
      return pwa;
    })
    .catch(err => {
      console.log('Error while updating WebPageTest information: ' + pwa.id + ' ' + err);
      return pwa;
    });
};

exports.count = function() {
  return db.count(ENTITY_NAME);
};


================================================
FILE: lib/search.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 URL = require('url');
const elasticlunr = require('elasticlunr');
const libCache = require('../lib/data-cache');
const libPwaIndex = require('../lib/pwa-index');

const CACHE_LIFETIME = 60 * 60 * 24 * 7; // 7 days
const SEARCH_INDEX_CHANGE = 'SearchIndexChange';

/**
 * Search class for the elasticlunr functions
 *
 * Exports a singleton object instance
 */
class Search {

  constructor() {
    this._initIndex();
  }

  _initIndex() {
    this._index = elasticlunr(function() {
      this.setRef('id');
      this.addField('displayName');
      this.addField('urlText');
    });
    this._modified = new Date();
  }

  /**
   * Create a doc element from a PWA.
   *
   * @param {PWA} PWA to index
   * @return {doc} a doc for the text search engine
   */
  _docFromPwa(pwa) {
    const url = URL.parse(pwa.absoluteStartUrl);
    const urlText = url.hostname.replace(/\.|-/g, ' ');
    return {
      id: pwa.id,
      displayName: pwa.displayName,
      urlText: urlText
    };
  }

  /**
   * Add a PWA to the search index.
   *
   * @param {PWA} PWA to index
   * @return {Promise<doc>} the doc added to the text search engine
   */
  addPwa(pwa) {
    const doc = this._addPwa(pwa);
    this.sarchIndexChange();
    return Promise.resolve(doc);
  }

  /**
   * Add a list of PWA to the search index.
   *
   * @param {PWA[]} PWAs to index
   * @return {Promise<>}
   */
  addPwas(pwas) {
    pwas.forEach(pwa => this._addPwa(pwa));
    this.sarchIndexChange();
    return Promise.resolve();
  }

  _addPwa(pwa) {
    const doc = this._docFromPwa(pwa);
    this._index.addDoc(doc);
    return doc;
  }

  /**
   * Update a PWA on the search index.
   *
   * @param {PWA} PWA to update
   * @return {Promise<doc>} the doc updated on the text search engine
   */
  updatePwa(pwa) {
    const doc = this._docFromPwa(pwa);
    this._index.updateDoc(doc);
    this.sarchIndexChange();
    return Promise.resolve(doc);
  }

  /**
   * Remove a PWA from the search index.
   *
   * @param {PWA} PWA to remove
   * @return {Promise<doc>} the doc removed from the text search engine
   */
  removePwa(pwa) {
    const doc = this._docFromPwa(pwa);
    this._index.removeDoc(doc);
    this.sarchIndexChange();
    return Promise.resolve(doc);
  }

  /**
   * Search the text index.
   *
   * @param {string} query
   * @return {Promise<json>} with the matching PWA Ids and scores
   *
   * [{
   *    "ref": 123456789,
   *    "score": 0.5376053707962494
   *  },
   *  {
   *    "ref": 456789012,
   *    "score": 0.5237481076838757
   * }]
   */
  search(string) {
    const options = {expand: true};
    const result = this._index.search(string, options);
    // Update the search index for the next query
    this.checkForSearchIndexChange();
    return Promise.resolve(result);
  }

  /**
   * Record a change in the search index in memcached.
   *
   * @param {date} optional date
   */
  sarchIndexChange(date = new Date()) {
    libCache.set(SEARCH_INDEX_CHANGE, date, CACHE_LIFETIME)
      .catch(err => console.error('sarchIndexChange error', err.message));
  }

  /**
   * Check of the latest change in the search index and updete if needed.
   *
   * @param {date} optional date
   */
  checkForSearchIndexChange() {
    libCache.get(SEARCH_INDEX_CHANGE).then(lastChange => {
      lastChange = new Date(lastChange);
      if (lastChange && lastChange > this._modified) {
        console.log('Re-index PWAs');
        // Invalidate index and re-index all PWAs
        this._initIndex();
        libPwaIndex.indexAllPwas().then(_ => {
          const newDate = new Date();
          this._modified = newDate;
          this.sarchIndexChange(newDate);
        });
      }
    });
  }
}

// Export Search as a singleton object
module.exports = new Search();


================================================
FILE: lib/tasks.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 db = require('../lib/model-datastore');
const Task = require('../models/task');
const pwaLib = require('../lib/pwa');
const tasksLib = require('../lib/tasks');

const ENTITY_NAME = 'Task';
const E_SAVING_TASK = exports.E_SAVING_TASK = 1;
const E_GET_TASK_POP = exports.E_GET_TASK_POP = 2;
const E_DELETE_TASK_POP = exports.E_DELETE_TASK_POP = 3;

/**
 * Push a Task object into the DB.
 *
 * @param {Task} lighthouse
 * @return {Promise<Task>}
 */
exports.push = function(task) {
  return new Promise((resolve, reject) => {
    db.update(ENTITY_NAME, task.id, task)
      .then(result => {
        return resolve(result);
      })
      .catch(err => {
        console.error(err);
        return reject(E_SAVING_TASK);
      });
  });
};

exports.getTasks = async function(numTasks) {
  const result =
      await db.list(ENTITY_NAME, 0, numTasks, {field: 'created', config: {ascending: true}});
  const tasks = [];
  for (let entity of result.entities) {
    tasks.push(Object.assign(new Task(), entity));
  }
  return tasks;
};

exports.deleteTask = async function(taskId) {
  await db.delete(ENTITY_NAME, taskId);
};

/**
 * Pop the oldest Task
 *
 * @return {Promise<Task>}
 */
exports.pop = function() {
  return new Promise((resolve, reject) => {
    db.list(ENTITY_NAME, 0, 1, {field: 'created', config: {ascending: true}})
      .then(result => {
        if (result.entities.length === 0) {
          return resolve(null);
        }
        let task = Object.assign(new Task(), result.entities[0]);
        db.delete(ENTITY_NAME, task.id)
          .then(_ => {
            return resolve(task);
          }).catch(err => {
            console.error('Error deleting task', err);
            return reject(E_DELETE_TASK_POP);
          });
      })
      .catch(_ => {
        return reject(E_GET_TASK_POP);
      });
  });
};

/**
 * Execute a task
 *
 * @return {Promise<Task>}
 */
exports.executePwaTask = function(task) {
  if (!task) {
    return Promise.resolve();
  }
  return pwaLib.find(task.pwaId)
    .then(pwa => {
      // Dynamically get module and function to execute from task with a PWA
      // const moduleFromTask = require(task.modulePath);
      const moduleFromTask = require('../lib/pwa');
      const functionFromTask = Reflect.get(moduleFromTask, task.functionName);
      return functionFromTask.call(moduleFromTask, pwa)
        .then(_ => {
          return task;
        })
        .catch(err => {
          console.error('Error running task: ' + err);
          task.retries -= 1;
          if (task.retries >= 0) {
            tasksLib.push(task);
          }
          return task;
        });
    })
    .catch(err => {
      console.error(err);
    });
};

/**
 * Pop the oldest Task and execute it
 *
 * @return {Promise<Task | null>}
 */
exports.popExecute = function() {
  return tasksLib.pop()
    .then(task => {
      if (task) {
        return tasksLib.executePwaTask(task);
      }
      return null;
    });
};


================================================
FILE: lib/verify-id-token.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');
const CLIENT_ID = config.get('CLIENT_ID');
const CLIENT_SECRET = config.get('CLIENT_SECRET');

/**
 * @param {string} idToken
 * @return {Promise<GoogleLogin>}
 */
exports.verifyIdToken = function(idToken) {
  const {OAuth2Client} = require('google-auth-library');
  const client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET);
  return new Promise((resolve, reject) => {
    client.verifyIdToken({idToken, CLIENT_ID}, (err, googleLogin) => {
      if (err) {
        reject(err);
      }
      resolve(googleLogin);
    });
  });
};


================================================
FILE: lib/web-performance.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 config = require('../config/config');
const dataFetcher = require('../lib/data-fetcher');

const WEBPERFORMANCE_SERVER_URL = config.get('WEBPERFORMANCE_SERVER');
const WEBPERFORMANCE_SERVER_API_KEY = config.get('WEBPERFORMANCE_SERVER_API_KEY');
const WEBPERFORMANCE_SERVER_WEBPAGEURL = WEBPERFORMANCE_SERVER_URL + 'webpageurl';
const WEBPERFORMANCE_SERVER_PAGESPEED_REPORT = WEBPERFORMANCE_SERVER_URL + 'pagespeedreport/';
const WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT = WEBPERFORMANCE_SERVER_URL + 'webpagetestreport/';
const WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT = WEBPERFORMANCE_SERVER_URL + 'lighthousereport/';

function submitToWebPerformanceService(pwa) {
  const body = {
    id: pwa.id,
    url: pwa.absoluteStartUrl,
    source: 'pwa-directory',
    description: pwa.description,
    created: pwa.created
  };
  return dataFetcher.postJson(
    WEBPERFORMANCE_SERVER_WEBPAGEURL + '?key=' + WEBPERFORMANCE_SERVER_API_KEY, body);
}

/**
 * Submit PWA to the WebPerformance service.
 *
 * @param {number} a PWA
 * @return {Promise<Lighthouse>}
 */
exports.submitWebPageUrl = function(pwa) {
  return new Promise((resolve, reject) => {
    submitToWebPerformanceService(pwa)
      .then(result => {
        return resolve(result);
      })
      .catch(err => {
        return reject(err);
      });
  });
};

/**
 * Get Report for PWA.
 *
 * @param {PWA} a PWA
 * @return {Promise<Json>}
 */
function getReport(url) {
  return dataFetcher.fetchWithUA(url)
    .then(response => {
      if (response.status === 200) {
        return response.json();
      } else if (response.status === 404) {
        return Promise.reject('not available yet');
      }
      return Promise.reject(response);
    })
    .catch(err => {
      return Promise.reject(err);
    });
}

/**
 * Get PageSpeed Report for PWA.
 *
 * @param {PWA} a PWA
 * @return {Promise<Json>}
 */
exports.getPageSpeedReport = function(pwa) {
  return getReport(WEBPERFORMANCE_SERVER_PAGESPEED_REPORT + pwa.id + '?limit=1');
};

/**
 * Get WebPageTest Report for PWA.
 *
 * @param {PWA} a PWA
 * @return {Promise<Json>}
 */
exports.getWebPageTestReport = function(pwa) {
  return getReport(WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT + pwa.id + '?limit=1');
};

/**
 * Get Lighthouse Report for PWA.
 *
 * @param {PWA} a PWA
 * @return {Promise<Json>}
 */
exports.getLighthouseReport = function(pwa) {
  return getReport(WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT + pwa.id + '?limit=1');
};


================================================
FILE: lighthouse_machine/.dockerignore
================================================
.gitignore
.git
outputs

================================================
FILE: lighthouse_machine/.eslintrc.json
================================================
{
  "extends": "google",
  "installedESLint": true,
  // http://eslint.org/docs/rules/
  "rules": {
    "max-len": [2, 100, {
      "ignoreComments": true,
      "ignoreUrls": true,
      "tabWidth": 2
    }],
    "no-implicit-coercion": [2, {
      "boolean": false,
      "number": true,
      "string": true
    }],
    "no-unused-expressions": [2, {
      "allowShortCircuit": true,
      "allowTernary": false
    }],
    "no-unused-vars": [2, {
      "vars": "all",
      "args": "after-used",
      "argsIgnorePattern": "(^reject$|^_$)",
      "varsIgnorePattern": "(^_$)"
    }],
    "quotes": [2, "single"],
    "require-jsdoc": 0,
    "valid-jsdoc": 0,
    "prefer-arrow-callback": 1,
    "no-var": 1
  },
  // http://eslint.org/docs/user-guide/configuring#specifying-environments
  "env": {
    "node": true
  }
}


================================================
FILE: lighthouse_machine/.gitignore
================================================
outputs
node_modules


================================================
FILE: lighthouse_machine/Dockerfile
================================================
#	Copyright 2016-2017, Google, Inc.
# 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.

FROM ubuntu:latest

## PART 1: Core components
## =======================

# Install utilities
RUN apt-get update --fix-missing && apt-get -y upgrade &&\
apt-get install -y sudo apt-utils curl wget unzip git gnupg

# Install node 10
RUN curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - &&\
sudo apt-get install -y nodejs

# Install Xvfb and dbus for X11
RUN apt-get install -y xvfb dbus-x11

# Install Chrome for Ubuntu
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - &&\
sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' &&\
sudo apt-get update &&\
sudo apt-get install -y google-chrome-stable

# Install Yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list &&\
sudo apt-get update && sudo apt-get install yarn

# Copy key documents (except .dockerignored files)
COPY etc/xvfb /etc/init.d/xvfb
RUN chmod +x /etc/init.d/xvfb

# Add a user and make it a sudo user
RUN useradd -m chromeuser

# Copy the chrome-user script used to start Chrome as non-root
COPY chromeuser-script.sh /
RUN chmod +x /chromeuser-script.sh

## PART 2: Lighthouse
## ==================

# Download lighthouse
RUN git clone https://github.com/googlechrome/lighthouse &&\
cd /lighthouse &&\
git checkout tags/v4.2.0 &&\
npm install -g yarn &&\
npm install -g yarnpkg &&\
npm install -g @types/mkdirp &&\
npm install -g --save-dev run-sequence &&\
npm install -g typescript &&\
npm install -g &&\
yarn global add lighthouse

## PART 3: Express server
## ======================

# Install express
COPY package.json /
RUN npm install

# Add the simple server file
COPY server.js /
RUN chmod +x /server.js

# Add the cpu monitor file
COPY cpu_monitor.js /
RUN chmod +x /cpu_monitor.js

# Generate a self-signed SSL certificate
RUN openssl req \
  -new \
  -newkey rsa:4096 \
  -days 365 \
  -nodes \
  -x509 \
  -subj "/C=GB/ST=None/L=None/O=Google/CN=lighthouse-machine-X" \
  -keyout key.pem \
  -out cert.pem

# Expose ports 8080 and 8443
EXPOSE 8080
EXPOSE 8443

## PART 4: Final setup
## ===================

# Set the entrypoint
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]


================================================
FILE: lighthouse_machine/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 communiampion sent
      to the Licensor or its representatives, including but not limited to
      communiampion 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 communiampion 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 2015, Google Inc.

   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: lighthouse_machine/README.md
================================================
# Lighthouse machine
A Docker image to run [Lighthouse](https://github.com/GoogleChrome/lighthouse) scores on a server

## Build the image
```bash
docker build --no-cache -t lighthouse_machine .
```

## Run the container
```bash
# Run a new container
docker run -d -p 8080:8080 --cap-add=SYS_ADMIN lighthouse_machine
```

## Usage
```bash
curl -X GET 'http://localhost:8080?format=${format}&url=${url}'
```

where `format`is one of `json`, `html` (see [cli-options](https://github.com/GoogleChrome/lighthouse#cli-options) for more information)

## License
See [LICENSE](./LICENSE) for more.

## Disclaimer
This is not a Google product.


================================================
FILE: lighthouse_machine/app.yaml
================================================
#	Copyright 2016-2017, Google, Inc.
# 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.

runtime: custom
env: flex
service: lighthouse-machine
automatic_scaling:
  min_num_instances: 2
  max_num_instances: 6
  cool_down_period_sec: 60
  cpu_utilization:
    target_utilization: 0.6

resources:
  cpu: 1
  memory_gb: 4
  disk_size_gb: 10

handlers:
- url: /.*
  script: IGNORED
  secure: always

liveness_check:
   path: '/_ah/health'
   check_interval_sec: 30
   timeout_sec: 4
   failure_threshold: 3
   success_threshold: 2
   initial_delay_sec: 60

readiness_check:
  path: '/_ah/busy'
  check_interval_sec: 3
  timeout_sec: 2
  failure_threshold: 1
  success_threshold: 1
  app_start_timeout_sec: 300

network:
  instance_tag: lighthouse-machine


================================================
FILE: lighthouse_machine/chromeuser-script.sh
================================================
#!/bin/bash

# Copyright 2016-2017, Google, Inc.
# 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.

sudo chown -R chromeuser:chromeuser $TMP_PROFILE_DIR
export DISPLAY=:0
Xvfb :0 -screen 0 1024x768x24 &
nohup google-chrome --no-first-run --disable-gpu --no-sandbox --user-data-dir=$TMP_PROFILE_DIR --remote-debugging-port=9222 'about:blank' &


================================================
FILE: lighthouse_machine/cpu_monitor.js
================================================
/**
 * Copyright 2016-2017, Google, Inc.
 * 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 os = require('os');

// Create function to get CPU information
function cpuAverage() {
  // Initialise sum of idle and time of cores and fetch CPU info
  let totalIdle = 0;
  let totalTick = 0;
  let cpus = os.cpus();

  // Loop through CPU cores
  for (let i = 0, len = cpus.length; i < len; i++) {
    // Select CPU core
    let cpu = cpus[i];

    // Total up the time in the cores tick
    // eslint-disable-next-line guard-for-in
    for (let type in cpu.times) {
      totalTick += cpu.times[type];
    }

    // Total up the idle time of the core
    totalIdle += cpu.times.idle;
  }

  // Return the average Idle and Tick times
  return {idle: totalIdle / cpus.length, total: totalTick / cpus.length};
}

module.exports = (avgTime, callback) => {
  this.samples = [];
  this.samples[1] = cpuAverage();
  this.refresh = setInterval(() => {
    this.samples[0] = this.samples[1];
    this.samples[1] = cpuAverage();
    let totalDiff = this.samples[1].total - this.samples[0].total;
    let idleDiff = this.samples[1].idle - this.samples[0].idle;
    callback(1 - idleDiff / totalDiff);
  }, avgTime);
};


================================================
FILE: lighthouse_machine/entrypoint.sh
================================================
#!/bin/bash

# Copyright 2016-2017, Google, Inc.
# 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.

/etc/init.d/dbus start
/etc/init.d/xvfb start
sleep 1s

export DISPLAY=:1
TMP_PROFILE_DIR=$(mktemp -d -t lighthouse.XXXXXXXXXX)

su chromeuser
source /chromeuser-script.sh
sleep 3s

node /server.js


================================================
FILE: lighthouse_machine/etc/xvfb
================================================
#!/bin/bash

# Copyright 2016-2017, Google, Inc.
# 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.

XVFB_OUTPUT=/tmp/Xvfb.out
XVFB=/usr/bin/X11/Xvfb
XVFB_OPTIONS=":1 -screen 0 1024x768x24 -fbdir /var/run"

start()  {
echo -n "Starting : X Virtual Frame Buffer "
$XVFB $XVFB_OPTIONS >>$XVFB_OUTPUT 2>&1&
RETVAL=$?
echo
return $RETVAL
}

stop()   {
echo -n "Shutting down : X Virtual Frame Buffer"
echo
pkill Xvfb
echo
return 0
}

case "$1" in
start)
start
;;
stop)
stop
;;
status)
status xvfb
;;
restart)
    stop
    start
    ;;

*)
echo "Usage: xvfb {start|stop|status|restart}"
exit 1
;;
esac
exit $?


================================================
FILE: lighthouse_machine/package.json
================================================
{
  "name": "lighthouse_machine_server",
  "version": "1.0.0",
  "description": "A server for the lighthouse machine",
  "repository": "https://github.com/GoogleChrome/gulliver/lighthouse_machine",
  "author": "Google Inc.",
  "contributors": [
    {
      "name": "Cedric Bellet",
      "email": "cbellet@google.com"
    },
    {
      "name": "Julian Toledo",
      "email": "jtoledo@google.com"
    }
  ],
  "license": "Apache-2.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.16.3"
  }
}


================================================
FILE: lighthouse_machine/server.js
================================================
/**
 * Copyright 2016-2017, Google, Inc.
 * 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 express = require('express');
const exec = require('child_process').exec;
const http = require('http');
const https = require('https');
const fs = require('fs');
const cpuMonitor = require('./cpu_monitor');

// Chrome panick
let chromePanick = false;

// CPU monitoring
let cpuPoints = new Array(5);
let cpuAlert = false;

cpuMonitor(60000, load => {
  // Add new measurements to the cpuPoints array
  cpuPoints.pop();
  cpuPoints.unshift(load);

  // Calculate the avg and spread of the cpuPoints array
  let sum = 0;
  let i = 5;
  while (i--) sum += cpuPoints[i];
  let avg = sum / 5;
  let spread = Math.max.apply(Math, cpuPoints) - Math.min.apply(Math, cpuPoints);

  // If the CPU load is above 80% and the spread is less than 10%, trigger an alert
  cpuAlert = (avg > 0.8 && spread < 0.1);
  cpuAlert && console.log(`Average: ${avg}, Spread: ${spread}`);
});

// Constants
const HTTP_PORT = 8080;
const HTTPS_PORT = 8443;

// HTTPS options
const options = {
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

// App
const app = express();
let isBusy = false;

// Main endpoint
app.get('/', (req, res) => {
  if (isBusy) {
    res.sendStatus(429);
  } else {
    isBusy = true;
    res.setTimeout(500000, _ => {
      console.log('Request has timed out.');
      res.send(408);
    });
    try {
      exec(
        `lighthouse '${req.query.url}' --port 9222 --output-path ../report.${req.query.format} --output ${req.query.format}`,
        {
          cwd: '/lighthouse',
          timeout: 500000
        },
        error => {
          if (error !== null) {
            console.log(`exec error: ${error}`);

            // This is for when Chrome crashes and Lighthouse is unable to reconnect
            // to an appropriate instance of Chrome
            if (error.message.includes('Unable to connect')) {
              chromePanick = true;
            }
          }

          isBusy = false;
          res.sendFile(`/report.${req.query.format}`);
        }
      );
    } catch (e) {
      isBusy = false;
      res.status(500).send(e);
    }
  }
});

// Auto-healing endpoint
app.get('/_ah/health', (req, res) => {
  if (chromePanick) {
    // If we have a Chrome panick send a 500
    res.sendStatus(500);
  } else if (cpuAlert) {
    // if we have a CPU alert send a 500, otherwise send a 200
    res.sendStatus(500);
  } else {
    res.sendStatus(200);
  }
});

// Busy-ness endpoit
app.get('/_ah/busy', (req, res) => {
  if (isBusy) {
    res.sendStatus(503);
  } else {
    res.sendStatus(200);
  }
});

http.createServer(app).listen(HTTP_PORT);
https.createServer(options, app).listen(HTTPS_PORT);

console.log(
  `Running on https://localhost:${HTTPS_PORT} and http://localhost:${HTTP_PORT}`
);


================================================
FILE: middlewares/index.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 express = require('express');
const asset = require('../lib/asset-hashing').asset;
const router = express.Router(); // eslint-disable-line new-cap
const CSSPATH = asset.encode('/css/style.css');
const JSPATH = asset.encode('/js/gulliver.js');

router.use((req, res, next) => {
  res.setHeader('Content-Type', 'text/html');

  /* eslint-disable quotes */
  res.setHeader('content-security-policy', [
    `connect-src 'self' https://www.google-analytics.com https://web-performance-dot-pwa-directory.appspot.com https://fcm.googleapis.com`,
    `default-src 'self' https://accounts.google.com https://apis.google.com https://fcm.googleapis.com`,
    `script-src 'self' 'unsafe-eval' https://apis.google.com https://www.google-analytics.com https://www.gstatic.com`,
    `style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ https://www.gstatic.com`,
    `font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/`,
    `img-src 'self' https://storage.googleapis.com https://www.google-analytics.com`
  ].join('; '));
  /* eslint-enable quotes */
  res.setHeader('x-content-type-options', 'nosniff');
  res.setHeader('x-dns-prefetch-control', 'off');
  res.setHeader('x-download-options', 'noopen');
  res.setHeader('x-frame-options', 'SAMEORIGIN');
  res.setHeader('x-xss-protection', '1; mode=block');

  // Set the preload header if a full render is being requested.
  if (!req.query.contentOnly && !req.originalUrl.startsWith('/.app/')) {
    res.setHeader('Link',
      `<${CSSPATH}>; rel=preload; as=style, <${JSPATH}>; rel=preload; as=script`);
  }
  next();
});

module.exports = router;


================================================
FILE: models/favorite-pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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';

/**
 * Favorite Pwa for a user
 */
class FavoritePwa {
  constructor(pwaId, userId) {
    this.id = pwaId + '-' + userId;
    this.pwaId = pwaId;
    this.userId = userId;
  }
}

module.exports = FavoritePwa;


================================================
FILE: models/lighthouse.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 libLighthouse = require('../lib/lighthouse');

/**
 * Class representing a Lighthouse report for a PWA
 *
 * absoluteStartUrl is the absoluteStartUrl of the PWA
 * lighthouseJson is the Lighthouse's report as JSON object
 */
class Lighthouse {
  constructor(pwaId, absoluteStartUrl, lighthouseJson) {
    this.pwaId = pwaId;
    this.absoluteStartUrl = absoluteStartUrl;
    this._lighthouseJson = parseToJson(lighthouseJson);
    this.lighthouseInfo = libLighthouse.processLighthouseJson(this._lighthouseJson);
    this.totalScore = this.lighthouseInfo.totalScore;
    this.lighthouseVersion = this.lighthouseInfo.lighthouseVersion;
    this.date = (new Date()).toISOString().slice(0, 10);
    this.id = this.pwaId + '-' + this.date;
  }

  get lighthouseJson() {
    return this._lighthouseJson;
  }

  set lighthouseJson(value) {
    // lighthouseJson is stored as a string in the datastore
    this._lighthouseJson = parseToJson(value);
  }
}

function parseToJson(value) {
  if (value && Object.prototype.toString.call(value) === '[object String]') {
    return JSON.parse(value);
  }
  return value;
}

module.exports = Lighthouse;


================================================
FILE: models/manifest.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 url = require('url');

/**
 * Class representing a Web App Manifest
 */
class Manifest {
  constructor(manifestUrl, jsonManifest) {
    this.url = manifestUrl;
    this.raw = JSON.stringify(jsonManifest);
    this.name = jsonManifest.name;
    this.shortName = jsonManifest.short_name;
    this.description = jsonManifest.description;
    this.startUrl = jsonManifest.start_url;
    this.backgroundColor = jsonManifest.background_color;
    this.icons = jsonManifest.icons;
    this.scope = jsonManifest.scope;
  }

  getBestIcon() {
    function getIconSize(icon) {
      if (!icon.sizes) {
        return 0;
      }
      return parseInt(icon.sizes.substring(0, icon.sizes.indexOf('x')), 10);
    }

    if (!this.icons) {
      return null;
    }

    let bestIcon;
    let bestIconSize;

    for (let icon of this.icons) {
      if (!bestIcon) {
        bestIcon = icon;
        bestIconSize = getIconSize(icon);
      }

      const iconSize = getIconSize(icon);
      if (iconSize > bestIconSize) {
        bestIcon = icon;
        bestIconSize = iconSize;
      }

      // We can return 128 and 144 even if there are bigger ones.
      if (iconSize === 128 || iconSize === 144) {
        return icon;
      }
    }
    return bestIcon;
  }

  /** Gets the Url for the largest icon in the Manifest */
  getBestIconUrl() {
    let bestIcon = this.getBestIcon();
    if (!bestIcon || !bestIcon.src) {
      return '';
    }
    return url.resolve(this.url, bestIcon.src);
  }
}

module.exports = Manifest;


================================================
FILE: models/pwa.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 uri = require('urijs');
const URL = require('url');
const Manifest = require('../models/manifest');
const User = require('../models/user');

class Pwa {
  constructor(manifestUrl, manifestModel) {
    // remove hash from url
    manifestUrl && (this.manifestUrl = removeHash(manifestUrl));
    this._manifest = stringifyManifestIfNeeded(manifestModel);
    this.created = new Date();
    this.updated = this.created;
    this.visible = true;
  }

  get shortName() {
    if (!this.manifest) {
      return '';
    }
    return this.manifest.shortName || '';
  }

  get name() {
    if (!this.manifest) {
      return '';
    }
    return this.manifest.name || '';
  }

  get displayName() {
    return this.name ||
      this.shortName ||
      trimManifestFile(this.manifestUrl);
  }

  get description() {
    if (this.manifest && this.manifest.description) {
      return this.manifest.description;
    }

    return this.metaDescription || '';
  }

  get startUrl() {
    if (!this.manifest) {
      return '';
    }
    return this.manifest.startUrl || '';
  }

  get absoluteStartUrl() {
    if (!this.manifestUrl) {
      return '';
    }

    const startUrl = this.startUrl || '/';
    return this._cleanUrl(uri(startUrl).absoluteTo(this.manifestUrl).toString());
  }

  get backgroundColor() {
    if (!this.manifest) {
      return '#ffffff';
    }

    return this.manifest.backgroundColor || '#ffffff';
  }

  get manifest() {
    if (!this._manifest) {
      return null;
    }
    return new Manifest(this.manifestUrl, JSON.parse(this._manifest));
  }

  set manifest(value) {
    if (value && typeof value === 'object') {
      this._manifest = value.raw;
    } else {
      this._manifest = value;
    }
  }

  get manifestAsString() {
    return this._manifest;
  }

  setUser(user) {
    this.user = new User(user);
  }

  generateEncodedStartUrl() {
    const parsedUrl = URL.parse(this.absoluteStartUrl);
    this.encodedStartUrl = encodeURIComponent(parsedUrl.hostname + parsedUrl.pathname);
    return this.encodedStartUrl;
  }

  isNew() {
    return this.created === this.updated;
  }

  _cleanUrl(input) {
    const url = new URL.URL(input);
    for (const name of url.searchParams.keys()) {
      if (name.toLowerCase().startsWith('utm_')) {
        url.searchParams.delete(name);
      }
    }
    return url.toString();
  }
}

function trimManifestFile(url) {
  let startIndex = url.indexOf('//');
  if (startIndex === -1) {
    startIndex = 0;
  } else {
    startIndex += 2;
  }
  let endIndex = url.lastIndexOf('/');
  if (endIndex === -1) {
    endIndex = url.length;
  }
  return url.substring(startIndex, endIndex);
}

function stringifyManifestIfNeeded(manifest) {
  if (manifest && typeof manifest === 'object') {
    return manifest.raw;
  }
  return manifest;
}

function removeHash(urlString) {
  const url = URL.parse(urlString);
  url.hash = '';
  return url.format();
}

module.exports = Pwa;


================================================
FILE: models/task.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 Task {
  constructor(pwaId, modulePath, functionName, retries) {
    this.pwaId = pwaId;
    this.modulePath = modulePath;
    this.functionName = functionName;
    this.retries = retries;
    this.created = new Date();
  }
}

module.exports = Task;


================================================
FILE: models/user.js
================================================
/**
 * Copyright 2015-2016, Google, Inc.
 * 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 crypto = require('crypto');

/**
 * User from google-auth-library-nodejs client
 */
class User {
  constructor(googleLogin) {
    this.id = crypto.createHash('sha1').update(googleLogin.getPayload().sub).digest('hex');
  }
}

module.exports = User;


================================================
FILE: package.json
================================================
{
  "name": "gulliver",
  "version": "1.0.0",
  "description": "A directory of PWAs",
  "repository": "https://github.com/GoogleChrome/gulliver",
  "private": true,
  "scripts": {
    "start": "node app.js",
    "prestart": "BABEL_ENV=default rollup -c rollup-config/gulliver.js && npm run generate-msg-sw",
    "monitor": "nodemon app.js",
    "deploy": "npm run prestart && gcloud app deploy app.yaml",
    "mocha-app": "_mocha test/app/**/* --exit",
    "mocha-client": "BABEL_ENV=test _mocha --compilers js:babel-core/register test/client/**/*.js",
    "coverage": "istanbul cover _mocha --compilers js:babel-core/register test/app/**/*",
    "lint": "eslint .",
    "test": "npm run lint && npm run mocha-client && npm run mocha-app",
    "generate-msg-sw": "node firebase-messaging-sw-generator.js",
    "lint-fix": "eslint --fix ."
  },
  "author": "Google Inc.",
  "contributors": [
    {
      "name": "Julian Toledo",
      "email": "jtoledo@google.com"
    },
    {
      "name": "Michael Stillwell",
      "email": "stillers@google.com"
    },
    {
      "name": "Andre Bandarra",
      "email": "andreban@google.com"
    }
  ],
  "license": "Apache Version 2.0",
  "semistandard": {
    "globals": [
      "after",
      "afterEach",
      "before",
      "beforeEach",
      "describe",
      "it"
    ]
  },
  "engines": {
    "nodejs8": "8.12.0"
  },
  "dependencies": {
    "@google-cloud/datastore": "^1.4.2",
    "@google-cloud/storage": "^1.7.0",
    "babel-preset-es2015-rollup": "^3.0.0",
    "body-parser": "^1.18.3",
    "cheerio": "^0.22.0",
    "compression": "^1.7.3",
    "elasticlunr": "^0.9.5",
    "escape-html": "^1.0.3",
    "express": "^4.16.4",
    "express-csv": "^0.6.0",
    "express-minify-html": "^0.12.0",
    "express-sslify": "^1.2.0",
    "firebase": "^5.5.9",
    "google-auth-library": "^1.6.1",
    "handlebars": "^4.5.3",
    "hbs": "^4.0.4",
    "http-parser-js": "^0.4.13",
    "jsdom": "^9.5.0",
    "lodash.merge": "^4.6.2",
    "lodash.template": "^4.5.0",
    "memcached": "^2.2.2",
    "mime-types": "^2.1.21",
    "moment": "^2.22.2",
    "multer": "^1.4.1",
    "nconf": "^0.8.4",
    "node-fetch": "^2.6.1",
    "parse-color": "^1.0.0",
    "request": "^2.88.0",
    "rev-hash": "^1.0.0",
    "rollup": "^0.58.2",
    "rollup-plugin-babel": "^2.6.1",
    "rollup-plugin-commonjs": "^9.2.0",
    "rollup-plugin-node-resolve": "^2.0.0",
    "rollup-plugin-uglify": "^1.0.1",
    "rss": "^1.2.2",
    "serve-static": "^1.11.1",
    "sharp": "^0.17.0",
    "spdy": "^3.4.7",
    "strong-data-uri": "^1.0.6",
    "sw-offline-google-analytics": "^1.1.1",
    "sw-toolbox": "^3.2.1",
    "urijs": "^1.18.1",
    "url-polyfill": "^1.1.0",
    "whatwg-fetch": "^2.0.1",
    "yaku": "^0.17.6"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.24.1",
    "chai": "^3.0.0",
    "chai-as-promised": "^6.0.0",
    "eslint": "^6.6.0",
    "eslint-config-google": "^0.6.0",
    "istanbul": "^0.4.4",
    "mocha": "^5.2.0",
    "node-mocks-http": "^1.7.3",
    "simple-mock": "^0.7.0",
    "supertest": "^3.3.0"
  }
}


================================================
FILE: public/.well-known/assetlinks.json
================================================
[{
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.appspot.pwa_directory",
      "sha256_cert_fingerprints": ["1A:64:23:29:C2:BB:FA:18:45:A3:BE:02:08:DD:B4:8F:51:21:F9:2E:95:75:75:CA:2B:8B:47:75:94:C5:0F:64"]}
  }]


================================================
FILE: public/css/style.css
================================================
/* Copyright 2015-2016, Google, Inc.
 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 {
  height: 100%;
  box-sizing: border-box;
  overflow-x: hidden;
}
*,
*:before,
*:after {
  box-sizing: inherit;
  /* don't show Chrome's default blue tap highlight */
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

body {
  font-size: 16px;
  font-weight: 300;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  width: 100%;
  min-width: 310px;
  margin: 0;
  background: #F5F5F5;
  position: relative;
  min-height: 100%;
  padding-bottom: 10px;
  overflow-x: hidden;
}

pre, code {
  font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
  font-size: 14px;
}

h3 {
  font-size: 22px;
  font-weight: 400;
  letter-spacing: -.018em;
  text-overflow: ellipsis;
}

a {
  text-decoration: none;
  color: inherit;
}
a:hover {
  text-decoration: underline;
}
main {
  padding-top: 16px;
  padding-right: 8px;
  padding-left: 8px;
  padding-bottom: 44px;
  transition: opacity 0.3s ease-in-out;
  width: 100vw;
}

/* Navbar */
.navbar {
  position: relative;
  height: 82px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
      -ms-flex-direction: column;
          flex-direction: column;
}
.navbar-title {
  height: 55px;
  color: white;
  font-size: 30px;
  text-align: center;
  line-height: 55px;
}
.navbar-title #title {
  -ms-flex-item-align: start;
      align-self: flex-start;
  margin: 0 auto;
  text-align: left;
  padding-left: 55px;
  background-position: left center;
  background-repeat: no-repeat;
  background-image: url(/favicons/android-chrome-48x48.png);
  background-image: -webkit-image-set( url(/favicons/android-chrome-48x48.png) 1x, url(/favicons/android-chrome-96x96.png) 2x );
}
.navbar-subtitle {
  height: 20px;
  color: white;
  font-size: 18px;
  text-align: center;
  line-height: 20px;
}
.navbar a {
  color: white;
  text-decoration: none;
}
.section {
  color: white;
  font-size: 16px;
  font-weight: 500;
}
.section-top {
  height: 48px;
  line-height: 48px;
  display: flex;
  displa
Download .txt
gitextract_v5m2kdy5/

├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gcloudignore
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── FAQ.md
├── LICENSE
├── README.md
├── app.js
├── app.yaml
├── config/
│   ├── .gitignore
│   ├── config.example.json
│   └── config.js
├── controllers/
│   ├── api/
│   │   ├── favorite-pwa.js
│   │   ├── index.js
│   │   ├── lighthouse.js
│   │   ├── notifications.js
│   │   └── pwa.js
│   ├── app.js
│   ├── cache.js
│   ├── index.js
│   ├── pwa.js
│   ├── sw.js
│   └── tasks.js
├── cron.yaml
├── firebase-messaging-sw-generator.js
├── firebase-messaging-sw.tmpl
├── index.yaml
├── lib/
│   ├── asset-hashing.js
│   ├── color.js
│   ├── data-cache.js
│   ├── data-fetcher.js
│   ├── event-bus.js
│   ├── favorite-pwa.js
│   ├── images.js
│   ├── lighthouse.js
│   ├── manifest.js
│   ├── metadata.js
│   ├── model-datastore.js
│   ├── notifications.js
│   ├── promise-sequential.js
│   ├── pwa-index.js
│   ├── pwa.js
│   ├── search.js
│   ├── tasks.js
│   ├── verify-id-token.js
│   └── web-performance.js
├── lighthouse_machine/
│   ├── .dockerignore
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── Dockerfile
│   ├── LICENSE
│   ├── README.md
│   ├── app.yaml
│   ├── chromeuser-script.sh
│   ├── cpu_monitor.js
│   ├── entrypoint.sh
│   ├── etc/
│   │   └── xvfb
│   ├── package.json
│   └── server.js
├── middlewares/
│   └── index.js
├── models/
│   ├── favorite-pwa.js
│   ├── lighthouse.js
│   ├── manifest.js
│   ├── pwa.js
│   ├── task.js
│   └── user.js
├── package.json
├── public/
│   ├── .well-known/
│   │   └── assetlinks.json
│   ├── css/
│   │   └── style.css
│   ├── favicons/
│   │   └── browserconfig.xml
│   ├── google3915c2aaf77f961f.html
│   ├── humans.txt
│   ├── js/
│   │   ├── analytics.js
│   │   ├── chart.js
│   │   ├── event-target.js
│   │   ├── gapi.es6.js
│   │   ├── gulliver-config.js
│   │   ├── gulliver.es6.js
│   │   ├── loader.js
│   │   ├── messaging.js
│   │   ├── offline-support.js
│   │   ├── pwa-form.js
│   │   ├── routing/
│   │   │   ├── route.js
│   │   │   ├── router.js
│   │   │   └── transitions.js
│   │   ├── search-input.js
│   │   ├── shell.js
│   │   ├── signin.js
│   │   ├── ui/
│   │   │   ├── notification-checkbox.js
│   │   │   ├── share-button.js
│   │   │   └── signin-button.js
│   │   └── util/
│   │       └── requestIdleCallback.js
│   ├── manifest.json
│   ├── robots.txt
│   └── sw.js
├── rollup-config/
│   ├── gulliver.js
│   ├── lighthouse-chart.js
│   └── pwa-form.js
├── test/
│   ├── app/
│   │   ├── controllers/
│   │   │   ├── api/
│   │   │   │   ├── favorite-pwa.js
│   │   │   │   ├── lighthouse.js
│   │   │   │   └── pwa.js
│   │   │   ├── cache.js
│   │   │   └── tasks.js
│   │   ├── lib/
│   │   │   ├── asset-hashing.js
│   │   │   ├── color.js
│   │   │   ├── data-fetcher.js
│   │   │   ├── favorite-pwa.js
│   │   │   ├── images.js
│   │   │   ├── lighthouse-example.json
│   │   │   ├── lighthouse.js
│   │   │   ├── manifest.js
│   │   │   ├── model-datastore.js
│   │   │   ├── notifications.js
│   │   │   ├── promise-sequential.js
│   │   │   ├── pwa.js
│   │   │   ├── search.js
│   │   │   └── tasks.js
│   │   ├── manifests/
│   │   │   ├── icon-url-with-parameter.json
│   │   │   ├── inline-image-large-content.json
│   │   │   ├── invalid-theme-color.json
│   │   │   └── no-icon-array.json
│   │   ├── models/
│   │   │   └── pwa.js
│   │   └── views/
│   │       └── helpers/
│   │           └── index.js
│   └── client/
│       └── js/
│           └── event-target.js
├── third_party/
│   ├── Color.js
│   ├── README.md
│   ├── install.sh
│   └── manifest-parser.js
├── tsconfig.json
└── views/
    ├── 404.hbs
    ├── app/
    │   ├── offline.hbs
    │   └── shell.hbs
    ├── helpers/
    │   └── index.js
    ├── includes/
    │   ├── chevron_left.hbs
    │   ├── chevron_right.hbs
    │   ├── footer.hbs
    │   ├── head.hbs
    │   ├── header.hbs
    │   ├── hourglass.hbs
    │   ├── icon_log_in.hbs
    │   ├── icon_log_out.hbs
    │   ├── icon_search.hbs
    │   ├── icon_share.hbs
    │   ├── lighthouse.hbs
    │   ├── metadata.hbs
    │   ├── notifications_active.hbs
    │   ├── notifications_off.hbs
    │   ├── pagespeedinsight.hbs
    │   ├── pwadetails.hbs
    │   ├── score.hbs
    │   └── webpagetest.hbs
    └── pwas/
        ├── form.hbs
        ├── list.hbs
        ├── view-rss.hbs
        └── view.hbs
Download .txt
SYMBOL INDEX (387 symbols across 67 files)

FILE: app.js
  constant CACHE_CONTROL_SHORT_EXPIRES (line 36) | const CACHE_CONTROL_SHORT_EXPIRES = 60 * 10;
  constant CACHE_CONTROL_EXPIRES (line 37) | const CACHE_CONTROL_EXPIRES = 60 * 60 * 24;
  constant CACHE_CONTROL_NEVER_EXPIRE (line 38) | const CACHE_CONTROL_NEVER_EXPIRE = 31536000;
  constant ENVIRONMENT_PRODUCTION (line 39) | const ENVIRONMENT_PRODUCTION = 'production';

FILE: config/config.js
  function checkConfig (line 54) | function checkConfig(setting) {

FILE: controllers/api/lighthouse.js
  constant CACHE_CONTROL_EXPIRES (line 21) | const CACHE_CONTROL_EXPIRES = 60 * 60 * 24;

FILE: controllers/api/pwa.js
  constant CACHE_CONTROL_EXPIRES (line 27) | const CACHE_CONTROL_EXPIRES = 60 * 60 * 1;
  constant RSS (line 28) | const RSS = require('rss');
  function getDate (line 31) | function getDate(date) {
  class CsvWriter (line 35) | class CsvWriter {
    method write (line 36) | write(result, pwas) {
  class JsonWriter (line 57) | class JsonWriter {
    method write (line 58) | write(result, pwas) {
  function render (line 79) | function render(res, view, options) {
  function renderOnePwaRss (line 91) | function renderOnePwaRss(pwa, req, res) {
  function asyncForEach (line 104) | async function asyncForEach(array, callback) {
  class RssWriter (line 110) | class RssWriter {
    method write (line 111) | write(req, res, pwas) {

FILE: controllers/cache.js
  constant CACHE_LIFETIME (line 22) | const CACHE_LIFETIME = 60 * 60;

FILE: controllers/pwa.js
  constant LIST_PAGE_SIZE (line 28) | const LIST_PAGE_SIZE = 32;
  constant DEFAULT_PAGE_NUMBER (line 29) | const DEFAULT_PAGE_NUMBER = 1;
  constant DEFAULT_SORT_ORDER (line 30) | const DEFAULT_SORT_ORDER = 'newest';
  constant DEFAULT_TAB (line 31) | const DEFAULT_TAB = 'installable';
  constant DEFAULT_FILTER (line 32) | const DEFAULT_FILTER = {
  function setupListViewState (line 39) | function setupListViewState(req) {
  function setupListViewArguments (line 63) | function setupListViewArguments(req, viewState, result) {
  function listPwas (line 84) | function listPwas(req, res, next, sortOrder, filters) {
  function renderOnePwa (line 277) | function renderOnePwa(req, res) {
  function render (line 306) | function render(res, view, options) {

FILE: controllers/sw.js
  constant ASSETS (line 22) | const ASSETS = JSON.stringify([
  constant ASSETS_JS (line 27) | const ASSETS_JS = `const ASSETS = ${ASSETS};`;

FILE: controllers/tasks.js
  constant APP_ENGINE_CRON (line 24) | const APP_ENGINE_CRON = 'X-Appengine-Cron';
  function checkAppEngineCron (line 30) | function checkAppEngineCron(req, res, next) {
  function createOrUpdatePwaTasks (line 40) | function createOrUpdatePwaTasks(pwaList) {

FILE: lib/asset-hashing.js
  constant CHECKSUM_LENGTH (line 22) | const CHECKSUM_LENGTH = 10;
  constant CHECKSUM_PATTERN (line 23) | const CHECKSUM_PATTERN = /^[0-9a-z]{10}$/;
  class ChecksumProvider (line 25) | class ChecksumProvider {
    method constructor (line 27) | constructor(root) {
    method get (line 31) | get(assetPath) {
  class AssetChecksum (line 38) | class AssetChecksum {
    method constructor (line 40) | constructor(checksumProvider) {
    method encode (line 45) | encode(assetPath) {
    method decode (line 66) | decode(assetPath) {

FILE: lib/color.js
  function bestContrastRatio (line 20) | function bestContrastRatio(color1, color2, background) {
  function contrastRatio (line 31) | function contrastRatio(foreground, background = '#FFFFFF') {
  function relativeLuminance (line 56) | function relativeLuminance(color) {
  function componentRelativeLuminance_ (line 78) | function componentRelativeLuminance_(component) {

FILE: lib/data-cache.js
  constant PAGELIST_URLS (line 25) | const PAGELIST_URLS = 'PAGELIST_URLS';
  constant CACHE_LIFETIME (line 26) | const CACHE_LIFETIME = 60 * 60 * 6;
  function get (line 34) | function get(key) {
  function del (line 56) | function del(key) {
  function set (line 80) | function set(key, value, lifetime) {
  function replace (line 99) | function replace(key, value, lifetime) {
  function getMulti (line 111) | function getMulti(keys) {
  function flush (line 126) | function flush() {
  function storeCachedUrls (line 133) | function storeCachedUrls(url) {
  function flushCacheUrls (line 153) | function flushCacheUrls() {

FILE: lib/data-fetcher.js
  constant URL (line 18) | const URL = require('url');
  constant FIREBASE_AUTH (line 24) | const FIREBASE_AUTH = config.get('FIREBASE_AUTH');
  constant USER_AGENT (line 25) | const USER_AGENT = ['Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA5...
  function fetchMetadataDescription (line 34) | function fetchMetadataDescription(url) {
  function fetchLinkRelManifestUrl (line 49) | function fetchLinkRelManifestUrl(pageUrl) {
  function fetchWithUA (line 70) | function fetchWithUA(url) {
  function fetchJsonWithUA (line 87) | function fetchJsonWithUA(url) {
  function readFile (line 98) | function readFile(filename) {
  function _firebaseOptions (line 110) | function _firebaseOptions(payload) {
  function _handleFirebaseResponse (line 130) | function _handleFirebaseResponse(response) {
  function firebaseFetch (line 144) | function firebaseFetch(url, payload) {
  function postJson (line 165) | function postJson(url, body) {

FILE: lib/favorite-pwa.js
  constant ENTITY_NAME (line 21) | const ENTITY_NAME = 'FavoritePwa';

FILE: lib/images.js
  constant CLOUD_BUCKET (line 27) | const CLOUD_BUCKET = config.get('CLOUD_BUCKET');
  constant CACHE_CONTROL_EXPIRES (line 33) | const CACHE_CONTROL_EXPIRES = 60 * 60 * 24;
  function fetchAndSave (line 42) | function fetchAndSave(imageUrl, destFile) {
  function dataUriAndSave (line 79) | function dataUriAndSave(url, destFile) {
  function saveImages (line 96) | function saveImages(readStream, destFile, contentType) {
  function saveImage (line 117) | function saveImage(readStream, destFile, contentType, size) {
  function getPublicUrl (line 158) | function getPublicUrl(filename) {

FILE: lib/lighthouse.js
  constant ENTITY_NAME (line 29) | const ENTITY_NAME = 'Lighthouse';
  constant E_PWA_NOT_FOUND (line 30) | const E_PWA_NOT_FOUND = exports.E_PWA_NOT_FOUND = 1;
  constant E_FETCHING_STORING_LIGHTHOUSE (line 31) | const E_FETCHING_STORING_LIGHTHOUSE = exports.E_FETCHING_STORING_LIGHTHO...
  constant LIGTHOUSE_DATE_CHANGES (line 32) | const LIGTHOUSE_DATE_CHANGES = ['2016-12-01', '2017-03-01', '2017-05-05',

FILE: lib/manifest.js
  function fetchManifest (line 27) | function fetchManifest(manifestUrl) {
  function validateManifest (line 40) | function validateManifest(manifest, manifestUrl, documentUrl) {

FILE: lib/model-datastore.js
  constant ENTITY_COUNT_KIND (line 25) | const ENTITY_COUNT_KIND = 'counts';
  function fromDatastore (line 48) | function fromDatastore(obj) {
  function toDatastore (line 82) | function toDatastore(obj, nonIndexed) {
  function deepCopy (line 111) | function deepCopy(object) {
  function list (line 140) | function list(kind, offset, limit, sort, filters) {
  function createQuery (line 153) | function createQuery(kind, offset, limit, sort, filters) {
  function runQuery (line 174) | function runQuery(query) {
  function parseKey (line 195) | function parseKey(key) {
  function startTransaction (line 199) | function startTransaction(transaction) {
  function commitTransaction (line 210) | function commitTransaction(transaction) {
  function rollbackTransaction (line 221) | function rollbackTransaction(transaction) {
  function transactionGet (line 232) | function transactionGet(transaction, key) {
  function updateCount (line 243) | function updateCount(transaction, kind, inc) {
  function update (line 278) | function update(kind, id, data) {
  function updateWithCounts (line 317) | function updateWithCounts(kind, id, data) {
  function count (line 356) | function count(kind) {
  function read (line 376) | function read(kind, id) {
  function _deleteWithCounts (line 401) | function _deleteWithCounts(kind, id) {
  function _delete (line 433) | function _delete(kind, id) {

FILE: lib/pwa-index.js
  constant ENTITY_NAME (line 22) | const ENTITY_NAME = 'PWA';

FILE: lib/pwa.js
  constant DEFAULT_SORT_TYPE_KEY (line 40) | const DEFAULT_SORT_TYPE_KEY = 'score';
  constant ENTITY_NAME (line 41) | const ENTITY_NAME = 'PWA';
  constant E_MANIFEST_INVALID_URL (line 42) | const E_MANIFEST_INVALID_URL = exports.E_MANIFEST_INVALID_URL = 2;
  constant E_MANIFEST_URL_MISSING (line 43) | const E_MANIFEST_URL_MISSING = exports.E_MANIFEST_URL_MISSING = 3;
  constant E_MISSING_USER_INFORMATION (line 44) | const E_MISSING_USER_INFORMATION = exports.E_MISSING_USER_INFORMATION = 4;
  constant E_NOT_A_PWA (line 45) | const E_NOT_A_PWA = exports.E_NOT_A_PWA = 5;
  constant WAIT_TIME_NEW_PWAS (line 47) | const WAIT_TIME_NEW_PWAS = 10 * 60 * 1000;
  constant SORT_TYPE_MAP (line 49) | const SORT_TYPE_MAP = new Map([

FILE: lib/search.js
  constant URL (line 18) | const URL = require('url');
  constant CACHE_LIFETIME (line 23) | const CACHE_LIFETIME = 60 * 60 * 24 * 7;
  constant SEARCH_INDEX_CHANGE (line 24) | const SEARCH_INDEX_CHANGE = 'SearchIndexChange';
  class Search (line 31) | class Search {
    method constructor (line 33) | constructor() {
    method _initIndex (line 37) | _initIndex() {
    method _docFromPwa (line 52) | _docFromPwa(pwa) {
    method addPwa (line 68) | addPwa(pwa) {
    method addPwas (line 80) | addPwas(pwas) {
    method _addPwa (line 86) | _addPwa(pwa) {
    method updatePwa (line 98) | updatePwa(pwa) {
    method removePwa (line 111) | removePwa(pwa) {
    method search (line 133) | search(string) {
    method sarchIndexChange (line 146) | sarchIndexChange(date = new Date()) {
    method checkForSearchIndexChange (line 156) | checkForSearchIndexChange() {

FILE: lib/tasks.js
  constant ENTITY_NAME (line 23) | const ENTITY_NAME = 'Task';
  constant E_SAVING_TASK (line 24) | const E_SAVING_TASK = exports.E_SAVING_TASK = 1;
  constant E_GET_TASK_POP (line 25) | const E_GET_TASK_POP = exports.E_GET_TASK_POP = 2;
  constant E_DELETE_TASK_POP (line 26) | const E_DELETE_TASK_POP = exports.E_DELETE_TASK_POP = 3;

FILE: lib/verify-id-token.js
  constant CLIENT_ID (line 19) | const CLIENT_ID = config.get('CLIENT_ID');
  constant CLIENT_SECRET (line 20) | const CLIENT_SECRET = config.get('CLIENT_SECRET');

FILE: lib/web-performance.js
  constant WEBPERFORMANCE_SERVER_URL (line 21) | const WEBPERFORMANCE_SERVER_URL = config.get('WEBPERFORMANCE_SERVER');
  constant WEBPERFORMANCE_SERVER_API_KEY (line 22) | const WEBPERFORMANCE_SERVER_API_KEY = config.get('WEBPERFORMANCE_SERVER_...
  constant WEBPERFORMANCE_SERVER_WEBPAGEURL (line 23) | const WEBPERFORMANCE_SERVER_WEBPAGEURL = WEBPERFORMANCE_SERVER_URL + 'we...
  constant WEBPERFORMANCE_SERVER_PAGESPEED_REPORT (line 24) | const WEBPERFORMANCE_SERVER_PAGESPEED_REPORT = WEBPERFORMANCE_SERVER_URL...
  constant WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT (line 25) | const WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT = WEBPERFORMANCE_SERVER_U...
  constant WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT (line 26) | const WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT = WEBPERFORMANCE_SERVER_UR...
  function submitToWebPerformanceService (line 28) | function submitToWebPerformanceService(pwa) {
  function getReport (line 64) | function getReport(url) {

FILE: lighthouse_machine/cpu_monitor.js
  function cpuAverage (line 21) | function cpuAverage() {

FILE: lighthouse_machine/server.js
  constant HTTP_PORT (line 50) | const HTTP_PORT = 8080;
  constant HTTPS_PORT (line 51) | const HTTPS_PORT = 8443;

FILE: middlewares/index.js
  constant CSSPATH (line 19) | const CSSPATH = asset.encode('/css/style.css');
  constant JSPATH (line 20) | const JSPATH = asset.encode('/js/gulliver.js');

FILE: models/favorite-pwa.js
  class FavoritePwa (line 21) | class FavoritePwa {
    method constructor (line 22) | constructor(pwaId, userId) {

FILE: models/lighthouse.js
  class Lighthouse (line 26) | class Lighthouse {
    method constructor (line 27) | constructor(pwaId, absoluteStartUrl, lighthouseJson) {
    method lighthouseJson (line 38) | get lighthouseJson() {
    method lighthouseJson (line 42) | set lighthouseJson(value) {
  function parseToJson (line 48) | function parseToJson(value) {

FILE: models/manifest.js
  class Manifest (line 22) | class Manifest {
    method constructor (line 23) | constructor(manifestUrl, jsonManifest) {
    method getBestIcon (line 35) | getBestIcon() {
    method getBestIconUrl (line 71) | getBestIconUrl() {

FILE: models/pwa.js
  constant URL (line 19) | const URL = require('url');
  class Pwa (line 23) | class Pwa {
    method constructor (line 24) | constructor(manifestUrl, manifestModel) {
    method shortName (line 33) | get shortName() {
    method name (line 40) | get name() {
    method displayName (line 47) | get displayName() {
    method description (line 53) | get description() {
    method startUrl (line 61) | get startUrl() {
    method absoluteStartUrl (line 68) | get absoluteStartUrl() {
    method backgroundColor (line 77) | get backgroundColor() {
    method manifest (line 85) | get manifest() {
    method manifest (line 92) | set manifest(value) {
    method manifestAsString (line 100) | get manifestAsString() {
    method setUser (line 104) | setUser(user) {
    method generateEncodedStartUrl (line 108) | generateEncodedStartUrl() {
    method isNew (line 114) | isNew() {
    method _cleanUrl (line 118) | _cleanUrl(input) {
  function trimManifestFile (line 129) | function trimManifestFile(url) {
  function stringifyManifestIfNeeded (line 143) | function stringifyManifestIfNeeded(manifest) {
  function removeHash (line 150) | function removeHash(urlString) {

FILE: models/task.js
  class Task (line 18) | class Task {
    method constructor (line 19) | constructor(pwaId, modulePath, functionName, retries) {

FILE: models/user.js
  class User (line 23) | class User {
    method constructor (line 24) | constructor(googleLogin) {

FILE: public/js/analytics.js
  class Analytics (line 18) | class Analytics {
    method constructor (line 19) | constructor(window, config) {
    method _init (line 27) | _init() {
    method _setupA2HTracking (line 42) | _setupA2HTracking() {
    method trackOutboundClick (line 50) | trackOutboundClick(url) {
    method trackPageView (line 54) | trackPageView(url) {

FILE: public/js/chart.js
  class Chart (line 23) | class Chart {
    method constructor (line 25) | constructor(config) {
    method _loadChartsApi (line 31) | _loadChartsApi() {
    method load (line 52) | load() {
    method drawChart (line 60) | drawChart() {

FILE: public/js/event-target.js
  class EventTarget (line 16) | class EventTarget {
    method constructor (line 17) | constructor() {
    method addEventListener (line 21) | addEventListener(type, callback) {
    method removeEventListener (line 30) | removeEventListener(type, callback) {
    method getEventListeners (line 38) | getEventListeners(type) {
    method dispatchEvent (line 42) | dispatchEvent(event) {

FILE: public/js/gapi.es6.js
  function gapi (line 26) | function gapi(context = window, doc = document) {
  function gapiLoad (line 44) | function gapiLoad(name) {
  function clientLoad (line 60) | function clientLoad(name, version) {
  function authInit (line 75) | function authInit(params) {

FILE: public/js/gulliver-config.js
  class Config (line 18) | class Config {
    method constructor (line 19) | constructor(element) {
    method from (line 28) | static from(element) {

FILE: public/js/gulliver.es6.js
  constant CHART_BASE_URLS (line 46) | const CHART_BASE_URLS = {
  class Gulliver (line 52) | class Gulliver {
    method constructor (line 53) | constructor() {
    method _addRoute (line 90) | _addRoute(regexp, transitionStrategy, onRouteAttached, shellState) {
    method _setupRoutes (line 96) | _setupRoutes() {
    method setupServiceWorker (line 177) | setupServiceWorker() {
    method setupMessaging (line 190) | setupMessaging() {
    method setupBacklink (line 206) | setupBacklink() {

FILE: public/js/loader.js
  constant FADE_OUT_ANIMATION_LENGTH (line 20) | const FADE_OUT_ANIMATION_LENGTH = 500;
  class Loader (line 25) | class Loader {
    method constructor (line 33) | constructor(container, style) {
    method show (line 43) | show() {
    method hide (line 65) | hide() {

FILE: public/js/messaging.js
  constant SUBSCRIBE_ENDPOINT (line 22) | const SUBSCRIBE_ENDPOINT = '/api/notifications/subscribe';
  constant UNSUBSCRIBE_ENDPOINT (line 23) | const UNSUBSCRIBE_ENDPOINT = '/api/notifications/unsubscribe';
  constant TOPICS_ENDPOINT (line 24) | const TOPICS_ENDPOINT = '/api/notifications/topics';
  constant ERROR_PERMISSION_BLOCKED (line 26) | const ERROR_PERMISSION_BLOCKED = 'messaging/permission-blocked';
  constant ERROR_NOTIFICATIONS_BLOCKED (line 27) | const ERROR_NOTIFICATIONS_BLOCKED = 'messaging/notifications-blocked';
  class Messaging (line 30) | class Messaging {
    method constructor (line 31) | constructor(messagingSenderId) {
    method _postWithToken (line 45) | _postWithToken(url, token) {
    method _checkBlockedNotification (line 56) | _checkBlockedNotification(err) {
    method subscribe (line 68) | subscribe(topic) {
    method unsubscribe (line 94) | unsubscribe(topic) {
    method getSubscriptions (line 114) | getSubscriptions() {
    method isSubscribed (line 144) | isSubscribed(topic) {
    method isNotificationBlocked (line 151) | isNotificationBlocked() {

FILE: public/js/offline-support.js
  class OfflineSupport (line 18) | class OfflineSupport {
    method constructor (line 20) | constructor(window, router) {
    method _setupEventhandlers (line 31) | _setupEventhandlers() {
    method isAvailable (line 53) | isAvailable(href) {
    method markAsCached (line 68) | markAsCached(anchors) {

FILE: public/js/pwa-form.js
  constant SVG (line 21) | const SVG = '<svg version="1" xmlns="http://www.w3.org/2000/svg" width="...
  class PwaForm (line 23) | class PwaForm {
    method constructor (line 24) | constructor(window, signIn) {
    method setup (line 29) | setup() {
    method _addPwa (line 49) | _addPwa(container, manifestUrl) {
    method _setupListeners (line 95) | _setupListeners() {

FILE: public/js/routing/route.js
  class Route (line 20) | class Route {
    method constructor (line 21) | constructor(matchRegex, transitionStrategy, onAttached) {
    method matches (line 27) | matches(url) {
    method retrieveContent (line 31) | retrieveContent(url) {
    method transitionOut (line 37) | transitionOut(container) {
    method transitionIn (line 41) | transitionIn(container) {
    method onAttached (line 45) | onAttached() {
    method getContentOnlyUrl (line 55) | getContentOnlyUrl(url) {

FILE: public/js/routing/router.js
  class Router (line 20) | class Router {
    method constructor (line 21) | constructor(window, container) {
    method findRoute (line 33) | findRoute(url) {
    method addEventListener (line 37) | addEventListener(type, callback) {
    method _updateContent (line 41) | _updateContent() {
    method addRoute (line 65) | addRoute(route) {
    method navigate (line 69) | navigate(url) {
    method setupInitialRoute (line 75) | setupInitialRoute() {
    method _dispatchNavigateEvent (line 87) | _dispatchNavigateEvent(url, route) {
    method _dispatchOutboundNavigationEvent (line 98) | _dispatchOutboundNavigationEvent(url) {
    method _isNotLeftClickWithoutModifiers (line 107) | _isNotLeftClickWithoutModifiers(e) {
    method _takeOverAnchorLinks (line 111) | _takeOverAnchorLinks(root) {

FILE: public/js/routing/transitions.js
  class FadeInOutTransitionStrategy (line 19) | class FadeInOutTransitionStrategy {
    method transitionIn (line 20) | transitionIn(container) {
    method transitionOut (line 24) | transitionOut(container) {
  class LoaderTransitionStrategy (line 29) | class LoaderTransitionStrategy {
    method constructor (line 30) | constructor(window) {
    method transitionIn (line 36) | transitionIn(container) {
    method transitionOut (line 41) | transitionOut(container) {

FILE: public/js/search-input.js
  class SearchButton (line 23) | class SearchButton {
    method setupSearchElements (line 27) | setupSearchElements(router) {

FILE: public/js/shell.js
  class Shell (line 18) | class Shell {
    method constructor (line 19) | constructor(document) {
    method setStateForRoute (line 28) | setStateForRoute(route, shellState) {
    method _showElement (line 32) | _showElement(element, visible) {
    method _updateTab (line 40) | _updateTab(tab, options) {
    method onRouteChange (line 53) | onRouteChange(route) {

FILE: public/js/signin.js
  class SignIn (line 19) | class SignIn {
    method constructor (line 20) | constructor(window, config) {
    method _init (line 27) | _init() {
    method _setupUserChangeEvents (line 43) | _setupUserChangeEvents(auth) {
    method signedIn (line 58) | get signedIn() {
    method user (line 62) | get user() {
    method idToken (line 69) | get idToken() {
    method signIn (line 76) | signIn() {
    method signOut (line 84) | signOut() {
    method _setupEventHandlers (line 97) | _setupEventHandlers() {

FILE: public/js/ui/notification-checkbox.js
  class NotificationCheckbox (line 18) | class NotificationCheckbox {
    method constructor (line 19) | constructor(messaging, checkbox, topic) {
    method _setupEventListener (line 44) | _setupEventListener() {

FILE: public/js/ui/share-button.js
  class ShareButton (line 18) | class ShareButton {
    method constructor (line 19) | constructor(window, element, nameElement) {
    method _init (line 26) | _init() {
    method _setupEventListeners (line 34) | _setupEventListeners() {
    method _getTitle (line 41) | _getTitle() {
    method share (line 49) | share() {

FILE: public/js/ui/signin-button.js
  class SignInButton (line 18) | class SignInButton {
    method constructor (line 19) | constructor(window, signIn, element) {
    method _setupEventListeners (line 26) | _setupEventListeners() {
  class SignOutButton (line 45) | class SignOutButton {
    method constructor (line 46) | constructor(window, signIn, element) {
    method _setupEventListeners (line 53) | _setupEventListeners() {

FILE: public/sw.js
  constant VERSION (line 14) | const VERSION = '24';
  constant PREFIX (line 15) | const PREFIX = 'gulliver';
  constant CACHE_NAME (line 16) | const CACHE_NAME = `${PREFIX}-v${VERSION}`;
  constant PWA_OPTION (line 17) | const PWA_OPTION = {
  constant PWA_LIST_OPTION (line 26) | const PWA_LIST_OPTION = {
  constant OFFLINE_URL (line 35) | const OFFLINE_URL = '/.app/offline';
  constant SHELL_URL (line 36) | const SHELL_URL = '/.app/shell';
  constant OFFLINE (line 38) | const OFFLINE = [

FILE: test/app/controllers/api/pwa.js
  constant MANIFEST_URL (line 35) | const MANIFEST_URL = 'https://pwa-directory.appspot.com/manifest.json';
  constant MANIFEST_DATA (line 37) | const MANIFEST_DATA = {

FILE: test/app/controllers/tasks.js
  constant APP_ENGINE_CRON (line 34) | const APP_ENGINE_CRON = 'X-Appengine-Cron';
  constant MANIFEST_URL (line 35) | const MANIFEST_URL = 'https://pwa-directory.appspot.com/manifest.json';

FILE: test/app/lib/data-fetcher.js
  constant LIGHTHOUSE_JSON_EXAMPLE (line 27) | const LIGHTHOUSE_JSON_EXAMPLE = './test/app/lib/lighthouse-example.json';

FILE: test/app/lib/favorite-pwa.js
  constant ENTITY_NAME (line 30) | const ENTITY_NAME = 'FAVORITE-PWA';
  constant TEST_FAV_PWA (line 31) | const TEST_FAV_PWA = new FavoritePwa(123456789, 987654321);

FILE: test/app/lib/images.js
  constant MANIFEST_URL (line 31) | const MANIFEST_URL = 'https://mobile.twitter.com/manifest.json';
  constant MANIFEST_DATA (line 32) | const MANIFEST_DATA = './test/app/manifests/inline-image-large-content.j...

FILE: test/app/lib/lighthouse.js
  constant LIGHTHOUSE_JSON_EXAMPLE (line 29) | const LIGHTHOUSE_JSON_EXAMPLE = './test/app/lib/lighthouse-example.json';

FILE: test/app/lib/model-datastore.js
  constant ENTITY_NAME (line 28) | const ENTITY_NAME = 'test';
  class TestClass (line 30) | class TestClass {
  constant DB_OBJECT (line 37) | const DB_OBJECT = {

FILE: test/app/lib/promise-sequential.js
  function test (line 44) | function test(result) {

FILE: test/app/lib/pwa.js
  constant MANIFEST_URL (line 36) | const MANIFEST_URL = 'https://www.domain.com/manifest-br.json';
  constant START_URL (line 37) | const START_URL = 'https://www.domain.com/?utm_source=homescreen';
  constant LIGHTHOUSE_JSON_EXAMPLE (line 38) | const LIGHTHOUSE_JSON_EXAMPLE = './test/app/lib/lighthouse-example.json';
  constant MANIFEST_DATA (line 41) | const MANIFEST_DATA = {
  constant MANIFEST_NO_ICON (line 52) | const MANIFEST_NO_ICON = {name: 'Test', description: 'Manifest without i...
  constant MANIFEST_INVALID_THEME_COLOR (line 53) | const MANIFEST_INVALID_THEME_COLOR = {

FILE: test/app/lib/search.js
  constant MANIFEST_URL (line 30) | const MANIFEST_URL = 'https://pwa-directory.appspot.com/manifest.json';
  constant MANIFEST_DATA (line 32) | const MANIFEST_DATA = {

FILE: test/app/lib/tasks.js
  constant MANIFEST_URL (line 34) | const MANIFEST_URL = 'https://www.terra.com.br/manifest-br.json';
  constant MANIFEST_DATA (line 35) | const MANIFEST_DATA = './test/app/manifests/icon-url-with-parameter.json';

FILE: test/app/models/pwa.js
  constant MANIFEST_URL (line 23) | const MANIFEST_URL = 'http://www.example.com/';

FILE: third_party/Color.js
  method constructor (line 39) | constructor(rgba, format, originalText) {
  method parse (line 63) | static parse(text) {
  method fromRGBA (line 153) | static fromRGBA(rgba) {
  method fromHSVA (line 161) | static fromHSVA(hsva) {
  method _parseRgbNumeric (line 171) | static _parseRgbNumeric(value) {
  method _parseHueNumeric (line 184) | static _parseHueNumeric(value) {
  method _parseSatLightNumeric (line 192) | static _parseSatLightNumeric(value) {
  method _parseAlphaNumeric (line 200) | static _parseAlphaNumeric(value) {
  method _hsva2hsla (line 208) | static _hsva2hsla(hsva, out_hsla) {
  method hsl2rgb (line 229) | static hsl2rgb(hsl, out_rgb) {
  method hsva2rgba (line 274) | static hsva2rgba(hsva, out_rgba) {
  method luminance (line 288) | static luminance(rgba) {
  method blendColors (line 306) | static blendColors(fgRGBA, bgRGBA, out_blended) {
  method calculateContrastRatio (line 323) | static calculateContrastRatio(fgRGBA, bgRGBA) {
  method desiredLuminance (line 347) | static desiredLuminance(luminance, contrast, lighter) {
  method detectColorFormat (line 366) | static detectColorFormat(color) {
  method format (line 387) | format() {
  method hsla (line 394) | hsla() {
  method canonicalHSLA (line 432) | canonicalHSLA() {
  method hsva (line 440) | hsva() {
  method hasAlpha (line 453) | hasAlpha() {
  method canBeShortHex (line 460) | canBeShortHex() {
  method asString (line 474) | asString(format) {
  method rgba (line 553) | rgba() {
  method canonicalRGBA (line 560) | canonicalRGBA() {
  method nickname (line 571) | nickname() {
  method toProtocolRGBA (line 588) | toProtocolRGBA() {
  method invert (line 599) | invert() {
  method setAlpha (line 612) | setAlpha(alpha) {

FILE: third_party/manifest-parser.js
  constant ALLOWED_DISPLAY_VALUES (line 25) | const ALLOWED_DISPLAY_VALUES = [
  constant DEFAULT_DISPLAY_MODE (line 35) | const DEFAULT_DISPLAY_MODE = 'browser';
  constant ALLOWED_ORIENTATION_VALUES (line 37) | const ALLOWED_ORIENTATION_VALUES = [
  function parseString (line 48) | function parseString(raw, trim) {
  function parseColor (line 68) | function parseColor(raw) {
  function parseName (line 86) | function parseName(jsonInput) {
  function parseShortName (line 90) | function parseShortName(jsonInput) {
  function checkSameOrigin (line 100) | function checkSameOrigin(url1, url2) {
  function parseStartUrl (line 112) | function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
  function parseDisplay (line 159) | function parseDisplay(jsonInput) {
  function parseOrientation (line 177) | function parseOrientation(jsonInput) {
  function parseIcon (line 189) | function parseIcon(raw, manifestUrl) {
  function parseIcons (line 237) | function parseIcons(jsonInput, manifestUrl) {
  function parseApplication (line 273) | function parseApplication(raw) {
  function parseRelatedApplications (line 302) | function parseRelatedApplications(jsonInput) {
  function parsePreferRelatedApplications (line 335) | function parsePreferRelatedApplications(jsonInput) {
  function parseThemeColor (line 356) | function parseThemeColor(jsonInput) {
  function parseBackgroundColor (line 360) | function parseBackgroundColor(jsonInput) {
  function parse (line 371) | function parse(string, manifestUrl, documentUrl) {

FILE: views/helpers/index.js
  constant DEFAULT_LIGHT (line 17) | const DEFAULT_LIGHT = '#ffffff';
  constant DEFAULT_DARK (line 18) | const DEFAULT_DARK = '#000000';
  function contrastColor (line 25) | function contrastColor(hexcolor) {
  function syntaxHighlight (line 57) | function syntaxHighlight(json) {
Condensed preview — 158 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (927K chars).
[
  {
    "path": ".babelrc",
    "chars": 849,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": ".eslintignore",
    "chars": 120,
    "preview": "/coverage\n/third_party\n/public/js/gulliver.js\n/public/js/lighthouse-chart.js\n/public/js/pwa-form.js\n/lighthouse_machine\n"
  },
  {
    "path": ".eslintrc.json",
    "chars": 848,
    "preview": "{\n  \"extends\": \"google\",\n  // http://eslint.org/docs/rules/\n  \"rules\": {\n    \"max-len\": [2, 100, {\n      \"ignoreComments"
  },
  {
    "path": ".gcloudignore",
    "chars": 34,
    "preview": "node_modules/\nlighthouse_machine/\n"
  },
  {
    "path": ".gitignore",
    "chars": 154,
    "preview": ".DS_Store\nnode_modules\nnpm-debug.log\n/coverage\n.jshintrc\n.idea/\nkey.json\npublic/js/gulliver.js\npublic/js/gulliver.js.map"
  },
  {
    "path": ".travis.yml",
    "chars": 307,
    "preview": "language: node_js\nsudo: required\ndist: trusty\nnode_js:\n  - \"8\"\nbefore_script:\n  - npm install\nscript:\n  - npm test\nenv:\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1450,
    "preview": "Want to contribute? Great! First, read this page (including the small print at the end).\n\n### Before you contribute\nBefo"
  },
  {
    "path": "FAQ.md",
    "chars": 2982,
    "preview": "# PWA Directory FAQ\n\n### What is PWA Directory?\nIs an open source directory of Progressive Web Apps driven by user submi"
  },
  {
    "path": "LICENSE",
    "chars": 11343,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 6751,
    "preview": "```diff\n! This project has been deprecated.\n```\n\n# Gulliver\n\n[Gulliver](https://pwa-directory.appspot.com/) is a directo"
  },
  {
    "path": "app.js",
    "chars": 4732,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "app.yaml",
    "chars": 767,
    "preview": "#\tCopyright 2015-2016, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use "
  },
  {
    "path": "config/.gitignore",
    "chars": 12,
    "preview": "config.json\n"
  },
  {
    "path": "config/config.example.json",
    "chars": 1035,
    "preview": "{\n  \"//\": \"See README.md for more information about what to put here\",\n  \"GCLOUD_PROJECT\": \"run `gcloud config get-value"
  },
  {
    "path": "config/config.js",
    "chars": 1885,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/api/favorite-pwa.js",
    "chars": 4253,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/api/index.js",
    "chars": 1145,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/api/lighthouse.js",
    "chars": 1570,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/api/notifications.js",
    "chars": 1785,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/api/pwa.js",
    "chars": 7240,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/app.js",
    "chars": 935,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/cache.js",
    "chars": 1628,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/index.js",
    "chars": 1585,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/pwa.js",
    "chars": 9341,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/sw.js",
    "chars": 1158,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "controllers/tasks.js",
    "chars": 3895,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "cron.yaml",
    "chars": 976,
    "preview": "cron:\n- description: (Node) Daily PWA info update job\n  url: /tasks/cron\n  schedule: every day 13:00\n\n- description: (No"
  },
  {
    "path": "firebase-messaging-sw-generator.js",
    "chars": 1253,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "firebase-messaging-sw.tmpl",
    "chars": 1034,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "index.yaml",
    "chars": 292,
    "preview": "indexes:\n- kind: Lighthouse\n  properties:\n  - name: pwaId\n    direction: asc\n  - name: date\n    direction: desc\n- kind: "
  },
  {
    "path": "lib/asset-hashing.js",
    "chars": 2291,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/color.js",
    "chars": 2737,
    "preview": "/**\n * Copyright 2015-2018, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/data-cache.js",
    "chars": 4002,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/data-fetcher.js",
    "chars": 4705,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/event-bus.js",
    "chars": 723,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/favorite-pwa.js",
    "chars": 2356,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/images.js",
    "chars": 5233,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/lighthouse.js",
    "chars": 6779,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/manifest.js",
    "chars": 1904,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may n"
  },
  {
    "path": "lib/metadata.js",
    "chars": 1313,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/model-datastore.js",
    "chars": 10251,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/notifications.js",
    "chars": 2534,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/promise-sequential.js",
    "chars": 1141,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/pwa-index.js",
    "chars": 2397,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/pwa.js",
    "chars": 15072,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/search.js",
    "chars": 4383,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/tasks.js",
    "chars": 3583,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/verify-id-token.js",
    "chars": 1191,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lib/web-performance.js",
    "chars": 3075,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lighthouse_machine/.dockerignore",
    "chars": 23,
    "preview": ".gitignore\n.git\noutputs"
  },
  {
    "path": "lighthouse_machine/.eslintrc.json",
    "chars": 825,
    "preview": "{\n  \"extends\": \"google\",\n  \"installedESLint\": true,\n  // http://eslint.org/docs/rules/\n  \"rules\": {\n    \"max-len\": [2, 1"
  },
  {
    "path": "lighthouse_machine/.gitignore",
    "chars": 21,
    "preview": "outputs\nnode_modules\n"
  },
  {
    "path": "lighthouse_machine/Dockerfile",
    "chars": 2923,
    "preview": "#\tCopyright 2016-2017, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use "
  },
  {
    "path": "lighthouse_machine/LICENSE",
    "chars": 11355,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "lighthouse_machine/README.md",
    "chars": 636,
    "preview": "# Lighthouse machine\nA Docker image to run [Lighthouse](https://github.com/GoogleChrome/lighthouse) scores on a server\n\n"
  },
  {
    "path": "lighthouse_machine/app.yaml",
    "chars": 1241,
    "preview": "#\tCopyright 2016-2017, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use "
  },
  {
    "path": "lighthouse_machine/chromeuser-script.sh",
    "chars": 836,
    "preview": "#!/bin/bash\n\n# Copyright 2016-2017, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "lighthouse_machine/cpu_monitor.js",
    "chars": 1732,
    "preview": "/**\n * Copyright 2016-2017, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "lighthouse_machine/entrypoint.sh",
    "chars": 791,
    "preview": "#!/bin/bash\n\n# Copyright 2016-2017, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "lighthouse_machine/etc/xvfb",
    "chars": 1097,
    "preview": "#!/bin/bash\n\n# Copyright 2016-2017, Google, Inc.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "lighthouse_machine/package.json",
    "chars": 560,
    "preview": "{\n  \"name\": \"lighthouse_machine_server\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A server for the lighthouse machine\",\n "
  },
  {
    "path": "lighthouse_machine/server.js",
    "chars": 3362,
    "preview": "/**\n * Copyright 2016-2017, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "middlewares/index.js",
    "chars": 2243,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "models/favorite-pwa.js",
    "chars": 824,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "models/lighthouse.js",
    "chars": 1759,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may n"
  },
  {
    "path": "models/manifest.js",
    "chars": 2131,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "models/pwa.js",
    "chars": 3556,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "models/task.js",
    "chars": 871,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "models/user.js",
    "chars": 869,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "package.json",
    "chars": 3069,
    "preview": "{\n  \"name\": \"gulliver\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A directory of PWAs\",\n  \"repository\": \"https://github.co"
  },
  {
    "path": "public/.well-known/assetlinks.json",
    "chars": 308,
    "preview": "[{\n    \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n    \"target\": {\n      \"namespace\": \"android_app\",\n   "
  },
  {
    "path": "public/css/style.css",
    "chars": 17113,
    "preview": "/* Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use t"
  },
  {
    "path": "public/favicons/browserconfig.xml",
    "chars": 240,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<browserconfig>\r\n  <msapplication>\r\n    <tile>\r\n      <square150x150logo src=\"/f"
  },
  {
    "path": "public/google3915c2aaf77f961f.html",
    "chars": 53,
    "preview": "google-site-verification: google3915c2aaf77f961f.html"
  },
  {
    "path": "public/humans.txt",
    "chars": 430,
    "preview": "# humanstxt.org/\n# The humans responsible & technology colophon\n\n# TEAM\n\n    Julian Toledo -- @juliantoledo\n    Michael "
  },
  {
    "path": "public/js/analytics.js",
    "chars": 1741,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/chart.js",
    "chars": 3189,
    "preview": "/**\n * copyright 2015-2016, google, inc.\n * licensed under the apache license, version 2.0 (the \"license\");\n * you may n"
  },
  {
    "path": "public/js/event-target.js",
    "chars": 1418,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/gapi.es6.js",
    "chars": 2970,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/gulliver-config.js",
    "chars": 934,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/gulliver.es6.js",
    "chars": 7278,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/loader.js",
    "chars": 2168,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/messaging.js",
    "chars": 4678,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/offline-support.js",
    "chars": 2696,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/pwa-form.js",
    "chars": 5526,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/routing/route.js",
    "chars": 1653,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/routing/router.js",
    "chars": 4104,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/routing/transitions.js",
    "chars": 1273,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/search-input.js",
    "chars": 1509,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/shell.js",
    "chars": 1832,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/signin.js",
    "chars": 2773,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/ui/notification-checkbox.js",
    "chars": 2042,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/ui/share-button.js",
    "chars": 1527,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/ui/signin-button.js",
    "chars": 1959,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "public/js/util/requestIdleCallback.js",
    "chars": 1119,
    "preview": "/*!\n * Copyright 2015 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
  },
  {
    "path": "public/manifest.json",
    "chars": 718,
    "preview": "{\n\t\"name\": \"PWA Directory\",\n\t\"short_name\": \"PwaDirectory\",\n\t\"description\": \"A Directory of PWAs\",\n\t\"start_url\": \"/?utm_s"
  },
  {
    "path": "public/robots.txt",
    "chars": 25,
    "preview": "User-Agent: *\nDisallow: \n"
  },
  {
    "path": "public/sw.js",
    "chars": 4524,
    "preview": "/* eslint-env serviceworker, browser */\n\n// sw-offline-google-analytics *must* be imported and initialized before\n// sw-"
  },
  {
    "path": "rollup-config/gulliver.js",
    "chars": 778,
    "preview": "import babel from 'rollup-plugin-babel';\nimport uglify from 'rollup-plugin-uglify';\nimport nodeResolve from 'rollup-plug"
  },
  {
    "path": "rollup-config/lighthouse-chart.js",
    "chars": 794,
    "preview": "import babel from 'rollup-plugin-babel';\nimport uglify from 'rollup-plugin-uglify';\nimport nodeResolve from 'rollup-plug"
  },
  {
    "path": "rollup-config/pwa-form.js",
    "chars": 778,
    "preview": "import babel from 'rollup-plugin-babel';\nimport uglify from 'rollup-plugin-uglify';\nimport nodeResolve from 'rollup-plug"
  },
  {
    "path": "test/app/controllers/api/favorite-pwa.js",
    "chars": 5805,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/controllers/api/lighthouse.js",
    "chars": 2577,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/controllers/api/pwa.js",
    "chars": 2669,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/controllers/cache.js",
    "chars": 2747,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/controllers/tasks.js",
    "chars": 2893,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/asset-hashing.js",
    "chars": 2839,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/color.js",
    "chars": 2806,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/data-fetcher.js",
    "chars": 2942,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/favorite-pwa.js",
    "chars": 3409,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/images.js",
    "chars": 3832,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/lighthouse-example.json",
    "chars": 449061,
    "preview": "[\n  {\n  \"id\": \"5636139285217280-20180307\",\n  \"webPageUrlId\": \"5636139285217280\",\n  \"url\": \"https://pwa-directory.appspot"
  },
  {
    "path": "test/app/lib/lighthouse.js",
    "chars": 3002,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/manifest.js",
    "chars": 2814,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/model-datastore.js",
    "chars": 4050,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/notifications.js",
    "chars": 5523,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/promise-sequential.js",
    "chars": 1741,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/pwa.js",
    "chars": 9716,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/search.js",
    "chars": 4027,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/lib/tasks.js",
    "chars": 5279,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/manifests/icon-url-with-parameter.json",
    "chars": 209,
    "preview": "{\n  \"name\": \"Test\",\n  \"icons\": [\n    {\n      \"src\": \"img/launcher-icon.png?v2\",\n      \"sizes\": \"192x192\",\n      \"type\": "
  },
  {
    "path": "test/app/manifests/inline-image-large-content.json",
    "chars": 1987,
    "preview": "{\"short_name\":\"Twitter\",\"name\":\"Twitter\",\"icons\":[{\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABl"
  },
  {
    "path": "test/app/manifests/invalid-theme-color.json",
    "chars": 163,
    "preview": "{\n  \"start_url\": \"https://www.terra.com.br/?utm_source=homescreen\",\n  \"description\": \"Manifest with an invalid theme_col"
  },
  {
    "path": "test/app/manifests/no-icon-array.json",
    "chars": 306,
    "preview": "{\n  \"name\": \"Test\",\n  \"description\": \"Manifest with icons that are not in array\",\n  \"icons\": {\n    \"16\": \"img/icons/icon"
  },
  {
    "path": "test/app/models/pwa.js",
    "chars": 6203,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/app/views/helpers/index.js",
    "chars": 1850,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "test/client/js/event-target.js",
    "chars": 1506,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "third_party/Color.js",
    "chars": 23725,
    "preview": "/*\n * Copyright (C) 2009 Apple Inc.  All rights reserved.\n * Copyright (C) 2009 Joseph Pecoraro\n *\n * Redistribution and"
  },
  {
    "path": "third_party/README.md",
    "chars": 116,
    "preview": "See `./install.sh` for information on where the `*.js` files in this directory\ncome from, and how to generate them.\n"
  },
  {
    "path": "third_party/install.sh",
    "chars": 1084,
    "preview": "#!/usr/bin/env sh\n\n# Downloads and patches the two files from lighthouse and devtools that we need\n# to validate manifes"
  },
  {
    "path": "third_party/manifest-parser.js",
    "chars": 10297,
    "preview": "/**\n * @license\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "tsconfig.json",
    "chars": 123,
    "preview": "{\n  // https://www.typescriptlang.org/docs/handbook/compiler-options.html\n  \"compilerOptions\": {\n    \"target\": \"es5\"\n  }"
  },
  {
    "path": "views/404.hbs",
    "chars": 954,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  },
  {
    "path": "views/app/offline.hbs",
    "chars": 925,
    "preview": "{{!--\nCopyright 2015-2016, Google, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use"
  },
  {
    "path": "views/app/shell.hbs",
    "chars": 905,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  },
  {
    "path": "views/helpers/index.js",
    "chars": 3976,
    "preview": "/**\n * Copyright 2015-2016, Google, Inc.\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may n"
  },
  {
    "path": "views/includes/chevron_left.hbs",
    "chars": 210,
    "preview": "<svg fill=\"#000000\" height=\"48\" viewBox=\"0 0 24 24\" width=\"48\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path d=\"M15.41 7"
  },
  {
    "path": "views/includes/chevron_right.hbs",
    "chars": 211,
    "preview": "<svg fill=\"#1976D2\" height=\"48\" viewBox=\"0 0 24 24\" width=\"48\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path d=\"M10 6L8."
  },
  {
    "path": "views/includes/footer.hbs",
    "chars": 1580,
    "preview": "{{!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not us"
  },
  {
    "path": "views/includes/head.hbs",
    "chars": 2308,
    "preview": "{{!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not us"
  },
  {
    "path": "views/includes/header.hbs",
    "chars": 2958,
    "preview": "{{!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not us"
  },
  {
    "path": "views/includes/hourglass.hbs",
    "chars": 306,
    "preview": "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path d=\"M6 2v6h."
  },
  {
    "path": "views/includes/icon_log_in.hbs",
    "chars": 341,
    "preview": "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path d=\"M3 5v14c"
  },
  {
    "path": "views/includes/icon_log_out.hbs",
    "chars": 363,
    "preview": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n  <path d=\"M19,3 C20.11,3 21,3.9 21,"
  },
  {
    "path": "views/includes/icon_search.hbs",
    "chars": 399,
    "preview": "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path d=\"M15.5 14"
  },
  {
    "path": "views/includes/icon_share.hbs",
    "chars": 509,
    "preview": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n    <path d=\"M0 0h24v24H0z\" fill=\"no"
  },
  {
    "path": "views/includes/lighthouse.hbs",
    "chars": 672,
    "preview": "<section class=\"detail-general\">\n  <h3>Lighthouse  {{> score pwa}} </h3>  \n  <div class=\"chart\" type='lighthouse' {{#if "
  },
  {
    "path": "views/includes/metadata.hbs",
    "chars": 1832,
    "preview": "<!-- Schema.org markup for Google+ -->\n<meta itemprop=\"name\" content=\"{{title}}\">\n<meta itemprop=\"description\" content=\""
  },
  {
    "path": "views/includes/notifications_active.hbs",
    "chars": 572,
    "preview": "<svg id=\"notifications_active\" fill=\"#000000\" height=\"32\" viewBox=\"0 0 24 24\" width=\"32\" xmlns=\"http://www.w3.org/2000/s"
  },
  {
    "path": "views/includes/notifications_off.hbs",
    "chars": 532,
    "preview": "<svg id=\"notifications_off\" fill=\"#000000\" height=\"32\" viewBox=\"0 0 24 24\" width=\"32\" xmlns=\"http://www.w3.org/2000/svg\""
  },
  {
    "path": "views/includes/pagespeedinsight.hbs",
    "chars": 241,
    "preview": "<section class=\"detail-general\">\n  <h3>PageSpeed Insights</h3>\n  <div class=\"chart\" type='psi' {{#if pwa.id }} pwa={{pwa"
  },
  {
    "path": "views/includes/pwadetails.hbs",
    "chars": 1002,
    "preview": "<a href=\"{{pwa.absoluteStartUrl}}\"  id=\"pwa\" class=\"detail-general\" style=\"background-color: {{pwa.backgroundColor}}; co"
  },
  {
    "path": "views/includes/score.hbs",
    "chars": 177,
    "preview": "<span class=\"score\">\n{{#if lighthouseScore}}\n{{lighthouseScore}}\n{{else}}\n<i class=\"fa fa-hourglass-half\" aria-hidden=\"t"
  },
  {
    "path": "views/includes/webpagetest.hbs",
    "chars": 236,
    "preview": "<section class=\"detail-general\">\n  <h3>WebPageTest</h3>\n  <div class=\"chart\" type='wpt' {{#if pwa.id }} pwa={{pwa.id}} {"
  },
  {
    "path": "views/pwas/form.hbs",
    "chars": 1643,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  },
  {
    "path": "views/pwas/list.hbs",
    "chars": 3281,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  },
  {
    "path": "views/pwas/view-rss.hbs",
    "chars": 1372,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  },
  {
    "path": "views/pwas/view.hbs",
    "chars": 1284,
    "preview": "<!-- Copyright 2015-2016, Google, Inc.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use"
  }
]

About this extraction

This page contains the full source code of the GoogleChromeLabs/gulliver GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 158 files (839.8 KB), approximately 315.3k tokens, and a symbol index with 387 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.

Copied to clipboard!