Repository: davideast/hnpwa-firebase
Branch: master
Commit: 810a3ba97781
Files: 24
Total size: 26.9 KB
Directory structure:
gitextract_b36yke40/
├── .firebaserc
├── .gitignore
├── README.md
├── firebase.json
├── package.json
└── src/
├── build/
│ ├── build.ts
│ ├── css/
│ │ ├── base.css
│ │ ├── item.css
│ │ └── stories.css
│ ├── deploy_staging.sh
│ ├── index.html
│ ├── offline.server.ts
│ ├── sw.main.js
│ ├── tsconfig.json
│ └── workbox.types.ts
├── server/
│ ├── embedcss.ts
│ ├── index.ts
│ ├── package.json
│ ├── templates.ts
│ ├── tsconfig.json
│ └── utils.ts
└── static/
├── 404.html
├── manifest.json
└── sw.reg.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .firebaserc
================================================
{
"projects": {
"default": "hnpwa-firebase",
"staging": "hnpwa-firebase-staging"
}
}
================================================
FILE: .gitignore
================================================
node_modules
.DS_STORE
npm-debug.log
firebase-debug.log
dist
================================================
FILE: README.md
================================================
HNPWA Firebase
A Dynamic, CDN Cached, HNPWA implementation on Firebase Dynamic Hosting.
Demo
## Highlights
- **Emerging Markets** - 1.3
- **3G** - 0.7
- **CDN Cached** - Every file on Firebase Hosting is served through a CDN
- **Dynamic** - Cloud Functions + Firebase Hosting = Dynamic CDN population
## Contribute!?
```bash
git clone https://github.com/davideast/hnpwa-firebase.git
npm i
npm run build
# Basic serving
npm run serve
# Firebase Hosting Emulation
node_modules/.bin/firebase login
# Use your own project
node_modules/.bin/firebase use -add
npm run serve:firebase
# Offline serving A.K.A - Deving on an Airplane/bus/elevator
npm run save:offline # save current HNAPI data set offline
# go offline
npm run serve:offline:api
# open new terminal or tmux or something
npm run serve:offline
```
================================================
FILE: firebase.json
================================================
{
"functions": {
"source": "dist/server"
},
"hosting": {
"public": "dist/static",
"rewrites": [
{
"source": "/@(news|newest|show|ask|jobs|item)",
"function": "server"
},
{
"source": "/",
"function": "server"
},
{
"source": "/item/*",
"function": "server"
}
],
"headers": [
{
"source": "/sw.main.js",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
}
]
},
{
"source": "/images/firebase-logo-64.png",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000"
}
]
}
]
}
}
================================================
FILE: package.json
================================================
{
"name": "hnpwa-firebase-hosting",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@types/handlebars": "^4.0.33",
"@types/request-promise": "^4.1.36",
"csso": "^3.1.1",
"firebase": "^4.1.4",
"firebase-admin": "^4.2.1",
"firebase-functions": "^0.5.9",
"fs-extra": "^4.0.0",
"handlebars": "^4.0.10",
"html-minifier": "^3.5.2",
"ncp": "^2.0.0",
"request-promise": "^4.2.1",
"typescript": "^2.4.1",
"uglify-js": "^3.0.24",
"workbox-build": "^1.0.1"
},
"devDependencies": {
"@types/fs-extra": "^3.0.3",
"@types/html-minifier": "^1.1.30",
"@types/ncp": "^2.0.0",
"@types/node": "^8.0.9",
"@types/uglify-js": "^2.6.29",
"firebase-tools": "^3.9.1",
"hnpwa-api": "^0.1.5",
"lighthouse": "^2.1.0",
"lighthouse-ci": "https://github.com/ebidel/lighthouse-ci"
},
"scripts": {
"test": "echo \"lol tests\" && exit 0",
"tsc": "node_modules/.bin/tsc -p src/server/tsconfig.json && node_modules/.bin/tsc -p src/build/tsconfig.json",
"tsc:watch": "node_modules/.bin/tsc -p src/server/tsconfig.json -w",
"build:clean": "rm -rf dist",
"build": "npm run build:clean && npm run tsc && node dist/build/build.js",
"serve": "npm run tsc && API_BASE=https://hnpwa.com/api/v0 node dist/build/offline.server",
"serve:firebase": "npm run tsc && firebase serve --only functions,hosting",
"save:offline": "node_modules/.bin/hnpwa-api --save",
"serve:offline:api": "node_modules/.bin/hnpwa-api --serve --offline",
"serve:offline": "npm run tsc && API_BASE=http://localhost:3002 node dist/build/offline.server",
"fns:deps": "cd dist/server && npm i",
"deploy": "cd dist/server && npm i && firebase deploy --token \"$FIREBASE_TOKEN\" --project hnpwa-firebase",
"deploy:staging": "cd dist/server && npm i && firebase deploy --token \"$FIREBASE_TOKEN\" --project hnpwa-firebase-staging"
},
"keywords": [],
"author": "",
"license": "ISC"
}
================================================
FILE: src/build/build.ts
================================================
import { WorkboxBuild, Manifest } from './workbox.types';
import * as embed from '../server/embedcss';
import * as uglify from 'uglify-js';
import * as htmlmin from 'html-minifier';
import * as fs from 'fs-extra';
import * as ncpi from 'ncp';
const workbox: WorkboxBuild = require('workbox-build');
const PRECACHE_MATCHER = '[/** ::MANIFEST:: **/]';
const minify = htmlmin.minify;
const ncp = ncpi.ncp;
function copyDir(source: string, destination: string) {
return new Promise((resolve, reject) => {
ncp(source, destination, function ncpCopy(err) {
if (err) {
reject(err);
}
resolve();
});
});
}
/**
* Copy static assets into the directory to deploy to Firebase Hosting
*/
async function copyStatic() {
const staticDir = process.cwd() + '/src/static';
const distDir = process.cwd() + '/dist/static';
await copyDir(staticDir, distDir)
console.log(`Copying ${staticDir} to ${distDir}`);
}
async function copyServer() {
const cwd = process.cwd();
return [{
data: fs.readFileSync(`${cwd}/src/server/package.json`, 'utf8'),
path: process.cwd() + '/dist/server/package.json',
}, {
data: fs.readFileSync(`${cwd}/src/server/package-lock.json`, 'utf8'),
path: process.cwd() + '/dist/server/package-lock.json',
}];
}
/**
* Copy workbox from npm
*/
async function copyWorkbox() {
const cwd = process.cwd();
const pkgPath = `${cwd}/node_modules/workbox-sw/package.json`;
const pkg = require(pkgPath);
const readPath = `${cwd}/node_modules/workbox-sw/${pkg.main}`;
const data = fs.readFileSync(readPath, 'utf8');
const path = `${cwd}/dist/static/workbox-sw.prod.js`;
return [{ data, path }];
}
/**
* Generate precache entries for the ServiceWorker
*/
function generateEntries() {
return workbox.getFileManifestEntries({
globDirectory: './src/static',
globPatterns: ['**\/*.{html,js,css,png,jpg,json}'],
globIgnores: ['sw.main.js','404.html', 'images/icons/**/*', 'index.html'],
});
}
/**
* Generate top level Service Worker given precache entries
*/
async function createSW(entries: Manifest[]) {
const swTemplate = fs.readFileSync(process.cwd() + '/src/build/sw.main.js', 'utf8');
const data = swTemplate.replace(PRECACHE_MATCHER, JSON.stringify(entries));
const path = process.cwd() + '/dist/static/sw.main.js';
return [{ data, path }];
}
/**
* Minify the SW registration code
*/
async function createMinifiedSWRegistration() {
const swregTemplate = fs.readFileSync(process.cwd() + '/src/static/sw.reg.js', 'utf8');
const data = uglify.minify(swregTemplate).code;
const path = process.cwd() + '/dist/static/sw.reg.js';
return [{ data, path }];
}
/**
* Compress the index.html template
*/
async function createCompressedIndex() {
const indexFile = fs.readFileSync(process.cwd() + '/src/build/index.html', 'utf8');
const data = minify(indexFile, {
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
const path = process.cwd() + '/dist/server/index.html';
return [{data, path }];
}
/**
* Create the style tags for the "story" and "item" based pages. This styles
* are generated statically once, and then dynamically plugged when a request
* hits.
*/
async function generateStyles() {
const storyCss = await embed.combineCss([
process.cwd() + '/src/build/css/base.css',
process.cwd() + '/src/build/css/stories.css'
]);
const itemCss = await embed.combineCss([
process.cwd() + '/src/build/css/base.css',
process.cwd() + '/src/build/css/item.css'
]);
const storiesStyleTag = embed.style(storyCss);
const itemStyleTag = embed.style(itemCss);
return [
{ path: process.cwd() + '/dist/server/stories.css.html', data: storiesStyleTag },
{ path: process.cwd() + '/dist/server/item.css.html', data: itemStyleTag },
];
}
/**
* Build Steps
* (assume tsc has ran)
* - Copy assets from npm
* - Generate SW entries
* - Generate SW from entries
* - Minify SW
* - Minify SW registration
* - Minify index.html
* - Generate CSS HTML tags, write to server/css
*/
async function build() {
await copyStatic();
const entries = await generateEntries();
const sw = await createSW(entries);
const reg = await createMinifiedSWRegistration();
const index = await createCompressedIndex();
const css = await generateStyles();
const workbox = await copyWorkbox();
const server = await copyServer();
const all = sw.concat(reg, index, css, workbox, server);
return all.map(file => {
console.log(`Writing ${file.path}.`);
return fs.writeFileSync(file.path, file.data, 'utf8');
});
}
try {
build();
} catch(e) {
console.log(e);
}
================================================
FILE: src/build/css/base.css
================================================
/* Base */
* { box-sizing: border-box; }
p,h1,h2,h3,h4,h5 { padding: 0; margin: 0; }
a, a:visited, a:hover {
color: #0288d1;
}
body {
border-top: 16px solid #ffa100;
color: rgba(0,0,0,0.87);
font-family: Roboto, Helvetica, Arial, sans-serif;
font-size: calc(15px + 7 * ((100vw - 500px) / 900));
margin: 0;
padding: 0;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
/* Layout */
.hn-lc {
margin: 0 auto;
min-width: 320px;
padding: 0 2rem;
}
/* Header module */
.hn-nb {
display: flex;
justify-content: space-between;
padding: 20px 0px;
}
.hn-hi {
padding-right: 30px;
}
.hn-nl {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.hn-nl li a {
color: rgba(0, 0, 0, 0.63);
font-size: 1.2rem;
text-decoration: none;
}
.hn-nl li a:hover {
text-decoration: underline;
}
@media(max-width: 380px) {
.hn-nl li a {
font-size: 1rem;
}
}
@media(min-width: 1000px) {
.hn-lc {
padding: 0 6rem;
}
}
================================================
FILE: src/build/css/item.css
================================================
.hn-i h2 {
padding: 1rem 0;
}
.hn-fl {
font-weight: lighter;
}
.hn-c {
padding: 1rem 0 0;
font-size: 1rem;
}
.hn-cl .hn-cl {
margin-left: .5rem
}
.hn-c p,
.hn-ch {
margin-bottom: .8rem;
word-break: break-word;
}
.hn-ct {
padding-left: .5rem
}
.hn-c .hn-ch::before {
content: '[-] ';
cursor: pointer;
}
.hn-c .hidden.hn-ch::before {
content: '[+] ';
}
.hn-c .hidden.hn-ch ~ .hn-cb {
display:none;
}
================================================
FILE: src/build/css/stories.css
================================================
.hn-sl > section {
border-bottom: 1px solid rgba(239, 239, 239, .8);
display: flex;
padding: 1rem 0;
}
.hn-sr {
padding: 15px;
text-align: right;
width: 85px;
}
.hn-sr h2 {
color: rgba(0, 0, 0, 0.63);
}
.hn-sd {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
.hn-sd h4 {
font-weight: 400;
padding: 4px 0;
}
.hn-sd h4 a {
color: rgba(0, 0, 0, 0.87);
text-decoration: none;
}
.hn-sm {
font-size: 0.7rem;
}
.hn-p {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.1rem;
position: -webkit-sticky;
position: sticky;
top: 0;
background-color: rgb(255, 255, 255);
}
================================================
FILE: src/build/deploy_staging.sh
================================================
node_modules/.bin/firebase deploy --non-interactive --token "$FIREBASE_TOKEN" --project hnpwa-firebase-staging
curl https://hnpwa-firebase-staging.firebaseapp.com
node node_modules/lighthouse-ci/runlighthouse.js --score=97 https://hnpwa-firebase-staging.firebaseapp.com
================================================
FILE: src/build/index.html
================================================
HNPWA Firebase Node.js Hosting
================================================
FILE: src/build/offline.server.ts
================================================
import * as express from 'express';
import { app as router } from '../server';
const app = express();
app.use(router);
app.use(express.static(process.cwd() + '/dist/static'));
const PORT = process.env['PORT'] || 3004;
const API_BASE = process.env['API_BASE'] || 'http://localhost:3002';
app.listen(PORT, () => console.log(`Listening on ${PORT}. Using API: ${API_BASE}`));
================================================
FILE: src/build/sw.main.js
================================================
importScripts('/workbox-sw.prod.js');
var w = new self.WorkboxSW();
self.addEventListener('install', event => event.waitUntil(self.skipWaiting()));
self.addEventListener('activate', event => event.waitUntil(self.clients.claim()));
w.precache([/** ::MANIFEST:: **/]);
w.router.registerRoute('/', w.strategies.networkFirst());
w.router.registerRoute(/^\/$|news|newest|show|ask|jobs|item/, w.strategies.networkFirst());
================================================
FILE: src/build/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */
"strict": true, /* Enable all strict type-checking options. */
"typeRoots": [
"../../node_modules/@types"
],
"lib": [
"es2015"
],
"moduleResolution": "node",
"outDir": "../../dist"
},
"files": [
"build.ts",
"offline.server.ts"
]
}
================================================
FILE: src/build/workbox.types.ts
================================================
export interface WorkboxBuildOptions {
globDirectory: string;
globPatterns: string[];
globIgnores?: string[];
swDest?: string;
templatedUrls?: { [key: string]: string[] };
mainfestDest?: string;
swSrc?: string;
}
export interface Manifest {
revision: string;
ur: string;
}
export interface WorkboxBuild {
generateFileManifest(opts: WorkboxBuildOptions): Promise;
generateSW(opts: WorkboxBuildOptions): Promise;
getFileManifestEntries(opts: WorkboxBuildOptions): Promise;
injectManifest(opts: WorkboxBuildOptions): Promise;
}
================================================
FILE: src/server/embedcss.ts
================================================
import * as utils from './utils';
const csso: { minify(css: string): { css: string } } = require('csso');
const EMBED_REPLACE_TOKEN = //;
export async function combineCss(cssFiles: string[]) {
const cssPromises = await cssFiles.map((path: string) => utils.readFile(path));
const cssContent = await Promise.all(cssPromises);
const cssCombined = cssContent.join('\n');
const compressedCss = csso.minify(cssCombined).css;
return compressedCss;
}
export async function embedInHtml(htmlFile: string, styleTagFile: string, token = EMBED_REPLACE_TOKEN) {
const tag = await utils.readFile(styleTagFile);
const html = await utils.readFile(htmlFile);
const replaced = html.replace(EMBED_REPLACE_TOKEN, tag);
return replaced;
}
export function style(styles: string) {
const tag = ``;
const replaced = tag.replace('/** ::STYLES:: **/', styles);
return replaced;
}
================================================
FILE: src/server/index.ts
================================================
import * as functions from 'firebase-functions';
import * as express from 'express';
import * as fs from 'fs';
import * as request from 'request-promise';
import * as templates from './templates';
import * as Handlebars from 'handlebars';
import * as embedcss from './embedcss';
import * as htmlmin from 'html-minifier';
const minify = htmlmin.minify;
export const app = express.Router();
Handlebars.registerPartial('commentList', templates.commentList);
const API_BASE = process.env['API_BASE'] || 'https://api.hnpwa.com/v0';
const SECTION_MATCHER = /^\/$|news|newest|show|ask|jobs/;
const ITEM_MATCHER = /item\/(\d+$)/;
/**
* Map the max amount of pages per route, this makes a look up
* super easy when rendering the template.
*/
const MAX_PAGES: { [key: string]: number } = {
"news": 10,
"jobs": 1,
"ask": 3,
"show": 2,
"/": 10
};
/**
* Looks at a string path and returns the matching result.
*/
function topicLookup(path: string) {
if (path === '/') {
return 'news';
}
return `${path.match(SECTION_MATCHER)![0]}`
}
/**
* Get stories from the API based on the required topic and page number
* @param opts
*/
async function getStories(opts: { path: string, topic: string, page: string}) {
const { path, topic, page } = opts;
// get story data
let storiesJson;
// No page lookup is required if it is the root page
if(path === '/') {
storiesJson = await request(`${API_BASE}/news/1.json`);
} else {
storiesJson = await request(`${API_BASE}/${topic}/${page}.json`);
}
return JSON.parse(storiesJson);
}
function getPagerOptions(topic: string, page: string) {
const pageInt = parseInt(page, 10);
const back = pageInt - 1;
const next = pageInt + 1
const nextPositive = back > 0;
const max = MAX_PAGES[topic];
const current = `${page}/${max}`;
const maxedOut = pageInt < MAX_PAGES[topic];
return { back, next, nextPositive, max, current, maxedOut };
}
async function createStoryPage(topic: string, page: string, stories: any[]) {
// compile html from template
const template = Handlebars.compile(templates.story);
const pagerTemplate = Handlebars.compile(templates.pager);
const storyHtml = stories.map((story: any, i: number) => {
// handle story rank in template
return template({ rank: i + 1, ...story });
}).join('');
// Embed CSS in HTML template
const styledIndex = await embedcss.embedInHtml(
__dirname + '/index.html',
__dirname + '/stories.css.html'
);
// Dynamically render the stories in the HTML template
const storiesIndex = styledIndex.replace('', storyHtml);
const { next, back, nextPositive, current, maxedOut } = getPagerOptions(topic, page);
const pageHtml = pagerTemplate({ topic, next, back, nextPositive, current, maxedOut });
return storiesIndex.replace('', pageHtml);
}
/**
* Create an entire section based on it's topic name
*/
async function renderStories(path: string, page = "1") {
const topic = topicLookup(path);
const stories = await getStories({ path, topic, page });
const allIndex = await createStoryPage(topic, page, stories);
// minify html
return minify(allIndex, {
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
}
/**
* Create a single item based on it's id
*/
async function renderItem(id: string) {
const itemJson = await request(`${API_BASE}/item/${id}.json`)
const item = JSON.parse(itemJson);
const template = Handlebars.compile(templates.commentTree);
const html = template(item);
// Embed CSS in HTML template
const styledIndex = await embedcss.embedInHtml(
__dirname + '/index.html',
__dirname + '/item.css.html'
);
const itemIndex = styledIndex.replace('', html);
return minify(itemIndex, {
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
}
/**
* Set the Cache-Control header as middleware so we don't have to set it for each and
* every route.
*/
function cacheControl(req: express.Request, res: express.Response, next: Function) {
res.set('Cache-Control', 'public; max-age=300, s-maxage=600, stale-while-revalidate=400');
res.set('Link', ';rel=preload;as=script,;rel=preload;as=image');
next();
}
app.use(cacheControl);
/**
* Handle main routes like 'news', 'ask', 'show', 'jobs', etc...
*/
app.get(SECTION_MATCHER, async (req, res) => {
let page = req.query.page;
if(!page) {
page = "1";
}
const storiesHtml = await renderStories(req.path, page);
res.send(storiesHtml);
});
/**
* Handle id query param (/item?id=1)
*/
app.get('/item', async(req, res) => {
let id = req.query.id;
const itemHtml = await renderItem(id);
res.send(itemHtml);
});
/**
* Handle clean routes (/item/1)
*/
app.get(ITEM_MATCHER, async (req, res) => {
const id = req.path.replace('/item/', '');
const itemHtml = await renderItem(id);
res.send(itemHtml);
});
/**
* Export express app to Cloud Functions
*/
export let server = functions.https.onRequest(app as any);
================================================
FILE: src/server/package.json
================================================
{
"name": "hnpwa_firebase",
"description": "HNPWA app on Firebase node.js hosting",
"dependencies": {
"@types/handlebars": "^4.0.33",
"@types/request-promise": "^4.1.35",
"csso": "^3.1.1",
"express": "^4.15.3",
"firebase-admin": "~4.2.1",
"firebase-functions": "^0.5.7",
"fs-extra": "^4.0.0",
"handlebars": "^4.0.10",
"html-minifier": "^3.5.2",
"request": "^2.81.0",
"request-promise": "^4.2.1"
},
"devDependencies": {
"workbox-build": "^1.0.1"
},
"private": true
}
================================================
FILE: src/server/templates.ts
================================================
// This is for syntax highlighting in VSCode
const html = String.raw;
export const story = html`
`;
export const commentTree = html`
{{title}} {{#if domain}}({{domain}}){{/if}}
{{points}} points by
{{user}}
{{time_ago}} | {{comments_count}} comments
{{{content}}}
{{> commentList comments }}
`;
export const commentList = html`
{{#each this}}
{{{ content }}}
{{> commentList comments }}
{{/each}}
`;
export const pager = html`
{{#if nextPositive}}
back
{{else}}
{{/if}}
{{current}}
{{#if maxedOut}}
next
{{else}}
{{/if}}
`;
================================================
FILE: src/server/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"strict": true,
"outDir": "../../dist/server"
}
}
================================================
FILE: src/server/utils.ts
================================================
import * as fs from 'fs';
export function readFile(path: string): Promise {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err: Error, data: any) => {
if(err) { reject(err); }
resolve(data);
});
});
}
export function writeFile(path: string, data: string): Promise {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, (err: Error) => {
if(err) { reject(err); return; }
resolve(data);
});
});
}
================================================
FILE: src/static/404.html
================================================
Page Not Found
404
Page Not Found
The specified file was not found on this website. Please check the URL for mistakes and try again.
Why am I seeing this?
This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.
================================================
FILE: src/static/manifest.json
================================================
{
"name": "HNPWA",
"short_name": "HNPWA",
"theme_color": "#ffa100",
"background_color": "#ffffff",
"display": "standalone",
"Scope": "/",
"start_url": "/",
"icons": [
{
"src": "images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}
================================================
FILE: src/static/sw.reg.js
================================================
(function () {
if ('serviceWorker' in navigator) {
function auf(registration, versionChangeCallback, offlineReadyCallback) {
registration.onupdatefound = function () {
var installingWorker = registration.installing;
installingWorker.onstatechange = function () {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
versionChangeCallback();
} else {
offlineReadyCallback();
}
}
};
};
}
navigator.serviceWorker.register('/sw.main.js').then(function (reg) {
auf(reg, function versionChanged() {
console.log('new version!');
}, function offlineReady() {
console.log('offline ready!');
});
});
}
}());