├── .gitignore ├── LICENSE ├── README.md ├── build.js ├── cargo-generate.toml ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app.ts ├── bindings.d.ts ├── handlers │ └── home.ts ├── index.ts ├── middleware │ ├── customHeader.ts │ ├── htmlErrors.ts │ ├── static.ts │ └── staticD.d.ts ├── routes.ts ├── styles │ ├── app.scss │ └── holiday.css └── templates │ ├── html.ts │ └── layout.ts ├── static └── favicon-32x32.png ├── test ├── app.test.ts ├── manifest.ts └── tsconfig.json ├── tsconfig.json └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # Optional npm cache directory 6 | .npm 7 | 8 | # Optional eslint cache 9 | .eslintcache 10 | 11 | build/ 12 | dist/ 13 | worker/ 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 {{ authors }} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌄 sunder-worker-template 2 | 3 | A batteries-included template for [Cloudflare Workers](https://workers.cloudflare.com) with the following configuration: 4 | 5 | * [Sunder](https://sunderjs.com) minimal web framework. 6 | * [ESBuild](https://esbuild.github.io/) for builds in <50ms. 7 | * [Typescript](https://www.typescriptlang.org/) for typechecking. 8 | * [Miniflare](https://miniflare.dev) and [Jest](https://jestjs.io/) for testing. 9 | * [Sass](https://sass-lang.com/) for CSS preprocessing and minification. 10 | * [Workers Sites](https://developers.cloudflare.com/workers/platform/sites) for static files. 11 | 12 | If you disagree with any of these choices it's easy to swap out that decision. 13 | 14 | ## 🚀 Getting started 15 | 16 | Press the green *"Use this template"* button in the top right to make a Github repository based on this one. 17 | 18 | ## Development 19 | To build and preview using Miniflare, use 20 | ``` 21 | npm run miniflare 22 | ``` 23 | 24 | To serve using Miniflare, watch changes and build as you make changes, use 25 | ``` 26 | npm run watch 27 | ``` 28 | 29 | To make a production build use 30 | ``` 31 | npm run build 32 | ``` 33 | 34 | ### Testing 35 | 36 | The tests are run using Jest. Use `npm test` to run your tests. 37 | 38 | This is the recommended way to develop most of your app. Write tests for core functionality instead of relying on Miniflare or `wrangler dev`. 39 | 40 | ### Publishing 41 | To publish, first make a build using `npm run build` and then use the Wrangler CLI tool. 42 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import estrella from "estrella"; 3 | import fsExtra from "fs-extra"; 4 | import chalk from "chalk"; 5 | import sass from "sass"; 6 | import { dirname } from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | const { build, watch } = estrella; 12 | const { remove, copy, writeFile } = fsExtra 13 | 14 | const cssEntryFile = "src/styles/app.scss"; 15 | const cssTargetFile = "dist/static/app.css"; 16 | 17 | async function buildStyles(config) { 18 | const result = sass.renderSync({ 19 | file: cssEntryFile, 20 | outputStyle: config.debug ? 'expanded' : 'compressed', 21 | }); 22 | 23 | try { 24 | await writeFile(cssTargetFile, result.css) 25 | console.log(chalk.greenBright(`Wrote ${cssTargetFile}`)) 26 | } catch (e) { 27 | console.warn(chalk.yellow(`Could not write ${cssTargetFile}`)) 28 | } 29 | return result.stats.includedFiles; 30 | } 31 | 32 | build({ 33 | entry: __dirname + "/src/index.ts", 34 | bundle: true, 35 | outdir: __dirname + "/dist", 36 | minify: false, 37 | external: ["__STATIC_CONTENT_MANIFEST"], 38 | outExtension: { ".js": ".mjs" }, 39 | format: "esm", 40 | onStart: async (config, changedFiles, context) => { 41 | const isInitialBuild = changedFiles.length === 0; 42 | if (isInitialBuild) { 43 | 44 | try { 45 | await copy("static", "dist/static", { recursive: true }); 46 | } catch (e) { 47 | console.warn(chalk.yellow("Could not remove existing dist folder and copy static assets (maybe you are running wrangler dev?)")) 48 | } 49 | 50 | const cssInputFiles = await buildStyles(config); 51 | if (config.watch) { 52 | watch(cssInputFiles, f => { 53 | buildStyles(config); 54 | }); 55 | } 56 | } 57 | } 58 | }); -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | exclude = ["*.png"] 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: "miniflare", 3 | testMatch: ["/test/build/test/*\.test\.js"], 4 | // transformIgnorePatterns: [ 5 | // "/node_modules/(?!(sunder)/)" // Sunder has ES Module exports which Jest doesn't understand 6 | // ], 7 | moduleNameMapper: { 8 | "__STATIC_CONTENT_MANIFEST": "/test/build/test/manifest.js" 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sunder-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "A starter template for Sunder applications on Cloudflare Workers", 6 | "type": "module", 7 | "scripts": { 8 | "build": "rimraf dist && node build.js -sourcemap", 9 | "build:nocleanup": "node build.js -sourcemap", 10 | "miniflare": "miniflare", 11 | "watch": "rimraf dist && miniflare --watch", 12 | "test": "cd test && tsc && cd .. && node --experimental-vm-modules node_modules/jest/bin/jest.js", 13 | "types:check": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json" 14 | }, 15 | "main": "dist/index.mjs", 16 | "devDependencies": { 17 | "@cloudflare/workers-types": "^3.1.1", 18 | "@types/jest": "^27.0.2", 19 | "@types/mime": "^2.0.3", 20 | "@types/node": "^16.11.7", 21 | "chalk": "^4.1.0", 22 | "cloudflare-worker-mock": "^1.2.0", 23 | "esbuild": "^0.8.18", 24 | "estrella": "1.3.0", 25 | "fs-extra": "^9.0.1", 26 | "jest": "^27.3.1", 27 | "jest-environment-miniflare": "^2.0.0", 28 | "miniflare": "^2.0.0", 29 | "rimraf": "^3.0.2", 30 | "sass": "^1.43.4", 31 | "typescript": "^4.4.4" 32 | }, 33 | "dependencies": { 34 | "@cloudflare/kv-asset-handler": "^0.2.0", 35 | "@popeindustries/lit-html-server": "^3.1.0", 36 | "sunder": "^0.10.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Router, Sunder } from "sunder"; 2 | import { customHeader } from "./middleware/customHeader"; 3 | import { renderErrorsAsJSON } from "sunder/middleware/render"; 4 | import { registerRoutes } from "./routes"; 5 | import { renderErrorsAsHTML } from "./middleware/htmlErrors"; 6 | import { Env } from "./bindings"; 7 | 8 | export function createApp() { 9 | const app = new Sunder(); 10 | const router = new Router(); 11 | registerRoutes(router); 12 | 13 | app.use(customHeader); 14 | 15 | app.use(renderErrorsAsHTML); 16 | app.use(renderErrorsAsJSON); 17 | 18 | app.use(router.middleware); 19 | 20 | return app; 21 | } -------------------------------------------------------------------------------- /src/bindings.d.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | // Added by Cloudflare Sites 3 | __STATIC_CONTENT: KVNamespace 4 | 5 | /** Add your bindings (Durable Objects, KV, etc. here) */ 6 | // MY_DURABLE_OBJECT: DurableObjectNamespace 7 | } 8 | 9 | declare module '__STATIC_CONTENT_MANIFEST' { 10 | const manifest: string 11 | export default manifest 12 | } 13 | -------------------------------------------------------------------------------- /src/handlers/home.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "sunder"; 2 | import { html, renderToString } from "@popeindustries/lit-html-server"; 3 | 4 | import { htmlDocumentTemplate, HTMLTemplateData } from "../templates/html"; 5 | import { basicLayout } from "../templates/layout"; 6 | 7 | export async function homeHandler(ctx: Context) { 8 | const pageData: HTMLTemplateData = { 9 | title: "{{ project-name }}", 10 | body: html` 11 |

