├── config
├── .gitignore
├── config.example.json
└── config.js
├── public
├── robots.txt
├── google3915c2aaf77f961f.html
├── favicon.ico
├── img
│ ├── lh-logo-128.png
│ ├── lh-logo-64.png
│ ├── lh_logo_bg.png
│ ├── lighthouse-18.png
│ ├── lighthouse-36.png
│ ├── feed-icon-24px.png
│ ├── feed-icon-48px.png
│ ├── pwa-directory-preview.png
│ ├── GitHub-Mark-Light-24px.png
│ └── GitHub-Mark-Light-48px.png
├── favicons
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── mstile-70x70.png
│ ├── apple-touch-icon.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── android-chrome-36x36.png
│ ├── android-chrome-48x48.png
│ ├── android-chrome-72x72.png
│ ├── android-chrome-96x96.png
│ ├── android-chrome-144x144.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-256x256.png
│ ├── android-chrome-384x384.png
│ ├── android-chrome-512x512.png
│ ├── browserconfig.xml
│ └── safari-pinned-tab.svg
├── .well-known
│ └── assetlinks.json
├── humans.txt
├── manifest.json
└── js
│ ├── gulliver-config.js
│ ├── util
│ └── requestIdleCallback.js
│ ├── routing
│ ├── transitions.js
│ └── route.js
│ ├── event-target.js
│ ├── search-input.js
│ ├── ui
│ ├── share-button.js
│ ├── signin-button.js
│ └── notification-checkbox.js
│ ├── analytics.js
│ ├── shell.js
│ ├── loader.js
│ ├── offline-support.js
│ ├── gapi.es6.js
│ ├── signin.js
│ └── chart.js
├── .gcloudignore
├── lighthouse_machine
├── .gitignore
├── .dockerignore
├── package.json
├── README.md
├── entrypoint.sh
├── chromeuser-script.sh
├── .eslintrc.json
├── etc
│ └── xvfb
├── app.yaml
├── cpu_monitor.js
├── Dockerfile
└── server.js
├── img
├── gulliver-details-one.png
├── gulliver-details-two.png
└── gulliver-landing-page.png
├── third_party
├── README.md
└── install.sh
├── .eslintignore
├── tsconfig.json
├── .gitignore
├── test
├── app
│ ├── manifests
│ │ ├── invalid-theme-color.json
│ │ ├── icon-url-with-parameter.json
│ │ ├── no-icon-array.json
│ │ └── inline-image-large-content.json
│ ├── lib
│ │ ├── promise-sequential.js
│ │ ├── manifest.js
│ │ ├── color.js
│ │ ├── asset-hashing.js
│ │ ├── data-fetcher.js
│ │ ├── lighthouse.js
│ │ ├── favorite-pwa.js
│ │ └── images.js
│ ├── views
│ │ └── helpers
│ │ │ └── index.js
│ └── controllers
│ │ ├── api
│ │ ├── lighthouse.js
│ │ └── pwa.js
│ │ ├── cache.js
│ │ └── tasks.js
└── client
│ └── js
│ └── event-target.js
├── views
├── includes
│ ├── score.hbs
│ ├── chevron_left.hbs
│ ├── chevron_right.hbs
│ ├── webpagetest.hbs
│ ├── pagespeedinsight.hbs
│ ├── hourglass.hbs
│ ├── icon_log_in.hbs
│ ├── icon_log_out.hbs
│ ├── icon_search.hbs
│ ├── icon_share.hbs
│ ├── notifications_off.hbs
│ ├── notifications_active.hbs
│ ├── lighthouse.hbs
│ ├── pwadetails.hbs
│ ├── footer.hbs
│ ├── metadata.hbs
│ ├── head.hbs
│ └── header.hbs
├── app
│ ├── shell.hbs
│ └── offline.hbs
├── 404.hbs
└── pwas
│ ├── view-rss.hbs
│ ├── view.hbs
│ ├── form.hbs
│ └── list.hbs
├── .travis.yml
├── index.yaml
├── lib
├── event-bus.js
├── promise-sequential.js
├── verify-id-token.js
├── metadata.js
├── manifest.js
├── asset-hashing.js
├── favorite-pwa.js
├── pwa-index.js
├── notifications.js
├── color.js
├── web-performance.js
└── tasks.js
├── app.yaml
├── rollup-config
├── gulliver.js
├── pwa-form.js
└── lighthouse-chart.js
├── models
├── favorite-pwa.js
├── task.js
├── user.js
├── lighthouse.js
├── manifest.js
└── pwa.js
├── .babelrc
├── .eslintrc.json
├── controllers
├── app.js
├── api
│ ├── index.js
│ ├── lighthouse.js
│ └── notifications.js
├── sw.js
├── index.js
└── cache.js
├── cron.yaml
├── firebase-messaging-sw.tmpl
├── firebase-messaging-sw-generator.js
├── CONTRIBUTING.md
├── middlewares
└── index.js
├── FAQ.md
└── package.json
/config/.gitignore:
--------------------------------------------------------------------------------
1 | config.json
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/.gcloudignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lighthouse_machine/
3 |
--------------------------------------------------------------------------------
/lighthouse_machine/.gitignore:
--------------------------------------------------------------------------------
1 | outputs
2 | node_modules
3 |
--------------------------------------------------------------------------------
/lighthouse_machine/.dockerignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .git
3 | outputs
--------------------------------------------------------------------------------
/public/google3915c2aaf77f961f.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google3915c2aaf77f961f.html
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/lh-logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/lh-logo-128.png
--------------------------------------------------------------------------------
/public/img/lh-logo-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/lh-logo-64.png
--------------------------------------------------------------------------------
/public/img/lh_logo_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/lh_logo_bg.png
--------------------------------------------------------------------------------
/img/gulliver-details-one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/img/gulliver-details-one.png
--------------------------------------------------------------------------------
/img/gulliver-details-two.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/img/gulliver-details-two.png
--------------------------------------------------------------------------------
/public/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/img/lighthouse-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/lighthouse-18.png
--------------------------------------------------------------------------------
/public/img/lighthouse-36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/lighthouse-36.png
--------------------------------------------------------------------------------
/img/gulliver-landing-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/img/gulliver-landing-page.png
--------------------------------------------------------------------------------
/public/img/feed-icon-24px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/feed-icon-24px.png
--------------------------------------------------------------------------------
/public/img/feed-icon-48px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/feed-icon-48px.png
--------------------------------------------------------------------------------
/public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/mstile-70x70.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/mstile-144x144.png
--------------------------------------------------------------------------------
/public/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/favicons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/mstile-310x150.png
--------------------------------------------------------------------------------
/public/favicons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/mstile-310x310.png
--------------------------------------------------------------------------------
/public/img/pwa-directory-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/pwa-directory-preview.png
--------------------------------------------------------------------------------
/public/img/GitHub-Mark-Light-24px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/GitHub-Mark-Light-24px.png
--------------------------------------------------------------------------------
/public/img/GitHub-Mark-Light-48px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/img/GitHub-Mark-Light-48px.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-36x36.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-48x48.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-72x72.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-96x96.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-144x144.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-256x256.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-384x384.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/gulliver/main/public/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/third_party/README.md:
--------------------------------------------------------------------------------
1 | See `./install.sh` for information on where the `*.js` files in this directory
2 | come from, and how to generate them.
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /third_party
3 | /public/js/gulliver.js
4 | /public/js/lighthouse-chart.js
5 | /public/js/pwa-form.js
6 | /lighthouse_machine
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://www.typescriptlang.org/docs/handbook/compiler-options.html
3 | "compilerOptions": {
4 | "target": "es5"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | /coverage
5 | .jshintrc
6 | .idea/
7 | key.json
8 | public/js/gulliver.js
9 | public/js/gulliver.js.map
10 | public/firebase-messaging-sw.js
11 |
12 |
--------------------------------------------------------------------------------
/test/app/manifests/invalid-theme-color.json:
--------------------------------------------------------------------------------
1 | {
2 | "start_url": "https://www.terra.com.br/?utm_source=homescreen",
3 | "description": "Manifest with an invalid theme_color",
4 | "theme_color": "not_a_real_color"
5 | }
6 |
--------------------------------------------------------------------------------
/views/includes/score.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#if lighthouseScore}}
3 | {{lighthouseScore}}
4 | {{else}}
5 |
6 | {{/if}}
7 |
8 |
--------------------------------------------------------------------------------
/views/includes/chevron_left.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/views/includes/chevron_right.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/views/includes/webpagetest.hbs:
--------------------------------------------------------------------------------
1 |
2 | WebPageTest
3 |
4 |
No WebPageTest Insights data available
5 |
6 |
7 |
--------------------------------------------------------------------------------
/views/includes/pagespeedinsight.hbs:
--------------------------------------------------------------------------------
1 |
2 | PageSpeed Insights
3 |
4 |
No PageSpeed Insights data available
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/app/manifests/icon-url-with-parameter.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "icons": [
4 | {
5 | "src": "img/launcher-icon.png?v2",
6 | "sizes": "192x192",
7 | "type": "image/png"
8 | }
9 | ],
10 | "start_url": "https://www.example.com/?utm_source=homescreen"
11 | }
12 |
--------------------------------------------------------------------------------
/public/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #7cc0ff
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/views/includes/hourglass.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/.well-known/assetlinks.json:
--------------------------------------------------------------------------------
1 | [{
2 | "relation": ["delegate_permission/common.handle_all_urls"],
3 | "target": {
4 | "namespace": "android_app",
5 | "package_name": "com.appspot.pwa_directory",
6 | "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"]}
7 | }]
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: required
3 | dist: trusty
4 | node_js:
5 | - "8"
6 | before_script:
7 | - npm install
8 | script:
9 | - npm test
10 | env:
11 | # evade checks in config.js
12 | - CLIENT_ID=placeholder CLIENT_SECRET=placeholder GCLOUD_PROJECT=placeholder CLOUD_BUCKET=placeholder FIREBASE_AUTH=placeholder API_TOKENS="abcdefghijk"
13 |
--------------------------------------------------------------------------------
/index.yaml:
--------------------------------------------------------------------------------
1 | indexes:
2 | - kind: Lighthouse
3 | properties:
4 | - name: pwaId
5 | direction: asc
6 | - name: date
7 | direction: desc
8 | - kind: PWA
9 | properties:
10 | - name: installable
11 | - name: lighthouseScore
12 | direction: desc
13 | - kind: PWA
14 | properties:
15 | - name: installable
16 | - name: created
17 | direction: desc
18 |
--------------------------------------------------------------------------------
/views/includes/icon_log_in.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/app/manifests/no-icon-array.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "description": "Manifest with icons that are not in array",
4 | "icons": {
5 | "16": "img/icons/icon16.png",
6 | "32": "img/icons/icon32.png",
7 | "60": "img/icons/icon60.png",
8 | "64": "img/icons/icon64.png",
9 | "90": "img/icons/icon90.png",
10 | "128": "img/icons/icon128.png"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/views/includes/icon_log_out.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/views/includes/icon_search.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | Julian Toledo -- @juliantoledo
7 | Michael Stillwell -- @ithinkihaveacat
8 | Andre Bandarra -- @andreban
9 | Alberto Medina -- @amedina
10 |
11 | # THANKS
12 |
13 | Ade Oshineye -- @ade_oshineye
14 |
15 | # TECHNOLOGY COLOPHON
16 |
17 | CSS3, HTML5, GoogleChrome sw-toolbox
18 | Node, Google App Engine, GoogleChrome Lighthouse
19 |
20 | Source: https://github.com/GoogleChrome/gulliver
21 |
--------------------------------------------------------------------------------
/views/includes/icon_share.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/views/includes/notifications_off.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/views/includes/notifications_active.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lighthouse_machine/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lighthouse_machine_server",
3 | "version": "1.0.0",
4 | "description": "A server for the lighthouse machine",
5 | "repository": "https://github.com/GoogleChrome/gulliver/lighthouse_machine",
6 | "author": "Google Inc.",
7 | "contributors": [
8 | {
9 | "name": "Cedric Bellet",
10 | "email": "cbellet@google.com"
11 | },
12 | {
13 | "name": "Julian Toledo",
14 | "email": "jtoledo@google.com"
15 | }
16 | ],
17 | "license": "Apache-2.0",
18 | "main": "server.js",
19 | "scripts": {
20 | "start": "node server.js"
21 | },
22 | "dependencies": {
23 | "express": "^4.16.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/views/includes/lighthouse.hbs:
--------------------------------------------------------------------------------
1 |
2 | Lighthouse {{> score pwa}}
3 |
4 |
No lighthouse data available
5 |
6 | {{#if pwa.lighthouseScore}}
7 |
13 | {{/if}}
14 |
15 |
--------------------------------------------------------------------------------
/lighthouse_machine/README.md:
--------------------------------------------------------------------------------
1 | # Lighthouse machine
2 | A Docker image to run [Lighthouse](https://github.com/GoogleChrome/lighthouse) scores on a server
3 |
4 | ## Build the image
5 | ```bash
6 | docker build --no-cache -t lighthouse_machine .
7 | ```
8 |
9 | ## Run the container
10 | ```bash
11 | # Run a new container
12 | docker run -d -p 8080:8080 --cap-add=SYS_ADMIN lighthouse_machine
13 | ```
14 |
15 | ## Usage
16 | ```bash
17 | curl -X GET 'http://localhost:8080?format=${format}&url=${url}'
18 | ```
19 |
20 | where `format`is one of `json`, `html` (see [cli-options](https://github.com/GoogleChrome/lighthouse#cli-options) for more information)
21 |
22 | ## License
23 | See [LICENSE](./LICENSE) for more.
24 |
25 | ## Disclaimer
26 | This is not a Google product.
27 |
--------------------------------------------------------------------------------
/lib/event-bus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const EventEmitter = require('events');
19 | const messageBus = new EventEmitter();
20 | module.exports = messageBus;
21 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PWA Directory",
3 | "short_name": "PwaDirectory",
4 | "description": "A Directory of PWAs",
5 | "start_url": "/?utm_source=homescreen",
6 | "icons": [
7 | {
8 | "src": "\/favicons\/android-chrome-192x192.png",
9 | "sizes": "192x192",
10 | "type": "image\/png"
11 | },
12 | {
13 | "src": "\/favicons\/android-chrome-512x512.png",
14 | "sizes": "512x512",
15 | "type": "image\/png"
16 | }
17 | ],
18 | "theme_color": "#7cc0ff",
19 | "background_color": "#7cc0ff",
20 | "display": "standalone",
21 | "scope": "/",
22 | "share_target": {
23 | "url_template": "/pwas/add?url={url}"
24 | },
25 | "//": "Some browsers will use this to enable push notifications.",
26 | "//": "It is the same for all projects, this is not your project's sender ID",
27 | "gcm_sender_id": "103953800507"
28 | }
29 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2015-2016, Google, Inc.
2 | # Licensed under the Apache License, Version 2.0 (the "License");
3 | # you may not use this file except in compliance with the License.
4 | # You may obtain a copy of the License at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # Unless required by applicable law or agreed to in writing, software
9 | # distributed under the License is distributed on an "AS IS" BASIS,
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | # See the License for the specific language governing permissions and
12 | # limitations under the License.
13 | #
14 | runtime: nodejs
15 | env: flexible
16 |
17 | instance_class: B4_1G
18 | manual_scaling:
19 | instances: 2
20 |
21 | handlers:
22 | - url: /.*
23 | script: IGNORED
24 | secure: always
25 |
26 | network:
27 | instance_tag: default-service
28 |
--------------------------------------------------------------------------------
/rollup-config/gulliver.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import uglify from 'rollup-plugin-uglify';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import commonsjs from 'rollup-plugin-commonjs';
5 |
6 | export default {
7 | entry: './public/js/gulliver.es6.js',
8 | plugins: [
9 | babel({exclude: 'node_modules/**'}),
10 | uglify(),
11 | nodeResolve(),
12 | commonsjs()
13 | ],
14 | // Quiet warning: https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined
15 | context: 'window',
16 | targets: [
17 | {
18 | dest: './public/js/gulliver.js',
19 | // Fixes 'navigator' not defined when using Firebase and strict mode:
20 | // http://stackoverflow.com/questions/31221357/webpack-firebase-disable-parsing-of-firebase
21 | useStrict: false,
22 | format: 'iife',
23 | sourceMap: true
24 | }
25 | ]
26 | };
27 |
--------------------------------------------------------------------------------
/rollup-config/pwa-form.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import uglify from 'rollup-plugin-uglify';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import commonsjs from 'rollup-plugin-commonjs';
5 |
6 | export default {
7 | entry: './public/js/pwa-form.es6.js',
8 | plugins: [
9 | babel({exclude: 'node_modules/**'}),
10 | uglify(),
11 | nodeResolve(),
12 | commonsjs()
13 | ],
14 | // Quiet warning: https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined
15 | context: 'window',
16 | targets: [
17 | {
18 | dest: './public/js/pwa-form.js',
19 | // Fixes 'navigator' not defined when using Firebase and strict mode:
20 | // http://stackoverflow.com/questions/31221357/webpack-firebase-disable-parsing-of-firebase
21 | useStrict: false,
22 | format: 'iife',
23 | sourceMap: true
24 | }
25 | ]
26 | };
27 |
--------------------------------------------------------------------------------
/rollup-config/lighthouse-chart.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import uglify from 'rollup-plugin-uglify';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import commonsjs from 'rollup-plugin-commonjs';
5 |
6 | export default {
7 | entry: './public/js/lighthouse-chart.es6.js',
8 | plugins: [
9 | babel({exclude: 'node_modules/**'}),
10 | uglify(),
11 | nodeResolve(),
12 | commonsjs()
13 | ],
14 | // Quiet warning: https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined
15 | context: 'window',
16 | targets: [
17 | {
18 | dest: './public/js/lighthouse-chart.js',
19 | // Fixes 'navigator' not defined when using Firebase and strict mode:
20 | // http://stackoverflow.com/questions/31221357/webpack-firebase-disable-parsing-of-firebase
21 | useStrict: false,
22 | format: 'iife',
23 | sourceMap: true
24 | }
25 | ]
26 | };
27 |
--------------------------------------------------------------------------------
/lighthouse_machine/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2016-2017, Google, Inc.
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | /etc/init.d/dbus start
17 | /etc/init.d/xvfb start
18 | sleep 1s
19 |
20 | export DISPLAY=:1
21 | TMP_PROFILE_DIR=$(mktemp -d -t lighthouse.XXXXXXXXXX)
22 |
23 | su chromeuser
24 | source /chromeuser-script.sh
25 | sleep 3s
26 |
27 | node /server.js
28 |
--------------------------------------------------------------------------------
/lighthouse_machine/chromeuser-script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2016-2017, Google, Inc.
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | sudo chown -R chromeuser:chromeuser $TMP_PROFILE_DIR
17 | export DISPLAY=:0
18 | Xvfb :0 -screen 0 1024x768x24 &
19 | nohup google-chrome --no-first-run --disable-gpu --no-sandbox --user-data-dir=$TMP_PROFILE_DIR --remote-debugging-port=9222 'about:blank' &
20 |
--------------------------------------------------------------------------------
/models/favorite-pwa.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | /**
19 | * Favorite Pwa for a user
20 | */
21 | class FavoritePwa {
22 | constructor(pwaId, userId) {
23 | this.id = pwaId + '-' + userId;
24 | this.pwaId = pwaId;
25 | this.userId = userId;
26 | }
27 | }
28 |
29 | module.exports = FavoritePwa;
30 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | {
16 | "env": {
17 | "test": {
18 | "presets": ["es2015"]
19 | },
20 | "default": {
21 | "presets": [
22 | [
23 | "es2015",
24 | {
25 | "modules": false
26 | }
27 | ]
28 | ],
29 | "plugins": ["external-helpers"]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models/task.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | class Task {
19 | constructor(pwaId, modulePath, functionName, retries) {
20 | this.pwaId = pwaId;
21 | this.modulePath = modulePath;
22 | this.functionName = functionName;
23 | this.retries = retries;
24 | this.created = new Date();
25 | }
26 | }
27 |
28 | module.exports = Task;
29 |
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const crypto = require('crypto');
19 |
20 | /**
21 | * User from google-auth-library-nodejs client
22 | */
23 | class User {
24 | constructor(googleLogin) {
25 | this.id = crypto.createHash('sha1').update(googleLogin.getPayload().sub).digest('hex');
26 | }
27 | }
28 |
29 | module.exports = User;
30 |
--------------------------------------------------------------------------------
/lighthouse_machine/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "google",
3 | "installedESLint": true,
4 | // http://eslint.org/docs/rules/
5 | "rules": {
6 | "max-len": [2, 100, {
7 | "ignoreComments": true,
8 | "ignoreUrls": true,
9 | "tabWidth": 2
10 | }],
11 | "no-implicit-coercion": [2, {
12 | "boolean": false,
13 | "number": true,
14 | "string": true
15 | }],
16 | "no-unused-expressions": [2, {
17 | "allowShortCircuit": true,
18 | "allowTernary": false
19 | }],
20 | "no-unused-vars": [2, {
21 | "vars": "all",
22 | "args": "after-used",
23 | "argsIgnorePattern": "(^reject$|^_$)",
24 | "varsIgnorePattern": "(^_$)"
25 | }],
26 | "quotes": [2, "single"],
27 | "require-jsdoc": 0,
28 | "valid-jsdoc": 0,
29 | "prefer-arrow-callback": 1,
30 | "no-var": 1
31 | },
32 | // http://eslint.org/docs/user-guide/configuring#specifying-environments
33 | "env": {
34 | "node": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "google",
3 | // http://eslint.org/docs/rules/
4 | "rules": {
5 | "max-len": [2, 100, {
6 | "ignoreComments": true,
7 | "ignoreUrls": true,
8 | "tabWidth": 2
9 | }],
10 | "no-implicit-coercion": [2, {
11 | "boolean": false,
12 | "number": true,
13 | "string": true
14 | }],
15 | "no-unused-expressions": [2, {
16 | "allowShortCircuit": true,
17 | "allowTernary": false
18 | }],
19 | "no-unused-vars": [2, {
20 | "vars": "all",
21 | "args": "after-used",
22 | "argsIgnorePattern": "(^reject$|^_$)",
23 | "varsIgnorePattern": "(^_$)"
24 | }],
25 | "quotes": [2, "single"],
26 | "require-jsdoc": 0,
27 | "valid-jsdoc": 0,
28 | "prefer-arrow-callback": 1,
29 | "no-var": 1
30 | },
31 | // http://eslint.org/docs/user-guide/configuring#specifying-environments
32 | "env": {
33 | "node": true
34 | },
35 | "parserOptions": {
36 | "ecmaVersion": 2017
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/views/app/shell.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {{> head}}
18 |
19 |
20 | {{> header}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{> footer}}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/controllers/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const router = express.Router(); // eslint-disable-line new-cap
20 |
21 | router.get('/shell', (req, res) => {
22 | res.render('app/shell.hbs');
23 | });
24 |
25 | router.get('/offline', (req, res, next) => { // eslint-disable-line no-unused-vars
26 | res.render('app/offline.hbs');
27 | });
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/views/app/offline.hbs:
--------------------------------------------------------------------------------
1 | {{!--
2 | Copyright 2015-2016, Google, Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | --}}
16 |
17 |
18 |
19 | Gulliver - PWA Directory - Offline
20 | {{> head}}
21 |
22 |
23 | {{> header}}
24 | Offline
25 |
31 | {{> footer}}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/public/js/gulliver-config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export default class Config {
19 | constructor(element) {
20 | if (!element) {
21 | console.log('%cConfig not found', 'color:red');
22 | return;
23 | }
24 | const configJSON = JSON.parse(element.innerHTML);
25 | Object.assign(this, configJSON);
26 | }
27 |
28 | static from(element) {
29 | return new Config(element);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/views/includes/pwadetails.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if pwa.iconUrl128}}
5 |
6 | {{else}}
7 |
8 |
9 | {{firstLetter pwa.name}}
10 |
11 | {{/if}}
12 |
13 | {{pwa.displayName}}
14 | {{pwa.absoluteStartUrl}}
15 | {{pwa.description}}
16 | {{#if pwa.created}}Added {{moment pwa.created}}, Updated {{moment pwa.updated}}{{/if}}
17 |
18 |
--------------------------------------------------------------------------------
/config/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "//": "See README.md for more information about what to put here",
3 | "GCLOUD_PROJECT": "run `gcloud config get-value project`",
4 | "CLOUD_BUCKET": "see https://console.cloud.google.com/storage/browser?project=$GCLOUD_PROJECT",
5 | "CLIENT_ID": "see https://console.cloud.google.com/apis/credentials?project=$GCLOUD_PROJECT",
6 | "CLIENT_SECRET": "see https://console.cloud.google.com/apis/credentials?project=$GCLOUD_PROJECT",
7 | "WEBPERFORMANCE_SERVER": "your Web Performance server URL (optional)",
8 | "WEBPERFORMANCE_SERVER_API_KEY": "your Key for the Web Performance Service",
9 | "GOOGLE_ANALYTICS": "your Google Analytics tracking code (optional)",
10 | "CANONICAL_ROOT": "your website root address. Can be http://localhost:8080 in development",
11 | "FIREBASE_AUTH": "the 'Server key' (optional); see https://console.firebase.google.com/project/$FIREBASE_PROJECT/settings/cloudmessaging",
12 | "FIREBASE_MSG_SENDER_ID": "the 'Sender ID' (optional); see https://console.firebase.google.com/project/$FIREBASE_PROJECT/settings/cloudmessaging"
13 | }
14 |
--------------------------------------------------------------------------------
/views/404.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 | {{#unless contentOnly}}
15 |
16 |
17 |
18 | {{> head}}
19 |
20 |
21 | {{> header}}
22 |
23 |
24 | {{/unless}}
25 | Page Not Found
26 | {{#unless contentOnly}}
27 |
28 |
29 |
30 |
31 | {{> footer}}
32 |
33 |
34 | {{/unless}}
35 |
--------------------------------------------------------------------------------
/cron.yaml:
--------------------------------------------------------------------------------
1 | cron:
2 | - description: (Node) Daily PWA info update job
3 | url: /tasks/cron
4 | schedule: every day 13:00
5 |
6 | - description: (Node) Execute PWA update tasks
7 | url: /tasks/execute?tasks=30
8 | schedule: every 1 minutes
9 |
10 | - description: (Node) Update unscored PWAs
11 | url: /tasks/updateunscored
12 | schedule: every 1 hours
13 |
14 | - description: Update unscored PWAs
15 | url: /taskcreator/task?unscored=true
16 | schedule: every 1 hours
17 | target: web-performance
18 |
19 | - description: UpdateManifestTask
20 | url: /taskcreator/task/UpdateManifestTask
21 | schedule: every day 16:00
22 | target: web-performance
23 |
24 | - description: UpdateIconTask
25 | url: /taskcreator/task/UpdateIconTask
26 | schedule: every monday 01:00
27 | target: web-performance
28 |
29 | - description: PageSpeedReportTask
30 | url: /taskcreator/task/PageSpeedReportTask
31 | schedule: every friday 01:00
32 | target: web-performance
33 |
34 | - description: WebPageTestReportTask
35 | url: /taskcreator/task/WebPageTestReportTask
36 | schedule: every sunday 01:00
37 | target: web-performance
38 |
--------------------------------------------------------------------------------
/firebase-messaging-sw.tmpl:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env serviceworker, browser */
17 | /* global firebase */
18 | importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-app.js');
19 | importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-messaging.js');
20 |
21 | firebase.initializeApp({
22 | messagingSenderId: '<%= firebaseMsgSenderId %>'
23 | });
24 | const messaging = firebase.messaging();
25 | messaging.setBackgroundMessageHandler(_ => {
26 | return self.registration.showNotification();
27 | });
28 |
--------------------------------------------------------------------------------
/third_party/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # Downloads and patches the two files from lighthouse and devtools that we need
4 | # to validate manifests.
5 | #
6 | # (These files are part of lighthouse, but depending on lighthouse would haul in
7 | # multiple MBs of dependencies. So, we'll just dump the files here.)
8 |
9 | curl -sO https://raw.githubusercontent.com/GoogleChrome/lighthouse/master/lighthouse-core/lib/manifest-parser.js
10 | curl -sO https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/master/front_end/common/Color.js
11 |
12 | # Generate patch via `git diff manifest-parser.js Color.js`
13 | git apply - << END
14 | diff --git i/third_party/manifest-parser.js w/third_party/manifest-parser.js
15 | index 5ac9d72..b9e7ead 100644
16 | --- i/third_party/manifest-parser.js
17 | +++ w/third_party/manifest-parser.js
18 | @@ -17,7 +17,10 @@
19 | 'use strict';
20 |
21 | const url = require('url');
22 | -const validateColor = require('./web-inspector').Color.parse;
23 | +
24 | +global.Common = {}; // the global is unfortunate, but necessary
25 | +require('./Color.js');
26 | +const validateColor = global.Common.Color.parse;
27 |
28 | const ALLOWED_DISPLAY_VALUES = [
29 | 'fullscreen',
30 | END
31 |
--------------------------------------------------------------------------------
/lib/promise-sequential.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | /** Execute a list of Promise return functions serially
19 | * @param {list} a list of promise returning functions to execute serially
20 | * @return {Promise} the result of the last promise in the list
21 | * Example:
22 | * promiseSequential.all([
23 | * _ => this.function1(result),
24 | * result => this.function2(result),
25 | * result => this.function3(result)
26 | * ]);
27 | */
28 | exports.all = function(promiseList) {
29 | return promiseList.reduce((promiseFn, fn) => {
30 | return promiseFn.then(fn);
31 | }, Promise.resolve());
32 | };
33 |
--------------------------------------------------------------------------------
/controllers/api/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const router = express.Router(); // eslint-disable-line new-cap
20 |
21 | // Includes APIs for Lighthouse (/api/lighthouse)
22 | router.use('/lighthouse', require('./lighthouse'));
23 |
24 | // Includes APIs for Notifications (/api/notifications)
25 | router.use('/notifications', require('./notifications'));
26 |
27 | // Includes APIs for FavoritePwas (/api/favoritepwa)
28 | router.use('/favorite-pwa', require('./favorite-pwa'));
29 |
30 | // Includes APIs for PWAs (/api/pwa)
31 | router.use('/pwa', require('./pwa'));
32 |
33 | module.exports = router;
34 |
--------------------------------------------------------------------------------
/public/js/util/requestIdleCallback.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2015 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 | * or implied. See the License for the specific language governing
14 | * permissions and limitations under the License.
15 | */
16 |
17 | /* eslint-env browser */
18 |
19 | /*
20 | * @see https://developers.google.com/web/updates/2015/08/using-requestidlecallback
21 | */
22 | window.requestIdleCallback = window.requestIdleCallback ||
23 | (cb => {
24 | return setTimeout(_ => {
25 | let start = Date.now();
26 | cb({
27 | didTimeout: false,
28 | timeRemaining: _ => {
29 | return Math.max(0, 50 - (Date.now() - start));
30 | }
31 | });
32 | }, 1);
33 | });
34 |
35 | window.cancelIdleCallback = window.cancelIdleCallback ||
36 | (id => {
37 | clearTimeout(id);
38 | });
39 |
--------------------------------------------------------------------------------
/controllers/sw.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const router = express.Router(); // eslint-disable-line new-cap
20 | const asset = require('../lib/asset-hashing').asset;
21 |
22 | const ASSETS = JSON.stringify([
23 | '/css/style.css',
24 | '/js/gulliver.js'
25 | ].map(assetPath => asset.encode(assetPath)));
26 |
27 | const ASSETS_JS = `const ASSETS = ${ASSETS};`;
28 |
29 | router.get('/sw-assets-precache.js', (req, res) => {
30 | res.setHeader('Content-Type', 'application/javascript');
31 | res.setHeader('Cache-Control', 'no-cache, max-age=0');
32 | res.send(ASSETS_JS);
33 | });
34 |
35 | module.exports = router;
36 |
--------------------------------------------------------------------------------
/lib/verify-id-token.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const config = require('../config/config');
19 | const CLIENT_ID = config.get('CLIENT_ID');
20 | const CLIENT_SECRET = config.get('CLIENT_SECRET');
21 |
22 | /**
23 | * @param {string} idToken
24 | * @return {Promise}
25 | */
26 | exports.verifyIdToken = function(idToken) {
27 | const {OAuth2Client} = require('google-auth-library');
28 | const client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET);
29 | return new Promise((resolve, reject) => {
30 | client.verifyIdToken({idToken, CLIENT_ID}, (err, googleLogin) => {
31 | if (err) {
32 | reject(err);
33 | }
34 | resolve(googleLogin);
35 | });
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/lighthouse_machine/etc/xvfb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2016-2017, Google, Inc.
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | XVFB_OUTPUT=/tmp/Xvfb.out
17 | XVFB=/usr/bin/X11/Xvfb
18 | XVFB_OPTIONS=":1 -screen 0 1024x768x24 -fbdir /var/run"
19 |
20 | start() {
21 | echo -n "Starting : X Virtual Frame Buffer "
22 | $XVFB $XVFB_OPTIONS >>$XVFB_OUTPUT 2>&1&
23 | RETVAL=$?
24 | echo
25 | return $RETVAL
26 | }
27 |
28 | stop() {
29 | echo -n "Shutting down : X Virtual Frame Buffer"
30 | echo
31 | pkill Xvfb
32 | echo
33 | return 0
34 | }
35 |
36 | case "$1" in
37 | start)
38 | start
39 | ;;
40 | stop)
41 | stop
42 | ;;
43 | status)
44 | status xvfb
45 | ;;
46 | restart)
47 | stop
48 | start
49 | ;;
50 |
51 | *)
52 | echo "Usage: xvfb {start|stop|status|restart}"
53 | exit 1
54 | ;;
55 | esac
56 | exit $?
57 |
--------------------------------------------------------------------------------
/public/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/firebase-messaging-sw-generator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const fs = require('fs');
19 | const template = require('lodash.template');
20 | const config = require('./config/config');
21 |
22 | const firebaseMsgSenderId = config.get('FIREBASE_MSG_SENDER_ID');
23 |
24 | fs.readFile('./firebase-messaging-sw.tmpl', 'utf8', (error, data) => {
25 | if (error) {
26 | console.error('Error reading template: ', error);
27 | return;
28 | }
29 |
30 | const firebaseMessagingSwFileContent = template(data)({
31 | firebaseMsgSenderId: firebaseMsgSenderId
32 | });
33 |
34 | fs.writeFile('./public/firebase-messaging-sw.js', firebaseMessagingSwFileContent, err => {
35 | if (err) {
36 | console.log('Error Writing firebase-messaging-sw: ', err);
37 | }
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/views/pwas/view-rss.hbs:
--------------------------------------------------------------------------------
1 |
11 |
12 |
21 |
22 |
23 |
24 | Open in PWA Directory
25 |
26 |
--------------------------------------------------------------------------------
/views/pwas/view.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 | {{#unless contentOnly}}
15 |
16 |
17 |
18 | {{> head}}
19 |
20 |
21 | {{> header}}
22 |
23 |
24 | {{/unless}}
25 | {{> pwadetails pwa=pwa}}
26 | {{> lighthouse lighthouse=lighthouse}}
27 | {{> webpagetest}}
28 | {{> pagespeedinsight}}
29 | {{#if pwa.manifest}}
30 |
31 |
32 |
{{{highlightedJson rawManifestJson}}}
33 |
34 | {{/if}}
35 | {{#unless contentOnly}}
36 |
37 |
38 |
39 |
40 | {{> footer}}
41 |
42 |
43 | {{/unless}}
44 |
--------------------------------------------------------------------------------
/lighthouse_machine/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2016-2017, Google, Inc.
2 | # Licensed under the Apache License, Version 2.0 (the "License");
3 | # you may not use this file except in compliance with the License.
4 | # You may obtain a copy of the License at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # Unless required by applicable law or agreed to in writing, software
9 | # distributed under the License is distributed on an "AS IS" BASIS,
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | # See the License for the specific language governing permissions and
12 | # limitations under the License.
13 |
14 | runtime: custom
15 | env: flex
16 | service: lighthouse-machine
17 | automatic_scaling:
18 | min_num_instances: 2
19 | max_num_instances: 6
20 | cool_down_period_sec: 60
21 | cpu_utilization:
22 | target_utilization: 0.6
23 |
24 | resources:
25 | cpu: 1
26 | memory_gb: 4
27 | disk_size_gb: 10
28 |
29 | handlers:
30 | - url: /.*
31 | script: IGNORED
32 | secure: always
33 |
34 | liveness_check:
35 | path: '/_ah/health'
36 | check_interval_sec: 30
37 | timeout_sec: 4
38 | failure_threshold: 3
39 | success_threshold: 2
40 | initial_delay_sec: 60
41 |
42 | readiness_check:
43 | path: '/_ah/busy'
44 | check_interval_sec: 3
45 | timeout_sec: 2
46 | failure_threshold: 1
47 | success_threshold: 1
48 | app_start_timeout_sec: 300
49 |
50 | network:
51 | instance_tag: lighthouse-machine
52 |
--------------------------------------------------------------------------------
/lib/metadata.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | /**
19 | * Generates the default metadata from a http request
20 | */
21 | module.exports.fromRequest = function(req, newUrl) {
22 | const host = req.get('host');
23 | const url = newUrl || req.protocol + '://' + host + req.originalUrl;
24 | const timestamp = new Date().toISOString();
25 | const logo = req.protocol + '://' + host + '/favicons/android-chrome-512x512.png';
26 | const leader = req.protocol + '://' + host + '/img/pwa-directory-preview.png';
27 | const metadata = {
28 | url: url,
29 | host: host,
30 | datePublished: timestamp,
31 | dateModified: timestamp,
32 | logo: logo,
33 | logoWidth: '512',
34 | logoHeight: '512',
35 | leader: leader,
36 | leaderWidth: '2008',
37 | leaderHeight: '1386'
38 | };
39 | return metadata;
40 | };
41 |
--------------------------------------------------------------------------------
/public/js/routing/transitions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 | import Loader from '../loader';
18 |
19 | export class FadeInOutTransitionStrategy {
20 | transitionIn(container) {
21 | container.classList.remove('transition');
22 | }
23 |
24 | transitionOut(container) {
25 | container.classList.add('transition');
26 | }
27 | }
28 |
29 | export class LoaderTransitionStrategy {
30 | constructor(window) {
31 | this._window = window;
32 | const loaderDiv = window.document.querySelector('.page-loader');
33 | this._loader = new Loader(loaderDiv);
34 | }
35 |
36 | transitionIn(container) {
37 | container.classList.remove('transition');
38 | this._loader.hide();
39 | }
40 |
41 | transitionOut(container) {
42 | container.classList.add('transition');
43 | this._loader.show();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Want to contribute? Great! First, read this page (including the small print at the end).
2 |
3 | ### Before you contribute
4 | Before we can use your code, you must sign the
5 | [Google Individual Contributor License Agreement]
6 | (https://cla.developers.google.com/about/google-individual)
7 | (CLA), which you can do online. The CLA is necessary mainly because you own the
8 | copyright to your changes, even after your contribution becomes part of our
9 | codebase, so we need your permission to use and distribute your code. We also
10 | need to be sure of various other things—for instance that you'll tell us if you
11 | know that your code infringes on other people's patents. You don't have to sign
12 | the CLA until after you've submitted your code for review and a member has
13 | approved it, but you must do it before we can put your code into our codebase.
14 | Before you start working on a larger contribution, you should get in touch with
15 | us first through the issue tracker with your idea so that we can help out and
16 | possibly guide you. Coordinating up front makes it much easier to avoid
17 | frustration later on.
18 |
19 | ### Code reviews
20 | All submissions, including submissions by project members, require review. We
21 | use Github pull requests for this purpose.
22 |
23 | ### The small print
24 | Contributions made by corporations are covered by a different agreement than
25 | the one above, the
26 | [Software Grant and Corporate Contributor License Agreement]
27 | (https://cla.developers.google.com/about/google-corporate).
28 |
--------------------------------------------------------------------------------
/public/js/event-target.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | export default class EventTarget {
17 | constructor() {
18 | this._listeners = new Map();
19 | }
20 |
21 | addEventListener(type, callback) {
22 | let typeListeners = this._listeners.get(type);
23 | if (!typeListeners) {
24 | typeListeners = new Set();
25 | this._listeners.set(type, typeListeners);
26 | }
27 | typeListeners.add(callback);
28 | }
29 |
30 | removeEventListener(type, callback) {
31 | const typeListeners = this._listeners.get(type);
32 | if (!typeListeners) {
33 | return;
34 | }
35 | typeListeners.delete(callback);
36 | }
37 |
38 | getEventListeners(type) {
39 | return this._listeners.get(type);
40 | }
41 |
42 | dispatchEvent(event) {
43 | if (!event.type) {
44 | return;
45 | }
46 |
47 | const typeListeners = this._listeners.get(event.type);
48 | if (!typeListeners) {
49 | return;
50 | }
51 |
52 | typeListeners.forEach(callback => callback(event));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/public/js/search-input.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /*
17 | * Generate gulliver.js from this file via `npm prestart`. (`npm start` will run
18 | * `prestart` automatically.)
19 | */
20 |
21 | /* eslint-env browser */
22 |
23 | class SearchButton {
24 | /**
25 | * Setup/configure search button
26 | */
27 | setupSearchElements(router) {
28 | const eventHandler = event => {
29 | event.preventDefault();
30 | const searchValue = document.querySelector('#search-input').value;
31 | if (searchValue.length === 0) {
32 | router.navigate('/');
33 | } else {
34 | const urlParams = new URLSearchParams(window.location.search);
35 | // Only navigate if the search query changes
36 | if (searchValue !== urlParams.get('query')) {
37 | router.navigate('/pwas/search?query=' + searchValue);
38 | }
39 | document.querySelector('#search-input').blur();
40 | }
41 | };
42 | document.querySelector('#search').addEventListener('submit', eventHandler);
43 | }
44 | }
45 |
46 | export default new SearchButton();
47 |
--------------------------------------------------------------------------------
/test/client/js/event-target.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it beforeEach afterEach */
17 | 'use strict';
18 | import EventTarget from '../../../public/js/event-target';
19 | const assert = require('assert');
20 | const simpleMock = require('simple-mock');
21 |
22 | describe('js.event-target', () => {
23 | let eventTarget;
24 | const callbackA = simpleMock.spy(() => {});
25 |
26 | beforeEach(() => {
27 | eventTarget = new EventTarget();
28 | });
29 |
30 | afterEach(() => {
31 | simpleMock.restore();
32 | callbackA.reset();
33 | });
34 |
35 | it('Fires the correct callback', () => {
36 | eventTarget.addEventListener('event-a', callbackA);
37 | eventTarget.dispatchEvent({type: 'event-a'});
38 | assert.equal(callbackA.callCount, 1);
39 | });
40 |
41 | it('Does not invoke a removed callback', () => {
42 | eventTarget.addEventListener('event-a', callbackA);
43 | eventTarget.removeEventListener('event-a', callbackA);
44 | eventTarget.dispatchEvent({type: 'event-a'});
45 | assert.equal(callbackA.callCount, 0);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/views/includes/footer.hbs:
--------------------------------------------------------------------------------
1 | {{!-- Copyright 2015-2016, Google, Inc.
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License. --}}
13 |
14 |
15 |
16 |
17 |
18 | Offline
19 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/controllers/api/lighthouse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const lighthouseLib = require('../../lib/lighthouse');
20 | const router = express.Router(); // eslint-disable-line new-cap
21 | const CACHE_CONTROL_EXPIRES = 60 * 60 * 24; // 1 day.
22 |
23 | /**
24 | * GET /api/lighthouse-graph/:pwaId
25 | *
26 | * Returns the Lighthouse Graph information for a PWA
27 | * it uses the Google Charts JSON format:
28 | * https://developers.google.com/chart/interactive/docs/reference#dataparam
29 | */
30 | router.get('/graph/:pwaId', (req, res) => {
31 | res.setHeader('Content-Type', 'application/json');
32 |
33 | lighthouseLib.getLighthouseGraphByPwaId(req.params.pwaId)
34 | .then(lighthouseGraph => {
35 | if (lighthouseGraph) {
36 | res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);
37 | res.json(lighthouseGraph);
38 | } else {
39 | res.status(404);
40 | res.json('not found');
41 | }
42 | })
43 | .catch(err => {
44 | res.status(500);
45 | res.json(err);
46 | });
47 | });
48 |
49 | module.exports = router;
50 |
--------------------------------------------------------------------------------
/public/js/ui/share-button.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export class ShareButton {
19 | constructor(window, element, nameElement) {
20 | this.element = element;
21 | this._nameElement = nameElement;
22 | this._window = window;
23 | this._init();
24 | }
25 |
26 | _init() {
27 | if (!this._window.navigator.share) {
28 | return;
29 | }
30 | this.element.classList.remove('hidden');
31 | this._setupEventListeners();
32 | }
33 |
34 | _setupEventListeners() {
35 | const clickListener = () => {
36 | this.share();
37 | };
38 | this.element.addEventListener('click', clickListener);
39 | }
40 |
41 | _getTitle() {
42 | const pwaName = this._window.document.querySelector('#pwa-name');
43 | if (!pwaName) {
44 | return 'PWA Directory';
45 | }
46 | return pwaName.innerText.trim();
47 | }
48 |
49 | share() {
50 | const title = this._getTitle();
51 | this._window.navigator.share({
52 | title,
53 | url: this._window.location.href
54 | }).catch(err => {
55 | console.log(`Share failed, reason: ${err}`);
56 | });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/controllers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const router = express.Router(); // eslint-disable-line new-cap
20 | const config = require('../config/config');
21 | const bodyParser = require('body-parser');
22 |
23 | router.use(bodyParser.json());
24 |
25 | // API
26 | router.use('/api', require('./api'));
27 |
28 | // Tasks
29 | router.use('/tasks', require('./tasks'));
30 |
31 | // PWAs
32 | router.use('/pwas', require('./pwa'));
33 |
34 | router.get('/', (req, res) => {
35 | req.url = '/pwas';
36 | router.handle(req, res);
37 | });
38 |
39 | router.get('/installable', (req, res) => {
40 | req.url = '/pwas/installable';
41 | router.handle(req, res);
42 | });
43 |
44 | // ServiceWorker
45 | router.use('/js', require('./sw'));
46 |
47 | // /.shell hosts app shell dependencies
48 | router.use('/.app', require('./app'));
49 |
50 | /**
51 | * This route is used to send config.json to firebase-messaging-sw.js
52 | */
53 | router.get('/messaging-config.json', (req, res) => {
54 | // eslint-disable-next-line camelcase
55 | res.json({firebase_msg_sender_id: config.get('FIREBASE_MSG_SENDER_ID')});
56 | });
57 |
58 | module.exports = router;
59 |
--------------------------------------------------------------------------------
/controllers/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const router = express.Router(); // eslint-disable-line new-cap
20 | const libCache = require('../lib/data-cache');
21 |
22 | const CACHE_LIFETIME = 60 * 60; // 1 hour
23 |
24 | /**
25 | * GET *
26 | *
27 | * Serves cached HTML or
28 | * overrides res.send to be able to cache rendered HTML before sending.
29 | */
30 | router.get('*', (req, res, next) => {
31 | const url = req.originalUrl;
32 | libCache.get(url)
33 | .then(cachedHtml => {
34 | console.log('From cache: ' + url);
35 | res.send(cachedHtml);
36 | })
37 | .catch(_ => {
38 | // Overrides res.send to be able to cache before sending.
39 | res.sendResponse = res.send;
40 | res.send = body => {
41 | libCache.set(url, body, CACHE_LIFETIME)
42 | .then(_ => {
43 | libCache.storeCachedUrls(url);
44 | console.log('Stored in cache: ' + url);
45 | })
46 | .catch(_ => {
47 | console.log('Error setting cache for: ' + url);
48 | });
49 | res.sendResponse(body);
50 | };
51 | next();
52 | });
53 | });
54 |
55 | module.exports = router;
56 |
--------------------------------------------------------------------------------
/views/pwas/form.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 | {{#unless contentOnly}}
15 |
16 |
17 |
18 | {{> head}}
19 |
20 |
21 | {{> header}}
22 |
23 |
24 | {{/unless}}
25 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{#unless contentOnly}}
46 |
47 |
48 |
49 |
50 | {{> footer}}
51 |
52 |
53 | {{/unless}}
54 |
--------------------------------------------------------------------------------
/public/js/routing/route.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | import 'url-polyfill/url-polyfill';
19 |
20 | export default class Route {
21 | constructor(matchRegex, transitionStrategy, onAttached) {
22 | this._transitionStrategy = transitionStrategy;
23 | this._matchRegex = matchRegex;
24 | this._onAttached = onAttached;
25 | }
26 |
27 | matches(url) {
28 | return this._matchRegex.test(url);
29 | }
30 |
31 | retrieveContent(url) {
32 | const contentUrl = this.getContentOnlyUrl(url);
33 | return fetch(contentUrl)
34 | .then(response => response.text());
35 | }
36 |
37 | transitionOut(container) {
38 | this._transitionStrategy.transitionOut(container);
39 | }
40 |
41 | transitionIn(container) {
42 | this._transitionStrategy.transitionIn(container);
43 | }
44 |
45 | onAttached() {
46 | if (this._onAttached && Array.isArray(this._onAttached)) {
47 | this._onAttached.forEach(onAttached => {
48 | onAttached && onAttached();
49 | });
50 | return;
51 | }
52 | return this._onAttached && this._onAttached();
53 | }
54 |
55 | getContentOnlyUrl(url) {
56 | const u = new URL(url);
57 | u.searchParams.append('contentOnly', 'true');
58 | return u.toString();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/test/app/manifests/inline-image-large-content.json:
--------------------------------------------------------------------------------
1 | {"short_name":"Twitter","name":"Twitter","icons":[{"src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAk1BMVEX///8dofJ9yfggo/IeofLF5/wnpvM6rfT7/f8sqPM0q/MkpPLs9/7l9P7Q7P2k2fr3/P+T0vnw+f7g8/2FzfhevPZWufVKtPXz+v604Pt2x/dAsPT9/v+Lz/lwxPdrwvdRt/Wv3vqr3PpjvvbL6vy64vua1vlau/bZ7/2+5PtnwPbd8f1EsvROtvXn9v6e1/lHsvRrYjr1AAAESUlEQVR42u3c2XbaMBQF0HM84xkMmHkmDCFN///r2iRNmzRMVmRj1rr7iRdkX10hCVkyhBBCCCGEEEIIIYQQQgghhBBCCCGEOC9aN419Z7OftOexhzszsgcuP3A6SQ/3o9d0+UUwiU8Eu2ihVtJmg8ctt/hqnc9QKzuXJ5njFJ+M+jm5gEb2Gt/iGRbPOWT4p/XwEqzvQaPcifENrRkvaOzwJl1PA75ItOaf9CMoi4a8yJwDiBZT5z0nHjTqkByGUPTD5RXM9uRDnJYNjX6Y/C3vQUnvwOIM6JTw1ZNaBEsW9+RBp857sS0U98zi/B/QaeTwj58hisoChfvP8EcEHbb8a5gpZK8oN8Or1vPPFXTo859GwSJtFjb7AQCtxTLgGFo88IOgjyI2LMgyPPTscdck2R1BC+PzFdoFio1ZUGA0p7nJV34I/QG86IYFvqrO2UKTAf/j27iO16Aycw1djCMNNcU1VlRmzaFNm18NY1yhSVVmAn2ej/cWLeU+6Kq5qUY7HuVfvohPNcEaOkU8IbdxVs+ikoYNBUoV2YlxRkYlw0doNuFpm9W5YUzFvgfd1jxn1vd09qId6Jc6PMsfR/oyMEUJBrzA6iYtfPFIBXuUYGvxInOTZPgsrEsTAva8ysHoh/jAqU0Ajyav5e6buwhv8rr8BoA2CwkOnUH7YTGrTwDpkBUZoBxbh9VooyR9i5V4QFkeWIkFtBvs/kRQSQ5saJczfxtn5wHL9wjtuiTNzsMqReyybFYK7SZ8Yw43HYslc6HfmIrqMhXq87zaDwMRL6h5Lwq4PKvunRBgsDLOCCVYsTIblOInq9JEKRJWZYVSeC6r4Xgox5zVWKIsXVYiQVkyhxWwQnx2b/OJLkrU5jH30oJeTFg2s4UyeUciuJc+qKJWtEPZ5g5L5I5QuqzLz+5gHvSfucuSBCEq4S0OvOzm2+TOitslpMHMULqw9f7BNgJqNkH5+mwc8m4+dKif+YhvUN89U/vHAlUtcQURqhA6LMkYx91LCvweqtHzWYo5qrLjB3VfDapslS7IUB1vxnd38FzvmHDIV3fyT7jAgRJVjQgVC3Pq1Efl0ul9zqI/WDjUZObhJqKpRR38ELcS799DqMUOdRXR2OWLWuxQV/SYTPIGVVkJaiEdW/cwAp/U2td6HeiinX8XM6BTItXqt55RA17ToZqgj9vzEpesyQkBBWnik6zLCYHCtkaDypY93FaYzKjObOKW0rg5M/kNboxbCeO58RTwW6xBD1Wyl8vJwGgPBtPNweH3uTYq1jJMamO2U1Rv26Umywy3Yc+oQdfG1WoYQtfGbdlLi8rM6Qq3l7UbVOKPI9SDt54GLMiZ2CPUSK8/LZCHxmCdonY8ezwLeJGzaca1qvtP0lUyeHJOVnzXWGzre/N/jTJ70TSWT0O/8ZvvunlnYjT7cc1eTiaEEEIIIYQQQgghhBBCCCGEEEIITX4BpH1Py9uEEC8AAAAASUVORK5CYII=","sizes":"192x192","type":"image/png"}],"start_url":"/","display":"standalone","orientation":"portrait","background_color":"white","theme_color":"white"}
2 |
--------------------------------------------------------------------------------
/lighthouse_machine/cpu_monitor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016-2017, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const os = require('os');
19 |
20 | // Create function to get CPU information
21 | function cpuAverage() {
22 | // Initialise sum of idle and time of cores and fetch CPU info
23 | let totalIdle = 0;
24 | let totalTick = 0;
25 | let cpus = os.cpus();
26 |
27 | // Loop through CPU cores
28 | for (let i = 0, len = cpus.length; i < len; i++) {
29 | // Select CPU core
30 | let cpu = cpus[i];
31 |
32 | // Total up the time in the cores tick
33 | // eslint-disable-next-line guard-for-in
34 | for (let type in cpu.times) {
35 | totalTick += cpu.times[type];
36 | }
37 |
38 | // Total up the idle time of the core
39 | totalIdle += cpu.times.idle;
40 | }
41 |
42 | // Return the average Idle and Tick times
43 | return {idle: totalIdle / cpus.length, total: totalTick / cpus.length};
44 | }
45 |
46 | module.exports = (avgTime, callback) => {
47 | this.samples = [];
48 | this.samples[1] = cpuAverage();
49 | this.refresh = setInterval(() => {
50 | this.samples[0] = this.samples[1];
51 | this.samples[1] = cpuAverage();
52 | let totalDiff = this.samples[1].total - this.samples[0].total;
53 | let idleDiff = this.samples[1].idle - this.samples[0].idle;
54 | callback(1 - idleDiff / totalDiff);
55 | }, avgTime);
56 | };
57 |
--------------------------------------------------------------------------------
/models/lighthouse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the 'License');
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an 'AS IS' BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const libLighthouse = require('../lib/lighthouse');
19 |
20 | /**
21 | * Class representing a Lighthouse report for a PWA
22 | *
23 | * absoluteStartUrl is the absoluteStartUrl of the PWA
24 | * lighthouseJson is the Lighthouse's report as JSON object
25 | */
26 | class Lighthouse {
27 | constructor(pwaId, absoluteStartUrl, lighthouseJson) {
28 | this.pwaId = pwaId;
29 | this.absoluteStartUrl = absoluteStartUrl;
30 | this._lighthouseJson = parseToJson(lighthouseJson);
31 | this.lighthouseInfo = libLighthouse.processLighthouseJson(this._lighthouseJson);
32 | this.totalScore = this.lighthouseInfo.totalScore;
33 | this.lighthouseVersion = this.lighthouseInfo.lighthouseVersion;
34 | this.date = (new Date()).toISOString().slice(0, 10);
35 | this.id = this.pwaId + '-' + this.date;
36 | }
37 |
38 | get lighthouseJson() {
39 | return this._lighthouseJson;
40 | }
41 |
42 | set lighthouseJson(value) {
43 | // lighthouseJson is stored as a string in the datastore
44 | this._lighthouseJson = parseToJson(value);
45 | }
46 | }
47 |
48 | function parseToJson(value) {
49 | if (value && Object.prototype.toString.call(value) === '[object String]') {
50 | return JSON.parse(value);
51 | }
52 | return value;
53 | }
54 |
55 | module.exports = Lighthouse;
56 |
--------------------------------------------------------------------------------
/public/js/analytics.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export default class Analytics {
19 | constructor(window, config) {
20 | this.navigator = window.navigator;
21 | this.window = window;
22 | this.config = config;
23 | this._init();
24 | this._setupA2HTracking();
25 | }
26 |
27 | _init() {
28 | // Setup Tracking if analytics is not loaded yet.
29 | if (!this.window.ga) {
30 | this.window.ga = (...args) => {
31 | (this.window.ga.q = this.window.ga.q || []).push(args);
32 | };
33 | }
34 |
35 | this.window.ga('create', this.config.ga_id, 'auto');
36 | this.window.ga('set', 'transport', 'beacon');
37 | }
38 |
39 | /**
40 | * Setup a listener to track Add to Homescreen events.
41 | */
42 | _setupA2HTracking() {
43 | this.window.addEventListener('beforeinstallprompt', e => {
44 | e.userChoice.then(choiceResult => {
45 | this.window.ga('send', 'event', 'A2H', choiceResult.outcome);
46 | });
47 | });
48 | }
49 |
50 | trackOutboundClick(url) {
51 | this.window.ga('send', 'event', 'outbound', 'click', url, {transport: 'beacon'});
52 | }
53 |
54 | trackPageView(url) {
55 | this.window.ga('set', 'page', url);
56 | this.window.ga('set', 'dimension1', this.navigator.onLine);
57 | this.window.ga('send', 'pageview');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/test/app/lib/promise-sequential.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it */
17 | 'use strict';
18 |
19 | const promiseSequential = require('../../../lib/promise-sequential');
20 |
21 | const chai = require('chai');
22 | const chaiAsPromised = require('chai-as-promised');
23 | chai.use(chaiAsPromised);
24 | chai.should();
25 | const assert = require('chai').assert;
26 |
27 | describe('lib.promise-sequential', () => {
28 | describe('#all', () => {
29 | it('executes all functions on the list', () => {
30 | const promiseFunctions = [
31 | _ => 1,
32 | result => result + 1,
33 | result => result + 2,
34 | result => result + 3
35 | ];
36 | return promiseSequential.all(promiseFunctions).should.be.fulfilled.then(result => {
37 | assert.equal(result, 7);
38 | });
39 | });
40 | });
41 |
42 | describe('#all', () => {
43 | it('executes all functions on the list without parameters', () => {
44 | function test(result) {
45 | result += 1;
46 | return result;
47 | }
48 | const promiseFunctions = [
49 | _ => (10),
50 | test,
51 | test,
52 | test,
53 | test
54 | ];
55 | return promiseSequential.all(promiseFunctions).should.be.fulfilled.then(result => {
56 | assert.equal(result, 14);
57 | });
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/controllers/api/notifications.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const bodyParser = require('body-parser');
20 | const router = express.Router(); // eslint-disable-line new-cap
21 | const notificationsLib = require('../../lib/notifications');
22 | const jsonParser = bodyParser.json();
23 |
24 | router.get('/topics/', (req, res) => {
25 | const token = req.query.token;
26 | notificationsLib.list(token)
27 | .then(subscriptions => {
28 | res.json({
29 | subscriptions: subscriptions
30 | });
31 | })
32 | .catch(err => {
33 | res.status(500);
34 | res.json(err);
35 | });
36 | });
37 |
38 | router.post('/subscribe/:topic/', jsonParser, (req, res) => {
39 | const token = req.body.token;
40 | const topic = req.params.topic;
41 | notificationsLib.subscribe(token, topic)
42 | .then(_ => {
43 | res.json({success: true});
44 | })
45 | .catch(err => {
46 | res.status(500);
47 | res.json(err);
48 | });
49 | });
50 |
51 | router.post('/unsubscribe/:topic/', jsonParser, (req, res) => {
52 | const token = req.body.token.trim();
53 | const topic = req.params.topic;
54 | notificationsLib.unsubscribe(token, topic)
55 | .then(_ => {
56 | res.json({success: true});
57 | })
58 | .catch(err => {
59 | res.status(500);
60 | res.json(err);
61 | });
62 | });
63 |
64 | module.exports = router;
65 |
66 |
--------------------------------------------------------------------------------
/public/js/shell.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export default class Shell {
19 | constructor(document) {
20 | this._document = document;
21 | this._backlink = document.querySelector('#backlink');
22 | this._tabs = Array.from(document.querySelectorAll('#installable, #newest, #score, #tabs'));
23 | this._subtitle = document.querySelector('#subtitle');
24 | this._search = document.querySelector('#search');
25 | this._states = new Map();
26 | }
27 |
28 | setStateForRoute(route, shellState) {
29 | this._states.set(route, shellState);
30 | }
31 |
32 | _showElement(element, visible) {
33 | if (visible) {
34 | element.classList.remove('hidden');
35 | return;
36 | }
37 | element.classList.add('hidden');
38 | }
39 |
40 | _updateTab(tab, options) {
41 | this._showElement(tab, options.showTabs);
42 | if (!options.currentTab) {
43 | return;
44 | }
45 |
46 | if (tab.id === options.currentTab) {
47 | tab.classList.add('activetab');
48 | return;
49 | }
50 | tab.classList.remove('activetab');
51 | }
52 |
53 | onRouteChange(route) {
54 | const options = this._states.get(route);
55 | this._showElement(this._backlink, options.backlink);
56 | this._showElement(this._subtitle, options.subtitle);
57 | this._showElement(this._search, options.search);
58 | this._tabs.forEach(tab => this._updateTab(tab, options));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/test/app/views/helpers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it */
17 | 'use strict';
18 |
19 | let assert = require('assert');
20 | let helpers = require('../../../../views/helpers');
21 |
22 | describe('views.helpers', () => {
23 | describe('#contrastColor', () => {
24 | it('should return "white" when the value is not present', () => {
25 | assert.equal('#000000', helpers.contrastColor(null));
26 | assert.equal('#000000', helpers.contrastColor(''));
27 | assert.equal('#000000', helpers.contrastColor('transparent'));
28 | });
29 |
30 | it('it should understand HTML colors by name', () => {
31 | assert.equal('#ffffff', helpers.contrastColor('black'));
32 | assert.equal('#000000', helpers.contrastColor('white'));
33 | });
34 | });
35 |
36 | describe('#firstLetter', () => {
37 | it('should return an empty string when value is not present', () => {
38 | assert.equal('', helpers.firstLetter(null));
39 | });
40 |
41 | it('should return "G" for "Gulliver"', () => {
42 | assert.equal('G', helpers.firstLetter('Gulliver'));
43 | });
44 |
45 | it('should return "G" for "gulliver"', () => {
46 | assert.equal('G', helpers.firstLetter('gulliver'));
47 | });
48 | });
49 |
50 | it('#moment', () => {
51 | assert.ok(helpers.moment());
52 | assert.ok(helpers.moment(null));
53 | assert.ok(helpers.moment(new Date()), 'Returns a value for a Date');
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/views/includes/metadata.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
61 |
--------------------------------------------------------------------------------
/lib/manifest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the 'License');
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an 'AS IS' BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const dataFetcher = require('../lib/data-fetcher');
19 | const Manifest = require('../models/manifest');
20 |
21 | /**
22 | * Fetches the Manifest from the manifestUrl.
23 | *
24 | * @param {string} manifestUrl
25 | * @return {Promise}
26 | */
27 | function fetchManifest(manifestUrl) {
28 | return dataFetcher.fetchJsonWithUA(manifestUrl)
29 | .then(json => new Manifest(manifestUrl, json));
30 | }
31 |
32 | /**
33 | * Wrapper for the manifest validator from lighthouse.
34 | *
35 | * @param {Manifest} manifest
36 | * @param {string} manifestUrl URL of manifest itself
37 | * @param {string} documentUrl URL of document that links to the manifest
38 | * @return string[] errors found in manifest
39 | */
40 | function validateManifest(manifest, manifestUrl, documentUrl) {
41 | const parse = require('../third_party/manifest-parser.js');
42 | const res = parse(manifest, manifestUrl, documentUrl);
43 | // Lighthouse annotates the actual elements with validation errors; "flatten"
44 | // these here.
45 | function flatten(obj) {
46 | const debugString = obj.debugString ? [obj.debugString] : [];
47 | if (typeof obj.value !== 'object') {
48 | return debugString;
49 | }
50 | return Object.keys(obj.value).reduce((acc, k) => {
51 | return acc.concat(flatten(obj.value[k]));
52 | }, debugString);
53 | }
54 | return flatten(res);
55 | }
56 |
57 | module.exports = {
58 | fetchManifest,
59 | validateManifest
60 | };
61 |
--------------------------------------------------------------------------------
/config/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | // Hierarchical node.js configuration with command-line arguments, environment
19 | // variables, and files.
20 | const nconf = require('nconf');
21 | const path = require('path');
22 |
23 | nconf
24 | // 1. Command-line arguments
25 | .argv()
26 | // 2. Environment variables
27 | .env([
28 | 'CLOUD_BUCKET',
29 | 'GCLOUD_PROJECT',
30 | 'PORT',
31 | 'CLIENT_ID',
32 | 'CLIENT_SECRET',
33 | 'WEBPERFORMANCE_SERVER',
34 | 'WEBPERFORMANCE_SERVER_API_KEY',
35 | 'GOOGLE_ANALYTICS',
36 | 'FIREBASE_AUTH',
37 | 'CANONICAL_ROOT',
38 | 'FIREBASE_MSG_SENDER_ID',
39 | 'API_TOKENS'
40 | ])
41 | // 3. Config file
42 | .file({file: path.join(__dirname, 'config.json')})
43 | // 4. Defaults
44 | .defaults({
45 | PORT: 8080 // Port used by HTTP server
46 | });
47 |
48 | // Check for required settings
49 | checkConfig('GCLOUD_PROJECT');
50 | checkConfig('CLOUD_BUCKET');
51 | checkConfig('CLIENT_ID');
52 | checkConfig('CLIENT_SECRET');
53 |
54 | function checkConfig(setting) {
55 | // If setting undefined, throw error
56 | if (!nconf.get(setting)) {
57 | throw new Error(`You must set the ${setting} environment variable or add it to ` +
58 | 'config/config.json!');
59 | }
60 | // If setting includes a space, throw error
61 | if (nconf.get(setting).match(/\s/)) {
62 | throw new Error(`The ${setting} environment variable is suspicious ("${nconf.get(setting)}")`);
63 | }
64 | }
65 |
66 | module.exports = nconf;
67 |
--------------------------------------------------------------------------------
/public/js/ui/signin-button.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export class SignInButton {
19 | constructor(window, signIn, element) {
20 | this.signIn = signIn;
21 | this.element = element;
22 | this._window = window;
23 | this._setupEventListeners();
24 | }
25 |
26 | _setupEventListeners() {
27 | // Make SignIn button react to userchange events.
28 | this._window.addEventListener('userchange', () => {
29 | if (this.signIn.signedIn) {
30 | this.element.classList.add('hidden');
31 | } else {
32 | this.element.classList.remove('hidden');
33 | }
34 | });
35 |
36 | const clickListener = () => {
37 | if (!this.signIn.signedIn) {
38 | this.signIn.signIn();
39 | }
40 | };
41 | this.element.addEventListener('click', clickListener);
42 | }
43 | }
44 |
45 | export class SignOutButton {
46 | constructor(window, signIn, element) {
47 | this.signIn = signIn;
48 | this.element = element;
49 | this._window = window;
50 | this._setupEventListeners();
51 | }
52 |
53 | _setupEventListeners() {
54 | // Make SignOut button react to userchange events.
55 | this._window.addEventListener('userchange', () => {
56 | if (this.signIn.signedIn) {
57 | this.element.classList.remove('hidden');
58 | } else {
59 | this.element.classList.add('hidden');
60 | }
61 | });
62 |
63 | const clickListener = () => {
64 | if (this.signIn.signedIn) {
65 | this.signIn.signOut();
66 | }
67 | };
68 | this.element.addEventListener('click', clickListener);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/public/js/ui/notification-checkbox.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export default class NotificationCheckbox {
19 | constructor(messaging, checkbox, topic) {
20 | if (!checkbox) {
21 | console.error('checkbox parameter cannot be null');
22 | return;
23 | }
24 | this.messaging = messaging;
25 | this.checkbox = checkbox;
26 | this.topic = topic;
27 | this._setupEventListener();
28 |
29 | // Initilize checkbox state.
30 | this.messaging.isNotificationBlocked()
31 | .then(blocked => {
32 | if (blocked) {
33 | checkbox.disabled = true;
34 | return;
35 | }
36 |
37 | this.messaging.isSubscribed(topic)
38 | .then(subscribed => {
39 | checkbox.checked = subscribed;
40 | });
41 | });
42 | }
43 |
44 | _setupEventListener() {
45 | this.checkbox.addEventListener('change', e => {
46 | if (e.target.checked) {
47 | this.messaging.subscribe(this.topic)
48 | .catch(e => {
49 | console.error('Error subscribing to topic: ', e);
50 | this.checkbox.checked = false;
51 | if (e.blocked) {
52 | this.checkbox.disabled = true;
53 | }
54 | });
55 | return;
56 | }
57 | this.messaging.unsubscribe(this.topic)
58 | .catch(err => {
59 | console.error('Error unsubscribing from topic: ', err);
60 | if (err.blocked) {
61 | this.checkbox.disabled = true;
62 | this.checkbox.checked = false;
63 | return;
64 | }
65 | this.checkbox.checked = true;
66 | return;
67 | });
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/middlewares/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | const express = require('express');
17 | const asset = require('../lib/asset-hashing').asset;
18 | const router = express.Router(); // eslint-disable-line new-cap
19 | const CSSPATH = asset.encode('/css/style.css');
20 | const JSPATH = asset.encode('/js/gulliver.js');
21 |
22 | router.use((req, res, next) => {
23 | res.setHeader('Content-Type', 'text/html');
24 |
25 | /* eslint-disable quotes */
26 | res.setHeader('content-security-policy', [
27 | `connect-src 'self' https://www.google-analytics.com https://web-performance-dot-pwa-directory.appspot.com https://fcm.googleapis.com`,
28 | `default-src 'self' https://accounts.google.com https://apis.google.com https://fcm.googleapis.com`,
29 | `script-src 'self' 'unsafe-eval' https://apis.google.com https://www.google-analytics.com https://www.gstatic.com`,
30 | `style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ https://www.gstatic.com`,
31 | `font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/`,
32 | `img-src 'self' https://storage.googleapis.com https://www.google-analytics.com`
33 | ].join('; '));
34 | /* eslint-enable quotes */
35 | res.setHeader('x-content-type-options', 'nosniff');
36 | res.setHeader('x-dns-prefetch-control', 'off');
37 | res.setHeader('x-download-options', 'noopen');
38 | res.setHeader('x-frame-options', 'SAMEORIGIN');
39 | res.setHeader('x-xss-protection', '1; mode=block');
40 |
41 | // Set the preload header if a full render is being requested.
42 | if (!req.query.contentOnly && !req.originalUrl.startsWith('/.app/')) {
43 | res.setHeader('Link',
44 | `<${CSSPATH}>; rel=preload; as=style, <${JSPATH}>; rel=preload; as=script`);
45 | }
46 | next();
47 | });
48 |
49 | module.exports = router;
50 |
--------------------------------------------------------------------------------
/models/manifest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 | const url = require('url');
18 |
19 | /**
20 | * Class representing a Web App Manifest
21 | */
22 | class Manifest {
23 | constructor(manifestUrl, jsonManifest) {
24 | this.url = manifestUrl;
25 | this.raw = JSON.stringify(jsonManifest);
26 | this.name = jsonManifest.name;
27 | this.shortName = jsonManifest.short_name;
28 | this.description = jsonManifest.description;
29 | this.startUrl = jsonManifest.start_url;
30 | this.backgroundColor = jsonManifest.background_color;
31 | this.icons = jsonManifest.icons;
32 | this.scope = jsonManifest.scope;
33 | }
34 |
35 | getBestIcon() {
36 | function getIconSize(icon) {
37 | if (!icon.sizes) {
38 | return 0;
39 | }
40 | return parseInt(icon.sizes.substring(0, icon.sizes.indexOf('x')), 10);
41 | }
42 |
43 | if (!this.icons) {
44 | return null;
45 | }
46 |
47 | let bestIcon;
48 | let bestIconSize;
49 |
50 | for (let icon of this.icons) {
51 | if (!bestIcon) {
52 | bestIcon = icon;
53 | bestIconSize = getIconSize(icon);
54 | }
55 |
56 | const iconSize = getIconSize(icon);
57 | if (iconSize > bestIconSize) {
58 | bestIcon = icon;
59 | bestIconSize = iconSize;
60 | }
61 |
62 | // We can return 128 and 144 even if there are bigger ones.
63 | if (iconSize === 128 || iconSize === 144) {
64 | return icon;
65 | }
66 | }
67 | return bestIcon;
68 | }
69 |
70 | /** Gets the Url for the largest icon in the Manifest */
71 | getBestIconUrl() {
72 | let bestIcon = this.getBestIcon();
73 | if (!bestIcon || !bestIcon.src) {
74 | return '';
75 | }
76 | return url.resolve(this.url, bestIcon.src);
77 | }
78 | }
79 |
80 | module.exports = Manifest;
81 |
--------------------------------------------------------------------------------
/public/js/loader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | import './util/requestIdleCallback';
19 |
20 | const FADE_OUT_ANIMATION_LENGTH = 500;
21 |
22 | /**
23 | * A CSS only loader showing three dots.
24 | */
25 | class Loader {
26 |
27 | /**
28 | * Create a new loader.
29 | *
30 | * @param container {HTMLElement} the element containing the loader
31 | * @param style {String} optional hex color or css class for styling the loader
32 | */
33 | constructor(container, style) {
34 | this.style = style || '';
35 | this.container = container;
36 | }
37 |
38 | /**
39 | * addLoader adds a CSS loader to the given element.
40 | *
41 | * @param container {HTMLElement} the element containing the loader.
42 | */
43 | show() {
44 | const loader = document.createElement('div');
45 | loader.style['align-items'] = 'center';
46 | loader.classList.add('loader');
47 | for (let i = 0; i < 3; i++) {
48 | const dot = document.createElement('div');
49 | dot.classList.add('loader-dot');
50 | if (this.style.startsWith('#')) {
51 | dot.style['background-color'] = this.style;
52 | } else if (this.style) {
53 | dot.classList.add(this.style);
54 | }
55 | loader.appendChild(dot);
56 | }
57 | this.container.appendChild(loader);
58 | }
59 |
60 | /**
61 | * removeLoader removes a CSS loader from the given element.
62 | *
63 | * @param container {HTMLElement} the element containing the loader.
64 | */
65 | hide() {
66 | const loaders = this.container.querySelectorAll('.loader');
67 | loaders.forEach(loader => {
68 | loader.classList.add('fadeOut');
69 | window.requestIdleCallback(() => loader.remove(), {
70 | timeout: FADE_OUT_ANIMATION_LENGTH
71 | });
72 | });
73 | }
74 | }
75 | export default Loader;
76 |
--------------------------------------------------------------------------------
/views/includes/head.hbs:
--------------------------------------------------------------------------------
1 | {{!-- Copyright 2015-2016, Google, Inc.
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License. --}}
13 |
14 |
15 | {{title}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 | {{> metadata}}
45 |
46 |
47 |
--------------------------------------------------------------------------------
/lib/asset-hashing.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const fs = require('fs');
19 | const path = require('path');
20 | const revHash = require('rev-hash');
21 |
22 | const CHECKSUM_LENGTH = 10;
23 | const CHECKSUM_PATTERN = /^[0-9a-z]{10}$/;
24 |
25 | class ChecksumProvider {
26 |
27 | constructor(root) {
28 | this.root_ = root;
29 | }
30 |
31 | get(assetPath) {
32 | const buffer = fs.readFileSync(path.join(this.root_, assetPath));
33 | return revHash(buffer);
34 | }
35 |
36 | }
37 |
38 | class AssetChecksum {
39 |
40 | constructor(checksumProvider) {
41 | this.checksumProvider_ = checksumProvider;
42 | this.checksumCache_ = {};
43 | }
44 |
45 | encode(assetPath) {
46 | if (!assetPath) {
47 | return assetPath;
48 | }
49 | let result = this.checksumCache_[assetPath];
50 | if (result) {
51 | return result;
52 | }
53 | const checksum = this.checksumProvider_.get(assetPath);
54 | const index = assetPath.lastIndexOf('.');
55 | if (index === -1) {
56 | return assetPath;
57 | }
58 | result = assetPath.substring(0, index) +
59 | '.' +
60 | checksum +
61 | assetPath.substring(index, assetPath.length);
62 | this.checksumCache_[assetPath] = result;
63 | return result;
64 | }
65 |
66 | decode(assetPath) {
67 | if (!assetPath) {
68 | return assetPath;
69 | }
70 | const fragments = assetPath.split('.');
71 | if (fragments.length <= 1) {
72 | return assetPath;
73 | }
74 | const checksumIndex = fragments.length - 2;
75 | if (!fragments[checksumIndex].match(CHECKSUM_PATTERN)) {
76 | return assetPath;
77 | }
78 | fragments.splice(checksumIndex, 1);
79 | return fragments.join('.');
80 | }
81 |
82 | }
83 |
84 | module.exports.asset = new AssetChecksum(new ChecksumProvider('public'));
85 |
86 | // Exported for testing
87 | module.exports.ChecksumProvider = ChecksumProvider;
88 | module.exports.AssetChecksum = AssetChecksum;
89 | module.exports.CHECKSUM_LENGTH = CHECKSUM_LENGTH;
90 |
--------------------------------------------------------------------------------
/lib/favorite-pwa.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const db = require('../lib/model-datastore');
19 | const FavoritePwa = require('../models/favorite-pwa');
20 |
21 | const ENTITY_NAME = 'FavoritePwa';
22 |
23 | /**
24 | * Saves a FavoritePwa object into the DB.
25 | *
26 | * @param {FavoritePwa} lighthouse
27 | * @return {Promise}
28 | */
29 | exports.save = function(favoritePwa) {
30 | return db.update(ENTITY_NAME, favoritePwa.id, favoritePwa)
31 | .catch(err => {
32 | console.log(err);
33 | return Promise.reject('Error saving the FavoritePwa');
34 | });
35 | };
36 |
37 | /**
38 | * Retrieves FavoritePwas for a given User.
39 | *
40 | * @param {number} userId
41 | * @return {Promise>}
42 | */
43 | exports.findByUserId = function(userId) {
44 | console.log(userId);
45 | const query = db.createQuery(ENTITY_NAME).filter('userId', '=', userId);
46 | return db.runQuery(query).then(result => {
47 | if (!result || result.entities.length === 0) {
48 | return null;
49 | }
50 | let favoritePwas = result.entities.map(entry => {
51 | return new FavoritePwa(entry.pwaId, entry.userId);
52 | });
53 | return favoritePwas;
54 | });
55 | };
56 |
57 | /**
58 | * Retrieves a FavoritePwa for given User & PWA.
59 | *
60 | * @param {number} pwaId
61 | * @param {number} userId
62 | * @return {Promise}
63 | */
64 | exports.findFavoritePwa = function(pwaId, userId) {
65 | const query = db.createQuery(ENTITY_NAME).filter('pwaId', '=', parseInt(pwaId, 10))
66 | .filter('userId', '=', userId).limit(1);
67 | return db.runQuery(query).then(result => {
68 | if (!result || result.entities.length === 0) {
69 | return null;
70 | }
71 | return new FavoritePwa(result.entities[0].pwaId, result.entities[0].userId);
72 | });
73 | };
74 |
75 | /**
76 | * Deletes a FavoritePwa from DB.
77 | *
78 | * @param {number} key of the FavoritePwa
79 | * @return {Promise<>}
80 | */
81 | exports.delete = function(key) {
82 | return db.delete(ENTITY_NAME, key);
83 | };
84 |
--------------------------------------------------------------------------------
/lib/pwa-index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const libSearch = require('../lib/search');
19 | const libPwa = require('../lib/pwa');
20 | const Pwa = require('../models/pwa');
21 | const db = require('../lib/model-datastore');
22 | const ENTITY_NAME = 'PWA';
23 |
24 | /**
25 | * Add all PWAs from the DB into the text search index.
26 | */
27 | exports.indexAllPwas = _ => {
28 | let indexPage = (skip, limit) =>
29 | db.list(ENTITY_NAME, skip, limit)
30 | .then(result => {
31 | const pwas = result.entities.map(pwa => {
32 | return Object.assign(new Pwa(), pwa);
33 | });
34 | libSearch.addPwas(pwas);
35 | if (result.hasMore) {
36 | return indexPage(skip + limit, limit);
37 | }
38 | console.log('All PWAs indexed');
39 | return Promise.resolve();
40 | });
41 | return indexPage(0, 100);
42 | };
43 |
44 | /**
45 | * Search for PWAS using the text search index.
46 | *
47 | * @param {string} query
48 | * @return {resultPage} resultPage with an arrays of PWAs and hasMore boolean
49 | */
50 | exports.searchPwas = string => {
51 | return libSearch.search(string).then(result => {
52 | let pwas = new Array(result.length);
53 | let find = (currentValue, index) => {
54 | return libPwa.find(currentValue.ref).then(pwa => {
55 | // Inserting at index to keep result order
56 | // because Promises run in parallel
57 | pwas[index] = pwa;
58 | });
59 | };
60 | return Promise.all(result.map(find)).then(_ => {
61 | // Returning all results without pagination for now
62 | const resultPage = {
63 | pwas: pwas,
64 | hasMore: false
65 | };
66 | return Promise.resolve(resultPage);
67 | });
68 | });
69 | };
70 |
71 | /**
72 | * Update PWA in the search index.
73 | *
74 | * @param {Pwa} pwa to update
75 | * @return {Promise}
76 | */
77 | exports.updateSearchIndex = function(pwa) {
78 | if (pwa.isNew()) {
79 | libSearch.addPwa(pwa);
80 | } else {
81 | libSearch.updatePwa(pwa);
82 | }
83 | return Promise.resolve(pwa);
84 | };
85 |
--------------------------------------------------------------------------------
/test/app/controllers/api/lighthouse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it before afterEach */
17 | 'use strict';
18 |
19 | const controllerApi = require('../../../../controllers/api');
20 | const lighthouseLib = require('../../../../lib/lighthouse');
21 |
22 | const express = require('express');
23 | const app = express();
24 | const request = require('supertest');
25 | const simpleMock = require('simple-mock');
26 | const chai = require('chai');
27 | const chaiAsPromised = require('chai-as-promised');
28 | chai.use(chaiAsPromised);
29 | chai.should();
30 | let assert = require('chai').assert;
31 |
32 | describe('controllers.api.lighthouse', () => {
33 | before(done => {
34 | app.use(controllerApi);
35 | done();
36 | });
37 |
38 | describe('GET /api/lighthouse-graph/', () => {
39 | afterEach(() => {
40 | simpleMock.restore();
41 | });
42 |
43 | it('respond with 200 if PWA exist', done => {
44 | simpleMock.mock(lighthouseLib, 'getLighthouseGraphByPwaId').resolveWith('mocked graph data');
45 | // /api/ is part of the router, we need to start from /lighthouse-graph/
46 | request(app)
47 | .get('/lighthouse/graph/1234567')
48 | .expect('Content-Type', /json/)
49 | .expect(200).should.be.fulfilled.then(res => {
50 | assert.equal(res.body, 'mocked graph data');
51 | assert.equal(lighthouseLib.getLighthouseGraphByPwaId.callCount, 1);
52 | assert.equal(lighthouseLib.getLighthouseGraphByPwaId.lastCall.arg, 1234567);
53 | done();
54 | });
55 | });
56 |
57 | it('respond with 404 if PWA does not exist', done => {
58 | simpleMock.mock(lighthouseLib, 'getLighthouseGraphByPwaId').resolveWith(null);
59 | // /api/ is part of the router, we need to start from /lighthouse-graph/
60 | request(app)
61 | .get('/lighthouse/graph/123')
62 | .expect('Content-Type', /json/)
63 | .expect(400).should.be.rejected.then(res => {
64 | assert.equal(res.body, undefined);
65 | assert.equal(lighthouseLib.getLighthouseGraphByPwaId.callCount, 1);
66 | assert.equal(lighthouseLib.getLighthouseGraphByPwaId.lastCall.arg, '123');
67 | done();
68 | });
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/lib/notifications.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const dataFetcher = require('../lib/data-fetcher');
19 |
20 | exports.list = function(token) {
21 | if (!token) {
22 | return Promise.reject(new Error('Missing token'));
23 | }
24 |
25 | const url = 'https://iid.googleapis.com/iid/info/' + token + '?details=true';
26 | return dataFetcher.firebaseFetch(url)
27 | .then(userDetails => {
28 | if (!userDetails || !userDetails.rel || !userDetails.rel.topics) {
29 | return Promise.resolve([]);
30 | }
31 | return Object.keys(userDetails.rel.topics);
32 | });
33 | };
34 |
35 | exports.subscribe = function(token, topic) {
36 | if (!token) {
37 | return Promise.reject(new Error('Missing token'));
38 | }
39 |
40 | if (!topic) {
41 | return Promise.reject(new Error('Missing topic'));
42 | }
43 |
44 | const url = 'https://iid.googleapis.com/iid/v1:batchAdd';
45 | const payload = {
46 | to: '/topics/' + topic,
47 | registration_tokens: [token] // eslint-disable-line camelcase
48 | };
49 | return dataFetcher.firebaseFetch(url, payload);
50 | };
51 |
52 | exports.unsubscribe = function(token, topic) {
53 | if (!token) {
54 | return Promise.reject(new Error('Missing token'));
55 | }
56 |
57 | if (!topic) {
58 | return Promise.reject(new Error('Missing topic'));
59 | }
60 |
61 | const url = 'https://iid.googleapis.com/iid/v1:batchRemove';
62 | const payload = {
63 | to: '/topics/' + topic,
64 | registration_tokens: [token] // eslint-disable-line camelcase
65 | };
66 | return dataFetcher.firebaseFetch(url, payload);
67 | };
68 |
69 | exports.sendPush = function(topic, notification) {
70 | if (!topic) {
71 | return Promise.reject(new Error('Missing topic'));
72 | }
73 |
74 | if (!notification) {
75 | return Promise.reject(new Error('Missing notification'));
76 | }
77 |
78 | // Require the notification to have a title, at minimum
79 | if (!notification.title) {
80 | return Promise.reject(new Error('Missing notification title'));
81 | }
82 | const url = 'https://fcm.googleapis.com/fcm/send';
83 | const payload = {
84 | to: '/topics/' + topic,
85 | notification: notification
86 | };
87 | return dataFetcher.firebaseFetch(url, payload);
88 | };
89 |
--------------------------------------------------------------------------------
/test/app/controllers/api/pwa.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it before afterEach */
17 | 'use strict';
18 |
19 | const controllerApi = require('../../../../controllers/api');
20 | const libPwa = require('../../../../lib/pwa');
21 | const testPwa = require('../../models/pwa');
22 | const config = require('../../../../config/config');
23 | const apiKeyArray = config.get('API_TOKENS');
24 |
25 | const express = require('express');
26 | const app = express();
27 | const request = require('supertest');
28 | const simpleMock = require('simple-mock');
29 | const chai = require('chai');
30 | const chaiAsPromised = require('chai-as-promised');
31 | chai.use(chaiAsPromised);
32 | chai.should();
33 | let assert = require('chai').assert;
34 |
35 | const MANIFEST_URL = 'https://pwa-directory.appspot.com/manifest.json';
36 | /* eslint-disable camelcase */
37 | const MANIFEST_DATA = {
38 | name: 'PWA Directory',
39 | short_name: 'PwaDirectory',
40 | start_url: '/?utm_source=homescreen'
41 | };
42 |
43 | describe('controllers.api.pwa', () => {
44 | before(done => {
45 | app.use(controllerApi);
46 | done();
47 | });
48 |
49 | describe('GET /api/pwa', () => {
50 | const pwa = testPwa.newPwa(MANIFEST_URL, MANIFEST_DATA);
51 | pwa.id = '789';
52 | const result = {};
53 | result.pwas = [pwa];
54 |
55 | afterEach(() => {
56 | simpleMock.restore();
57 | });
58 |
59 | it('respond with 200 and json', done => {
60 | simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result));
61 | // /api/ is part of the router, we need to start from /pwa/
62 | request(app)
63 | .get('/pwa?key=' + apiKeyArray[0])
64 | .expect(200)
65 | .expect('Content-Type', /json/).should.be.fulfilled.then(_ => {
66 | assert.equal(libPwa.list.callCount, 1);
67 | done();
68 | });
69 | });
70 |
71 | it('respond with 200 and csv', done => {
72 | simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result));
73 | // /api/ is part of the router, we need to start from /pwa/
74 | request(app)
75 | .get('/pwa?format=csv&key=' + apiKeyArray[0])
76 | .expect(200)
77 | .expect('Content-Type', 'text/csv; charset=utf-8').should.be.fulfilled.then(_ => {
78 | assert.equal(libPwa.list.callCount, 1);
79 | done();
80 | });
81 | });
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/public/js/offline-support.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | export default class OfflineSupport {
19 |
20 | constructor(window, router) {
21 | this.window = window;
22 | this.router = router;
23 | this._setupEventhandlers();
24 | }
25 |
26 | /**
27 | * All elements with class .gulliver-online-aware will:
28 | * have an 'online' dataset property that reflects the current online state.
29 | * receive a 'change' event whenever the state changes.
30 | */
31 | _setupEventhandlers() {
32 | const body = this.window.document.querySelector('body');
33 | this.window.addEventListener('online', () => {
34 | body.removeAttribute('offline');
35 | });
36 |
37 | this.window.addEventListener('offline', () => {
38 | body.setAttribute('offline', 'true');
39 | this.markAsCached(this.window.document.querySelectorAll('.offline-aware'));
40 | });
41 |
42 | const onLine = this.window.navigator.onLine;
43 | if (onLine !== undefined && !onLine) {
44 | body.setAttribute('offline', 'true');
45 | }
46 | }
47 |
48 | /**
49 | * Check if a Url is navigable.
50 | * @param url the url to be checke for availability
51 | * @returns true if the user is online or the URL is cached
52 | */
53 | isAvailable(href) {
54 | if (!href || this.window.navigator.onLine) return Promise.resolve(true);
55 | return caches.match(href)
56 | .then(response => response.status === 200)
57 | .catch(() => false);
58 | }
59 |
60 | /**
61 | * Checks if the href on the anchor is available in the cached
62 | * and marks the element with the cached attribute.
63 | *
64 | * If the url is available, the `cached` attribute is added with
65 | * the value `true`. Otherwise, the `cached` attribute is removed.
66 | * @param {@NodeList} a list of anchors.
67 | */
68 | markAsCached(anchors) {
69 | anchors.forEach(anchor => {
70 | if (!anchor.href) {
71 | return;
72 | }
73 | const route = this.router.findRoute(anchor.href);
74 | if (!route) {
75 | return;
76 | }
77 | const contentHref = route.getContentOnlyUrl(anchor.href);
78 | this.isAvailable(contentHref).then(available => {
79 | if (available) {
80 | anchor.setAttribute('cached', 'true');
81 | return;
82 | }
83 | anchor.removeAttribute('cached');
84 | });
85 | });
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/test/app/lib/manifest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it */
17 |
18 | const fs = require('fs');
19 |
20 | const chai = require('chai');
21 | const chaiAsPromised = require('chai-as-promised');
22 | const assert = require('chai').assert;
23 |
24 | chai.use(chaiAsPromised);
25 | chai.should();
26 |
27 | const libManifest = require('../../../lib/manifest');
28 |
29 | describe('lib.manifest', () => {
30 | describe('#validateManifest', () => {
31 | // We assume lighthouse has tests for the manifest parser, just run some
32 | // minimal smoke tests here.
33 | it('returns empty array if manifest ok', () => {
34 | const manifest = fs.readFileSync('./test/app/manifests/icon-url-with-parameter.json');
35 | const manifestUrl = 'https://example.com/';
36 | const documentUrl = 'https://www.example.com/';
37 | const actual = libManifest.validateManifest(manifest, manifestUrl, documentUrl);
38 | assert.deepEqual(actual, []);
39 | });
40 | it('returns same-origin error if documentUrl does not match', () => {
41 | const manifest = fs.readFileSync('./test/app/manifests/icon-url-with-parameter.json');
42 | const manifestUrl = 'https://example.com/';
43 | const documentUrl = 'https://bar.com/';
44 | const actual = libManifest.validateManifest(manifest, manifestUrl, documentUrl);
45 | assert.deepEqual(actual, ['ERROR: start_url must be same-origin as document']);
46 | });
47 | it('returns icon error if icon value invalid', () => {
48 | const manifest = fs.readFileSync('./test/app/manifests/no-icon-array.json');
49 | const manifestUrl = 'https://example.com/';
50 | const documentUrl = 'https://bar.com/';
51 | const actual = libManifest.validateManifest(manifest, manifestUrl, documentUrl);
52 | assert.deepEqual(actual, ['ERROR: \'icons\' expected to be an array but is not.']);
53 | });
54 | it('returns multiple errors if start_url and theme_color invalid', () => {
55 | const manifest = fs.readFileSync('./test/app/manifests/invalid-theme-color.json');
56 | const manifestUrl = 'https://example.com/';
57 | const documentUrl = 'https://bar.com/';
58 | const actual = libManifest.validateManifest(manifest, manifestUrl, documentUrl);
59 | assert.deepEqual(actual, [
60 | 'ERROR: start_url must be same-origin as document',
61 | 'ERROR: color parsing failed.'
62 | ]);
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/lib/color.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2018, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const parseColor = require('parse-color');
19 |
20 | function bestContrastRatio(color1, color2, background) {
21 | return contrastRatio(color1, background) > contrastRatio(color2, background) ? color1 : color2;
22 | }
23 |
24 | /**
25 | * Calculates the contrast ratio, as described on https://www.w3.org/TR/WCAG20/#contrast-ratiodef
26 | *
27 | * @param {string} foreground the foreground color.
28 | * @param {string} background the background color, Defaults to #FFFFFF.
29 | * @returns {Number} the contrast ration.
30 | */
31 | function contrastRatio(foreground, background = '#FFFFFF') {
32 | if (background.trim() === 'transparent') background = 'white';
33 |
34 | const bgLuminance = relativeLuminance(background);
35 | const fgLuminance = relativeLuminance(foreground);
36 |
37 | let darker;
38 | let lighter;
39 | if (fgLuminance > bgLuminance) {
40 | lighter = fgLuminance;
41 | darker = bgLuminance;
42 | } else {
43 | lighter = bgLuminance;
44 | darker = fgLuminance;
45 | }
46 |
47 | return (lighter + 0.05) / (darker + 0.05);
48 | }
49 |
50 | /**
51 | * Calculates the relative luminance, as described on https://www.w3.org/TR/WCAG20/#relativeluminancedef
52 | *
53 | * @param {string} color the foreground color.
54 | * @returns {Number} the relative luminance.
55 | */
56 | function relativeLuminance(color) {
57 | let colorRed;
58 | let colorGreen;
59 | let colorBlue;
60 |
61 | try {
62 | [colorRed, colorGreen, colorBlue] = parseColor(color.trim()).rgb;
63 | } catch (error) {
64 | throw new Error('Error parsing Color with parseColor' + error);
65 | }
66 | let red = componentRelativeLuminance_(colorRed);
67 | let green = componentRelativeLuminance_(colorGreen);
68 | let blue = componentRelativeLuminance_(colorBlue);
69 |
70 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
71 | }
72 |
73 | /**
74 | * Generates the luminance of a single color component.
75 | * @param {Number} component the value to have the luminance calculated
76 | * @returns {Number} the calculated luminance of the color component.
77 | */
78 | function componentRelativeLuminance_(component) {
79 | let c = component / 255;
80 | return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
81 | }
82 |
83 | module.exports = {
84 | contrastRatio: contrastRatio,
85 | relativeLuminance: relativeLuminance,
86 | bestContrastRatio: bestContrastRatio
87 | };
88 |
--------------------------------------------------------------------------------
/test/app/controllers/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it before afterEach */
17 | 'use strict';
18 |
19 | const controllersCache = require('../../../controllers/cache');
20 | const libCache = require('../../../lib/data-cache');
21 |
22 | const express = require('express');
23 | const app = express();
24 | const request = require('supertest');
25 | const simpleMock = require('simple-mock');
26 | const chai = require('chai');
27 | const chaiAsPromised = require('chai-as-promised');
28 | chai.use(chaiAsPromised);
29 | chai.should();
30 | const assert = require('chai').assert;
31 |
32 | describe('controllers.cache', () => {
33 | before(done => {
34 | app.use('/',
35 | controllersCache,
36 | (req, res) => {
37 | res.send('PageRendered');
38 | }
39 | );
40 | done();
41 | });
42 |
43 | describe('GET /', () => {
44 | afterEach(() => {
45 | simpleMock.restore();
46 | });
47 |
48 | it('Page from cache', done => {
49 | simpleMock.mock(libCache, 'get').resolveWith('PageFromCache');
50 | simpleMock.mock(libCache, 'set').resolveWith();
51 | request(app)
52 | .get('/')
53 | .expect(200).should.be.fulfilled.then(res => {
54 | assert.equal(libCache.get.callCount, 1);
55 | assert.equal(res.text, 'PageFromCache');
56 | done();
57 | });
58 | });
59 |
60 | it('Not in cache, rendered directly', done => {
61 | simpleMock.mock(libCache, 'get').rejectWith('Not in cache').resolveWith('/');
62 | simpleMock.mock(libCache, 'set').resolveWith();
63 | simpleMock.mock(libCache, 'storeCachedUrls').resolveWith();
64 | request(app)
65 | .get('/')
66 | .expect(200).should.be.fulfilled.then(res => {
67 | assert.equal(res.text, 'PageRendered');
68 | assert.equal(libCache.get.callCount, 1);
69 | assert.equal(libCache.get.calls[0].args[0], '/');
70 | assert.equal(libCache.set.callCount, 1);
71 | assert.equal(libCache.set.calls[0].args[0], '/');
72 | assert.equal(libCache.set.calls[0].args[1], 'PageRendered');
73 | assert.equal(libCache.storeCachedUrls.callCount, 1);
74 | assert.equal(libCache.storeCachedUrls.calls[0].args[0], '/');
75 | done();
76 | })
77 | .catch(err => {
78 | console.log(err);
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/app/lib/color.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it */
17 | 'use strict';
18 |
19 | const assert = require('chai').assert;
20 | const color = require('../../../lib/color');
21 |
22 | describe('color.js', () => {
23 | describe('contrastRatio', () => {
24 | it('Calculates correct ratio for #000000', () => {
25 | const ratio = color.contrastRatio('#000000');
26 | assert.equal(ratio, 21);
27 | });
28 |
29 | it('Calculates correct ratio for #000000 / #FFFFFF', () => {
30 | const ratio = color.contrastRatio('#000000', '#FFFFFF');
31 | assert.equal(ratio, 21);
32 | });
33 |
34 | it('Calculates correct ratio for #FFFFFF / #000000', () => {
35 | const ratio = color.contrastRatio('#FFFFFF', '#000000');
36 | assert.equal(ratio, 21);
37 | });
38 |
39 | it('Calculates correct ratio for #FFFFFF / #FFFFFF', () => {
40 | const ratio = color.contrastRatio('#FFFFFF', '#FFFFFF');
41 | assert.equal(ratio, 1);
42 | });
43 |
44 | it('Calculates correct ratio for #FFFFFF / transparent', () => {
45 | const ratio = color.contrastRatio('#FFFFFF', 'transparent');
46 | assert.equal(ratio, 1);
47 | });
48 | });
49 |
50 | describe('bestContrastRatio', () => {
51 | it('Selects best contrast between #000000 and #FFFFFF agains #000000', () => {
52 | const bestContrast = color.bestContrastRatio('#000000', '#FFFFFF', '#000000');
53 | assert.equal(bestContrast, '#FFFFFF');
54 | });
55 |
56 | it('Selects best contrast between #000000 and #FFFFFF agains black', () => {
57 | const bestContrast = color.bestContrastRatio('#000000', '#FFFFFF', 'black');
58 | assert.equal(bestContrast, '#FFFFFF');
59 | });
60 | });
61 |
62 | describe('relativeLuminance', () => {
63 | it('Calculates correct luminance for #FFFFFF', () => {
64 | const luminance = color.relativeLuminance('#FFFFFF');
65 | assert.equal(luminance, 1);
66 | });
67 |
68 | it('Calculates correct luminance for #000000', () => {
69 | const luminance = color.relativeLuminance('#000000');
70 | assert.equal(luminance, 0);
71 | });
72 |
73 | it('Calculates correct luminance for "#000000 "', () => {
74 | const luminance = color.relativeLuminance('#000000 ');
75 | assert.equal(luminance, 0);
76 | });
77 |
78 | it('Calculates correct luminance for "black"', () => {
79 | const luminance = color.relativeLuminance('black');
80 | assert.equal(luminance, 0);
81 | });
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # PWA Directory FAQ
2 |
3 | ### What is PWA Directory?
4 | Is an open source directory of Progressive Web Apps driven by user submissions.
5 |
6 | ### What are the goals of this project?
7 | 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.
8 |
9 | ### Is this a Google product?
10 | No, it was built by the Google Developer Relations team as an example for the Web developer community.
11 |
12 | ### How does it rank PWAs?
13 | We use [Lighthouse](https://github.com/GoogleChrome/lighthouse), that runs a set of checks validating the existence of the features,
14 | capabilities, and performance that should characterize a PWA.
15 |
16 | ### Why is my Lighthouse score different from the Lighthouse Chrome Extension?
17 | 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.
18 |
19 | ### Why are you using Server Side Rendering?
20 | We found that there are not that many examples of PWAs using Server Side Rendering and that many developers would benefit from one.
21 |
22 | ### What technologies did you use?
23 | *Backend*
24 | - [Node.js](https://nodejs.org/en/)
25 | - [Express.js](http://expressjs.com/)
26 | - [Handlebars](http://handlebarsjs.com/)
27 | - [Google App Engine Node.js Flexible Environment](https://cloud.google.com/appengine/docs/flexible/nodejs/)
28 |
29 | *Frontend*
30 | - JavaScript (vanilla)
31 | - [Service Worker Precache](https://github.com/GoogleChrome/sw-precache)
32 | - [Service Worker Toolbox](https://github.com/GoogleChrome/sw-toolbox)
33 |
34 | *Storage*
35 | - [Google Cloud Datastore](https://cloud.google.com/datastore/) for general data
36 | - [Google Cloud Storage](https://cloud.google.com/storage/) for images only
37 |
38 | ### Why are you using Javascript without a framework?
39 | 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.
40 |
41 | ### What do you plan for the near future?
42 | 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.
43 |
44 | 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.
45 |
46 | ###Why didn’t you just collaborate with other existing PWA directories?
47 | 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.
48 |
49 | ### How do I request features or submit bugs?
50 |
51 | Please submit them directly in our [GitHub issues section](https://github.com/GoogleChrome/gulliver/issues).
52 |
--------------------------------------------------------------------------------
/test/app/lib/asset-hashing.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it beforeEach afterEach*/
17 | 'use strict';
18 |
19 | const assert = require('chai').assert;
20 | const simple = require('simple-mock');
21 |
22 | const assetHashing = require('../../../lib/asset-hashing');
23 |
24 | describe('ChecksumProvider', () => {
25 | it('calculates checksum', () => {
26 | const checksumProvider = new assetHashing.ChecksumProvider(__dirname);
27 | assert.equal(checksumProvider.get('asset-hashing.js').length, assetHashing.CHECKSUM_LENGTH);
28 | });
29 | });
30 |
31 | describe('AssetChecksum', () => {
32 | let checksumProvider = new assetHashing.ChecksumProvider();
33 | let asset;
34 |
35 | beforeEach(() => {
36 | simple.mock(checksumProvider, 'get', () => '1234567890');
37 | asset = new assetHashing.AssetChecksum(checksumProvider);
38 | });
39 |
40 | describe('encode', () => {
41 | it('adds checksum to file name', () => {
42 | assert.equal(asset.encode('public/style.css'), 'public/style.1234567890.css');
43 | });
44 | it('ignores dirs', () => {
45 | assert.equal(asset.encode('public/style'), 'public/style');
46 | });
47 | it('ignores empty string', () => {
48 | assert.equal(asset.encode(''), '');
49 | });
50 | it('ignores null', () => {
51 | assert.equal(asset.encode(null), null);
52 | });
53 | it('caches results', () => {
54 | asset.encode('public/style.css');
55 | asset.encode('public/style.css');
56 | assert.equal(checksumProvider.get.callCount, 1);
57 | });
58 | });
59 |
60 | describe('decode', () => {
61 | it('ignores dirs', () => {
62 | assert.equal(asset.decode('public/style'), 'public/style');
63 | });
64 | it('ignores empty string', () => {
65 | assert.equal(asset.decode(''), '');
66 | });
67 | it('ignores null', () => {
68 | assert.equal(asset.decode(null), null);
69 | });
70 | it('ignores non checksums', () => {
71 | assert.equal(asset.decode('style.12345/7890.css'), 'style.12345/7890.css');
72 | });
73 | it('removes checksum from file name', () => {
74 | assert.equal(asset.decode('public/style.1234567890.css'), 'public/style.css');
75 | });
76 | it('only checksums with length of 10', () => {
77 | assert.equal(asset.decode('public/style.123456789.css'), 'public/style.123456789.css');
78 | assert.equal(asset.decode('public/style.12345678900.css'), 'public/style.12345678900.css');
79 | });
80 | });
81 |
82 | afterEach(() => {
83 | simple.restore();
84 | });
85 | });
86 |
87 |
--------------------------------------------------------------------------------
/views/includes/header.hbs:
--------------------------------------------------------------------------------
1 | {{!-- Copyright 2015-2016, Google, Inc.
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License. --}}
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 | {{> notifications_active }}
24 | {{> notifications_off }}
25 |
26 |
27 |
28 |
29 |
30 |
{{> chevron_left }}
31 |
32 |
38 |
39 | {{> icon_share}}
40 |
41 |
42 | {{> icon_log_in}}
43 |
44 |
45 | {{> icon_log_out}}
46 |
47 |
48 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/app/lib/data-fetcher.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it afterEach*/
17 | 'use strict';
18 |
19 | let dataFetcher = require('../../../lib/data-fetcher');
20 | const simpleMock = require('simple-mock');
21 | let chai = require('chai');
22 | let chaiAsPromised = require('chai-as-promised');
23 | const assert = require('chai').assert;
24 | chai.use(chaiAsPromised);
25 | chai.should();
26 |
27 | const LIGHTHOUSE_JSON_EXAMPLE = './test/app/lib/lighthouse-example.json';
28 |
29 | describe('lib.data-fetcher', () => {
30 | it('fetchMetadataDescription(null) should fail', () => {
31 | return dataFetcher.fetchMetadataDescription(null).should.be.rejectedWith(Error);
32 | });
33 |
34 | it('fetchMetadataDescription(https://www.google.com) should work', () => {
35 | return dataFetcher.fetchMetadataDescription('https://www.google.com').should.be.fulfilled;
36 | });
37 |
38 | it('readfile(LIGHTHOUSE_JSON_EXAMPLE) should work', () => {
39 | return dataFetcher.readFile(LIGHTHOUSE_JSON_EXAMPLE).should.be.fulfilled;
40 | });
41 |
42 | describe('#_firebaseOptions', () => {
43 | it('should call with GET method', () => {
44 | const options = dataFetcher._firebaseOptions();
45 | assert.equal(options.method, 'GET');
46 | assert(options.headers.Authorization, 'Should contain Authorization header');
47 | });
48 |
49 | it('should call with POST method when payload exists', () => {
50 | const options = dataFetcher._firebaseOptions({});
51 | assert.equal(options.method, 'POST');
52 | assert(options.headers.Authorization, 'Should contain Authorization header');
53 | assert.equal(options.headers['content-type'], 'application/json', 'Correct content-type');
54 | });
55 | });
56 |
57 | describe('#_handleFirebaseResponse', () => {
58 | afterEach(() => {
59 | simpleMock.restore();
60 | });
61 |
62 | it('should succeed when code is 200', () => {
63 | const response = {};
64 | simpleMock.mock(response, 'status', 200);
65 | simpleMock.mock(response, 'json').resolveWith({});
66 |
67 | return dataFetcher._handleFirebaseResponse(response).should.be.fulfilled
68 | .then(() => {
69 | assert(response.json.called);
70 | });
71 | });
72 |
73 | it('should reject when code is not 200', () => {
74 | const response = {};
75 | simpleMock.mock(response, 'status', 402);
76 | simpleMock.mock(response, 'text').resolveWith({});
77 |
78 | return dataFetcher._handleFirebaseResponse(response).should.be.rejected
79 | .then(() => {
80 | assert(response.text.called);
81 | });
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/test/app/controllers/tasks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it before afterEach */
17 | 'use strict';
18 |
19 | const controllerTasks = require('../../../controllers/tasks');
20 | const tasksLib = require('../../../lib/tasks');
21 | const pwaLib = require('../../../lib/pwa');
22 | const Pwa = require('../../../models/pwa');
23 |
24 | const express = require('express');
25 | const app = express();
26 | const request = require('supertest');
27 | const simpleMock = require('simple-mock');
28 | const chai = require('chai');
29 | const chaiAsPromised = require('chai-as-promised');
30 | chai.use(chaiAsPromised);
31 | chai.should();
32 | const assert = require('chai').assert;
33 |
34 | const APP_ENGINE_CRON = 'X-Appengine-Cron';
35 | const MANIFEST_URL = 'https://pwa-directory.appspot.com/manifest.json';
36 |
37 | describe('controllers.tasks', () => {
38 | let listPwas = {};
39 | before(done => {
40 | app.use(controllerTasks);
41 | let pwa1 = new Pwa(MANIFEST_URL, null);
42 | pwa1.id = 123456789;
43 | let pwa2 = new Pwa(MANIFEST_URL, null);
44 | pwa2.id = 234567890;
45 | pwa2.lighthouseScore = 99;
46 | listPwas.pwas = [pwa1, pwa2];
47 | done();
48 | });
49 |
50 | describe('GET /tasks/cron', () => {
51 | afterEach(() => {
52 | simpleMock.restore();
53 | });
54 |
55 | it('respond with 403 forbidden when X-Appengine-Cron not present', done => {
56 | request(app)
57 | .get('/cron')
58 | .expect(403, done);
59 | });
60 | });
61 |
62 | describe('GET /tasks/updateunscored', () => {
63 | afterEach(() => {
64 | simpleMock.restore();
65 | });
66 |
67 | it('respond with 403 forbidden when X-Appengine-Cron not present', done => {
68 | request(app)
69 | .get('/updateunscored')
70 | .expect(403, done);
71 | });
72 |
73 | it('respond with 200 when X-Appengine-Cron is present', done => {
74 | simpleMock.mock(tasksLib, 'push').resolveWith(null);
75 | simpleMock.mock(pwaLib, 'list').resolveWith(listPwas);
76 | request(app)
77 | .get('/updateunscored')
78 | .set(APP_ENGINE_CRON, true)
79 | .expect(200).should.be.fulfilled.then(_ => {
80 | assert.equal(pwaLib.list.callCount, 1);
81 | assert.equal(tasksLib.push.callCount, 1);
82 | done();
83 | });
84 | });
85 | });
86 |
87 | describe('GET /tasks/execute', () => {
88 | afterEach(() => {
89 | simpleMock.restore();
90 | });
91 |
92 | it('respond with 403 forbidden when X-Appengine-Cron not present', done => {
93 | request(app)
94 | .get('/execute')
95 | .expect(403, done);
96 | });
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/test/app/lib/lighthouse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it */
17 | 'use strict';
18 |
19 | const lighthouseLib = require('../../../lib/lighthouse');
20 | const dataFetcher = require('../../../lib/data-fetcher');
21 |
22 | let simpleMock = require('simple-mock');
23 | let chai = require('chai');
24 | let chaiAsPromised = require('chai-as-promised');
25 | chai.use(chaiAsPromised);
26 | chai.should();
27 | let assert = require('chai').assert;
28 |
29 | const LIGHTHOUSE_JSON_EXAMPLE = './test/app/lib/lighthouse-example.json';
30 |
31 | describe('lib.lighthouse', () => {
32 | it('processLighthouseJson(lighthouse-example.json) should work', () => {
33 | return dataFetcher.readFile(LIGHTHOUSE_JSON_EXAMPLE)
34 | .then(data => {
35 | const rawData = JSON.parse(data)[0].rawData.value;
36 | const lighthouseInfo = lighthouseLib.processLighthouseJson(JSON.parse(rawData));
37 | assert.equal(lighthouseInfo.totalScore, 91);
38 | assert.equal(lighthouseInfo.lighthouseVersion, '2.9.1');
39 | assert.equal(lighthouseInfo.reportCategories[1].name, 'Progressive Web App');
40 | });
41 | });
42 |
43 | it('getLighthouseGraphByPwaId should return null if theres not data for PWA', () => {
44 | simpleMock.mock(lighthouseLib, 'getLighthouseByPwaId').resolveWith([]);
45 | return lighthouseLib.getLighthouseGraphByPwaId(123).should.be.fulfilled.then(json => {
46 | assert.equal(json, null);
47 | assert.equal(lighthouseLib.getLighthouseByPwaId.callCount, 1);
48 | });
49 | });
50 |
51 | let lighthouseData = {};
52 | lighthouseData = {
53 | date: '2016-10-27',
54 | id: '5768151446847488-2016-10-27',
55 | totalScore: 69,
56 | lighthouseInfo: [],
57 | lighthouseVersion: '1.1.6',
58 | pwaId: 5768151446847488,
59 | absoluteStartUrl: 'https://www.ampproject.org/'
60 | };
61 |
62 | it('getLighthouseGraphByPwaId should crete graph formatted data', () => {
63 | simpleMock.mock(lighthouseLib, 'getLighthouseByPwaId').resolveWith([lighthouseData]);
64 | return lighthouseLib.getLighthouseGraphByPwaId(123).should.be.fulfilled.then(json => {
65 | const date = new Date(Date.parse('2016-10-27'));
66 | assert.equal(json.cols[0].label, 'Date');
67 | assert.equal(json.cols[0].type, 'date');
68 | assert.equal(json.cols[1].label, 'Score');
69 | assert.equal(json.cols[1].type, 'number');
70 | assert.equal(json.rows[0].c[0].v,
71 | 'Date(' + date.getFullYear() + ',' + date.getMonth() + ',' + date.getDate() + ')');
72 | assert.equal(json.rows[0].c[1].v, 69);
73 | assert.equal(lighthouseLib.getLighthouseByPwaId.callCount, 1);
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/public/js/gapi.es6.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 |
18 | /**
19 | * Returns a Promise that fulfills to `window.gapi`. Note that this function
20 | * will probably create the global properties `gapiReady` and `gapiResolve`.
21 | *
22 | * @param {typeof window} context
23 | * @param {typeof document} doc
24 | * @return {Promise}
25 | */
26 | export function gapi(context = window, doc = document) {
27 | return context.gapiReady || new Promise(resolve => {
28 | // Adapted from GA embed code
29 | const c = 'gapiResolve';
30 | const s = doc.createElement('script');
31 | const p = doc.getElementsByTagName('script')[0];
32 | s.async = 1;
33 | s.src = `https://apis.google.com/js/api.js?onload=${c}`;
34 | p.parentNode.insertBefore(s, p);
35 | context[c] = () => resolve(window.gapi);
36 | });
37 | }
38 |
39 | /**
40 | * @template T
41 | * @param {string} name the library to load
42 | * @return {Promise} resolves to window.gapi[name]
43 | */
44 | export function gapiLoad(name) {
45 | return gapi().then(g => {
46 | return new Promise(resolve => {
47 | g.load(name, () => resolve(g[name]));
48 | });
49 | });
50 | }
51 |
52 | /**
53 | * Promise'd version of [`gapi.client.load`](https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientloadname--------version--------callback).
54 | *
55 | * @template T
56 | * @param {string} name the API client to load
57 | * @param {string} [version="v1"] version
58 | * @return {Promise} resolves to gapi.client[name]
59 | */
60 | export function clientLoad(name, version) {
61 | version = version ? version : 'v1';
62 | return gapiLoad('client').then(client => {
63 | return new Promise(resolve => {
64 | client.load(name, version, () => resolve(client[name]));
65 | });
66 | });
67 | }
68 |
69 | /**
70 | * Promise'd version of [`gapi.auth2.init`](https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams).
71 | *
72 | * @param {any} params https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams
73 | * @return {Promise} Promise resolving to an initialized gapi.auth2.GoogleAuth object
74 | */
75 | export function authInit(params) {
76 | return gapiLoad('auth2').then(auth2 => {
77 | /* Ideally we'd just return `auth2.init(params)` here, but
78 | * instead we need to work around a few bugs and surprises in
79 | * `auth2.init()` and the "Promise" it returns.
80 | */
81 | return new Promise(resolve => {
82 | auth2.init(params).then(t => {
83 | t.then = null;
84 | resolve(t);
85 | });
86 | });
87 | });
88 | }
89 |
--------------------------------------------------------------------------------
/public/js/signin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* eslint-env browser */
17 | import {authInit} from './gapi.es6.js';
18 |
19 | export default class SignIn {
20 | constructor(window, config) {
21 | this.window = window;
22 | this.config = config;
23 | this._init();
24 | this._setupEventHandlers();
25 | }
26 |
27 | _init() {
28 | /* eslint-disable camelcase */
29 | const params = {
30 | scope: 'profile',
31 | client_id: this.config.client_id,
32 | fetch_basic_profile: false
33 | };
34 | /* eslint-enable camelcase */
35 |
36 | return authInit(params).then(auth => {
37 | this.auth = auth;
38 | this._setupUserChangeEvents(auth);
39 | return this;
40 | });
41 | }
42 |
43 | _setupUserChangeEvents(auth) {
44 | this.window.auth = auth; // TODO: Temporary Hack to Make 'ui/client-transition.js' work.
45 | // Fire 'userchange' event on page load (not just when status changes)
46 | this.window.dispatchEvent(new CustomEvent('userchange', {
47 | detail: auth.currentUser.get()
48 | }));
49 |
50 | // Fire 'userchange' event when status changes
51 | auth.currentUser.listen(user => {
52 | window.dispatchEvent(new CustomEvent('userchange', {
53 | detail: user
54 | }));
55 | });
56 | }
57 |
58 | get signedIn() {
59 | return this.user && this.user.isSignedIn();
60 | }
61 |
62 | get user() {
63 | if (!this.auth) {
64 | return null;
65 | }
66 | return this.auth.currentUser.get();
67 | }
68 |
69 | get idToken() {
70 | if (!this.signedIn) {
71 | return null;
72 | }
73 | return this.user.getAuthResponse().id_token;
74 | }
75 |
76 | signIn() {
77 | if (!this.auth) {
78 | console.log('Auth not ready!');
79 | return;
80 | }
81 | this.auth.signIn();
82 | }
83 |
84 | signOut() {
85 | if (!this.auth) {
86 | console.log('Auth not ready!');
87 | return;
88 | }
89 | this.auth.signOut();
90 | }
91 |
92 | /**
93 | * All elements with class .gulliver-signedin-aware will:
94 | * have a 'signedin' dataset property that reflects the current signed in state.
95 | * receive a 'change' event whenever the state changes.
96 | */
97 | _setupEventHandlers() {
98 | const body = this.window.document.querySelector('body');
99 | this.window.addEventListener('userchange', e => {
100 | const user = e.detail;
101 | if (user.isSignedIn()) {
102 | body.setAttribute('signedIn', 'true');
103 | } else {
104 | body.removeAttribute('signedIn');
105 | }
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lighthouse_machine/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2016-2017, Google, Inc.
2 | # Licensed under the Apache License, Version 2.0 (the "License");
3 | # you may not use this file except in compliance with the License.
4 | # You may obtain a copy of the License at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # Unless required by applicable law or agreed to in writing, software
9 | # distributed under the License is distributed on an "AS IS" BASIS,
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | # See the License for the specific language governing permissions and
12 | # limitations under the License.
13 |
14 | FROM ubuntu:latest
15 |
16 | ## PART 1: Core components
17 | ## =======================
18 |
19 | # Install utilities
20 | RUN apt-get update --fix-missing && apt-get -y upgrade &&\
21 | apt-get install -y sudo apt-utils curl wget unzip git gnupg
22 |
23 | # Install node 10
24 | RUN curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - &&\
25 | sudo apt-get install -y nodejs
26 |
27 | # Install Xvfb and dbus for X11
28 | RUN apt-get install -y xvfb dbus-x11
29 |
30 | # Install Chrome for Ubuntu
31 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - &&\
32 | sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' &&\
33 | sudo apt-get update &&\
34 | sudo apt-get install -y google-chrome-stable
35 |
36 | # Install Yarn
37 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - &&\
38 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list &&\
39 | sudo apt-get update && sudo apt-get install yarn
40 |
41 | # Copy key documents (except .dockerignored files)
42 | COPY etc/xvfb /etc/init.d/xvfb
43 | RUN chmod +x /etc/init.d/xvfb
44 |
45 | # Add a user and make it a sudo user
46 | RUN useradd -m chromeuser
47 |
48 | # Copy the chrome-user script used to start Chrome as non-root
49 | COPY chromeuser-script.sh /
50 | RUN chmod +x /chromeuser-script.sh
51 |
52 | ## PART 2: Lighthouse
53 | ## ==================
54 |
55 | # Download lighthouse
56 | RUN git clone https://github.com/googlechrome/lighthouse &&\
57 | cd /lighthouse &&\
58 | git checkout tags/v4.2.0 &&\
59 | npm install -g yarn &&\
60 | npm install -g yarnpkg &&\
61 | npm install -g @types/mkdirp &&\
62 | npm install -g --save-dev run-sequence &&\
63 | npm install -g typescript &&\
64 | npm install -g &&\
65 | yarn global add lighthouse
66 |
67 | ## PART 3: Express server
68 | ## ======================
69 |
70 | # Install express
71 | COPY package.json /
72 | RUN npm install
73 |
74 | # Add the simple server file
75 | COPY server.js /
76 | RUN chmod +x /server.js
77 |
78 | # Add the cpu monitor file
79 | COPY cpu_monitor.js /
80 | RUN chmod +x /cpu_monitor.js
81 |
82 | # Generate a self-signed SSL certificate
83 | RUN openssl req \
84 | -new \
85 | -newkey rsa:4096 \
86 | -days 365 \
87 | -nodes \
88 | -x509 \
89 | -subj "/C=GB/ST=None/L=None/O=Google/CN=lighthouse-machine-X" \
90 | -keyout key.pem \
91 | -out cert.pem
92 |
93 | # Expose ports 8080 and 8443
94 | EXPOSE 8080
95 | EXPOSE 8443
96 |
97 | ## PART 4: Final setup
98 | ## ===================
99 |
100 | # Set the entrypoint
101 | COPY entrypoint.sh /
102 | RUN chmod +x /entrypoint.sh
103 | ENTRYPOINT ["/entrypoint.sh"]
104 |
--------------------------------------------------------------------------------
/public/js/chart.js:
--------------------------------------------------------------------------------
1 | /**
2 | * copyright 2015-2016, google, inc.
3 | * licensed under the apache license, version 2.0 (the "license");
4 | * you may not use this file except in compliance with the license.
5 | * you may obtain a copy of the license at
6 | *
7 | * http://www.apache.org/licenses/license-2.0
8 | *
9 | * unless required by applicable law or agreed to in writing, software
10 | * distributed under the license is distributed on an "as is" basis,
11 | * without warranties or conditions of any kind, either express or implied.
12 | * see the license for the specific language governing permissions and
13 | * limitations under the license.
14 | */
15 |
16 | /* global google */
17 | /* eslint-env browser */
18 | import Loader from './loader';
19 |
20 | /**
21 | * Use to make the API request to get the Lighthouse chart data for a PWA.
22 | */
23 | export default class Chart {
24 |
25 | constructor(config) {
26 | this.chartElement = config.chartElement;
27 | this.url = config.url;
28 | this.loader = new Loader(this.chartElement, 'dark-primary-background');
29 | }
30 |
31 | _loadChartsApi() {
32 | return new Promise((resolve, reject) => {
33 | const chartScript = document.getElementById('google-chart');
34 | if (chartScript) {
35 | if (window.google) {
36 | resolve(window.google);
37 | } else {
38 | chartScript.addEventListener('load', _ => resolve(window.google));
39 | }
40 | } else {
41 | const script = document.createElement('script');
42 | script.id = 'google-chart';
43 | script.defer = true;
44 | script.src = 'https://www.gstatic.com/charts/loader.js';
45 | script.onload = _ => resolve(window.google);
46 | script.onerror = reject;
47 | document.head.appendChild(script);
48 | }
49 | });
50 | }
51 |
52 | load() {
53 | this.loader.show();
54 | this._loadChartsApi().then(google => {
55 | google.charts.load('45.2', {packages: ['annotationchart']});
56 | google.charts.setOnLoadCallback(this.drawChart.bind(this));
57 | });
58 | }
59 |
60 | drawChart() {
61 | if (!this.url) {
62 | return;
63 | }
64 | const pagewith = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
65 | fetch(this.url)
66 | .then(response => response.json())
67 | .then(jsonData => {
68 | // Create our data table out of JSON data loaded from server.
69 | const data = new google.visualization.DataTable(jsonData);
70 | if (data.getNumberOfRows() > 0) {
71 | const chart = new google.visualization.AnnotationChart(this.chartElement);
72 | const options = {
73 | height: 242,
74 | displayAnnotations: false,
75 | displayRangeSelector: false,
76 | displayZoomButtons: (pagewith > 420),
77 | legendPosition: 'newRow',
78 | thickness: 4,
79 | min: 0,
80 | max: 100
81 | };
82 | chart.draw(data, options);
83 | this.loader.hide();
84 | } else {
85 | this.loader.hide();
86 | const missingChart = this.chartElement.querySelector('div#chart-missing');
87 | missingChart.classList.add('fadeIn');
88 | }
89 | })
90 | .catch(err => {
91 | this.loader.hide();
92 | const missingChart = document.getElementById('chart-missing');
93 | missingChart.classList.add('fadeIn');
94 | console.error('There was an error drawing the chart!', err);
95 | });
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/views/pwas/list.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 | {{#unless contentOnly}}
15 |
16 |
17 |
18 | {{> head}}
19 |
20 |
21 | {{> header}}
22 |
23 |
24 | {{/unless}}
25 |
32 |
33 |
49 |
50 |
74 | {{#unless contentOnly}}
75 |
76 |
77 |
78 |
79 | {{> footer}}
80 |
81 |
82 | {{/unless}}
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gulliver",
3 | "version": "1.0.0",
4 | "description": "A directory of PWAs",
5 | "repository": "https://github.com/GoogleChrome/gulliver",
6 | "private": true,
7 | "scripts": {
8 | "start": "node app.js",
9 | "prestart": "BABEL_ENV=default rollup -c rollup-config/gulliver.js && npm run generate-msg-sw",
10 | "monitor": "nodemon app.js",
11 | "deploy": "npm run prestart && gcloud app deploy app.yaml",
12 | "mocha-app": "_mocha test/app/**/* --exit",
13 | "mocha-client": "BABEL_ENV=test _mocha --compilers js:babel-core/register test/client/**/*.js",
14 | "coverage": "istanbul cover _mocha --compilers js:babel-core/register test/app/**/*",
15 | "lint": "eslint .",
16 | "test": "npm run lint && npm run mocha-client && npm run mocha-app",
17 | "generate-msg-sw": "node firebase-messaging-sw-generator.js",
18 | "lint-fix": "eslint --fix ."
19 | },
20 | "author": "Google Inc.",
21 | "contributors": [
22 | {
23 | "name": "Julian Toledo",
24 | "email": "jtoledo@google.com"
25 | },
26 | {
27 | "name": "Michael Stillwell",
28 | "email": "stillers@google.com"
29 | },
30 | {
31 | "name": "Andre Bandarra",
32 | "email": "andreban@google.com"
33 | }
34 | ],
35 | "license": "Apache Version 2.0",
36 | "semistandard": {
37 | "globals": [
38 | "after",
39 | "afterEach",
40 | "before",
41 | "beforeEach",
42 | "describe",
43 | "it"
44 | ]
45 | },
46 | "engines": {
47 | "nodejs8": "8.12.0"
48 | },
49 | "dependencies": {
50 | "@google-cloud/datastore": "^1.4.2",
51 | "@google-cloud/storage": "^1.7.0",
52 | "babel-preset-es2015-rollup": "^3.0.0",
53 | "body-parser": "^1.18.3",
54 | "cheerio": "^0.22.0",
55 | "compression": "^1.7.3",
56 | "elasticlunr": "^0.9.5",
57 | "escape-html": "^1.0.3",
58 | "express": "^4.16.4",
59 | "express-csv": "^0.6.0",
60 | "express-minify-html": "^0.12.0",
61 | "express-sslify": "^1.2.0",
62 | "firebase": "^5.5.9",
63 | "google-auth-library": "^1.6.1",
64 | "handlebars": "^4.5.3",
65 | "hbs": "^4.0.4",
66 | "http-parser-js": "^0.4.13",
67 | "jsdom": "^9.5.0",
68 | "lodash.merge": "^4.6.2",
69 | "lodash.template": "^4.5.0",
70 | "memcached": "^2.2.2",
71 | "mime-types": "^2.1.21",
72 | "moment": "^2.22.2",
73 | "multer": "^1.4.1",
74 | "nconf": "^0.8.4",
75 | "node-fetch": "^2.6.1",
76 | "parse-color": "^1.0.0",
77 | "request": "^2.88.0",
78 | "rev-hash": "^1.0.0",
79 | "rollup": "^0.58.2",
80 | "rollup-plugin-babel": "^2.6.1",
81 | "rollup-plugin-commonjs": "^9.2.0",
82 | "rollup-plugin-node-resolve": "^2.0.0",
83 | "rollup-plugin-uglify": "^1.0.1",
84 | "rss": "^1.2.2",
85 | "serve-static": "^1.11.1",
86 | "sharp": "^0.17.0",
87 | "spdy": "^3.4.7",
88 | "strong-data-uri": "^1.0.6",
89 | "sw-offline-google-analytics": "^1.1.1",
90 | "sw-toolbox": "^3.2.1",
91 | "urijs": "^1.18.1",
92 | "url-polyfill": "^1.1.0",
93 | "whatwg-fetch": "^2.0.1",
94 | "yaku": "^0.17.6"
95 | },
96 | "devDependencies": {
97 | "babel-preset-es2015": "^6.24.1",
98 | "chai": "^3.0.0",
99 | "chai-as-promised": "^6.0.0",
100 | "eslint": "^6.6.0",
101 | "eslint-config-google": "^0.6.0",
102 | "istanbul": "^0.4.4",
103 | "mocha": "^5.2.0",
104 | "node-mocks-http": "^1.7.3",
105 | "simple-mock": "^0.7.0",
106 | "supertest": "^3.3.0"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/web-performance.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const config = require('../config/config');
19 | const dataFetcher = require('../lib/data-fetcher');
20 |
21 | const WEBPERFORMANCE_SERVER_URL = config.get('WEBPERFORMANCE_SERVER');
22 | const WEBPERFORMANCE_SERVER_API_KEY = config.get('WEBPERFORMANCE_SERVER_API_KEY');
23 | const WEBPERFORMANCE_SERVER_WEBPAGEURL = WEBPERFORMANCE_SERVER_URL + 'webpageurl';
24 | const WEBPERFORMANCE_SERVER_PAGESPEED_REPORT = WEBPERFORMANCE_SERVER_URL + 'pagespeedreport/';
25 | const WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT = WEBPERFORMANCE_SERVER_URL + 'webpagetestreport/';
26 | const WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT = WEBPERFORMANCE_SERVER_URL + 'lighthousereport/';
27 |
28 | function submitToWebPerformanceService(pwa) {
29 | const body = {
30 | id: pwa.id,
31 | url: pwa.absoluteStartUrl,
32 | source: 'pwa-directory',
33 | description: pwa.description,
34 | created: pwa.created
35 | };
36 | return dataFetcher.postJson(
37 | WEBPERFORMANCE_SERVER_WEBPAGEURL + '?key=' + WEBPERFORMANCE_SERVER_API_KEY, body);
38 | }
39 |
40 | /**
41 | * Submit PWA to the WebPerformance service.
42 | *
43 | * @param {number} a PWA
44 | * @return {Promise}
45 | */
46 | exports.submitWebPageUrl = function(pwa) {
47 | return new Promise((resolve, reject) => {
48 | submitToWebPerformanceService(pwa)
49 | .then(result => {
50 | return resolve(result);
51 | })
52 | .catch(err => {
53 | return reject(err);
54 | });
55 | });
56 | };
57 |
58 | /**
59 | * Get Report for PWA.
60 | *
61 | * @param {PWA} a PWA
62 | * @return {Promise}
63 | */
64 | function getReport(url) {
65 | return dataFetcher.fetchWithUA(url)
66 | .then(response => {
67 | if (response.status === 200) {
68 | return response.json();
69 | } else if (response.status === 404) {
70 | return Promise.reject('not available yet');
71 | }
72 | return Promise.reject(response);
73 | })
74 | .catch(err => {
75 | return Promise.reject(err);
76 | });
77 | }
78 |
79 | /**
80 | * Get PageSpeed Report for PWA.
81 | *
82 | * @param {PWA} a PWA
83 | * @return {Promise}
84 | */
85 | exports.getPageSpeedReport = function(pwa) {
86 | return getReport(WEBPERFORMANCE_SERVER_PAGESPEED_REPORT + pwa.id + '?limit=1');
87 | };
88 |
89 | /**
90 | * Get WebPageTest Report for PWA.
91 | *
92 | * @param {PWA} a PWA
93 | * @return {Promise}
94 | */
95 | exports.getWebPageTestReport = function(pwa) {
96 | return getReport(WEBPERFORMANCE_SERVER_WEBPAGETEST_REPORT + pwa.id + '?limit=1');
97 | };
98 |
99 | /**
100 | * Get Lighthouse Report for PWA.
101 | *
102 | * @param {PWA} a PWA
103 | * @return {Promise}
104 | */
105 | exports.getLighthouseReport = function(pwa) {
106 | return getReport(WEBPERFORMANCE_SERVER_LIGHTHOUSE_REPORT + pwa.id + '?limit=1');
107 | };
108 |
--------------------------------------------------------------------------------
/lighthouse_machine/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016-2017, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const express = require('express');
19 | const exec = require('child_process').exec;
20 | const http = require('http');
21 | const https = require('https');
22 | const fs = require('fs');
23 | const cpuMonitor = require('./cpu_monitor');
24 |
25 | // Chrome panick
26 | let chromePanick = false;
27 |
28 | // CPU monitoring
29 | let cpuPoints = new Array(5);
30 | let cpuAlert = false;
31 |
32 | cpuMonitor(60000, load => {
33 | // Add new measurements to the cpuPoints array
34 | cpuPoints.pop();
35 | cpuPoints.unshift(load);
36 |
37 | // Calculate the avg and spread of the cpuPoints array
38 | let sum = 0;
39 | let i = 5;
40 | while (i--) sum += cpuPoints[i];
41 | let avg = sum / 5;
42 | let spread = Math.max.apply(Math, cpuPoints) - Math.min.apply(Math, cpuPoints);
43 |
44 | // If the CPU load is above 80% and the spread is less than 10%, trigger an alert
45 | cpuAlert = (avg > 0.8 && spread < 0.1);
46 | cpuAlert && console.log(`Average: ${avg}, Spread: ${spread}`);
47 | });
48 |
49 | // Constants
50 | const HTTP_PORT = 8080;
51 | const HTTPS_PORT = 8443;
52 |
53 | // HTTPS options
54 | const options = {
55 | key: fs.readFileSync('key.pem'),
56 | cert: fs.readFileSync('cert.pem')
57 | };
58 |
59 | // App
60 | const app = express();
61 | let isBusy = false;
62 |
63 | // Main endpoint
64 | app.get('/', (req, res) => {
65 | if (isBusy) {
66 | res.sendStatus(429);
67 | } else {
68 | isBusy = true;
69 | res.setTimeout(500000, _ => {
70 | console.log('Request has timed out.');
71 | res.send(408);
72 | });
73 | try {
74 | exec(
75 | `lighthouse '${req.query.url}' --port 9222 --output-path ../report.${req.query.format} --output ${req.query.format}`,
76 | {
77 | cwd: '/lighthouse',
78 | timeout: 500000
79 | },
80 | error => {
81 | if (error !== null) {
82 | console.log(`exec error: ${error}`);
83 |
84 | // This is for when Chrome crashes and Lighthouse is unable to reconnect
85 | // to an appropriate instance of Chrome
86 | if (error.message.includes('Unable to connect')) {
87 | chromePanick = true;
88 | }
89 | }
90 |
91 | isBusy = false;
92 | res.sendFile(`/report.${req.query.format}`);
93 | }
94 | );
95 | } catch (e) {
96 | isBusy = false;
97 | res.status(500).send(e);
98 | }
99 | }
100 | });
101 |
102 | // Auto-healing endpoint
103 | app.get('/_ah/health', (req, res) => {
104 | if (chromePanick) {
105 | // If we have a Chrome panick send a 500
106 | res.sendStatus(500);
107 | } else if (cpuAlert) {
108 | // if we have a CPU alert send a 500, otherwise send a 200
109 | res.sendStatus(500);
110 | } else {
111 | res.sendStatus(200);
112 | }
113 | });
114 |
115 | // Busy-ness endpoit
116 | app.get('/_ah/busy', (req, res) => {
117 | if (isBusy) {
118 | res.sendStatus(503);
119 | } else {
120 | res.sendStatus(200);
121 | }
122 | });
123 |
124 | http.createServer(app).listen(HTTP_PORT);
125 | https.createServer(options, app).listen(HTTPS_PORT);
126 |
127 | console.log(
128 | `Running on https://localhost:${HTTPS_PORT} and http://localhost:${HTTP_PORT}`
129 | );
130 |
--------------------------------------------------------------------------------
/test/app/lib/favorite-pwa.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it before beforeEach */
17 | 'use strict';
18 |
19 | const assert = require('assert');
20 | const config = require('../../../config/config');
21 |
22 | const datastore = require('@google-cloud/datastore');
23 | const ds = datastore({
24 | projectId: config.get('GCLOUD_PROJECT')
25 | });
26 |
27 | const FavoritePwa = require('../../../models/favorite-pwa');
28 | const libFavoritePwa = require('../../../lib/favorite-pwa');
29 |
30 | const ENTITY_NAME = 'FAVORITE-PWA';
31 | const TEST_FAV_PWA = new FavoritePwa(123456789, 987654321);
32 |
33 | describe('lib.favorite-pwa', () => {
34 | const skipTests = process.env.TRAVIS;
35 | // Skip tests if Running in CI
36 | before(function() {
37 | this.timeout(3000);
38 | if (skipTests) {
39 | this.skip();
40 | return;
41 | }
42 |
43 | // Deletes all entities on the 'test' namespace before each test.
44 | return new Promise((resolve, reject) => {
45 | const q = ds.createQuery(ENTITY_NAME).filter('pwaId', '=', parseInt(TEST_FAV_PWA.pwaId, 10));
46 | ds.runQuery(q, (err, entities) => {
47 | if (err) {
48 | return reject(err);
49 | }
50 |
51 | const keys = entities.map(entity => {
52 | return entity.key;
53 | });
54 |
55 | // Delete counts for 'test'.
56 | keys[keys.length] = ds.key(['counts', ENTITY_NAME]);
57 | ds.delete(keys, err => {
58 | return reject(err);
59 | });
60 |
61 | return resolve();
62 | });
63 | });
64 | });
65 |
66 | describe('#save and find', () => {
67 | beforeEach(function() {
68 | if (skipTests) {
69 | this.skip();
70 | return;
71 | }
72 | });
73 |
74 | let savedFavoritePwa;
75 | before(() => {
76 | if (skipTests) {
77 | return;
78 | }
79 | return libFavoritePwa.save(TEST_FAV_PWA)
80 | .then(saved => {
81 | savedFavoritePwa = saved;
82 | });
83 | });
84 |
85 | it('save', () => {
86 | assert.equal(savedFavoritePwa.pwaId, TEST_FAV_PWA.pwaId);
87 | assert.equal(savedFavoritePwa.userId, TEST_FAV_PWA.userId);
88 | });
89 |
90 | it('findByUserId', () => {
91 | return libFavoritePwa.findByUserId(TEST_FAV_PWA.userId)
92 | .then(foundFavoritePwas => {
93 | assert.equal(foundFavoritePwas[0].pwaId, TEST_FAV_PWA.pwaId);
94 | assert.equal(foundFavoritePwas[0].userId, TEST_FAV_PWA.userId);
95 | });
96 | });
97 |
98 | it('findFavoritePwa', () => {
99 | return libFavoritePwa.findFavoritePwa(TEST_FAV_PWA.pwaId, TEST_FAV_PWA.userId)
100 | .then(foundFavoritePwa => {
101 | assert.equal(foundFavoritePwa.pwaId, TEST_FAV_PWA.pwaId);
102 | assert.equal(foundFavoritePwa.userId, TEST_FAV_PWA.userId);
103 | });
104 | });
105 | });
106 |
107 | describe('#delete', () => {
108 | beforeEach(function() {
109 | if (skipTests) {
110 | this.skip();
111 | return;
112 | }
113 | });
114 |
115 | it('delete', () => {
116 | return libFavoritePwa.save(TEST_FAV_PWA)
117 | .then(saved => {
118 | return libFavoritePwa.delete(saved.id).should.be.fulfilled;
119 | });
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/lib/tasks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const db = require('../lib/model-datastore');
19 | const Task = require('../models/task');
20 | const pwaLib = require('../lib/pwa');
21 | const tasksLib = require('../lib/tasks');
22 |
23 | const ENTITY_NAME = 'Task';
24 | const E_SAVING_TASK = exports.E_SAVING_TASK = 1;
25 | const E_GET_TASK_POP = exports.E_GET_TASK_POP = 2;
26 | const E_DELETE_TASK_POP = exports.E_DELETE_TASK_POP = 3;
27 |
28 | /**
29 | * Push a Task object into the DB.
30 | *
31 | * @param {Task} lighthouse
32 | * @return {Promise}
33 | */
34 | exports.push = function(task) {
35 | return new Promise((resolve, reject) => {
36 | db.update(ENTITY_NAME, task.id, task)
37 | .then(result => {
38 | return resolve(result);
39 | })
40 | .catch(err => {
41 | console.error(err);
42 | return reject(E_SAVING_TASK);
43 | });
44 | });
45 | };
46 |
47 | exports.getTasks = async function(numTasks) {
48 | const result =
49 | await db.list(ENTITY_NAME, 0, numTasks, {field: 'created', config: {ascending: true}});
50 | const tasks = [];
51 | for (let entity of result.entities) {
52 | tasks.push(Object.assign(new Task(), entity));
53 | }
54 | return tasks;
55 | };
56 |
57 | exports.deleteTask = async function(taskId) {
58 | await db.delete(ENTITY_NAME, taskId);
59 | };
60 |
61 | /**
62 | * Pop the oldest Task
63 | *
64 | * @return {Promise}
65 | */
66 | exports.pop = function() {
67 | return new Promise((resolve, reject) => {
68 | db.list(ENTITY_NAME, 0, 1, {field: 'created', config: {ascending: true}})
69 | .then(result => {
70 | if (result.entities.length === 0) {
71 | return resolve(null);
72 | }
73 | let task = Object.assign(new Task(), result.entities[0]);
74 | db.delete(ENTITY_NAME, task.id)
75 | .then(_ => {
76 | return resolve(task);
77 | }).catch(err => {
78 | console.error('Error deleting task', err);
79 | return reject(E_DELETE_TASK_POP);
80 | });
81 | })
82 | .catch(_ => {
83 | return reject(E_GET_TASK_POP);
84 | });
85 | });
86 | };
87 |
88 | /**
89 | * Execute a task
90 | *
91 | * @return {Promise}
92 | */
93 | exports.executePwaTask = function(task) {
94 | if (!task) {
95 | return Promise.resolve();
96 | }
97 | return pwaLib.find(task.pwaId)
98 | .then(pwa => {
99 | // Dynamically get module and function to execute from task with a PWA
100 | // const moduleFromTask = require(task.modulePath);
101 | const moduleFromTask = require('../lib/pwa');
102 | const functionFromTask = Reflect.get(moduleFromTask, task.functionName);
103 | return functionFromTask.call(moduleFromTask, pwa)
104 | .then(_ => {
105 | return task;
106 | })
107 | .catch(err => {
108 | console.error('Error running task: ' + err);
109 | task.retries -= 1;
110 | if (task.retries >= 0) {
111 | tasksLib.push(task);
112 | }
113 | return task;
114 | });
115 | })
116 | .catch(err => {
117 | console.error(err);
118 | });
119 | };
120 |
121 | /**
122 | * Pop the oldest Task and execute it
123 | *
124 | * @return {Promise}
125 | */
126 | exports.popExecute = function() {
127 | return tasksLib.pop()
128 | .then(task => {
129 | if (task) {
130 | return tasksLib.executePwaTask(task);
131 | }
132 | return null;
133 | });
134 | };
135 |
--------------------------------------------------------------------------------
/test/app/lib/images.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | /* global describe it afterEach before */
17 | 'use strict';
18 |
19 | const libImages = require('../../../lib/images');
20 | const dataFetcher = require('../../../lib/data-fetcher');
21 | const Manifest = require('../../../models/manifest');
22 |
23 | const httpMocks = require('node-mocks-http');
24 | const simpleMock = require('simple-mock');
25 | const chai = require('chai');
26 | const chaiAsPromised = require('chai-as-promised');
27 | chai.use(chaiAsPromised);
28 | chai.should();
29 | const assert = require('chai').assert;
30 |
31 | const MANIFEST_URL = 'https://mobile.twitter.com/manifest.json';
32 | const MANIFEST_DATA = './test/app/manifests/inline-image-large-content.json';
33 |
34 | describe('lib.images', () => {
35 | let manifest;
36 | before(done => {
37 | dataFetcher.readFile(MANIFEST_DATA)
38 | .then(jsonString => {
39 | manifest = new Manifest(MANIFEST_URL, JSON.parse(jsonString));
40 | done();
41 | });
42 | });
43 | afterEach(() => {
44 | simpleMock.restore();
45 | });
46 |
47 | it('fetchAndSave fail fetch for HTTP error', () => {
48 | const response = {};
49 | response.status = 400;
50 | simpleMock.mock(dataFetcher, 'fetchWithUA').resolveWith(response);
51 | simpleMock.mock(libImages, 'saveImage').resolveWith(null);
52 | return libImages.fetchAndSave('http://www.test.com', null).should.be.rejectedWith(
53 | 'Bad Response (400) loading image: undefined');
54 | });
55 |
56 | it('fetchAndSave fail for unsoported protocol (ftp:)', () => {
57 | const response = {};
58 | response.status = 400;
59 | simpleMock.mock(dataFetcher, 'fetchWithUA').resolveWith(response);
60 | simpleMock.mock(libImages, 'saveImage').resolveWith(null);
61 | return libImages.fetchAndSave('ftp://www.test.com', null).should.be.rejectedWith(
62 | 'Unsupported Protocol: ftp:');
63 | });
64 |
65 | it('fetchAndSave works with http url', () => {
66 | const headers = {};
67 | simpleMock.mock(headers, 'get').returnWith('image/jpeg');
68 | const response = httpMocks.createResponse();
69 | response.headers = headers;
70 | response.status = 200;
71 | response.body = '';
72 | simpleMock.mock(dataFetcher, 'fetchWithUA').resolveWith(response);
73 | simpleMock.mock(libImages, 'saveImage').resolveWith('http://url.for.newimage.in.bucket.com');
74 | return libImages.fetchAndSave('http://www.test.com', 'destFile').should.be.fulfilled.then(_ => {
75 | assert.equal(libImages.saveImage.callCount, 3);
76 | });
77 | });
78 |
79 | it('fetchAndSave works with https url', () => {
80 | const response = httpMocks.createResponse();
81 | const headers = {};
82 | simpleMock.mock(headers, 'get').returnWith('image/jpeg');
83 | response.headers = headers;
84 | response.status = 200;
85 | response.body = '';
86 | simpleMock.mock(dataFetcher, 'fetchWithUA').resolveWith(response);
87 | simpleMock.mock(libImages, 'saveImage').resolveWith('http://url.for.newimage.in.bucket.com');
88 | return libImages.fetchAndSave('https://www.test.com', 'destFile').should.be.fulfilled.then(_ => {
89 | assert.equal(libImages.saveImage.callCount, 3);
90 | });
91 | });
92 |
93 | it('dataUriAndSave data uri', () => {
94 | const bestIconUrl = manifest.getBestIconUrl();
95 | simpleMock.mock(libImages, 'saveImage').resolveWith('http://url.for.newimage.in.bucket.com');
96 | return libImages.dataUriAndSave(bestIconUrl).should.be.fulfilled.then(_ => {
97 | assert.equal(libImages.saveImage.callCount, 3);
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/models/pwa.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2016, Google, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | 'use strict';
17 |
18 | const uri = require('urijs');
19 | const URL = require('url');
20 | const Manifest = require('../models/manifest');
21 | const User = require('../models/user');
22 |
23 | class Pwa {
24 | constructor(manifestUrl, manifestModel) {
25 | // remove hash from url
26 | manifestUrl && (this.manifestUrl = removeHash(manifestUrl));
27 | this._manifest = stringifyManifestIfNeeded(manifestModel);
28 | this.created = new Date();
29 | this.updated = this.created;
30 | this.visible = true;
31 | }
32 |
33 | get shortName() {
34 | if (!this.manifest) {
35 | return '';
36 | }
37 | return this.manifest.shortName || '';
38 | }
39 |
40 | get name() {
41 | if (!this.manifest) {
42 | return '';
43 | }
44 | return this.manifest.name || '';
45 | }
46 |
47 | get displayName() {
48 | return this.name ||
49 | this.shortName ||
50 | trimManifestFile(this.manifestUrl);
51 | }
52 |
53 | get description() {
54 | if (this.manifest && this.manifest.description) {
55 | return this.manifest.description;
56 | }
57 |
58 | return this.metaDescription || '';
59 | }
60 |
61 | get startUrl() {
62 | if (!this.manifest) {
63 | return '';
64 | }
65 | return this.manifest.startUrl || '';
66 | }
67 |
68 | get absoluteStartUrl() {
69 | if (!this.manifestUrl) {
70 | return '';
71 | }
72 |
73 | const startUrl = this.startUrl || '/';
74 | return this._cleanUrl(uri(startUrl).absoluteTo(this.manifestUrl).toString());
75 | }
76 |
77 | get backgroundColor() {
78 | if (!this.manifest) {
79 | return '#ffffff';
80 | }
81 |
82 | return this.manifest.backgroundColor || '#ffffff';
83 | }
84 |
85 | get manifest() {
86 | if (!this._manifest) {
87 | return null;
88 | }
89 | return new Manifest(this.manifestUrl, JSON.parse(this._manifest));
90 | }
91 |
92 | set manifest(value) {
93 | if (value && typeof value === 'object') {
94 | this._manifest = value.raw;
95 | } else {
96 | this._manifest = value;
97 | }
98 | }
99 |
100 | get manifestAsString() {
101 | return this._manifest;
102 | }
103 |
104 | setUser(user) {
105 | this.user = new User(user);
106 | }
107 |
108 | generateEncodedStartUrl() {
109 | const parsedUrl = URL.parse(this.absoluteStartUrl);
110 | this.encodedStartUrl = encodeURIComponent(parsedUrl.hostname + parsedUrl.pathname);
111 | return this.encodedStartUrl;
112 | }
113 |
114 | isNew() {
115 | return this.created === this.updated;
116 | }
117 |
118 | _cleanUrl(input) {
119 | const url = new URL.URL(input);
120 | for (const name of url.searchParams.keys()) {
121 | if (name.toLowerCase().startsWith('utm_')) {
122 | url.searchParams.delete(name);
123 | }
124 | }
125 | return url.toString();
126 | }
127 | }
128 |
129 | function trimManifestFile(url) {
130 | let startIndex = url.indexOf('//');
131 | if (startIndex === -1) {
132 | startIndex = 0;
133 | } else {
134 | startIndex += 2;
135 | }
136 | let endIndex = url.lastIndexOf('/');
137 | if (endIndex === -1) {
138 | endIndex = url.length;
139 | }
140 | return url.substring(startIndex, endIndex);
141 | }
142 |
143 | function stringifyManifestIfNeeded(manifest) {
144 | if (manifest && typeof manifest === 'object') {
145 | return manifest.raw;
146 | }
147 | return manifest;
148 | }
149 |
150 | function removeHash(urlString) {
151 | const url = URL.parse(urlString);
152 | url.hash = '';
153 | return url.format();
154 | }
155 |
156 | module.exports = Pwa;
157 |
--------------------------------------------------------------------------------