@hyperspan/framework 1.0.0-alpha.4 → 1.0.0-alpha.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "src/server.ts",
6
6
  "types": "src/server.ts",
@@ -21,10 +21,6 @@
21
21
  "types": "./src/middleware.ts",
22
22
  "default": "./src/middleware.ts"
23
23
  },
24
- "./middleware/zod": {
25
- "types": "./src/middleware/zod.ts",
26
- "default": "./src/middleware/zod.ts"
27
- },
28
24
  "./utils": {
29
25
  "types": "./src/utils.ts",
30
26
  "default": "./src/utils.ts"
@@ -80,7 +76,7 @@
80
76
  "typescript": "^5.9.3"
81
77
  },
82
78
  "dependencies": {
83
- "@hyperspan/html": "0.2.0",
79
+ "@hyperspan/html": "^1.0.0-alpha",
84
80
  "zod": "^4.1.12"
85
81
  }
86
82
  }
package/src/actions.ts CHANGED
@@ -2,7 +2,7 @@ import { html, HSHtml } from '@hyperspan/html';
2
2
  import { createRoute, returnHTMLResponse } from './server';
3
3
  import * as z from 'zod/v4';
4
4
  import type { Hyperspan as HS } from './types';
5
- import { assetHash } from './utils';
5
+ import { assetHash, formDataToJSON } from './utils';
6
6
  import * as actionsClient from './client/_hs/hyperspan-actions.client';
7
7
  import { renderClientJS } from './client/js';
8
8
 
@@ -115,73 +115,4 @@ export function createAction<T extends z.ZodTypeAny>(params: { name: string; sch
115
115
  };
116
116
 
117
117
  return api;
118
- }
119
-
120
- /**
121
- * Return JSON data structure for a given FormData object
122
- * Accounts for array fields (e.g. name="options[]" or <select multiple>)
123
- *
124
- * @link https://stackoverflow.com/a/75406413
125
- */
126
- export function formDataToJSON(formData: FormData): Record<string, string | string[]> {
127
- let object = {};
128
-
129
- /**
130
- * Parses FormData key xxx`[x][x][x]` fields into array
131
- */
132
- const parseKey = (key: string) => {
133
- const subKeyIdx = key.indexOf('[');
134
-
135
- if (subKeyIdx !== -1) {
136
- const keys = [key.substring(0, subKeyIdx)];
137
- key = key.substring(subKeyIdx);
138
-
139
- for (const match of key.matchAll(/\[(?<key>.*?)]/gm)) {
140
- if (match.groups) {
141
- keys.push(match.groups.key);
142
- }
143
- }
144
- return keys;
145
- } else {
146
- return [key];
147
- }
148
- };
149
-
150
- /**
151
- * Recursively iterates over keys and assigns key/values to object
152
- */
153
- const assign = (keys: string[], value: FormDataEntryValue, object: any): void => {
154
- const key = keys.shift();
155
-
156
- // When last key in the iterations
157
- if (key === '' || key === undefined) {
158
- return object.push(value);
159
- }
160
-
161
- if (Reflect.has(object, key)) {
162
- // If key has been found, but final pass - convert the value to array
163
- if (keys.length === 0) {
164
- if (!Array.isArray(object[key])) {
165
- object[key] = [object[key], value];
166
- return;
167
- }
168
- }
169
- // Recurse again with found object
170
- return assign(keys, value, object[key]);
171
- }
172
-
173
- // Create empty object for key, if next key is '' do array instead, otherwise set value
174
- if (keys.length >= 1) {
175
- object[key] = keys[0] === '' ? [] : {};
176
- return assign(keys, value, object[key]);
177
- } else {
178
- object[key] = value;
179
- }
180
- };
181
-
182
- for (const pair of formData.entries()) {
183
- assign(parseKey(pair[0]), pair[1], object);
184
- }
185
-
186
- return object;
187
118
  }
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse } from './server';
1
+ export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse, IS_PROD } from './server';
2
2
  export type { Hyperspan } from './types';
package/src/server.ts CHANGED
@@ -2,6 +2,7 @@ import { HSHtml, html, isHSHtml, renderStream, renderAsync, render } from '@hype
2
2
  import { executeMiddleware } from './middleware';
3
3
  import type { Hyperspan as HS } from './types';
