honertia 0.1.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/LICENSE +21 -0
- package/README.md +610 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +11 -0
- package/dist/effect/action.d.ts +107 -0
- package/dist/effect/action.d.ts.map +1 -0
- package/dist/effect/action.js +150 -0
- package/dist/effect/auth.d.ts +94 -0
- package/dist/effect/auth.d.ts.map +1 -0
- package/dist/effect/auth.js +204 -0
- package/dist/effect/bridge.d.ts +40 -0
- package/dist/effect/bridge.d.ts.map +1 -0
- package/dist/effect/bridge.js +103 -0
- package/dist/effect/errors.d.ts +78 -0
- package/dist/effect/errors.d.ts.map +1 -0
- package/dist/effect/errors.js +37 -0
- package/dist/effect/handler.d.ts +25 -0
- package/dist/effect/handler.d.ts.map +1 -0
- package/dist/effect/handler.js +120 -0
- package/dist/effect/index.d.ts +16 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +25 -0
- package/dist/effect/responses.d.ts +73 -0
- package/dist/effect/responses.d.ts.map +1 -0
- package/dist/effect/responses.js +104 -0
- package/dist/effect/routing.d.ts +90 -0
- package/dist/effect/routing.d.ts.map +1 -0
- package/dist/effect/routing.js +124 -0
- package/dist/effect/schema.d.ts +263 -0
- package/dist/effect/schema.d.ts.map +1 -0
- package/dist/effect/schema.js +586 -0
- package/dist/effect/services.d.ts +85 -0
- package/dist/effect/services.d.ts.map +1 -0
- package/dist/effect/services.js +24 -0
- package/dist/effect/validation.d.ts +38 -0
- package/dist/effect/validation.d.ts.map +1 -0
- package/dist/effect/validation.js +69 -0
- package/dist/helpers.d.ts +65 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +116 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/middleware.d.ts +14 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +113 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +4 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +34 -0
- package/dist/setup.d.ts +113 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +96 -0
- package/dist/test-utils.d.ts +105 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +210 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/package.json +71 -0
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Setup
|
|
3
|
+
*
|
|
4
|
+
* Provides a single setup function that configures all Honertia middleware.
|
|
5
|
+
* This is the recommended way to set up Honertia in your Hono app.
|
|
6
|
+
*/
|
|
7
|
+
import type { MiddlewareHandler, Env, Context } from 'hono';
|
|
8
|
+
import type { HonertiaConfig } from './types.js';
|
|
9
|
+
import { type EffectBridgeConfig } from './effect/bridge.js';
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for Honertia setup.
|
|
12
|
+
*/
|
|
13
|
+
export interface HonertiaSetupConfig<E extends Env = Env> {
|
|
14
|
+
/**
|
|
15
|
+
* Honertia core configuration.
|
|
16
|
+
*/
|
|
17
|
+
honertia: HonertiaConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Effect bridge configuration (optional).
|
|
20
|
+
*/
|
|
21
|
+
effect?: EffectBridgeConfig<E>;
|
|
22
|
+
/**
|
|
23
|
+
* Auth configuration (optional).
|
|
24
|
+
*/
|
|
25
|
+
auth?: {
|
|
26
|
+
userKey?: string;
|
|
27
|
+
sessionCookie?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Additional middleware to run after core Honertia setup.
|
|
31
|
+
* These run in order after effectBridge.
|
|
32
|
+
*/
|
|
33
|
+
middleware?: MiddlewareHandler<E>[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sets up all Honertia middleware in the correct order.
|
|
37
|
+
*
|
|
38
|
+
* This bundles:
|
|
39
|
+
* - `honertia()` - Core Honertia middleware
|
|
40
|
+
* - `loadUser()` - Loads authenticated user into context
|
|
41
|
+
* - `shareAuthMiddleware()` - Shares auth state with pages
|
|
42
|
+
* - `effectBridge()` - Sets up Effect runtime for each request
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { setupHonertia, createTemplate } from 'honertia'
|
|
47
|
+
*
|
|
48
|
+
* app.use('*', setupHonertia({
|
|
49
|
+
* honertia: {
|
|
50
|
+
* version: '1.0.0',
|
|
51
|
+
* render: createTemplate({ title: 'My App', scripts: [...] }),
|
|
52
|
+
* },
|
|
53
|
+
* }))
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function setupHonertia<E extends Env>(config: HonertiaSetupConfig<E>): MiddlewareHandler<E>;
|
|
57
|
+
/**
|
|
58
|
+
* Error handler configuration.
|
|
59
|
+
*/
|
|
60
|
+
export interface ErrorHandlerConfig {
|
|
61
|
+
/**
|
|
62
|
+
* Component to render for errors.
|
|
63
|
+
* @default 'Error'
|
|
64
|
+
*/
|
|
65
|
+
component?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Whether to show detailed error messages in development.
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
showDevErrors?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Environment variable key to check for development mode.
|
|
73
|
+
* @default 'ENVIRONMENT'
|
|
74
|
+
*/
|
|
75
|
+
envKey?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Value that indicates development mode.
|
|
78
|
+
* @default 'development'
|
|
79
|
+
*/
|
|
80
|
+
devValue?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Creates error handlers for Hono apps using Honertia.
|
|
84
|
+
*
|
|
85
|
+
* Returns an object with `notFound` and `onError` handlers
|
|
86
|
+
* that you can pass to app.notFound() and app.onError().
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const { notFound, onError } = createErrorHandlers()
|
|
91
|
+
* app.notFound(notFound)
|
|
92
|
+
* app.onError(onError)
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function createErrorHandlers<E extends Env>(config?: ErrorHandlerConfig): {
|
|
96
|
+
notFound: (c: Context<E>) => Response | Promise<Response>;
|
|
97
|
+
onError: (err: Error, c: Context<E>) => Response | Promise<Response>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Registers error handlers on a Hono app.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { registerErrorHandlers } from 'honertia'
|
|
105
|
+
*
|
|
106
|
+
* registerErrorHandlers(app)
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function registerErrorHandlers<E extends Env>(app: {
|
|
110
|
+
notFound: (handler: any) => void;
|
|
111
|
+
onError: (handler: any) => void;
|
|
112
|
+
}, config?: ErrorHandlerConfig): void;
|
|
113
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE1E;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACtD;;OAEG;IACH,QAAQ,EAAE,cAAc,CAAA;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAA;IAE9B;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IAED;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAA;CACpC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,GAAG,EACzC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC7B,iBAAiB,CAAC,CAAC,CAAC,CAmBtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,GAAE,kBAAuB;kBAQ3D,OAAO,CAAC,CAAC,CAAC;mBAOT,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC;EAU3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,GAAG,EACjD,GAAG,EAAE;IAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAA;CAAE,EAC1E,MAAM,GAAE,kBAAuB,GAC9B,IAAI,CAIN"}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Setup
|
|
3
|
+
*
|
|
4
|
+
* Provides a single setup function that configures all Honertia middleware.
|
|
5
|
+
* This is the recommended way to set up Honertia in your Hono app.
|
|
6
|
+
*/
|
|
7
|
+
import { createMiddleware } from 'hono/factory';
|
|
8
|
+
import { honertia } from './middleware.js';
|
|
9
|
+
import { loadUser, shareAuthMiddleware } from './effect/auth.js';
|
|
10
|
+
import { effectBridge } from './effect/bridge.js';
|
|
11
|
+
/**
|
|
12
|
+
* Sets up all Honertia middleware in the correct order.
|
|
13
|
+
*
|
|
14
|
+
* This bundles:
|
|
15
|
+
* - `honertia()` - Core Honertia middleware
|
|
16
|
+
* - `loadUser()` - Loads authenticated user into context
|
|
17
|
+
* - `shareAuthMiddleware()` - Shares auth state with pages
|
|
18
|
+
* - `effectBridge()` - Sets up Effect runtime for each request
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { setupHonertia, createTemplate } from 'honertia'
|
|
23
|
+
*
|
|
24
|
+
* app.use('*', setupHonertia({
|
|
25
|
+
* honertia: {
|
|
26
|
+
* version: '1.0.0',
|
|
27
|
+
* render: createTemplate({ title: 'My App', scripts: [...] }),
|
|
28
|
+
* },
|
|
29
|
+
* }))
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function setupHonertia(config) {
|
|
33
|
+
const middlewares = [
|
|
34
|
+
honertia(config.honertia),
|
|
35
|
+
loadUser(config.auth),
|
|
36
|
+
shareAuthMiddleware(),
|
|
37
|
+
effectBridge(config.effect),
|
|
38
|
+
...(config.middleware ?? []),
|
|
39
|
+
];
|
|
40
|
+
return createMiddleware(async (c, next) => {
|
|
41
|
+
const dispatch = async (i) => {
|
|
42
|
+
if (i >= middlewares.length) {
|
|
43
|
+
await next();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await middlewares[i](c, () => dispatch(i + 1));
|
|
47
|
+
};
|
|
48
|
+
await dispatch(0);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates error handlers for Hono apps using Honertia.
|
|
53
|
+
*
|
|
54
|
+
* Returns an object with `notFound` and `onError` handlers
|
|
55
|
+
* that you can pass to app.notFound() and app.onError().
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const { notFound, onError } = createErrorHandlers()
|
|
60
|
+
* app.notFound(notFound)
|
|
61
|
+
* app.onError(onError)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function createErrorHandlers(config = {}) {
|
|
65
|
+
const { component = 'Error', showDevErrors = true, envKey = 'ENVIRONMENT', devValue = 'development', } = config;
|
|
66
|
+
const notFound = (c) => {
|
|
67
|
+
return c.var.honertia.render(component, {
|
|
68
|
+
status: 404,
|
|
69
|
+
message: 'Page not found',
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const onError = (err, c) => {
|
|
73
|
+
console.error(err);
|
|
74
|
+
const isDev = showDevErrors && c.env?.[envKey] === devValue;
|
|
75
|
+
return c.var.honertia.render(component, {
|
|
76
|
+
status: 500,
|
|
77
|
+
message: isDev ? err.message : 'Something went wrong',
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
return { notFound, onError };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Registers error handlers on a Hono app.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { registerErrorHandlers } from 'honertia'
|
|
88
|
+
*
|
|
89
|
+
* registerErrorHandlers(app)
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function registerErrorHandlers(app, config = {}) {
|
|
93
|
+
const { notFound, onError } = createErrorHandlers(config);
|
|
94
|
+
app.notFound(notFound);
|
|
95
|
+
app.onError(onError);
|
|
96
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for testing Honertia middleware and components.
|
|
5
|
+
* Inspired by Inertia.js test patterns.
|
|
6
|
+
*/
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import type { PageObject } from './types.js';
|
|
9
|
+
export interface TestAppOptions {
|
|
10
|
+
version?: string | (() => string);
|
|
11
|
+
render?: (page: PageObject) => string | Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
export interface TestRequestOptions {
|
|
14
|
+
method?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
body?: BodyInit | null;
|
|
17
|
+
}
|
|
18
|
+
export interface InertiaRequestOptions extends TestRequestOptions {
|
|
19
|
+
version?: string;
|
|
20
|
+
partialComponent?: string;
|
|
21
|
+
partialData?: string;
|
|
22
|
+
partialExcept?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a test Hono app with honertia middleware configured
|
|
26
|
+
*/
|
|
27
|
+
export declare function createTestApp(options?: TestAppOptions): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
28
|
+
/**
|
|
29
|
+
* Makes a regular (non-Inertia) request
|
|
30
|
+
*/
|
|
31
|
+
export declare function makeRequest(app: Hono, path: string, options?: TestRequestOptions): Response | Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Makes an Inertia request with X-Inertia header
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeInertiaRequest(app: Hono, path: string, options?: InertiaRequestOptions): Response | Promise<Response>;
|
|
36
|
+
/**
|
|
37
|
+
* Parses an Inertia JSON response
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseInertiaResponse(res: Response): Promise<PageObject>;
|
|
40
|
+
/**
|
|
41
|
+
* Extracts page object from HTML response
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseHtmlResponse(res: Response): Promise<PageObject | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Asserts that a response is a valid Inertia JSON response
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertInertiaResponse(res: Response): void;
|
|
48
|
+
/**
|
|
49
|
+
* Asserts that a response is a valid HTML response
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertHtmlResponse(res: Response): void;
|
|
52
|
+
/**
|
|
53
|
+
* Asserts that a response is a version mismatch (409)
|
|
54
|
+
*/
|
|
55
|
+
export declare function assertVersionMismatch(res: Response, expectedLocation?: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Asserts that a page object has the expected structure
|
|
58
|
+
*/
|
|
59
|
+
export declare function assertPageObject(page: PageObject, expected: Partial<PageObject>): void;
|
|
60
|
+
export declare const mockUsers: {
|
|
61
|
+
id: number;
|
|
62
|
+
name: string;
|
|
63
|
+
email: string;
|
|
64
|
+
}[];
|
|
65
|
+
export declare const mockProjects: {
|
|
66
|
+
id: number;
|
|
67
|
+
name: string;
|
|
68
|
+
status: string;
|
|
69
|
+
}[];
|
|
70
|
+
export declare const mockErrors: {
|
|
71
|
+
email: string;
|
|
72
|
+
password: string;
|
|
73
|
+
};
|
|
74
|
+
export declare const edgeCaseStrings: {
|
|
75
|
+
emoji: string;
|
|
76
|
+
unicode: string;
|
|
77
|
+
multiByteEmoji: string;
|
|
78
|
+
htmlEntities: string;
|
|
79
|
+
quotes: string;
|
|
80
|
+
newlines: string;
|
|
81
|
+
specialChars: string;
|
|
82
|
+
longString: string;
|
|
83
|
+
emptyString: string;
|
|
84
|
+
whitespace: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Creates a delayed promise for testing async behavior
|
|
88
|
+
*/
|
|
89
|
+
export declare function delay(ms: number): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Creates a lazy prop that resolves after a delay
|
|
92
|
+
*/
|
|
93
|
+
export declare function lazyProp<T>(value: T, delayMs?: number): () => Promise<T>;
|
|
94
|
+
export interface RequestTracker {
|
|
95
|
+
requests: Array<{
|
|
96
|
+
path: string;
|
|
97
|
+
method: string;
|
|
98
|
+
timestamp: number;
|
|
99
|
+
}>;
|
|
100
|
+
track: (path: string, method: string) => void;
|
|
101
|
+
reset: () => void;
|
|
102
|
+
count: () => number;
|
|
103
|
+
}
|
|
104
|
+
export declare function createRequestTracker(): RequestTracker;
|
|
105
|
+
//# sourceMappingURL=test-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,YAAY,CAAA;AAM5D,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxD;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,8EAkBzD;AAMD;;GAEG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,IAAI,EACT,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,gCASjC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,EACT,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,qBAA0B,gCAsCpC;AAMD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAG7E;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CASjF;AAMD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,QAalD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,QAS/C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,EAAE,gBAAgB,CAAC,EAAE,MAAM,QAa7E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAY/E;AAMD,eAAO,MAAM,SAAS;;;;GAIrB,CAAA;AAED,eAAO,MAAM,YAAY;;;;GAGxB,CAAA;AAED,eAAO,MAAM,UAAU;;;CAGtB,CAAA;AAMD,eAAO,MAAM,eAAe;;;;;;;;;;;CAW3B,CAAA;AAMD;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,SAAI,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,CAOnE;AAMD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACpE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,KAAK,EAAE,MAAM,MAAM,CAAA;CACpB;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAerD"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for testing Honertia middleware and components.
|
|
5
|
+
* Inspired by Inertia.js test patterns.
|
|
6
|
+
*/
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import { honertia, HEADERS } from './middleware.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// App Factory
|
|
11
|
+
// =============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Creates a test Hono app with honertia middleware configured
|
|
14
|
+
*/
|
|
15
|
+
export function createTestApp(options = {}) {
|
|
16
|
+
const { version = '1.0.0', render = (page) => `<!DOCTYPE html><html><body><div id="app" data-page='${JSON.stringify(page)}'></div></body></html>`, } = options;
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.use('*', honertia({
|
|
19
|
+
version,
|
|
20
|
+
render,
|
|
21
|
+
}));
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Request Helpers
|
|
26
|
+
// =============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Makes a regular (non-Inertia) request
|
|
29
|
+
*/
|
|
30
|
+
export function makeRequest(app, path, options = {}) {
|
|
31
|
+
const { method = 'GET', headers = {}, body } = options;
|
|
32
|
+
return app.request(path, {
|
|
33
|
+
method,
|
|
34
|
+
headers,
|
|
35
|
+
body,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Makes an Inertia request with X-Inertia header
|
|
40
|
+
*/
|
|
41
|
+
export function makeInertiaRequest(app, path, options = {}) {
|
|
42
|
+
const { method = 'GET', headers = {}, body, version, partialComponent, partialData, partialExcept, } = options;
|
|
43
|
+
const inertiaHeaders = {
|
|
44
|
+
[HEADERS.HONERTIA]: 'true',
|
|
45
|
+
...headers,
|
|
46
|
+
};
|
|
47
|
+
if (version) {
|
|
48
|
+
inertiaHeaders[HEADERS.VERSION] = version;
|
|
49
|
+
}
|
|
50
|
+
if (partialComponent) {
|
|
51
|
+
inertiaHeaders[HEADERS.PARTIAL_COMPONENT] = partialComponent;
|
|
52
|
+
}
|
|
53
|
+
if (partialData) {
|
|
54
|
+
inertiaHeaders[HEADERS.PARTIAL_DATA] = partialData;
|
|
55
|
+
}
|
|
56
|
+
if (partialExcept) {
|
|
57
|
+
inertiaHeaders[HEADERS.PARTIAL_EXCEPT] = partialExcept;
|
|
58
|
+
}
|
|
59
|
+
return app.request(path, {
|
|
60
|
+
method,
|
|
61
|
+
headers: inertiaHeaders,
|
|
62
|
+
body,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Response Helpers
|
|
67
|
+
// =============================================================================
|
|
68
|
+
/**
|
|
69
|
+
* Parses an Inertia JSON response
|
|
70
|
+
*/
|
|
71
|
+
export async function parseInertiaResponse(res) {
|
|
72
|
+
const json = await res.json();
|
|
73
|
+
return json;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extracts page object from HTML response
|
|
77
|
+
*/
|
|
78
|
+
export async function parseHtmlResponse(res) {
|
|
79
|
+
const html = await res.text();
|
|
80
|
+
const match = html.match(/data-page='([^']+)'/);
|
|
81
|
+
if (match) {
|
|
82
|
+
return JSON.parse(match[1]);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Assertions
|
|
88
|
+
// =============================================================================
|
|
89
|
+
/**
|
|
90
|
+
* Asserts that a response is a valid Inertia JSON response
|
|
91
|
+
*/
|
|
92
|
+
export function assertInertiaResponse(res) {
|
|
93
|
+
if (res.status !== 200) {
|
|
94
|
+
throw new Error(`Expected status 200, got ${res.status}`);
|
|
95
|
+
}
|
|
96
|
+
const contentType = res.headers.get('Content-Type');
|
|
97
|
+
if (!contentType?.includes('application/json')) {
|
|
98
|
+
throw new Error(`Expected JSON content type, got ${contentType}`);
|
|
99
|
+
}
|
|
100
|
+
if (res.headers.get(HEADERS.HONERTIA) !== 'true') {
|
|
101
|
+
throw new Error(`Missing ${HEADERS.HONERTIA} header`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Asserts that a response is a valid HTML response
|
|
106
|
+
*/
|
|
107
|
+
export function assertHtmlResponse(res) {
|
|
108
|
+
if (res.status !== 200) {
|
|
109
|
+
throw new Error(`Expected status 200, got ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
const contentType = res.headers.get('Content-Type');
|
|
112
|
+
if (!contentType?.includes('text/html')) {
|
|
113
|
+
throw new Error(`Expected HTML content type, got ${contentType}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Asserts that a response is a version mismatch (409)
|
|
118
|
+
*/
|
|
119
|
+
export function assertVersionMismatch(res, expectedLocation) {
|
|
120
|
+
if (res.status !== 409) {
|
|
121
|
+
throw new Error(`Expected status 409, got ${res.status}`);
|
|
122
|
+
}
|
|
123
|
+
const location = res.headers.get(HEADERS.LOCATION);
|
|
124
|
+
if (!location) {
|
|
125
|
+
throw new Error(`Missing ${HEADERS.LOCATION} header`);
|
|
126
|
+
}
|
|
127
|
+
if (expectedLocation && !location.includes(expectedLocation)) {
|
|
128
|
+
throw new Error(`Expected location to include ${expectedLocation}, got ${location}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Asserts that a page object has the expected structure
|
|
133
|
+
*/
|
|
134
|
+
export function assertPageObject(page, expected) {
|
|
135
|
+
if (expected.component !== undefined && page.component !== expected.component) {
|
|
136
|
+
throw new Error(`Expected component ${expected.component}, got ${page.component}`);
|
|
137
|
+
}
|
|
138
|
+
if (expected.url !== undefined && page.url !== expected.url) {
|
|
139
|
+
throw new Error(`Expected url ${expected.url}, got ${page.url}`);
|
|
140
|
+
}
|
|
141
|
+
if (expected.version !== undefined && page.version !== expected.version) {
|
|
142
|
+
throw new Error(`Expected version ${expected.version}, got ${page.version}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Mock Data
|
|
147
|
+
// =============================================================================
|
|
148
|
+
export const mockUsers = [
|
|
149
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
150
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
151
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
152
|
+
];
|
|
153
|
+
export const mockProjects = [
|
|
154
|
+
{ id: 1, name: 'Project A', status: 'active' },
|
|
155
|
+
{ id: 2, name: 'Project B', status: 'archived' },
|
|
156
|
+
];
|
|
157
|
+
export const mockErrors = {
|
|
158
|
+
email: 'Invalid email address',
|
|
159
|
+
password: 'Password must be at least 8 characters',
|
|
160
|
+
};
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// Special Characters / Edge Cases
|
|
163
|
+
// =============================================================================
|
|
164
|
+
export const edgeCaseStrings = {
|
|
165
|
+
emoji: '😀🎉🚀',
|
|
166
|
+
unicode: '日本語テスト',
|
|
167
|
+
multiByteEmoji: '👨👩👧👦',
|
|
168
|
+
htmlEntities: '<script>alert("xss")</script>',
|
|
169
|
+
quotes: `"single'quotes"`,
|
|
170
|
+
newlines: 'line1\nline2\rline3\r\n',
|
|
171
|
+
specialChars: '& < > " \' / \\',
|
|
172
|
+
longString: 'a'.repeat(10000),
|
|
173
|
+
emptyString: '',
|
|
174
|
+
whitespace: ' \t\n ',
|
|
175
|
+
};
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Timing Utilities
|
|
178
|
+
// =============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Creates a delayed promise for testing async behavior
|
|
181
|
+
*/
|
|
182
|
+
export function delay(ms) {
|
|
183
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Creates a lazy prop that resolves after a delay
|
|
187
|
+
*/
|
|
188
|
+
export function lazyProp(value, delayMs = 0) {
|
|
189
|
+
return async () => {
|
|
190
|
+
if (delayMs > 0) {
|
|
191
|
+
await delay(delayMs);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export function createRequestTracker() {
|
|
197
|
+
const requests = [];
|
|
198
|
+
return {
|
|
199
|
+
requests,
|
|
200
|
+
track(path, method) {
|
|
201
|
+
requests.push({ path, method, timestamp: Date.now() });
|
|
202
|
+
},
|
|
203
|
+
reset() {
|
|
204
|
+
requests.length = 0;
|
|
205
|
+
},
|
|
206
|
+
count() {
|
|
207
|
+
return requests.length;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Types
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from 'hono';
|
|
5
|
+
export interface PageObject<TProps = Record<string, unknown>> {
|
|
6
|
+
component: string;
|
|
7
|
+
props: TProps & {
|
|
8
|
+
errors?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
url: string;
|
|
11
|
+
version: string;
|
|
12
|
+
clearHistory?: boolean;
|
|
13
|
+
encryptHistory?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface HonertiaConfig {
|
|
16
|
+
version: string | (() => string);
|
|
17
|
+
render: (page: PageObject, ctx?: Context) => string | Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
export interface RenderOptions {
|
|
20
|
+
clearHistory?: boolean;
|
|
21
|
+
encryptHistory?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface HonertiaInstance {
|
|
24
|
+
render<T extends Record<string, unknown>>(component: string, props?: T, options?: RenderOptions): Response | Promise<Response>;
|
|
25
|
+
share(key: string, value: unknown | (() => unknown | Promise<unknown>)): void;
|
|
26
|
+
getShared(): Record<string, unknown>;
|
|
27
|
+
setErrors(errors: Record<string, string>): void;
|
|
28
|
+
}
|
|
29
|
+
export declare const HEADERS: {
|
|
30
|
+
readonly HONERTIA: "X-Inertia";
|
|
31
|
+
readonly VERSION: "X-Inertia-Version";
|
|
32
|
+
readonly PARTIAL_COMPONENT: "X-Inertia-Partial-Component";
|
|
33
|
+
readonly PARTIAL_DATA: "X-Inertia-Partial-Data";
|
|
34
|
+
readonly PARTIAL_EXCEPT: "X-Inertia-Partial-Except";
|
|
35
|
+
readonly LOCATION: "X-Inertia-Location";
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,MAAM,WAAW,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IACnD,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACtE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,aAAa,GACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE/B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAA;IAC7E,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAChD;AAED,eAAO,MAAM,OAAO;;;;;;;CAOV,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Types
|
|
3
|
+
*/
|
|
4
|
+
export const HEADERS = {
|
|
5
|
+
HONERTIA: 'X-Inertia',
|
|
6
|
+
VERSION: 'X-Inertia-Version',
|
|
7
|
+
PARTIAL_COMPONENT: 'X-Inertia-Partial-Component',
|
|
8
|
+
PARTIAL_DATA: 'X-Inertia-Partial-Data',
|
|
9
|
+
PARTIAL_EXCEPT: 'X-Inertia-Partial-Except',
|
|
10
|
+
LOCATION: 'X-Inertia-Location',
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "honertia",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inertia.js-style server-driven SPA adapter for Hono",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./effect": {
|
|
15
|
+
"types": "./dist/effect/index.d.ts",
|
|
16
|
+
"import": "./dist/effect/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./schema": {
|
|
19
|
+
"types": "./dist/schema.d.ts",
|
|
20
|
+
"import": "./dist/schema.js"
|
|
21
|
+
},
|
|
22
|
+
"./auth": {
|
|
23
|
+
"types": "./dist/auth.d.ts",
|
|
24
|
+
"import": "./dist/auth.js"
|
|
25
|
+
},
|
|
26
|
+
"./react": {
|
|
27
|
+
"types": "./dist/react.d.ts",
|
|
28
|
+
"import": "./dist/react.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"test:watch": "bun test --watch",
|
|
41
|
+
"test:coverage": "bun test --coverage",
|
|
42
|
+
"prepublishOnly": "bun run build"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"hono",
|
|
46
|
+
"inertia",
|
|
47
|
+
"spa",
|
|
48
|
+
"react",
|
|
49
|
+
"cloudflare",
|
|
50
|
+
"workers"
|
|
51
|
+
],
|
|
52
|
+
"author": "Patrick Ogilvie",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/patrickogilvie/honertia"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"effect": "^3.12.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"hono": ">=4.0.0",
|
|
63
|
+
"better-auth": ">=1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/bun": "^1.1.0",
|
|
67
|
+
"better-auth": "^1.0.0",
|
|
68
|
+
"hono": "^4.6.0",
|
|
69
|
+
"typescript": "^5.3.0"
|
|
70
|
+
}
|
|
71
|
+
}
|