Merged PR 1879: Warenbegleitschein Übersicht und Details

Related work items: #5137, #5138
This commit is contained in:
Lorenz Hilpert
2025-07-10 16:00:16 +00:00
parent f6b2b554bb
commit a36d746fb8
115 changed files with 37648 additions and 32935 deletions

View File

@@ -0,0 +1,277 @@
# Common Decorators Library
A collection of TypeScript decorators for common cross-cutting concerns in Angular applications.
## Installation
This library is already configured in the project's `tsconfig.base.json`. Import decorators using:
```typescript
import { InFlight, InFlightWithKey, InFlightWithCache } from '@isa/common/decorators';
```
## Available Decorators
### 🚀 InFlight Decorators
Prevent multiple simultaneous calls to the same async method. All concurrent calls receive the same Promise result.
#### Basic Usage
```typescript
import { InFlight } from '@isa/common/decorators';
@Injectable()
class DataService {
@InFlight()
async fetchData(): Promise<Data> {
// Even if called multiple times simultaneously,
// only one API call will be made
return await this.http.get<Data>('/api/data').toPromise();
}
}
```
**Benefits:**
- Prevents duplicate API calls
- Reduces server load
- Improves application performance
- All callers receive the same result
### 🔑 InFlightWithKey
Prevents duplicate calls while considering method arguments. Each unique set of arguments gets its own in-flight tracking.
```typescript
import { InFlightWithKey } from '@isa/common/decorators';
@Injectable()
class UserService {
@InFlightWithKey({
keyGenerator: (userId: string) => userId
})
async fetchUser(userId: string): Promise<User> {
// Multiple calls with same userId share the same request
// Different userIds can execute simultaneously
return await this.http.get<User>(`/api/users/${userId}`).toPromise();
}
@InFlightWithKey() // Uses JSON.stringify by default
async searchUsers(query: string, page: number): Promise<User[]> {
return await this.http.get<User[]>(`/api/users/search`, {
params: { query, page: page.toString() }
}).toPromise();
}
}
```
**Configuration Options:**
- `keyGenerator?: (...args) => string` - Custom key generation function
- If not provided, uses `JSON.stringify(args)` as the key
### 🗄️ InFlightWithCache
Combines in-flight request deduplication with result caching.
```typescript
import { InFlightWithCache } from '@isa/common/decorators';
@Injectable()
class ProductService {
@InFlightWithCache({
cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
keyGenerator: (productId: string) => productId
})
async getProduct(productId: string): Promise<Product> {
// Results are cached for 5 minutes
// Multiple calls within cache time return cached result
return await this.http.get<Product>(`/api/products/${productId}`).toPromise();
}
}
```
**Configuration Options:**
- `cacheTime?: number` - Cache duration in milliseconds
- `keyGenerator?: (...args) => string` - Custom key generation function
## How It Works
### Memory Management
All decorators use `WeakMap` for memory efficiency:
- Automatic garbage collection when instances are destroyed
- No memory leaks
- Per-instance state isolation
### Error Handling
- Failed requests are not cached
- In-flight tracking is cleaned up on both success and error
- All concurrent callers receive the same error
### Thread Safety
- Decorators are instance-aware
- Each service instance has its own in-flight tracking
- No shared state between instances
## Real-World Examples
### Solving Your Original Problem
```typescript
// Before: Multiple simultaneous calls
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple calls = multiple API requests
return await this.apiCall();
}
}
// After: Using InFlight decorator
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
@InFlight()
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple simultaneous calls = single API request
return await this.apiCall();
}
}
```
### Advanced Scenarios
```typescript
@Injectable()
class OrderService {
// Different cache times for different data types
@InFlightWithCache({ cacheTime: 30 * 1000 }) // 30 seconds
async getOrderStatus(orderId: string): Promise<OrderStatus> {
return await this.http.get<OrderStatus>(`/api/orders/${orderId}/status`).toPromise();
}
@InFlightWithCache({ cacheTime: 10 * 60 * 1000 }) // 10 minutes
async getOrderHistory(customerId: string): Promise<Order[]> {
return await this.http.get<Order[]>(`/api/customers/${customerId}/orders`).toPromise();
}
// Custom key generation for complex parameters
@InFlightWithKey({
keyGenerator: (filter: OrderFilter) =>
`${filter.status}-${filter.dateFrom}-${filter.dateTo}`
})
async searchOrders(filter: OrderFilter): Promise<Order[]> {
return await this.http.post<Order[]>('/api/orders/search', filter).toPromise();
}
}
```
## Best Practices
### ✅ Do
- Use `@InFlight()` for simple methods without parameters
- Use `@InFlightWithKey()` for methods with parameters
- Use `@InFlightWithCache()` for expensive operations with stable results
- Provide custom `keyGenerator` for complex parameter objects
- Set appropriate cache times based on data volatility
### ❌ Don't
- Use on methods that return different results for the same input
- Use excessively long cache times for dynamic data
- Use on methods that have side effects (POST, PUT, DELETE)
- Rely on argument order for default key generation
## Performance Considerations
### Memory Usage
- `InFlight`: Minimal memory overhead (one Promise per instance)
- `InFlightWithKey`: Memory usage scales with unique parameter combinations
- `InFlightWithCache`: Additional memory for cached results
### Cleanup
- In-flight requests are automatically cleaned up on completion
- Cache entries are cleaned up on expiry
- WeakMap ensures instances can be garbage collected
## Testing
The decorators are fully tested with comprehensive unit tests. Key test scenarios include:
- Multiple simultaneous calls deduplication
- Error handling and cleanup
- Cache expiration
- Instance isolation
- Key generation
Run tests with:
```bash
npx nx test common-decorators
```
## Migration Guide
### From Manual Implementation
```typescript
// Before: Manual in-flight tracking
class MyService {
private inFlight: Promise<Data> | null = null;
async fetchData(): Promise<Data> {
if (this.inFlight) {
return this.inFlight;
}
this.inFlight = this.doFetch();
try {
return await this.inFlight;
} finally {
this.inFlight = null;
}
}
}
// After: Using decorator
class MyService {
@InFlight()
async fetchData(): Promise<Data> {
return await this.doFetch();
}
}
```
### From RxJS shareReplay
```typescript
// Before: RxJS approach
class MyService {
private data$ = this.http.get<Data>('/api/data').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
getData(): Observable<Data> {
return this.data$;
}
}
// After: Promise-based with decorator
class MyService {
@InFlightWithCache({ cacheTime: 5 * 60 * 1000 })
async getData(): Promise<Data> {
return await this.http.get<Data>('/api/data').toPromise();
}
}
```
## Contributing
When adding new decorators:
1. Add implementation in `src/lib/`
2. Include comprehensive unit tests
3. Update this documentation
4. Export from `src/index.ts`

