• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fastrodev / fastro / 6039904094

31 Aug 2023 04:20PM UTC coverage: 88.78% (-0.07%) from 88.852%
6039904094

push

github

web-flow
Merge pull request #264 from fastrodev/dev

update modular file extension

129 of 154 branches covered (0.0%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

955 of 1067 relevant lines covered (89.5%)

22.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.31
/http/server.ts
1
// deno-lint-ignore-file no-explicit-any
1✔
2
import { Esbuild } from "../build/esbuild.ts";
1✔
3
import { EsbuildMod } from "../build/esbuildMod.ts";
1✔
4
import {
1✔
5
  addSalt,
1✔
6
  exportCryptoKey,
1✔
7
  keyPromise,
1✔
8
  reverseString,
1✔
9
  SALT,
1✔
10
} from "../crypto/key.ts";
1✔
11

12
import {
1✔
13
  ConnInfo,
14
  contentType,
1✔
15
  extname,
1✔
16
  Handler,
17
  JSX,
18
  renderToString,
1✔
19
  Status,
1✔
20
  STATUS_TEXT,
1✔
21
  toHashString,
1✔
22
} from "./deps.ts";
1✔
23

24
import { Render } from "./render.tsx";
1✔
25
import { version } from "./version.ts";
1✔
26

27
type ServerHandler = Deno.ServeHandler | Handler;
28

29
type HandlerArgument =
30
  | ServerHandler
31
  | RequestHandler
32
  | MiddlewareArgument;
33

34
export interface Next {
35
  (error?: Error, data?: unknown): unknown;
36
}
37

38
export type Hook = (
39
  server: Fastro,
40
  request: Request,
41
  info: Info,
42
) => Response | Promise<Response>;
43
export class HttpRequest extends Request {
×
44
  record!: Record<string, any>;
×
45
  match?: URLPatternResult | null;
×
46
  params?: Record<string, string | undefined>;
×
47
  query?: Record<string, string>;
×
48
  [key: string]: any;
49
}
×
50

51
export type Info = Deno.ServeHandlerInfo | ConnInfo;
52

53
type Meta = {
54
  name?: string;
55
  content?: string;
56
  charset?: string;
57
  property?: string;
58
  itemprop?: string;
59
};
60

61
type Script = {
62
  type?: string;
63
  src?: string;
64
  crossorigin?: "anonymous" | "use-credentials" | "" | undefined;
65
  nonce?: string;
66
  integrity?: string;
67
};
68

69
type Link = {
70
  href?: string;
71
  as?: string;
72
  onload?: any;
73
  rel?: string;
74
  integrity?: string;
75
  media?: string;
76
  crossorigin?: "anonymous" | "use-credentials" | "" | undefined;
77
};
78

79
type NoScriptLink = {
80
  rel?: string;
81
  href?: string;
82
};
83

84
type ModuleFunction = (f: Fastro) => Fastro;
85

86
export type RenderOptions = {
87
  build?: boolean;
88
  cache?: boolean;
89
  pageFolder?: string;
90
  status?: number;
91
  props?: any;
92
  development?: boolean;
93
  html?: {
94
    lang?: string;
95
    class?: string;
96
    style?: JSX.CSSProperties;
97
    head?: {
98
      title?: string;
99
      descriptions?: string;
100
      meta?: Meta[];
101
      script?: Script[];
102
      link?: Link[];
103
      headStyle?: string;
104
      headScript?: string;
105
      noScriptLink?: NoScriptLink;
106
    };
107
    body?: {
108
      class?: string;
109
      style?: JSX.CSSProperties;
110
      script?: Script[];
111
      root: {
112
        class?: string;
113
        style?: JSX.CSSProperties;
114
      };
115
    };
116
  };
117
};
118

119
export const hydrateFolder = ".hydrate";
1✔
120

121
export class Context {
1✔
122
  server!: Fastro;
51✔
123
  info!: Info;
51✔
124
  [key: string]: any;
125
  render!: (options?: RenderOptions) => Response | Promise<Response>;
51✔
126
  send!: (data: unknown, status?: number) => Response | Promise<Response>;
51✔
127
}
51✔
128

129
type RequestHandler = (
130
  request: HttpRequest,
131
  ctx: Context,
132
) =>
133
  | Response
134
  | Promise<Response>;
135

136
type MiddlewareArgument = (
137
  request: HttpRequest,
138
  ctx: Context,
139
  next: Next,
140
) =>
141
  | Promise<unknown>
142
  | unknown;
143

144
type RouteNest = {
145
  handler: HandlerArgument;
146
  method?: string;
147
  pathname?: string;
148
  params?: Record<string, string | undefined>;
149
  query?: Record<string, string>;
150
  match?: URLPatternResult;
151
  element?: Component;
152
  handlers?: Array<MiddlewareArgument>;
153
};
154

155
type Route = {
156
  method: string;
157
  path: string;
158
  handler: HandlerArgument;
159
};
160

161
type Middleware = {
162
  method?: string;
163
  path?: string;
164
  handler: MiddlewareArgument;
165
};
166

167
export type PageComponent = {
168
  component: FunctionComponent | JSX.Element;
169
  folder: string;
170
};
171

172
export type Component = FunctionComponent | JSX.Element | PageComponent;
173

174
export function isPageComponent(c: PageComponent) {
1✔
175
  return c.component != undefined && c.folder != undefined;
×
176
}
22✔
177

178
export type FunctionComponent = (props?: any) => JSX.Element;
179
type Page = {
180
  path: string;
181
  element: Component;
182
  handlers: Array<MiddlewareArgument>;
183
};
184

185
export interface Fastro {
186
  /**
187
   * Immediately close the server listeners and associated HTTP connections.
188
   * @returns void
189
   */
190
  close: () => void;
191
  /**
192
   * Add application level middleware
193
   *
194
   * ### Example
195
   *
196
   * ```ts
197
   * import fastro from "../mod.ts";
198
   *
199
   * const f = new fastro();
200
   * f.use((req: HttpRequest, _ctx: Context, next: Next) => {
201
   *   console.log(`${req.method} ${req.url}`);
202
   *   return next();
203
   * });
204
   *
205
   * await f.serve();
206
   * ```
207
   * @param handler
208
   */
209
  use(...handler: Array<HandlerArgument>): Fastro;
210
  get(path: string, ...handler: Array<HandlerArgument>): Fastro;
211
  post(path: string, ...handler: Array<HandlerArgument>): Fastro;
212
  put(path: string, ...handler: Array<HandlerArgument>): Fastro;
213
  delete(path: string, ...handler: Array<HandlerArgument>): Fastro;
214
  patch(path: string, ...handler: Array<HandlerArgument>): Fastro;
215
  options(path: string, ...handler: Array<HandlerArgument>): Fastro;
216
  head(path: string, ...handler: Array<HandlerArgument>): Fastro;
217
  /**
218
   * Allow you access Server, Request, and Info after Middleware, Routes, Pages, and Static File Processing.
219
   * It can return `Response`, `Promise<Response>` or `void`.
220
   *
221
   * ### Example
222
   *
223
   * ```ts
224
   * import fastro, { Fastro, Info } from "../mod.ts";
225
   *
226
   * const f = new fastro();
227
   *
228
   * f.hook((_f: Fastro, _r: Request, _i: Info) => new Response("Hello World"));
229
   *
230
   * await f.serve();
231
   * ```
232
   *
233
   * @param hook
234
   */
235
  hook(hook: Hook): Fastro;
236
  /**
237
   * Allow you to access static files with custom `path`, `folder`, `maxAge`, and `referer` checking.
238
   *
239
   * ### Example
240
   *
241
   * ```ts
242
   * import fastro from "../mod.ts";
243
   *
244
   * const f = new fastro();
245
   *
246
   * f.static("/static", { folder: "static", maxAge: 90, referer: true });
247
   *
248
   * await f.serve();
249
   * ```
250
   * @param path
251
   * @param options
252
   */
253
  static(
254
    path: string,
255
    options?: { maxAge?: number; folder?: string; referer?: boolean },
256
  ): Fastro;
257
  /**
258
   * Allow you to define SSR page with custom `path`, `element`, and `handler`
259
   *
260
   * ### Example
261
   *
262
   * ```ts
263
   * import fastro, { Context, HttpRequest } from "../mod.ts";
264
   * import user from "../pages/user.tsx";
265
   *
266
   * const f = new fastro();
267
   *
268
   * f.static("/static", { folder: "static", maxAge: 90 });
269
   *
270
   * f.page("/", user, (_req: HttpRequest, ctx: Context) => {
271
   *     const options = {
272
   *       props: { data: "Guest" },
273
   *       status: 200,
274
   *       html: { head: { title: "React Component" } },
275
   *     };
276
   *     return ctx.render(options);
277
   *   },
278
   * );
279
   * await f.serve();
280
   * ```
281
   * @param path
282
   * @param element
283
   * @param handler
284
   */
285
  page(
286
    path: string,
287
    element: Component,
288
    ...handler: Array<MiddlewareArgument>
289
  ): Fastro;
290
  register(mf: ModuleFunction): Fastro;
291
  getStaticFolder(): string;
292
  getStaticPath(): string;
293
  getDevelopmentStatus(): boolean;
294
  /**
295
   * Add a handler directly
296
   *
297
   * @param method
298
   * @param path
299
   * @param handler
300
   */
301
  push(
302
    method?: string,
303
    path?: string,
304
    ...handler: Array<HandlerArgument>
305
  ): Fastro;
306
  onListen(handler: ListenHandler): void;
307
  finished(): Promise<void> | undefined;
308
  getNest(): Nest;
309
  record: Record<string, any>;
310
  /**
311
   * Serves HTTP requests
312
   *
313
   * If the server was constructed without a specified port, 8000 is used.
314
   */
315
  serve(): Promise<void>;
316
}
317

318
type ListenHandler = (params: { hostname: string; port: number }) => void;
319

320
type Static = {
321
  file: string;
322
  contentType: string;
323
};
324

325
type Nest = Record<
326
  string,
327
  any
328
>;
329

330
export const BUILD_ID = Deno.env.get("DENO_DEPLOYMENT_ID") || toHashString(
1✔
331
  new Uint8Array(
1✔
332
    await crypto.subtle.digest(
1✔
333
      "sha-1",
1✔
334
      new TextEncoder().encode(crypto.randomUUID()),
1✔
335
    ),
336
  ),
337
  "hex",
1✔
338
);
1✔
339

340
export default class HttpServer implements Fastro {
1✔
341
  #server: Deno.Server | undefined;
5✔
342
  #routes: Route[];
5✔
343
  #middlewares: Middleware[];
5✔
344
  #pages: Page[];
5✔
345
  #patterns: Record<string, URLPattern>;
5✔
346
  #root: URLPattern;
5✔
347
  #port;
5✔
348
  #ac;
5✔
349
  #staticUrl: string;
5✔
350
  #staticReferer: boolean;
5✔
351
  #staticFolder: string;
5✔
352
  #maxAge: number;
5✔
353
  record: Record<string, any>;
5✔
354
  #development: boolean;
5✔
355
  #nest: Nest;
5✔
356
  #body: ReadableStream<any> | undefined;
5✔
357
  #listenHandler: ListenHandler | undefined;
5✔
358
  #hook: Hook | undefined;
5✔
359
  #staticPath: string | undefined;
5✔
360

361
  constructor(options?: { port?: number }) {
5✔
362
    this.#port = options?.port ?? 8000;
9✔
363
    this.#routes = [];
9✔
364
    this.#middlewares = [];
9✔
365
    this.#pages = [];
9✔
366
    this.#hook = undefined;
9✔
367
    this.#patterns = {};
9✔
368
    this.#nest = {};
9✔
369
    this.record = {};
9✔
370
    this.#root = new URLPattern({ pathname: "/*" });
27✔
371
    this.#ac = new AbortController();
9✔
372
    this.#staticUrl = "";
9✔
373
    this.#staticReferer = false;
9✔
374
    this.#staticFolder = "";
9✔
375
    this.#maxAge = 0;
9✔
376
    this.#development = this.#getDevelopment();
9✔
377
    this.#handleInit();
9✔
378
    if (this.#development) this.#handleDevelopment();
9✔
379
    const status = this.#development ? "Development" : "Production";
9✔
380
    console.log(
9✔
381
      `%cStatus %c${status}`,
9✔
382
      "color: blue",
9✔
383
      "color: yellow",
9✔
384
    );
385
  }
9✔
386

387
  getNest(): Nest {
5✔
388
    return this.#nest;
6✔
389
  }
6✔
390

391
  #getDevelopment = () => {
5✔
392
    return Deno.env.get("ENV") === "DEVELOPMENT";
9✔
393
  };
5✔
394

395
  #handleInitialData = async () => {
5✔
396
    const key = await keyPromise;
9✔
397
    let exportedKeyString = await exportCryptoKey(key);
9✔
398
    exportedKeyString = btoa(exportedKeyString);
9✔
399
    exportedKeyString = addSalt(exportedKeyString, SALT);
9✔
400
    exportedKeyString = reverseString(exportedKeyString);
9✔
401
    exportedKeyString = "{" + exportedKeyString + "}";
9✔
402
    return exportedKeyString;
9✔
403
  };
5✔
404

405
  serve = async (options?: { port: number }) => {
5✔
406
    this.record["exportedKeyString"] = await this.#handleInitialData();
9✔
407
    this.record["salt"] = SALT;
9✔
408
    const port = options?.port ?? this.#port;
9✔
409
    const [s] = await this.#build();
9✔
410
    if (s) return;
×
411

412
    this.#server = Deno.serve({
9✔
413
      port,
9✔
414
      handler: this.#handleRequest,
9✔
415
      onListen: this.#listenHandler,
9✔
416
      onError: this.#handleError,
9✔
417
      signal: this.#ac.signal,
9✔
418
    });
