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