View File

@@ -0,0 +1,34 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'common',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'common',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "common-decorators",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/common/decorators/src",
"prefix": "common",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/common/decorators"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1 @@
export * from './lib/in-flight.decorator';

View File

@@ -0,0 +1,321 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InFlight, InFlightWithKey, InFlightWithCache } from './in-flight.decorator';
describe('InFlight Decorators', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllTimers();
});
describe('InFlight', () => {
class TestService {
callCount = 0;
@InFlight()
async fetchData(delay = 100): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, delay));
return `result-${this.callCount}`;
}
@InFlight()
async fetchWithError(delay = 100): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, delay));
throw new Error('Test error');
}
}
it('should prevent multiple simultaneous calls', async () => {
const service = new TestService();
// Make three simultaneous calls
const promise1 = service.fetchData();
const promise2 = service.fetchData();
const promise3 = service.fetchData();
// Advance timers to complete the async operation
await vi.runAllTimersAsync();
// All promises should resolve to the same value
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toBe('result-1');
expect(result2).toBe('result-1');
expect(result3).toBe('result-1');
expect(service.callCount).toBe(1);
});
it('should allow subsequent calls after completion', async () => {
const service = new TestService();
// First call
const promise1 = service.fetchData();
await vi.runAllTimersAsync();
const result1 = await promise1;
expect(result1).toBe('result-1');
// Second call after first completes
const promise2 = service.fetchData();
await vi.runAllTimersAsync();
const result2 = await promise2;
expect(result2).toBe('result-2');
expect(service.callCount).toBe(2);
});
it('should handle errors properly', async () => {
const service = new TestService();
// Make multiple calls that will error
const promise1 = service.fetchWithError();
const promise2 = service.fetchWithError();
await vi.runAllTimersAsync();
// Both should reject with the same error
await expect(promise1).rejects.toThrow('Test error');
await expect(promise2).rejects.toThrow('Test error');
expect(service.callCount).toBe(1);
// Should allow new call after error
const promise3 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise3).rejects.toThrow('Test error');
expect(service.callCount).toBe(2);
});
it('should maintain separate state per instance', async () => {
const service1 = new TestService();
const service2 = new TestService();
// Make simultaneous calls on different instances
const promise1 = service1.fetchData();
const promise2 = service2.fetchData();
await vi.runAllTimersAsync();
const [result1, result2] = await Promise.all([promise1, promise2]);
// Each instance should have made its own call
expect(result1).toBe('result-1');
expect(result2).toBe('result-1');
expect(service1.callCount).toBe(1);
expect(service2.callCount).toBe(1);
});
});
describe('InFlightWithKey', () => {
class UserService {
callCounts = new Map<string, number>();
@InFlightWithKey({
keyGenerator: (userId: string) => userId
})
async fetchUser(userId: string, delay = 100): Promise<{ id: string; name: string }> {
const count = (this.callCounts.get(userId) || 0) + 1;
this.callCounts.set(userId, count);
await new Promise(resolve => setTimeout(resolve, delay));
return { id: userId, name: `User ${userId} - Call ${count}` };
}
@InFlightWithKey()
async fetchWithDefaultKey(param1: string, param2: number): Promise<string> {
const key = `${param1}-${param2}`;
const count = (this.callCounts.get(key) || 0) + 1;
this.callCounts.set(key, count);
await new Promise(resolve => setTimeout(resolve, 100));
return `Result ${count}`;
}
}
it('should deduplicate calls with same key', async () => {
const service = new UserService();
// Multiple calls with same userId
const promise1 = service.fetchUser('user1');
const promise2 = service.fetchUser('user1');
const promise3 = service.fetchUser('user1');
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
expect(result2).toEqual(result1);
expect(result3).toEqual(result1);
expect(service.callCounts.get('user1')).toBe(1);
});
it('should allow simultaneous calls with different keys', async () => {
const service = new UserService();
// Calls with different userIds
const promise1 = service.fetchUser('user1');
const promise2 = service.fetchUser('user2');
const promise3 = service.fetchUser('user1'); // Duplicate of first
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
expect(result2).toEqual({ id: 'user2', name: 'User user2 - Call 1' });
expect(result3).toEqual(result1); // Same as first call
expect(service.callCounts.get('user1')).toBe(1);
expect(service.callCounts.get('user2')).toBe(1);
});
it('should use JSON.stringify as default key generator', async () => {
const service = new UserService();
// Multiple calls with same arguments
const promise1 = service.fetchWithDefaultKey('test', 123);
const promise2 = service.fetchWithDefaultKey('test', 123);
// Different arguments
const promise3 = service.fetchWithDefaultKey('test', 456);
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toBe('Result 1');
expect(result2).toBe('Result 1'); // Same as first
expect(result3).toBe('Result 1'); // Different key, separate call
expect(service.callCounts.get('test-123')).toBe(1);
expect(service.callCounts.get('test-456')).toBe(1);
});
});
describe('InFlightWithCache', () => {
class DataService {
callCount = 0;
@InFlightWithCache({
cacheTime: 1000, // 1 second cache
keyGenerator: (query: string) => query
})
async search(query: string): Promise<string[]> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 100));
return [`result-${query}-${this.callCount}`];
}
@InFlightWithCache({
cacheTime: 500
})
async fetchWithExpiry(id: number): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 50));
return `data-${id}-${this.callCount}`;
}
}
it('should cache results for specified time', async () => {
const service = new DataService();
// First call
const promise1 = service.search('test');
await vi.runAllTimersAsync();
const result1 = await promise1;
expect(result1).toEqual(['result-test-1']);
expect(service.callCount).toBe(1);
// Second call within cache time - should return cached result
const result2 = await service.search('test');
expect(result2).toEqual(['result-test-1']);
expect(service.callCount).toBe(1); // No new call
// Advance time past cache expiry
vi.advanceTimersByTime(1100);
// Third call after cache expiry - should make new call
const promise3 = service.search('test');
await vi.runAllTimersAsync();
const result3 = await promise3;
expect(result3).toEqual(['result-test-2']);
expect(service.callCount).toBe(2);
});
it('should handle in-flight deduplication with caching', async () => {
const service = new DataService();
// Multiple simultaneous calls
const promise1 = service.search('query1');
const promise2 = service.search('query1');
const promise3 = service.search('query1');
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
// All should get same result
expect(result1).toEqual(['result-query1-1']);
expect(result2).toEqual(result1);
expect(result3).toEqual(result1);
expect(service.callCount).toBe(1);
// Subsequent call should use cache
const result4 = await service.search('query1');
expect(result4).toEqual(['result-query1-1']);
expect(service.callCount).toBe(1);
});
it('should clean up expired cache entries', async () => {
const service = new DataService();
// Make a call
const promise1 = service.fetchWithExpiry(1);
await vi.runAllTimersAsync();
await promise1;
// Advance time past cache expiry
vi.advanceTimersByTime(600);
// Make another call - should not use expired cache
service.callCount = 0; // Reset for clarity
const promise2 = service.fetchWithExpiry(1);
await vi.runAllTimersAsync();
const result2 = await promise2;
expect(result2).toBe('data-1-1');
expect(service.callCount).toBe(1); // New call was made
});
it('should handle errors without caching them', async () => {
class ErrorService {
callCount = 0;
@InFlightWithCache({ cacheTime: 1000 })
async fetchWithError(): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 50));
throw new Error('API Error');
}
}
const service = new ErrorService();
// First call that errors
const promise1 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise1).rejects.toThrow('API Error');
expect(service.callCount).toBe(1);
// Second call should not use cache (errors aren't cached)
const promise2 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise2).rejects.toThrow('API Error');
expect(service.callCount).toBe(2);
});
});
});