9✔
419
  };
5✔
420

421
  #hydrateExist = async () => {
5✔
422
    for await (const dirEntry of Deno.readDir(`${Deno.cwd()}`)) {
9✔
423
      if (dirEntry.name === hydrateFolder) {
38✔
424
        return true;
41✔
425
      }
41✔
426
    }
38✔
427
    return false;
10✔
428
  };
5✔
429

430
  #createHydrateFile = async (e: string) => {
5✔
431
    const t = `${Deno.cwd()}/${hydrateFolder}/${e.toLowerCase()}.hydrate.tsx`;
14✔
432
    const js = this.#createHydrate(e.toLowerCase());
14✔
433
    await Deno.writeTextFile(t, js);
14✔
434
  };
5✔
435

436
  async #buildComponent(elementName: string) {
5✔
437
    const es = new Esbuild(elementName);
14✔
438
    await es.build();
14✔
439
    es.stop();
14✔
440
  }
14✔
441

442
  #createHydrate(comp: string) {
5✔
443
    return `/** == ${
14✔
444
      new Date().toLocaleString()
14✔
445
    } == DO NOT EDIT!! AUTOMATICALLY GENERATED BY FASTRO TO HYDRATE HTML FILES SO JS FILES CAN INTERACT. **/ 
14✔
446
import { h, hydrate } from "https://esm.sh/preact@10.17.1";import ${comp} from "../pages/${comp.toLowerCase()}.tsx";import { atobMe as a, clean as c,extractOriginalString as ex,importCryptoKey as i,keyType,keyUsages,reverseString as rev,} from "https://deno.land/x/fastro@${version}/crypto/key.ts";import { decryptData as d } from "https://deno.land/x/fastro@${version}/crypto/decrypt.ts";declare global {interface Window {__INITIAL_DATA__: any;}} fetch("/__INITIAL_DATA__").then((r) => r.json()).then((v) => { let r = c(v.d) as any;let s = c(v.s) as any;s = rev(s);s = a(s);r = rev(r);r = ex(r, s);r = a(r);i(r,keyType,keyUsages).then((k) => {d(k, window.__INITIAL_DATA__).then((v) => {delete window.__INITIAL_DATA__;const p = v as any;const r = document.getElementById("root");if (r) hydrate(h(${comp}, JSON.parse(p)), r);}).catch((error) => console.error(error));}).catch((error) => console.error(error));}).catch((error) => console.error(error));`;
14✔
447
  }
