Better
This commit is contained in:
1 parent
ea193e1264
commit
2fea308b38
106 files changed
+1234
-878
No files matched your search
@@ -0,0 +1,108 @@
|
||||
// A very small HTTP router, so the worker reads as a set of routes rather than
|
||||
// a chain of ifs unpicking a URL. The shape is lifted from the one in
|
||||
// htmx-serviceworker-todo, which is in turn a miniature of Hono or Express:
|
||||
// register a method and a path pattern, get the matching handler back.
|
||||
//
|
||||
// Patterns are matched segment by segment against a path relative to the
|
||||
// worker's own scope, so the app still works if it is ever served from a
|
||||
// subdirectory. A segment written as :name matches anything and lands in
|
||||
// params. Segment counts must be equal -- there are no wildcards, because
|
||||
// nothing here needs one.
|
||||
//
|
||||
// A handler returning a string gets it as an HTML response; returning a
|
||||
// Response passes it through untouched; returning nothing means "not mine
|
||||
// after all", and the caller falls back to serving the URL as a file.
|
||||
(function (scope) {
|
||||
'use strict';
|
||||
|
||||
function parse(pattern) {
|
||||
return pattern.split('/').map(function (part) {
|
||||
return part.charAt(0) === ':' ? { name: part.slice(1) } : { part: part };
|
||||
});
|
||||
}
|
||||
|
||||
// Everything a handler is given: the event it must attach cache writes to,
|
||||
// the URL, the path parameters, and the two questions every handler here
|
||||
// asks -- who is calling, and are they replacing the whole document.
|
||||
function Call(event, url, params) {
|
||||
this.event = event;
|
||||
this.request = event.request;
|
||||
this.url = url;
|
||||
this.params = params;
|
||||
// htmx stamps every request it makes; a navigation never carries it.
|
||||
this.htmx = this.request.headers.get('HX-Request') === 'true';
|
||||
this.navigate = this.request.mode === 'navigate';
|
||||
}
|
||||
|
||||
Call.prototype.form = function () {
|
||||
return this.request.formData();
|
||||
};
|
||||
|
||||
// base is the worker's scope path, e.g. "/" -- the prefix every pattern is
|
||||
// written without.
|
||||
function Router(base) {
|
||||
this.base = base;
|
||||
this.routes = [];
|
||||
}
|
||||
|
||||
Router.prototype.route = function (method, pattern, handler) {
|
||||
this.routes.push({ method: method, path: parse(pattern), handler: handler });
|
||||
return this;
|
||||
};
|
||||
|
||||
Router.prototype.get = function (pattern, handler) {
|
||||
return this.route('GET', pattern, handler);
|
||||
};
|
||||
|
||||
Router.prototype.post = function (pattern, handler) {
|
||||
return this.route('POST', pattern, handler);
|
||||
};
|
||||
|
||||
// The path a pattern is matched against: absolute, but with the scope
|
||||
// removed, so "/GEN" is "/GEN" whether the app sits at the origin root or
|
||||
// under /bible/.
|
||||
Router.prototype.relative = function (url) {
|
||||
var path = url.pathname;
|
||||
return path.indexOf(this.base) === 0 ? '/' + path.slice(this.base.length) : path;
|
||||
};
|
||||
|
||||
Router.prototype.find = function (method, path) {
|
||||
var segments = path.split('/');
|
||||
for (var i = 0; i < this.routes.length; i++) {
|
||||
var route = this.routes[i];
|
||||
if (route.method !== method || route.path.length !== segments.length) continue;
|
||||
var params = {};
|
||||
var ok = route.path.every(function (segment, n) {
|
||||
if (!segment.name) return segment.part === segments[n];
|
||||
params[segment.name] = decodeURIComponent(segments[n]);
|
||||
return true;
|
||||
});
|
||||
if (ok) return { handler: route.handler, params: params };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Returns a promise of a Response, or null when nothing matched -- null
|
||||
// being the signal to the caller that this URL is a file, not a route.
|
||||
Router.prototype.handle = function (event, url) {
|
||||
var found = this.find(event.request.method, this.relative(url));
|
||||
if (!found) return null;
|
||||
|
||||
return Promise.resolve(found.handler(new Call(event, url, found.params)))
|
||||
.then(function (result) {
|
||||
if (result instanceof Response) return result;
|
||||
if (typeof result !== 'string') return null;
|
||||
return new Response(result, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
// Built here from live state -- the bookmark, the templates, the
|
||||
// cache -- so storing it would pin a stale copy over the real thing.
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
scope.Router = Router;
|
||||
})(self);
|
||||
Reference in new issue
Block a user