View File

@@ -0,0 +1,251 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Decorator that prevents multiple simultaneous calls to the same async method.
* All concurrent calls will receive the same Promise result.
*
* @example
* ```typescript
* class MyService {
* @InFlight()
* async fetchData(): Promise<Data> {
* // This method will only execute once even if called multiple times simultaneously
* return await api.getData();
* }
* }
* ```
*/
export function InFlight<
T extends (...args: any[]) => Promise<any>,
>(): MethodDecorator {
const inFlightMap = new WeakMap<object, Promise<any>>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Check if there's already an in-flight request for this instance
const existingRequest = inFlightMap.get(this);
if (existingRequest) {
return existingRequest;
}
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
inFlightMap.delete(this);
return result;
})
.catch((error: any) => {
// Clean up after error
inFlightMap.delete(this);
throw error;
});
inFlightMap.set(this, promise);
return promise;
};
return descriptor;
};
}
/**
* Decorator that prevents multiple simultaneous calls to the same async method
* while considering method arguments. Each unique set of arguments gets its own
* in-flight tracking.
*
* @param options Configuration options for the decorator
* @example
* ```typescript
* class UserService {
* @InFlightWithKey({
* keyGenerator: (userId: string) => userId
* })
* async fetchUser(userId: string): Promise<User> {
* // Calls with different userIds can execute simultaneously
* // Calls with the same userId will share the same promise
* return await api.getUser(userId);
* }
* }
* ```
*/
export interface InFlightWithKeyOptions<T extends (...args: any[]) => any> {
/**
* Generate a cache key from the method arguments.
* If not provided, uses JSON.stringify on all arguments.
*/
keyGenerator?: (...args: Parameters<T>) => string;
}
export function InFlightWithKey<T extends (...args: any[]) => Promise<any>>(
options: InFlightWithKeyOptions<T> = {},
): MethodDecorator {
const inFlightMap = new WeakMap<object, Map<string, Promise<any>>>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Initialize map for this instance if needed
if (!inFlightMap.has(this)) {
inFlightMap.set(this, new Map());
}
const instanceMap = inFlightMap.get(this)!;
// Generate cache key
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
// Check if there's already an in-flight request for this key
const existingRequest = instanceMap.get(key);
if (existingRequest) {
return existingRequest;
}
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
instanceMap.delete(key);
return result;
})
.catch((error: any) => {
// Clean up after error
instanceMap.delete(key);
throw error;
});
instanceMap.set(key, promise);
return promise;
};
return descriptor;
};
}
/**
* Decorator that prevents multiple simultaneous calls to the same async method
* with additional caching capabilities.
*
* @param options Configuration options for the decorator
* @example
* ```typescript
* class DataService {
* @InFlightWithCache({
* cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
* keyGenerator: (params: QueryParams) => params.query
* })
* async searchData(params: QueryParams): Promise<SearchResult> {
* return await api.search(params);
* }
* }
* ```
*/
export interface InFlightWithCacheOptions<T extends (...args: any[]) => any> {
/**
* Generate a cache key from the method arguments.
* If not provided, uses JSON.stringify on all arguments.
*/
keyGenerator?: (...args: Parameters<T>) => string;
/**
* Time in milliseconds to keep the result cached after completion.
* If not provided, result is not cached after completion.
*/
cacheTime?: number;
}
export function InFlightWithCache<T extends (...args: any[]) => Promise<any>>(
options: InFlightWithCacheOptions<T> = {},
): MethodDecorator {
const inFlightMap = new WeakMap<object, Map<string, Promise<any>>>();
const cacheMap = new WeakMap<
object,
Map<string, { result: any; expiry: number }>
>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Initialize maps for this instance if needed
if (!inFlightMap.has(this)) {
inFlightMap.set(this, new Map());
cacheMap.set(this, new Map());
}
const instanceInFlight = inFlightMap.get(this)!;
const instanceCache = cacheMap.get(this)!;
// Generate cache key
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
// Check cache first (if cacheTime is set)
if (options.cacheTime) {
const cached = instanceCache.get(key);
if (cached && cached.expiry > Date.now()) {
return Promise.resolve(cached.result);
}
// Clean up expired cache entry
if (cached) {
instanceCache.delete(key);
}
}
// Check if there's already an in-flight request
const existingRequest = instanceInFlight.get(key);
if (existingRequest) {
return existingRequest;
}
// Create new request
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Cache result if cacheTime is set
if (options.cacheTime) {
instanceCache.set(key, {
result,
expiry: Date.now() + options.cacheTime,
});
}
return result;
})
.finally(() => {
// Always clean up in-flight request
instanceInFlight.delete(key);
});
instanceInFlight.set(key, promise);
return promise;
};
return descriptor;
};
}

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}

View File

@@ -0,0 +1,27 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/common/decorators',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../../coverage/libs/common/decorators',
provider: 'v8' as const,
},
},
}));