Welcome to the Sunder Starter template.

12 | 13 |
14 |

15 | Edit this page in src/handlers/home.ts 16 |

17 |
18 | `, 19 | }; 20 | const templateResult = htmlDocumentTemplate(basicLayout(pageData)); 21 | ctx.response.body = await renderToString(templateResult); 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "./app"; 2 | 3 | const app = createApp(); 4 | 5 | 6 | export default { 7 | async fetch(request: Request, env: any, ctx: any) { 8 | return app.fetch(request, env, ctx) 9 | } 10 | } -------------------------------------------------------------------------------- /src/middleware/customHeader.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "sunder"; 2 | 3 | /** 4 | * Example middleware that adds a custom header 5 | */ 6 | export async function customHeader(ctx: Context, next: Function) { 7 | ctx.response.set('X-My-Custom-Header', `Sunder`); 8 | await next(); 9 | } -------------------------------------------------------------------------------- /src/middleware/htmlErrors.ts: -------------------------------------------------------------------------------- 1 | import { html, renderToString } from "@popeindustries/lit-html-server"; 2 | import { Context, MiddlewareNextFunction } from "sunder"; 3 | import { htmlDocumentTemplate } from "../templates/html"; 4 | import { basicLayout } from "../templates/layout"; 5 | 6 | /** 7 | * Rewrites JSON errors as a HTML page if the request Accept header contains HTML. 8 | * 9 | * This must be placed before the `renderErrorsAsJSON` middleware. Perhaps that's counter-intuitive, but errors get caught in reverse order. 10 | */ 11 | export async function renderErrorsAsHTML(ctx: Context<{}, { assetPath: string }>, next: MiddlewareNextFunction) { 12 | try { 13 | await next(); 14 | } catch (e) { 15 | if (!ctx.request.has("accept") || ctx.request.get("accept")!.indexOf("html") === -1) { 16 | throw e; // "html" is not present in the accept header, pass the error on. 17 | } 18 | 19 | // The JSON error middleware before sets body to a JSON object with a message field 20 | if (!ctx.response.body || (ctx.response.body as {message: string}).message === undefined) { 21 | console.error("Could not render error as HTML, is the middleware registered before the JSON error middleware?"); 22 | throw e; 23 | } 24 | const msg = (ctx.response.body as {message: string}).message; 25 | const errorHtml = html` 26 |

${ctx.response.status}${ctx.response.statusText ? " " + ctx.response.statusText : ""}

27 |

28 | ${msg} 29 |