14✔
448

449
  #build = async () => {
5✔
450
    // deno-lint-ignore no-deprecated-deno-api
9✔
451
    if (Deno.run == undefined) return [];
×
452
    if (!await this.#hydrateExist()) {
9✔
453
      await Deno.mkdir(`${Deno.cwd()}/${hydrateFolder}`);
10✔
454
    }
10✔
455

456
    for (let index = 0; index < this.#pages.length; index++) {
9✔
457
      const page = this.#pages[index];
19✔
458
      if (isPageComponent(page.element as PageComponent)) {
×
459
        const pc = page.element as PageComponent;
×
460
        if (this.#isJSX(pc.component as JSX.Element)) continue;
×
461
        const fc = pc.component as FunctionComponent;
×
462
        await this.#createHydratePageComponentFile(pc);
×
463
        await this.#buildPageComponent(pc);
×
464
        this.#consoleLog(fc.name.toLowerCase());
×
465
      } else {
×
466
        if (this.#isJSX(page.element as JSX.Element)) continue;
19✔
467
        const fc = page.element as FunctionComponent;
28✔
468
        await this.#createHydrateFile(fc.name);
28✔
469
        await this.#buildComponent(fc.name);
28✔
470
        this.#consoleLog(fc.name.toLowerCase());
28✔
471
      }
28✔
472
    }
19✔
473

474
    return Deno.args.filter((v) => v === "--hydrate");
×
475
  };
5✔
476

477
  #consoleLog(name: string) {
5✔
478
    console.log(
14✔
479
      `%c${name}.js %cCreated!`,
14✔
480
      "color: blue",
14✔
481
      "color: green",
14✔
482
    );
483
  }
