@hyperspan/framework 0.5.5 → 1.0.0-alpha.1

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/src/types.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Hyperspan Types
3
+ */
4
+ export namespace Hyperspan {
5
+ export interface Server {
6
+ _config: Hyperspan.Config;
7
+ _routes: Array<Hyperspan.Route>;
8
+ _middleware: Array<Hyperspan.MiddlewareFunction>;
9
+ use: (middleware: Hyperspan.MiddlewareFunction) => Hyperspan.Server;
10
+ get: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
11
+ post: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
12
+ put: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
13
+ patch: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
14
+ delete: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
15
+ options: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
16
+ };
17
+
18
+ export type Plugin = (config: Hyperspan.Config) => Promise<void> | void;
19
+
20
+ export type Config = {
21
+ appDir: string;
22
+ publicDir: string;
23
+ plugins: Array<Hyperspan.Plugin>; // Loaders for client islands
24
+ // For customizing the routes and adding your own...
25
+ beforeRoutesAdded?: (server: Hyperspan.Server) => void;
26
+ afterRoutesAdded?: (server: Hyperspan.Server) => void;
27
+ };
28
+
29
+ export interface Context {
30
+ vars: Record<string, any>;
31
+ route: {
32
+ path: string;
33
+ params: Record<string, string>;
34
+ cssImports?: string[];
35
+ }
36
+ req: {
37
+ url: URL;
38
+ raw: Request;
39
+ method: string; // Always uppercase
40
+ headers: Headers; // Case-insensitive
41
+ query: URLSearchParams;
42
+ body: any;
43
+ };
44
+ res: {
45
+ headers: Headers; // Headers to merge with final outgoing response
46
+ html: (html: string, options?: { status?: number; headers?: Record<string, string> }) => Response
47
+ json: (json: any, options?: { status?: number; headers?: Record<string, string> }) => Response;
48
+ text: (text: string, options?: { status?: number; headers?: Record<string, string> }) => Response;
49
+ redirect: (url: string, options?: { status?: number; headers?: Record<string, string> }) => Response;
50
+ error: (error: Error, options?: { status?: number; headers?: Record<string, string> }) => Response;
51
+ notFound: (options?: { status?: number; headers?: Record<string, string> }) => Response;
52
+ raw: Response;
53
+ };
54
+ };
55
+
56
+ export type ClientIslandOptions = {
57
+ ssr?: boolean;
58
+ loading?: 'lazy' | undefined;
59
+ };
60
+
61
+ export type RouteConfig = {
62
+ name?: string;
63
+ path?: string;
64
+ cssImports?: string[];
65
+ };
66
+ export type RouteHandler = (context: Hyperspan.Context) => unknown;
67
+ export type RouteHandlerOptions = {
68
+ middleware?: Hyperspan.MiddlewareFunction[];
69
+ }
70
+
71
+ // TypeScript inference for typed route params
72
+ // Source - https://stackoverflow.com/a/78170543
73
+ // Posted by jcalz
74
+ // Retrieved 2025-11-12, License - CC BY-SA 4.0
75
+ export type RouteParamsParser<T extends string, A = unknown> =
76
+ T extends `${string}:${infer F}/${infer R}` ? RouteParamsParser<R, A & Record<F, string>> :
77
+ (A & (T extends `${string}:${infer F}` ? Record<F, string> : unknown)) extends
78
+ infer U ? { [K in keyof U]: U[K] } : never
79
+
80
+
81
+ /**
82
+ * Next function type for middleware
83
+ */
84
+ export type NextFunction = () => Promise<Response>;
85
+
86
+ /**
87
+ * Middleware function signature
88
+ * Accepts context and next function, returns a Response
89
+ */
90
+ export type MiddlewareFunction = (
91
+ context: Hyperspan.Context,
92
+ next: Hyperspan.NextFunction
93
+ ) => Promise<Response> | Response;
94
+
95
+ export interface Route {
96
+ _kind: 'hsRoute';
97
+ _name: string | undefined;
98
+ _config: Hyperspan.RouteConfig;
99
+ _path(): string;
100
+ _methods(): string[];
101
+ get: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
102
+ post: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
103
+ put: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
104
+ patch: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
105
+ delete: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
106
+ options: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
107
+ middleware: (middleware: Array<Hyperspan.MiddlewareFunction>) => Hyperspan.Route;
108
+ fetch: (request: Request) => Promise<Response>;
109
+ };
110
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function assetHash(content: string): string {
4
+ return createHash('md5').update(content).digest('hex');
5
+ }
package/build.ts DELETED
@@ -1,16 +0,0 @@
1
- import { build } from 'bun';
2
-
3
- const entrypoints = ['./src/server.ts', './src/assets.ts', './src/middleware.ts'];
4
- const external = ['@hyperspan/html'];
5
- const outdir = './dist';
6
- const target = 'node';
7
- const splitting = true;
8
-
9
- // Build JS
10
- await build({
11
- entrypoints,
12
- external,
13
- outdir,
14
- target,
15
- splitting,
16
- });
package/dist/assets.js DELETED
@@ -1,120 +0,0 @@
1
- import"./chunk-atw8cdg1.js";
2
-
3
- // src/assets.ts
4
- import { html } from "@hyperspan/html";
5
- import { createHash } from "node:crypto";
6
- import { readdir } from "node:fs/promises";
7
- import { resolve } from "node:path";
8
- var IS_PROD = false;
9
- var PWD = import.meta.dir;
10
- var CLIENTJS_PUBLIC_PATH = "/_hs/js";
11
- var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
12
- var clientImportMap = new Map;
13
- var clientJSFiles = new Map;
14
- async function buildClientJS() {
15
- const sourceFile = resolve(PWD, "../", "./src/clientjs/hyperspan-client.ts");
16
- const output = await Bun.build({
17
- entrypoints: [sourceFile],
18
- outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
19
- naming: IS_PROD ? "[dir]/[name]-[hash].[ext]" : undefined,
20
- minify: IS_PROD
21
- });
22
- const jsFile = output.outputs[0].path.split("/").reverse()[0];
23
- clientJSFiles.set("_hs", { src: `${CLIENTJS_PUBLIC_PATH}/${jsFile}` });
24
- }
25
- function renderClientJS(module, loadScript) {
26
- if (!module.__CLIENT_JS) {
27
- throw new Error(`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`);
28
- }
29
- return html.raw(module.__CLIENT_JS.renderScriptTag({
30
- loadScript: loadScript ? typeof loadScript === "string" ? loadScript : functionToString(loadScript) : undefined
31
- }));
32
- }
33
- function functionToString(fn) {
34
- let str = fn.toString().trim();
35
- if (!str.includes("function ")) {
36
- if (str.includes("async ")) {
37
- str = "async function " + str.replace("async ", "");
38
- } else {
39
- str = "function " + str;
40
- }
41
- }
42
- const lines = str.split(`
43
- `);
44
- const firstLine = lines[0];
45
- const lastLine = lines[lines.length - 1];
46
- if (!lastLine?.includes("}")) {
47
- return str.replace("=> ", "{ return ") + "; }";
48
- }
49
- if (firstLine.includes("=>")) {
50
- return str.replace("=> ", "");
51
- }
52
- return str;
53
- }
54
- var clientCSSFiles = new Map;
55
- async function buildClientCSS() {
56
- if (clientCSSFiles.has("_hs")) {
57
- return clientCSSFiles.get("_hs");
58
- }
59
- const cssDir = "./public/_hs/css/";
60
- const cssFiles = await readdir(cssDir);
61
- let foundCSSFile = "";
62
- for (const file of cssFiles) {
63
- if (!file.endsWith(".css")) {
64
- continue;
65
- }
66
- foundCSSFile = file.replace(cssDir, "");
67
- clientCSSFiles.set("_hs", foundCSSFile);
68
- break;
69
- }
70
- if (!foundCSSFile) {
71
- console.log(`Unable to build CSS files from ${cssDir}`);
72
- }
73
- }
74
- function hyperspanStyleTags() {
75
- const cssFiles = Array.from(clientCSSFiles.entries());
76
- return html`${cssFiles.map(([_, file]) => html`<link rel="stylesheet" href="/_hs/css/${file}" />`)}`;
77
- }
78
- function hyperspanScriptTags() {
79
- const jsFiles = Array.from(clientJSFiles.entries());
80
- return html`
81
- <script type="importmap">
82
- {"imports": ${Object.fromEntries(clientImportMap)}}
83
- </script>
84
- ${jsFiles.map(([key, file]) => html`<script
85
- id="js-${key}"
86
- type="${file.type || "text/javascript"}"
87
- src="${file.src}"
88
- ></script>`)}
89
- `;
90
- }
91
- function assetHash(content) {
92
- return createHash("md5").update(content).digest("hex");
93
- }
94
- var ISLAND_DEFAULTS = () => ({
95
- ssr: true,
96
- loading: undefined
97
- });
98
- function renderIsland(Component, props, options = ISLAND_DEFAULTS()) {
99
- if (Component.__HS_ISLAND?.render) {
100
- return html.raw(Component.__HS_ISLAND.render(props, options));
101
- }
102
- throw new Error(`Module ${Component.name} was not loaded with an island plugin! Did you forget to install an island plugin and add it to the createServer() 'islandPlugins' config?`);
103
- }
104
- export {
105
- renderIsland,
106
- renderClientJS,
107
- hyperspanStyleTags,
108
- hyperspanScriptTags,
109
- functionToString,
110
- clientJSFiles,
111
- clientImportMap,
112
- clientCSSFiles,
113
- buildClientJS,
114
- buildClientCSS,
115
- assetHash,
116
- ISLAND_PUBLIC_PATH,
117
- ISLAND_DEFAULTS,
118
- CLIENTJS_PUBLIC_PATH
119
- };
120
-
@@ -1,19 +0,0 @@
1
- var __create = Object.create;
2
- var __getProtoOf = Object.getPrototypeOf;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __toESM = (mod, isNodeMode, target) => {
7
- target = mod != null ? __create(__getProtoOf(mod)) : {};
8
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
- for (let key of __getOwnPropNames(mod))
10
- if (!__hasOwnProp.call(to, key))
11
- __defProp(to, key, {
12
- get: () => mod[key],
13
- enumerable: true
14
- });
15
- return to;
16
- };
17
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
18
-
19
- export { __toESM, __commonJS };
@@ -1,179 +0,0 @@
1
- import {
2
- __commonJS,
3
- __toESM
4
- } from "./chunk-atw8cdg1.js";
5
-
6
- // node_modules/timestring/index.js
7
- var require_timestring = __commonJS((exports, module) => {
8
- module.exports = parseTimestring;
9
- var DEFAULT_OPTS = {
10
- hoursPerDay: 24,
11
- daysPerWeek: 7,
12
- weeksPerMonth: 4,
13
- monthsPerYear: 12,
14
- daysPerYear: 365.25
15
- };
16
- var UNIT_MAP = {
17
- ms: ["ms", "milli", "millisecond", "milliseconds"],
18
- s: ["s", "sec", "secs", "second", "seconds"],
19
- m: ["m", "min", "mins", "minute", "minutes"],
20
- h: ["h", "hr", "hrs", "hour", "hours"],
21
- d: ["d", "day", "days"],
22
- w: ["w", "week", "weeks"],
23
- mth: ["mon", "mth", "mths", "month", "months"],
24
- y: ["y", "yr", "yrs", "year", "years"]
25
- };
26
- function parseTimestring(value, returnUnit, opts) {
27
- opts = Object.assign({}, DEFAULT_OPTS, opts || {});
28
- if (typeof value === "number" || value.match(/^[-+]?[0-9.]+$/g)) {
29
- value = parseInt(value) + "ms";
30
- }
31
- let totalSeconds = 0;
32
- const unitValues = getUnitValues(opts);
33
- const groups = value.toLowerCase().replace(/[^.\w+-]+/g, "").match(/[-+]?[0-9.]+[a-z]+/g);
34
- if (groups === null) {
35
- throw new Error(`The value [${value}] could not be parsed by timestring`);
36
- }
37
- groups.forEach((group) => {
38
- const value2 = group.match(/[0-9.]+/g)[0];
39
- const unit = group.match(/[a-z]+/g)[0];
40
- totalSeconds += getSeconds(value2, unit, unitValues);
41
- });
42
- if (returnUnit) {
43
- return convert(totalSeconds, returnUnit, unitValues);
44
- }
45
- return totalSeconds;
46
- }
47
- function getUnitValues(opts) {
48
- const unitValues = {
49
- ms: 0.001,
50
- s: 1,
51
- m: 60,
52
- h: 3600
53
- };
54
- unitValues.d = opts.hoursPerDay * unitValues.h;
55
- unitValues.w = opts.daysPerWeek * unitValues.d;
56
- unitValues.mth = opts.daysPerYear / opts.monthsPerYear * unitValues.d;
57
- unitValues.y = opts.daysPerYear * unitValues.d;
58
- return unitValues;
59
- }
60
- function getUnitKey(unit) {
61
- for (const key of Object.keys(UNIT_MAP)) {
62
- if (UNIT_MAP[key].indexOf(unit) > -1) {
63
- return key;
64
- }
65
- }
66
- throw new Error(`The unit [${unit}] is not supported by timestring`);
67
- }
68
- function getSeconds(value, unit, unitValues) {
69
- return value * unitValues[getUnitKey(unit)];
70
- }
71
- function convert(value, unit, unitValues) {
72
- return value / unitValues[getUnitKey(unit)];
73
- }
74
- });
75
-
76
- // node_modules/hono/dist/middleware/etag/digest.js
77
- var mergeBuffers = (buffer1, buffer2) => {
78
- if (!buffer1) {
79
- return buffer2;
80
- }
81
- const merged = new Uint8Array(new ArrayBuffer(buffer1.byteLength + buffer2.byteLength));
82
- merged.set(new Uint8Array(buffer1), 0);
83
- merged.set(buffer2, buffer1.byteLength);
84
- return merged;
85
- };
86
- var generateDigest = async (stream, generator) => {
87
- if (!stream) {
88
- return null;
89
- }
90
- let result = undefined;
91
- const reader = stream.getReader();
92
- for (;; ) {
93
- const { value, done } = await reader.read();
94
- if (done) {
95
- break;
96
- }
97
- result = await generator(mergeBuffers(result, value));
98
- }
99
- if (!result) {
100
- return null;
101
- }
102
- return Array.prototype.map.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, "0")).join("");
103
- };
104
-
105
- // node_modules/hono/dist/middleware/etag/index.js
106
- var RETAINED_304_HEADERS = [
107
- "cache-control",
108
- "content-location",
109
- "date",
110
- "etag",
111
- "expires",
112
- "vary"
113
- ];
114
- var stripWeak = (tag) => tag.replace(/^W\//, "");
115
- function etagMatches(etag2, ifNoneMatch) {
116
- return ifNoneMatch != null && ifNoneMatch.split(/,\s*/).some((t) => stripWeak(t) === stripWeak(etag2));
117
- }
118
- function initializeGenerator(generator) {
119
- if (!generator) {
120
- if (crypto && crypto.subtle) {
121
- generator = (body) => crypto.subtle.digest({
122
- name: "SHA-1"
123
- }, body);
124
- }
125
- }
126
- return generator;
127
- }
128
- var etag = (options) => {
129
- const retainedHeaders = options?.retainedHeaders ?? RETAINED_304_HEADERS;
130
- const weak = options?.weak ?? false;
131
- const generator = initializeGenerator(options?.generateDigest);
132
- return async function etag2(c, next) {
133
- const ifNoneMatch = c.req.header("If-None-Match") ?? null;
134
- await next();
135
- const res = c.res;
136
- let etag3 = res.headers.get("ETag");
137
- if (!etag3) {
138
- if (!generator) {
139
- return;
140
- }
141
- const hash = await generateDigest(res.clone().body, generator);
142
- if (hash === null) {
143
- return;
144
- }
145
- etag3 = weak ? `W/"${hash}"` : `"${hash}"`;
146
- }
147
- if (etagMatches(etag3, ifNoneMatch)) {
148
- c.res = new Response(null, {
149
- status: 304,
150
- statusText: "Not Modified",
151
- headers: {
152
- ETag: etag3
153
- }
154
- });
155
- c.res.headers.forEach((_, key) => {
156
- if (retainedHeaders.indexOf(key.toLowerCase()) === -1) {
157
- c.res.headers.delete(key);
158
- }
159
- });
160
- } else {
161
- c.res.headers.set("ETag", etag3);
162
- }
163
- };
164
- };
165
-
166
- // src/middleware.ts
167
- var import_timestring = __toESM(require_timestring(), 1);
168
- function cacheTime(timeStrOrSeconds) {
169
- return (c, next) => etag()(c, () => {
170
- if (c.req.method.toUpperCase() === "GET") {
171
- const timeInSeconds = typeof timeStrOrSeconds === "number" ? timeStrOrSeconds : import_timestring.default(timeStrOrSeconds);
172
- c.header("Cache-Control", `public, max-age=${timeInSeconds}`);
173
- }
174
- return next();
175
- });
176
- }
177
- export {
178
- cacheTime
179
- };