Full Code of davideast/hnpwa-firebase for AI

master 810a3ba97781 cached
24 files
26.9 KB
8.4k tokens
34 symbols
1 requests
Download .txt
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
================================================
<p align="center">
  <h1 align="center">HNPWA Firebase</h1>
  <p align="center">A Dynamic, CDN Cached, HNPWA implementation on Firebase Dynamic Hosting.</p>
</p>
<p align="center">
  <a align="center" href="https://hnpwa-firebase.firebaseapp.com"><h3 align="center">Demo</h3></a>
</p>
<p align="center">
<a href="https://hnpwa-firebase.firebaseapp.com">
<img width="284" height="524" src="https://raw.githubusercontent.com/davideast/hnpwa-firebase/master/hnpwa-firebase.png">
</a>
</p>

## 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 <your-test-proj>
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
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#ffa100">
    <title>HNPWA Firebase Node.js Hosting</title>

    <!-- ::EMBEDDED-STYLES:: -->
    
  </head>
  <body>
    <div class="hn-lc">

      <nav class="hn-nb">
        <div class="hn-hi">
          <img height="60" width="44" alt="Firebase Logo" src="/images/firebase-logo-64.png">
        </div>

        <ul class="hn-nl">
          <li>
            <a href="/news">NEWS</a>
          </li>
          <li>
            <a href="/ask">ASK</a>
          </li>
          <li>
            <a href="/show">SHOW</a>
          </li>
          <li>
            <a href="/jobs">JOBS</a>
          </li>
        </ul>
      </nav>

      <!-- ::PAGER:: -->

      <section class="hn-sl">

        <!-- ::STORIES:: -->

      </section>

      <section class="hn-i">

        <!-- ::ITEM:: -->

      </section>

    </div>
    <script defer src="/sw.reg.js"></script>
  </body>
</html>


================================================
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<void>;
  generateSW(opts: WorkboxBuildOptions): Promise<void>;
  getFileManifestEntries(opts: WorkboxBuildOptions): Promise<Manifest[]>;
  injectManifest(opts: WorkboxBuildOptions): Promise<void>;
}


================================================
FILE: src/server/embedcss.ts
================================================
import * as utils from './utils';
const csso: { minify(css: string): { css: string } } = require('csso');

const EMBED_REPLACE_TOKEN = /<!-- ::EMBEDDED-STYLES:: -->/;
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 = `<style media="screen">/** ::STYLES:: **/</style>`;
  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('<!-- ::STORIES:: -->', storyHtml);  
  const { next, back, nextPositive, current, maxedOut } = getPagerOptions(topic, page);
  const pageHtml = pagerTemplate({ topic, next, back, nextPositive, current, maxedOut });
  return storiesIndex.replace('<!-- ::PAGER:: -->', 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('<!-- ::ITEM:: -->', 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', '</sw.reg.js>;rel=preload;as=script,</images/firebase-logo-64.png>;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`
<section>
  <div class="hn-sr">
    <h2>{{rank}}</h2>
  </div>
  <div class="hn-sd">
    <h4>
      <a href="{{url}}">
      {{title}}
      {{#if domain}}
      <span>({{domain}})</span>
      {{/if}}
      </a>
    </h4>
    <div class="hn-sm">
      {{#if points}}
      {{points}} points
      {{/if}} 
      {{#if user}} 
      by <a href="/users/{{user}}">{{user}}</a> 
      {{/if}}
      {{time_ago}} |
      <a href="/item/{{id}}">{{comments_count}} comments</a>
    </div>
  </div>
</section>
`;

export const commentTree = html`
<div>
  <h2>{{title}} {{#if domain}}({{domain}}){{/if}}</h2>
  <div>
    <p class="hn-fl">
      {{points}} points by
      <a href="/user/{{user}}">{{user}}</a>
      {{time_ago}} | {{comments_count}} comments
    </p>
  </div>
  <div class="hn-c">
    {{{content}}}
  </div>
  {{> commentList comments }}
</div>
<script>
  const found = document.querySelectorAll('.hn-c .hn-ch');
  for (let i = 0, len = found.length; i < len; i++) {
    found[i].addEventListener('click', function(e) {
      if (e.target.classList.contains('hidden')) {
        e.target.classList.remove('hidden');
      } else {
        e.target.classList.add('hidden');
      }
    });
  }
</script>
`;

export const commentList = html`
  <div class="hn-cl">
    {{#each this}}
      <div class="hn-ct">
        <div class="hn-c">
          <div class="hn-ch">
            <a href="/user/{{user}}">{{ user }}</a>
            {{ time_ago }}
          </div>
          <div class="hn-cb">
            {{{ content }}}

            {{> commentList comments }}
          </div>
        </div>

      </div>
    {{/each}}
  </div>
`;

export const pager = html`
<div class="hn-p">
  {{#if nextPositive}}
  <a href="/{{topic}}?page={{back}}">back</a>
  {{else}}
  <div></div>
  {{/if}}
  <div>{{current}}</div>
  {{#if maxedOut}}
  <a href="/{{topic}}?page={{next}}">next</a>
  {{else}}
  <div></div>
  {{/if}}  
</div>
`;


================================================
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<string> {
  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<any> {
  return new Promise((resolve, reject) => {
    fs.writeFile(path, data, (err: Error) => {
      if(err) { reject(err); return; }
      resolve(data);
    });
  });  
}


================================================
FILE: src/static/404.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Page Not Found</title>

    <style media="screen">
      body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
      #message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
      #message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
      #message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
      #message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
      #message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
      #message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
      #message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
      #load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
      @media (max-width: 600px) {
        body, #message { margin-top: 0; background: white; box-shadow: none; }
        body { border-top: 16px solid #ffa100; }
      }
    </style>
  </head>
  <body>
    <div id="message">
      <h2>404</h2>
      <h1>Page Not Found</h1>
      <p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
      <h3>Why am I seeing this?</h3>
      <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>
    </div>
  </body>
</html>


================================================
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!');
         });
      });
   }
}());
Download .txt
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
Download .txt
SYMBOL INDEX (34 symbols across 7 files)

