@application-kit/testing 0.1.0 → 0.3.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @application-kit/testing
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2e91985: Вход `/vitest`: фабрики `defineApplicationUnitConfig` и `defineApplicationEndToEndConfig` — `vitest.config` и
8
+ `vitest.config.e2e` приложения поверх экспорта его `vite.config`. Unit — проекты `server` и `client`, ячейка шарда CI по
9
+ `--shard` и `ProjectShardSequencer`, покрытие v8; e2e — окружение `node` и алиас `graphql` на CJS-вход против двух realm
10
+ у mercurius. Оба профиля — кеш `.vitest-module-cache`, `testTimeout` 20 с, inline `@application-kit/graphql`. Спеки,
11
+ исходники клиента, покрытие, `setupFiles` и дополнительные пакеты для inline — параметры. Новый optional peer входа —
12
+ `vite`.
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 48e6aec: Вход `/vitest` — раскладка ячеек CI по проектам vitest: `ProjectShardSequencer` (первая ячейка — проект client целиком, остальные — проект server LPT по размеру спек, чтобы объединение blob-отчётов покрытия двух трансформов оставалось точным), `parseShard` для `--shard=i/n` из argv и `assignShard`; `vitest` — optional peer этого входа.
19
+
3
20
  ## 0.1.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # @application-kit/testing
2
2
 
3
3
  Тестовая инфраструктура NestJS-приложений: стабы шин CQRS, логгер-накопитель, фейковый microservice-транспорт,
4
- сборка web-приложения для e2e и Postgres в testcontainers. Для `devDependencies` приложения, в прод не попадает.
4
+ сборка web-приложения для e2e, Postgres в testcontainers, конфиги vitest приложения и раскладка ячеек CI по проектам
5
+ vitest (вход `/vitest`).
6
+ Для `devDependencies` приложения, в прод не попадает.
5
7
 
6
8
  ## Установка
7
9
 
@@ -11,7 +13,8 @@ yarn add --dev @application-kit/testing
11
13
 
12
14
  Peer-зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`,
13
15
  `@nestjs/microservices`, `@nestjs/platform-fastify`, `@nestjs/testing`, `nestjs-pino`, `oxide.ts`, `rxjs`,
14
- `testcontainers`, `typeorm`. Все обязательные: подпутей и опциональных peers нет.
16
+ `testcontainers`, `typeorm` — обязательные для корневого входа. Вход `/vitest` несёт свои optional peers `vitest` и
17
+ `vite` (`mergeConfig` фабрик конфигов).
15
18
 
16
19
  ## Состав
17
20
 
@@ -32,3 +35,62 @@ const { dataSource } = await testDataSource({
32
35
  namingStrategy: new TypeormPluralNamingStrategy(),
33
36
  })
34
37
  ```
