@application-kit/nestjs-vite 0.3.0 → 0.5.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 (49) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +64 -3
  3. package/dist/cjs/content-security-policy/content-security-policy.config.d.ts +7 -0
  4. package/dist/cjs/content-security-policy/content-security-policy.config.js +44 -0
  5. package/dist/cjs/content-security-policy/content-security-policy.environment.d.ts +5 -0
  6. package/dist/cjs/content-security-policy/content-security-policy.environment.js +24 -0
  7. package/dist/cjs/content-security-policy/content-security-policy.interface.d.ts +4 -0
  8. package/dist/cjs/content-security-policy/content-security-policy.interface.js +2 -0
  9. package/dist/cjs/content-security-policy/index.d.ts +3 -0
  10. package/dist/cjs/content-security-policy/index.js +19 -0
  11. package/dist/cjs/index.d.ts +1 -0
  12. package/dist/cjs/index.js +1 -0
  13. package/dist/cjs/renderer/html-template.d.ts +1 -1
  14. package/dist/cjs/renderer/html-template.js +5 -4
  15. package/dist/cjs/renderer/render-html-page.js +4 -1
  16. package/dist/cjs/renderer/vite-application-renderer.d.ts +3 -1
  17. package/dist/cjs/renderer/vite-application-renderer.js +8 -3
  18. package/dist/cjs/vite-application-renderer.interface.d.ts +1 -0
  19. package/dist/cjs/vite-plugin/application-vite-config-options.interface.d.ts +16 -0
  20. package/dist/cjs/vite-plugin/application-vite-config-options.interface.js +2 -0
  21. package/dist/cjs/vite-plugin/define-application-vite-config.d.ts +3 -0
  22. package/dist/cjs/vite-plugin/define-application-vite-config.js +89 -0
  23. package/dist/cjs/vite-plugin/index.d.ts +2 -0
  24. package/dist/cjs/vite-plugin/index.js +2 -0
  25. package/dist/cjs/vite-ssr.module.js +7 -1
  26. package/dist/esm/content-security-policy/content-security-policy.config.d.ts +7 -0
  27. package/dist/esm/content-security-policy/content-security-policy.config.js +41 -0
  28. package/dist/esm/content-security-policy/content-security-policy.environment.d.ts +5 -0
  29. package/dist/esm/content-security-policy/content-security-policy.environment.js +20 -0
  30. package/dist/esm/content-security-policy/content-security-policy.interface.d.ts +4 -0
  31. package/dist/esm/content-security-policy/content-security-policy.interface.js +1 -0
  32. package/dist/esm/content-security-policy/index.d.ts +3 -0
  33. package/dist/esm/content-security-policy/index.js +3 -0
  34. package/dist/esm/index.d.ts +1 -0
  35. package/dist/esm/index.js +1 -0
  36. package/dist/esm/renderer/html-template.d.ts +1 -1
  37. package/dist/esm/renderer/html-template.js +5 -4
  38. package/dist/esm/renderer/render-html-page.js +4 -1
  39. package/dist/esm/renderer/vite-application-renderer.d.ts +3 -1
  40. package/dist/esm/renderer/vite-application-renderer.js +8 -3
  41. package/dist/esm/vite-application-renderer.interface.d.ts +1 -0
  42. package/dist/esm/vite-plugin/application-vite-config-options.interface.d.ts +16 -0
  43. package/dist/esm/vite-plugin/application-vite-config-options.interface.js +1 -0
  44. package/dist/esm/vite-plugin/define-application-vite-config.d.ts +3 -0
  45. package/dist/esm/vite-plugin/define-application-vite-config.js +82 -0
  46. package/dist/esm/vite-plugin/index.d.ts +2 -0
  47. package/dist/esm/vite-plugin/index.js +2 -0
  48. package/dist/esm/vite-ssr.module.js +7 -1
  49. package/package.json +7 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @application-kit/nestjs-vite
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9909a33: HTML-страницы получают `Content-Security-Policy` с новым nonce на каждый запрос, и тот же nonce стоит на теге состояния. Режим задаёт `CONTENT_SECURITY_POLICY_MODE`: `enforce` по умолчанию, `report-only` или `off` для отката. В `peerDependencies` добавлены `@libs-for-dev/nestjs-config-module`, `class-transformer` и `class-validator`. Клиенту с `zod` нужен `z.config({ jitless: true })`, иначе его проба `eval` пишет нарушение в консоль.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - ed011f8: `defineApplicationViteConfig({ root, appPath, clientHtml, environment, plugins, buildPlugins })` во входе `/vite-plugin`
14
+ (и в корне) — весь `vite.config` Nest-SSR-приложения объектом. Контракт выходных имён: серверные входы `[name].mjs` и
15
+ чанки `[hash].mjs` в корне `dist`, клиент — `assets/[hash].js`, ассеты — `assets/[hash][extname]`, карт нет. Серверную
16
+ сборку отличает `--ssr` в `process.argv`; порты dev-сервера и HMR — из `environment`. Плагины клиента и плагины только
17
+ сборки приходят параметрами, зависимостей у пакета не прибавилось.
18
+
3
19
  ## 0.3.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -26,6 +26,49 @@ export default defineConfig({
26
26
  статическими расширениями и внутренние пути Vite уходят в Vite, остальные — в Nest; при
27
27
  горячем обновлении приложение пересоздаётся.
28
28
 