` 30 | 31 | ctx.response.body = await renderToString(htmlDocumentTemplate(basicLayout({body: errorHtml}))); 32 | ctx.response.set("content-type", "text/html;charset=utf-8"); 33 | throw e; 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/middleware/static.ts: -------------------------------------------------------------------------------- 1 | import manifestJSON from '__STATIC_CONTENT_MANIFEST'; 2 | import { getAssetFromKV, Options } from "@cloudflare/kv-asset-handler"; 3 | import { Context, MiddlewareNextFunction } from "sunder"; 4 | import { Env } from '@/bindings'; 5 | 6 | export function serveStaticAssetsFromKV(options: Partial = {}) { 7 | const manifest = JSON.parse(manifestJSON); 8 | options.ASSET_MANIFEST = manifest; 9 | 10 | return async function (ctx: Context, next: MiddlewareNextFunction) { 11 | options.ASSET_NAMESPACE = ctx.env.__STATIC_CONTENT; 12 | try { 13 | const resp = await getAssetFromKV( 14 | (ctx as any).event, 15 | { 16 | mapRequestToAsset: (req: Request) => new Request(ctx.url.origin + "/" + ctx.params.assetPath, req), 17 | ...options 18 | } 19 | ); 20 | ctx.response.overwrite(resp, {mergeHeaders: true}); 21 | } catch (e) { 22 | let pathname = ctx.url.pathname; 23 | ctx.throw(404, `${pathname} not found`); 24 | } 25 | await next(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/middleware/staticD.d.ts: -------------------------------------------------------------------------------- 1 | declare module '__STATIC_CONTENT_MANIFEST' { 2 | const manifest: string 3 | export default manifest 4 | } 5 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "sunder"; 2 | import { Env } from "./bindings"; 3 | import { homeHandler } from "./handlers/home"; 4 | import { serveStaticAssetsFromKV } from "./middleware/static"; 5 | 6 | export function registerRoutes(router: Router) { 7 | router.get("/", homeHandler); 8 | 9 | router.get("/static/:assetPath+", serveStaticAssetsFromKV()) 10 | router.head("/static/:assetPath+", serveStaticAssetsFromKV()) 11 | 12 | // Example inline route with a named parameter 13 | router.get("/hello/:name", ({ response, params }) => { 14 | response.body = `Hello ${params.name}!`; 15 | }); 16 | 17 | router.get("/robots.txt", (ctx) => { 18 | // This disallows all bots/scrapers 19 | ctx.response.body = `Agent: *\nDisallow: /`; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | // Holiday is a very minimal CSS framework, do swap it out if you want something more powerful. 2 | // Holiday's home: https://holidaycss.js.org/ 3 | @use "holiday.css"; 4 | -------------------------------------------------------------------------------- /src/styles/holiday.css: -------------------------------------------------------------------------------- 1 | /*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ 2 | 3 | /* 4 | Document 5 | ======== 6 | */ 7 | 8 | /** 9 | Use a better box model (opinionated). 10 | */ 11 | 12 | *, *::before, *::after { 13 | box-sizing: border-box; 14 | } 15 | 16 | /** 17 | Use a more readable tab size (opinionated). 18 | */ 19 | 20 | :root { 21 | -moz-tab-size: 4; 22 | tab-size: 4; 23 | } 24 | 25 | /** 26 | 1. Correct the line height in all browsers. 27 | 2. Prevent adjustments of font size after orientation changes in iOS. 28 | */ 29 | 30 | html { 31 | line-height: 1.15; 32 | /* 1 */ 33 | -webkit-text-size-adjust: 100%; 34 | /* 2 */ 35 | } 36 | 37 | /* 38 | Sections 39 | ======== 40 | */ 41 | 42 | /** 43 | Remove the margin in all browsers. 44 | */ 45 | 46 | body { 47 | margin: 0; 48 | } 49 | 50 | /** 51 | Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 52 | */ 53 | 54 | body { 55 | font-family: system-ui, -apple-system, /* Firefox supports this but not yet `system-ui` */ 56 | 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; 57 | } 58 | 59 | /* 60 | Grouping content 61 | ================ 62 | */ 63 | 64 | /** 65 | 1. Add the correct height in Firefox. 66 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 67 | */ 68 | 69 | hr { 70 | height: 0; 71 | /* 1 */ 72 | color: inherit; 73 | /* 2 */ 74 | } 75 | 76 | /* 77 | Text-level semantics 78 | ==================== 79 | */ 80 | 81 | /** 82 | Add the correct text decoration in Chrome, Edge, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | text-decoration: underline dotted; 87 | } 88 | 89 | /** 90 | Add the correct font weight in Edge and Safari. 91 | */ 92 | 93 | b, strong { 94 | font-weight: bolder; 95 | } 96 | 97 | /** 98 | 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 99 | 2. Correct the odd 'em' font sizing in all browsers. 100 | */ 101 | 102 | code, kbd, samp, pre { 103 | font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; 104 | /* 1 */ 105 | font-size: 1em; 106 | /* 2 */ 107 | } 108 | 109 | /** 110 | Add the correct font size in all browsers. 111 | */ 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | /** 118 | Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. 119 | */ 120 | 121 | sub, sup { 122 | font-size: 75%; 123 | line-height: 0; 124 | position: relative; 125 | vertical-align: baseline; 126 | } 127 | 128 | sub { 129 | bottom: -0.25em; 130 | } 131 | 132 | sup { 133 | top: -0.5em; 134 | } 135 | 136 | /* 137 | Tabular data 138 | ============ 139 | */ 140 | 141 | /** 142 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 143 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 144 | */ 145 | 146 | table { 147 | text-indent: 0; 148 | /* 1 */ 149 | border-color: inherit; 150 | /* 2 */ 151 | } 152 | 153 | /* 154 | Forms 155 | ===== 156 | */ 157 | 158 | /** 159 | 1. Change the font styles in all browsers. 160 | 2. Remove the margin in Firefox and Safari. 161 | */ 162 | 163 | button, input, optgroup, select, textarea { 164 | font-family: inherit; 165 | /* 1 */ 166 | font-size: 100%; 167 | /* 1 */ 168 | line-height: 1.15; 169 | /* 1 */ 170 | margin: 0; 171 | /* 2 */ 172 | } 173 | 174 | /** 175 | Remove the inheritance of text transform in Edge and Firefox. 176 | 1. Remove the inheritance of text transform in Firefox. 177 | */ 178 | 179 | button, select { 180 | /* 1 */ 181 | text-transform: none; 182 | } 183 | 184 | /** 185 | Correct the inability to style clickable types in iOS and Safari. 186 | */ 187 | 188 | button, [type='button'], [type='reset'], [type='submit'] { 189 | -webkit-appearance: button; 190 | } 191 | 192 | /** 193 | Remove the inner border and padding in Firefox. 194 | */ 195 | 196 | ::-moz-focus-inner { 197 | border-style: none; 198 | padding: 0; 199 | } 200 | 201 | /** 202 | Restore the focus styles unset by the previous rule. 203 | */ 204 | 205 | :-moz-focusring { 206 | outline: 1px dotted ButtonText; 207 | } 208 | 209 | /** 210 | Remove the additional ':invalid' styles in Firefox. 211 | See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 212 | */ 213 | 214 | :-moz-ui-invalid { 215 | box-shadow: none; 216 | } 217 | 218 | /** 219 | Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. 220 | */ 221 | 222 | legend { 223 | padding: 0; 224 | } 225 | 226 | /** 227 | Add the correct vertical alignment in Chrome and Firefox. 228 | */ 229 | 230 | progress { 231 | vertical-align: baseline; 232 | } 233 | 234 | /** 235 | Correct the cursor style of increment and decrement buttons in Safari. 236 | */ 237 | 238 | ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { 239 | height: auto; 240 | } 241 | 242 | /** 243 | 1. Correct the odd appearance in Chrome and Safari. 244 | 2. Correct the outline style in Safari. 245 | */ 246 | 247 | [type='search'] { 248 | -webkit-appearance: textfield; 249 | /* 1 */ 250 | outline-offset: -2px; 251 | /* 2 */ 252 | } 253 | 254 | /** 255 | Remove the inner padding in Chrome and Safari on macOS. 256 | */ 257 | 258 | ::-webkit-search-decoration { 259 | -webkit-appearance: none; 260 | } 261 | 262 | /** 263 | 1. Correct the inability to style clickable types in iOS and Safari. 264 | 2. Change font properties to 'inherit' in Safari. 265 | */ 266 | 267 | ::-webkit-file-upload-button { 268 | -webkit-appearance: button; 269 | /* 1 */ 270 | font: inherit; 271 | /* 2 */ 272 | } 273 | 274 | /* 275 | Interactive 276 | =========== 277 | */ 278 | 279 | /* 280 | Add the correct display in Chrome and Safari. 281 | */ 282 | 283 | summary { 284 | display: list-item; 285 | } 286 | 287 | abbr { 288 | cursor: help; 289 | } 290 | 291 | button, summary, [type="button"], [type="reset"], [type="submit"], [type="color"], [type="file"], [type="range"], label>[type="checkbox"]:enabled, label>[type="radio"]:enabled { 292 | cursor: pointer; 293 | } 294 | 295 | [readonly] { 296 | cursor: default; 297 | } 298 | 299 | /* stylelint-disable-next-line selector-max-specificity */ 300 | 301 | :disabled, [type="checkbox"][id]:disabled+[for], [type="radio"][id]:disabled+[for] { 302 | cursor: not-allowed; 303 | } 304 | 305 | :root { 306 | --max-body-width: 48rem; 307 | } 308 | 309 | html { 310 | height: 100%; 311 | line-height: 1.4; 312 | } 313 | 314 | h1, h2, h3 { 315 | line-height: 1.15; 316 | } 317 | 318 | body { 319 | display: flex; 320 | flex-direction: column; 321 | width: calc(100% - 1rem); 322 | max-width: var(--max-body-width); 323 | min-height: 100%; 324 | margin: 0 auto; 325 | overflow-wrap: break-word; 326 | } 327 | 328 | main { 329 | flex-grow: 1; 330 | } 331 | 332 | img { 333 | max-width: 100%; 334 | max-height: 100vh; 335 | } 336 | 337 | table { 338 | display: block; 339 | overflow-x: auto; 340 | } 341 | 342 | pre { 343 | padding: 1rem; 344 | /* Prism hack */ 345 | /* stylelint-disable declaration-no-important */ 346 | margin-top: 1rem !important; 347 | margin-bottom: 1rem !important; 348 | /* stylelint-enable */ 349 | overflow-x: auto; 350 | line-height: 1.15; 351 | } 352 | 353 | code { 354 | padding: 0.25rem; 355 | } 356 | 357 | pre code { 358 | padding: unset; 359 | } 360 | 361 | kbd { 362 | display: inline-flex; 363 | align-items: center; 364 | justify-content: center; 365 | min-width: 1.5rem; 366 | max-width: calc(100% - 0.1rem * 2); 367 | min-height: 1.5rem; 368 | padding: 0 0.25rem; 369 | margin: 0 0.1rem; 370 | overflow: hidden; 371 | } 372 | 373 | kbd kbd { 374 | min-width: unset; 375 | min-height: unset; 376 | padding: 0; 377 | margin: 0; 378 | } 379 | 380 | kbd kbd:not(:first-child) { 381 | padding-left: 0.25rem; 382 | } 383 | 384 | kbd kbd:not(:last-child) { 385 | padding-right: 0.25rem; 386 | } 387 | 388 | iframe { 389 | width: 100%; 390 | border: none; 391 | } 392 | 393 | dialog { 394 | max-height: calc(100% - 1rem); 395 | overflow-y: auto; 396 | border: none; 397 | } 398 | 399 | audio, video, embed, object { 400 | width: 100%; 401 | } 402 | 403 | [type="range"], meter, progress { 404 | display: block; 405 | width: 100%; 406 | height: 2.25rem; 407 | } 408 | 409 | [type="color"] { 410 | height: 2.25rem; 411 | vertical-align: top; 412 | } 413 | 414 | td, th, details, button, [type="button"], [type="reset"], [type="submit"] { 415 | padding: 0.5rem; 416 | } 417 | 418 | input:not([type]), [type="email"], [type="hidden"], [type="number"], [type="password"], [type="search"], [type="tel"], [type="text"], [type="url"] { 419 | display: block; 420 | width: 100%; 421 | height: 2.25rem; 422 | padding: 0.5rem; 423 | } 424 | 425 | [type="file"] { 426 | display: block; 427 | width: 100%; 428 | height: 2.25rem; 429 | /* Works in Chrome (but poorly), doesn't work in Firefox */ 430 | padding-top: 0.35rem; 431 | } 432 | 433 | [type="date"], [type="datetime-local"], [type="time"], [type="month"], [type="week"] { 434 | display: block; 435 | width: 100%; 436 | height: 2.25rem; 437 | /* 0.4rem is a hack for mobile Chrome */ 438 | padding: 0.4rem 0.5rem; 439 | } 440 | 441 | output { 442 | display: block; 443 | width: 100%; 444 | } 445 | 446 | textarea { 447 | display: block; 448 | width: 100%; 449 | min-height: 8em; 450 | padding: 0.5rem; 451 | } 452 | 453 | select { 454 | display: block; 455 | width: 100%; 456 | min-height: 2.25rem; 457 | /* "padding: 0.5rem;" messes up the height of selects */ 458 | padding: 0.45rem 0.5rem; 459 | } 460 | 461 | summary { 462 | padding: 0.5rem; 463 | margin: -0.5rem; 464 | } 465 | 466 | [type="image"] { 467 | vertical-align: bottom; 468 | } 469 | 470 | fieldset { 471 | padding: 0.75rem; 472 | } 473 | 474 | label>[type="color"] { 475 | margin-left: 0.25rem; 476 | } 477 | 478 | label { 479 | display: flex; 480 | flex-wrap: wrap; 481 | align-items: center; 482 | width: 100%; 483 | margin-top: 1rem; 484 | } 485 | 486 | legend+label { 487 | margin-top: 0; 488 | } 489 | 490 | [type="checkbox"]+label, [type="radio"]+label { 491 | display: inline-flex; 492 | width: unset; 493 | vertical-align: text-bottom; 494 | } 495 | 496 | /* stylelint-disable-next-line plugin/stylelint-group-selectors */ 497 | 498 | blockquote>p:first-child, fieldset>label:first-child { 499 | margin-top: 0; 500 | } 501 | 502 | label>[type="checkbox"], label>[type="radio"] { 503 | min-height: 1rem; 504 | margin-right: 0.25rem; 505 | } 506 | 507 | blockquote { 508 | padding: 1rem 2rem; 509 | margin-right: 0; 510 | margin-left: 0; 511 | } 512 | 513 | blockquote>p:last-child { 514 | margin-bottom: 0; 515 | } 516 | 517 | footer { 518 | margin-top: 1rem; 519 | } 520 | 521 | figure>figcaption, body>header { 522 | text-align: center; 523 | } 524 | 525 | body>footer { 526 | padding-bottom: 1rem; 527 | text-align: center; 528 | } 529 | 530 | figure { 531 | margin-right: 0; 532 | margin-left: 0; 533 | text-align: center; 534 | } 535 | 536 | /* stylelint-disable-next-line selector-max-universal */ 537 | 538 | figure>* { 539 | text-align: initial; 540 | } 541 | 542 | dt { 543 | font-weight: bold; 544 | } 545 | 546 | dd { 547 | margin-bottom: 1rem; 548 | } 549 | 550 | li { 551 | margin-top: 0.5rem; 552 | margin-bottom: 0.5rem; 553 | } 554 | 555 | picture { 556 | position: relative; 557 | left: calc(-50vw + 50%); 558 | display: block; 559 | width: 100vw; 560 | text-align: center; 561 | } 562 | 563 | /* highlight.js hack */ 564 | 565 | /* stylelint-disable-next-line selector-max-class */ 566 | 567 | .hljs { 568 | /* stylelint-disable-next-line declaration-no-important */ 569 | padding: 1rem !important; 570 | margin: -1rem; 571 | } 572 | 573 | @media (max-width: 50rem) { 574 | table { 575 | width: calc(100% + 1rem); 576 | margin-left: -0.5rem; 577 | } 578 | video { 579 | width: calc(100% + 1rem); 580 | max-height: 100vh; 581 | margin-left: -0.5rem; 582 | } 583 | pre { 584 | width: calc(100% + 1rem); 585 | /* !important is Prism hack */ 586 | /* stylelint-disable declaration-no-important */ 587 | padding-right: 0.5rem !important; 588 | padding-left: 0.5rem !important; 589 | margin-left: -0.5rem !important; 590 | /* stylelint-enable */ 591 | } 592 | /* highlight.js hack */ 593 | /* stylelint-disable-next-line selector-max-class */ 594 | .hljs { 595 | /* stylelint-disable declaration-no-important */ 596 | padding-right: 0.5rem !important; 597 | padding-left: 0.5rem !important; 598 | /* stylelint-enable */ 599 | margin: -1rem -0.5rem; 600 | } 601 | } 602 | 603 | @media (hover: hover) { 604 | body { 605 | overflow-x: hidden; 606 | } 607 | nav { 608 | display: flex; 609 | background-color: var(--background-color); 610 | border-bottom: var(--border-width) solid var(--border-color); 611 | } 612 | body>nav { 613 | position: relative; 614 | left: calc(-50vw + 50%); 615 | width: 100vw; 616 | } 617 | nav ul { 618 | padding-left: 0; 619 | } 620 | body>nav>ul { 621 | width: calc(var(--max-body-width) + 2rem); 622 | padding-right: 0.5rem; 623 | padding-left: 0.5rem; 624 | margin: 0.5rem auto; 625 | } 626 | nav ul li { 627 | position: relative; 628 | display: inline-block; 629 | } 630 | nav>ul>li { 631 | padding: 0.5rem; 632 | margin: 0; 633 | } 634 | nav ul li a { 635 | text-decoration: none; 636 | white-space: nowrap; 637 | } 638 | nav ul li ul { 639 | position: absolute; 640 | left: -9999px; 641 | z-index: 1; 642 | min-width: calc(100% + var(--border-width) * 2); 643 | padding: 0.25rem 0.5rem; 644 | margin-top: 0.5rem; 645 | margin-left: calc(-0.5rem - var(--border-width)); 646 | background-color: var(--background-color); 647 | border: var(--border-width) solid var(--border-color); 648 | border-radius: var(--border-radius); 649 | } 650 | nav ul li ul li { 651 | width: 100%; 652 | } 653 | /* stylelint-disable-next-line selector-max-compound-selectors, selector-max-type */ 654 | nav ul li ul li ul { 655 | min-width: calc(100% + 1rem + var(--border-width) * 2); 656 | margin-top: 0.75rem; 657 | } 658 | nav :focus~ul, nav :focus~ul ul, nav ul ul:focus-within, nav ul li:hover ul { 659 | left: initial; 660 | } 661 | /* stylelint-disable-next-line selector-max-universal */ 662 | nav li>*:not(ul):not(a):not(:only-child) { 663 | cursor: default; 664 | } 665 | /* stylelint-disable-next-line selector-max-universal */ 666 | nav li>*:not(ul):not(:only-child)::after { 667 | content: " ▾"; 668 | } 669 | } 670 | 671 | @media not all and (hover: hover) { 672 | nav { 673 | position: relative; 674 | left: calc(-50vw + 50%); 675 | width: 100vw; 676 | padding-top: 2rem; 677 | background-image: url('data:image/svg+xml;utf8,'); 678 | background-repeat: no-repeat; 679 | background-position: top; 680 | border-bottom: var(--border-width) solid var(--border-color); 681 | } 682 | /* stylelint-disable-next-line selector-max-universal */ 683 | nav>* { 684 | display: none; 685 | } 686 | nav:hover>ul { 687 | display: inherit; 688 | } 689 | /* stylelint-disable-next-line selector-max-universal */ 690 | nav:hover>*:not(ul) { 691 | display: unset; 692 | } 693 | @media (prefers-color-scheme: dark) { 694 | nav { 695 | background-image: url('data:image/svg+xml;utf8,'); 696 | } 697 | } 698 | } 699 | 700 | :root { 701 | color-scheme: light dark; 702 | --border-color: #dbdbdb; 703 | --border-hover-color: #b5b5b5; 704 | --background-color: #fff; 705 | --highlighted-background-color: #f5f5f5; 706 | --text-color: #363636; 707 | --danger-color: #f14668; 708 | --danger-text-color: #fff; 709 | --danger-hover-color: #f03a5f; 710 | --success-color: #48c774; 711 | --success-text-color: #fff; 712 | --success-hover-color: #3ec46d; 713 | --danger-text-background-color: #fde0e6; 714 | --success-text-background-color: #effaf3; 715 | --border-radius: 0.25rem; 716 | --border-width: 1px; 717 | --code-text-color: #f14668; 718 | --code-background-color: #f5f5f5; 719 | --link-color: #3273dc; 720 | --link-visited-color: #b86bff; 721 | --link-hover-color: #363636; 722 | --link-active-color: #363636; 723 | } 724 | 725 | @media (prefers-color-scheme: dark) { 726 | :root { 727 | --border-color: #5f6267; 728 | --border-hover-color: #bcbebd; 729 | --background-color: #202124; 730 | --highlighted-background-color: #292b2e; 731 | --text-color: #fff; 732 | --danger-color: #770018; 733 | --danger-text-color: #fff; 734 | --danger-hover-color: #6b0015; 735 | --success-color: #006624; 736 | --success-text-color: #fff; 737 | --success-hover-color: #006122; 738 | --danger-text-background-color: #770018; 739 | --success-text-background-color: #006624; 740 | --code-text-color: #f1a0b0; 741 | --code-background-color: #292b2e; 742 | --link-color: #90b3ed; 743 | --link-visited-color: #cb93ff; 744 | --link-hover-color: #fff; 745 | --link-active-color: #fff; 746 | } 747 | input::-webkit-calendar-picker-indicator { 748 | filter: invert(); 749 | } 750 | } 751 | 752 | input { 753 | border-radius: var(--border-radius); 754 | } 755 | 756 | body { 757 | color: var(--text-color); 758 | background-color: var(--background-color); 759 | } 760 | 761 | pre { 762 | background-color: var(--code-background-color); 763 | /* Prism Dracula theme hack */ 764 | /* stylelint-disable-next-line declaration-no-important */ 765 | border-radius: 0 !important; 766 | } 767 | 768 | code { 769 | color: var(--code-text-color); 770 | background-color: var(--code-background-color); 771 | } 772 | 773 | pre code { 774 | background-color: inherit; 775 | } 776 | 777 | a, a code { 778 | color: var(--link-color); 779 | text-decoration: none; 780 | } 781 | 782 | a:visited, a:visited code { 783 | color: var(--link-visited-color); 784 | } 785 | 786 | a:hover, a:hover code { 787 | color: var(--link-hover-color); 788 | } 789 | 790 | a:active, a:active code { 791 | color: var(--link-active-color); 792 | } 793 | 794 | button, dialog, textarea, select { 795 | color: var(--text-color); 796 | background-color: var(--background-color); 797 | border: var(--border-width) solid var(--border-color); 798 | border-radius: var(--border-radius); 799 | } 800 | 801 | fieldset, details { 802 | border: var(--border-width) solid var(--border-color); 803 | border-radius: var(--border-radius); 804 | } 805 | 806 | summary { 807 | margin: calc(-0.5rem - var(--border-width)); 808 | border: var(--border-width) solid transparent; 809 | border-radius: var(--border-radius); 810 | } 811 | 812 | input:not([type]), [type="date"], [type="datetime-local"], [type="email"], [type="hidden"], [type="month"], [type="number"], [type="password"], [type="search"], [type="tel"], [type="text"], [type="time"], [type="url"], [type="week"], [type="button"], [type="color"] { 813 | color: var(--text-color); 814 | background-color: var(--background-color); 815 | border: var(--border-width) solid var(--border-color); 816 | } 817 | 818 | [type="reset"] { 819 | color: var(--danger-text-color); 820 | background-color: var(--danger-color); 821 | border: var(--border-width) solid transparent; 822 | } 823 | 824 | [type="submit"], button:not([type]) { 825 | color: var(--success-text-color); 826 | background-color: var(--success-color); 827 | border: var(--border-width) solid transparent; 828 | } 829 | 830 | input:not([type]):disabled, [type="date"]:disabled, [type="datetime-local"]:disabled, [type="email"]:disabled, [type="hidden"]:disabled, [type="month"]:disabled, [type="number"]:disabled, [type="password"]:disabled, [type="search"]:disabled, [type="tel"]:disabled, [type="text"]:disabled, [type="time"]:disabled, [type="url"]:disabled, [type="week"]:disabled, textarea:disabled, select:disabled { 831 | background-color: var(--highlighted-background-color); 832 | } 833 | 834 | select:enabled, [type="date"]:enabled, [type="datetime-local"]:enabled, [type="time"]:enabled, [type="month"]:enabled, [type="week"]:enabled { 835 | background-color: var(--background-color); 836 | } 837 | 838 | button:focus, [type="button"]:focus, [type="color"]:focus, [type="reset"]:focus, [type="submit"]:focus, button:not([type]):focus, input:not([type]):focus, [type="date"]:focus, [type="datetime-local"]:focus, [type="email"]:focus, [type="hidden"]:focus, [type="image"]:focus, [type="month"]:focus, [type="number"]:focus, [type="password"]:focus, [type="search"]:focus, [type="tel"]:focus, [type="text"]:focus, [type="time"]:focus, [type="url"]:focus, [type="week"]:focus, textarea:focus, select:focus, summary:focus { 839 | border-color: var(--border-hover-color); 840 | outline: none; 841 | box-shadow: 0 0 0.2rem 0.01rem var(--border-hover-color); 842 | } 843 | 844 | [type="image"]:enabled:hover { 845 | filter: brightness(95%); 846 | } 847 | 848 | /* stylelint-disable-next-line selector-max-specificity */ 849 | 850 | button:enabled:hover, [type="button"]:enabled:hover, [type="color"]:enabled:hover, input:not([type]):enabled:hover, [type="date"]:enabled:hover, [type="datetime-local"]:enabled:hover, [type="email"]:enabled:hover, [type="hidden"]:enabled:hover, [type="month"]:enabled:hover, [type="number"]:enabled:hover, [type="password"]:enabled:hover, [type="range"]:enabled:hover, [type="search"]:enabled:hover, [type="tel"]:enabled:hover, [type="text"]:enabled:hover, [type="time"]:enabled:hover, [type="url"]:enabled:hover, [type="week"]:enabled:hover, textarea:enabled:hover, select:enabled:hover, summary:hover { 851 | border-color: var(--border-hover-color); 852 | } 853 | 854 | [type="reset"]:enabled:hover { 855 | background-color: var(--danger-hover-color); 856 | border-color: transparent; 857 | } 858 | 859 | /* stylelint-disable-next-line selector-max-specificity */ 860 | 861 | [type="submit"]:enabled:hover, button:not([type]):enabled:hover { 862 | background-color: var(--success-hover-color); 863 | border-color: transparent; 864 | } 865 | 866 | table { 867 | border-collapse: collapse; 868 | } 869 | 870 | caption { 871 | font-weight: bold; 872 | } 873 | 874 | thead { 875 | border-bottom: calc(var(--border-width) * 2) solid var(--border-color); 876 | } 877 | 878 | tfoot { 879 | border-top: calc(var(--border-width) * 2) solid var(--border-color); 880 | } 881 | 882 | thead>tr:not(:first-child), tbody>tr:not(:first-child), tfoot>tr:not(:first-child) { 883 | border-top: var(--border-width) solid var(--border-color); 884 | } 885 | 886 | /* stylelint-disable-next-line plugin/stylelint-group-selectors */ 887 | 888 | thead>tr:nth-child(even), tbody>tr:nth-child(even), tfoot>tr:nth-child(even) { 889 | background-color: var(--highlighted-background-color); 890 | } 891 | 892 | kbd { 893 | background-color: var(--highlighted-background-color); 894 | border: var(--border-width) solid var(--border-hover-color); 895 | border-radius: var(--border-radius); 896 | box-shadow: inset 0 0 0 0.2rem var(--background-color); 897 | } 898 | 899 | kbd kbd { 900 | border: unset; 901 | border-radius: 0; 902 | box-shadow: inset 0 -0.2rem 0 0 var(--background-color), inset 0 0.2rem 0 0 var(--background-color); 903 | } 904 | 905 | [open] summary { 906 | margin-bottom: 0.5rem; 907 | } 908 | 909 | del { 910 | background-color: var(--danger-text-background-color); 911 | } 912 | 913 | ins { 914 | text-decoration: none; 915 | background-color: var(--success-text-background-color); 916 | } 917 | 918 | blockquote { 919 | background-color: var(--highlighted-background-color); 920 | border-left: 0.5rem solid var(--border-color); 921 | } 922 | 923 | body>footer { 924 | padding-top: 1rem; 925 | border-top: var(--border-width) solid var(--border-color); 926 | } 927 | 928 | hr { 929 | border-color: var(--border-color); 930 | border-style: solid; 931 | border-width: var(--border-width) 0 0; 932 | } 933 | 934 | /* stylelint-disable-next-line selector-max-specificity */ 935 | 936 | :disabled, [type="checkbox"][id]:disabled+[for], [type="radio"][id]:disabled+[for] { 937 | opacity: 0.5; 938 | } 939 | 940 | :invalid:not(form) { 941 | border-color: var(--danger-color); 942 | border-style: solid; 943 | border-width: var(--border-width); 944 | } 945 | 946 | :disabled :disabled { 947 | opacity: unset; 948 | } 949 | 950 | :invalid:not([type="checkbox"]):focus { 951 | border-color: var(--danger-hover-color); 952 | outline: none; 953 | box-shadow: 0 0 0.2rem 0.05rem var(--danger-hover-color); 954 | } 955 | 956 | /* Firefox hack */ 957 | 958 | :invalid:not([type="checkbox"]):not(:focus) { 959 | box-shadow: none; 960 | } 961 | 962 | /* stylelint-disable-next-line selector-max-pseudo-class, selector-max-specificity */ 963 | 964 | :invalid:not(form):enabled:hover { 965 | border-color: var(--danger-hover-color); 966 | } -------------------------------------------------------------------------------- /src/templates/html.ts: -------------------------------------------------------------------------------- 1 | import { html } from "@popeindustries/lit-html-server"; 2 | 3 | export interface HTMLTemplateData { 4 | head?: any, 5 | body?: any 6 | title?: string 7 | } 8 | 9 | // A very basic HTML Template - do change or extend this. 10 | export function htmlDocumentTemplate(data: HTMLTemplateData) { 11 | 12 | return html` 13 | 14 | 15 | 16 | ${data.title ? html`${data.title}` : undefined} 17 | ${data.head} 18 | 19 | 20 | ${data.body} 21 | 22 | ` 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/layout.ts: -------------------------------------------------------------------------------- 1 | import { html } from "@popeindustries/lit-html-server"; 2 | import { HTMLTemplateData } from "./html"; 3 | 4 | /** 5 | * Takes given html template data and puts it in a common layout with a header and footer 6 | */ 7 | export function basicLayout(htmlTemplateData: HTMLTemplateData): HTMLTemplateData { 8 | const newHeaders = html` 9 | 10 | 11 | ${htmlTemplateData.head}` 12 | 13 | const bodyInLayout = html` 14 |
15 |

