├── .gitignore ├── .travis.yml ├── README.md ├── appveyor.yml ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── package.json ├── rollup.config.js ├── src ├── client.js ├── routes │ ├── [list] │ │ ├── [page].html │ │ ├── _ItemSummary.html │ │ └── rss.js │ ├── _components │ │ └── Nav.html │ ├── _error.html │ ├── _layout.html │ ├── about.html │ ├── index.html │ ├── item │ │ ├── [id].html │ │ └── _Comment.html │ ├── rss.js │ └── user │ │ └── [name].html ├── server.js ├── service-worker.js └── template.html ├── static ├── favicon.png ├── manifest.json ├── svelte-192.png ├── svelte-logo-192.png └── svelte-logo-512.png └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /cypress/screenshots 4 | /yarn.lock 5 | /yarn-error.log 6 | /__sapper__ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | env: 6 | global: 7 | - BUILD_TIMEOUT=10000 8 | install: 9 | - npm install 10 | - npm install cypress 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DEPRECATED — see https://github.com/sveltejs/hn.svelte.dev instead 2 | 3 | --- 4 | 5 | # sapper-hacker-news 6 | 7 | Testing ground for [sapper](https://github.com/rich-harris/sapper), a work-in-progress app development framework based on Svelte. 8 | 9 | ```bash 10 | yarn 11 | yarn run dev 12 | ``` 13 | 14 | ...then navigate to [localhost:3000](http://localhost:3000). 15 | 16 | Or to run in production mode, `yarn start`. 17 | 18 | 19 | ## Lots still to do 20 | 21 | Some of these are Svelte things, some of these are Sapper things: 22 | 23 | * [x] Need a declarative way to set the document title on both client and server 24 | * [x] Preloading, on server and client, to avoid the flash of unfetched content 25 | * [x] Critical CSS 26 | * [ ] Sapper doesn't currently watch the `routes` folder, so you have to keep restarting the server. Also, webpack only runs once 27 | * [x] Service worker 28 | * [x] Build optimised production version 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | shallow_clone: true 4 | 5 | init: 6 | - git config --global core.autocrlf false 7 | 8 | build: off 9 | 10 | environment: 11 | matrix: 12 | # node.js 13 | - nodejs_version: stable 14 | 15 | install: 16 | - ps: Install-Product node $env:nodejs_version 17 | - npm install cypress 18 | - npm install 19 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "video": false 4 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | describe('Sapper template app', () => { 2 | beforeEach(() => { 3 | cy.visit('/') 4 | }); 5 | 6 | it('has the correct