29
+ ## Vite-конфиг приложения
30
+
31
+ `defineApplicationViteConfig(options)` из `@application-kit/nestjs-vite/vite-plugin` — весь `vite.config`
32
+ Nest-SSR-приложения: dev-сервер Nest (`vitePluginNode`), серверные входы `vite build --ssr <вход>` и клиентская сборка
33
+ в один `dist`.
34
+
35
+ ```ts
36
+ // vite.config.mts
37
+ import { defineApplicationViteConfig } from '@application-kit/nestjs-vite/vite-plugin'
38
+
39
+ export default defineApplicationViteConfig({
40
+ appPath: path.resolve(import.meta.dirname, 'src/create-web-application.ts'),
41
+ buildPlugins: [cleanup()],
42
+ clientHtml: path.resolve(import.meta.dirname, 'src/client/index.html'),
43
+ environment: process.env,
44
+ plugins: [vue(), htmlMinifier({ minify: true })],
45
+ root: import.meta.dirname,
46
+ })
47
+ ```
48
+
49
+ - Конфиг — объект, а не функция: `vitest.config` приложения мержит его как объект. Серверную сборку отличает флаг
50
+ `--ssr` в `process.argv` в момент вызова.
51
+ - Пресет не знает о фреймворке клиента: его плагины, обработка HTML и script-теги модулей приходят в `plugins` (после
52
+ плагина Nest), плагины только сборки (rolldown) — в `buildPlugins`.
53
+ - Окружение: `PORT` (dev-сервер, 3000), `VITE_SERVER_HMR_PORT` и `VITE_SERVER_HMR_CLIENT_PORT` (HMR, 3001), `NODE_ENV=test`
54
+ выключает прогрев клиентского графа — в e2e браузера нет, а SSR-запросы ждали бы этот фон.
55
+ - Алиас `@` — `<root>/src`; `optimizeDeps` — `pino` без автопоиска; логгер Vite без предупреждений «Sourcemap for …» о
56
+ картах зависимостей.
57
+
58
+ Контракт выходных имён — на него опираются команды запуска, образ и раздача статики приложения:
59
+
60
+ | Что | Имя |
61
+ |---|---|
62
+ | серверный вход (`web`, `cli`, `consumer`, `tracing`, `ormconfig`, …) | `dist/[name].mjs` — без хеша, запускается по имени |
63
+ | серверный чанк | `dist/[hash].mjs` |
64
+ | клиентский вход и чанки | `dist/assets/[hash].js` |
65
+ | ассеты | `dist/assets/[hash][extname]` |
66
+ | карты `*.map` | нет |
67
+
68
+ Серверный код — `.mjs`: в `dist` нет `package.json` с `"type": "module"`, и `.js` Node прочёл бы как CommonJS. Клиент
69
+ лежит только под `assets/`: каталог отдаётся наружу с вечным кешем, имена контент-адресные, серверного кода и карт в нём
70
+ нет.
71
+
29
72
  ## SSR-страница
30
73
 