{{ project-name }}

16 |
17 | 22 | ${htmlTemplateData.body} 23 | ` 29 | 30 | return {...htmlTemplateData, 31 | head: newHeaders, 32 | body: bodyInLayout, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SunderJS/sunder-worker-template/a0be62fecb04757b1796a9c26df8ff2e2c9c62d0/static/favicon-32x32.png -------------------------------------------------------------------------------- /test/app.test.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "../src/app"; 2 | 3 | function createEvent(request: Request): FetchEvent { 4 | // @ts-ignore Sadly the typing seem to be off here, creating a FetchEvent is valid in a test environment. 5 | return new FetchEvent("fetch", {request}); 6 | } 7 | 8 | describe("App integration tests", () => { 9 | 10 | test("Returns 404 with JSON for an unknown route", async () => { 11 | const app = createApp(); 12 | app.silent = true; // We expect an error and it would be logged otherwise 13 | const evt = createEvent(new Request("https://example.com/doesnt-exist")); 14 | const resp = await app.handle(evt); 15 | 16 | expect(resp.status).toEqual(404); 17 | expect(await resp.json()).toHaveProperty("message"); 18 | }); 19 | 20 | test("Renders a homepage with a title", async () => { 21 | const app = createApp(); 22 | const resp = await app.handle(createEvent(new Request("https://example.com/"))); 23 | 24 | expect(resp.status).toEqual(200); 25 | expect(await resp.text()).toMatch(/.*<\/title>/); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/manifest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Content manifest mock file for the `__STATIC_CONTENT_MANIFEST` module which is magically present when using Cloudflare Workers Sites 3 | */ 4 | export default "{}"; -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@cloudflare/workers-types", 6 | "jest" 7 | ], 8 | "noEmit": false, 9 | "outDir": "./build", 10 | }, 11 | "include": [ 12 | "../src/**/*", 13 | "**/*" 14 | ], 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext" 7 | ], 8 | "types": [ 9 | "@cloudflare/workers-types" 10 | ], 11 | "moduleResolution": "node", 12 | "strict": true, 13 | "baseUrl": "./", 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ] 18 | }, 19 | "declaration": true, 20 | "noEmit": true, 21 | "skipLibCheck": true, 22 | "noEmitHelpers": true 23 | }, 24 | "include": [ 25 | "src/**/*", 26 | "bindings.d.ts" 27 | ] 28 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | # Enter your zone id and account id, then rename this file to wrangler.toml. 2 | # Consider removing wrangler.toml from the .gitignore file 3 | compatibility_date = "2021-11-25" 4 | 5 | type = "javascript" 6 | name = "{{ project-name }}" 7 | account_id = "" 8 | workers_dev = true 9 | zone_id = "" 10 | 11 | [site] 12 | entry-point = "./" 13 | bucket = "./dist/static" 14 | 15 | [build] 16 | command = "npm run build:nocleanup" 17 | 18 | [build.upload] 19 | format = "modules" 20 | main = "index.mjs" 21 | --------------------------------------------------------------------------------