', () => { 7 | cy.contains('h1', 'Great success!') 8 | }); 9 | 10 | it('navigates to /about', () => { 11 | cy.get('nav a').contains('about').click(); 12 | cy.url().should('include', '/about'); 13 | }); 14 | 15 | it('navigates to /blog', () => { 16 | cy.get('nav a').contains('blog').click(); 17 | cy.url().should('include', '/blog'); 18 | }); 19 | }); -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sapper-hacker-news", 3 | "description": "Hacker News built with Svelte and Sapper", 4 | "version": "0.0.1", 5 | "author": "Rich Harris", 6 | "scripts": { 7 | "dev": "sapper dev", 8 | "sapper": "sapper build --legacy", 9 | "export": "sapper export", 10 | "start": "node __sapper__/build", 11 | "cy:run": "cypress run", 12 | "cy:open": "cypress open", 13 | "test": "run-p --race dev cy:run", 14 | "stage": "now", 15 | "deploy": "npm run stage && now alias", 16 | "predeploy": "git-branch-is master && git diff --exit-code", 17 | "prestage": "npm run sapper" 18 | }, 19 | "dependencies": { 20 | "@babel/runtime": "^7.0.0", 21 | "compression": "^1.7.3", 22 | "express": "^4.16.3", 23 | "node-fetch": "^2.2.0", 24 | "serve-static": "^1.13.2" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.0.0", 28 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 29 | "@babel/plugin-transform-runtime": "^7.0.0", 30 | "@babel/preset-env": "^7.0.0", 31 | "git-branch-is": "^2.0.0", 32 | "now": "^11.4.1", 33 | "npm-run-all": "^4.1.3", 34 | "rollup": "^0.65.2", 35 | "rollup-plugin-babel": "^4.0.3", 36 | "rollup-plugin-commonjs": "^9.1.6", 37 | "rollup-plugin-node-resolve": "^3.4.0", 38 | "rollup-plugin-replace": "^2.0.0", 39 | "rollup-plugin-svelte": "^4.3.0", 40 | "rollup-plugin-terser": "^2.0.2", 41 | "sapper": "^0.22.2", 42 | "svelte": "^2.13.4" 43 | }, 44 | "now": { 45 | "alias": "hn.svelte.technology", 46 | "files": [ 47 | "static", 48 | "__sapper__/build" 49 | ], 50 | "env": { 51 | "NODE_ENV": "production" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import replace from 'rollup-plugin-replace'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import svelte from 'rollup-plugin-svelte'; 5 | import babel from 'rollup-plugin-babel'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import config from 'sapper/config/rollup.js'; 8 | import pkg from './package.json'; 9 | 10 | const mode = process.env.NODE_ENV; 11 | const dev = mode === 'development'; 12 | const legacy = !!process.env.SAPPER_LEGACY_BUILD; 13 | 14 | export default { 15 | client: { 16 | input: config.client.input(), 17 | output: config.client.output(), 18 | plugins: [ 19 | svelte({ 20 | dev, 21 | hydratable: true, 22 | emitCss: true 23 | }), 24 | resolve(), 25 | replace({ 26 | 'process.browser': true, 27 | 'process.env.NODE_ENV': JSON.stringify(mode) 28 | }), 29 | commonjs(), 30 | 31 | legacy && babel({ 32 | extensions: ['.js', '.html'], 33 | runtimeHelpers: true, 34 | exclude: ['node_modules/@babel/**'], 35 | presets: [ 36 | ['@babel/preset-env', { 37 | targets: '> 0.25%, not dead' 38 | }] 39 | ], 40 | plugins: [ 41 | '@babel/plugin-syntax-dynamic-import', 42 | ['@babel/plugin-transform-runtime', { 43 | useESModules: true 44 | }] 45 | ] 46 | }), 47 | 48 | !dev && terser({ 49 | module: true 50 | }) 51 | ], 52 | 53 | // temporary, pending Rollup 1.0 54 | experimentalCodeSplitting: true 55 | }, 56 | 57 | server: { 58 | input: config.server.input(), 59 | output: config.server.output(), 60 | plugins: [ 61 | svelte({ 62 | generate: 'ssr', 63 | dev 64 | }), 65 | resolve(), 66 | replace({ 67 | 'process.browser': false, 68 | 'process.env.NODE_ENV': JSON.stringify(mode) 69 | }), 70 | commonjs() 71 | ], 72 | external: Object.keys(pkg.dependencies).concat( 73 | require('module').builtinModules || Object.keys(process.binding('natives')) 74 | ), 75 | 76 | // temporary, pending Rollup 1.0 77 | experimentalCodeSplitting: true 78 | }, 79 | 80 | serviceworker: { 81 | input: config.serviceworker.input(), 82 | output: config.serviceworker.output(), 83 | plugins: [ 84 | resolve(), 85 | replace({ 86 | 'process.browser': true, 87 | 'process.env.NODE_ENV': JSON.stringify(mode) 88 | }), 89 | commonjs(), 90 | !dev && terser() 91 | ] 92 | } 93 | }; -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import * as sapper from '../__sapper__/client.js'; 2 | 3 | sapper.start({ 4 | target: document.querySelector('#sapper') 5 | }); -------------------------------------------------------------------------------- /src/routes/[list]/[page].html: -------------------------------------------------------------------------------- 1 | 2 | Svelte Hacker News 3 | 4 | 5 | {#each items as item, i} 6 | {#if item} 7 | 8 | {/if} 9 | {/each} 10 | 11 | {#if next} 12 | More... 13 | {/if} 14 | 15 | -------------------------------------------------------------------------------- /src/routes/[list]/_ItemSummary.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/[list]/rss.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const render = (list, items) => ` 4 | 5 | 6 | Svelte HN (${list}) 7 | https://hn.svelte.technology/${list}/1 8 | Links from the orange site 9 | 10 | https://hn.svelte.technology/favicon.png 11 | Svelte HN (${list}) 12 | https://hn.svelte.technology/${list}/1 13 | 14 | ${items.map(item => ` 15 | 16 | ${item.title}${item.domain ? ` (${item.domain})` : ''} 17 | https://hn.svelte.technology/item/${item.id} 18 | link / ` : '' 20 | }comments 21 | ]]> 22 | ${new Date(item.time * 1000).toUTCString()} 23 | 24 | `).join('\n')} 25 | 26 | `; 27 | 28 | export function get(req, res) { 29 | const list = ( 30 | req.params.list === 'top' ? 'news' : 31 | req.params.list === 'new' ? 'newest' : 32 | req.params.list 33 | ); 34 | 35 | res.set({ 36 | 'Cache-Control': `max-age=0, s-max-age=${600}`, // 10 minutes 37 | 'Content-Type': 'application/rss+xml' 38 | }); 39 | 40 | fetch(`https://api.hnpwa.com/v0/${list}/1.json`) 41 | .then(r => r.json()) 42 | .then(items => { 43 | const feed = render(list, items); 44 | res.end(feed); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/routes/_components/Nav.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/_error.html: -------------------------------------------------------------------------------- 1 | 2 | {status} | Svelte Hacker News 3 | 4 | 5 |

{status}

6 |

{error.message}

-------------------------------------------------------------------------------- /src/routes/_layout.html: -------------------------------------------------------------------------------- 1 |