31
74
  ```ts
@@ -61,17 +104,35 @@ export class ClientModule {}
61
104
 
62
105
  | Константа | В шаблоне | Чем заменяется |
63
106
  |---|---|---|
64
- | `STATE_PLACEHOLDER` | `<script id="state"></script>` | `<script>` с присваиваниями `window[ключ] = значение` |
107
+ | `STATE_PLACEHOLDER` | `<script id="state"></script>` | `<script id="state" nonce="…">` с присваиваниями `window[ключ] = значение` |
65
108
  | `TEMPLATE_PLACEHOLDER` | `<div id="app"></div>` | тот же `div` с разметкой `template` внутри |
66
109
  | `LANG_ATTRIBUTE_PATTERN` | `<html lang="…">` с любым значением | `lang` страницы |
67
110
 
68
111
  Значения состояния уходят JS-литералами строки с экранированным `<`: `</script>` в данных не закроет тег, какой бы
69
112
  сериализатор ни выбрало приложение. Клиентский энтри читает их до гидрации.
70
113
 
71
- `renderHtmlPage(renderer, request, reply)` — один путь HTML-ответа: ставит статус рендера и `Content-Type: text/html`
72
- и возвращает HTML. Контроллер с `@Res({ passthrough: true })` возвращает его, фильтр — отправляет. Декоратор
114
+ `renderHtmlPage(renderer, request, reply)` — один путь HTML-ответа: ставит статус рендера, `Content-Type: text/html` и
115
+ заголовки рендера и возвращает HTML. Контроллер с `@Res({ passthrough: true })` возвращает его, фильтр — отправляет. Декоратор
73
116
  `@Header('Content-Type')` на маршруте страницы не нужен.
74
117
 
118
+ ### Content Security Policy
119
+
120
+ Рендерер выпускает на каждый ответ новый nonce и ставит его в заголовок и на тег состояния:
121
+ `script-src 'self' 'nonce-…'; object-src 'none'; base-uri 'self'`. Заголовок ставит пакет, потому что значение меняется
122
+ с каждым запросом, а прокси перед приложением этого не умеют. Страницу с nonce нельзя кешировать на прокси или CDN: все
123
+ посетители получили бы один nonce.
124
+
125
+ Режим — переменная `CONTENT_SECURITY_POLICY_MODE`, проверяется при старте:
126
+
127
+ | Значение | Заголовок |
128
+ |---|---|
129
+ | `enforce` (по умолчанию) | `Content-Security-Policy` |
130
+ | `report-only` | `Content-Security-Policy-Report-Only` |
131
+ | `off` | нет, и nonce на тег не ставится |
132
+
133
+ `eval` и `new Function` политика запрещает, и браузер пишет нарушение в консоль, даже если код перехватывает ошибку. Так
134
+ поступает проба `allowsEval` в `zod`: клиенту с `zod` нужен `z.config({ jitless: true })` до первой схемы.
135
+
75
136
  ### Фильтры
76
137
 
77
138
  - `NotFoundFilter` — регистрирует `ViteSsrModule`: `NotFoundException` отвечает отрендеренной страницей
@@ -0,0 +1,7 @@
1
+ import type { ContentSecurityPolicyInterface } from './content-security-policy.interface.js';
2
+ import { ContentSecurityPolicyEnvironment } from './content-security-policy.environment.js';
3
+ export declare class ContentSecurityPolicyConfig {
4
+ private readonly environment;
5
+ constructor(environment: ContentSecurityPolicyEnvironment);
6
+ issue(): ContentSecurityPolicyInterface;
7
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ContentSecurityPolicyConfig = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_crypto_1 = require("node:crypto");
18
+ const content_security_policy_environment_js_1 = require("./content-security-policy.environment.js");
19
+ const NONCE_BYTES = 16;
20
+ const HEADER_BY_MODE = {
21
+ enforce: 'Content-Security-Policy',
22
+ 'report-only': 'Content-Security-Policy-Report-Only',
23
+ };
24
+ const buildPolicy = (nonce) => `script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'`;
25
+ let ContentSecurityPolicyConfig = class ContentSecurityPolicyConfig {
26
+ environment;
27
+ constructor(environment) {
28
+ this.environment = environment;
29
+ }
30
+ issue() {
31
+ const mode = this.environment.CONTENT_SECURITY_POLICY_MODE;
32
+ if (mode === 'off') {
33
+ return { headers: {}, nonce: undefined };
34
+ }
35
+ const nonce = (0, node_crypto_1.randomBytes)(NONCE_BYTES).toBase64();
36
+ return { headers: { [HEADER_BY_MODE[mode]]: buildPolicy(nonce) }, nonce };
37
+ }
38
+ };
39
+ exports.ContentSecurityPolicyConfig = ContentSecurityPolicyConfig;
40
+ exports.ContentSecurityPolicyConfig = ContentSecurityPolicyConfig = __decorate([
41
+ (0, common_1.Injectable)(),
42
+ __param(0, (0, common_1.Inject)(content_security_policy_environment_js_1.ContentSecurityPolicyEnvironment)),
43
+ __metadata("design:paramtypes", [content_security_policy_environment_js_1.ContentSecurityPolicyEnvironment])
44
+ ], ContentSecurityPolicyConfig);
@@ -0,0 +1,5 @@
1
+ export declare const CONTENT_SECURITY_POLICY_MODES: readonly ["enforce", "off", "report-only"];
2
+ export type ContentSecurityPolicyMode = (typeof CONTENT_SECURITY_POLICY_MODES)[number];
3
+ export declare class ContentSecurityPolicyEnvironment {
4
+ CONTENT_SECURITY_POLICY_MODE: ContentSecurityPolicyMode;
5
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ContentSecurityPolicyEnvironment = exports.CONTENT_SECURITY_POLICY_MODES = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ exports.CONTENT_SECURITY_POLICY_MODES = ['enforce', 'off', 'report-only'];
16
+ class ContentSecurityPolicyEnvironment {
17
+ CONTENT_SECURITY_POLICY_MODE = 'enforce';
18
+ }
19
+ exports.ContentSecurityPolicyEnvironment = ContentSecurityPolicyEnvironment;
20
+ __decorate([
21
+ (0, class_transformer_1.Expose)(),
22
+ (0, class_validator_1.IsIn)(exports.CONTENT_SECURITY_POLICY_MODES),
23
+ __metadata("design:type", String)
24
+ ], ContentSecurityPolicyEnvironment.prototype, "CONTENT_SECURITY_POLICY_MODE", void 0);
@@ -0,0 +1,4 @@
1
+ export interface ContentSecurityPolicyInterface {
2
+ readonly headers: Readonly<Record<string, string>>;
3
+ readonly nonce: string | undefined;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export * from './content-security-policy.config.js';
2
+ export * from './content-security-policy.environment.js';
3
+ export * from './content-security-policy.interface.js';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./content-security-policy.config.js"), exports);
18
+ __exportStar(require("./content-security-policy.environment.js"), exports);
19
+ __exportStar(require("./content-security-policy.interface.js"), exports);
@@ -1,3 +1,4 @@
1
+ export * from './content-security-policy/index.js';
1
2
  export * from './filters/index.js';
2
3
  export * from './html-loader/index.js';
3
4
  export * from './renderer/index.js';
package/dist/cjs/index.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./content-security-policy/index.js"), exports);
17
18
  __exportStar(require("./filters/index.js"), exports);
18
19
  __exportStar(require("./html-loader/index.js"), exports);
19
20
  __exportStar(require("./renderer/index.js"), exports);
@@ -2,4 +2,4 @@ import type { SsrPageInterface } from '../ssr-entry/ssr-entry.interface.js';
2
2
  export declare const STATE_PLACEHOLDER = "<script id=\"state\"></script>";
3
3
  export declare const TEMPLATE_PLACEHOLDER = "<div id=\"app\"></div>";
4
4
  export declare const LANG_ATTRIBUTE_PATTERN: RegExp;
5
- export declare const fillHtmlTemplate: (html: string, page: SsrPageInterface) => string;
5
+ export declare const fillHtmlTemplate: (html: string, page: SsrPageInterface, nonce?: string) => string;
@@ -6,13 +6,14 @@ exports.TEMPLATE_PLACEHOLDER = '<div id="app"></div>';
6
6
  exports.LANG_ATTRIBUTE_PATTERN = /<html\s+lang="[^"]*"/u;
7
7
  const toScriptLiteral = (value) => JSON.stringify(value).replaceAll('<', String.raw `\u003C`);
8
8
  const toAttributeValue = (value) => value.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
9
- const toStateScript = (state) => {
9
+ const toStateScript = (state, nonce) => {
10
10
  const assignments = Object.entries(state)
11
11
  .map(([key, value]) => `window[${toScriptLiteral(key)}] = ${toScriptLiteral(value)};`);
12
- return `<script>${assignments.join('')}</script>`;
12
+ const nonceAttribute = nonce === undefined ? '' : ` nonce="${toAttributeValue(nonce)}"`;
13
+ return `<script id="state"${nonceAttribute}>${assignments.join('')}</script>`;
13
14
  };
14
- const fillHtmlTemplate = (html, page) => html
15
- .replace(exports.STATE_PLACEHOLDER, () => toStateScript(page.state))
15
+ const fillHtmlTemplate = (html, page, nonce) => html
16
+ .replace(exports.STATE_PLACEHOLDER, () => toStateScript(page.state, nonce))
16
17
  .replace(exports.TEMPLATE_PLACEHOLDER, () => `<div id="app">${page.template}</div>`)
17
18
  .replace(exports.LANG_ATTRIBUTE_PATTERN, () => `<html lang="${toAttributeValue(page.lang)}"`);
18
19
  exports.fillHtmlTemplate = fillHtmlTemplate;
@@ -2,8 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderHtmlPage = void 0;
4
4
  const renderHtmlPage = async (renderer, request, reply) => {
5
- const { html, statusCode } = await renderer.renderApplication(request);
5
+ const { headers = {}, html, statusCode } = await renderer.renderApplication(request);
6
6
  void reply.code(statusCode).header('Content-Type', 'text/html');
7
+ for (const [name, value] of Object.entries(headers)) {
8
+ void reply.header(name, value);
9
+ }
7
10
  return html;
8
11
  };
9
12
  exports.renderHtmlPage = renderHtmlPage;
@@ -3,10 +3,12 @@ import type { HtmlLoaderInterface } from '../html-loader/html-loader.interface.j
3
3
  import type { SsrEntryInterface } from '../ssr-entry/ssr-entry.interface.js';
4
4
  import type { ApplicationInterface, ViteApplicationRendererInterface } from '../vite-application-renderer.interface.js';
5
5
  import type { ViteSsrOptionsInterface } from '../vite-ssr-options.interface.js';
6
+ import { ContentSecurityPolicyConfig } from '../content-security-policy/content-security-policy.config.js';
6
7
  export declare class ViteApplicationRenderer implements ViteApplicationRendererInterface {
7
8
  private readonly ssrEntry;
8
9
  private readonly htmlLoader;
9
10
  private readonly options;
10
- constructor(ssrEntry: SsrEntryInterface, htmlLoader: HtmlLoaderInterface, options: Pick<ViteSsrOptionsInterface, 'buildInitialState' | 'htmlPath'>);
11
+ private readonly contentSecurityPolicy;
12
+ constructor(ssrEntry: SsrEntryInterface, htmlLoader: HtmlLoaderInterface, options: Pick<ViteSsrOptionsInterface, 'buildInitialState' | 'htmlPath'>, contentSecurityPolicy: ContentSecurityPolicyConfig);
11
13
  renderApplication(request: FastifyRequest): Promise<ApplicationInterface>;
12
14
  }
@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.ViteApplicationRenderer = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
+ const content_security_policy_config_js_1 = require("../content-security-policy/content-security-policy.config.js");
17
18
  const html_loader_interface_js_1 = require("../html-loader/html-loader.interface.js");
18
19
  const ssr_entry_interface_js_1 = require("../ssr-entry/ssr-entry.interface.js");
19
20
  const vite_constants_js_1 = require("../vite.constants.js");
@@ -22,16 +23,19 @@ let ViteApplicationRenderer = class ViteApplicationRenderer {
22
23
  ssrEntry;
23
24
  htmlLoader;
24
25
  options;
25
- constructor(ssrEntry, htmlLoader, options) {
26
+ contentSecurityPolicy;
27
+ constructor(ssrEntry, htmlLoader, options, contentSecurityPolicy) {
26
28
  this.ssrEntry = ssrEntry;
27
29
  this.htmlLoader = htmlLoader;
28
30
  this.options = options;
31
+ this.contentSecurityPolicy = contentSecurityPolicy;
29
32
  }
30
33
  async renderApplication(request) {
31
34
  const { url } = request;
32
35
  const html = await this.htmlLoader.loadHtml(this.options.htmlPath, url);
33
36
  const page = await this.ssrEntry.render(url, this.options.buildInitialState(request));
34
- return { html: (0, html_template_js_1.fillHtmlTemplate)(html, page), statusCode: page.statusCode };
37
+ const { headers, nonce } = this.contentSecurityPolicy.issue();
38
+ return { headers, html: (0, html_template_js_1.fillHtmlTemplate)(html, page, nonce), statusCode: page.statusCode };
35
39
  }
36
40
  };
37
41
  exports.ViteApplicationRenderer = ViteApplicationRenderer;
@@ -40,5 +44,6 @@ exports.ViteApplicationRenderer = ViteApplicationRenderer = __decorate([
40
44
  __param(0, (0, common_1.Inject)(ssr_entry_interface_js_1.SSR_ENTRY_INTERFACE)),
41
45
  __param(1, (0, common_1.Inject)(html_loader_interface_js_1.HTML_LOADER_INTERFACE)),
42
46
  __param(2, (0, common_1.Inject)(vite_constants_js_1.VITE_SSR_OPTIONS)),
43
- __metadata("design:paramtypes", [Object, Object, Object])
47
+ __param(3, (0, common_1.Inject)(content_security_policy_config_js_1.ContentSecurityPolicyConfig)),
48
+ __metadata("design:paramtypes", [Object, Object, Object, content_security_policy_config_js_1.ContentSecurityPolicyConfig])
44
49
  ], ViteApplicationRenderer);
@@ -1,6 +1,7 @@
1
1
  import type { HttpStatus } from '@nestjs/common';
2
2
  import type { FastifyRequest } from 'fastify';
3
3
  interface AppInterface {
4
+ headers?: Readonly<Record<string, string>>;
4
5
  html: string;
5
6
  statusCode: HttpStatus;
6
7
  }
@@ -0,0 +1,16 @@
1
+ import type { BuildOptions, PluginOption } from 'vite';
2
+ export interface ApplicationViteConfigOptionsInterface {
3
+ appPath: string;
4
+ buildPlugins?: BuildPluginsType;
5
+ clientHtml: string;
6
+ environment: ApplicationViteEnvironmentInterface;
7
+ plugins: PluginOption[];
8
+ root: string;
9
+ }
10
+ export interface ApplicationViteEnvironmentInterface {
11
+ NODE_ENV?: string;
12
+ PORT?: string;
13
+ VITE_SERVER_HMR_CLIENT_PORT?: string;
14
+ VITE_SERVER_HMR_PORT?: string;
15
+ }
16
+ export type BuildPluginsType = NonNullable<NonNullable<BuildOptions['rolldownOptions']>['plugins']>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import type { UserConfig } from 'vite';
2
+ import type { ApplicationViteConfigOptionsInterface } from './application-vite-config-options.interface.js';
3
+ export declare const defineApplicationViteConfig: (options: ApplicationViteConfigOptionsInterface) => UserConfig;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.defineApplicationViteConfig = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_process_1 = __importDefault(require("node:process"));
9
+ const vite_1 = require("vite");
10
+ const vite_plugin_node_js_1 = require("./vite-plugin-node.js");
11
+ const CLIENT_ENTRY = 'assets/main';
12
+ const DEV_SERVER_PORT = 3000;
13
+ const HMR_PORT = 3001;
14
+ const SOURCEMAP_WARNING = 'Sourcemap for';
15
+ const VENDOR_GROUP = /\/node_modules\/(?<group>[\w\-@]*)/u;
16
+ const vendorGroupName = (moduleId) => {
17
+ const match = VENDOR_GROUP.exec(moduleId);
18
+ return match?.groups ? match.groups.group : 'unknown';
19
+ };
20
+ const outputOf = (isServerBuild) => ({
21
+ assetFileNames: 'assets/[hash][extname]',
22
+ chunkFileNames: isServerBuild ? '[hash].mjs' : 'assets/[hash].js',
23
+ codeSplitting: {
24
+ groups: [
25
+ { name: 'layout', test: /layouts/u },
26
+ { maxSize: 200_000, minSize: 100_000, name: vendorGroupName, priority: 10, test: /node_modules/u },
27
+ ],
28
+ },
29
+ comments: false,
30
+ dir: './dist',
31
+ entryFileNames: (chunk) => (chunk.name === CLIENT_ENTRY ? 'assets/[hash].js' : '[name].mjs'),
32
+ format: 'esm',
33
+ hashCharacters: 'base36',
34
+ minifyInternalExports: false,
35
+ });
36
+ const buildOf = (options, isServerBuild) => ({
37
+ chunkSizeWarningLimit: 400,
38
+ emptyOutDir: false,
39
+ manifest: true,
40
+ minify: false,
41
+ rolldownOptions: {
42
+ input: { [CLIENT_ENTRY]: options.clientHtml },
43
+ output: outputOf(isServerBuild),
44
+ plugins: options.buildPlugins ?? [],
45
+ },
46
+ sourcemap: false,
47
+ });
48
+ const portOf = (value, fallback) => Number(value) || fallback;
49
+ const serverOf = (environment) => ({
50
+ allowedHosts: true,
51
+ hmr: {
52
+ clientPort: portOf(environment.VITE_SERVER_HMR_CLIENT_PORT, HMR_PORT),
53
+ port: portOf(environment.VITE_SERVER_HMR_PORT, HMR_PORT),
54
+ },
55
+ host: '0.0.0.0',
56
+ middlewareMode: false,
57
+ port: portOf(environment.PORT, DEV_SERVER_PORT),
58
+ preTransformRequests: environment.NODE_ENV !== 'test',
59
+ strictPort: true,
60
+ watch: {
61
+ ignored: ['**/*.gql', 'reports/**/*'],
62
+ interval: 400,
63
+ usePolling: true,
64
+ },
65
+ });
66
+ const createApplicationLogger = () => {
67
+ const logger = (0, vite_1.createLogger)();
68
+ const warnOnce = logger.warnOnce.bind(logger);
69
+ logger.warnOnce = (message, options) => {
70
+ if (!message.includes(SOURCEMAP_WARNING)) {
71
+ warnOnce(message, options);
72
+ }
73
+ };
74
+ return logger;
75
+ };
76
+ const defineApplicationViteConfig = (options) => {
77
+ const isServerBuild = node_process_1.default.argv.includes('--ssr');
78
+ return {
79
+ appType: 'custom',
80
+ build: buildOf(options, isServerBuild),
81
+ customLogger: createApplicationLogger(),
82
+ optimizeDeps: { include: ['pino'], noDiscovery: true },
83
+ plugins: [...(0, vite_plugin_node_js_1.vitePluginNode)({ appPath: options.appPath }), ...options.plugins],
84
+ resolve: { alias: { '@': node_path_1.default.resolve(options.root, 'src') } },
85
+ root: node_path_1.default.resolve(options.root),
86
+ server: serverOf(options.environment),
87
+ };
88
+ };
89
+ exports.defineApplicationViteConfig = defineApplicationViteConfig;
@@ -1,4 +1,6 @@
1
+ export * from './application-vite-config-options.interface.js';
1
2
  export * from './create-middleware.js';
3
+ export * from './define-application-vite-config.js';
2
4
  export * from './nest-handler.js';
3
5
  export * from './plugin-config.interface.js';
4
6
  export * from './vite-plugin-node.js';
@@ -14,7 +14,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./application-vite-config-options.interface.js"), exports);
17
18
  __exportStar(require("./create-middleware.js"), exports);
19
+ __exportStar(require("./define-application-vite-config.js"), exports);
18
20
  __exportStar(require("./nest-handler.js"), exports);
19
21
  __exportStar(require("./plugin-config.interface.js"), exports);
20
22
  __exportStar(require("./vite-plugin-node.js"), exports);
@@ -7,8 +7,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ViteSsrModule = void 0;
10
+ const nestjs_config_module_1 = require("@libs-for-dev/nestjs-config-module");
10
11
  const common_1 = require("@nestjs/common");
11
12
  const core_1 = require("@nestjs/core");
13
+ const content_security_policy_config_js_1 = require("./content-security-policy/content-security-policy.config.js");
14
+ const content_security_policy_environment_js_1 = require("./content-security-policy/content-security-policy.environment.js");
12
15
  const not_found_filter_js_1 = require("./filters/not-found.filter.js");
13
16
  const html_loader_with_vite_js_1 = require("./html-loader/html-loader-with-vite.js");
14
17
  const html_loader_interface_js_1 = require("./html-loader/html-loader.interface.js");
@@ -37,7 +40,10 @@ let ViteSsrModule = class ViteSsrModule {
37
40
  return {
38
41
  exports: [vite_application_renderer_interface_js_1.VITE_APPLICATION_RENDERER],
39
42
  global: true,
40
- imports: isProduction ? [] : [vite_module_js_1.ViteModule],
43
+ imports: [
44
+ nestjs_config_module_1.ConfigsModule.forFeature([content_security_policy_config_js_1.ContentSecurityPolicyConfig], [content_security_policy_environment_js_1.ContentSecurityPolicyEnvironment]),
45
+ ...isProduction ? [] : [vite_module_js_1.ViteModule],
46
+ ],
41
47
  module: this,
42
48
  providers: [
43
49
  { provide: vite_constants_js_1.VITE_SSR_OPTIONS, useValue: options },
@@ -0,0 +1,7 @@
1
+ import type { ContentSecurityPolicyInterface } from './content-security-policy.interface.js';
2
+ import { ContentSecurityPolicyEnvironment } from './content-security-policy.environment.js';
3
+ export declare class ContentSecurityPolicyConfig {
4
+ private readonly environment;
5
+ constructor(environment: ContentSecurityPolicyEnvironment);
6
+ issue(): ContentSecurityPolicyInterface;
7
+ }
@@ -0,0 +1,41 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable } from '@nestjs/common';
14
+ import { randomBytes } from 'node:crypto';
15
+ import { ContentSecurityPolicyEnvironment } from './content-security-policy.environment.js';
16
+ const NONCE_BYTES = 16;
17
+ const HEADER_BY_MODE = {
18
+ enforce: 'Content-Security-Policy',
19
+ 'report-only': 'Content-Security-Policy-Report-Only',
20
+ };
21
+ const buildPolicy = (nonce) => `script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'`;
22
+ let ContentSecurityPolicyConfig = class ContentSecurityPolicyConfig {
23
+ environment;
24
+ constructor(environment) {
25
+ this.environment = environment;
26
+ }
27
+ issue() {
28
+ const mode = this.environment.CONTENT_SECURITY_POLICY_MODE;
29
+ if (mode === 'off') {
30
+ return { headers: {}, nonce: undefined };
31
+ }
32
+ const nonce = randomBytes(NONCE_BYTES).toBase64();
33
+ return { headers: { [HEADER_BY_MODE[mode]]: buildPolicy(nonce) }, nonce };
34
+ }
35
+ };
36
+ ContentSecurityPolicyConfig = __decorate([
37
+ Injectable(),
38
+ __param(0, Inject(ContentSecurityPolicyEnvironment)),
39
+ __metadata("design:paramtypes", [ContentSecurityPolicyEnvironment])
40
+ ], ContentSecurityPolicyConfig);
41
+ export { ContentSecurityPolicyConfig };
@@ -0,0 +1,5 @@
1
+ export declare const CONTENT_SECURITY_POLICY_MODES: readonly ["enforce", "off", "report-only"];
2
+ export type ContentSecurityPolicyMode = (typeof CONTENT_SECURITY_POLICY_MODES)[number];
3
+ export declare class ContentSecurityPolicyEnvironment {
4
+ CONTENT_SECURITY_POLICY_MODE: ContentSecurityPolicyMode;
5
+ }
@@ -0,0 +1,20 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Expose } from 'class-transformer';
11
+ import { IsIn } from 'class-validator';
12
+ export const CONTENT_SECURITY_POLICY_MODES = ['enforce', 'off', 'report-only'];
13
+ export class ContentSecurityPolicyEnvironment {
14
+ CONTENT_SECURITY_POLICY_MODE = 'enforce';
15
+ }
16
+ __decorate([
17
+ Expose(),
18
+ IsIn(CONTENT_SECURITY_POLICY_MODES),
19
+ __metadata("design:type", String)
20
+ ], ContentSecurityPolicyEnvironment.prototype, "CONTENT_SECURITY_POLICY_MODE", void 0);
@@ -0,0 +1,4 @@
1
+ export interface ContentSecurityPolicyInterface {
2
+ readonly headers: Readonly<Record<string, string>>;
3
+ readonly nonce: string | undefined;
4
+ }
@@ -0,0 +1,3 @@
1
+ export * from './content-security-policy.config.js';
2
+ export * from './content-security-policy.environment.js';
3
+ export * from './content-security-policy.interface.js';
@@ -0,0 +1,3 @@
1
+ export * from './content-security-policy.config.js';
2
+ export * from './content-security-policy.environment.js';
3
+ export * from './content-security-policy.interface.js';
@@ -1,3 +1,4 @@
1
+ export * from './content-security-policy/index.js';
1
2
  export * from './filters/index.js';
2
3
  export * from './html-loader/index.js';
3
4
  export * from './renderer/index.js';
package/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './content-security-policy/index.js';
1
2
  export * from './filters/index.js';
2
3
  export * from './html-loader/index.js';
3
4
  export * from './renderer/index.js';
@@ -2,4 +2,4 @@ import type { SsrPageInterface } from '../ssr-entry/ssr-entry.interface.js';
2
2
  export declare const STATE_PLACEHOLDER = "<script id=\"state\"></script>";
3
3
  export declare const TEMPLATE_PLACEHOLDER = "<div id=\"app\"></div>";
4
4
  export declare const LANG_ATTRIBUTE_PATTERN: RegExp;
5
- export declare const fillHtmlTemplate: (html: string, page: SsrPageInterface) => string;
5
+ export declare const fillHtmlTemplate: (html: string, page: SsrPageInterface, nonce?: string) => string;
@@ -3,12 +3,13 @@ export const TEMPLATE_PLACEHOLDER = '<div id="app"></div>';
3
3
  export const LANG_ATTRIBUTE_PATTERN = /<html\s+lang="[^"]*"/u;
4
4
  const toScriptLiteral = (value) => JSON.stringify(value).replaceAll('<', String.raw `\u003C`);
5
5
  const toAttributeValue = (value) => value.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
6
- const toStateScript = (state) => {
6
+ const toStateScript = (state, nonce) => {
7
7
  const assignments = Object.entries(state)
8
8
  .map(([key, value]) => `window[${toScriptLiteral(key)}] = ${toScriptLiteral(value)};`);
9
- return `<script>${assignments.join('')}</script>`;
9
+ const nonceAttribute = nonce === undefined ? '' : ` nonce="${toAttributeValue(nonce)}"`;
10
+ return `<script id="state"${nonceAttribute}>${assignments.join('')}</script>`;
10
11
  };
11
- export const fillHtmlTemplate = (html, page) => html
12
- .replace(STATE_PLACEHOLDER, () => toStateScript(page.state))
12
+ export const fillHtmlTemplate = (html, page, nonce) => html
13
+ .replace(STATE_PLACEHOLDER, () => toStateScript(page.state, nonce))
13
14
  .replace(TEMPLATE_PLACEHOLDER, () => `<div id="app">${page.template}</div>`)
14
15
  .replace(LANG_ATTRIBUTE_PATTERN, () => `<html lang="${toAttributeValue(page.lang)}"`);
@@ -1,5 +1,8 @@
1
1
  export const renderHtmlPage = async (renderer, request, reply) => {
2
- const { html, statusCode } = await renderer.renderApplication(request);
2
+ const { headers = {}, html, statusCode } = await renderer.renderApplication(request);
3
3
  void reply.code(statusCode).header('Content-Type', 'text/html');
4
+ for (const [name, value] of Object.entries(headers)) {
5
+ void reply.header(name, value);
6
+ }
4
7
  return html;
5
8
  };
@@ -3,10 +3,12 @@ import type { HtmlLoaderInterface } from '../html-loader/html-loader.interface.j
3
3
  import type { SsrEntryInterface } from '../ssr-entry/ssr-entry.interface.js';
4
4
  import type { ApplicationInterface, ViteApplicationRendererInterface } from '../vite-application-renderer.interface.js';
5
5
  import type { ViteSsrOptionsInterface } from '../vite-ssr-options.interface.js';
6
+ import { ContentSecurityPolicyConfig } from '../content-security-policy/content-security-policy.config.js';
6
7
  export declare class ViteApplicationRenderer implements ViteApplicationRendererInterface {
7
8
  private readonly ssrEntry;
8
9
  private readonly htmlLoader;
9
10
  private readonly options;
10
- constructor(ssrEntry: SsrEntryInterface, htmlLoader: HtmlLoaderInterface, options: Pick<ViteSsrOptionsInterface, 'buildInitialState' | 'htmlPath'>);
11
+ private readonly contentSecurityPolicy;
12
+ constructor(ssrEntry: SsrEntryInterface, htmlLoader: HtmlLoaderInterface, options: Pick<ViteSsrOptionsInterface, 'buildInitialState' | 'htmlPath'>, contentSecurityPolicy: ContentSecurityPolicyConfig);
11
13
  renderApplication(request: FastifyRequest): Promise<ApplicationInterface>;
12
14
  }
@@ -11,6 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
13
  import { Inject, Injectable } from '@nestjs/common';
14
+ import { ContentSecurityPolicyConfig } from '../content-security-policy/content-security-policy.config.js';
14
15
  import { HTML_LOADER_INTERFACE } from '../html-loader/html-loader.interface.js';
15
16
  import { SSR_ENTRY_INTERFACE } from '../ssr-entry/ssr-entry.interface.js';
16
17
  import { VITE_SSR_OPTIONS } from '../vite.constants.js';
@@ -19,16 +20,19 @@ let ViteApplicationRenderer = class ViteApplicationRenderer {
19
20
  ssrEntry;
20
21
  htmlLoader;
21
22
  options;
22
- constructor(ssrEntry, htmlLoader, options) {
23
+ contentSecurityPolicy;
24
+ constructor(ssrEntry, htmlLoader, options, contentSecurityPolicy) {
23
25
  this.ssrEntry = ssrEntry;
24
26
  this.htmlLoader = htmlLoader;
25
27
  this.options = options;
28
+ this.contentSecurityPolicy = contentSecurityPolicy;
26
29
  }
27
30
  async renderApplication(request) {
28
31
  const { url } = request;
29
32
  const html = await this.htmlLoader.loadHtml(this.options.htmlPath, url);
30
33
  const page = await this.ssrEntry.render(url, this.options.buildInitialState(request));
31
- return { html: fillHtmlTemplate(html, page), statusCode: page.statusCode };
34
+ const { headers, nonce } = this.contentSecurityPolicy.issue();
35
+ return { headers, html: fillHtmlTemplate(html, page, nonce), statusCode: page.statusCode };
32
36
  }
33
37
  };
34
38
  ViteApplicationRenderer = __decorate([
@@ -36,6 +40,7 @@ ViteApplicationRenderer = __decorate([
36
40
  __param(0, Inject(SSR_ENTRY_INTERFACE)),
37
41
  __param(1, Inject(HTML_LOADER_INTERFACE)),
38
42
  __param(2, Inject(VITE_SSR_OPTIONS)),
39
- __metadata("design:paramtypes", [Object, Object, Object])
43
+ __param(3, Inject(ContentSecurityPolicyConfig)),
44
+ __metadata("design:paramtypes", [Object, Object, Object, ContentSecurityPolicyConfig])
40
45
  ], ViteApplicationRenderer);
41
46
  export { ViteApplicationRenderer };
@@ -1,6 +1,7 @@
1
1
  import type { HttpStatus } from '@nestjs/common';
2
2
  import type { FastifyRequest } from 'fastify';
3
3
  interface AppInterface {
4
+ headers?: Readonly<Record<string, string>>;
4
5
  html: string;
5
6
  statusCode: HttpStatus;
6
7
  }
@@ -0,0 +1,16 @@
1
+ import type { BuildOptions, PluginOption } from 'vite';
2
+ export interface ApplicationViteConfigOptionsInterface {
3
+ appPath: string;
4
+ buildPlugins?: BuildPluginsType;
5
+ clientHtml: string;
6
+ environment: ApplicationViteEnvironmentInterface;
7
+ plugins: PluginOption[];
8
+ root: string;
9
+ }
10
+ export interface ApplicationViteEnvironmentInterface {
11
+ NODE_ENV?: string;
12
+ PORT?: string;
13
+ VITE_SERVER_HMR_CLIENT_PORT?: string;
14
+ VITE_SERVER_HMR_PORT?: string;
15
+ }
16
+ export type BuildPluginsType = NonNullable<NonNullable<BuildOptions['rolldownOptions']>['plugins']>;
@@ -0,0 +1,3 @@
1
+ import type { UserConfig } from 'vite';
2
+ import type { ApplicationViteConfigOptionsInterface } from './application-vite-config-options.interface.js';
3
+ export declare const defineApplicationViteConfig: (options: ApplicationViteConfigOptionsInterface) => UserConfig;
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { createLogger } from 'vite';
4
+ import { vitePluginNode } from './vite-plugin-node.js';
5
+ const CLIENT_ENTRY = 'assets/main';
6
+ const DEV_SERVER_PORT = 3000;
7
+ const HMR_PORT = 3001;
8
+ const SOURCEMAP_WARNING = 'Sourcemap for';
9
+ const VENDOR_GROUP = /\/node_modules\/(?<group>[\w\-@]*)/u;
10
+ const vendorGroupName = (moduleId) => {
11
+ const match = VENDOR_GROUP.exec(moduleId);
12
+ return match?.groups ? match.groups.group : 'unknown';
13
+ };
14
+ const outputOf = (isServerBuild) => ({
15
+ assetFileNames: 'assets/[hash][extname]',
16
+ chunkFileNames: isServerBuild ? '[hash].mjs' : 'assets/[hash].js',
17
+ codeSplitting: {
18
+ groups: [
19
+ { name: 'layout', test: /layouts/u },
20
+ { maxSize: 200_000, minSize: 100_000, name: vendorGroupName, priority: 10, test: /node_modules/u },
21
+ ],
22
+ },
23
+ comments: false,
24
+ dir: './dist',
25
+ entryFileNames: (chunk) => (chunk.name === CLIENT_ENTRY ? 'assets/[hash].js' : '[name].mjs'),
26
+ format: 'esm',
27
+ hashCharacters: 'base36',
28
+ minifyInternalExports: false,
29
+ });
30
+ const buildOf = (options, isServerBuild) => ({
31
+ chunkSizeWarningLimit: 400,
32
+ emptyOutDir: false,
33
+ manifest: true,
34
+ minify: false,
35
+ rolldownOptions: {
36
+ input: { [CLIENT_ENTRY]: options.clientHtml },
37
+ output: outputOf(isServerBuild),
38
+ plugins: options.buildPlugins ?? [],
39
+ },
40
+ sourcemap: false,
41
+ });
42
+ const portOf = (value, fallback) => Number(value) || fallback;
43
+ const serverOf = (environment) => ({
44
+ allowedHosts: true,
45
+ hmr: {
46
+ clientPort: portOf(environment.VITE_SERVER_HMR_CLIENT_PORT, HMR_PORT),
47
+ port: portOf(environment.VITE_SERVER_HMR_PORT, HMR_PORT),
48
+ },
49
+ host: '0.0.0.0',
50
+ middlewareMode: false,
51
+ port: portOf(environment.PORT, DEV_SERVER_PORT),
52
+ preTransformRequests: environment.NODE_ENV !== 'test',
53
+ strictPort: true,
54
+ watch: {
55
+ ignored: ['**/*.gql', 'reports/**/*'],
56
+ interval: 400,
57
+ usePolling: true,
58
+ },
59
+ });
60
+ const createApplicationLogger = () => {
61
+ const logger = createLogger();
62
+ const warnOnce = logger.warnOnce.bind(logger);
63
+ logger.warnOnce = (message, options) => {
64
+ if (!message.includes(SOURCEMAP_WARNING)) {
65
+ warnOnce(message, options);
66
+ }
67
+ };
68
+ return logger;
69
+ };
70
+ export const defineApplicationViteConfig = (options) => {
71
+ const isServerBuild = process.argv.includes('--ssr');
72
+ return {
73
+ appType: 'custom',
74
+ build: buildOf(options, isServerBuild),
75
+ customLogger: createApplicationLogger(),
76
+ optimizeDeps: { include: ['pino'], noDiscovery: true },
77
+ plugins: [...vitePluginNode({ appPath: options.appPath }), ...options.plugins],
78
+ resolve: { alias: { '@': path.resolve(options.root, 'src') } },
79
+ root: path.resolve(options.root),
80
+ server: serverOf(options.environment),
81
+ };
82
+ };
@@ -1,4 +1,6 @@
1
+ export * from './application-vite-config-options.interface.js';
1
2
  export * from './create-middleware.js';
3
+ export * from './define-application-vite-config.js';
2
4
  export * from './nest-handler.js';
3
5
  export * from './plugin-config.interface.js';
4
6
  export * from './vite-plugin-node.js';
@@ -1,4 +1,6 @@
1
+ export * from './application-vite-config-options.interface.js';
1
2
  export * from './create-middleware.js';
3
+ export * from './define-application-vite-config.js';
2
4
  export * from './nest-handler.js';
3
5
  export * from './plugin-config.interface.js';
4
6
  export * from './vite-plugin-node.js';
@@ -4,8 +4,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ import { ConfigsModule } from '@libs-for-dev/nestjs-config-module';
7
8
  import { Module } from '@nestjs/common';
8
9
  import { APP_FILTER } from '@nestjs/core';
10
+ import { ContentSecurityPolicyConfig } from './content-security-policy/content-security-policy.config.js';
11
+ import { ContentSecurityPolicyEnvironment } from './content-security-policy/content-security-policy.environment.js';
9
12
  import { NotFoundFilter } from './filters/not-found.filter.js';
10
13
  import { HtmlLoaderWithVite } from './html-loader/html-loader-with-vite.js';
11
14
  import { HTML_LOADER_INTERFACE } from './html-loader/html-loader.interface.js';
@@ -34,7 +37,10 @@ let ViteSsrModule = class ViteSsrModule {
34
37
  return {
35
38
  exports: [VITE_APPLICATION_RENDERER],
36
39
  global: true,
37
- imports: isProduction ? [] : [ViteModule],
40
+ imports: [
41
+ ConfigsModule.forFeature([ContentSecurityPolicyConfig], [ContentSecurityPolicyEnvironment]),
42
+ ...isProduction ? [] : [ViteModule],
43
+ ],
38
44
  module: this,
39
45
  providers: [
40
46
  { provide: VITE_SSR_OPTIONS, useValue: options },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@application-kit/nestjs-vite",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Мост Vite ↔ NestJS для SSR: dev-обвязка, загрузка HTML и ssr-entry",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -66,6 +66,7 @@
66
66
  "test:unit": "vitest run"
67
67
  },
68
68
  "devDependencies": {
69
+ "@libs-for-dev/nestjs-config-module": "3.0.0",
69
70
  "@nestjs/common": "^11.2.1",
70
71
  "@nestjs/core": "^11.2.1",
71
72
  "@nestjs/platform-fastify": "^11.2.1",
@@ -73,6 +74,8 @@
73
74
  "@stryker-mutator/core": "^10.0.0",
74
75
  "@stryker-mutator/vitest-runner": "^10.0.0",
75
76
  "@types/node": "^26.2.0",
77
+ "class-transformer": "^0.5.1",
78
+ "class-validator": "^0.15.1",
76
79
  "fastify": "^5.12.1",
77
80
  "reflect-metadata": "^0.2.2",
78
81
  "rxjs": "^7.8.2",
@@ -81,9 +84,12 @@
81
84
  "vitest": "^4.1.11"
82
85
  },
83
86
  "peerDependencies": {
87
+ "@libs-for-dev/nestjs-config-module": "^3.0.0",
84
88
  "@nestjs/common": "^11.0.0",
85
89
  "@nestjs/core": "^11.0.0",
86
90
  "@nestjs/platform-fastify": "^11.0.0",
91
+ "class-transformer": "^0.5.1",
92
+ "class-validator": "^0.15.1",
87
93
  "fastify": "^5.0.0",
88
94
  "vite": "^8.0.0"
89
95
  },