4
4
  import { clientJSPlugin } from './plugins';
5
+ import { parsePath } from './utils';
5
6
  export type { HS as Hyperspan };
6
7
 
7
8
  export const IS_PROD = process.env.NODE_ENV === 'production';
@@ -49,7 +50,10 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
49
50
  method,
50
51
  headers,
51
52
  query,
52
- body: req.body,
53
+ async text() { return req.text() },
54
+ async json<T = unknown>() { return await req.json() as T },
55
+ async formData<T = unknown>() { return await req.formData() as T },
56
+ async urlencoded() { return new URLSearchParams(await req.text()) },
53
57
  },
54
58
  res: {
55
59
  headers: new Headers(),
@@ -365,16 +369,6 @@ export function isRunnableRoute(route: unknown): boolean {
365
369
  return typeof obj?._kind === 'string' && 'fetch' in obj;
366
370
  }
367
371
 
368
- /**
369
- * Is valid route path to add to server?
370
- */
371
- export function isValidRoutePath(path: string): boolean {
372
- const isHiddenRoute = path.includes('/__');
373
- const isTestFile = path.includes('.test') || path.includes('.spec');
374
-
375
- return !isHiddenRoute && !isTestFile && Boolean(path);
376
- }
377
-
378
372
  /**
379
373
  * Basic error handling
380
374
  * @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
@@ -470,53 +464,3 @@ export function createReadableStreamFromAsyncGenerator(output: AsyncGenerator) {
470
464
  },
471
465
  });
472
466
  }
473
-
474
- /**
475
- * Normalize URL path
476
- * Removes trailing slash and lowercases path
477
- */
478
- const ROUTE_SEGMENT_REGEX = /(\[[a-zA-Z_\.]+\])/g;
479
- export function parsePath(urlPath: string): { path: string, params: string[] } {
480
- const params: string[] = [];
481
- urlPath = urlPath.replace('index', '').replace('.ts', '').replace('.js', '');
482
-
483
- if (urlPath.startsWith('/')) {
484
- urlPath = urlPath.substring(1);
485
- }
486
-
487
- if (urlPath.endsWith('/')) {
488
- urlPath = urlPath.substring(0, urlPath.length - 1);
489
- }
490
-
491
- if (!urlPath) {
492
- return { path: '/', params: [] };
493
- }
494
-
495
- // Dynamic params
496
- if (ROUTE_SEGMENT_REGEX.test(urlPath)) {
497
- urlPath = urlPath.replace(ROUTE_SEGMENT_REGEX, (match: string) => {
498
- const paramName = match.replace(/[^a-zA-Z_\.]+/g, '');
499
- params.push(paramName);
500
-
501
- if (match.includes('...')) {
502
- return '*';
503
- } else {
504
- return ':' + paramName;
505
- }
506
- });
507
- }
508
-
509
- // Only lowercase non-param segments (do not lowercase after ':')
510
- return {
511
- path: (
512
- '/' +
513
- urlPath
514
- .split('/')
515
- .map((segment) =>
516
- segment.startsWith(':') || segment === '*' ? segment : segment.toLowerCase()
517
- )
518
- .join('/')
519
- ),
520
- params,
521
- };
522
- }
package/src/types.ts CHANGED
@@ -42,7 +42,10 @@ export namespace Hyperspan {
42
42
  method: string; // Always uppercase
43
43
  headers: Headers; // Case-insensitive
44
44
  query: URLSearchParams;
45
- body: any;
45
+ text: () => Promise<string>;
46
+ json<T = unknown>(): Promise<T>;
47
+ formData<T = unknown>(): Promise<T>;
48
+ urlencoded(): Promise<URLSearchParams>;
46
49
  };
47
50
  res: {
48
51
  headers: Headers; // Headers to merge with final outgoing response
package/src/utils.ts CHANGED
@@ -6,4 +6,134 @@ export function assetHash(content: string): string {
6
6
 
7
7
  export function randomHash(): string {
8
8
  return createHash('md5').update(randomBytes(32).toString('hex')).digest('hex');
9
+ }
10
+
11
+
12
+ /**
13
+ * Normalize URL path
14
+ * Removes trailing slash and lowercases path
15
+ */
16
+ const ROUTE_SEGMENT_REGEX = /(\[[a-zA-Z_\.]+\])/g;
17
+ export function parsePath(urlPath: string): { path: string, params: string[] } {
18
+ const params: string[] = [];
19
+ urlPath = urlPath.replace('index', '').replace('.ts', '').replace('.js', '');
20
+
21
+ if (urlPath.startsWith('/')) {
22
+ urlPath = urlPath.substring(1);
23
+ }
24
+
25
+ if (urlPath.endsWith('/')) {
26
+ urlPath = urlPath.substring(0, urlPath.length - 1);
27
+ }
28
+
29
+ if (!urlPath) {
30
+ return { path: '/', params: [] };
31
+ }
32
+
33
+ // Dynamic params
34
+ if (ROUTE_SEGMENT_REGEX.test(urlPath)) {
35
+ urlPath = urlPath.replace(ROUTE_SEGMENT_REGEX, (match: string) => {
36
+ const paramName = match.replace(/[^a-zA-Z_\.]+/g, '');
37
+ params.push(paramName);
38
+
39
+ if (match.includes('...')) {
40
+ return '*';
41
+ } else {
42
+ return ':' + paramName;
43
+ }
44
+ });
45
+ }
46
+
47
+ // Only lowercase non-param segments (do not lowercase after ':')
48
+ return {
49
+ path: (
50
+ '/' +
51
+ urlPath
52
+ .split('/')
53
+ .map((segment) =>
54
+ segment.startsWith(':') || segment === '*' ? segment : segment.toLowerCase()
55
+ )
56
+ .join('/')
57
+ ),
58
+ params,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Is valid route path to add to server?
64
+ */
65
+ export function isValidRoutePath(path: string): boolean {
66
+ const isHiddenRoute = path.includes('/__');
67
+ const isTestFile = path.includes('.test') || path.includes('.spec');
68
+
69
+ return !isHiddenRoute && !isTestFile && Boolean(path);
70
+ }
71
+
72
+ /**
73
+ * Return JSON data structure for a given FormData or URLSearchParams object
74
+ * Accounts for array fields (e.g. name="options[]" or <select multiple>)
75
+ *
76
+ * @link https://stackoverflow.com/a/75406413
77
+ */
78
+ export function formDataToJSON(formData: FormData | URLSearchParams): Record<string, string | string[]> {
79
+ let object = {};
80
+
81
+ /**
82
+ * Parses FormData key xxx`[x][x][x]` fields into array
83
+ */
84
+ const parseKey = (key: string) => {
85
+ const subKeyIdx = key.indexOf('[');
86
+
87
+ if (subKeyIdx !== -1) {
88
+ const keys = [key.substring(0, subKeyIdx)];
89
+ key = key.substring(subKeyIdx);
90
+
91
+ for (const match of key.matchAll(/\[(?<key>.*?)]/gm)) {
92
+ if (match.groups) {
93
+ keys.push(match.groups.key);
94
+ }
95
+ }
96
+ return keys;
97
+ } else {
98
+ return [key];
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Recursively iterates over keys and assigns key/values to object
104
+ */
105
+ const assign = (keys: string[], value: FormDataEntryValue, object: any): void => {
106
+ const key = keys.shift();
107
+
108
+ // When last key in the iterations
109
+ if (key === '' || key === undefined) {
110
+ return object.push(value);
111
+ }
112
+
113
+ if (Reflect.has(object, key)) {
114
+ // If key has been found, but final pass - convert the value to array
115
+ if (keys.length === 0) {
116
+ if (!Array.isArray(object[key])) {
117
+ object[key] = [object[key], value];
118
+ return;
119
+ }
120
+ }
121
+ // Recurse again with found object
122
+ return assign(keys, value, object[key]);
123
+ }
124
+
125
+ // Create empty object for key, if next key is '' do array instead, otherwise set value
126
+ if (keys.length >= 1) {
127
+ object[key] = keys[0] === '' ? [] : {};
128
+ return assign(keys, value, object[key]);
129
+ } else {
130
+ object[key] = value;
131
+ }
132
+ };
133
+
134
+ for (const pair of formData.entries()) {
135
+ assign(parseKey(pair[0]), pair[1], object);
136
+ }
137
+
138
+ return object;
9
139
  }