├── src ├── library │ └── utils.js ├── client.js ├── routes │ ├── index.svelte │ ├── _layout.svelte │ └── _error.svelte ├── components │ └── Button.svelte ├── server.js ├── template.html └── service-worker.js ├── static ├── robots.txt ├── favicons │ ├── favicon.png │ ├── logo-192.png │ ├── logo-512.png │ ├── browserconfig.xml │ └── safari-pinned-tab.svg ├── global.scss ├── site.webmanifest └── global.css ├── .gitignore ├── package.json ├── README.md └── rollup.config.js /src/library/utils.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /static/favicons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattpilott/sapper-start/HEAD/static/favicons/favicon.png -------------------------------------------------------------------------------- /static/favicons/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattpilott/sapper-start/HEAD/static/favicons/logo-192.png -------------------------------------------------------------------------------- /static/favicons/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattpilott/sapper-start/HEAD/static/favicons/logo-512.png -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import '../static/global.css'; 2 | import { start } from '@sapper/app'; 3 | 4 | start({ target: sapper }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | yarn-error.log 4 | /__sapper__/ 5 | pnpm-lock.yaml 6 | pnpm-debug.log 7 | .env 8 | .vscode 9 | .nova -------------------------------------------------------------------------------- /src/routes/index.svelte: -------------------------------------------------------------------------------- 1 |

Sapper launched successfully! 🚀

2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | {#if href} 2 | 3 | 4 | 5 | {:else} 6 | 9 | {/if} 10 | 11 | -------------------------------------------------------------------------------- /src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/global.scss: -------------------------------------------------------------------------------- 1 | @import '../node_modules/@neuekit/reboot'; 2 | 3 | html { 4 | font-family: -apple-system, 5 | BlinkMacSystemFont, 6 | Segoe UI, 7 | Helvetica, 8 | Arial, 9 | sans-serif, 10 | Apple Color Emoji, 11 | Segoe UI Emoji; 12 | -webkit-font-smoothing: antialiased; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | } -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import sirv from 'sirv'; 2 | import polka from 'polka'; 3 | import compression from 'compression'; 4 | import * as sapper from '@sapper/server'; 5 | import { sanslash } from '@neuekit/utilities/src/middleware'; 6 | import 'dotenv/config'; 7 | 8 | const { PORT, NODE_ENV } = process.env; 9 | const dev = NODE_ENV === 'development'; 10 | 11 | polka() // You can also use Express 12 | .use(sanslash) 13 | .use(compression({ threshold: 0 })) 14 | .use(sirv('static', { dev })) 15 | .use(sapper.middleware()) 16 | .listen(PORT, err => err && console.log('error', err)); -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#ffffff", 3 | "theme_color": "#333333", 4 | "name": "TODO", 5 | "short_name": "TODO", 6 | "display": "standalone", 7 | "start_url": "/", 8 | "icons": [ 9 | { 10 | "src": "/favicons/logo-192.png", 11 | "sizes": "192x192", 12 | "type": "image/png", 13 | "purpose": "any maskable" 14 | }, 15 | { 16 | "src": "/favicons/logo-512.png", 17 | "sizes": "512x512", 18 | "type": "image/png", 19 | "purpose": "any maskable" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/routes/_error.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | 31 | {status} 32 | 33 | 34 |

{status}

35 | 36 |

{error.message}

37 | 38 | {#if dev && error.stack} 39 |
{error.stack}
40 | {/if} -------------------------------------------------------------------------------- /src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | %sapper.base% 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | %sapper.scripts% 21 | 22 | 25 | %sapper.styles% 26 | 27 | 29 | %sapper.head% 30 | 31 | 32 | 34 |
%sapper.html%
35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TODO", 3 | "description": "TODO", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "sapper": "sapper dev", 7 | "build": "sapper build --legacy", 8 | "export": "sapper export --legacy", 9 | "start": "node __sapper__/build", 10 | "reload": "touch src/routes/_layout.svelte", 11 | "scss": "sass --no-source-map -s compressed ./static/", 12 | "autoprefixer": "postcss -u autoprefixer -r ./static/global.css --no-map", 13 | "build:css": "run-s 'scss' 'autoprefixer' 'reload'", 14 | "watch:css": "onchange './static/**/*.scss' -- npm run build:css", 15 | "dev": "run-p 'sapper' 'watch:css'" 16 | }, 17 | "dependencies": { 18 | "compression": "1.7.4", 19 | "polka": "0.5.2", 20 | "sirv": "1.0.12" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.14.3", 24 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 25 | "@babel/plugin-transform-runtime": "7.14.3", 26 | "@babel/preset-env": "^7.14.4", 27 | "@babel/runtime": "^7.14.0", 28 | "@neuekit/reboot": "1.10.2", 29 | "@neuekit/utilities": "1.8.0", 30 | "@rollup/plugin-alias": "3.1.2", 31 | "@rollup/plugin-babel": "^5.3.0", 32 | "@rollup/plugin-commonjs": "19.0.0", 33 | "@rollup/plugin-node-resolve": "^13.0.0", 34 | "@rollup/plugin-replace": "^2.4.2", 35 | "@rollup/plugin-url": "^6.0.0", 36 | "autoprefixer": "^10.2.6", 37 | "dotenv": "^10.0.0", 38 | "npm-run-all": "4.1.5", 39 | "onchange": "7.1.0", 40 | "postcss": "8.3.0", 41 | "postcss-cli": "8.3.1", 42 | "rollup": "^2.50.4", 43 | "rollup-plugin-svelte": "7.1.0", 44 | "rollup-plugin-terser": "7.0.2", 45 | "sapper": "0.29.1", 46 | "sass": "^1.34.0", 47 | "svelte": "^3.38.2", 48 | "svelte-preprocess": "^4.7.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Hero](https://i.postimg.cc/fLPyrJ3L/hero.png) 2 | 3 |

4 | An opinionated starter template based on the Rollup Sapper template. 🚀 5 |

6 | 7 | ## Hello 👋, 8 | 9 | ![GitHub release](https://img.shields.io/github/release/matt3224/sapper-start.svg?style=for-the-badge) 10 | 11 | The purpose of this repo was to save me time by doing all of the things I do on every Sapper project, but hey you might find it useful too right?! 12 | 13 | To clone it and get started: 14 | 15 | ```bash 16 | # for Rollup 17 | npx degit matt3224/sapper-start my-app 18 | cd my-app 19 | npm install 20 | npm run dev 21 | ``` 22 | 23 | Open up [localhost:3000](http://localhost:3000) and start clicking around. 24 | 25 | Consult [sapper.svelte.dev](https://sapper.svelte.dev) for help getting started with Sapper itself. 26 | 27 |
28 | 29 | If you use this a lot you can alias it by putting this in your zshrc or bashrc: 30 | ```bash 31 | alias sapper-start='npx degit https://github.com/matt3224/sapper-start .' 32 | ``` 33 | Then simply cd into your empty project directory and run `sapper-start` 34 | 35 | 36 | ## How does this differ from sapper-template? 37 | 38 | Good question, I found myself doing these changes on every project and thought I'd save myself and you some time by making a little repo you could import just like the official sapper-template. 39 | 40 | Here are the differences: 41 | * Adds SCSS support out-of-the-box 42 | * Adds css reset based on normalize 43 | * Adds `numworker: 1` in rollup.config to prevent crash on servers with a single processor core 44 | * Adds a light suggested structure for library and component folders 45 | * Adds an optional utility package (Tree-shaken so if you don't use it you don't get it) 46 | * Adds the package version in the html head as a meta tag 47 | * Adds PWA support by default 48 | * Adds standard allow all robots.txt 49 | * Adds server.js middleware to redirect trailing slash urls to non trailing slash 50 | * Adds ability to bypass relative paths with `~` & auto resolves .js, .mjs, .html, .svelte, .scss 51 | * eg. `import { util } from '~/utils';` instead of `import { util } from '../../utils.js';` 52 | * Changes global css link to a client.js import which prevents extraneous caching 53 | * Changes favicons with broader coverage 54 | * Changes single quotes to double in html/svelte files 55 | * Removes default routes, components and emptied index & layout 56 | * Removes custom browserslist config to use default 57 | 58 | 59 | ## Bugs and feedback 60 | 61 | If you run into an issue which you don't see in the normal sapper-template, open an [issue](https://github.com/matt3224/sapper-start/issues). 62 | -------------------------------------------------------------------------------- /static/global.css: -------------------------------------------------------------------------------- 1 | *{background-size:cover}*,::before,::after{background-repeat:no-repeat;background-position:center;box-sizing:border-box;position:relative}::before,::after{text-decoration:inherit;vertical-align:inherit}html{cursor:default;line-height:1.4;-moz-tab-size:3;-o-tab-size:3;tab-size:3;-webkit-tap-highlight-color:transparent;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.6875em 0}dl dl,dl ol,dl ul,ol dl,ul dl,ol ol,ol ul,ul ol,ul ul{margin:0}hr{height:0;overflow:visible}main,details{display:block}nav ol,nav ul{list-style:none;padding:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:83.3333%}audio,canvas,iframe,img,svg,video{vertical-align:middle}img,iframe{border-style:none}img,picture,svg{display:block;max-width:100%}img{height:auto}figure{margin:0}svg:not([fill]){fill:currentColor}svg:not(:root){overflow:hidden}table{border-collapse:collapse}button,input,optgroup,select,textarea{background-color:transparent;border-style:none;font-family:inherit;font-size:inherit;line-height:inherit;margin:0;word-break:normal}button,input{color:inherit;overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{cursor:pointer;-webkit-appearance:button}fieldset{border:1px solid silver;padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto;resize:vertical}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.5}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}dialog{background-color:#fff;border:solid;color:#000;display:block;height:-moz-fit-content;height:-webkit-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:-webkit-fit-content;width:fit-content}dialog:not([open]){display:none}summary{display:list-item}template{display:none}a,area,button,input,label,select,summary,textarea,[tabindex]{touch-action:manipulation}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}a:hover{outline-width:0}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0, 0, 0, 0);position:absolute}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;-webkit-font-smoothing:antialiased}a{text-decoration:none} 2 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | import { timestamp, files, shell } from '@sapper/service-worker'; 2 | 3 | const ASSETS = `cache${timestamp}`; 4 | 5 | // `shell` is an array of all the files generated by the bundler, 6 | // `files` is an array of everything in the `static` directory 7 | const to_cache = shell.concat(files); 8 | const staticAssets = new Set(to_cache); 9 | 10 | self.addEventListener('install', event => { 11 | event.waitUntil( 12 | caches 13 | .open(ASSETS) 14 | .then(cache => cache.addAll(to_cache)) 15 | .then(() => { 16 | self.skipWaiting(); 17 | }) 18 | ); 19 | }); 20 | 21 | self.addEventListener('activate', event => { 22 | event.waitUntil( 23 | caches.keys().then(async keys => { 24 | // delete old caches 25 | for (const key of keys) { 26 | if (key !== ASSETS) await caches.delete(key); 27 | } 28 | 29 | self.clients.claim(); 30 | }) 31 | ); 32 | }); 33 | 34 | 35 | /** 36 | * Fetch the asset from the network and store it in the cache. 37 | * Fall back to the cache if the user is offline. 38 | */ 39 | async function fetchAndCache(request) { 40 | const cache = await caches.open(`offline${timestamp}`) 41 | 42 | try { 43 | const response = await fetch(request); 44 | cache.put(request, response.clone()); 45 | return response; 46 | } catch (err) { 47 | const response = await cache.match(request); 48 | if (response) return response; 49 | 50 | throw err; 51 | } 52 | } 53 | 54 | self.addEventListener('fetch', event => { 55 | if (event.request.method !== 'GET' || event.request.headers.has('range')) return; 56 | 57 | const url = new URL(event.request.url); 58 | 59 | // don't try to handle e.g. data: URIs 60 | const isHttp = url.protocol.startsWith('http'); 61 | const isDevServerRequest = url.hostname === self.location.hostname && url.port !== self.location.port; 62 | const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname); 63 | const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset; 64 | 65 | if (isHttp && !isDevServerRequest && !skipBecauseUncached) { 66 | event.respondWith( 67 | (async () => { 68 | // always serve static files and bundler-generated assets from cache. 69 | // if your application has other URLs with data that will never change, 70 | // set this variable to true for them and they will only be fetched once. 71 | const cachedAsset = isStaticAsset && await caches.match(event.request); 72 | 73 | // for pages, you might want to serve a shell `service-worker-index.html` file, 74 | // which Sapper has generated for you. It's not right for every 75 | // app, but if it's right for yours then uncomment this section 76 | /* 77 | if (!cachedAsset && url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) { 78 | return caches.match('/service-worker-index.html'); 79 | } 80 | */ 81 | 82 | return cachedAsset || fetchAndCache(event.request); 83 | })() 84 | ); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /static/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* Rollup plugins */ 2 | import alias from '@rollup/plugin-alias'; 3 | import babel from '@rollup/plugin-babel'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import replace from '@rollup/plugin-replace'; 6 | import resolve from '@rollup/plugin-node-resolve'; 7 | import url from '@rollup/plugin-url'; 8 | import svelte from 'rollup-plugin-svelte'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | 11 | /* Packages */ 12 | import autoprefixer from 'autoprefixer'; 13 | import config from 'sapper/config/rollup.js'; 14 | import 'dotenv/config'; 15 | import path from 'path'; 16 | import pkg from './package.json'; 17 | import { scss, postcss } from 'svelte-preprocess'; 18 | 19 | /* Assignments */ 20 | const mode = process.env.NODE_ENV; 21 | const dev = mode === 'development'; 22 | const legacy = !!process.env.SAPPER_LEGACY_BUILD; 23 | 24 | const onwarn = (warning, onwarn) => 25 | (warning.code === 'MISSING_EXPORT' && /'preload'/.test(warning.message)) || 26 | (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || 27 | onwarn(warning); 28 | 29 | const customResolver = resolve({ 30 | extensions: ['.js', '.mjs', '.html', '.svelte', '.scss', '.json'] 31 | }); 32 | 33 | const aliasconfig = { 34 | customResolver, 35 | entries: [{ find: '~', replacement: path.join(__dirname, './src') }] 36 | }; 37 | 38 | const preprocess = [ 39 | scss({ sourceMap: false }), 40 | postcss({ plugins: [ autoprefixer ] }) 41 | ]; 42 | 43 | const replaceconfig = { 44 | preventAssignment: false, 45 | values: { 46 | 'process.browser': true, 47 | 'process.env.NODE_ENV': JSON.stringify(mode), 48 | 'pkg.version': pkg.version 49 | } 50 | }; 51 | 52 | /* Config */ 53 | export default { 54 | 55 | client: { 56 | input: config.client.input(), 57 | output: config.client.output(), 58 | onwarn, 59 | plugins: [ 60 | alias(aliasconfig), 61 | commonjs(), 62 | customResolver, 63 | replace(replaceconfig), 64 | resolve({ browser: true, dedupe: ['svelte'] }), 65 | svelte({ 66 | compilerOptions: { 67 | dev, 68 | hydratable: true 69 | }, 70 | preprocess 71 | }), 72 | url({ 73 | sourceDir: path.resolve(__dirname, 'src/node_modules/images'), 74 | publicPath: '/client/' 75 | }), 76 | legacy && babel({ 77 | exclude: ['node_modules/@babel/**'], 78 | extensions: ['.js', '.mjs', '.html', '.svelte'], 79 | plugins: [ 80 | '@babel/plugin-syntax-dynamic-import', 81 | ['@babel/plugin-transform-runtime', { 82 | useESModules: true 83 | }] 84 | ], 85 | presets: [['@babel/preset-env']], 86 | babelHelpers: 'runtime', 87 | }), 88 | 89 | !dev && terser({ module: true, numWorkers: 1 }) 90 | ], 91 | preserveEntrySignatures: false 92 | }, 93 | 94 | server: { 95 | input: config.server.input(), 96 | output: config.server.output(), 97 | onwarn, 98 | plugins: [ 99 | alias(aliasconfig), 100 | commonjs(), 101 | customResolver, 102 | replace({...replaceconfig, 'process.browser': false}), 103 | resolve({ dedupe: ['svelte'] }), 104 | svelte({ 105 | compilerOptions: { 106 | dev, 107 | generate: 'ssr', 108 | hydratable: true 109 | }, 110 | emitCss: false, 111 | preprocess 112 | }), 113 | url({ 114 | sourceDir: path.resolve(__dirname, 'src/node_modules/images'), 115 | publicPath: '/client/', 116 | emitFiles: false // already emitted by client build 117 | }), 118 | ], 119 | external: Object.keys(pkg.dependencies).concat(require('module').builtinModules), 120 | preserveEntrySignatures: 'strict' 121 | }, 122 | 123 | serviceworker: { 124 | input: config.serviceworker.input(), 125 | output: config.serviceworker.output(), 126 | onwarn, 127 | plugins: [ 128 | alias(aliasconfig), 129 | commonjs(), 130 | customResolver, 131 | replace(replaceconfig), 132 | resolve(), 133 | !dev && terser({ numWorkers: 1 }) 134 | ], 135 | preserveEntrySignatures: false 136 | } 137 | }; --------------------------------------------------------------------------------