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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +610 -0
  3. package/dist/auth.d.ts +10 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +11 -0
  6. package/dist/effect/action.d.ts +107 -0
  7. package/dist/effect/action.d.ts.map +1 -0
  8. package/dist/effect/action.js +150 -0
  9. package/dist/effect/auth.d.ts +94 -0
  10. package/dist/effect/auth.d.ts.map +1 -0
  11. package/dist/effect/auth.js +204 -0
  12. package/dist/effect/bridge.d.ts +40 -0
  13. package/dist/effect/bridge.d.ts.map +1 -0
  14. package/dist/effect/bridge.js +103 -0
  15. package/dist/effect/errors.d.ts +78 -0
  16. package/dist/effect/errors.d.ts.map +1 -0
  17. package/dist/effect/errors.js +37 -0
  18. package/dist/effect/handler.d.ts +25 -0
  19. package/dist/effect/handler.d.ts.map +1 -0
  20. package/dist/effect/handler.js +120 -0
  21. package/dist/effect/index.d.ts +16 -0
  22. package/dist/effect/index.d.ts.map +1 -0
  23. package/dist/effect/index.js +25 -0
  24. package/dist/effect/responses.d.ts +73 -0
  25. package/dist/effect/responses.d.ts.map +1 -0
  26. package/dist/effect/responses.js +104 -0
  27. package/dist/effect/routing.d.ts +90 -0
  28. package/dist/effect/routing.d.ts.map +1 -0
  29. package/dist/effect/routing.js +124 -0
  30. package/dist/effect/schema.d.ts +263 -0
  31. package/dist/effect/schema.d.ts.map +1 -0
  32. package/dist/effect/schema.js +586 -0
  33. package/dist/effect/services.d.ts +85 -0
  34. package/dist/effect/services.d.ts.map +1 -0
  35. package/dist/effect/services.js +24 -0
  36. package/dist/effect/validation.d.ts +38 -0
  37. package/dist/effect/validation.d.ts.map +1 -0
  38. package/dist/effect/validation.js +69 -0
  39. package/dist/helpers.d.ts +65 -0
  40. package/dist/helpers.d.ts.map +1 -0
  41. package/dist/helpers.js +116 -0
  42. package/dist/index.d.ts +14 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +26 -0
  45. package/dist/middleware.d.ts +14 -0
  46. package/dist/middleware.d.ts.map +1 -0
  47. package/dist/middleware.js +113 -0
  48. package/dist/react.d.ts +17 -0
  49. package/dist/react.d.ts.map +1 -0
  50. package/dist/react.js +4 -0
  51. package/dist/schema.d.ts +9 -0
  52. package/dist/schema.d.ts.map +1 -0
  53. package/dist/schema.js +34 -0
  54. package/dist/setup.d.ts +113 -0
  55. package/dist/setup.d.ts.map +1 -0
  56. package/dist/setup.js +96 -0
  57. package/dist/test-utils.d.ts +105 -0
  58. package/dist/test-utils.d.ts.map +1 -0
  59. package/dist/test-utils.js +210 -0
  60. package/dist/types.d.ts +37 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +11 -0
  63. package/package.json +71 -0
@@ -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
+ }
@@ -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
+ }