14✔
484

485
  #createHydratePageComponentFile = async (c: PageComponent) => {
×
486
    const fc = c.component as FunctionComponent;
×
487
    const t = `${Deno.cwd()}/${c.folder}/${fc.name.toLowerCase()}.hydrate.tsx`;
×
488
    const h = this.#createPageComponentHydrate(fc.name.toLowerCase());
×
489
    await Deno.writeTextFile(t, h);
×
490
  };
×
491

492
  #createPageComponentHydrate(comp: string) {
×
493
    return `// deno-lint-ignore-file no-explicit-any
×
494
/** == ${
×
495
      new Date().toLocaleString()
×
496
    } == DO NOT EDIT!! AUTOMATICALLY GENERATED BY FASTRO TO HYDRATE HTML FILES SO JS FILES CAN INTERACT. **/ 
×
497
import { h, hydrate } from "https://esm.sh/preact@10.17.1";import ${comp} from "./${comp.toLowerCase()}.page.tsx";import { atobMe as a, clean as c,extractOriginalString as ex,importCryptoKey as i,keyType,keyUsages,reverseString as rev,} from "https://deno.land/x/fastro@${version}/crypto/key.ts";import { decryptData as d } from "https://deno.land/x/fastro@${version}/crypto/decrypt.ts";declare global {interface Window {__INITIAL_DATA__: any;}} fetch("/__INITIAL_DATA__").then((r) => r.json()).then((v) => { let r = c(v.d) as any;let s = c(v.s) as any;s = rev(s);s = a(s);r = rev(r);r = ex(r, s);r = a(r);i(r,keyType,keyUsages).then((k) => {d(k, window.__INITIAL_DATA__).then((v) => {delete window.__INITIAL_DATA__;const p = v as any;const r = document.getElementById("root");if (r) hydrate(h(${comp}, JSON.parse(p)), r);}).catch((error) => console.error(error));}).catch((error) => console.error(error));}).catch((error) => console.error(error));`;
×
498
  }
×
499

500
  async #buildPageComponent(c: PageComponent) {
×
501
    const es = new EsbuildMod(c);
×
502
    await es.build();
×
503
    es.stop();
×
504
  }
×
505

506
  onListen = (handler: ListenHandler) => {
5✔
507
    this.#listenHandler = handler;
7✔
508
  };
5✔
509

510
  push(
5✔
511
    method?: string,
5✔
512
    path?: string,
5✔
513
    ...handler: Array<HandlerArgument>
5✔
514
  ) {
5✔
515
    if (method && path) {
33✔
516
      this.#patterns[path] = new URLPattern({
60✔
517
        pathname: path,
60✔
518
      });
60✔
519

520
      if (handler.length === 1) {
60✔
521
        return this.#pushHandler(method, path, handler[0]);
86✔
522
      }
86✔
523
      this.#pushHandler(method, path, handler[handler.length - 1]);
61✔
524

525
      for (let i = 0; i < handler.length - 1; i++) {
61✔
526
        this.#pushMiddleware(
61✔
527
          <MiddlewareArgument> <unknown> handler[i],
61✔
528
          method,
61✔
529
          path,
61✔
530
        );
531
      }
61✔
532
    } else {
33✔
533
      for (let i = 0; i < handler.length; i++) {
34✔
534
        this.#pushMiddleware(
34✔
535
          <MiddlewareArgument> <unknown> handler[i],
34✔
536
        );
537
      }
