@hyperspan/framework 0.5.4 → 1.0.0-alpha.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/package.json +35 -27
- package/src/callsites.ts +17 -0
- package/src/client/css.ts +2 -0
- package/src/client/js.ts +82 -0
- package/src/layout.ts +31 -0
- package/src/middleware/zod.ts +46 -0
- package/src/middleware.ts +53 -14
- package/src/plugins.ts +64 -58
- package/src/server.test.ts +88 -0
- package/src/server.ts +285 -428
- package/src/types.ts +110 -0
- package/src/utils.ts +5 -0
- package/build.ts +0 -16
- package/dist/assets.js +0 -120
- package/dist/chunk-atw8cdg1.js +0 -19
- package/dist/middleware.js +0 -179
- package/dist/server.js +0 -2266
- package/src/actions.test.ts +0 -106
- package/src/actions.ts +0 -256
- package/src/assets.ts +0 -176
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "src/server.ts",
|
|
6
6
|
"types": "src/server.ts",
|
|
7
7
|
"public": true,
|
|
8
8
|
"publishConfig": {
|
|
@@ -11,23 +11,39 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
13
|
"types": "./src/server.ts",
|
|
14
|
-
"default": "./
|
|
14
|
+
"default": "./src/server.ts"
|
|
15
15
|
},
|
|
16
16
|
"./server": {
|
|
17
17
|
"types": "./src/server.ts",
|
|
18
|
-
"default": "./
|
|
19
|
-
},
|
|
20
|
-
"./assets": {
|
|
21
|
-
"types": "./src/assets.ts",
|
|
22
|
-
"default": "./dist/assets.js"
|
|
18
|
+
"default": "./src/server.ts"
|
|
23
19
|
},
|
|
24
20
|
"./middleware": {
|
|
25
21
|
"types": "./src/middleware.ts",
|
|
26
|
-
"default": "./
|
|
22
|
+
"default": "./src/middleware.ts"
|
|
23
|
+
},
|
|
24
|
+
"./middleware/zod": {
|
|
25
|
+
"types": "./src/middleware/zod.ts",
|
|
26
|
+
"default": "./src/middleware/zod.ts"
|
|
27
|
+
},
|
|
28
|
+
"./utils": {
|
|
29
|
+
"types": "./src/utils.ts",
|
|
30
|
+
"default": "./src/utils.ts"
|
|
31
|
+
},
|
|
32
|
+
"./layout": {
|
|
33
|
+
"types": "./src/layout.ts",
|
|
34
|
+
"default": "./src/layout.ts"
|
|
35
|
+
},
|
|
36
|
+
"./client/css": {
|
|
37
|
+
"types": "./src/client/css.ts",
|
|
38
|
+
"default": "./src/client/css.ts"
|
|
39
|
+
},
|
|
40
|
+
"./client/js": {
|
|
41
|
+
"types": "./src/client/js.ts",
|
|
42
|
+
"default": "./src/client/js.ts"
|
|
27
43
|
},
|
|
28
|
-
"./
|
|
29
|
-
"types": "./src/
|
|
30
|
-
"default": "./src/
|
|
44
|
+
"./plugins": {
|
|
45
|
+
"types": "./src/plugins.ts",
|
|
46
|
+
"default": "./src/plugins.ts"
|
|
31
47
|
}
|
|
32
48
|
},
|
|
33
49
|
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
@@ -51,24 +67,16 @@
|
|
|
51
67
|
"url": "https://github.com/vlucas/hyperspan/issues"
|
|
52
68
|
},
|
|
53
69
|
"scripts": {
|
|
54
|
-
"
|
|
55
|
-
"clean": "rm -rf dist",
|
|
56
|
-
"test": "bun test",
|
|
57
|
-
"prepack": "bun run clean && bun run build"
|
|
70
|
+
"test": "bun test"
|
|
58
71
|
},
|
|
59
72
|
"devDependencies": {
|
|
60
|
-
"@types/bun": "^1.
|
|
61
|
-
"@types/node": "^
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"prettier": "^3.5.3",
|
|
65
|
-
"typescript": "^5.8.3"
|
|
73
|
+
"@types/bun": "^1.3.1",
|
|
74
|
+
"@types/node": "^24.10.0",
|
|
75
|
+
"prettier": "^3.5.2",
|
|
76
|
+
"typescript": "^5.9.3"
|
|
66
77
|
},
|
|
67
78
|
"dependencies": {
|
|
68
|
-
"@hyperspan/html": "0.
|
|
69
|
-
"
|
|
70
|
-
"isbot": "^5.1.30",
|
|
71
|
-
"timestring": "^7.0.0",
|
|
72
|
-
"zod": "^4.1.5"
|
|
79
|
+
"@hyperspan/html": "0.2.0",
|
|
80
|
+
"zod": "^4.1.12"
|
|
73
81
|
}
|
|
74
82
|
}
|
package/src/callsites.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
export function callsites() {
|
|
3
|
+
const _prepareStackTrace = Error.prepareStackTrace;
|
|
4
|
+
try {
|
|
5
|
+
let result: NodeJS.CallSite[] = [];
|
|
6
|
+
Error.prepareStackTrace = (_, callSites) => {
|
|
7
|
+
const callSitesWithoutCurrent = callSites.slice(1);
|
|
8
|
+
result = callSitesWithoutCurrent;
|
|
9
|
+
return callSitesWithoutCurrent;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
new Error().stack;
|
|
13
|
+
return result;
|
|
14
|
+
} finally {
|
|
15
|
+
Error.prepareStackTrace = _prepareStackTrace;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/client/js.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { html } from '@hyperspan/html';
|
|
2
|
+
import type { Hyperspan as HS } from '../types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const JS_PUBLIC_PATH = '/_hs/js';
|
|
6
|
+
export const JS_ISLAND_PUBLIC_PATH = '/_hs/js/islands';
|
|
7
|
+
export const JS_IMPORT_MAP = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render a client JS module as a script tag
|
|
11
|
+
*/
|
|
12
|
+
export function renderClientJS<T>(module: T, loadScript?: ((module: T) => void) | string) {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
if (!module.__CLIENT_JS) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return html.raw(
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
module.__CLIENT_JS.renderScriptTag({
|
|
23
|
+
loadScript: loadScript
|
|
24
|
+
? typeof loadScript === 'string'
|
|
25
|
+
? loadScript
|
|
26
|
+
: functionToString(loadScript)
|
|
27
|
+
: undefined,
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert a function to a string (results in loss of context!)
|
|
34
|
+
* Handles named, async, and arrow functions
|
|
35
|
+
*/
|
|
36
|
+
export function functionToString(fn: any) {
|
|
37
|
+
let str = fn.toString().trim();
|
|
38
|
+
|
|
39
|
+
// Ensure consistent output & handle async
|
|
40
|
+
if (!str.includes('function ')) {
|
|
41
|
+
if (str.includes('async ')) {
|
|
42
|
+
str = 'async function ' + str.replace('async ', '');
|
|
43
|
+
} else {
|
|
44
|
+
str = 'function ' + str;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lines = str.split('\n');
|
|
49
|
+
const firstLine = lines[0];
|
|
50
|
+
const lastLine = lines[lines.length - 1];
|
|
51
|
+
|
|
52
|
+
// Arrow function conversion
|
|
53
|
+
if (!lastLine?.includes('}')) {
|
|
54
|
+
return str.replace('=> ', '{ return ') + '; }';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cleanup arrow function
|
|
58
|
+
if (firstLine.includes('=>')) {
|
|
59
|
+
return str.replace('=> ', '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return str;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Island defaults
|
|
67
|
+
*/
|
|
68
|
+
export const ISLAND_DEFAULTS: () => HS.ClientIslandOptions = () => ({
|
|
69
|
+
ssr: true,
|
|
70
|
+
loading: undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export function renderIsland(Component: any, props: any, options = ISLAND_DEFAULTS()) {
|
|
74
|
+
// Render island with its own logic
|
|
75
|
+
if (Component.__HS_ISLAND?.render) {
|
|
76
|
+
return html.raw(Component.__HS_ISLAND.render(props, options));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Module ${Component.name} was not loaded with an island plugin! Did you forget to install an island plugin and add it to the 'islandPlugins' option in your hyperspan.config.ts file?`
|
|
81
|
+
);
|
|
82
|
+
}
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { html } from '@hyperspan/html';
|
|
2
|
+
import { JS_IMPORT_MAP } from './client/js';
|
|
3
|
+
import { CSS_PUBLIC_PATH, CSS_ROUTE_MAP } from './client/css';
|
|
4
|
+
import type { Hyperspan as HS } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Output the importmap for the client so we can use ESModules on the client to load JS files on demand
|
|
8
|
+
*/
|
|
9
|
+
export function hyperspanScriptTags() {
|
|
10
|
+
return html`
|
|
11
|
+
<script type="importmap">
|
|
12
|
+
{"imports": ${Object.fromEntries(JS_IMPORT_MAP)}}
|
|
13
|
+
</script>
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Output style tags for the current route's CSS imports
|
|
19
|
+
*/
|
|
20
|
+
export function hyperspanStyleTags(context: HS.Context) {
|
|
21
|
+
const styleTags = [];
|
|
22
|
+
const cssImports = context.route.cssImports ?? CSS_ROUTE_MAP.get(context.route.path) ?? [];
|
|
23
|
+
|
|
24
|
+
for (const cssFile of cssImports) {
|
|
25
|
+
styleTags.push(html`
|
|
26
|
+
<link rel="stylesheet" href="${CSS_PUBLIC_PATH}/${cssFile}" />
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return styleTags;
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Hyperspan as HS } from '../types';
|
|
2
|
+
import type { ZodAny, ZodObject, ZodError } from 'zod/v4';
|
|
3
|
+
import { flattenError } from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
export class ZodValidationError extends Error {
|
|
6
|
+
constructor(flattened: ReturnType<typeof flattenError>) {
|
|
7
|
+
super('Input validation error(s)');
|
|
8
|
+
this.name = 'ZodValidationError';
|
|
9
|
+
|
|
10
|
+
// Copy all properties from flattened error
|
|
11
|
+
Object.assign(this, flattened);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validateQuery(schema: ZodObject): HS.MiddlewareFunction {
|
|
16
|
+
return async (context: HS.Context, next: HS.NextFunction) => {
|
|
17
|
+
const query: Record<string, string> = Object.fromEntries(context.req.query.entries());
|
|
18
|
+
const validated = schema.safeParse(query);
|
|
19
|
+
|
|
20
|
+
if (!validated.success) {
|
|
21
|
+
const err = formatZodError(validated.error);
|
|
22
|
+
return context.res.error(err, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return next();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateBody(schema: ZodObject | ZodAny): HS.MiddlewareFunction {
|
|
30
|
+
return async (context: HS.Context, next: HS.NextFunction) => {
|
|
31
|
+
const body = await context.req.body;
|
|
32
|
+
const validated = schema.safeParse(body);
|
|
33
|
+
|
|
34
|
+
if (!validated.success) {
|
|
35
|
+
const err = formatZodError(validated.error);
|
|
36
|
+
return context.res.error(err, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return next();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatZodError(error: ZodError): ZodValidationError {
|
|
44
|
+
const zodError = flattenError(error);
|
|
45
|
+
return new ZodValidationError(zodError);
|
|
46
|
+
}
|
package/src/middleware.ts
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { etag } from 'hono/etag';
|
|
3
|
-
import timestring from 'timestring';
|
|
1
|
+
import type { Hyperspan as HS } from './types';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
4
|
+
* Type guard to check if a handler is a middleware function
|
|
5
|
+
* Middleware functions have 2 parameters (context, next)
|
|
6
|
+
* Route handlers have 1 parameter (context)
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
function isMiddlewareFunction(
|
|
9
|
+
handler: HS.MiddlewareFunction | HS.RouteHandler
|
|
10
|
+
): handler is HS.MiddlewareFunction {
|
|
11
|
+
return handler.length === 2;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute an array of middleware functions and route handlers
|
|
16
|
+
* Middleware functions receive (context, next) and can call next() to continue
|
|
17
|
+
* Route handlers receive (context) and are executed at the end of the chain
|
|
18
|
+
*
|
|
19
|
+
* @param context - The Hyperspan context
|
|
20
|
+
* @param handlers - Array of middleware functions and/or route handlers
|
|
21
|
+
* @returns Promise<Response>
|
|
22
|
+
*/
|
|
23
|
+
export async function executeMiddleware(
|
|
24
|
+
context: HS.Context,
|
|
25
|
+
handlers: Array<HS.MiddlewareFunction | HS.RouteHandler>
|
|
26
|
+
): Promise<Response> {
|
|
27
|
+
if (handlers.length === 0) {
|
|
28
|
+
return context.res.notFound();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create the next function for middleware
|
|
33
|
+
* This function will execute the next handler in the chain
|
|
34
|
+
*/
|
|
35
|
+
const createNext = (index: number): HS.NextFunction => {
|
|
36
|
+
return async (): Promise<Response> => {
|
|
37
|
+
if (index >= handlers.length) {
|
|
38
|
+
return context.res.notFound();
|
|
16
39
|
}
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
|
|
41
|
+
const handler = handlers[index];
|
|
42
|
+
|
|
43
|
+
// If it's middleware, execute it with the next function
|
|
44
|
+
if (isMiddlewareFunction(handler)) {
|
|
45
|
+
const next = createNext(index + 1);
|
|
46
|
+
const result = await handler(context, next);
|
|
47
|
+
return result instanceof Response ? result : context.res.html(String(result));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If it's a route handler, execute it and convert to Response
|
|
51
|
+
return await handler(context) as Response;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Start execution from the first handler
|
|
56
|
+
return await createNext(0)();
|
|
19
57
|
}
|
|
58
|
+
|
package/src/plugins.ts
CHANGED
|
@@ -1,65 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { Hyperspan as HS } from './types';
|
|
2
|
+
import { JS_PUBLIC_PATH, JS_IMPORT_MAP } from './client/js';
|
|
3
|
+
import { assetHash } from './utils';
|
|
4
|
+
import { IS_PROD } from './server';
|
|
5
|
+
import { join } from 'node:path';
|
|
3
6
|
|
|
7
|
+
export const CSS_PUBLIC_PATH = '/_hs/css';
|
|
4
8
|
const CLIENT_JS_CACHE = new Map<string, string>();
|
|
5
9
|
const EXPORT_REGEX = /export\{(.*)\}/g;
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Hyperspan Client JS Plugin
|
|
9
13
|
*/
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export function clientJSPlugin(): HS.Plugin {
|
|
15
|
+
return async (config: HS.Config) => {
|
|
16
|
+
// Define a Bun plugin to handle .client.ts files
|
|
17
|
+
await Bun.plugin({
|
|
18
|
+
name: 'Hyperspan Client JS Loader',
|
|
19
|
+
async setup(build) {
|
|
20
|
+
// when a .client.ts file is imported...
|
|
21
|
+
build.onLoad({ filter: /\.client\.ts$/ }, async (args) => {
|
|
22
|
+
const jsId = assetHash(args.path);
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Cache: Avoid re-processing the same file
|
|
25
|
+
if (IS_PROD && CLIENT_JS_CACHE.has(jsId)) {
|
|
26
|
+
return {
|
|
27
|
+
contents: CLIENT_JS_CACHE.get(jsId) || '',
|
|
28
|
+
loader: 'js',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// We need to build the file to ensure we can ship it to the client with dependencies
|
|
33
|
+
// Ironic, right? Calling Bun.build() inside of a plugin that runs on Bun.build()?
|
|
34
|
+
const result = await Bun.build({
|
|
35
|
+
entrypoints: [args.path],
|
|
36
|
+
outdir: join(config.publicDir, JS_PUBLIC_PATH),
|
|
37
|
+
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
38
|
+
external: Array.from(JS_IMPORT_MAP.keys()),
|
|
39
|
+
minify: IS_PROD,
|
|
40
|
+
format: 'esm',
|
|
41
|
+
target: 'browser',
|
|
42
|
+
env: 'APP_PUBLIC_*',
|
|
43
|
+
});
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
// Add output file to import map
|
|
46
|
+
const esmName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
|
|
47
|
+
JS_IMPORT_MAP.set(esmName, `${JS_PUBLIC_PATH}/${esmName}.js`);
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const contents = await result.outputs[0].text();
|
|
50
|
+
const exportLine = EXPORT_REGEX.exec(contents);
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
let exports = '{}';
|
|
53
|
+
if (exportLine) {
|
|
54
|
+
const exportName = exportLine[1];
|
|
55
|
+
exports =
|
|
56
|
+
'{' +
|
|
57
|
+
exportName
|
|
58
|
+
.split(',')
|
|
59
|
+
.map((name) => name.trim().split(' as '))
|
|
60
|
+
.map(([name, alias]) => `${alias === 'default' ? 'default as ' + name : alias}`)
|
|
61
|
+
.join(', ') +
|
|
62
|
+
'}';
|
|
63
|
+
}
|
|
64
|
+
const fnArgs = exports.replace(/(\w+)\s*as\s*(\w+)/g, '$1: $2');
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
// Export a special object that can be used to render the client JS as a script tag
|
|
67
|
+
const moduleCode = `// hyperspan:processed
|
|
63
68
|
import { functionToString } from '@hyperspan/framework/assets';
|
|
64
69
|
|
|
65
70
|
// Original file contents
|
|
@@ -78,13 +83,14 @@ export const __CLIENT_JS = {
|
|
|
78
83
|
}
|
|
79
84
|
`;
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
CLIENT_JS_CACHE.set(jsId, moduleCode);
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
88
|
+
return {
|
|
89
|
+
contents: moduleCode,
|
|
90
|
+
loader: 'js',
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { createRoute, createServer } from './server';
|
|
3
|
+
import type { Hyperspan as HS } from './types';
|
|
4
|
+
|
|
5
|
+
test('route fetch() returns a Response', async () => {
|
|
6
|
+
const route = createRoute().get((context) => {
|
|
7
|
+
return context.res.html('<h1>Hello World</h1>');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const request = new Request('http://localhost:3000/');
|
|
11
|
+
const response = await route.fetch(request);
|
|
12
|
+
|
|
13
|
+
expect(response).toBeInstanceOf(Response);
|
|
14
|
+
expect(response.status).toBe(200);
|
|
15
|
+
expect(await response.text()).toBe('<h1>Hello World</h1>');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('server with two routes can return Response from one', async () => {
|
|
19
|
+
const server = createServer({
|
|
20
|
+
appDir: './app',
|
|
21
|
+
staticFileRoot: './public',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Add two routes to the server
|
|
25
|
+
server.get('/users', (context) => {
|
|
26
|
+
return context.res.html('<h1>Users Page</h1>');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
server.get('/posts', (context) => {
|
|
30
|
+
return context.res.html('<h1>Posts Page</h1>');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// Test that we can get a Response from one of the routes
|
|
35
|
+
const request = new Request('http://localhost:3000/users');
|
|
36
|
+
const testRoute = server._routes.find((route) => route._path === '/users');
|
|
37
|
+
const response = await testRoute!.fetch(request);
|
|
38
|
+
|
|
39
|
+
expect(response).toBeInstanceOf(Response);
|
|
40
|
+
expect(response.status).toBe(200);
|
|
41
|
+
expect(await response.text()).toContain('Users Page');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('server returns a route with a POST request', async () => {
|
|
45
|
+
const server = createServer({
|
|
46
|
+
appDir: './app',
|
|
47
|
+
staticFileRoot: './public',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Add two routes to the server
|
|
51
|
+
server.get('/users', (context) => {
|
|
52
|
+
return context.res.html('<h1>GET /users</h1>');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
server.post('/users', (context) => {
|
|
56
|
+
return context.res.html('<h1>POST /users</h1>');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const route = server._routes.find((route) => route._path === '/users' && route._methods().includes('POST')) as HS.Route;
|
|
60
|
+
const request = new Request('http://localhost:3000/', { method: 'POST' });
|
|
61
|
+
const response = await route.fetch(request);
|
|
62
|
+
|
|
63
|
+
expect(response).toBeInstanceOf(Response);
|
|
64
|
+
expect(response.status).toBe(200);
|
|
65
|
+
expect(await response.text()).toBe('<h1>POST /users</h1>');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('returns 405 when route path matches but HTTP method does not', async () => {
|
|
69
|
+
const server = createServer({
|
|
70
|
+
appDir: './app',
|
|
71
|
+
staticFileRoot: './public',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Route registered for GET only
|
|
75
|
+
server.get('/users', (context) => {
|
|
76
|
+
return context.res.html('<h1>Users Page</h1>');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Attempt to POST to /users, which should return 405
|
|
80
|
+
const route = server._routes.find((route) => route._path === '/users')!;
|
|
81
|
+
const request = new Request('http://localhost:3000/users', { method: 'POST' });
|
|
82
|
+
const response = await route.fetch(request);
|
|
83
|
+
|
|
84
|
+
expect(response).toBeInstanceOf(Response);
|
|
85
|
+
expect(response.status).toBe(405);
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
expect(text).toContain('Method not allowed');
|
|
88
|
+
});
|