@jon49/sw 0.13.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/htmf.middleware.ts +37 -0
- package/lib/response.middleware.ts +60 -0
- package/lib/{routes.ts → routes.middleware.ts} +48 -115
- package/lib/sw-framework.ts +53 -0
- package/lib/utils.ts +6 -0
- package/package.json +5 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SwResponse } from "./sw-framework.js"
|
|
2
|
+
|
|
3
|
+
function isHtmf(req: Request) {
|
|
4
|
+
return req.headers.has("HF-Request")
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function htmfHeader(events: any = {}, messages: string[] = []) : Record<string, string> {
|
|
8
|
+
let userMessages =
|
|
9
|
+
messages?.length > 0
|
|
10
|
+
? { "user-messages": messages }
|
|
11
|
+
: null
|
|
12
|
+
return {
|
|
13
|
+
"hf-events": JSON.stringify({
|
|
14
|
+
...userMessages,
|
|
15
|
+
...(events || {})
|
|
16
|
+
}) ?? "{}"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useHtmf(req: Request, res: SwResponse, ctx: any): void {
|
|
21
|
+
if (!isHtmf(req)) return
|
|
22
|
+
|
|
23
|
+
let messages = ctx.messages || []
|
|
24
|
+
if (res.error) {
|
|
25
|
+
messages.push(res.error)
|
|
26
|
+
res.error = undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let headers = htmfHeader(ctx.events, ctx.messages)
|
|
30
|
+
|
|
31
|
+
Object.assign(res, {
|
|
32
|
+
headers: {
|
|
33
|
+
...res.headers,
|
|
34
|
+
...headers,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isHtml } from "./utils.js"
|
|
2
|
+
|
|
3
|
+
const encoder = new TextEncoder()
|
|
4
|
+
function streamResponse(response: { body: Generator, headers?: any }): Response {
|
|
5
|
+
let { body, headers } = response
|
|
6
|
+
const stream = new ReadableStream({
|
|
7
|
+
async start(controller: ReadableStreamDefaultController<any>) {
|
|
8
|
+
try {
|
|
9
|
+
for await (let x of body) {
|
|
10
|
+
if (typeof x === "string")
|
|
11
|
+
controller.enqueue(encoder.encode(x))
|
|
12
|
+
}
|
|
13
|
+
controller.close()
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return new Response(stream, {
|
|
21
|
+
headers: {
|
|
22
|
+
"content-type": "text/html; charset=utf-8",
|
|
23
|
+
...headers,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useResponse(_: Request, res: any, __: any) : void {
|
|
29
|
+
if (isHtml(res.body)) {
|
|
30
|
+
Object.assign(res, { response: streamResponse(res) })
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (res.error) {
|
|
35
|
+
Object.assign(res, {
|
|
36
|
+
response: new Response(res.error, {
|
|
37
|
+
status: 500,
|
|
38
|
+
headers: {
|
|
39
|
+
"content-type": "text/plain; charset=utf-8",
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (res.response) {
|
|
46
|
+
return res.response
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (res.body) {
|
|
50
|
+
Object.assign(res, {
|
|
51
|
+
response: new Response(res.body, {
|
|
52
|
+
status: res.status || 200,
|
|
53
|
+
headers: {
|
|
54
|
+
"content-type": res.type || "text/plain; charset=utf-8",
|
|
55
|
+
...res.headers,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { SwResponse } from "./sw-framework.js"
|
|
2
|
+
import { isHtml } from "./utils.js"
|
|
3
|
+
|
|
1
4
|
let { links, globalDb } =
|
|
2
5
|
// @ts-ignore
|
|
3
6
|
self.app as { links: { file: string, url: string }[], html: Function, db: any, globalDb: any }
|
|
@@ -29,48 +32,18 @@ interface ResponseOptions {
|
|
|
29
32
|
|
|
30
33
|
export let options: ResponseOptions = {}
|
|
31
34
|
|
|
32
|
-
// Test if value is Async Generator
|
|
33
|
-
let isHtml = (value: any) =>
|
|
34
|
-
value?.next instanceof Function
|
|
35
|
-
&& value.throw instanceof Function
|
|
36
|
-
|
|
37
35
|
// @ts-ignore
|
|
38
|
-
export async function
|
|
36
|
+
export async function useRoutes(req: Request, res: SwResponse, ctx: any): Promise<void> {
|
|
39
37
|
try {
|
|
40
|
-
const req: Request = event.request
|
|
41
38
|
const url = normalizeUrl(req.url)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
if (!url.pathname.startsWith("/web/")) {
|
|
40
|
+
res.response = await fetch(req)
|
|
41
|
+
} else {
|
|
42
|
+
Object.assign(res, await executeHandler({ url, req, ctx }))
|
|
43
|
+
}
|
|
46
44
|
} catch (error) {
|
|
47
45
|
console.error("Get Response Error", error)
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function getErrors(errors: any): string[] {
|
|
53
|
-
return typeof errors === "string"
|
|
54
|
-
? [errors]
|
|
55
|
-
: call(options.handleErrors, errors) ?? []
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isHtmf(req: Request) {
|
|
59
|
-
return req.headers.has("HF-Request")
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function htmfHeader(req: Request, events: any = {}, messages: string[] = [])
|
|
63
|
-
: Record<string, string> {
|
|
64
|
-
if (!isHtmf(req)) return {}
|
|
65
|
-
let userMessages =
|
|
66
|
-
messages?.length > 0
|
|
67
|
-
? { "user-messages": messages }
|
|
68
|
-
: null
|
|
69
|
-
return {
|
|
70
|
-
"hf-events": JSON.stringify({
|
|
71
|
-
...userMessages,
|
|
72
|
-
...(events || {})
|
|
73
|
-
}) ?? "{}"
|
|
46
|
+
res.error = "Oops something happened which shouldn't have!"
|
|
74
47
|
}
|
|
75
48
|
}
|
|
76
49
|
|
|
@@ -139,15 +112,14 @@ export async function findRoute(url: URL, method: unknown) {
|
|
|
139
112
|
interface ExectuteHandlerOptions {
|
|
140
113
|
url: URL
|
|
141
114
|
req: Request
|
|
142
|
-
|
|
143
|
-
event: FetchEvent
|
|
115
|
+
ctx: any
|
|
144
116
|
}
|
|
145
|
-
async function executeHandler({ url, req,
|
|
117
|
+
async function executeHandler({ url, req, ctx }: ExectuteHandlerOptions): Promise<SwResponse> {
|
|
146
118
|
let method = req.method.toLowerCase()
|
|
147
119
|
let isPost = method === "post"
|
|
148
120
|
if (!isPost) {
|
|
149
121
|
if (!url.pathname.endsWith("/")) {
|
|
150
|
-
return cacheResponse(url.pathname,
|
|
122
|
+
return { response: await cacheResponse(url.pathname, req) }
|
|
151
123
|
}
|
|
152
124
|
|
|
153
125
|
if (url.searchParams.get("login") === "success") {
|
|
@@ -175,71 +147,57 @@ async function executeHandler({ url, req, event }: ExectuteHandlerOptions): Prom
|
|
|
175
147
|
|
|
176
148
|
if (!result) {
|
|
177
149
|
return isPost
|
|
178
|
-
? redirect(req)
|
|
179
|
-
:
|
|
150
|
+
? { response: redirect(req) }
|
|
151
|
+
: { error: "Not found!" }
|
|
180
152
|
}
|
|
181
153
|
|
|
182
154
|
if (isPost && result.message == null) {
|
|
183
155
|
messages.push("Saved!")
|
|
184
156
|
}
|
|
185
157
|
|
|
186
|
-
if (isHtml(result)) {
|
|
187
|
-
return streamResponse({
|
|
188
|
-
body: result,
|
|
189
|
-
headers: htmfHeader(req, null, messages)
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
|
|
193
158
|
if (result.message?.length > 0) {
|
|
194
159
|
messages.push(result.message)
|
|
195
160
|
} else if (result.messages?.length > 0) {
|
|
196
161
|
messages.push(...result.messages)
|
|
197
162
|
}
|
|
163
|
+
ctx.messages = messages
|
|
164
|
+
ctx.events = result.events
|
|
198
165
|
|
|
199
|
-
result
|
|
200
|
-
|
|
201
|
-
|
|
166
|
+
if (isHtml(result)) {
|
|
167
|
+
return {
|
|
168
|
+
body: result,
|
|
169
|
+
}
|
|
202
170
|
}
|
|
203
171
|
|
|
204
172
|
if (isHtml(result.body)) {
|
|
205
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
...result,
|
|
175
|
+
type: "text/html",
|
|
176
|
+
}
|
|
206
177
|
} else {
|
|
207
178
|
if ("json" in result) {
|
|
208
179
|
result.body = JSON.stringify(result.json)
|
|
209
180
|
result.headers = {
|
|
210
181
|
...result.headers,
|
|
211
|
-
"content-type": "application/json"
|
|
212
182
|
}
|
|
213
183
|
}
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
184
|
+
return {
|
|
185
|
+
...result,
|
|
186
|
+
type: "application/json"
|
|
187
|
+
}
|
|
218
188
|
}
|
|
219
189
|
|
|
220
190
|
} catch (error) {
|
|
221
191
|
console.error(`"${method}" error:`, error, "\nURL:", url);
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
return redirect(req)
|
|
225
|
-
} else {
|
|
226
|
-
return new Response("Not Found!", { status: 404 })
|
|
227
|
-
}
|
|
192
|
+
if (isPost) {
|
|
193
|
+
return { error: "An error occurred while processing your request." }
|
|
228
194
|
} else {
|
|
229
|
-
|
|
230
|
-
let headers = htmfHeader(req, {}, errors)
|
|
231
|
-
return new Response(null, {
|
|
232
|
-
status: isPost ? 400 : 500,
|
|
233
|
-
headers
|
|
234
|
-
})
|
|
195
|
+
return { error: "Not found!" }
|
|
235
196
|
}
|
|
236
197
|
}
|
|
237
198
|
}
|
|
238
199
|
|
|
239
|
-
return
|
|
240
|
-
status: 404,
|
|
241
|
-
headers: htmfHeader(req, {}, ["Not Found!"])
|
|
242
|
-
})
|
|
200
|
+
return { error: "Not Found!" }
|
|
243
201
|
}
|
|
244
202
|
|
|
245
203
|
async function getData(req: Request) {
|
|
@@ -264,46 +222,6 @@ async function getData(req: Request) {
|
|
|
264
222
|
return o
|
|
265
223
|
}
|
|
266
224
|
|
|
267
|
-
async function cacheResponse(url: string, event?: { request: string | Request } | undefined): Promise<Response> {
|
|
268
|
-
url = links?.find(x => x.url === url)?.file || url
|
|
269
|
-
const match = await caches.match(url)
|
|
270
|
-
if (match) return match
|
|
271
|
-
const res = await fetch(event?.request || url)
|
|
272
|
-
if (!res || res.status !== 200 || res.type !== "basic") return res
|
|
273
|
-
const responseToCache = res.clone()
|
|
274
|
-
// @ts-ignore
|
|
275
|
-
let version: string = self.app?.version
|
|
276
|
-
?? (console.warn("The version number is not available, expected glboal value `self.app.version`."), "")
|
|
277
|
-
const cache = await caches.open(version)
|
|
278
|
-
cache.put(url, responseToCache)
|
|
279
|
-
return res
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const encoder = new TextEncoder()
|
|
283
|
-
function streamResponse(response: { body: Generator, headers?: any }): Response {
|
|
284
|
-
let { body, headers } = response
|
|
285
|
-
const stream = new ReadableStream({
|
|
286
|
-
async start(controller: ReadableStreamDefaultController<any>) {
|
|
287
|
-
try {
|
|
288
|
-
for await (let x of body) {
|
|
289
|
-
if (typeof x === "string")
|
|
290
|
-
controller.enqueue(encoder.encode(x))
|
|
291
|
-
}
|
|
292
|
-
controller.close()
|
|
293
|
-
} catch (error) {
|
|
294
|
-
console.error(error)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
return new Response(stream, {
|
|
300
|
-
headers: {
|
|
301
|
-
"content-type": "text/html; charset=utf-8",
|
|
302
|
-
...headers,
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
225
|
/**
|
|
308
226
|
* /my/url -> /my/url/
|
|
309
227
|
* /my/script.js -> /my/script.js
|
|
@@ -319,6 +237,21 @@ function isFile(s: string) {
|
|
|
319
237
|
return s.lastIndexOf("/") < s.lastIndexOf(".")
|
|
320
238
|
}
|
|
321
239
|
|
|
240
|
+
async function cacheResponse(url: string, req?: Request | undefined): Promise<Response> {
|
|
241
|
+
url = links?.find(x => x.url === url)?.file || url
|
|
242
|
+
const match = await caches.match(url)
|
|
243
|
+
if (match) return match
|
|
244
|
+
const res = await fetch(req || url)
|
|
245
|
+
if (!res || res.status !== 200 || res.type !== "basic") return res
|
|
246
|
+
const responseToCache = res.clone()
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
let version: string = self.app?.version
|
|
249
|
+
?? (console.warn("The version number is not available, expected glboal value `self.app.version`."), "")
|
|
250
|
+
const cache = await caches.open(version)
|
|
251
|
+
cache.put(url, responseToCache)
|
|
252
|
+
return res
|
|
253
|
+
}
|
|
254
|
+
|
|
322
255
|
export interface RouteGetArgs {
|
|
323
256
|
req: Request
|
|
324
257
|
query: any
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class SWFramework {
|
|
2
|
+
private middlewares: Middleware[];
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
this.middlewares = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
use(middleware: Middleware) {
|
|
9
|
+
if (typeof middleware !== 'function') {
|
|
10
|
+
throw new Error('Middleware must be a function');
|
|
11
|
+
}
|
|
12
|
+
this.middlewares.push(middleware);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start(fetchEvent: FetchEvent) : Promise<Response> {
|
|
16
|
+
let ctx = {} // Context object to pass to middlewares
|
|
17
|
+
let res: SwResponse = {}
|
|
18
|
+
let req = fetchEvent.request
|
|
19
|
+
for (let middleware of this.middlewares) {
|
|
20
|
+
let result = middleware(req, res, ctx)
|
|
21
|
+
if (result instanceof Promise) {
|
|
22
|
+
result = await result;
|
|
23
|
+
}
|
|
24
|
+
if (result === false) {
|
|
25
|
+
// Stop processing middlewares if one returns false
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return res.response ?? new Response(res.error || "Not Found", {
|
|
30
|
+
status: res.status || 404,
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": res.type || "text/plain; charset=utf-8",
|
|
33
|
+
...res.headers,
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export let swFramework = new SWFramework();
|
|
40
|
+
|
|
41
|
+
export type SwResponse = {
|
|
42
|
+
status?: number
|
|
43
|
+
headers?: { [key: string]: string }
|
|
44
|
+
response?: Response
|
|
45
|
+
error?: string
|
|
46
|
+
} & (
|
|
47
|
+
| { body?: never; type?: never }
|
|
48
|
+
| { body: any; type: "text/plain" | "application/json" | "text/html" | "application/xml" }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
interface Middleware {
|
|
52
|
+
(req: Request, res: SwResponse, ctx: any): void | Promise<void | false> | false
|
|
53
|
+
}
|
package/lib/utils.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jon49/sw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Packages for MVC service workers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"bin"
|
|
9
9
|
],
|
|
10
10
|
"exports": {
|
|
11
|
-
"./routes.js": "./lib/routes.ts",
|
|
11
|
+
"./routes.middleware.js": "./lib/routes.middleware.ts",
|
|
12
|
+
"./htmf.middleware.js": "./lib/htmf.middleware.ts",
|
|
13
|
+
"./response.middleware.js": "./lib/response.middleware.ts",
|
|
14
|
+
"./web-framework.js": "./lib/sw-framework.ts",
|
|
12
15
|
"./new-app-notifier.js": "./lib/new-app-notifier.ts",
|
|
13
16
|
"./validation.js": "./lib/validation.ts",
|
|
14
17
|
"./utils.js": "./lib/utils.ts",
|