34✔
538
    }
34✔
539

540
    return this;
35✔
541
  }
33✔
542

543
  #pushHandler(
5✔
544
    method: string,
5✔
545
    path: string,
5✔
546
    handler: HandlerArgument,
5✔
547
  ) {
548
    this.#routes.push({ method, path, handler });
185✔
549
    return this;
37✔
550
  }
37✔
551

552
  #pushMiddleware(
5✔
553
    handler: MiddlewareArgument,
5✔
554
    method?: string,
5✔
555
    path?: string,
5✔
556
  ) {
557
    this.#middlewares.push({ method, path, handler });
35✔
558
    return this;
7✔
559
  }
7✔
560

561
  use(...handler: HandlerArgument[]) {
5✔
562
    return this.push(undefined, undefined, ...handler);
6✔
563
  }
6✔
564

565
  get(path: string, ...handler: Array<HandlerArgument>) {
5✔
566
    return this.push("GET", path, ...handler);
15✔
567
  }
15✔
568

569
  post(path: string, ...handler: Array<HandlerArgument>) {
5✔
570
    return this.push("POST", path, ...handler);
6✔
571
  }
6✔
572

573
  put(path: string, ...handler: Array<HandlerArgument>) {
5✔
574
    return this.push("PUT", path, ...handler);
6✔
575
  }
6✔
576

577
  head(path: string, ...handler: Array<HandlerArgument>) {
5✔
578
    return this.push("HEAD", path, ...handler);
6✔
579
  }
6✔
580

581
  options(path: string, ...handler: Array<HandlerArgument>) {
5✔
582
    return this.push("OPTIONS", path, ...handler);
6✔
583
  }
6✔
584

585
  delete(path: string, ...handler: Array<HandlerArgument>) {
5✔
586
    return this.push("DELETE", path, ...handler);
6✔
587
  }
6✔
588

589
  patch(path: string, ...handler: Array<HandlerArgument>) {
5✔
590
    return this.push("PATCH", path, ...handler);
6✔
591
  }
6✔
592

593
  static(
5✔
594
    path: string,
5✔
595
    options?: { maxAge?: number; folder?: string; referer?: boolean },
5✔
596
  ) {
5✔
597
    this.#staticUrl = path;
8✔
598
    if (options?.folder) this.#staticFolder = options?.folder;
8✔
599
    if (options?.referer) this.#staticReferer = options.referer;
8✔
600
    if (options?.maxAge) this.#maxAge = options.maxAge;
8✔
601
    return this;
8✔
602
  }
8✔
603

604
  page(
5✔
605
    path: string,
5✔
606
    element: Component,
5✔
607
    ...handlers: MiddlewareArgument[]
5✔
608
  ): Fastro {
5✔
609
    this.#patterns[path] = new URLPattern({
15✔
610
      pathname: path,
15✔
611
    });
15✔
612
    this.#pages.push({ path, element, handlers });
75✔
613
    return this;
15✔
614
  }
15✔
615

616
  getDevelopmentStatus() {
5✔
617
    return this.#development;
17✔
618
  }
17✔
619

620
  #handleDevelopment = () => {
5✔
621
    const refreshPath = `/___refresh___`;
6✔
622
    this.#patterns[refreshPath] = new URLPattern({
6✔
623
      pathname: refreshPath,
6✔
624
    });
6✔
625
    const refreshStream = (_req: Request) => {
6✔
626
      let timerId: number | undefined = undefined;
7✔
627
      this.#body = new ReadableStream({
7✔
628
        start(controller) {
7✔
629
          controller.enqueue(`data: ${BUILD_ID}\nretry: 100\n\n`);
8✔
630
          timerId = setInterval(() => {
×
631
            controller.enqueue(`data: ${BUILD_ID}\n\n`);
×
632
          }, 1000);
×
633
        },
7✔
634
        cancel() {
7✔
635
          if (timerId !== undefined) {
8✔
636
            clearInterval(timerId);
8✔
637
          }
8✔
638
        },
8✔
639
      });
7✔
640
      return new Response(this.#body.pipeThrough(new TextEncoderStream()), {
7✔
641
        headers: {
7✔
642
          "content-type": "text/event-stream",
7✔
643
        },
7✔
644
      });
7✔
645
    };
6✔
646
    this.#pushHandler("GET", refreshPath, refreshStream);
6✔
647
  };
5✔
648

649
  #handleInit = () => {
5✔
650
    const initPath = `/__INITIAL_DATA__`;
9✔
651
    this.#patterns[initPath] = new URLPattern({
9✔
652
      pathname: initPath,
9✔
653
    });
9✔
654

655
    this.#pushHandler("GET", initPath, (req: HttpRequest) => {
9✔
656
      const ref = this.#checkReferer(req);
10✔
657
      if (ref) return ref;
10!
658

659
      let s = btoa(req.record["salt"]);
×
660
      s = reverseString(s);
×
661
      return Response.json({
×
662
        d: req.record["exportedKeyString"],
×
663
        s: `{${s}}`,
×
664
      }, {
×
665
        headers: new Headers({
×
666
          "Access-Control-Allow-Origin": "null",
×
667
          "Access-Control-Allow-Methods": "GET",
×
668
          "Access-Control-Allow-Headers": "Content-Type",
×
669
        }),
×
670
      });
×
671
    });
