@inertianest/core 0.0.1-alpha.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/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@inertianest/core",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "Core module for Inertia.js in NestJS",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "keywords": [],
8
+ "author": "",
9
+ "license": "ISC",
10
+ "dependencies": {
11
+ "@inertianest/node": "0.0.1-alpha.1"
12
+ },
13
+ "peerDependencies": {
14
+ "vite": "^7.1.3"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.9.3"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc"
21
+ }
22
+ }
@@ -0,0 +1,2 @@
1
+ export const INERTIA_OPTIONS = Symbol('INERTIA_OPTIONS');
2
+ export const VITE_DEV_SERVER = Symbol('VITE_DEV_SERVER');
@@ -0,0 +1 @@
1
+ export * from './inertia.decorator.js';
@@ -0,0 +1,45 @@
1
+ import { SetMetadata, createParamDecorator, type ExecutionContext } from '@nestjs/common';
2
+ import type { Request, Response } from 'express';
3
+
4
+ export const INERTIA_COMPONENT = Symbol('INERTIA_COMPONENT');
5
+
6
+ export interface InertiaRenderOptions {
7
+ component: string;
8
+ props?: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * Decorator to mark a controller method as an Inertia render endpoint
13
+ *
14
+ * @param component The React component name to render (e.g., 'Dashboard', 'Users/Index')
15
+ * @param defaultProps Optional default props to pass to the component
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * @Get()
20
+ * @Inertia('Dashboard')
21
+ * async index() {
22
+ * return { stats: await this.dashboardService.getStats() };
23
+ * }
24
+ * ```
25
+ */
26
+ export const Inertia = (component: string, defaultProps?: Record<string, unknown>) =>
27
+ SetMetadata(INERTIA_COMPONENT, { component, props: defaultProps ?? {} } as InertiaRenderOptions);
28
+
29
+ /**
30
+ * Parameter decorator to get the Express Request object
31
+ */
32
+ export const InertiaRequest = createParamDecorator(
33
+ (_data: unknown, ctx: ExecutionContext): Request => {
34
+ return ctx.switchToHttp().getRequest<Request>();
35
+ },
36
+ );
37
+
38
+ /**
39
+ * Parameter decorator to get the Express Response object
40
+ */
41
+ export const InertiaResponse = createParamDecorator(
42
+ (_data: unknown, ctx: ExecutionContext): Response => {
43
+ return ctx.switchToHttp().getResponse<Response>();
44
+ },
45
+ );
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './constants.js';
2
+ export * from './inertia.module.js';
3
+ export * from './inertia.service.js';
4
+ export * from './inertia.interceptor.js';
5
+ export * from './nest-adapter.js';
6
+ export * from './decorators/index.js';
@@ -0,0 +1,48 @@
1
+ import {
2
+ Injectable,
3
+ type NestInterceptor,
4
+ type ExecutionContext,
5
+ type CallHandler,
6
+ } from '@nestjs/common';
7
+ import { Observable } from 'rxjs';
8
+ import { mergeMap } from 'rxjs/operators';
9
+ import { Reflector } from '@nestjs/core';
10
+ import type { Request, Response } from 'express';
11
+ import { InertiaService } from './inertia.service.js';
12
+ import { INERTIA_COMPONENT, type InertiaRenderOptions } from './decorators/inertia.decorator.js';
13
+
14
+ @Injectable()
15
+ export class InertiaInterceptor implements NestInterceptor {
16
+ constructor(
17
+ private readonly reflector: Reflector,
18
+ private readonly inertiaService: InertiaService,
19
+ ) {}
20
+
21
+ intercept(context: ExecutionContext, next: CallHandler): Observable<void> {
22
+ const renderOptions = this.reflector.get<InertiaRenderOptions>(
23
+ INERTIA_COMPONENT,
24
+ context.getHandler(),
25
+ );
26
+
27
+ // If no @Inertia decorator is present, pass through normally
28
+ if (!renderOptions) {
29
+ return next.handle();
30
+ }
31
+
32
+ const ctx = context.switchToHttp();
33
+ const request = ctx.getRequest<Request>();
34
+ const response = ctx.getResponse<Response>();
35
+
36
+ return next.handle().pipe(
37
+ mergeMap(async (data) => {
38
+ const props = { ...renderOptions.props, ...(data || {}) };
39
+ await this.inertiaService.render(
40
+ request,
41
+ response,
42
+ renderOptions.component,
43
+ props,
44
+ );
45
+ }),
46
+ );
47
+ }
48
+ }
@@ -0,0 +1,135 @@
1
+ import { type DynamicModule, Global, Module, type OnModuleInit } from '@nestjs/common';
2
+ import { APP_INTERCEPTOR } from '@nestjs/core';
3
+ import { InertiaService } from './inertia.service.js';
4
+ import { defineConfig, type SharedData } from '@inertianest/node';
5
+ import type { ViteDevServer } from 'vite';
6
+ import { resolve } from 'path';
7
+ import { INERTIA_OPTIONS, VITE_DEV_SERVER } from './constants.js';
8
+ import { InertiaInterceptor } from './inertia.interceptor.js';
9
+
10
+ export { INERTIA_OPTIONS, VITE_DEV_SERVER };
11
+
12
+ export interface InertiaModuleOptions<T extends SharedData = SharedData> {
13
+ /**
14
+ * The ID of the root element where Inertia renders
15
+ */
16
+ rootElementId?: string;
17
+
18
+ /**
19
+ * Asset version for cache busting
20
+ */
21
+ assetsVersion?: string;
22
+
23
+ /**
24
+ * Enable history encryption
25
+ */
26
+ encryptHistory?: boolean;
27
+
28
+ /**
29
+ * Shared data available on all pages
30
+ */
31
+ sharedData?: T;
32
+
33
+ /**
34
+ * Path to the HTML entrypoint for development
35
+ */
36
+ indexEntrypoint?: string;
37
+
38
+ /**
39
+ * Path to the HTML entrypoint for production
40
+ */
41
+ indexBuildEntrypoint?: string;
42
+
43
+ /**
44
+ * Enable SSR mode
45
+ */
46
+ ssrEnabled?: boolean;
47
+
48
+ /**
49
+ * Path to the SSR entrypoint for development
50
+ */
51
+ ssrEntrypoint?: string;
52
+
53
+ /**
54
+ * Path to the SSR entrypoint for production
55
+ */
56
+ ssrBuildEntrypoint?: string;
57
+ }
58
+
59
+ @Global()
60
+ @Module({})
61
+ export class InertiaModule implements OnModuleInit {
62
+ private static viteDevServer: ViteDevServer | undefined;
63
+
64
+
65
+ async onModuleInit() {
66
+ // Vite middleware is set up in forRoot
67
+ }
68
+
69
+ static async forRoot<T extends SharedData = SharedData>(
70
+ options: InertiaModuleOptions<T> = {},
71
+ ): Promise<DynamicModule> {
72
+ const isDevelopment = process.env.NODE_ENV !== 'production';
73
+ let viteDevServer: ViteDevServer | undefined;
74
+
75
+ // Create Vite dev server in development mode
76
+ // Use dynamic import to avoid bundling vite in production
77
+ if (isDevelopment) {
78
+ const { createServer: createViteServer } = await import('vite');
79
+ viteDevServer = await createViteServer({
80
+ configFile: resolve(process.cwd(), 'vite.config.ts'),
81
+ server: { middlewareMode: true },
82
+ });
83
+ this.viteDevServer = viteDevServer;
84
+ }
85
+
86
+ // Build the config for node-inertiajs
87
+ const ssrEnabled = options.ssrEnabled ?? false;
88
+ const baseConfig = {
89
+ rootElementId: options.rootElementId ?? 'app',
90
+ assetsVersion: options.assetsVersion ?? '1',
91
+ encryptHistory: options.encryptHistory ?? false,
92
+ ...(options.sharedData !== undefined && { sharedData: options.sharedData }),
93
+ indexEntrypoint: options.indexEntrypoint ?? resolve(process.cwd(), 'client/index.html'),
94
+ indexBuildEntrypoint:
95
+ options.indexBuildEntrypoint ?? resolve(process.cwd(), 'dist/client/index.html'),
96
+ };
97
+
98
+ // Use defineConfig to resolve the configuration
99
+ const resolvedConfig = ssrEnabled
100
+ ? defineConfig({
101
+ ...baseConfig,
102
+ ssrEnabled: true,
103
+ ssrEntrypoint: options.ssrEntrypoint!,
104
+ ssrBuildEntrypoint: options.ssrBuildEntrypoint!,
105
+ })
106
+ : defineConfig({
107
+ ...baseConfig,
108
+ ssrEnabled: false,
109
+ });
110
+
111
+ return {
112
+ module: InertiaModule,
113
+ providers: [
114
+ {
115
+ provide: INERTIA_OPTIONS,
116
+ useValue: resolvedConfig,
117
+ },
118
+ {
119
+ provide: VITE_DEV_SERVER,
120
+ useValue: viteDevServer,
121
+ },
122
+ InertiaService,
123
+ {
124
+ provide: APP_INTERCEPTOR,
125
+ useClass: InertiaInterceptor,
126
+ },
127
+ ],
128
+ exports: [InertiaService, INERTIA_OPTIONS, VITE_DEV_SERVER],
129
+ };
130
+ }
131
+
132
+ static getViteDevServer(): ViteDevServer | undefined {
133
+ return this.viteDevServer;
134
+ }
135
+ }
@@ -0,0 +1,59 @@
1
+ import { Inject, Injectable } from '@nestjs/common';
2
+ import type { Request, Response } from 'express';
3
+ import { Inertia, type ResolvedConfig, type Data } from '@inertianest/node';
4
+ import type { ViteDevServer } from 'vite';
5
+ import { NestAdapter } from './nest-adapter.js';
6
+ import { INERTIA_OPTIONS, VITE_DEV_SERVER } from './constants.js';
7
+
8
+ @Injectable()
9
+ export class InertiaService {
10
+ constructor(
11
+ @Inject(INERTIA_OPTIONS) private readonly config: ResolvedConfig,
12
+ @Inject(VITE_DEV_SERVER) private readonly viteDevServer: ViteDevServer | undefined,
13
+ ) {}
14
+
15
+ /**
16
+ * Create an Inertia instance for the current request
17
+ */
18
+ createInertia(req: Request, res: Response): Inertia {
19
+ const adapter = new NestAdapter(req, res);
20
+ // Cast to any to handle Vite version differences
21
+ return new Inertia(adapter, this.config, this.viteDevServer as any);
22
+ }
23
+
24
+ /**
25
+ * Render an Inertia response
26
+ */
27
+ async render<T extends Record<string, unknown>>(
28
+ req: Request,
29
+ res: Response,
30
+ component: string,
31
+ props?: T,
32
+ ): Promise<void> {
33
+ const inertia = this.createInertia(req, res);
34
+ await inertia.render(component, props);
35
+ }
36
+
37
+ /**
38
+ * Share data for the current request
39
+ */
40
+ share(req: Request, res: Response, data: Record<string, Data>): Inertia {
41
+ const inertia = this.createInertia(req, res);
42
+ inertia.share(data);
43
+ return inertia;
44
+ }
45
+
46
+ /**
47
+ * Get the Vite dev server for middleware setup
48
+ */
49
+ getViteDevServer(): ViteDevServer | undefined {
50
+ return this.viteDevServer;
51
+ }
52
+
53
+ /**
54
+ * Get the resolved config
55
+ */
56
+ getConfig(): ResolvedConfig {
57
+ return this.config;
58
+ }
59
+ }
@@ -0,0 +1,89 @@
1
+ import { Adapter } from '@inertianest/node';
2
+ import type { Request, Response } from 'express';
3
+
4
+ /**
5
+ * NestJS/Express adapter for node-inertiajs
6
+ */
7
+ export class NestAdapter extends Adapter {
8
+ constructor(
9
+ private readonly req: Request,
10
+ private readonly res: Response,
11
+ ) {
12
+ super();
13
+ }
14
+
15
+ get url(): string {
16
+ return this.req.originalUrl || this.req.url || '/';
17
+ }
18
+
19
+ get method(): string {
20
+ return this.req.method || 'GET';
21
+ }
22
+
23
+ get statusCode(): number {
24
+ return this.res.statusCode || 200;
25
+ }
26
+
27
+ set statusCode(code: number) {
28
+ this.res.statusCode = code;
29
+ }
30
+
31
+ get request(): Request {
32
+ return this.req;
33
+ }
34
+
35
+ get response(): Response {
36
+ return this.res;
37
+ }
38
+
39
+ getHeader(name: string): string | string[] | undefined {
40
+ return this.req.headers[name.toLowerCase()] as string | string[] | undefined;
41
+ }
42
+
43
+ setHeader(name: string, value: string): void {
44
+ this.res.setHeader(name.toLowerCase(), value);
45
+ }
46
+
47
+ send(content: string): void {
48
+ this.res.setHeader('Content-Type', 'text/html');
49
+ this.res.end(content);
50
+ }
51
+
52
+ json(data: unknown): void {
53
+ try {
54
+ this.res.setHeader('Content-Type', 'application/json');
55
+ this.res.end(JSON.stringify(data));
56
+ } catch {
57
+ this.res.statusCode = 500;
58
+ this.res.end(JSON.stringify({ error: 'Failed to serialize JSON' }));
59
+ }
60
+ }
61
+
62
+ redirect(statusOrUrl: number | string, url?: string): void {
63
+ let status = 302;
64
+ let location = '';
65
+
66
+ if (typeof statusOrUrl === 'number' && typeof url === 'string') {
67
+ status = statusOrUrl;
68
+ location = url;
69
+ } else if (typeof statusOrUrl === 'string') {
70
+ location = statusOrUrl;
71
+ }
72
+
73
+ const encodedLocation = encodeURI(location);
74
+ this.statusCode = status;
75
+ this.setHeader('Location', encodedLocation);
76
+
77
+ const body = this.req?.headers?.accept?.includes('html')
78
+ ? `<p>${status}. Redirecting to <a href="${encodedLocation}">${encodedLocation}</a></p>`
79
+ : `${status}. Redirecting to ${encodedLocation}`;
80
+
81
+ this.setHeader('Content-Length', Buffer.byteLength(body).toString());
82
+
83
+ if (this.method === 'HEAD') {
84
+ this.res.end();
85
+ } else {
86
+ this.res.end(body);
87
+ }
88
+ }
89
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ // "rootDir": "./src",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+
13
+ // Decorator support (required for NestJS)
14
+ "experimentalDecorators": true,
15
+ "emitDecoratorMetadata": true,
16
+ // For nodejs:
17
+ // "lib": ["esnext"],
18
+ "types": ["node"],
19
+ // and npm install -D @types/node
20
+
21
+ // Other Outputs
22
+ "sourceMap": true,
23
+ "declaration": true,
24
+ "declarationMap": true,
25
+
26
+ // Stricter Typechecking Options
27
+ "noUncheckedIndexedAccess": true,
28
+ "exactOptionalPropertyTypes": true,
29
+
30
+ // Style Options
31
+ // "noImplicitReturns": true,
32
+ // "noImplicitOverride": true,
33
+ // "noUnusedLocals": true,
34
+ // "noUnusedParameters": true,
35
+ // "noFallthroughCasesInSwitch": true,
36
+ // "noPropertyAccessFromIndexSignature": true,
37
+
38
+ // Recommended Options
39
+ "strict": true,
40
+ "jsx": "react-jsx",
41
+ "verbatimModuleSyntax": true,
42
+ "isolatedModules": true,
43
+ "noUncheckedSideEffectImports": true,
44
+ "moduleDetection": "force",
45
+ "skipLibCheck": true
46
+ },
47
+ "include": ["src"],
48
+ "exclude": ["node_modules"]
49
+ }
50
+