38
+
39
+ ## Вход `/vitest`: конфиги vitest приложения
40
+
41
+ `defineApplicationUnitConfig(options)` и `defineApplicationEndToEndConfig(options)` собирают `vitest.config` и
42
+ `vitest.config.e2e` поверх экспорта `vite.config` приложения (`viteConfig` — объект, `mergeConfig` из `vite`):
43
+
44
+ ```ts
45
+ import { defineApplicationEndToEndConfig, defineApplicationUnitConfig } from '@application-kit/testing/vitest'
46
+
47
+ import viteConfig from './vite.config.mjs'
48
+
49
+ const E2E_SPECS = ['src/modules/*/tests/*.spec.ts']
50
+
51
+ // vitest.config.mts
52
+ export default defineApplicationUnitConfig({
53
+ clientSources: ['src/modules/*/user-interface/client/**'],
54
+ clientSpecs: ['src/modules/*/user-interface/client/**/*.spec.ts'],
55
+ coverage: { exclude: ['**/*.module.ts'], include: ['src'], thresholds: { lines: 95 } },
56
+ excludeSpecs: E2E_SPECS,
57
+ root: import.meta.dirname,
58
+ setupFiles: ['./src/testing/fast-hashing.ts'],
59
+ specs: ['src/**/*.spec.ts'],
60
+ viteConfig,
61
+ })
62
+
63
+ // vitest.config.e2e.mts
64
+ export default defineApplicationEndToEndConfig({
65
+ coverage: { include: E2E_SPECS, thresholds: { 100: true } },
66
+ root: import.meta.dirname,
67
+ setupFiles: ['./src/testing/e2e-environment.ts'],
68
+ specs: E2E_SPECS,
69
+ viteConfig,
70
+ })
71
+ ```
72
+
73
+ Спеки, исходники клиента, покрытие (глобы, исключения, пороги), `setupFiles` и дополнительные пакеты для
74
+ `server.deps.inline` (`inlineDependencies`) — у приложения. Фабрики решают сами:
75
+
76
+ - оба профиля — `globals`, `isolate: false`, `testTimeout` 20 с (таймаут ловит зависший промис, а не медленный раннер),
77
+ кеш трансформов `.vitest-module-cache` (путь — контракт кеша джоб `ts:test:*` шаблона CI), покрытие v8 с отчётом
78
+ `text` и `skipFull`; `@application-kit/graphql` идёт через Vite, а не нативно через Node — иначе ошибка из пакета и код
79
+ приложения видят разные копии `graphql`, и `instanceof GraphQLError` ложен;
80
+ - unit — проекты `server` (`node`, спеки `specs` без `excludeSpecs` и `clientSpecs`) и `client` (`jsdom`, `clientSpecs`),
81
+ ячейки шарда CI и `ProjectShardSequencer` (ниже);
82
+ - e2e — окружение `node` и алиас `graphql` на CJS-вход `node_modules/graphql/index.js` под `root`: Vite для SSR берёт
83
+ ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm.
84
+
85
+ ## Ячейки CI по проектам
86
+
87
+ `vitest --shard=i/n` сам режет спеки хешем, и при двух проектах разных трансформов (`server` в `node`, `client` в
88
+ `jsdom`) объединение blob-отчётов покрытия неточное: непокрытые файлы одна ячейка синтезирует ssr-картой, а другая даёт
89
+ web-карту того же файла, и statements/functions задваиваются. `ProjectShardSequencer` отдаёт первой ячейке проект
90
+ `client` целиком, остальным — проект `server` по размеру спек (LPT, одна и та же раскладка в каждой ячейке без обмена
91
+ данными). `parseShard(process.argv)` читает `--shard=i/n` и `--shard i/n`; без флага, с одной ячейкой или с мусором в
92
+ значении — `undefined`, то есть обычный прогон (и `--merge-reports`, и Stryker).
93
+
94
+ `defineApplicationUnitConfig` выбирает ячейку по `process.argv` в момент вызова: 1 — только проект `client` и покрытие
95
+ по `clientSources`, 2..n — только проект `server` и покрытие без `clientSources`, без флага — оба проекта. Свой конфиг
96
+ без фабрики собирается из `parseShard` и `ProjectShardSequencer` напрямую.
@@ -0,0 +1,20 @@
1
+ import type { UserConfig } from 'vite';
2
+ import type { CoverageOptions } from 'vitest/node';
3
+ export interface ApplicationTestConfigOptionsInterface {
4
+ coverage: CoverageScopeInterface;
5
+ inlineDependencies?: readonly string[];
6
+ root: string;
7
+ setupFiles: readonly string[];
8
+ specs: readonly string[];
9
+ viteConfig: UserConfig;
10
+ }
11
+ export interface ApplicationUnitConfigOptionsInterface extends ApplicationTestConfigOptionsInterface {
12
+ clientSources: readonly string[];
13
+ clientSpecs: readonly string[];
14
+ excludeSpecs: readonly string[];
15
+ }
16
+ export interface CoverageScopeInterface {
17
+ exclude?: readonly string[];
18
+ include: readonly string[];
19
+ thresholds: NonNullable<CoverageOptions['thresholds']>;
20
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import type { ViteUserConfig } from 'vitest/config';
2
+ import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
3
+ export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
4
+ export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
@@ -0,0 +1,87 @@
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.defineApplicationEndToEndConfig = exports.defineApplicationUnitConfig = 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 project_shard_sequencer_js_1 = require("./project-shard-sequencer.js");
11
+ const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
12
+ const TEST_TIMEOUT = 20_000;
13
+ const cellOf = (argv) => {
14
+ const shard = (0, project_shard_sequencer_js_1.parseShard)(argv);
15
+ if (shard === undefined) {
16
+ return 'all';
17
+ }
18
+ return shard.index === 1 ? 'client' : 'server';
19
+ };
20
+ const coverageOf = (scope) => ({
21
+ clean: true,
22
+ cleanOnRerun: true,
23
+ exclude: [...(scope.exclude ?? [])],
24
+ include: [...scope.include],
25
+ provider: 'v8',
26
+ reporter: ['text'],
27
+ skipFull: true,
28
+ thresholds: scope.thresholds,
29
+ });
30
+ const sharedTestOptions = (options) => ({
31
+ experimental: { fsModuleCache: true, fsModuleCachePath: '.vitest-module-cache' },
32
+ globals: true,
33
+ isolate: false,
34
+ server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
35
+ setupFiles: [...options.setupFiles],
36
+ testTimeout: TEST_TIMEOUT,
37
+ });
38
+ const serverProject = (options) => ({
39
+ extends: true,
40
+ test: {
41
+ environment: 'node',
42
+ exclude: [...options.excludeSpecs, ...options.clientSpecs],
43
+ include: [...options.specs],
44
+ name: 'server',
45
+ },
46
+ });
47
+ const clientProject = (options) => ({
48
+ extends: true,
49
+ test: { environment: 'jsdom', include: [...options.clientSpecs], name: 'client' },
50
+ });
51
+ const PROJECTS_OF_CELL = {
52
+ all: [serverProject, clientProject],
53
+ client: [clientProject],
54
+ server: [serverProject],
55
+ };
56
+ const unitCoverageScope = (cell, options) => ({
57
+ exclude: [...(cell === 'server' ? options.clientSources : []), ...(options.coverage.exclude ?? [])],
58
+ include: cell === 'client' ? options.clientSources : options.coverage.include,
59
+ thresholds: options.coverage.thresholds,
60
+ });
61
+ const defineApplicationUnitConfig = (options) => {
62
+ const cell = cellOf(node_process_1.default.argv);
63
+ return (0, vite_1.mergeConfig)(options.viteConfig, {
64
+ root: options.root,
65
+ test: {
66
+ ...sharedTestOptions(options),
67
+ coverage: coverageOf(unitCoverageScope(cell, options)),
68
+ projects: PROJECTS_OF_CELL[cell].map((project) => project(options)),
69
+ sequence: { sequencer: project_shard_sequencer_js_1.ProjectShardSequencer },
70
+ },
71
+ });
72
+ };
73
+ exports.defineApplicationUnitConfig = defineApplicationUnitConfig;
74
+ const defineApplicationEndToEndConfig = (options) => {
75
+ const graphql = node_path_1.default.resolve(options.root, 'node_modules/graphql/index.js');
76
+ return (0, vite_1.mergeConfig)(options.viteConfig, {
77
+ resolve: { alias: { graphql } },
78
+ root: options.root,
79
+ test: {
80
+ ...sharedTestOptions(options),
81
+ coverage: coverageOf(options.coverage),
82
+ environment: 'node',
83
+ include: [...options.specs],
84
+ },
85
+ });
86
+ };
87
+ exports.defineApplicationEndToEndConfig = defineApplicationEndToEndConfig;
@@ -0,0 +1,3 @@
1
+ export * from './application-vitest-config.js';
2
+ export * from './application-vitest-config-options.interface.js';
3
+ export * from './project-shard-sequencer.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("./application-vitest-config.js"), exports);
18
+ __exportStar(require("./application-vitest-config-options.interface.js"), exports);
19
+ __exportStar(require("./project-shard-sequencer.js"), exports);
@@ -0,0 +1,15 @@
1
+ import type { TestSpecification } from 'vitest/node';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ export interface ShardInterface {
4
+ readonly count: number;
5
+ readonly index: number;
6
+ }
7
+ export interface WeightedSpecInterface {
8
+ readonly path: string;
9
+ readonly weight: number;
10
+ }
11
+ export declare const parseShard: (argv: readonly string[]) => ShardInterface | undefined;
12
+ export declare const assignShard: (specs: readonly WeightedSpecInterface[], index: number, count: number) => string[];
13
+ export declare class ProjectShardSequencer extends BaseSequencer {
14
+ shard(files: TestSpecification[]): Promise<TestSpecification[]>;
15
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProjectShardSequencer = exports.assignShard = exports.parseShard = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_1 = require("vitest/node");
6
+ const SHARD_FLAG = '--shard';
7
+ const shardArgument = (argv) => {
8
+ const position = argv.findIndex((argument) => argument === SHARD_FLAG || argument.startsWith(`${SHARD_FLAG}=`));
9
+ const flag = argv[position];
10
+ if (flag === undefined) {
11
+ return undefined;
12
+ }
13
+ return flag.includes('=') ? flag.slice(SHARD_FLAG.length + 1) : argv[position + 1];
14
+ };
15
+ const isCell = (value) => Number.isSafeInteger(value) && (value ?? 0) > 0;
16
+ const isRange = (index, count) => count >= 2 && index <= count;
17
+ const toShard = (value) => {
18
+ const [index, count] = value.split('/').map(Number);
19
+ return isCell(index) && isCell(count) && isRange(index, count) ? { count, index } : undefined;
20
+ };
21
+ const parseShard = (argv) => {
22
+ const value = shardArgument(argv);
23
+ return value === undefined ? undefined : toShard(value);
24
+ };
25
+ exports.parseShard = parseShard;
26
+ const takeLightest = (weights, weight) => {
27
+ const lightest = weights.indexOf(Math.min(...weights));
28
+ weights[lightest] = (weights[lightest] ?? 0) + weight;
29
+ return lightest + 1;
30
+ };
31
+ const assignShard = (specs, index, count) => {
32
+ const weights = Array.from({ length: count }, () => 0);
33
+ const ordered = specs.toSorted((left, right) => right.weight - left.weight || left.path.localeCompare(right.path));
34
+ const placed = ordered.map((spec) => ({ path: spec.path, shard: takeLightest(weights, spec.weight) }));
35
+ return placed.filter((spec) => spec.shard === index).map((spec) => spec.path);
36
+ };
37
+ exports.assignShard = assignShard;
38
+ class ProjectShardSequencer extends node_1.BaseSequencer {
39
+ async shard(files) {
40
+ const { shard } = this.ctx.config;
41
+ if (shard === undefined || shard.index === 1) {
42
+ return files;
43
+ }
44
+ const specs = await Promise.all(files.map(async (file) => {
45
+ const { size } = await (0, promises_1.stat)(file.moduleId);
46
+ return { path: file.moduleId, weight: size };
47
+ }));
48
+ const assigned = new Set((0, exports.assignShard)(specs, shard.index - 1, shard.count - 1));
49
+ return files.filter((file) => assigned.has(file.moduleId));
50
+ }
51
+ }
52
+ exports.ProjectShardSequencer = ProjectShardSequencer;
@@ -0,0 +1,20 @@
1
+ import type { UserConfig } from 'vite';
2
+ import type { CoverageOptions } from 'vitest/node';
3
+ export interface ApplicationTestConfigOptionsInterface {
4
+ coverage: CoverageScopeInterface;
5
+ inlineDependencies?: readonly string[];
6
+ root: string;
7
+ setupFiles: readonly string[];
8
+ specs: readonly string[];
9
+ viteConfig: UserConfig;
10
+ }
11
+ export interface ApplicationUnitConfigOptionsInterface extends ApplicationTestConfigOptionsInterface {
12
+ clientSources: readonly string[];
13
+ clientSpecs: readonly string[];
14
+ excludeSpecs: readonly string[];
15
+ }
16
+ export interface CoverageScopeInterface {
17
+ exclude?: readonly string[];
18
+ include: readonly string[];
19
+ thresholds: NonNullable<CoverageOptions['thresholds']>;
20
+ }
@@ -0,0 +1,4 @@
1
+ import type { ViteUserConfig } from 'vitest/config';
2
+ import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
3
+ export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
4
+ export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { mergeConfig } from 'vite';
4
+ import { parseShard, ProjectShardSequencer } from './project-shard-sequencer.js';
5
+ const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
6
+ const TEST_TIMEOUT = 20_000;
7
+ const cellOf = (argv) => {
8
+ const shard = parseShard(argv);
9
+ if (shard === undefined) {
10
+ return 'all';
11
+ }
12
+ return shard.index === 1 ? 'client' : 'server';
13
+ };
14
+ const coverageOf = (scope) => ({
15
+ clean: true,
16
+ cleanOnRerun: true,
17
+ exclude: [...(scope.exclude ?? [])],
18
+ include: [...scope.include],
19
+ provider: 'v8',
20
+ reporter: ['text'],
21
+ skipFull: true,
22
+ thresholds: scope.thresholds,
23
+ });
24
+ const sharedTestOptions = (options) => ({
25
+ experimental: { fsModuleCache: true, fsModuleCachePath: '.vitest-module-cache' },
26
+ globals: true,
27
+ isolate: false,
28
+ server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
29
+ setupFiles: [...options.setupFiles],
30
+ testTimeout: TEST_TIMEOUT,
31
+ });
32
+ const serverProject = (options) => ({
33
+ extends: true,
34
+ test: {
35
+ environment: 'node',
36
+ exclude: [...options.excludeSpecs, ...options.clientSpecs],
37
+ include: [...options.specs],
38
+ name: 'server',
39
+ },
40
+ });
41
+ const clientProject = (options) => ({
42
+ extends: true,
43
+ test: { environment: 'jsdom', include: [...options.clientSpecs], name: 'client' },
44
+ });
45
+ const PROJECTS_OF_CELL = {
46
+ all: [serverProject, clientProject],
47
+ client: [clientProject],
48
+ server: [serverProject],
49
+ };
50
+ const unitCoverageScope = (cell, options) => ({
51
+ exclude: [...(cell === 'server' ? options.clientSources : []), ...(options.coverage.exclude ?? [])],
52
+ include: cell === 'client' ? options.clientSources : options.coverage.include,
53
+ thresholds: options.coverage.thresholds,
54
+ });
55
+ export const defineApplicationUnitConfig = (options) => {
56
+ const cell = cellOf(process.argv);
57
+ return mergeConfig(options.viteConfig, {
58
+ root: options.root,
59
+ test: {
60
+ ...sharedTestOptions(options),
61
+ coverage: coverageOf(unitCoverageScope(cell, options)),
62
+ projects: PROJECTS_OF_CELL[cell].map((project) => project(options)),
63
+ sequence: { sequencer: ProjectShardSequencer },
64
+ },
65
+ });
66
+ };
67
+ export const defineApplicationEndToEndConfig = (options) => {
68
+ const graphql = path.resolve(options.root, 'node_modules/graphql/index.js');
69
+ return mergeConfig(options.viteConfig, {
70
+ resolve: { alias: { graphql } },
71
+ root: options.root,
72
+ test: {
73
+ ...sharedTestOptions(options),
74
+ coverage: coverageOf(options.coverage),
75
+ environment: 'node',
76
+ include: [...options.specs],
77
+ },
78
+ });
79
+ };
@@ -0,0 +1,3 @@
1
+ export * from './application-vitest-config.js';
2
+ export * from './application-vitest-config-options.interface.js';
3
+ export * from './project-shard-sequencer.js';
@@ -0,0 +1,3 @@
1
+ export * from './application-vitest-config.js';
2
+ export * from './application-vitest-config-options.interface.js';
3
+ export * from './project-shard-sequencer.js';
@@ -0,0 +1,15 @@
1
+ import type { TestSpecification } from 'vitest/node';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ export interface ShardInterface {
4
+ readonly count: number;
5
+ readonly index: number;
6
+ }
7
+ export interface WeightedSpecInterface {
8
+ readonly path: string;
9
+ readonly weight: number;
10
+ }
11
+ export declare const parseShard: (argv: readonly string[]) => ShardInterface | undefined;
12
+ export declare const assignShard: (specs: readonly WeightedSpecInterface[], index: number, count: number) => string[];
13
+ export declare class ProjectShardSequencer extends BaseSequencer {
14
+ shard(files: TestSpecification[]): Promise<TestSpecification[]>;
15
+ }
@@ -0,0 +1,46 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ const SHARD_FLAG = '--shard';
4
+ const shardArgument = (argv) => {
5
+ const position = argv.findIndex((argument) => argument === SHARD_FLAG || argument.startsWith(`${SHARD_FLAG}=`));
6
+ const flag = argv[position];
7
+ if (flag === undefined) {
8
+ return undefined;
9
+ }
10
+ return flag.includes('=') ? flag.slice(SHARD_FLAG.length + 1) : argv[position + 1];
11
+ };
12
+ const isCell = (value) => Number.isSafeInteger(value) && (value ?? 0) > 0;
13
+ const isRange = (index, count) => count >= 2 && index <= count;
14
+ const toShard = (value) => {
15
+ const [index, count] = value.split('/').map(Number);
16
+ return isCell(index) && isCell(count) && isRange(index, count) ? { count, index } : undefined;
17
+ };
18
+ export const parseShard = (argv) => {
19
+ const value = shardArgument(argv);
20
+ return value === undefined ? undefined : toShard(value);
21
+ };
22
+ const takeLightest = (weights, weight) => {
23
+ const lightest = weights.indexOf(Math.min(...weights));
24
+ weights[lightest] = (weights[lightest] ?? 0) + weight;
25
+ return lightest + 1;
26
+ };
27
+ export const assignShard = (specs, index, count) => {
28
+ const weights = Array.from({ length: count }, () => 0);
29
+ const ordered = specs.toSorted((left, right) => right.weight - left.weight || left.path.localeCompare(right.path));
30
+ const placed = ordered.map((spec) => ({ path: spec.path, shard: takeLightest(weights, spec.weight) }));
31
+ return placed.filter((spec) => spec.shard === index).map((spec) => spec.path);
32
+ };
33
+ export class ProjectShardSequencer extends BaseSequencer {
34
+ async shard(files) {
35
+ const { shard } = this.ctx.config;
36
+ if (shard === undefined || shard.index === 1) {
37
+ return files;
38
+ }
39
+ const specs = await Promise.all(files.map(async (file) => {
40
+ const { size } = await stat(file.moduleId);
41
+ return { path: file.moduleId, weight: size };
42
+ }));
43
+ const assigned = new Set(assignShard(specs, shard.index - 1, shard.count - 1));
44
+ return files.filter((file) => assigned.has(file.moduleId));
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@application-kit/testing",
3
- "version": "0.1.0",
4
- "description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения и Postgres в testcontainers",
3
+ "version": "0.3.0",
4
+ "description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers, раскладка ячеек CI по проектам vitest и фабрики конфигов vitest",
5
5
  "keywords": [
6
6
  "cqrs",
7
7
  "nestjs",
8
8
  "testcontainers",
9
- "testing"
9
+ "testing",
10
+ "vitest"
10
11
  ],
11
12
  "homepage": "https://gitlab.com/application-kit/application-kit/-/tree/main/packages/testing",
12
13
  "bugs": "https://gitlab.com/application-kit/application-kit/-/issues",
@@ -32,7 +33,17 @@
32
33
  "default": "./dist/cjs/index.js"
33
34
  }
34
35
  },
35
- "./package.json": "./package.json"
36
+ "./package.json": "./package.json",
37
+ "./vitest": {
38
+ "import": {
39
+ "types": "./dist/esm/vitest/index.d.ts",
40
+ "default": "./dist/esm/vitest/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/cjs/vitest/index.d.ts",
44
+ "default": "./dist/cjs/vitest/index.js"
45
+ }
46
+ }
36
47
  },
37
48
  "main": "./dist/cjs/index.js",
38
49
  "module": "./dist/esm/index.js",
@@ -41,7 +52,7 @@
41
52
  "dist"
42
53
  ],
43
54
  "scripts": {
44
- "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
55
+ "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=vitest && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
45
56
  "lint:tsc": "tsc --noEmit --pretty --project .",
46
57
  "test:mutation": "stryker run",
47
58
  "test:unit": "vitest run"
@@ -65,6 +76,7 @@
65
76
  "testcontainers": "12.1.0",
66
77
  "typeorm": "^1.1.0",
67
78
  "typescript": "^6.0.3",
79
+ "vite": "^8.2.1",
68
80
  "vitest": "^4.1.11"
69
81
  },
70
82
  "peerDependencies": {
@@ -78,7 +90,17 @@
78
90
  "oxide.ts": "^1.1.0",
79
91
  "rxjs": "^7.8.2",
80
92
  "testcontainers": "^12.1.0",
81
- "typeorm": "^1.0.0"
93
+ "typeorm": "^1.0.0",
94
+ "vite": "^8.0.0",
95
+ "vitest": "^4.1.0"
96
+ },
97
+ "peerDependenciesMeta": {
98
+ "vite": {
99
+ "optional": true
100
+ },
101
+ "vitest": {
102
+ "optional": true
103
+ }
82
104
  },
83
105
  "engines": {
84
106
  "node": ">=24"