@application-kit/nestjs-vite 0.4.0 → 0.6.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.
- package/CHANGELOG.md +14 -0
- package/README.md +25 -3
- package/dist/cjs/content-security-policy/content-security-policy.config.d.ts +7 -0
- package/dist/cjs/content-security-policy/content-security-policy.config.js +44 -0
- package/dist/cjs/content-security-policy/content-security-policy.environment.d.ts +5 -0
- package/dist/cjs/content-security-policy/content-security-policy.environment.js +24 -0
- package/dist/cjs/content-security-policy/content-security-policy.interface.d.ts +4 -0
- package/dist/cjs/content-security-policy/content-security-policy.interface.js +2 -0
- package/dist/cjs/content-security-policy/index.d.ts +3 -0
- package/dist/cjs/content-security-policy/index.js +19 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/renderer/html-template.d.ts +1 -1
- package/dist/cjs/renderer/html-template.js +5 -4
- package/dist/cjs/renderer/render-html-page.js +4 -1
- package/dist/cjs/renderer/vite-application-renderer.d.ts +3 -1
- package/dist/cjs/renderer/vite-application-renderer.js +8 -3
- package/dist/cjs/static-assets-gate/assert-static-assets-layout.d.ts +10 -0
- package/dist/cjs/static-assets-gate/assert-static-assets-layout.js +105 -0
- package/dist/cjs/static-assets-gate/index.d.ts +1 -0
- package/dist/cjs/static-assets-gate/index.js +17 -0
- package/dist/cjs/vite-application-renderer.interface.d.ts +1 -0
- package/dist/cjs/vite-ssr.module.js +7 -1
- package/dist/esm/content-security-policy/content-security-policy.config.d.ts +7 -0
- package/dist/esm/content-security-policy/content-security-policy.config.js +41 -0
- package/dist/esm/content-security-policy/content-security-policy.environment.d.ts +5 -0
- package/dist/esm/content-security-policy/content-security-policy.environment.js +20 -0
- package/dist/esm/content-security-policy/content-security-policy.interface.d.ts +4 -0
- package/dist/esm/content-security-policy/content-security-policy.interface.js +1 -0
- package/dist/esm/content-security-policy/index.d.ts +3 -0
- package/dist/esm/content-security-policy/index.js +3 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/renderer/html-template.d.ts +1 -1
- package/dist/esm/renderer/html-template.js +5 -4
- package/dist/esm/renderer/render-html-page.js +4 -1
- package/dist/esm/renderer/vite-application-renderer.d.ts +3 -1
- package/dist/esm/renderer/vite-application-renderer.js +8 -3
- package/dist/esm/static-assets-gate/assert-static-assets-layout.d.ts +10 -0
- package/dist/esm/static-assets-gate/assert-static-assets-layout.js +98 -0
- package/dist/esm/static-assets-gate/index.d.ts +1 -0
- package/dist/esm/static-assets-gate/index.js +1 -0
- package/dist/esm/vite-application-renderer.interface.d.ts +1 -0
- package/dist/esm/vite-ssr.module.js +7 -1
- package/package.json +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @application-kit/nestjs-vite
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e9411eb: Спеки-гейты продукта переехали в пакеты: спека теперь — вызов помощника с параметрами продукта, а нарушение приходит ошибкой со списком.
|
|
8
|
+
|
|
9
|
+
`ui/testing`: `assertTokensDeclared`. `kafka/testing` (новый вход): `assertConsumerTopicsConsistent`. `authorization/testing` (новый вход): `assertAbilityRegistryConsistent`. `nestjs-vite/static-assets-gate` (новый вход, отдельно от `/testing`, чтобы спеки с фейком рендерера не грузили `vite`): `assertStaticAssetsLayout`. `lint-config` разрешает импорт нового входа `nestjs-vite/static-assets-gate`.
|
|
10
|
+
|
|
11
|
+
## 0.5.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 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` пишет нарушение в консоль.
|
|
16
|
+
|
|
3
17
|
## 0.4.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -104,17 +104,35 @@ export class ClientModule {}
|
|
|
104
104
|
|
|
105
105
|
| Константа | В шаблоне | Чем заменяется |
|
|
106
106
|
|---|---|---|
|
|
107
|
-
| `STATE_PLACEHOLDER` | `<script id="state"></script>` | `<script>` с присваиваниями `window[ключ] = значение` |
|
|
107
|
+
| `STATE_PLACEHOLDER` | `<script id="state"></script>` | `<script id="state" nonce="…">` с присваиваниями `window[ключ] = значение` |
|
|
108
108
|
| `TEMPLATE_PLACEHOLDER` | `<div id="app"></div>` | тот же `div` с разметкой `template` внутри |
|
|
109
109
|
| `LANG_ATTRIBUTE_PATTERN` | `<html lang="…">` с любым значением | `lang` страницы |
|
|
110
110
|
|
|
111
111
|
Значения состояния уходят JS-литералами строки с экранированным `<`: `</script>` в данных не закроет тег, какой бы
|
|
112
112
|
сериализатор ни выбрало приложение. Клиентский энтри читает их до гидрации.
|
|
113
113
|
|
|
114
|
-
`renderHtmlPage(renderer, request, reply)` — один путь HTML-ответа: ставит статус
|
|
115
|
-
и возвращает HTML. Контроллер с `@Res({ passthrough: true })` возвращает его, фильтр — отправляет. Декоратор
|
|
114
|
+
`renderHtmlPage(renderer, request, reply)` — один путь HTML-ответа: ставит статус рендера, `Content-Type: text/html` и
|
|
115
|
+
заголовки рендера и возвращает HTML. Контроллер с `@Res({ passthrough: true })` возвращает его, фильтр — отправляет. Декоратор
|
|
116
116
|
`@Header('Content-Type')` на маршруте страницы не нужен.
|
|
117
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
|
+
|
|
118
136
|
### Фильтры
|
|
119
137
|
|
|
120
138
|
- `NotFoundFilter` — регистрирует `ViteSsrModule`: `NotFoundException` отвечает отрендеренной страницей
|
|
@@ -130,6 +148,10 @@ export class ClientModule {}
|
|
|
130
148
|
`VITE_APPLICATION_RENDERER` для спек, предмет которых не SSR (контроллеры зависят от рендерера, а настоящий поднимает
|
|
131
149
|
Vite). Фейк отвечает `<div id="app">{url}</div>` со статусом 200; сам он — `fakeApplicationRenderer`. Vite вход не тянет.
|
|
132
150
|
|
|
151
|
+
Вход `@application-kit/nestjs-vite/static-assets-gate` — `assertStaticAssetsLayout`: гейт периметра прод-образа. Читает
|
|
152
|
+
`vite.config` продукта обеими сборками и падает со списком расхождений, если клиентская сборка вышла за подкаталог
|
|
153
|
+
`assets` под хешем, серверные входы переименованы, появились карты исходников или нехешируемая статика в `publicDir`.
|
|
154
|
+
|
|
133
155
|
## Формат
|
|
134
156
|
|
|
135
157
|
ESM и CJS. Это две копии с разными классами и токенами — в одном процессе пакет
|
|
@@ -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,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);
|
package/dist/cjs/index.d.ts
CHANGED
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('&', '&').replaceAll('"', '"');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface StaticAssetsLayoutOptionsInterface {
|
|
2
|
+
readonly clientModuleFile: string;
|
|
3
|
+
readonly iconFile: string;
|
|
4
|
+
readonly indexHtmlFile: string;
|
|
5
|
+
readonly rootDirectory: string;
|
|
6
|
+
readonly serverEntries: readonly string[];
|
|
7
|
+
readonly tsconfigBuildFile: string;
|
|
8
|
+
readonly viteConfigFile: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const assertStaticAssetsLayout: (options: StaticAssetsLayoutOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
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.assertStaticAssetsLayout = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_util_1 = require("node:util");
|
|
10
|
+
const vite_1 = require("vite");
|
|
11
|
+
const ASSETS_DIRECTORY = 'assets';
|
|
12
|
+
const CLIENT_ENTRY = `${ASSETS_DIRECTORY}/main`;
|
|
13
|
+
const CHUNK_NAME = 'chunk';
|
|
14
|
+
const isPresent = async (file) => {
|
|
15
|
+
try {
|
|
16
|
+
await (0, promises_1.access)(file);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const loadBuildConfig = async ({ rootDirectory, viteConfigFile }, isSsrBuild) => {
|
|
24
|
+
const { argv } = process;
|
|
25
|
+
process.argv = isSsrBuild ? [...argv, '--ssr'] : argv;
|
|
26
|
+
try {
|
|
27
|
+
const loaded = await (0, vite_1.loadConfigFromFile)({ command: 'build', isSsrBuild, mode: 'production' }, node_path_1.default.join(rootDirectory, viteConfigFile), rootDirectory, 'silent');
|
|
28
|
+
if (loaded === null) {
|
|
29
|
+
throw new Error(`${viteConfigFile} не найден`);
|
|
30
|
+
}
|
|
31
|
+
return loaded.config;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
process.argv = argv;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const buildOf = (config) => config.build ?? {};
|
|
38
|
+
const outputOf = (config, viteConfigFile) => {
|
|
39
|
+
const output = buildOf(config).rolldownOptions?.output;
|
|
40
|
+
if (output === undefined || Array.isArray(output)) {
|
|
41
|
+
throw new TypeError(`${viteConfigFile}: ожидается один набор output`);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
};
|
|
45
|
+
const fileNameOf = (pattern, name) => (typeof pattern === 'function'
|
|
46
|
+
? pattern({ exports: [], facadeModuleId: null, isDynamicEntry: false, isEntry: true, moduleIds: [], name })
|
|
47
|
+
: pattern);
|
|
48
|
+
const violationIf = (isViolated, message) => (isViolated ? [message] : []);
|
|
49
|
+
const mismatch = (what, actual, expected) => ((0, node_util_1.isDeepStrictEqual)(actual, expected)
|
|
50
|
+
? []
|
|
51
|
+
: [`${what}: ${JSON.stringify(actual)}, ожидается ${JSON.stringify(expected)}`]);
|
|
52
|
+
const clientOutputViolations = (output) => [
|
|
53
|
+
...mismatch('клиентские ассеты', output.assetFileNames, `${ASSETS_DIRECTORY}/[hash][extname]`),
|
|
54
|
+
...mismatch('клиентские чанки', fileNameOf(output.chunkFileNames, CHUNK_NAME), `${ASSETS_DIRECTORY}/[hash].js`),
|
|
55
|
+
...mismatch('клиентский вход', fileNameOf(output.entryFileNames, CLIENT_ENTRY), `${ASSETS_DIRECTORY}/[hash].js`),
|
|
56
|
+
];
|
|
57
|
+
const serverOutputViolations = (output, serverEntries) => [
|
|
58
|
+
...mismatch('серверные чанки', fileNameOf(output.chunkFileNames, CHUNK_NAME), '[hash].mjs'),
|
|
59
|
+
...serverEntries.flatMap((entry) => mismatch(`серверный вход ${entry}`, fileNameOf(output.entryFileNames, entry), '[name].mjs')),
|
|
60
|
+
];
|
|
61
|
+
const listPublicAssets = async (rootDirectory) => {
|
|
62
|
+
try {
|
|
63
|
+
return await (0, promises_1.readdir)(node_path_1.default.join(rootDirectory, 'public', ASSETS_DIRECTORY));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const placementViolations = async ({ iconFile, indexHtmlFile, rootDirectory }, input) => {
|
|
70
|
+
const [html, iconPresent, publicAssets] = await Promise.all([
|
|
71
|
+
(0, promises_1.readFile)(node_path_1.default.join(rootDirectory, indexHtmlFile), 'utf8'),
|
|
72
|
+
isPresent(node_path_1.default.join(rootDirectory, iconFile)),
|
|
73
|
+
listPublicAssets(rootDirectory),
|
|
74
|
+
]);
|
|
75
|
+
const iconHref = `href="./${ASSETS_DIRECTORY}/${node_path_1.default.basename(iconFile)}"`;
|
|
76
|
+
return [
|
|
77
|
+
...mismatch('клиентский вход сборки', input, { [CLIENT_ENTRY]: node_path_1.default.join(rootDirectory, indexHtmlFile) }),
|
|
78
|
+
...violationIf(!html.includes(iconHref), `${indexHtmlFile} не содержит ${iconHref}`),
|
|
79
|
+
...violationIf(!iconPresent, `нет иконки ${iconFile}`),
|
|
80
|
+
...publicAssets.map((file) => `нехешируемая статика под вечным кешем: public/${ASSETS_DIRECTORY}/${file}`),
|
|
81
|
+
];
|
|
82
|
+
};
|
|
83
|
+
const assertStaticAssetsLayout = async (options) => {
|
|
84
|
+
const { clientModuleFile, rootDirectory, serverEntries, tsconfigBuildFile, viteConfigFile } = options;
|
|
85
|
+
const clientConfig = await loadBuildConfig(options, false);
|
|
86
|
+
const serverConfig = await loadBuildConfig(options, true);
|
|
87
|
+
const [clientModule, tsconfig, placement] = await Promise.all([
|
|
88
|
+
(0, promises_1.readFile)(node_path_1.default.join(rootDirectory, clientModuleFile), 'utf8'),
|
|
89
|
+
(0, promises_1.readFile)(node_path_1.default.join(rootDirectory, tsconfigBuildFile), 'utf8'),
|
|
90
|
+
placementViolations(options, buildOf(clientConfig).rolldownOptions?.input),
|
|
91
|
+
]);
|
|
92
|
+
const violations = [
|
|
93
|
+
...violationIf(clientModule.includes('useStaticAssets'), `${clientModuleFile} раздаёт статику приложением`),
|
|
94
|
+
...clientOutputViolations(outputOf(clientConfig, viteConfigFile)),
|
|
95
|
+
...serverOutputViolations(outputOf(serverConfig, viteConfigFile), serverEntries),
|
|
96
|
+
...placement,
|
|
97
|
+
...mismatch('карта клиентской сборки', buildOf(clientConfig).sourcemap, false),
|
|
98
|
+
...mismatch('карта серверной сборки', buildOf(serverConfig).sourcemap, false),
|
|
99
|
+
...violationIf(!tsconfig.includes('"sourceMap": false'), `${tsconfigBuildFile}: нужен "sourceMap": false`),
|
|
100
|
+
];
|
|
101
|
+
if (violations.length > 0) {
|
|
102
|
+
throw new Error(`Гейт статики прод-сборки:\n${violations.join('\n')}`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
exports.assertStaticAssetsLayout = assertStaticAssetsLayout;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-static-assets-layout.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
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("./assert-static-assets-layout.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:
|
|
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 @@
|
|
|
1
|
+
export {};
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -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('&', '&').replaceAll('"', '"');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
__param(3, Inject(ContentSecurityPolicyConfig)),
|
|
44
|
+
__metadata("design:paramtypes", [Object, Object, Object, ContentSecurityPolicyConfig])
|
|
40
45
|
], ViteApplicationRenderer);
|
|
41
46
|
export { ViteApplicationRenderer };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface StaticAssetsLayoutOptionsInterface {
|
|
2
|
+
readonly clientModuleFile: string;
|
|
3
|
+
readonly iconFile: string;
|
|
4
|
+
readonly indexHtmlFile: string;
|
|
5
|
+
readonly rootDirectory: string;
|
|
6
|
+
readonly serverEntries: readonly string[];
|
|
7
|
+
readonly tsconfigBuildFile: string;
|
|
8
|
+
readonly viteConfigFile: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const assertStaticAssetsLayout: (options: StaticAssetsLayoutOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { access, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { loadConfigFromFile } from 'vite';
|
|
5
|
+
const ASSETS_DIRECTORY = 'assets';
|
|
6
|
+
const CLIENT_ENTRY = `${ASSETS_DIRECTORY}/main`;
|
|
7
|
+
const CHUNK_NAME = 'chunk';
|
|
8
|
+
const isPresent = async (file) => {
|
|
9
|
+
try {
|
|
10
|
+
await access(file);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const loadBuildConfig = async ({ rootDirectory, viteConfigFile }, isSsrBuild) => {
|
|
18
|
+
const { argv } = process;
|
|
19
|
+
process.argv = isSsrBuild ? [...argv, '--ssr'] : argv;
|
|
20
|
+
try {
|
|
21
|
+
const loaded = await loadConfigFromFile({ command: 'build', isSsrBuild, mode: 'production' }, path.join(rootDirectory, viteConfigFile), rootDirectory, 'silent');
|
|
22
|
+
if (loaded === null) {
|
|
23
|
+
throw new Error(`${viteConfigFile} не найден`);
|
|
24
|
+
}
|
|
25
|
+
return loaded.config;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
process.argv = argv;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const buildOf = (config) => config.build ?? {};
|
|
32
|
+
const outputOf = (config, viteConfigFile) => {
|
|
33
|
+
const output = buildOf(config).rolldownOptions?.output;
|
|
34
|
+
if (output === undefined || Array.isArray(output)) {
|
|
35
|
+
throw new TypeError(`${viteConfigFile}: ожидается один набор output`);
|
|
36
|
+
}
|
|
37
|
+
return output;
|
|
38
|
+
};
|
|
39
|
+
const fileNameOf = (pattern, name) => (typeof pattern === 'function'
|
|
40
|
+
? pattern({ exports: [], facadeModuleId: null, isDynamicEntry: false, isEntry: true, moduleIds: [], name })
|
|
41
|
+
: pattern);
|
|
42
|
+
const violationIf = (isViolated, message) => (isViolated ? [message] : []);
|
|
43
|
+
const mismatch = (what, actual, expected) => (isDeepStrictEqual(actual, expected)
|
|
44
|
+
? []
|
|
45
|
+
: [`${what}: ${JSON.stringify(actual)}, ожидается ${JSON.stringify(expected)}`]);
|
|
46
|
+
const clientOutputViolations = (output) => [
|
|
47
|
+
...mismatch('клиентские ассеты', output.assetFileNames, `${ASSETS_DIRECTORY}/[hash][extname]`),
|
|
48
|
+
...mismatch('клиентские чанки', fileNameOf(output.chunkFileNames, CHUNK_NAME), `${ASSETS_DIRECTORY}/[hash].js`),
|
|
49
|
+
...mismatch('клиентский вход', fileNameOf(output.entryFileNames, CLIENT_ENTRY), `${ASSETS_DIRECTORY}/[hash].js`),
|
|
50
|
+
];
|
|
51
|
+
const serverOutputViolations = (output, serverEntries) => [
|
|
52
|
+
...mismatch('серверные чанки', fileNameOf(output.chunkFileNames, CHUNK_NAME), '[hash].mjs'),
|
|
53
|
+
...serverEntries.flatMap((entry) => mismatch(`серверный вход ${entry}`, fileNameOf(output.entryFileNames, entry), '[name].mjs')),
|
|
54
|
+
];
|
|
55
|
+
const listPublicAssets = async (rootDirectory) => {
|
|
56
|
+
try {
|
|
57
|
+
return await readdir(path.join(rootDirectory, 'public', ASSETS_DIRECTORY));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const placementViolations = async ({ iconFile, indexHtmlFile, rootDirectory }, input) => {
|
|
64
|
+
const [html, iconPresent, publicAssets] = await Promise.all([
|
|
65
|
+
readFile(path.join(rootDirectory, indexHtmlFile), 'utf8'),
|
|
66
|
+
isPresent(path.join(rootDirectory, iconFile)),
|
|
67
|
+
listPublicAssets(rootDirectory),
|
|
68
|
+
]);
|
|
69
|
+
const iconHref = `href="./${ASSETS_DIRECTORY}/${path.basename(iconFile)}"`;
|
|
70
|
+
return [
|
|
71
|
+
...mismatch('клиентский вход сборки', input, { [CLIENT_ENTRY]: path.join(rootDirectory, indexHtmlFile) }),
|
|
72
|
+
...violationIf(!html.includes(iconHref), `${indexHtmlFile} не содержит ${iconHref}`),
|
|
73
|
+
...violationIf(!iconPresent, `нет иконки ${iconFile}`),
|
|
74
|
+
...publicAssets.map((file) => `нехешируемая статика под вечным кешем: public/${ASSETS_DIRECTORY}/${file}`),
|
|
75
|
+
];
|
|
76
|
+
};
|
|
77
|
+
export const assertStaticAssetsLayout = async (options) => {
|
|
78
|
+
const { clientModuleFile, rootDirectory, serverEntries, tsconfigBuildFile, viteConfigFile } = options;
|
|
79
|
+
const clientConfig = await loadBuildConfig(options, false);
|
|
80
|
+
const serverConfig = await loadBuildConfig(options, true);
|
|
81
|
+
const [clientModule, tsconfig, placement] = await Promise.all([
|
|
82
|
+
readFile(path.join(rootDirectory, clientModuleFile), 'utf8'),
|
|
83
|
+
readFile(path.join(rootDirectory, tsconfigBuildFile), 'utf8'),
|
|
84
|
+
placementViolations(options, buildOf(clientConfig).rolldownOptions?.input),
|
|
85
|
+
]);
|
|
86
|
+
const violations = [
|
|
87
|
+
...violationIf(clientModule.includes('useStaticAssets'), `${clientModuleFile} раздаёт статику приложением`),
|
|
88
|
+
...clientOutputViolations(outputOf(clientConfig, viteConfigFile)),
|
|
89
|
+
...serverOutputViolations(outputOf(serverConfig, viteConfigFile), serverEntries),
|
|
90
|
+
...placement,
|
|
91
|
+
...mismatch('карта клиентской сборки', buildOf(clientConfig).sourcemap, false),
|
|
92
|
+
...mismatch('карта серверной сборки', buildOf(serverConfig).sourcemap, false),
|
|
93
|
+
...violationIf(!tsconfig.includes('"sourceMap": false'), `${tsconfigBuildFile}: нужен "sourceMap": false`),
|
|
94
|
+
];
|
|
95
|
+
if (violations.length > 0) {
|
|
96
|
+
throw new Error(`Гейт статики прод-сборки:\n${violations.join('\n')}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-static-assets-layout.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-static-assets-layout.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:
|
|
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
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Мост Vite ↔ NestJS для SSR: dev-обвязка, загрузка HTML и ssr-entry",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nestjs",
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"./package.json": "./package.json",
|
|
35
|
+
"./static-assets-gate": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/esm/static-assets-gate/index.d.ts",
|
|
38
|
+
"default": "./dist/esm/static-assets-gate/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/cjs/static-assets-gate/index.d.ts",
|
|
42
|
+
"default": "./dist/cjs/static-assets-gate/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
35
45
|
"./testing": {
|
|
36
46
|
"import": {
|
|
37
47
|
"types": "./dist/esm/testing/index.d.ts",
|
|
@@ -60,12 +70,13 @@
|
|
|
60
70
|
"dist"
|
|
61
71
|
],
|
|
62
72
|
"scripts": {
|
|
63
|
-
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=testing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
73
|
+
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=static-assets-gate,testing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
64
74
|
"lint:tsc": "tsc --noEmit --pretty --project .",
|
|
65
75
|
"test:mutation": "stryker run",
|
|
66
76
|
"test:unit": "vitest run"
|
|
67
77
|
},
|
|
68
78
|
"devDependencies": {
|
|
79
|
+
"@libs-for-dev/nestjs-config-module": "3.0.0",
|
|
69
80
|
"@nestjs/common": "^11.2.1",
|
|
70
81
|
"@nestjs/core": "^11.2.1",
|
|
71
82
|
"@nestjs/platform-fastify": "^11.2.1",
|
|
@@ -73,6 +84,8 @@
|
|
|
73
84
|
"@stryker-mutator/core": "^10.0.0",
|
|
74
85
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
75
86
|
"@types/node": "^26.2.0",
|
|
87
|
+
"class-transformer": "^0.5.1",
|
|
88
|
+
"class-validator": "^0.15.1",
|
|
76
89
|
"fastify": "^5.12.1",
|
|
77
90
|
"reflect-metadata": "^0.2.2",
|
|
78
91
|
"rxjs": "^7.8.2",
|
|
@@ -81,9 +94,12 @@
|
|
|
81
94
|
"vitest": "^4.1.11"
|
|
82
95
|
},
|
|
83
96
|
"peerDependencies": {
|
|
97
|
+
"@libs-for-dev/nestjs-config-module": "^3.0.0",
|
|
84
98
|
"@nestjs/common": "^11.0.0",
|
|
85
99
|
"@nestjs/core": "^11.0.0",
|
|
86
100
|
"@nestjs/platform-fastify": "^11.0.0",
|
|
101
|
+
"class-transformer": "^0.5.1",
|
|
102
|
+
"class-validator": "^0.15.1",
|
|
87
103
|
"fastify": "^5.0.0",
|
|
88
104
|
"vite": "^8.0.0"
|
|
89
105
|
},
|