@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 +22 -0
- package/src/constants.ts +2 -0
- package/src/decorators/index.ts +1 -0
- package/src/decorators/inertia.decorator.ts +45 -0
- package/src/index.ts +6 -0
- package/src/inertia.interceptor.ts +48 -0
- package/src/inertia.module.ts +135 -0
- package/src/inertia.service.ts +59 -0
- package/src/nest-adapter.ts +89 -0
- package/tsconfig.json +50 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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,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
|
+
|