×
672
  };
5✔
673

674
  #handleError = (error: unknown) => {
5✔
675
    const err: Error = error as Error;
6✔
676
    console.error(error);
6✔
677
    return new Response(err.stack);
6✔
678
  };
5✔
679

680
  #handleHook = async (h: Hook, r: Request, i: Info) => {
5✔
681
    const x = await h(this, r, i);
8✔
682
    if (this.#isResponse(x)) return x;
8✔
683
  };
5✔
684

685
  #checkReferer = (req: Request) => {
5✔
686
    const referer = req.headers.get("referer");
10✔
687
    const host = req.headers.get("host") as string;
10✔
688
    if (!referer || !referer?.includes(host)) {
×
689
      return new Response(STATUS_TEXT[Status.NotFound], {
10✔
690
        status: Status.NotFound,
10✔
691
      });
10✔
692
    }
10✔
693
  };
5✔
694

695
  #handleRequest = async (
5✔
696
    req: Request,
5✔
697
    i: Info,
5✔
698
  ) => {
699
    const m = await this.#findMiddleware(req, i);
45✔
700
    if (m) return this.#handleResponse(m);
45✔
701

702
    const r = this.#findRoute(req.method, req.url);
83✔
703
    if (r?.handler) {
45✔
704
      const h = r?.handler as RequestHandler;
62✔
705
      const res = await h(
62✔
706
        this.#transformRequest(this.record, req, r.params, r.query, r.match),
62✔
707
        this.#initContext(i),
62✔
708
      );
709
      return this.#handleResponse(res);
78✔
710
    }
78✔
711

712
    const p = this.#findPage(req);
66✔
713
    if (p && p.handlers && p.match && p.element) {
45✔
714
      const result = await this.#handlePageMiddleware(
57✔
715
        p.handlers,
57✔
716
        req,
57✔
717
        i,
57✔
718
        p.match,
57✔
719
        p.element,
57✔
720
      );
721

722
      if (result) return this.#handleResponse(result);
57✔
723
    }
57✔
724

725
    const s = (await this.#findStaticFiles(this.#staticUrl, req.url)) as Static;
54✔
726
    if (s) {
45✔
727
      const ref = this.#checkReferer(req);
48✔
728
      if (ref && this.#staticReferer) return ref;
48✔
729
      return new Response(s.file, {
50✔
730
        headers: {
50✔
731
          "Content-Type": s.contentType,
50✔
732
          "Cache-Control": `max-age=${this.#maxAge}`,
50✔
733
        },
50✔
734
      });
50✔
735
    }
50✔
736

737
    const b = await this.#handleBinary(this.#staticUrl, req.url);
51✔
738
    if (b) {
45✔
739
      const ref = this.#checkReferer(req);
46✔
740
      if (ref && this.#staticReferer) return ref;
×
741
      return this.#handleResponse(b);
46✔
742
    }
46✔
743

744
    const h = this.#findHook();
50✔
745
    if (h) {
45✔
746
      const res = await this.#handleHook(h, req, i);
48✔
747
      if (res) return this.#handleResponse(res);
48✔
748
    }
48✔
749

750
    return new Response(STATUS_TEXT[Status.NotFound], {
47✔
751
      status: Status.NotFound,
47✔
752
    });
47✔
753
  };
5✔
754

755
  hook = (hook: Hook) => {
5✔
756
    this.#hook = hook;
6✔
757
    return this;
6✔
758
  };
5✔
759

760
  #findHook = () => {
5✔
761
    return this.#hook;
10✔
762
  };
5✔
763

764
  #findPage = (r: Request) => {
5✔
765
    if (this.#pages.length === 0) return null;
26✔
766
    let page: Page | undefined = undefined;
43✔
767
    const nestID = `p${r.url}`;
43✔
768
    const p = this.#nest[nestID];
43✔
769
    if (p) return p as RouteNest;
26✔
770
    let pattern: URLPattern | null = null;
41✔
771
    for (let index = 0; index < this.#pages.length; index++) {
26✔
772
      const p = this.#pages[index];
93✔
773
      const m = this.#patterns[p.path].test(r.url);
93✔
774
      if (m) {
93✔
775
        page = p;
103✔
776
        pattern = this.#patterns[p.path];
103✔
777
        break;
103✔
778
      }
103✔
779
    }
93✔
780

781
    const e = pattern?.exec(r.url);
26✔
782
    if (!e) return this.#nest[nestID] = null;
26✔
783
    return this.#nest[nestID] = {
36✔
784
      params: e.pathname.groups,
36✔
785
      query: this.#iterableToRecord(new URL(r.url).searchParams),
36✔
786
      handler: () => {},
×
787
      match: e,
36✔
788
      handlers: page?.handlers,
26✔
789
      element: page?.element,
26✔
790
    };
26✔
791
  };
5✔
792

793
  #handlePageMiddleware = async (
5✔
794
    handlers: MiddlewareArgument[],
5✔
795
    r: Request,
5✔
796
    i: Info,
5✔
797
    match: URLPatternResult,
5✔
798
    element: Component,
