├── .gitignore
├── wrangler.toml
├── package.json
├── src
├── hono.logger.js
├── sw.js
├── hono.serve-static.js
└── hono.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | worker
4 | package-lock.json
5 | yarn.lock
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name="service-worker-magic"
2 | compatibility_date = "2022-01-01"
3 |
4 | [vars]
5 | FROM = "Server"
6 |
7 | [build.upload]
8 | format = "service-worker"
9 |
10 | [site]
11 | bucket = "./src"
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "service-worker-magic",
3 | "version": "1.0.0",
4 | "main": "src/sw.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "wrangler dev src/sw.js --log-level none",
8 | "publish": "wrangler publish src/sw.js"
9 | },
10 | "dependencies": {
11 | "hono": "^0.3.7"
12 | },
13 | "devDependencies": {
14 | "wrangler": "^3.14.0"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/hono.logger.js:
--------------------------------------------------------------------------------
1 | var l=/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;var a=t=>{let n=t.match(l);return n?n[5]:""};var b=(t,n)=>{let o=n||{},r=o.delimiter||",",e=o.separator||".";return t=t.toString().split("."),t[0]=t[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g,"$1"+r),t.join(e)},u=t=>{let n=Date.now()-t;return b([n<1e4?n+"ms":Math.round(n/1e3)+"s"])},c={Outgoing:"-->",Incoming:"<--",Error:"xxx"},$=(t=0)=>({7:`\x1B[35m${t}\x1B[0m`,5:`\x1B[31m${t}\x1B[0m`,4:`\x1B[33m${t}\x1B[0m`,3:`\x1B[36m${t}\x1B[0m`,2:`\x1B[32m${t}\x1B[0m`,1:`\x1B[32m${t}\x1B[0m`,0:`\x1B[33m${t}\x1B[0m`})[t/100|0];function m(t,n,o,r,e,i,s){let g=n===c.Incoming?` ${n} ${o} ${r}`:` ${n} ${o} ${r} ${$(e)} ${i} ${s}`;t(g)}var d=(t=console.log)=>async(n,o)=>{let{method:r}=n.req,e=a(n.req.url);m(t,c.Incoming,r,e);let i=Date.now();try{await o()}catch(x){throw m(t,c.Error,r,e,n.res.status||500,u(i)),x}let s=parseFloat(n.res.headers.get("Content-Length")),g=isNaN(s)?"0":s<1024?`${s}b`:`${s/1024}kB`;m(t,c.Outgoing,r,e,n.res.status,u(i),g)};export{d as logger};
2 |
--------------------------------------------------------------------------------
/src/sw.js:
--------------------------------------------------------------------------------
1 | import { Hono } from './hono.js'
2 | import { serveStatic } from './hono.serve-static.js'
3 | import { logger } from './hono.logger.js'
4 |
5 | let from
6 |
7 | try {
8 | from = FROM // "Server" is set on Cloudflare Workers environment variables
9 | } catch {
10 | from = 'Service Worker'
11 | }
12 |
13 | const app = new Hono()
14 |
15 | // Middleware
16 | app.use('/sw/*', logger())
17 | app.use('/server/*', logger())
18 | app.use('/:name{.+.js}', serveStatic({ root: './' }))
19 |
20 | const script = ``
39 |
40 | const header = `
41 |
42 |
43 |
44 |
45 | `
47 |
48 | const footer = `
49 |
50 | `
51 |
52 | // Top page
53 | app.get('/', (c) => {
54 | const html = `${header}
55 | ${script}
56 | This is ${new URL(c.req.url).pathname}
57 | Hello! from ${from}!
58 | Registering Service Worker...
59 | ${footer}
60 | `
61 | return c.html(html)
62 | })
63 |
64 | // Handler
65 | const handler = (c) => {
66 | const html = `${header}
67 | This is ${new URL(c.req.url).pathname}
68 | Hello! from ${from}!
69 | ${footer}
70 | `
71 | return c.html(html)
72 | }
73 |
74 | // Route
75 | app.get('/server/hello', handler)
76 | app.get('/sw/hello', handler)
77 |
78 | // addEventListener('fetch'...
79 | app.fire()
80 |
--------------------------------------------------------------------------------
/src/hono.serve-static.js:
--------------------------------------------------------------------------------
1 | var s=async a=>{let t;typeof __STATIC_CONTENT_MANIFEST=="string"?t=JSON.parse(__STATIC_CONTENT_MANIFEST):t=__STATIC_CONTENT_MANIFEST;let e=__STATIC_CONTENT,i=t[a]||a;if(!i)return;let o=await e.get(i,{type:"arrayBuffer"});return o&&(o=o),o},c=a=>{let t=a.filename,e=a.root||"",i=a.defaultDocument||"index.html";t.endsWith("/")?t=t.concat(i):t.match(/\.[a-zA-Z0-9]+$/)||(t=t.concat("/"+i)),t=t.replace(/^\//,""),e=e.replace(/\/$/,"");let o=e?e+"/"+t:t;return o=o.replace(/^\.?\//,""),o};var r=a=>{let t=/\.([a-zA-Z0-9]+?)$/,e=a.match(t);if(!e)return;let i=l[e[1]];return(i.startsWith("text")||i==="application/json")&&(i+="; charset=utf-8"),i},l={aac:"audio/aac",abw:"application/x-abiword",arc:"application/x-freearc",avi:"video/x-msvideo",azw:"application/vnd.amazon.ebook",bin:"application/octet-stream",bmp:"image/bmp",bz:"application/x-bzip",bz2:"application/x-bzip2",csh:"application/x-csh",css:"text/css",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",eot:"application/vnd.ms-fontobject",epub:"application/epub+zip",gz:"application/gzip",gif:"image/gif",htm:"text/html",html:"text/html",ico:"image/vnd.microsoft.icon",ics:"text/calendar",jar:"application/java-archive",jpeg:"image/jpeg",jpg:"image/jpeg",js:"text/javascript",json:"application/json",jsonld:"application/ld+json",mid:"audio/x-midi",midi:"audio/x-midi",mjs:"text/javascript",mp3:"audio/mpeg",mpeg:"video/mpeg",mpkg:"application/vnd.apple.installer+xml",odp:"application/vnd.oasis.opendocument.presentation",ods:"application/vnd.oasis.opendocument.spreadsheet",odt:"application/vnd.oasis.opendocument.text",oga:"audio/ogg",ogv:"video/ogg",ogx:"application/ogg",opus:"audio/opus",otf:"font/otf",png:"image/png",pdf:"application/pdf",php:"application/php",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",rar:"application/vnd.rar",rtf:"application/rtf",sh:"application/x-sh",svg:"image/svg+xml",swf:"application/x-shockwave-flash",tar:"application/x-tar",tif:"image/tiff",tiff:"image/tiff",ts:"video/mp2t",ttf:"font/ttf",txt:"text/plain",vsd:"application/vnd.visio",wav:"audio/wav",weba:"audio/webm",webm:"video/webm",webp:"image/webp",woff:"font/woff",woff2:"font/woff2",xhtml:"application/xhtml+xml",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xml:"application/xml",xul:"application/vnd.mozilla.xul+xml",zip:"application/zip","3gp":"video/3gpp","3g2":"video/3gpp2","7z":"application/x-7z-compressed"};var m="index.html",u=(a={root:""})=>async(t,e)=>{await e();let i=new URL(t.req.url),o=c({filename:i.pathname,root:a.root,defaultDocument:m}),n=await s(o);if(n){let p=r(o);p&&t.header("Content-Type",p),t.res=t.body(n)}};export{u as serveStatic};
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Service Worker Magic
2 |
3 | - Server (Cloudflare Workers) code is [`sw.js`](./src/sw.js).
4 | - Browser ( Service Worker ) code is [`sw.js`](./src/sw.js).
5 | - Cloudflare Workers [`sw.js`](./src/sw.js) serves [`sw.js`](./src/sw.js).
6 | - Service Worker [`sw.js`](./src/sw.js) will be registered on `/`. The scope is `/sw/*`.
7 | - `/server/hello` => from the server.
8 | - `/sw/hello` => from the browser not from a server. Request is intercepted by Service Worker.
9 |
10 | It's magic.
11 |
12 | ## Demo
13 |
14 | -
15 |
16 | ## Screencast
17 |
18 | 
19 |
20 | ## Walkthrough
21 |
22 | ### Server
23 |
24 | Run Cloudflare Workers on your terminal:
25 |
26 | ```sh
27 | $ wrangler dev sw.js
28 | ```
29 |
30 | `sw.js` is served by `sw.js`.
31 |
32 | ### Browser
33 |
34 | Access `/`. Your browser will load `sw.js`:
35 |
36 | ```js
37 | navigator.serviceWorker.register('/sw.js', { scope: '/sw/', type: 'module' })
38 | ```
39 |
40 | Service Worker is registered.
41 |
42 | ### Then...
43 |
44 | - `/server/hello` => content is returned from the server.
45 | - `/sw/hello` => content is returned from the browser. Not from the server.
46 |
47 | ### Attention
48 |
49 | If Service Worker does not work, clear the cache on your browser.
50 |
51 | ## Code
52 |
53 | Just [`sw.js`](./src/sw.js).
54 |
55 | ### Short version
56 |
57 | It does not work well.
58 |
59 | ```js
60 | import { Hono } from './hono.js'
61 | import { serveStatic } from './hono.serve-static.js'
62 | import { logger } from './hono.logger.js'
63 |
64 | let from = 'Service Worker'
65 |
66 | try {
67 | from = FROM // "Server" is set on Cloudflare Workers environment variables
68 | }
69 |
70 | const app = new Hono()
71 |
72 | // Middleware
73 | app.use('/sw/*', logger())
74 | app.use('/server/*', logger())
75 | app.use('/:name{.+.js}', serveStatic({ root: './' }))
76 |
77 |
78 | // Top page
79 | app.get('/', (c) => {
80 | const html = `
81 |
84 | `
85 | return c.html(html)
86 | })
87 |
88 | // Handler
89 | const handler = (c) => {
90 | const text = `Hello! from ${from}!`
91 | return c.text(text)
92 | }
93 |
94 | // Route
95 | app.get('/server/hello', handler)
96 | app.get('/sw/hello', handler)
97 |
98 | // addEventListener('fetch'...
99 | app.fire()
100 | ```
101 |
102 | ## Related projects
103 |
104 | `sw.js` is using Hono as Service Worker framework.
105 |
106 | - Hono\[炎\]
107 |
108 | ## Author
109 |
110 | Yusuke Wada
111 |
112 | ## License
113 |
114 | MIT
115 |
--------------------------------------------------------------------------------
/src/hono.js:
--------------------------------------------------------------------------------
1 | var A=Object.defineProperty,O=Object.defineProperties;var I=Object.getOwnPropertyDescriptors;var E=Object.getOwnPropertySymbols;var B=Object.prototype.hasOwnProperty,D=Object.prototype.propertyIsEnumerable;var v=(n,e,t)=>e in n?A(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,f=(n,e)=>{for(var t in e||(e={}))B.call(e,t)&&v(n,t,e[t]);if(E)for(var t of E(e))D.call(e,t)&&v(n,t,e[t]);return n},P=(n,e)=>O(n,I(e));var F=/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/,w=n=>{let e=n.split(/\//);return e[0]===""&&e.shift(),e},L=n=>{let e=n.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);return e?e[2]?[e[1],"("+e[2]+")"]:[e[1],"(.+)"]:null},N=n=>{let e=n.match(F);return e?e[5]:""},_=n=>{let e=n.match(F);return!!(e&&e[1])};var G="ALL",k=class{constructor(e,t){this.handler=e,this.params=t}},H=()=>null,g=class{constructor(e,t,r){this.children=r||{},this.method={},e&&t&&(this.method[e]=t),this.middlewares=[]}insert(e,t,r){let s=this,u=w(t);for(let o=0,a=u.length;o{let e=[];return function(t,r){let s=-1;return u(0);async function u(o){if(o<=s)return Promise.reject(new Error("next() called multiple times"));s=o;let a=n[o];if(o===n.length&&(a=r),!a)return Promise.resolve();try{return Promise.resolve(a(t,u.bind(null,o+1))).catch(i=>{throw e.push(i),e[0]})}catch(i){return Promise.reject(i)}}}};var M=n=>W[n],W={200:"OK",201:"Created",202:"Accepted",204:"No Content",206:"Partial Content",301:"Moved Permanently",302:"Moved Temporarily",303:"See Other",304:"Not Modified",307:"Temporary Redirect",308:"Permanent Redirect",400:"Bad Request",401:"Unauthorized",402:"Payment Required",403:"Forbidden",404:"Not Found",405:"Not Allowed",406:"Not Acceptable",408:"Request Time-out",409:"Conflict",410:"Gone",411:"Length Required",412:"Precondition Failed",413:"Request Entity Too Large",414:"Request-URI Too Large",415:"Unsupported Media Type",416:"Requested Range Not Satisfiable",421:"Misdirected Request",429:"Too Many Requests",500:"Internal Server Error",501:"Not Implemented",502:"Bad Gateway",503:"Service Temporarily Unavailable",504:"Gateway Time-out",505:"HTTP Version Not Supported",507:"Insufficient Storage"};var R=class{constructor(e,t){this.req=e,t&&(this.res=t.res,this.env=t.env,this.event=t.event),this._headers={}}header(e,t){this.res&&this.res.headers.set(e,t),this._headers[e]=t}status(e){if(this.res){console.warn("c.res.status is already set.");return}this._status=e,this._statusText=M(e)}newResponse(e,t={}){t.status=t.status||this._status,t.statusText=t.statusText||this._statusText,t.headers=f(f({},this._headers),t.headers);let r=0;return e&&(e instanceof ArrayBuffer?r=e.byteLength:typeof e=="string"&&(r=new TextEncoder().encode(e).byteLength||0)),t.headers=P(f({},t.headers),{"Content-Length":r.toString()}),new Response(e,t)}body(e,t=this._status,r=this._headers){return this.newResponse(e,{status:t,headers:r})}text(e,t=this._status,r={}){if(typeof e!="string")throw new TypeError("text method arg must be a string!");return r["Content-Type"]||(r["Content-Type"]="text/plain; charset=UTF-8"),this.body(e,t,r)}json(e,t=this._status,r={}){if(typeof e!="object")throw new TypeError("json method arg must be a object!");let s=JSON.stringify(e);return r["Content-Type"]||(r["Content-Type"]="application/json; charset=UTF-8"),this.body(s,t,r)}html(e,t=this._status,r={}){if(typeof e!="string")throw new TypeError("html method arg must be a string!");return r["Content-Type"]||(r["Content-Type"]="text/html; charset=UTF-8"),this.body(e,t,r)}redirect(e,t=302){if(typeof e!="string")throw new TypeError("location must be a string!");if(!_(e)){let r=new URL(this.req.url);r.pathname=e,e=r.toString()}return this.newResponse(null,{status:t,headers:{Location:e}})}};var q="ALL",b=class{constructor(){this.node=new g}add(e,t,r){this.node.insert(e,t,r)}match(e,t){return this.node.search(e,t)}},S=class{constructor(){this.router=new b,this.middlewareRouters=[],this.tempPath="/"}get(e,...t){return this.addRoute("get",e,...t)}post(e,...t){return this.addRoute("post",e,...t)}put(e,...t){return this.addRoute("put",e,...t)}head(e,...t){return this.addRoute("head",e,...t)}delete(e,...t){return this.addRoute("delete",e,...t)}options(e,...t){return this.addRoute("options",e,...t)}patch(e,...t){return this.addRoute("patch",e,...t)}all(e,...t){return this.addRoute("all",e,...t)}route(e){return this.tempPath=e,this}use(e,t){if(t.constructor.name!=="AsyncFunction")throw new TypeError("middleware must be a async function!");let r=new b;r.add(q,e,t),this.middlewareRouters.push(r)}addRoute(e,t,...r){return e=e.toUpperCase(),typeof t=="string"?(this.tempPath=t,this.router.add(e,t,r)):(r.unshift(t),this.router.add(e,this.tempPath,r)),this}async matchRoute(e,t){return this.router.match(e,t)}async dispatch(e,t,r){let[s,u]=[e.method,N(e.url)],o=await this.matchRoute(s,u);e.param=d=>{if(o)return o.params[d]},e.header=d=>e.headers.get(d),e.query=d=>new URL(l.req.url).searchParams.get(d);let a=o?o.handler[0]:this.notFound,i=[];for(let d of this.middlewareRouters){let h=d.match(q,u);h&&i.push(h.handler)}let p=async(d,h)=>{let c=await a(d);if(!(c instanceof Response))throw new TypeError("response must be a instace of Response");d.res=c,await h()};i.push(p);let m=C(i),l=new R(e,{env:t,event:r,res:null});return await m(l),l.res}async handleEvent(e){return this.dispatch(e.request,{},e).catch(t=>this.onError(t))}async fetch(e,t,r){return this.dispatch(e,t,r).catch(s=>this.onError(s))}fire(){addEventListener("fetch",e=>{e.respondWith(this.handleEvent(e))})}onError(e){console.error(`${e}`);let t="Internal Server Error";return new Response(t,{status:500,headers:{"Content-Length":t.length.toString()}})}notFound(){let e="Not Found";return new Response(e,{status:404,headers:{"Content-Length":e.length.toString()}})}};export{R as Context,S as Hono};
2 |
--------------------------------------------------------------------------------