@application-kit/nestjs-vite 0.5.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 +8 -0
- package/README.md +4 -0
- 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/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/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.5.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -148,6 +148,10 @@ export class ClientModule {}
|
|
|
148
148
|
`VITE_APPLICATION_RENDERER` для спек, предмет которых не SSR (контроллеры зависят от рендерера, а настоящий поднимает
|
|
149
149
|
Vite). Фейк отвечает `<div id="app">{url}</div>` со статусом 200; сам он — `fakeApplicationRenderer`. Vite вход не тянет.
|
|
150
150
|
|
|
151
|
+
Вход `@application-kit/nestjs-vite/static-assets-gate` — `assertStaticAssetsLayout`: гейт периметра прод-образа. Читает
|
|
152
|
+
`vite.config` продукта обеими сборками и падает со списком расхождений, если клиентская сборка вышла за подкаталог
|
|
153
|
+
`assets` под хешем, серверные входы переименованы, появились карты исходников или нехешируемая статика в `publicDir`.
|
|
154
|
+
|
|
151
155
|
## Формат
|
|
152
156
|
|
|
153
157
|
ESM и CJS. Это две копии с разными классами и токенами — в одном процессе пакет
|
|
@@ -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);
|
|
@@ -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';
|
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,7 +70,7 @@
|
|
|
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"
|