5✔
799
  ) => {
800
    const searchParams = new URL(r.url).searchParams;
17✔
801
    const ctx = this.#initContext(i, element, r);
17✔
802
    let result: unknown;
17✔
803

804
    for (let index = 0; index < handlers.length; index++) {
17✔
805
      const h = handlers[index];
32✔
806
      const req = this.#transformRequest(
32✔
807
        this.record,
32✔
808
        r,
32✔
809
        match?.pathname.groups,
32✔
810
        this.#iterableToRecord(searchParams),
32✔
811
        match,
32✔
812
      );
813

814
      const x = await h(req, ctx, (error, data) => {
32✔
815
        if (error) throw error;
×
816
        return data;
35✔
817
      });
32✔
818

819
      if (this.#isResponse(x)) {
32✔
820
        result = x;
44✔
821
        break;
44✔
822
      }
44✔
823
    }
32✔
824

825
    return result;
32✔
826
  };
5✔
827

828
  #findMiddleware = async (r: Request, i: Info) => {
5✔
829
    if (this.#middlewares.length === 0) return undefined;
45✔
830
    const method: string = r.method, url: string = r.url;
66✔
831
    const ctx = this.#initContext(i);
66✔
832
    const searchParams = new URL(r.url).searchParams;
66✔
833
    let result: unknown;
66✔
834

835
    for (let index = 0; index < this.#middlewares.length; index++) {
45✔
836
      const m = this.#middlewares[index];
86✔
837
      const nestId = `${method}${m.method}${m.path}${url}`;
86✔
838

839
      const match = this.#findMatch(m, nestId, url);
86✔
840
      if (!match) continue;
86✔
841
      const req = this.#transformRequest(
86✔
842
        this.record,
86✔
843
        r,
86✔
844
        match?.pathname.groups,
86✔
845
        this.#iterableToRecord(searchParams),
86✔
846
        match,
86✔
847
      );
848
      const x = await m.handler(
86✔
849
        req,
86✔
850
        ctx,
86✔
851
        (error, data) => {
86✔
852
          if (error) throw error;
×
853
          return data;
105✔
854
        },
86✔
855
      );
856

857
      if (this.#isResponse(x)) {
86✔
858
        result = x;
88✔
859
        break;
88✔
860
      }
88✔
861
    }
86✔
862

863
    return result;
86✔
864
  };
5✔
865

866
  #findMatch(
5✔
867
    m: Middleware | Page,
5✔
868
    nestId: string,
5✔
869
    url: string,
5✔
870
  ) {
871
    const r = this.#nest[nestId];
46✔
872
    if (r) return r as URLPatternResult;
46✔
873

874
    const pattern = m.path ? this.#patterns[m.path] : this.#root;
46✔
875
    const result = pattern.exec(url);
46✔
876
    if (result) {
46✔
877
      return this.#nest[nestId] = result;
66✔
878
    }
66✔
879
  }
46✔
880

881
  #findRoute(method: string, url: string) {
5✔
882
    const nestID = `route${method + url}`;
43✔
883
    const r = this.#nest[nestID];
43✔
884
    if (r) return r;
×
885

886
    let h: Route | undefined = undefined;
43✔
887
    let p: URLPattern | null = null;
43✔
888
    for (let index = 0; index < this.#routes.length; index++) {
43✔
889
      const r = this.#routes[index];
357✔
890
      const m = this.#patterns[r.path].test(url);
357✔
891
      if ((r.method === method) && m) {
357✔
892
        h = r;
374✔
893
        p = this.#patterns[r.path];
374✔
894
        break;
374✔
895
      }
374✔
896
    }
357✔
897
    if (!h) return this.#nest[nestID] = null;
43✔
898

899
    const e = p?.exec(url);
43✔
900
    if (!e) return this.#nest[nestID] = null;
×
901
    return this.#nest[nestID] = {
60✔
902
      params: e.pathname.groups,
60✔
903
      query: this.#iterableToRecord(new URL(url).searchParams),
60✔
904
      handler: h?.handler,
43✔
905
      match: e,
43✔
906
    };
43✔
907
  }
43✔
908

909
  #handleResponse(res: any, status = 200) {
