htmx-router 1.0.0-pre1 → 1.0.0-pre2
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/cli/config.d.ts +13 -0
- package/cli/config.js +11 -0
- package/cli/index.d.ts +2 -0
- package/cli/index.js +38 -0
- package/cookies.d.ts +29 -0
- package/cookies.js +80 -0
- package/css.d.ts +21 -0
- package/css.js +60 -0
- package/defer.d.ts +14 -0
- package/defer.js +80 -0
- package/endpoint.d.ts +20 -0
- package/endpoint.js +40 -0
- package/event-source.d.ts +26 -0
- package/event-source.js +116 -0
- package/index.d.ts +19 -0
- package/index.js +2 -0
- package/internal/client.d.ts +1 -0
- package/internal/client.js +14 -0
- package/internal/compile/manifest.d.ts +1 -0
- package/internal/compile/manifest.js +179 -0
- package/internal/component/defer.d.ts +4 -0
- package/internal/component/defer.js +19 -0
- package/internal/component/head.d.ts +5 -0
- package/internal/component/head.js +22 -0
- package/internal/component/index.d.ts +4 -0
- package/internal/component/index.js +4 -0
- package/internal/component/scripts.d.ts +4 -0
- package/internal/component/scripts.js +23 -0
- package/internal/mount.d.ts +10 -0
- package/internal/mount.js +88 -0
- package/internal/request/http.d.ts +10 -0
- package/internal/request/http.js +61 -0
- package/internal/request/index.d.ts +17 -0
- package/internal/request/index.js +8 -0
- package/internal/request/native.d.ts +9 -0
- package/internal/request/native.js +48 -0
- package/internal/router.d.ts +15 -0
- package/internal/router.js +24 -0
- package/internal/util.d.ts +4 -0
- package/internal/util.js +49 -0
- package/package.json +1 -1
- package/response.d.ts +13 -0
- package/response.js +46 -0
- package/router.d.ts +33 -0
- package/router.js +206 -0
- package/shell.d.ts +120 -0
- package/shell.js +261 -0
- package/util/parameters.d.ts +10 -0
- package/util/parameters.js +1 -0
- package/util/path-builder.d.ts +1 -0
- package/util/path-builder.js +45 -0
- package/util/route.d.ts +2 -0
- package/util/route.js +58 -0
- package/vite/bundle-splitter.d.ts +4 -0
- package/vite/bundle-splitter.js +26 -0
- package/vite/client-island.d.ts +4 -0
- package/vite/client-island.js +14 -0
- package/vite/index.d.ts +3 -0
- package/vite/index.js +3 -0
- package/vite/router.d.ts +2 -0
- package/vite/router.js +29 -0
- package/example/eventdim-react/package.json +0 -67
- package/example/eventdim-react/server.js +0 -90
- package/example/island-react/global.d.ts +0 -8
- package/example/island-react/package.json +0 -38
- package/example/island-react/server.js +0 -58
package/cli/config.d.ts
ADDED
package/cli/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
const DEFAULT = {
|
|
4
|
+
framework: "generic"
|
|
5
|
+
};
|
|
6
|
+
export async function ReadConfig() {
|
|
7
|
+
const path = process.argv[2] || "./htmx.config.json";
|
|
8
|
+
if (!existsSync(path))
|
|
9
|
+
return DEFAULT;
|
|
10
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
11
|
+
}
|
package/cli/index.d.ts
ADDED
package/cli/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readFile, writeFile } from "fs/promises";
|
|
5
|
+
import * as components from "../internal/component/index.js";
|
|
6
|
+
import { CompileManifest } from "../internal/compile/manifest.js";
|
|
7
|
+
import { ReadConfig } from "./config.js";
|
|
8
|
+
const config = await ReadConfig();
|
|
9
|
+
console.info("");
|
|
10
|
+
if (config.client) {
|
|
11
|
+
console.info(`Generating ${chalk.green("client island")} manifest`);
|
|
12
|
+
const source = await readFile(config.client.source, "utf8");
|
|
13
|
+
await writeFile(config.client.output.server, CompileManifest(config.framework, source, true));
|
|
14
|
+
console.log(` - ${chalk.cyan("server")} ${chalk.gray(config.client.output.server)}`);
|
|
15
|
+
await writeFile(config.client.output.client, CompileManifest(config.framework, source, false));
|
|
16
|
+
console.log(` - ${chalk.cyan("client")} ${chalk.gray(config.client.output.client)}`);
|
|
17
|
+
console.log("");
|
|
18
|
+
}
|
|
19
|
+
if (config.component) {
|
|
20
|
+
console.info(`Generating ${chalk.green("components")} for ${chalk.cyan(config.framework)}`);
|
|
21
|
+
const padding = Math.max(...Object.keys(config.component).map(x => x.length)) || 0;
|
|
22
|
+
for (const key in config.component) {
|
|
23
|
+
const prefix = ` - ${chalk.cyan(key)}` + " ".repeat(padding + 1 - key.length);
|
|
24
|
+
const component = components[key];
|
|
25
|
+
if (!component) {
|
|
26
|
+
console.log(prefix + chalk.red("unknown component"));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const source = component[config.framework] || component["*"];
|
|
30
|
+
if (!source) {
|
|
31
|
+
console.log(prefix + chalk.red("unable to find definition for ") + chalk.cyan(config.framework));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const output = config.component[key];
|
|
35
|
+
await writeFile(output, source);
|
|
36
|
+
console.log(prefix + chalk.gray(output));
|
|
37
|
+
}
|
|
38
|
+
}
|
package/cookies.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CookieOptions {
|
|
2
|
+
domain?: string | undefined;
|
|
3
|
+
expires?: Date;
|
|
4
|
+
httpOnly?: boolean;
|
|
5
|
+
maxAge?: number;
|
|
6
|
+
partitioned?: boolean;
|
|
7
|
+
path?: string;
|
|
8
|
+
priority?: "low" | "medium" | "high";
|
|
9
|
+
sameSite?: "lax" | "strict" | "none";
|
|
10
|
+
secure?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Helper provided in the Generic and RouteContext which provides reading and updating cookies
|
|
14
|
+
*/
|
|
15
|
+
export declare class Cookies {
|
|
16
|
+
private source;
|
|
17
|
+
private config;
|
|
18
|
+
private map;
|
|
19
|
+
constructor(source?: Document | string | null);
|
|
20
|
+
private parse;
|
|
21
|
+
get(name: string): string | null;
|
|
22
|
+
entries(): Array<[string, string]>;
|
|
23
|
+
[Symbol.iterator](): IterableIterator<[string, string]>;
|
|
24
|
+
has(name: string): boolean;
|
|
25
|
+
set(name: string, value: string, options?: CookieOptions): void;
|
|
26
|
+
unset(name: string): void;
|
|
27
|
+
/** Creates the response headers required to make the changes done to these cookies */
|
|
28
|
+
export(): string[];
|
|
29
|
+
}
|
package/cookies.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper provided in the Generic and RouteContext which provides reading and updating cookies
|
|
3
|
+
*/
|
|
4
|
+
export class Cookies {
|
|
5
|
+
source;
|
|
6
|
+
config;
|
|
7
|
+
map;
|
|
8
|
+
constructor(source) {
|
|
9
|
+
this.source = source || null;
|
|
10
|
+
this.config = {};
|
|
11
|
+
this.map = {};
|
|
12
|
+
}
|
|
13
|
+
parse() {
|
|
14
|
+
if (this.source === null)
|
|
15
|
+
return;
|
|
16
|
+
const source = typeof this.source === "object" ? this.source.cookie : this.source;
|
|
17
|
+
for (const line of source.split("; ")) {
|
|
18
|
+
const [name, value] = line.split("=");
|
|
19
|
+
this.map[name] = value;
|
|
20
|
+
}
|
|
21
|
+
// keep source it document
|
|
22
|
+
if (typeof this.source === "string")
|
|
23
|
+
this.source = null;
|
|
24
|
+
}
|
|
25
|
+
get(name) {
|
|
26
|
+
this.parse();
|
|
27
|
+
return this.map[name] || null;
|
|
28
|
+
}
|
|
29
|
+
entries() {
|
|
30
|
+
this.parse();
|
|
31
|
+
return Object.entries(this.map);
|
|
32
|
+
}
|
|
33
|
+
*[Symbol.iterator]() {
|
|
34
|
+
this.parse();
|
|
35
|
+
for (const [key, value] of Object.entries(this.map)) {
|
|
36
|
+
yield [key, value];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
has(name) {
|
|
40
|
+
this.parse();
|
|
41
|
+
return name in this.map;
|
|
42
|
+
}
|
|
43
|
+
set(name, value, options = {}) {
|
|
44
|
+
this.parse();
|
|
45
|
+
options.path ||= "/";
|
|
46
|
+
this.config[name] = options;
|
|
47
|
+
this.map[name] = value;
|
|
48
|
+
if (typeof this.source === "object")
|
|
49
|
+
document.cookie = `${name}=${value}`;
|
|
50
|
+
}
|
|
51
|
+
unset(name) {
|
|
52
|
+
this.parse();
|
|
53
|
+
return this.set(name, "", { maxAge: 0 });
|
|
54
|
+
}
|
|
55
|
+
/** Creates the response headers required to make the changes done to these cookies */
|
|
56
|
+
export() {
|
|
57
|
+
const headers = new Array();
|
|
58
|
+
for (const name in this.config) {
|
|
59
|
+
let config = "";
|
|
60
|
+
for (const opt in this.config[name]) {
|
|
61
|
+
const prop = opt === "maxAge"
|
|
62
|
+
? "Max-Age"
|
|
63
|
+
: opt[0].toUpperCase() + opt.slice(1);
|
|
64
|
+
const raw = this.config[name][opt];
|
|
65
|
+
if (raw === true) {
|
|
66
|
+
config += `; ${prop}`;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (raw === false)
|
|
70
|
+
continue;
|
|
71
|
+
let value = String(raw);
|
|
72
|
+
value = value[0].toUpperCase() + value.slice(1);
|
|
73
|
+
config += `; ${prop}=${value}`;
|
|
74
|
+
}
|
|
75
|
+
const cookie = name + "=" + this.map[name] + config + ";";
|
|
76
|
+
headers.push(cookie);
|
|
77
|
+
}
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/css.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RouteContext } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a new css class to be included in the sheet
|
|
4
|
+
* Use .this as your class name in the source, and it will be replaced with a unique name
|
|
5
|
+
*/
|
|
6
|
+
export declare class Style {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly style: string;
|
|
9
|
+
readonly hash: string;
|
|
10
|
+
constructor(name: string, style: string);
|
|
11
|
+
toString(): string;
|
|
12
|
+
}
|
|
13
|
+
export declare function GetSheetUrl(): string;
|
|
14
|
+
/**
|
|
15
|
+
* RouteTree mounting point
|
|
16
|
+
*/
|
|
17
|
+
export declare const path = "_/style/$hash";
|
|
18
|
+
export declare const parameters: {
|
|
19
|
+
hash: StringConstructor;
|
|
20
|
+
};
|
|
21
|
+
export declare function loader(ctx: RouteContext<typeof parameters>): Promise<Response | null>;
|
package/css.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { QuickHash } from "./internal/util.js";
|
|
2
|
+
const classNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
let cache = null;
|
|
5
|
+
/**
|
|
6
|
+
* Create a new css class to be included in the sheet
|
|
7
|
+
* Use .this as your class name in the source, and it will be replaced with a unique name
|
|
8
|
+
*/
|
|
9
|
+
export class Style {
|
|
10
|
+
name; // unique name generated based on the original name and hash of the style
|
|
11
|
+
style; // the mutated source
|
|
12
|
+
hash;
|
|
13
|
+
constructor(name, style) {
|
|
14
|
+
if (!name.match(classNamePattern))
|
|
15
|
+
throw new Error("Cannot use given name for CSS class");
|
|
16
|
+
this.hash = QuickHash(style);
|
|
17
|
+
this.name = `${name}-${this.hash}`;
|
|
18
|
+
style = style.replaceAll(".this", "." + this.name);
|
|
19
|
+
this.style = style;
|
|
20
|
+
registry.set(this.name, this);
|
|
21
|
+
cache = null;
|
|
22
|
+
}
|
|
23
|
+
toString() {
|
|
24
|
+
return this.name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function BuildSheet() {
|
|
28
|
+
let composite = "";
|
|
29
|
+
let sheet = "";
|
|
30
|
+
for (const [key, def] of registry) {
|
|
31
|
+
composite += key;
|
|
32
|
+
sheet += def.style;
|
|
33
|
+
}
|
|
34
|
+
const hash = QuickHash(composite);
|
|
35
|
+
cache = { hash, sheet };
|
|
36
|
+
return cache;
|
|
37
|
+
}
|
|
38
|
+
function GetSheet() {
|
|
39
|
+
return cache || BuildSheet();
|
|
40
|
+
}
|
|
41
|
+
export function GetSheetUrl() {
|
|
42
|
+
const sheet = GetSheet();
|
|
43
|
+
return `/_/style/${sheet.hash}.css`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* RouteTree mounting point
|
|
47
|
+
*/
|
|
48
|
+
export const path = "_/style/$hash";
|
|
49
|
+
export const parameters = {
|
|
50
|
+
hash: String
|
|
51
|
+
};
|
|
52
|
+
export async function loader(ctx) {
|
|
53
|
+
const build = GetSheet();
|
|
54
|
+
if (!ctx.params.hash.startsWith(build.hash))
|
|
55
|
+
return null;
|
|
56
|
+
const headers = new Headers();
|
|
57
|
+
headers.set("Content-Type", "text/css");
|
|
58
|
+
headers.set("Cache-Control", "public, max-age=604800");
|
|
59
|
+
return new Response(build.sheet, { headers });
|
|
60
|
+
}
|
package/defer.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Parameterized, Parameterizer, ParameterShaper } from "./util/parameters.js";
|
|
2
|
+
import { RenderFunction } from "./index.js";
|
|
3
|
+
import { RouteContext } from "./router.js";
|
|
4
|
+
export declare function RegisterDeferral<T extends ParameterShaper>(shape: T, func: RenderFunction<T>, converter?: Parameterizer<T>): string;
|
|
5
|
+
export declare function Deferral<T extends ParameterShaper>(func: RenderFunction<T>, params?: Parameterized<T>): string;
|
|
6
|
+
/**
|
|
7
|
+
* RouteTree mounting point
|
|
8
|
+
*/
|
|
9
|
+
export declare const path = "_/defer/$";
|
|
10
|
+
export declare const parameters: {
|
|
11
|
+
$: StringConstructor;
|
|
12
|
+
};
|
|
13
|
+
export declare function loader(ctx: RouteContext<typeof parameters>): Promise<Response | null>;
|
|
14
|
+
export declare const action: typeof loader;
|
package/defer.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
|
+
ServerOnlyWarning("dynamic-ref");
|
|
3
|
+
import { RouteContext } from "./router.js";
|
|
4
|
+
import { QuickHash } from "./internal/util.js";
|
|
5
|
+
const registry = new Map();
|
|
6
|
+
const index = new Map();
|
|
7
|
+
function MakeIdentity(func) {
|
|
8
|
+
const hash = QuickHash(String(func));
|
|
9
|
+
const name = `${encodeURIComponent(func.name)}-${hash}`;
|
|
10
|
+
const url = `/_/defer/${name}`;
|
|
11
|
+
return { name, url };
|
|
12
|
+
}
|
|
13
|
+
export function RegisterDeferral(shape, func, converter) {
|
|
14
|
+
const existing = index.get(func);
|
|
15
|
+
if (existing)
|
|
16
|
+
return existing.url;
|
|
17
|
+
const { url, name } = MakeIdentity(func);
|
|
18
|
+
registry.set(name, func);
|
|
19
|
+
index.set(func, { url, shape, converter });
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
export function Deferral(func, params) {
|
|
23
|
+
const entry = index.get(func);
|
|
24
|
+
let url;
|
|
25
|
+
if (!entry) {
|
|
26
|
+
const identity = MakeIdentity(func);
|
|
27
|
+
console.warn(`Warn: Function ${identity.name} has not registered before use`);
|
|
28
|
+
url = identity.url;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
url = entry.url;
|
|
32
|
+
}
|
|
33
|
+
if (!params)
|
|
34
|
+
return url;
|
|
35
|
+
const query = new URLSearchParams();
|
|
36
|
+
if (entry?.converter) {
|
|
37
|
+
const convert = entry.converter;
|
|
38
|
+
for (const key in convert) {
|
|
39
|
+
if (!(key in params))
|
|
40
|
+
throw new Error(`Missing parameter ${key}`);
|
|
41
|
+
const raw = params[key];
|
|
42
|
+
const str = convert[key](raw);
|
|
43
|
+
query.set(key, str);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
for (const key in params)
|
|
48
|
+
query.set(key, String(params[key]));
|
|
49
|
+
}
|
|
50
|
+
return url + "?" + query.toString();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* RouteTree mounting point
|
|
54
|
+
*/
|
|
55
|
+
export const path = "_/defer/$";
|
|
56
|
+
export const parameters = {
|
|
57
|
+
"$": String
|
|
58
|
+
};
|
|
59
|
+
export async function loader(ctx) {
|
|
60
|
+
const endpoint = registry.get(ctx.params["$"]);
|
|
61
|
+
if (!endpoint)
|
|
62
|
+
return null;
|
|
63
|
+
const prelude = {};
|
|
64
|
+
for (const [key, value] of ctx.url.searchParams)
|
|
65
|
+
prelude[key] = value;
|
|
66
|
+
const entry = index.get(endpoint);
|
|
67
|
+
if (!entry) {
|
|
68
|
+
console.warn(`Warn: Function ${endpoint.name} was not registered for defer use`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const forward = new RouteContext(ctx, prelude, entry.shape);
|
|
72
|
+
const res = await endpoint(forward);
|
|
73
|
+
if (res instanceof Response)
|
|
74
|
+
return res;
|
|
75
|
+
if (res === null)
|
|
76
|
+
return null;
|
|
77
|
+
ctx.headers.set("X-Partial", "true");
|
|
78
|
+
return ctx.render(res);
|
|
79
|
+
}
|
|
80
|
+
export const action = loader;
|
package/endpoint.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RenderFunction, RouteContext } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a route-less endpoint
|
|
4
|
+
* The name is optional and will be inferred from the function if not given (helpful for network waterfalls)
|
|
5
|
+
*/
|
|
6
|
+
export declare class Endpoint {
|
|
7
|
+
readonly render: RenderFunction<{}>;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly url: string;
|
|
10
|
+
constructor(render: RenderFunction<{}>, name?: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* RouteTree mounting point
|
|
14
|
+
*/
|
|
15
|
+
export declare const path = "_/endpoint/$";
|
|
16
|
+
export declare const parameters: {
|
|
17
|
+
$: StringConstructor;
|
|
18
|
+
};
|
|
19
|
+
export declare function loader(ctx: RouteContext<typeof parameters>): Promise<Response | null>;
|
|
20
|
+
export declare const action: typeof loader;
|
package/endpoint.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
|
+
ServerOnlyWarning("endpoint");
|
|
3
|
+
import { QuickHash } from "./internal/util.js";
|
|
4
|
+
const registry = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Create a route-less endpoint
|
|
7
|
+
* The name is optional and will be inferred from the function if not given (helpful for network waterfalls)
|
|
8
|
+
*/
|
|
9
|
+
export class Endpoint {
|
|
10
|
+
render;
|
|
11
|
+
name;
|
|
12
|
+
url;
|
|
13
|
+
constructor(render, name) {
|
|
14
|
+
this.render = render;
|
|
15
|
+
name ||= render.name;
|
|
16
|
+
const hash = QuickHash(String(render));
|
|
17
|
+
this.name = name ? `${encodeURIComponent(name)}-${hash}` : hash;
|
|
18
|
+
this.url = `/_/endpoint/${this.name}`;
|
|
19
|
+
registry.set(this.name, this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* RouteTree mounting point
|
|
24
|
+
*/
|
|
25
|
+
export const path = "_/endpoint/$";
|
|
26
|
+
export const parameters = {
|
|
27
|
+
"$": String
|
|
28
|
+
};
|
|
29
|
+
export async function loader(ctx) {
|
|
30
|
+
const endpoint = registry.get(ctx.params["$"]);
|
|
31
|
+
if (!endpoint)
|
|
32
|
+
return null;
|
|
33
|
+
const res = await endpoint.render(ctx);
|
|
34
|
+
if (res === null)
|
|
35
|
+
return null;
|
|
36
|
+
if (res instanceof Response)
|
|
37
|
+
return res;
|
|
38
|
+
return ctx.render(res);
|
|
39
|
+
}
|
|
40
|
+
export const action = loader;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
|
|
3
|
+
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
4
|
+
*/
|
|
5
|
+
export declare class EventSource {
|
|
6
|
+
private controller;
|
|
7
|
+
private timer;
|
|
8
|
+
private state;
|
|
9
|
+
readonly response: Response;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
constructor(request: Request, keepAlive?: number);
|
|
12
|
+
get readyState(): number;
|
|
13
|
+
private sendBytes;
|
|
14
|
+
private sendText;
|
|
15
|
+
private keepAlive;
|
|
16
|
+
dispatch(type: string, data: string): boolean;
|
|
17
|
+
close(unlink?: boolean): boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class EventSourceSet extends Set<EventSource> {
|
|
20
|
+
/** Send update to all EventSources, auto closing failed dispatches */
|
|
21
|
+
dispatch(type: string, data: string): void;
|
|
22
|
+
/** Cull all closed connections */
|
|
23
|
+
cull(): void;
|
|
24
|
+
/** Close all connections */
|
|
25
|
+
closeAll(): void;
|
|
26
|
+
}
|
package/event-source.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
|
+
ServerOnlyWarning("event-source");
|
|
3
|
+
/**
|
|
4
|
+
* Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
|
|
5
|
+
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
6
|
+
*/
|
|
7
|
+
export class EventSource {
|
|
8
|
+
controller;
|
|
9
|
+
timer;
|
|
10
|
+
state;
|
|
11
|
+
response;
|
|
12
|
+
url; // just to make it polyfill
|
|
13
|
+
constructor(request, keepAlive = 30_000) {
|
|
14
|
+
this.controller = null;
|
|
15
|
+
this.state = 0;
|
|
16
|
+
this.url = request.url;
|
|
17
|
+
const stream = new ReadableStream({
|
|
18
|
+
start: (c) => { this.controller = c; this.state = 1; },
|
|
19
|
+
cancel: () => { this.close(); }
|
|
20
|
+
});
|
|
21
|
+
request.signal.addEventListener('abort', () => this.close());
|
|
22
|
+
this.response = new Response(stream, { headers });
|
|
23
|
+
this.timer = setInterval(() => this.keepAlive(), keepAlive);
|
|
24
|
+
register.add(this);
|
|
25
|
+
}
|
|
26
|
+
get readyState() {
|
|
27
|
+
return this.state;
|
|
28
|
+
}
|
|
29
|
+
sendBytes(chunk) {
|
|
30
|
+
if (!this.controller)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
this.controller.enqueue(chunk);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(e);
|
|
38
|
+
this.close(); // unbind on failure
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
sendText(chunk) {
|
|
43
|
+
return this.sendBytes(encoder.encode(chunk));
|
|
44
|
+
}
|
|
45
|
+
keepAlive() {
|
|
46
|
+
return this.sendText("\n\n");
|
|
47
|
+
}
|
|
48
|
+
dispatch(type, data) {
|
|
49
|
+
return this.sendText(`event: ${type}\ndata: ${data}\n\n`);
|
|
50
|
+
}
|
|
51
|
+
close(unlink = true) {
|
|
52
|
+
if (this.state === 2)
|
|
53
|
+
return false;
|
|
54
|
+
if (unlink)
|
|
55
|
+
register.delete(this);
|
|
56
|
+
try {
|
|
57
|
+
this.controller?.close();
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error(e);
|
|
61
|
+
this.controller = null;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Cleanup
|
|
65
|
+
if (this.timer)
|
|
66
|
+
clearInterval(this.timer);
|
|
67
|
+
this.controller = null;
|
|
68
|
+
this.state = 2;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class EventSourceSet extends Set {
|
|
73
|
+
/** Send update to all EventSources, auto closing failed dispatches */
|
|
74
|
+
dispatch(type, data) {
|
|
75
|
+
for (const stream of this) {
|
|
76
|
+
if (stream.readyState === 0)
|
|
77
|
+
continue; // skip initializing
|
|
78
|
+
const success = stream.dispatch(type, data);
|
|
79
|
+
if (!success)
|
|
80
|
+
this.delete(stream);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Cull all closed connections */
|
|
84
|
+
cull() {
|
|
85
|
+
for (const stream of this) {
|
|
86
|
+
if (stream.readyState !== 2)
|
|
87
|
+
continue;
|
|
88
|
+
this.delete(stream);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Close all connections */
|
|
92
|
+
closeAll() {
|
|
93
|
+
for (const stream of this)
|
|
94
|
+
stream.close();
|
|
95
|
+
this.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// global for easy reuse
|
|
99
|
+
const encoder = new TextEncoder();
|
|
100
|
+
const headers = new Headers();
|
|
101
|
+
// Chunked encoding with immediate forwarding by proxies (i.e. nginx)
|
|
102
|
+
headers.set("X-Accel-Buffering", "no");
|
|
103
|
+
headers.set("Transfer-Encoding", "chunked");
|
|
104
|
+
headers.set("Content-Type", "text/event-stream");
|
|
105
|
+
headers.set("Keep-Alive", "timeout=120"); // the maximum keep alive chrome shouldn't ignore
|
|
106
|
+
headers.set("Connection", "keep-alive");
|
|
107
|
+
// Auto close all SSE streams when shutdown requested
|
|
108
|
+
// Without this graceful shutdowns will hang indefinitely
|
|
109
|
+
const register = new EventSourceSet();
|
|
110
|
+
function CloseAll() {
|
|
111
|
+
register.closeAll();
|
|
112
|
+
}
|
|
113
|
+
if (process) {
|
|
114
|
+
process.on('SIGTERM', CloseAll);
|
|
115
|
+
process.on('SIGTERM', CloseAll);
|
|
116
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ParameterShaper } from "./util/parameters.js";
|
|
2
|
+
import type { RouteContext } from "./router.js";
|
|
3
|
+
import { createRequestHandler } from "./internal/request/index.js";
|
|
4
|
+
export type RenderFunction<T extends ParameterShaper = {}> = (ctx: RouteContext<T>) => Promise<Response | JSX.Element | null>;
|
|
5
|
+
export type CatchFunction<T extends ParameterShaper = {}> = (ctx: RouteContext<T>, err: unknown) => Promise<Response | JSX.Element>;
|
|
6
|
+
export type RouteModule<T extends ParameterShaper> = {
|
|
7
|
+
parameters?: T;
|
|
8
|
+
loader?: RenderFunction<T>;
|
|
9
|
+
action?: RenderFunction<T>;
|
|
10
|
+
error?: CatchFunction<T>;
|
|
11
|
+
route?: (params: Record<string, string>) => string;
|
|
12
|
+
};
|
|
13
|
+
export type ClientIslandManifest<T> = {
|
|
14
|
+
[K in keyof T]: ClientIsland<T[K]>;
|
|
15
|
+
};
|
|
16
|
+
type ClientIsland<T> = T extends (props: infer P) => JSX.Element ? (props: P & {
|
|
17
|
+
children?: JSX.Element;
|
|
18
|
+
}) => JSX.Element : T;
|
|
19
|
+
export { createRequestHandler, RouteContext };
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function GetClientEntryURL(): Promise<string | undefined>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "./util.js";
|
|
2
|
+
ServerOnlyWarning("client-url");
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
export async function GetClientEntryURL() {
|
|
5
|
+
if (process.env.NODE_ENV !== "production")
|
|
6
|
+
return "/app/entry.client.ts";
|
|
7
|
+
const config = JSON.parse(await readFile("./dist/client/.vite/manifest.json", "utf8"));
|
|
8
|
+
for (const key in config) {
|
|
9
|
+
const def = config[key];
|
|
10
|
+
if (!def.isEntry)
|
|
11
|
+
continue;
|
|
12
|
+
return "/" + def.file;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CompileManifest(adapter: string, source: string, ssr: boolean): string;
|