@nestjs-ssr/react 0.3.13 → 0.3.14

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/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CmxWHIK3.mjs';
1
+ export { P as PageContextProvider, a as PageProps, c as createSSRHooks, u as updatePageContext, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-DXjw-66u.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
  export { R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CUV31oda.js';
1
+ export { P as PageContextProvider, a as PageProps, c as createSSRHooks, u as updatePageContext, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-Bo3Z0m-_.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
  export { R as RenderContext } from './render-response.interface-ClWJXKL4.js';
@@ -257,6 +257,27 @@ interface RenderConfig {
257
257
  * ```
258
258
  */
259
259
  allowedHeaders?: string[];
260
+ /**
261
+ * Enable JSON API mode for content negotiation
262
+ *
263
+ * When enabled, routes with `@Render()` respond with JSON when the request
264
+ * includes `Accept: application/json`. The response body is the raw controller
265
+ * return value — no wrapper, no metadata.
266
+ *
267
+ * Can be overridden per-route via `@Render(Component, { jsonApi: true/false })`.
268
+ *
269
+ * @default false
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * // Enable globally
274
+ * RenderModule.forRoot({ jsonApi: true })
275
+ *
276
+ * // Then disable on specific routes
277
+ * @Render(SecretPage, { jsonApi: false })
278
+ * ```
279
+ */
280
+ jsonApi?: boolean;
260
281
  /**
261
282
  * Cookie names to pass to client
262
283
  * By default, no cookies are passed to client for security
@@ -708,7 +729,8 @@ declare class RenderInterceptor implements NestInterceptor {
708
729
  private allowedHeaders?;
709
730
  private allowedCookies?;
710
731
  private contextFactory?;
711
- constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined);
732
+ private jsonApiEnabled?;
733
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined, jsonApiEnabled?: boolean | undefined);
712
734
  /**
713
735
  * Resolve the layout hierarchy for a given route
714
736
  * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
@@ -735,6 +757,15 @@ declare class RenderInterceptor implements NestInterceptor {
735
757
  * The swap target's outlet will contain the filtered layouts.
736
758
  */
737
759
  private filterLayoutsFromSwapTarget;
760
+ /**
761
+ * Check if the request wants a JSON response via Accept header
762
+ */
763
+ private isJsonRequest;
764
+ /**
765
+ * Resolve whether JSON API is enabled for a given route.
766
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
767
+ */
768
+ private isJsonApiEnabled;
738
769
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
739
770
  }
740
771
 
@@ -257,6 +257,27 @@ interface RenderConfig {
257
257
  * ```
258
258
  */
259
259
  allowedHeaders?: string[];
260
+ /**
261
+ * Enable JSON API mode for content negotiation
262
+ *
263
+ * When enabled, routes with `@Render()` respond with JSON when the request
264
+ * includes `Accept: application/json`. The response body is the raw controller
265
+ * return value — no wrapper, no metadata.
266
+ *
267
+ * Can be overridden per-route via `@Render(Component, { jsonApi: true/false })`.
268
+ *
269
+ * @default false
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * // Enable globally
274
+ * RenderModule.forRoot({ jsonApi: true })
275
+ *
276
+ * // Then disable on specific routes
277
+ * @Render(SecretPage, { jsonApi: false })
278
+ * ```
279
+ */
280
+ jsonApi?: boolean;
260
281
  /**
261
282
  * Cookie names to pass to client
262
283
  * By default, no cookies are passed to client for security
@@ -708,7 +729,8 @@ declare class RenderInterceptor implements NestInterceptor {
708
729
  private allowedHeaders?;
709
730
  private allowedCookies?;
710
731
  private contextFactory?;
711
- constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined);
732
+ private jsonApiEnabled?;
733
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined, jsonApiEnabled?: boolean | undefined);
712
734
  /**
713
735
  * Resolve the layout hierarchy for a given route
714
736
  * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
@@ -735,6 +757,15 @@ declare class RenderInterceptor implements NestInterceptor {
735
757
  * The swap target's outlet will contain the filtered layouts.
736
758
  */
737
759
  private filterLayoutsFromSwapTarget;
760
+ /**
761
+ * Check if the request wants a JSON response via Accept header
762
+ */
763
+ private isJsonRequest;
764
+ /**
765
+ * Resolve whether JSON API is enabled for a given route.
766
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
767
+ */
768
+ private isJsonApiEnabled;
738
769
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
739
770
  }
740
771
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-DcpOFSp4.mjs';
1
+ export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-CF5JpFZh.mjs';
2
2
  import React, { ComponentType, ReactNode } from 'react';
3
- import { P as PageProps } from './use-page-context-CmxWHIK3.mjs';
4
- export { a as PageContextProvider, c as createSSRHooks, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CmxWHIK3.mjs';
3
+ import { a as PageProps } from './use-page-context-DXjw-66u.mjs';
4
+ export { P as PageContextProvider, c as createSSRHooks, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-DXjw-66u.mjs';
5
5
  import { R as RenderContext, H as HeadData, a as RenderResponse } from './render-response.interface-ClWJXKL4.mjs';
6
6
  import '@nestjs/common';
7
7
  import 'http';
@@ -101,6 +101,24 @@ interface PageComponentWithLayout<TPageProps = {}, TLayoutProps = {}> {
101
101
  layoutProps?: TLayoutProps;
102
102
  }
103
103
 
104
+ /**
105
+ * Type representing the JSON API response shape.
106
+ *
107
+ * When JSON API mode is enabled and a request includes `Accept: application/json`,
108
+ * the response body is the raw controller return value — this type.
109
+ *
110
+ * @typeParam T - The controller return type (same shape as the React component's data props)
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Controller returns { recipes: Recipe[], total: number }
115
+ * // JSON API response is typed as:
116
+ * type Response = JsonApiResponse<{ recipes: Recipe[]; total: number }>;
117
+ * // → { recipes: Recipe[]; total: number }
118
+ * ```
119
+ */
120
+ type JsonApiResponse<T> = T;
121
+
104
122
  /**
105
123
  * Extract the data type T from PageProps<T>.
106
124
  * PageProps<T> = T & { head?, context }, so we extract T by removing those keys.
@@ -134,6 +152,15 @@ interface RenderOptions {
134
152
  * Props to pass to the layout component
135
153
  */
136
154
  layoutProps?: Record<string, any>;
155
+ /**
156
+ * Enable or disable JSON API mode for this route.
157
+ * Overrides the module-level `jsonApi` setting.
158
+ *
159
+ * - `true`: This route serves JSON when `Accept: application/json` is sent
160
+ * - `false`: This route returns 406 for JSON requests
161
+ * - `undefined`: Uses the module-level setting (default `false`)
162
+ */
163
+ jsonApi?: boolean;
137
164
  }
138
165
  /**
139
166
  * Decorator to render a React component as the response.
@@ -222,4 +249,4 @@ interface LayoutDecoratorOptions {
222
249
  */
223
250
  declare function Layout(layout: LayoutComponent<any>, options?: LayoutDecoratorOptions): ClassDecorator;
224
251
 
225
- export { HeadData, Layout, type LayoutComponent, type LayoutDecoratorOptions, type LayoutProps, type PageComponentWithLayout, PageProps, Render, RenderContext, type RenderOptions, RenderResponse };
252
+ export { HeadData, type JsonApiResponse, Layout, type LayoutComponent, type LayoutDecoratorOptions, type LayoutProps, type PageComponentWithLayout, PageProps, Render, RenderContext, type RenderOptions, RenderResponse };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-CGfEDKI4.js';
1
+ export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-Cd0UvVmK.js';
2
2
  import React, { ComponentType, ReactNode } from 'react';
3
- import { P as PageProps } from './use-page-context-CUV31oda.js';
4
- export { a as PageContextProvider, c as createSSRHooks, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CUV31oda.js';
3
+ import { a as PageProps } from './use-page-context-Bo3Z0m-_.js';
4
+ export { P as PageContextProvider, c as createSSRHooks, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-Bo3Z0m-_.js';
5
5
  import { R as RenderContext, H as HeadData, a as RenderResponse } from './render-response.interface-ClWJXKL4.js';
6
6
  import '@nestjs/common';
7
7
  import 'http';
@@ -101,6 +101,24 @@ interface PageComponentWithLayout<TPageProps = {}, TLayoutProps = {}> {
101
101
  layoutProps?: TLayoutProps;
102
102
  }
103
103
 
104
+ /**
105
+ * Type representing the JSON API response shape.
106
+ *
107
+ * When JSON API mode is enabled and a request includes `Accept: application/json`,
108
+ * the response body is the raw controller return value — this type.
109
+ *
110
+ * @typeParam T - The controller return type (same shape as the React component's data props)
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Controller returns { recipes: Recipe[], total: number }
115
+ * // JSON API response is typed as:
116
+ * type Response = JsonApiResponse<{ recipes: Recipe[]; total: number }>;
117
+ * // → { recipes: Recipe[]; total: number }
118
+ * ```
119
+ */
120
+ type JsonApiResponse<T> = T;
121
+
104
122
  /**
105
123
  * Extract the data type T from PageProps<T>.
106
124
  * PageProps<T> = T & { head?, context }, so we extract T by removing those keys.
@@ -134,6 +152,15 @@ interface RenderOptions {
134
152
  * Props to pass to the layout component
135
153
  */
136
154
  layoutProps?: Record<string, any>;
155
+ /**
156
+ * Enable or disable JSON API mode for this route.
157
+ * Overrides the module-level `jsonApi` setting.
158
+ *
159
+ * - `true`: This route serves JSON when `Accept: application/json` is sent
160
+ * - `false`: This route returns 406 for JSON requests
161
+ * - `undefined`: Uses the module-level setting (default `false`)
162
+ */
163
+ jsonApi?: boolean;
137
164
  }
138
165
  /**
139
166
  * Decorator to render a React component as the response.
@@ -222,4 +249,4 @@ interface LayoutDecoratorOptions {
222
249
  */
223
250
  declare function Layout(layout: LayoutComponent<any>, options?: LayoutDecoratorOptions): ClassDecorator;
224
251
 
225
- export { HeadData, Layout, type LayoutComponent, type LayoutDecoratorOptions, type LayoutProps, type PageComponentWithLayout, PageProps, Render, RenderContext, type RenderOptions, RenderResponse };
252
+ export { HeadData, type JsonApiResponse, Layout, type LayoutComponent, type LayoutDecoratorOptions, type LayoutProps, type PageComponentWithLayout, PageProps, Render, RenderContext, type RenderOptions, RenderResponse };
package/dist/index.js CHANGED
@@ -1135,12 +1135,14 @@ exports.RenderInterceptor = class RenderInterceptor {
1135
1135
  allowedHeaders;
1136
1136
  allowedCookies;
1137
1137
  contextFactory;
1138
- constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1138
+ jsonApiEnabled;
1139
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory, jsonApiEnabled) {
1139
1140
  this.reflector = reflector;
1140
1141
  this.renderService = renderService;
1141
1142
  this.allowedHeaders = allowedHeaders;
1142
1143
  this.allowedCookies = allowedCookies;
1143
1144
  this.contextFactory = contextFactory;
1145
+ this.jsonApiEnabled = jsonApiEnabled;
1144
1146
  }
1145
1147
  /**
1146
1148
  * Resolve the layout hierarchy for a given route
@@ -1241,6 +1243,25 @@ exports.RenderInterceptor = class RenderInterceptor {
1241
1243
  const index = layouts.findIndex((l) => (l.layout.displayName || l.layout.name) === swapTarget);
1242
1244
  return index >= 0 ? layouts.slice(index + 1) : layouts;
1243
1245
  }
1246
+ /**
1247
+ * Check if the request wants a JSON response via Accept header
1248
+ */
1249
+ isJsonRequest(request) {
1250
+ const accept = request.headers["accept"];
1251
+ if (!accept || typeof accept !== "string") return false;
1252
+ return accept.includes("application/json");
1253
+ }
1254
+ /**
1255
+ * Resolve whether JSON API is enabled for a given route.
1256
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
1257
+ */
1258
+ isJsonApiEnabled(context) {
1259
+ const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
1260
+ if (renderOptions?.jsonApi !== void 0) {
1261
+ return renderOptions.jsonApi;
1262
+ }
1263
+ return this.jsonApiEnabled ?? false;
1264
+ }
1244
1265
  intercept(context, next) {
1245
1266
  const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
1246
1267
  if (!viewPathOrComponent) {
@@ -1292,6 +1313,17 @@ exports.RenderInterceptor = class RenderInterceptor {
1292
1313
  const renderResponse = isRenderResponse(data) ? data : {
1293
1314
  props: data
1294
1315
  };
1316
+ const hasSegmentHeader = !!request.headers["x-current-layouts"];
1317
+ if (!hasSegmentHeader && this.isJsonRequest(request)) {
1318
+ if (this.isJsonApiEnabled(context)) {
1319
+ response.type("application/json");
1320
+ return renderResponse.props;
1321
+ }
1322
+ throw new common.HttpException({
1323
+ error: "Not Acceptable",
1324
+ message: "JSON response not available for this route"
1325
+ }, common.HttpStatus.NOT_ACCEPTABLE);
1326
+ }
1295
1327
  const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
1296
1328
  const fullData = {
1297
1329
  data: renderResponse.props,
@@ -1337,13 +1369,16 @@ exports.RenderInterceptor = _ts_decorate6([
1337
1369
  _ts_param3(3, common.Inject("ALLOWED_COOKIES")),
1338
1370
  _ts_param3(4, common.Optional()),
1339
1371
  _ts_param3(4, common.Inject("CONTEXT_FACTORY")),
1372
+ _ts_param3(5, common.Optional()),
1373
+ _ts_param3(5, common.Inject("JSON_API")),
1340
1374
  _ts_metadata5("design:type", Function),
1341
1375
  _ts_metadata5("design:paramtypes", [
1342
1376
  typeof core.Reflector === "undefined" ? Object : core.Reflector,
1343
1377
  typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
1344
1378
  Array,
1345
1379
  Array,
1346
- typeof ContextFactory === "undefined" ? Object : ContextFactory
1380
+ typeof ContextFactory === "undefined" ? Object : ContextFactory,
1381
+ Boolean
1347
1382
  ])
1348
1383
  ], exports.RenderInterceptor);
1349
1384
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1607,6 +1642,10 @@ exports.RenderModule = class _RenderModule {
1607
1642
  provide: "ALLOWED_COOKIES",
1608
1643
  useValue: config?.allowedCookies || []
1609
1644
  });
1645
+ providers.push({
1646
+ provide: "JSON_API",
1647
+ useValue: config?.jsonApi ?? false
1648
+ });
1610
1649
  if (config?.context) {
1611
1650
  providers.push({
1612
1651
  provide: "CONTEXT_FACTORY",
@@ -1738,6 +1777,13 @@ exports.RenderModule = class _RenderModule {
1738
1777
  inject: [
1739
1778
  "RENDER_CONFIG"
1740
1779
  ]
1780
+ },
1781
+ {
1782
+ provide: "JSON_API",
1783
+ useFactory: /* @__PURE__ */ __name((config) => config?.jsonApi ?? false, "useFactory"),
1784
+ inject: [
1785
+ "RENDER_CONFIG"
1786
+ ]
1741
1787
  }
1742
1788
  ];
1743
1789
  return {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, Logger, Optional, Inject, Global, Module, SetMetadata } from '@nestjs/common';
1
+ import { Injectable, Logger, Optional, Inject, HttpException, HttpStatus, Global, Module, SetMetadata } from '@nestjs/common';
2
2
  import { Reflector, HttpAdapterHost, APP_INTERCEPTOR } from '@nestjs/core';
3
3
  import { existsSync, readFileSync } from 'fs';
4
4
  import { join, relative } from 'path';
@@ -1128,12 +1128,14 @@ var RenderInterceptor = class {
1128
1128
  allowedHeaders;
1129
1129
  allowedCookies;
1130
1130
  contextFactory;
1131
- constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1131
+ jsonApiEnabled;
1132
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory, jsonApiEnabled) {
1132
1133
  this.reflector = reflector;
1133
1134
  this.renderService = renderService;
1134
1135
  this.allowedHeaders = allowedHeaders;
1135
1136
  this.allowedCookies = allowedCookies;
1136
1137
  this.contextFactory = contextFactory;
1138
+ this.jsonApiEnabled = jsonApiEnabled;
1137
1139
  }
1138
1140
  /**
1139
1141
  * Resolve the layout hierarchy for a given route
@@ -1234,6 +1236,25 @@ var RenderInterceptor = class {
1234
1236
  const index = layouts.findIndex((l) => (l.layout.displayName || l.layout.name) === swapTarget);
1235
1237
  return index >= 0 ? layouts.slice(index + 1) : layouts;
1236
1238
  }
1239
+ /**
1240
+ * Check if the request wants a JSON response via Accept header
1241
+ */
1242
+ isJsonRequest(request) {
1243
+ const accept = request.headers["accept"];
1244
+ if (!accept || typeof accept !== "string") return false;
1245
+ return accept.includes("application/json");
1246
+ }
1247
+ /**
1248
+ * Resolve whether JSON API is enabled for a given route.
1249
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
1250
+ */
1251
+ isJsonApiEnabled(context) {
1252
+ const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
1253
+ if (renderOptions?.jsonApi !== void 0) {
1254
+ return renderOptions.jsonApi;
1255
+ }
1256
+ return this.jsonApiEnabled ?? false;
1257
+ }
1237
1258
  intercept(context, next) {
1238
1259
  const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
1239
1260
  if (!viewPathOrComponent) {
@@ -1285,6 +1306,17 @@ var RenderInterceptor = class {
1285
1306
  const renderResponse = isRenderResponse(data) ? data : {
1286
1307
  props: data
1287
1308
  };
1309
+ const hasSegmentHeader = !!request.headers["x-current-layouts"];
1310
+ if (!hasSegmentHeader && this.isJsonRequest(request)) {
1311
+ if (this.isJsonApiEnabled(context)) {
1312
+ response.type("application/json");
1313
+ return renderResponse.props;
1314
+ }
1315
+ throw new HttpException({
1316
+ error: "Not Acceptable",
1317
+ message: "JSON response not available for this route"
1318
+ }, HttpStatus.NOT_ACCEPTABLE);
1319
+ }
1288
1320
  const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
1289
1321
  const fullData = {
1290
1322
  data: renderResponse.props,
@@ -1330,13 +1362,16 @@ RenderInterceptor = _ts_decorate6([
1330
1362
  _ts_param3(3, Inject("ALLOWED_COOKIES")),
1331
1363
  _ts_param3(4, Optional()),
1332
1364
  _ts_param3(4, Inject("CONTEXT_FACTORY")),
1365
+ _ts_param3(5, Optional()),
1366
+ _ts_param3(5, Inject("JSON_API")),
1333
1367
  _ts_metadata5("design:type", Function),
1334
1368
  _ts_metadata5("design:paramtypes", [
1335
1369
  typeof Reflector === "undefined" ? Object : Reflector,
1336
1370
  typeof RenderService === "undefined" ? Object : RenderService,
1337
1371
  Array,
1338
1372
  Array,
1339
- typeof ContextFactory === "undefined" ? Object : ContextFactory
1373
+ typeof ContextFactory === "undefined" ? Object : ContextFactory,
1374
+ Boolean
1340
1375
  ])
1341
1376
  ], RenderInterceptor);
1342
1377
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1600,6 +1635,10 @@ var RenderModule = class _RenderModule {
1600
1635
  provide: "ALLOWED_COOKIES",
1601
1636
  useValue: config?.allowedCookies || []
1602
1637
  });
1638
+ providers.push({
1639
+ provide: "JSON_API",
1640
+ useValue: config?.jsonApi ?? false
1641
+ });
1603
1642
  if (config?.context) {
1604
1643
  providers.push({
1605
1644
  provide: "CONTEXT_FACTORY",
@@ -1731,6 +1770,13 @@ var RenderModule = class _RenderModule {
1731
1770
  inject: [
1732
1771
  "RENDER_CONFIG"
1733
1772
  ]
1773
+ },
1774
+ {
1775
+ provide: "JSON_API",
1776
+ useFactory: /* @__PURE__ */ __name((config) => config?.jsonApi ?? false, "useFactory"),
1777
+ inject: [
1778
+ "RENDER_CONFIG"
1779
+ ]
1734
1780
  }
1735
1781
  ];
1736
1782
  return {
@@ -1,4 +1,4 @@
1
- export { E as ErrorPageDevelopment, a as ErrorPageProduction, b as RenderInterceptor, c as RenderModule, d as RenderService, e as StreamingErrorHandler, T as TemplateParserService } from '../index-DcpOFSp4.mjs';
1
+ export { E as ErrorPageDevelopment, a as ErrorPageProduction, b as RenderInterceptor, c as RenderModule, d as RenderService, e as StreamingErrorHandler, T as TemplateParserService } from '../index-CF5JpFZh.mjs';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
4
  import '../render-response.interface-ClWJXKL4.mjs';
@@ -1,4 +1,4 @@
1
- export { E as ErrorPageDevelopment, a as ErrorPageProduction, b as RenderInterceptor, c as RenderModule, d as RenderService, e as StreamingErrorHandler, T as TemplateParserService } from '../index-CGfEDKI4.js';
1
+ export { E as ErrorPageDevelopment, a as ErrorPageProduction, b as RenderInterceptor, c as RenderModule, d as RenderService, e as StreamingErrorHandler, T as TemplateParserService } from '../index-Cd0UvVmK.js';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
4
  import '../render-response.interface-ClWJXKL4.js';
@@ -1116,12 +1116,14 @@ exports.RenderInterceptor = class RenderInterceptor {
1116
1116
  allowedHeaders;
1117
1117
  allowedCookies;
1118
1118
  contextFactory;
1119
- constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1119
+ jsonApiEnabled;
1120
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory, jsonApiEnabled) {
1120
1121
  this.reflector = reflector;
1121
1122
  this.renderService = renderService;
1122
1123
  this.allowedHeaders = allowedHeaders;
1123
1124
  this.allowedCookies = allowedCookies;
1124
1125
  this.contextFactory = contextFactory;
1126
+ this.jsonApiEnabled = jsonApiEnabled;
1125
1127
  }
1126
1128
  /**
1127
1129
  * Resolve the layout hierarchy for a given route
@@ -1222,6 +1224,25 @@ exports.RenderInterceptor = class RenderInterceptor {
1222
1224
  const index = layouts.findIndex((l) => (l.layout.displayName || l.layout.name) === swapTarget);
1223
1225
  return index >= 0 ? layouts.slice(index + 1) : layouts;
1224
1226
  }
1227
+ /**
1228
+ * Check if the request wants a JSON response via Accept header
1229
+ */
1230
+ isJsonRequest(request) {
1231
+ const accept = request.headers["accept"];
1232
+ if (!accept || typeof accept !== "string") return false;
1233
+ return accept.includes("application/json");
1234
+ }
1235
+ /**
1236
+ * Resolve whether JSON API is enabled for a given route.
1237
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
1238
+ */
1239
+ isJsonApiEnabled(context) {
1240
+ const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
1241
+ if (renderOptions?.jsonApi !== void 0) {
1242
+ return renderOptions.jsonApi;
1243
+ }
1244
+ return this.jsonApiEnabled ?? false;
1245
+ }
1225
1246
  intercept(context, next) {
1226
1247
  const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
1227
1248
  if (!viewPathOrComponent) {
@@ -1273,6 +1294,17 @@ exports.RenderInterceptor = class RenderInterceptor {
1273
1294
  const renderResponse = isRenderResponse(data) ? data : {
1274
1295
  props: data
1275
1296
  };
1297
+ const hasSegmentHeader = !!request.headers["x-current-layouts"];
1298
+ if (!hasSegmentHeader && this.isJsonRequest(request)) {
1299
+ if (this.isJsonApiEnabled(context)) {
1300
+ response.type("application/json");
1301
+ return renderResponse.props;
1302
+ }
1303
+ throw new common.HttpException({
1304
+ error: "Not Acceptable",
1305
+ message: "JSON response not available for this route"
1306
+ }, common.HttpStatus.NOT_ACCEPTABLE);
1307
+ }
1276
1308
  const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
1277
1309
  const fullData = {
1278
1310
  data: renderResponse.props,
@@ -1318,13 +1350,16 @@ exports.RenderInterceptor = _ts_decorate6([
1318
1350
  _ts_param3(3, common.Inject("ALLOWED_COOKIES")),
1319
1351
  _ts_param3(4, common.Optional()),
1320
1352
  _ts_param3(4, common.Inject("CONTEXT_FACTORY")),
1353
+ _ts_param3(5, common.Optional()),
1354
+ _ts_param3(5, common.Inject("JSON_API")),
1321
1355
  _ts_metadata5("design:type", Function),
1322
1356
  _ts_metadata5("design:paramtypes", [
1323
1357
  typeof core.Reflector === "undefined" ? Object : core.Reflector,
1324
1358
  typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
1325
1359
  Array,
1326
1360
  Array,
1327
- typeof ContextFactory === "undefined" ? Object : ContextFactory
1361
+ typeof ContextFactory === "undefined" ? Object : ContextFactory,
1362
+ Boolean
1328
1363
  ])
1329
1364
  ], exports.RenderInterceptor);
1330
1365
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1588,6 +1623,10 @@ exports.RenderModule = class _RenderModule {
1588
1623
  provide: "ALLOWED_COOKIES",
1589
1624
  useValue: config?.allowedCookies || []
1590
1625
  });
1626
+ providers.push({
1627
+ provide: "JSON_API",
1628
+ useValue: config?.jsonApi ?? false
1629
+ });
1591
1630
  if (config?.context) {
1592
1631
  providers.push({
1593
1632
  provide: "CONTEXT_FACTORY",
@@ -1719,6 +1758,13 @@ exports.RenderModule = class _RenderModule {
1719
1758
  inject: [
1720
1759
  "RENDER_CONFIG"
1721
1760
  ]
1761
+ },
1762
+ {
1763
+ provide: "JSON_API",
1764
+ useFactory: /* @__PURE__ */ __name((config) => config?.jsonApi ?? false, "useFactory"),
1765
+ inject: [
1766
+ "RENDER_CONFIG"
1767
+ ]
1722
1768
  }
1723
1769
  ];
1724
1770
  return {
@@ -1,4 +1,4 @@
1
- import { Injectable, Logger, Optional, Inject, Global, Module } from '@nestjs/common';
1
+ import { Injectable, Logger, Optional, Inject, HttpException, HttpStatus, Global, Module } from '@nestjs/common';
2
2
  import { Reflector, HttpAdapterHost, APP_INTERCEPTOR } from '@nestjs/core';
3
3
  import { existsSync, readFileSync } from 'fs';
4
4
  import { join, relative } from 'path';
@@ -1110,12 +1110,14 @@ var RenderInterceptor = class {
1110
1110
  allowedHeaders;
1111
1111
  allowedCookies;
1112
1112
  contextFactory;
1113
- constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1113
+ jsonApiEnabled;
1114
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory, jsonApiEnabled) {
1114
1115
  this.reflector = reflector;
1115
1116
  this.renderService = renderService;
1116
1117
  this.allowedHeaders = allowedHeaders;
1117
1118
  this.allowedCookies = allowedCookies;
1118
1119
  this.contextFactory = contextFactory;
1120
+ this.jsonApiEnabled = jsonApiEnabled;
1119
1121
  }
1120
1122
  /**
1121
1123
  * Resolve the layout hierarchy for a given route
@@ -1216,6 +1218,25 @@ var RenderInterceptor = class {
1216
1218
  const index = layouts.findIndex((l) => (l.layout.displayName || l.layout.name) === swapTarget);
1217
1219
  return index >= 0 ? layouts.slice(index + 1) : layouts;
1218
1220
  }
1221
+ /**
1222
+ * Check if the request wants a JSON response via Accept header
1223
+ */
1224
+ isJsonRequest(request) {
1225
+ const accept = request.headers["accept"];
1226
+ if (!accept || typeof accept !== "string") return false;
1227
+ return accept.includes("application/json");
1228
+ }
1229
+ /**
1230
+ * Resolve whether JSON API is enabled for a given route.
1231
+ * Priority: route-level @Render jsonApi → module-level jsonApi → false
1232
+ */
1233
+ isJsonApiEnabled(context) {
1234
+ const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
1235
+ if (renderOptions?.jsonApi !== void 0) {
1236
+ return renderOptions.jsonApi;
1237
+ }
1238
+ return this.jsonApiEnabled ?? false;
1239
+ }
1219
1240
  intercept(context, next) {
1220
1241
  const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
1221
1242
  if (!viewPathOrComponent) {
@@ -1267,6 +1288,17 @@ var RenderInterceptor = class {
1267
1288
  const renderResponse = isRenderResponse(data) ? data : {
1268
1289
  props: data
1269
1290
  };
1291
+ const hasSegmentHeader = !!request.headers["x-current-layouts"];
1292
+ if (!hasSegmentHeader && this.isJsonRequest(request)) {
1293
+ if (this.isJsonApiEnabled(context)) {
1294
+ response.type("application/json");
1295
+ return renderResponse.props;
1296
+ }
1297
+ throw new HttpException({
1298
+ error: "Not Acceptable",
1299
+ message: "JSON response not available for this route"
1300
+ }, HttpStatus.NOT_ACCEPTABLE);
1301
+ }
1270
1302
  const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
1271
1303
  const fullData = {
1272
1304
  data: renderResponse.props,
@@ -1312,13 +1344,16 @@ RenderInterceptor = _ts_decorate6([
1312
1344
  _ts_param3(3, Inject("ALLOWED_COOKIES")),
1313
1345
  _ts_param3(4, Optional()),
1314
1346
  _ts_param3(4, Inject("CONTEXT_FACTORY")),
1347
+ _ts_param3(5, Optional()),
1348
+ _ts_param3(5, Inject("JSON_API")),
1315
1349
  _ts_metadata5("design:type", Function),
1316
1350
  _ts_metadata5("design:paramtypes", [
1317
1351
  typeof Reflector === "undefined" ? Object : Reflector,
1318
1352
  typeof RenderService === "undefined" ? Object : RenderService,
1319
1353
  Array,
1320
1354
  Array,
1321
- typeof ContextFactory === "undefined" ? Object : ContextFactory
1355
+ typeof ContextFactory === "undefined" ? Object : ContextFactory,
1356
+ Boolean
1322
1357
  ])
1323
1358
  ], RenderInterceptor);
1324
1359
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1582,6 +1617,10 @@ var RenderModule = class _RenderModule {
1582
1617
  provide: "ALLOWED_COOKIES",
1583
1618
  useValue: config?.allowedCookies || []
1584
1619
  });
1620
+ providers.push({
1621
+ provide: "JSON_API",
1622
+ useValue: config?.jsonApi ?? false
1623
+ });
1585
1624
  if (config?.context) {
1586
1625
  providers.push({
1587
1626
  provide: "CONTEXT_FACTORY",
@@ -1713,6 +1752,13 @@ var RenderModule = class _RenderModule {
1713
1752
  inject: [
1714
1753
  "RENDER_CONFIG"
1715
1754
  ]
1755
+ },
1756
+ {
1757
+ provide: "JSON_API",
1758
+ useFactory: /* @__PURE__ */ __name((config) => config?.jsonApi ?? false, "useFactory"),
1759
+ inject: [
1760
+ "RENDER_CONFIG"
1761
+ ]
1716
1762
  }
1717
1763
  ];
1718
1764
  return {
@@ -279,4 +279,4 @@ declare const useHeader: (name: string) => string | undefined;
279
279
  declare const useCookies: () => Record<string, string>;
280
280
  declare const useCookie: (name: string) => string | undefined;
281
281
 
282
- export { type PageProps as P, PageContextProvider as a, useCookies as b, createSSRHooks as c, useHeader as d, useHeaders as e, usePageContext as f, useParams as g, useQuery as h, useRequest as i, updatePageContext as j, useCookie as u };
282
+ export { PageContextProvider as P, type PageProps as a, useCookie as b, createSSRHooks as c, useCookies as d, useHeader as e, useHeaders as f, usePageContext as g, useParams as h, useQuery as i, useRequest as j, updatePageContext as u };
@@ -279,4 +279,4 @@ declare const useHeader: (name: string) => string | undefined;
279
279
  declare const useCookies: () => Record<string, string>;
280
280
  declare const useCookie: (name: string) => string | undefined;
281
281
 
282
- export { type PageProps as P, PageContextProvider as a, useCookies as b, createSSRHooks as c, useHeader as d, useHeaders as e, usePageContext as f, useParams as g, useQuery as h, useRequest as i, updatePageContext as j, useCookie as u };
282
+ export { PageContextProvider as P, type PageProps as a, useCookie as b, createSSRHooks as c, useCookies as d, useHeader as e, useHeaders as f, usePageContext as g, useParams as h, useQuery as i, useRequest as j, updatePageContext as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "React SSR for NestJS that respects Clean Architecture. Proper DI, SOLID principles, clear separation of concerns.",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -141,11 +141,11 @@
141
141
  "dependencies": {
142
142
  "citty": "^0.2.1",
143
143
  "consola": "^3.4.2",
144
- "devalue": "^5.6.3",
144
+ "devalue": "^5.6.4",
145
145
  "escape-html": "^1.0.3"
146
146
  },
147
147
  "devDependencies": {
148
- "@microsoft/api-extractor": "^7.57.6",
148
+ "@microsoft/api-extractor": "^7.57.7",
149
149
  "@nestjs/common": "^11.1.16",
150
150
  "@nestjs/core": "^11.1.16",
151
151
  "@nestjs/platform-express": "^11.1.16",
@@ -155,14 +155,14 @@
155
155
  "@testing-library/react": "^16.3.2",
156
156
  "@types/escape-html": "^1.0.4",
157
157
  "@types/express": "^5.0.6",
158
- "@types/node": "^25.3.5",
158
+ "@types/node": "^25.5.0",
159
159
  "@types/react": "^19.2.14",
160
160
  "@types/react-dom": "^19.2.3",
161
161
  "@types/supertest": "^7.2.0",
162
162
  "@vitejs/plugin-react": "^5.1.4",
163
- "@vitest/coverage-v8": "^4.0.18",
164
- "@vitest/ui": "^4.0.18",
165
- "happy-dom": "^20.8.3",
163
+ "@vitest/coverage-v8": "^4.1.0",
164
+ "@vitest/ui": "^4.1.0",
165
+ "happy-dom": "^20.8.4",
166
166
  "react": "^19.2.4",
167
167
  "react-dom": "^19.2.4",
168
168
  "rxjs": "^7.8.2",
@@ -170,7 +170,7 @@
170
170
  "tsup": "^8.5.1",
171
171
  "typescript": "^5.9.3",
172
172
  "vite": "^7.3.1",
173
- "vitest": "^4.0.18"
173
+ "vitest": "^4.1.0"
174
174
  },
175
175
  "publishConfig": {
176
176
  "access": "public"