@hyperspan/framework 2.0.0-alpha.7 → 2.0.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/dist/actions.js +109 -0
- package/dist/callsites.js +18 -0
- package/dist/client/css.js +6 -0
- package/dist/client/js.js +314 -0
- package/dist/client/manifest.js +55 -0
- package/dist/cookies.js +142 -0
- package/dist/dev-bindings.js +15 -0
- package/dist/fetch-handler.js +108 -0
- package/dist/html.js +1 -0
- package/dist/index.js +50 -0
- package/dist/island.js +17 -0
- package/dist/layout.js +42 -0
- package/dist/middleware.js +89 -0
- package/dist/register-routes.js +41 -0
- package/dist/server.js +580 -0
- package/dist/ssr/install-server-dom-mock.js +2 -0
- package/dist/ssr/mock-dom.js +370 -0
- package/dist/utils.js +157 -0
- package/package.json +33 -18
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +1 -1
- package/src/adapter.test.ts +1 -5
- package/src/client/js.test.ts +82 -17
- package/src/client/js.ts +163 -52
- package/src/client/manifest.ts +21 -12
- package/src/dev-bindings.ts +14 -0
- package/src/fetch-handler.test.ts +23 -0
- package/src/fetch-handler.ts +4 -3
- package/src/html.test.ts +13 -0
- package/src/html.ts +1 -0
- package/src/index.ts +3 -0
- package/src/island.ts +1 -1
- package/src/layout.test.ts +1 -1
- package/src/layout.ts +1 -1
- package/src/server.test.ts +49 -2
- package/src/server.ts +34 -18
- package/src/types.ts +28 -11
- package/.claude/settings.local.json +0 -9
- package/tsconfig.json +0 -25
package/src/island.ts
CHANGED
package/src/layout.test.ts
CHANGED
package/src/layout.ts
CHANGED
package/src/server.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect, describe } from 'vitest';
|
|
2
|
-
import { createRoute, createServer, createContext } from './server';
|
|
2
|
+
import { createRoute, createServer, createContext, initServerRoutes } from './server';
|
|
3
3
|
import { createAction } from './actions';
|
|
4
|
-
import { html, placeholder } from '
|
|
4
|
+
import { html, placeholder } from './html';
|
|
5
5
|
import type { Hyperspan as HS } from './types';
|
|
6
6
|
|
|
7
7
|
test('route fetch() returns a Response', async () => {
|
|
@@ -92,6 +92,25 @@ test('server returns a route with a POST request', async () => {
|
|
|
92
92
|
expect(await response.text()).toBe('<h1>POST /users</h1>');
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
test('POST HTML response does not inherit request Content-Length', async () => {
|
|
96
|
+
const route = createRoute().post((context: HS.Context) => {
|
|
97
|
+
return context.res.html('<h1>hello world this is longer than the form body</h1>');
|
|
98
|
+
});
|
|
99
|
+
const body = 'email=a@b.com&password=secret';
|
|
100
|
+
const request = new Request('http://localhost:3000/', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
104
|
+
'content-length': String(body.length),
|
|
105
|
+
},
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
const response = await route.fetch(request);
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
expect(text).toBe('<h1>hello world this is longer than the form body</h1>');
|
|
111
|
+
expect(Number(response.headers.get('content-length') ?? text.length)).toBe(text.length);
|
|
112
|
+
});
|
|
113
|
+
|
|
95
114
|
test('server returns a route with a ALL request', async () => {
|
|
96
115
|
const server = await createServer({
|
|
97
116
|
appDir: './app',
|
|
@@ -566,3 +585,31 @@ describe('when streaming is disabled', () => {
|
|
|
566
585
|
expect(text).not.toContain('hs:loading');
|
|
567
586
|
});
|
|
568
587
|
});
|
|
588
|
+
|
|
589
|
+
test('initServerRoutes runs beforeRoutesAdded, then addRoutes, then afterRoutesAdded', async () => {
|
|
590
|
+
const order: string[] = [];
|
|
591
|
+
const server = await createServer({
|
|
592
|
+
appDir: './app',
|
|
593
|
+
publicDir: './public',
|
|
594
|
+
plugins: [],
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await initServerRoutes(
|
|
598
|
+
server,
|
|
599
|
+
{
|
|
600
|
+
beforeRoutesAdded(s) {
|
|
601
|
+
order.push('before');
|
|
602
|
+
s.use(async (_c, next) => next());
|
|
603
|
+
},
|
|
604
|
+
afterRoutesAdded() {
|
|
605
|
+
order.push('after');
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
() => {
|
|
609
|
+
order.push('routes');
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
expect(order).toEqual(['before', 'routes', 'after']);
|
|
614
|
+
expect(server._middleware['*']).toHaveLength(1);
|
|
615
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import './ssr/install-server-dom-mock';
|
|
2
|
-
import {
|
|
3
|
-
HSHtml,
|
|
4
|
-
html,
|
|
5
|
-
isHSHtml,
|
|
6
|
-
renderStream,
|
|
7
|
-
renderAsync,
|
|
8
|
-
render,
|
|
9
|
-
_typeOf,
|
|
10
|
-
} from '@hyperspan/html';
|
|
2
|
+
import { HSHtml, html, isHSHtml, renderStream, renderAsync, render, _typeOf } from './html';
|
|
11
3
|
import { isbot } from 'isbot';
|
|
12
4
|
import { executeMiddleware } from './middleware';
|
|
13
5
|
import { buildUrl, parsePath, removeUndefined } from './utils';
|
|
@@ -75,7 +67,8 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
75
67
|
const url = new URL(req.url);
|
|
76
68
|
const query = new URLSearchParams(url.search);
|
|
77
69
|
const method = req.method.toUpperCase();
|
|
78
|
-
const
|
|
70
|
+
const requestHeaders = new Headers(req.headers);
|
|
71
|
+
const responseHeaders = new Headers();
|
|
79
72
|
const path = route?._path() || '/';
|
|
80
73
|
const requestParams = (req as Request & { params?: Record<string, string | undefined> }).params;
|
|
81
74
|
const params: Record<string, string | undefined> = Object.assign(
|
|
@@ -95,12 +88,21 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
95
88
|
// Status override for the response. Will use if set. (e.g. c.res.status = 400)
|
|
96
89
|
let status: number | undefined = undefined;
|
|
97
90
|
|
|
91
|
+
const copyHeaders = (source: Headers, target: Headers) => {
|
|
92
|
+
source.forEach((value, key) => {
|
|
93
|
+
if (key.toLowerCase() === 'set-cookie') return;
|
|
94
|
+
target.set(key, value);
|
|
95
|
+
});
|
|
96
|
+
const cookies = typeof source.getSetCookie === 'function' ? source.getSetCookie() : [];
|
|
97
|
+
for (const cookie of cookies) {
|
|
98
|
+
target.append('Set-Cookie', cookie);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
98
102
|
const merge = async (response: Response) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...Object.fromEntries(response.headers.entries()),
|
|
103
|
-
};
|
|
103
|
+
const mergedHeaders = new Headers();
|
|
104
|
+
copyHeaders(responseHeaders, mergedHeaders);
|
|
105
|
+
copyHeaders(response.headers, mergedHeaders);
|
|
104
106
|
|
|
105
107
|
return new Response(await response.text(), {
|
|
106
108
|
status: context.res.status ?? response.status,
|
|
@@ -124,7 +126,7 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
124
126
|
raw: req,
|
|
125
127
|
url,
|
|
126
128
|
method,
|
|
127
|
-
headers,
|
|
129
|
+
headers: requestHeaders,
|
|
128
130
|
query,
|
|
129
131
|
cookies: new Cookies(req),
|
|
130
132
|
async text() {
|
|
@@ -141,8 +143,8 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
141
143
|
},
|
|
142
144
|
},
|
|
143
145
|
res: {
|
|
144
|
-
cookies: new Cookies(req,
|
|
145
|
-
headers,
|
|
146
|
+
cookies: new Cookies(req, responseHeaders),
|
|
147
|
+
headers: responseHeaders,
|
|
146
148
|
status,
|
|
147
149
|
html: (html: string, options?: ResponseInit) =>
|
|
148
150
|
merge(
|
|
@@ -504,6 +506,20 @@ export async function createServer(config: HS.Config = {} as HS.Config): Promise
|
|
|
504
506
|
return api;
|
|
505
507
|
}
|
|
506
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Shared route lifecycle for Vite dev, production entries, and `hyperspan start`.
|
|
511
|
+
* Always: `beforeRoutesAdded` → add routes → `afterRoutesAdded`.
|
|
512
|
+
*/
|
|
513
|
+
export async function initServerRoutes(
|
|
514
|
+
server: HS.Server,
|
|
515
|
+
config: Pick<HS.Config, 'beforeRoutesAdded' | 'afterRoutesAdded'>,
|
|
516
|
+
addRoutes: (server: HS.Server) => void | Promise<void>
|
|
517
|
+
): Promise<void> {
|
|
518
|
+
await config.beforeRoutesAdded?.(server);
|
|
519
|
+
await addRoutes(server);
|
|
520
|
+
await config.afterRoutesAdded?.(server);
|
|
521
|
+
}
|
|
522
|
+
|
|
507
523
|
/**
|
|
508
524
|
* Checks if a response is HTML content
|
|
509
525
|
*/
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HSHtml } from '
|
|
1
|
+
import { HSHtml } from './html';
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -69,8 +69,12 @@ export namespace Hyperspan {
|
|
|
69
69
|
outDir: string;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
export type ServerCreateContext = {
|
|
73
|
+
env: unknown;
|
|
74
|
+
};
|
|
75
|
+
|
|
72
76
|
export type DeployEntryContext = {
|
|
73
|
-
createHyperspanServer: () => Promise<Server>;
|
|
77
|
+
createHyperspanServer: (ctx: ServerCreateContext) => Promise<Server>;
|
|
74
78
|
config: Config;
|
|
75
79
|
};
|
|
76
80
|
|
|
@@ -79,6 +83,18 @@ export namespace Hyperspan {
|
|
|
79
83
|
fetch?(request: Request, env?: unknown, ctx?: unknown): Response | Promise<Response>;
|
|
80
84
|
};
|
|
81
85
|
|
|
86
|
+
/** Optional snippets interpolated into generated `dist/server.ts`. Null/omitted slots emit nothing. */
|
|
87
|
+
export type ServerEntrySlots = {
|
|
88
|
+
beforeFileContent?: string | null;
|
|
89
|
+
beforeCreateServer?: string | null;
|
|
90
|
+
afterCreateServer?: string | null;
|
|
91
|
+
beforeRoutes?: string | null;
|
|
92
|
+
afterRoutes?: string | null;
|
|
93
|
+
afterFileContent?: string | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ServerEntryTemplate = (slots?: ServerEntrySlots) => string;
|
|
97
|
+
|
|
82
98
|
export type Adapter = {
|
|
83
99
|
/** Diagnostic name, e.g. `node`, `cloudflare`. */
|
|
84
100
|
name: string;
|
|
@@ -88,18 +104,17 @@ export namespace Hyperspan {
|
|
|
88
104
|
*/
|
|
89
105
|
devModule?: string;
|
|
90
106
|
resolveDevEnv?(root: string): Promise<unknown>;
|
|
91
|
-
/**
|
|
92
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Render generated `dist/server.ts` from the shared bootstrap template.
|
|
109
|
+
* Fill slots (especially `afterFileContent`) for the platform entry.
|
|
110
|
+
*/
|
|
111
|
+
renderServerEntry(template: ServerEntryTemplate): string | Promise<string>;
|
|
93
112
|
afterBuild?(ctx: AdapterAfterBuildContext): void | Promise<void>;
|
|
94
113
|
};
|
|
95
114
|
|
|
96
115
|
/** Runtime adapter object (`nodeAdapter()`, `cloudflareAdapter()`, …). */
|
|
97
116
|
export type DeployAdapter = Adapter;
|
|
98
117
|
|
|
99
|
-
export type ServerCreateContext = {
|
|
100
|
-
env: unknown;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
118
|
export type Config = {
|
|
104
119
|
appDir: string;
|
|
105
120
|
publicDir: string;
|
|
@@ -118,8 +133,8 @@ export namespace Hyperspan {
|
|
|
118
133
|
*/
|
|
119
134
|
beforeServerCreate?: (ctx: ServerCreateContext) => void | Promise<void>;
|
|
120
135
|
// For customizing the routes and adding your own...
|
|
121
|
-
beforeRoutesAdded?: (server: Hyperspan.Server) => void
|
|
122
|
-
afterRoutesAdded?: (server: Hyperspan.Server) => void
|
|
136
|
+
beforeRoutesAdded?: (server: Hyperspan.Server) => void | Promise<void>;
|
|
137
|
+
afterRoutesAdded?: (server: Hyperspan.Server) => void | Promise<void>;
|
|
123
138
|
responseOptions?: ResponseOptions;
|
|
124
139
|
};
|
|
125
140
|
|
|
@@ -415,7 +430,7 @@ export namespace Hyperspan {
|
|
|
415
430
|
* Client JS Module = ESM Module + Public Path + Render Script Tag
|
|
416
431
|
*/
|
|
417
432
|
export type ClientJSBuildResult = {
|
|
418
|
-
assetHash: string; //
|
|
433
|
+
assetHash: string; // Identity of the resolved path; production file hash comes from Vite
|
|
419
434
|
esmName: string; // Filename of the built JavaScript file without the extension
|
|
420
435
|
publicPath: string; // Full public path of the built JavaScript file
|
|
421
436
|
/**
|
|
@@ -443,6 +458,8 @@ export type Adapter = Hyperspan.Adapter;
|
|
|
443
458
|
export type AdapterAfterBuildContext = Hyperspan.AdapterAfterBuildContext;
|
|
444
459
|
export type DeployEntry = Hyperspan.DeployEntry;
|
|
445
460
|
export type DeployEntryContext = Hyperspan.DeployEntryContext;
|
|
461
|
+
export type ServerEntrySlots = Hyperspan.ServerEntrySlots;
|
|
462
|
+
export type ServerEntryTemplate = Hyperspan.ServerEntryTemplate;
|
|
446
463
|
|
|
447
464
|
declare global {
|
|
448
465
|
interface DocumentEventMap {
|
package/tsconfig.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compileOnSave": true,
|
|
3
|
-
"include": ["src/**/*.ts"],
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"outDir": "dist",
|
|
6
|
-
"target": "ES2020",
|
|
7
|
-
"lib": ["ESNext", "dom", "dom.iterable"],
|
|
8
|
-
"module": "esnext",
|
|
9
|
-
"moduleResolution": "bundler",
|
|
10
|
-
"moduleDetection": "force",
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"strict": true,
|
|
15
|
-
"declaration": true,
|
|
16
|
-
"sourceMap": true,
|
|
17
|
-
"downlevelIteration": true,
|
|
18
|
-
"skipLibCheck": true,
|
|
19
|
-
"allowSyntheticDefaultImports": true,
|
|
20
|
-
"forceConsistentCasingInFileNames": true,
|
|
21
|
-
"allowJs": true,
|
|
22
|
-
"baseUrl": "."
|
|
23
|
-
},
|
|
24
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
25
|
-
}
|