FILE: src/build/build.ts
  constant PRECACHE_MATCHER (line 10) | const PRECACHE_MATCHER = '[/** ::MANIFEST:: **/]';
  function copyDir (line 14) | function copyDir(source: string, destination: string) {
  function copyStatic (line 28) | async function copyStatic() {
  function copyServer (line 35) | async function copyServer() {
  function copyWorkbox (line 49) | async function copyWorkbox() {
  function generateEntries (line 62) | function generateEntries() {
  function createSW (line 73) | async function createSW(entries: Manifest[]) {
  function createMinifiedSWRegistration (line 83) | async function createMinifiedSWRegistration() {
  function createCompressedIndex (line 93) | async function createCompressedIndex() {
  function generateStyles (line 109) | async function generateStyles() {
  function build (line 137) | async function build() {

FILE: src/build/offline.server.ts
  constant PORT (line 8) | const PORT = process.env['PORT'] || 3004;
  constant API_BASE (line 9) | const API_BASE = process.env['API_BASE'] || 'http://localhost:3002';

FILE: src/build/workbox.types.ts
  type WorkboxBuildOptions (line 1) | interface WorkboxBuildOptions {
  type Manifest (line 11) | interface Manifest {
  type WorkboxBuild (line 16) | interface WorkboxBuild {

FILE: src/server/embedcss.ts
  constant EMBED_REPLACE_TOKEN (line 4) | const EMBED_REPLACE_TOKEN = /<!-- ::EMBEDDED-STYLES:: -->/;
  function combineCss (line 5) | async function combineCss(cssFiles: string[]) {
  function embedInHtml (line 13) | async function embedInHtml(htmlFile: string, styleTagFile: string, token...
  function style (line 20) | function style(styles: string) {

FILE: src/server/index.ts
  constant API_BASE (line 14) | const API_BASE = process.env['API_BASE'] || 'https://api.hnpwa.com/v0';
  constant SECTION_MATCHER (line 15) | const SECTION_MATCHER = /^\/$|news|newest|show|ask|jobs/;
  constant ITEM_MATCHER (line 16) | const ITEM_MATCHER = /item\/(\d+$)/;
  constant MAX_PAGES (line 22) | const MAX_PAGES: { [key: string]: number } = {
  function topicLookup (line 33) | function topicLookup(path: string) {
  function getStories (line 44) | async function getStories(opts: { path: string, topic: string, page: str...
  function getPagerOptions (line 57) | function getPagerOptions(topic: string, page: string) {
  function createStoryPage (line 68) | async function createStoryPage(topic: string, page: string, stories: any...
  function renderStories (line 91) | async function renderStories(path: string, page = "1") {
  function renderItem (line 107) | async function renderItem(id: string) {
  function cacheControl (line 129) | function cacheControl(req: express.Request, res: express.Response, next:...

FILE: src/server/utils.ts
  function readFile (line 3) | function readFile(path: string): Promise<string> {
  function writeFile (line 12) | function writeFile(path: string, data: string): Promise<any> {

FILE: src/static/sw.reg.js
  function auf (line 3) | function auf(registration, versionChangeCallback, offlineReadyCallback) {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (30K chars).
[
  {
    "path": ".firebaserc",
    "chars": 97,
    "preview": "{\n  \"projects\": {\n    \"default\": \"hnpwa-firebase\",\n    \"staging\": \"hnpwa-firebase-staging\"\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 62,
    "preview": "node_modules\n.DS_STORE\nnpm-debug.log\nfirebase-debug.log\ndist\n\n"
  },
  {
    "path": "README.md",
    "chars": 1221,
    "preview": "<p align=\"center\">\n  <h1 align=\"center\">HNPWA Firebase</h1>\n  <p align=\"center\">A Dynamic, CDN Cached, HNPWA implementat"
  },
  {
    "path": "firebase.json",
    "chars": 765,
    "preview": "{\n  \"functions\": {\n    \"source\": \"dist/server\"\n  },\n  \"hosting\": {\n    \"public\": \"dist/static\",\n    \"rewrites\": [\n      "
  },
  {
    "path": "package.json",
    "chars": 2003,
    "preview": "{\n  \"name\": \"hnpwa-firebase-hosting\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"dependencies\":"
  },
  {
    "path": "src/build/build.ts",
    "chars": 4685,
    "preview": "import { WorkboxBuild, Manifest } from './workbox.types';\nimport * as embed from '../server/embedcss';\nimport * as uglif"
  },
  {
    "path": "src/build/css/base.css",
    "chars": 1069,
    "preview": "/* Base */      \n* { box-sizing: border-box; }\np,h1,h2,h3,h4,h5 { padding: 0; margin: 0; }\n\na, a:visited, a:hover {\n    "
  },
  {
    "path": "src/build/css/item.css",
    "chars": 448,
    "preview": ".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: 1"
  },
  {
    "path": "src/build/css/stories.css",
    "chars": 751,
    "preview": ".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 "
  },
  {
    "path": "src/build/deploy_staging.sh",
    "chars": 270,
    "preview": "node_modules/.bin/firebase deploy --non-interactive --token \"$FIREBASE_TOKEN\" --project hnpwa-firebase-staging\ncurl http"
  },
  {
    "path": "src/build/index.html",
    "chars": 1114,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-wid"
  },
  {
    "path": "src/build/offline.server.ts",
    "chars": 374,
    "preview": "import * as express from 'express';\nimport { app as router } from '../server';\n\nconst app = express();\napp.use(router);\n"
  },
  {
    "path": "src/build/sw.main.js",
    "chars": 417,
    "preview": "importScripts('/workbox-sw.prod.js');\nvar w = new self.WorkboxSW();\nself.addEventListener('install', event => event.wait"
  },
  {
    "path": "src/build/tsconfig.json",
    "chars": 629,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",                          /* Specify ECMAScript target version: 'ES3' (defa"
  },
  {
    "path": "src/build/workbox.types.ts",
    "chars": 583,
    "preview": "export interface WorkboxBuildOptions {\n  globDirectory: string;\n  globPatterns: string[];\n  globIgnores?: string[];\n  sw"
  },
  {
    "path": "src/server/embedcss.ts",
    "chars": 956,
    "preview": "import * as utils from './utils';\nconst csso: { minify(css: string): { css: string } } = require('csso');\n\nconst EMBED_R"
  },
  {
    "path": "src/server/index.ts",
    "chars": 5073,
    "preview": "import * as functions from 'firebase-functions';\nimport * as express from 'express';\nimport * as fs from 'fs';\nimport * "
  },
  {
    "path": "src/server/package.json",
    "chars": 528,
    "preview": "{\n  \"name\": \"hnpwa_firebase\",\n  \"description\": \"HNPWA app on Firebase node.js hosting\",\n  \"dependencies\": {\n    \"@types/"
  },
  {
    "path": "src/server/templates.ts",
    "chars": 2026,
    "preview": "// This is for syntax highlighting in VSCode\nconst html = String.raw;\n\nexport const story = html`\n<section>\n  <div class"
  },
  {
    "path": "src/server/tsconfig.json",
    "chars": 135,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"module\": \"commonjs\",\n    \"strict\": true,\n    \"outDir\": \"../../dist"
  },
  {
    "path": "src/server/utils.ts",
    "chars": 496,
    "preview": "import * as fs from 'fs';\n\nexport function readFile(path: string): Promise<string> {\n  return new Promise((resolve, reje"
  },
  {
    "path": "src/static/404.html",
    "chars": 1808,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initia"
  },
  {
    "path": "src/static/manifest.json",
    "chars": 1091,
    "preview": "{\n  \"name\": \"HNPWA\",\n  \"short_name\": \"HNPWA\",\n  \"theme_color\": \"#ffa100\",\n  \"background_color\": \"#ffffff\",\n  \"display\": "
  },
  {
    "path": "src/static/sw.reg.js",
    "chars": 894,
    "preview": "(function () {\n   if ('serviceWorker' in navigator) {\n      function auf(registration, versionChangeCallback, offlineRea"
  }
]

About this extraction

This page contains the full source code of the davideast/hnpwa-firebase GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (26.9 KB), approximately 8.4k tokens, and a symbol index with 34 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!