5✔
910
    if (this.#isString(res)) return new Response(res, { status });
127✔
911
    if (this.#isResponse(res)) return res as Response;
41✔
912
    if (this.#isJSX(res)) return this.#renderToString(res, status);
41✔
913
    if (this.#isJSON(res) || Array.isArray(res)) {
41✔
914
      return Response.json(res, { status });
129✔
915
    }
43✔
916
    return new Response(res, { status });
126✔
917
  }
41✔
918

919
  getStaticPath() {
5✔
920
    return this.#staticUrl;
17✔
921
  }
17✔
922

923
  getStaticFolder() {
5✔
924
    return this.#staticFolder;
6✔
925
  }
6✔
926

927
  #findStaticFiles = async (url: string, reqUrl: string) => {
5✔
928
    const [nestID, pathname] = this.#nestIDPathname(url, reqUrl);
14✔
929
    if (this.#nest[nestID]) return this.#nest[nestID];
14✔
930

931
    const pattern = new URLPattern({ pathname });
66✔
932
    const match = pattern.exec(reqUrl);
22✔
933
    if (!match) return this.#nest[nestID] = null;
×
934

935
    const input = match?.pathname.groups["0"];
14✔
936
    const filePath = `${this.#staticFolder}/${input}`;
14✔
937
    const ct = contentType(extname(filePath)) || "application/octet-stream";
14✔
938

939
    const binary = ["png", "jpeg", "jpg", "gif", "pdf", "aac"];
112✔
940
    const b = binary.filter((v) => ct.includes(v));
14✔
941
    if (b.length > 0) return this.#nest[nestID] = null;
14✔
942

943
    let file;
21✔
944
    try {
21✔
945
      file = await Deno.readTextFile(`./${filePath}`);
21✔
946
    } catch {
14✔
947
      return this.#nest[nestID] = null;
19✔
948
    }
19✔
949

950
    return this.#nest[nestID] = { contentType: ct, file };
64✔
951
  };
5✔
952

953
  #nestIDPathname = (url: string, reqUrl: string) => {
5✔
954
    const staticUrl = url === "/" ? "" : url;
20✔
955
    const pathname = `${staticUrl}/*`;
20✔
956
    const nestID = `${pathname}${reqUrl}`;
20✔
957

958
    return [nestID, pathname];
80✔
959
  };
5✔
960

961
  #handleBinary = async (url: string, reqUrl: string) => {
5✔
962
    const [nestID, pathname] = this.#nestIDPathname(url, reqUrl);
11✔
963
    try {
11✔
964
      const match = new URLPattern({ pathname }).exec(reqUrl);
33✔
965
      const filePath = `${this.#staticFolder}/${match?.pathname.groups["0"]}`;
11✔
966
      const ct = contentType(extname(filePath)) || "application/octet-stream";
11✔
967

968
      if (filePath === "/") return this.#nest[nestID] = null;
×
969
      const file = await Deno.open(`./${filePath}`, { read: true });
33✔
970
      return new Response(file.readable, {
12✔
971
        headers: {
12✔
972
          "Content-Type": ct,
12✔
973
          "Cache-Control": `max-age=${this.#maxAge}`,
12✔
974
        },
12✔
975
      });
12✔
976
    } catch {
11✔
977
      return this.#nest[nestID] = null;
16✔
978
    }
16✔
979
  };
5✔
980

981
  #iterableToRecord(
5✔
982
    params: URLSearchParams,
5✔
983
  ) {
984
    const record: Record<string, string> = {};
68✔
985
    params.forEach((v, k) => (record[k] = v));
68✔
986
    return record;
68✔
987
  }
68✔
988

989
  #initContext(
5✔
990
    info: Info,
5✔
991
    element?: Component,
5✔
992
    req?: Request,
5✔
993
  ) {
994
    const ctx = new Context();
55✔
995
    ctx.server = this;
55✔
996
    ctx.info = info;
55✔
997
    ctx.render = (options?: RenderOptions) => {
55✔
998
      if (!element) return new Response("Component not found");
×
999
      return this.#renderElement(element, options, req);
67✔
1000
    };
55✔
1001
    ctx.send = (data: unknown, status?: number) => {
55✔
1002
      return this.#handleResponse(data, status);
57✔
1003
    };
55✔
1004
    return ctx;
55✔
1005
  }
55✔
1006

1007
  #transformRequest(
5✔
1008
    record: Record<string, any>,
5✔
1009
    r: Request,
5✔
1010
    params?: Record<string, string | undefined> | undefined,
5✔
1011
    query?: Record<string, string>,
5✔
1012
    match?: URLPatternResult | null,
5✔
1013
  ) {
1014
    const req = r as HttpRequest;
58✔
1015
    req.record = record;
58✔
1016
    req.match = match;
58✔
1017
    req.params = params;
58✔
1018
    req.query = query;
58✔
1019
    return req;
58✔
1020
  }
58✔
1021

1022
  #renderToString(element: JSX.Element, status?: number) {
5✔
1023
    const component = renderToString(element);
6✔
1024
    return new Response(component, {
6✔
1025
      status,
6✔
1026
      headers: {
6✔
1027
        "content-type": "text/html",
6✔
1028
      },
6✔
1029
    });
6✔
1030
  }
6✔
1031

1032
  #renderElement(
5✔
1033
    element: Component,
5✔
1034
    options?: RenderOptions,
5✔
1035
    req?: Request,
5✔
1036
  ) {
1037
    const opt = options ?? { html: {} };
52✔
1038
    const r = new Render(element, opt, this.#nest, this, req);
17✔
1039
    return r.render();
17✔
1040
  }
17✔
1041

1042
  #isJSX(res: JSX.Element) {
5✔
1043
    return res && res.props != undefined && res.type != undefined;
19✔
1044
  }
19✔
1045

1046
  #isString(res: any) {
5✔
1047
    return typeof res === "string";
41✔
1048
  }
41✔
1049

1050
  #isResponse(res: any) {
5✔
1051
    return res instanceof Response;
78✔
1052
  }
78✔
1053

1054
  #isJSON(val: unknown) {
5✔
1055
    try {
8✔
1056
      const s = JSON.stringify(val);
8✔
1057
      JSON.parse(s);
8✔
1058
      return true;
8✔
1059
    } catch {
8✔
1060
      return false;
9✔
1061
    }
9✔
1062
  }
8✔
1063

1064
  finished = () => {
5✔
1065
    return this.#server?.finished;
9✔
1066
  };
5✔
1067

1068
  register = (mf: ModuleFunction) => {
5✔
1069
    return mf(this);
6✔
1070
  };
5✔
1071

1072
  close() {
5✔
1073
    if (this.#server) {
9✔
1074
      this.#server.finished.then(() => console.log("Server closed"));
9✔
1075
      console.log("Closing server...");
9✔
1076
      this.#ac.abort();
9✔
1077
    }
9✔
1078
  }
9✔
1079
}
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc