@qlover/create-app 0.10.6 → 0.12.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 +22 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +3 -3
- package/dist/templates/next-app/eslint.config.mjs +76 -1
- package/dist/templates/next-app/package.json +1 -0
- package/dist/templates/react-app/docs/en/test-guide.md +194 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +194 -0
- package/dist/templates/react-app/eslint.config.mjs +79 -7
- package/dist/templates/react-app/package.json +2 -2
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +3 -0
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +5 -0
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +3 -0
- package/dist/templates/react-app/src/base/services/I18nService.ts +3 -0
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +24 -0
- package/dist/templates/react-app/src/base/services/UserBootstrap.ts +3 -0
- package/dist/templates/react-app/src/base/services/UserService.ts +3 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/ChatMessageBridge.ts +6 -0
- package/package.json +4 -3
|
@@ -5,6 +5,7 @@ import importPlugin from 'eslint-plugin-import';
|
|
|
5
5
|
import prettierPlugin from 'eslint-plugin-prettier';
|
|
6
6
|
import unusedImports from 'eslint-plugin-unused-imports';
|
|
7
7
|
import qloverEslint from '@qlover/eslint-plugin';
|
|
8
|
+
import tseslint from 'typescript-eslint';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = dirname(__filename);
|
|
@@ -106,7 +107,7 @@ const eslintConfig = [
|
|
|
106
107
|
rules: {
|
|
107
108
|
'@qlover-eslint/ts-class-method-return': 'error',
|
|
108
109
|
'@qlover-eslint/ts-class-member-accessibility': 'error',
|
|
109
|
-
'@qlover-eslint/ts-class-override': '
|
|
110
|
+
'@qlover-eslint/ts-class-override': 'off',
|
|
110
111
|
'@qlover-eslint/require-root-testid': ['error', {
|
|
111
112
|
exclude: ['/Provider$/']
|
|
112
113
|
}],
|
|
@@ -188,6 +189,80 @@ const eslintConfig = [
|
|
|
188
189
|
'import/no-default-export': 'error',
|
|
189
190
|
}
|
|
190
191
|
},
|
|
192
|
+
// TypeScript files with type checking for ts-class-override rule
|
|
193
|
+
// The ts-class-override rule requires full type information to accurately detect:
|
|
194
|
+
// - Methods that override parent class methods (via extends)
|
|
195
|
+
// - Methods that implement interface methods (via implements)
|
|
196
|
+
// Without type checking, the rule falls back to AST-based heuristics which are less accurate
|
|
197
|
+
// This separate config block enables type checking only for TypeScript files to provide
|
|
198
|
+
// accurate override detection while maintaining good performance
|
|
199
|
+
...tseslint.configs.recommendedTypeChecked.map((config) => ({
|
|
200
|
+
...config,
|
|
201
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
202
|
+
ignores: [
|
|
203
|
+
'**/dist/**',
|
|
204
|
+
'**/build/**',
|
|
205
|
+
'**/ts-build/**',
|
|
206
|
+
'**/node_modules/**',
|
|
207
|
+
'**/.nx/**',
|
|
208
|
+
'**/.cache/**',
|
|
209
|
+
'**/coverage/**',
|
|
210
|
+
'**/*.d.ts',
|
|
211
|
+
'**/*.config.ts',
|
|
212
|
+
'**/*.test.ts',
|
|
213
|
+
'**/__mocks__/**',
|
|
214
|
+
'**/__tests__/**',
|
|
215
|
+
'**/*.spec.ts',
|
|
216
|
+
...(config.ignores || [])
|
|
217
|
+
],
|
|
218
|
+
languageOptions: {
|
|
219
|
+
...config.languageOptions,
|
|
220
|
+
parserOptions: {
|
|
221
|
+
...config.languageOptions?.parserOptions,
|
|
222
|
+
project: './tsconfig.json',
|
|
223
|
+
tsconfigRootDir: __dirname
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
plugins: {
|
|
227
|
+
...config.plugins,
|
|
228
|
+
'@qlover-eslint': qloverEslint
|
|
229
|
+
},
|
|
230
|
+
rules: {
|
|
231
|
+
...config.rules,
|
|
232
|
+
// Enable ts-class-override rule with full type information
|
|
233
|
+
// This rule is disabled in the base config above and only enabled here where
|
|
234
|
+
// type information is available, ensuring accurate detection of override relationships
|
|
235
|
+
'@qlover-eslint/ts-class-override': 'error',
|
|
236
|
+
// Disable other type-checked rules to avoid performance impact
|
|
237
|
+
// We only need type checking for ts-class-override, so we disable other
|
|
238
|
+
// type-aware rules that would slow down linting without providing value
|
|
239
|
+
'@typescript-eslint/ban-ts-comment': 'off',
|
|
240
|
+
'@typescript-eslint/restrict-template-expressions': 'off',
|
|
241
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
242
|
+
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
|
243
|
+
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
|
244
|
+
'@typescript-eslint/no-unsafe-return': 'off',
|
|
245
|
+
'@typescript-eslint/no-empty-object-type': 'off',
|
|
246
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
247
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
248
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
249
|
+
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
|
250
|
+
'@typescript-eslint/no-unsafe-literal-comparison': 'off',
|
|
251
|
+
'@typescript-eslint/no-unsafe-nullish-coalescing': 'off',
|
|
252
|
+
'@typescript-eslint/no-unsafe-optional-chaining': 'off',
|
|
253
|
+
'@typescript-eslint/unbound-method': 'off',
|
|
254
|
+
'@typescript-eslint/await-thenable': 'off',
|
|
255
|
+
'@typescript-eslint/no-floating-promises': 'off',
|
|
256
|
+
'@typescript-eslint/no-misused-promises': 'off',
|
|
257
|
+
'@typescript-eslint/require-await': 'off',
|
|
258
|
+
'@typescript-eslint/no-base-to-string': 'off',
|
|
259
|
+
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
|
260
|
+
'@typescript-eslint/no-duplicate-type-constituents': 'off',
|
|
261
|
+
// Disable @typescript-eslint/no-unused-vars as we use unused-imports/no-unused-vars instead
|
|
262
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
263
|
+
'@typescript-eslint/only-throw-error': 'off'
|
|
264
|
+
}
|
|
265
|
+
})),
|
|
191
266
|
// 为特定文件允许 default export
|
|
192
267
|
{
|
|
193
268
|
files: [
|
|
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
|
|
|
627
627
|
|
|
628
628
|
### Type Safety Testing
|
|
629
629
|
|
|
630
|
+
TypeScript project tests should not only verify runtime behavior but also ensure the correctness of the type system. Vitest provides the `expectTypeOf` utility for compile-time type checking.
|
|
631
|
+
|
|
632
|
+
#### Why Type Testing?
|
|
633
|
+
|
|
634
|
+
1. **Type Inference Validation**: Ensure TypeScript correctly infers complex types
|
|
635
|
+
2. **Generic Constraint Checking**: Verify generic parameter constraints
|
|
636
|
+
3. **Type Compatibility**: Ensure type definitions match actual usage
|
|
637
|
+
4. **API Contract Guarantee**: Prevent breaking changes in type definitions
|
|
638
|
+
|
|
639
|
+
#### Basic Type Testing
|
|
640
|
+
|
|
630
641
|
```typescript
|
|
642
|
+
import { describe, it, expectTypeOf } from 'vitest';
|
|
643
|
+
|
|
631
644
|
describe('TypeSafetyTests', () => {
|
|
632
645
|
it('should maintain type safety', () => {
|
|
633
646
|
const processor = new DataProcessor<User>();
|
|
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
|
|
|
636
649
|
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
650
|
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
651
|
});
|
|
652
|
+
|
|
653
|
+
it('should infer correct return types', () => {
|
|
654
|
+
const result = getData();
|
|
655
|
+
|
|
656
|
+
// Verify return type
|
|
657
|
+
expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
|
|
658
|
+
expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should validate parameter types', () => {
|
|
662
|
+
function processUser(user: User): void {
|
|
663
|
+
// implementation
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Verify parameter type
|
|
667
|
+
expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
|
|
668
|
+
expectTypeOf(processUser).parameter(0).toHaveProperty('id');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### Generic Type Testing
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
describe('Generic Type Tests', () => {
|
|
677
|
+
it('should work with generic constraints', () => {
|
|
678
|
+
class Storage<T extends { id: number }> {
|
|
679
|
+
store(item: T): T {
|
|
680
|
+
return item;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const storage = new Storage<User>();
|
|
685
|
+
|
|
686
|
+
// Verify generic type
|
|
687
|
+
expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
|
|
688
|
+
expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should validate complex generic types', () => {
|
|
692
|
+
type ApiResponse<T> = {
|
|
693
|
+
data: T;
|
|
694
|
+
status: number;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const response: ApiResponse<User[]> = {
|
|
699
|
+
data: [],
|
|
700
|
+
status: 200
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Verify nested generic type
|
|
704
|
+
expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
|
|
705
|
+
expectTypeOf(response.data).toEqualTypeOf<User[]>();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### Union and Intersection Type Testing
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
describe('Union and Intersection Types', () => {
|
|
714
|
+
it('should handle union types correctly', () => {
|
|
715
|
+
type Result = Success | Error;
|
|
716
|
+
type Success = { status: 'success'; data: string };
|
|
717
|
+
type Error = { status: 'error'; message: string };
|
|
718
|
+
|
|
719
|
+
function handleResult(result: Result): void {
|
|
720
|
+
// implementation
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Verify union type
|
|
724
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
|
|
725
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle intersection types correctly', () => {
|
|
729
|
+
type Timestamped = { createdAt: Date; updatedAt: Date };
|
|
730
|
+
type UserWithTimestamp = User & Timestamped;
|
|
731
|
+
|
|
732
|
+
const user: UserWithTimestamp = {
|
|
733
|
+
id: 1,
|
|
734
|
+
name: 'John',
|
|
735
|
+
createdAt: new Date(),
|
|
736
|
+
updatedAt: new Date()
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Verify intersection type contains all properties
|
|
740
|
+
expectTypeOf(user).toHaveProperty('id');
|
|
741
|
+
expectTypeOf(user).toHaveProperty('name');
|
|
742
|
+
expectTypeOf(user).toHaveProperty('createdAt');
|
|
743
|
+
expectTypeOf(user).toHaveProperty('updatedAt');
|
|
744
|
+
});
|
|
639
745
|
});
|
|
640
746
|
```
|
|
641
747
|
|
|
748
|
+
#### Type Narrowing Testing
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
describe('Type Narrowing Tests', () => {
|
|
752
|
+
it('should validate type guards', () => {
|
|
753
|
+
function isString(value: unknown): value is string {
|
|
754
|
+
return typeof value === 'string';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const value: unknown = 'test';
|
|
758
|
+
|
|
759
|
+
if (isString(value)) {
|
|
760
|
+
// Within this scope, value should be narrowed to string type
|
|
761
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should validate discriminated unions', () => {
|
|
766
|
+
type Shape =
|
|
767
|
+
| { kind: 'circle'; radius: number }
|
|
768
|
+
| { kind: 'rectangle'; width: number; height: number };
|
|
769
|
+
|
|
770
|
+
function getArea(shape: Shape): number {
|
|
771
|
+
if (shape.kind === 'circle') {
|
|
772
|
+
// In this branch, shape should be narrowed to circle type
|
|
773
|
+
expectTypeOf(shape).toHaveProperty('radius');
|
|
774
|
+
expectTypeOf(shape).not.toHaveProperty('width');
|
|
775
|
+
return Math.PI * shape.radius ** 2;
|
|
776
|
+
} else {
|
|
777
|
+
// In this branch, shape should be narrowed to rectangle type
|
|
778
|
+
expectTypeOf(shape).toHaveProperty('width');
|
|
779
|
+
expectTypeOf(shape).toHaveProperty('height');
|
|
780
|
+
return shape.width * shape.height;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### Practical Recommendations
|
|
788
|
+
|
|
789
|
+
1. **Combine with Runtime Tests**: Type tests should complement runtime tests
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('Combined Runtime and Type Tests', () => {
|
|
793
|
+
it('should validate both runtime behavior and types', () => {
|
|
794
|
+
function add(a: number, b: number): number {
|
|
795
|
+
return a + b;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Type test
|
|
799
|
+
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
|
|
800
|
+
expectTypeOf(add).returns.toEqualTypeOf<number>();
|
|
801
|
+
|
|
802
|
+
// Runtime test
|
|
803
|
+
expect(add(1, 2)).toBe(3);
|
|
804
|
+
expect(add(-1, 1)).toBe(0);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
2. **Test Type Inference**: Ensure TypeScript correctly infers types, avoid overusing `any`
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
describe('Type Inference Tests', () => {
|
|
813
|
+
it('should infer types correctly', () => {
|
|
814
|
+
const data = { id: 1, name: 'John' };
|
|
815
|
+
|
|
816
|
+
// Verify inferred type
|
|
817
|
+
expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
|
|
818
|
+
expectTypeOf(data.id).toEqualTypeOf<number>();
|
|
819
|
+
expectTypeOf(data.name).toEqualTypeOf<string>();
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
3. **Use TypeScript Compiler Checks**: Run `tsc --noEmit` in CI to ensure no type errors
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# Add script in package.json
|
|
828
|
+
{
|
|
829
|
+
"scripts": {
|
|
830
|
+
"type-check": "tsc --noEmit",
|
|
831
|
+
"test": "pnpm type-check && vitest run"
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
642
836
|
---
|
|
643
837
|
|
|
644
838
|
## Performance Testing
|
|
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
|
|
|
627
627
|
|
|
628
628
|
### 类型安全测试
|
|
629
629
|
|
|
630
|
+
TypeScript 项目的测试不仅要验证运行时行为,还应该确保类型系统的正确性。Vitest 提供了 `expectTypeOf` 工具来进行编译时类型检查。
|
|
631
|
+
|
|
632
|
+
#### 为什么需要类型测试?
|
|
633
|
+
|
|
634
|
+
1. **类型推断验证**:确保 TypeScript 能正确推断复杂类型
|
|
635
|
+
2. **泛型约束检查**:验证泛型参数的约束条件
|
|
636
|
+
3. **类型兼容性**:确保类型定义与实际使用匹配
|
|
637
|
+
4. **API 契约保证**:防止类型定义的破坏性变更
|
|
638
|
+
|
|
639
|
+
#### 基础类型测试
|
|
640
|
+
|
|
630
641
|
```typescript
|
|
642
|
+
import { describe, it, expectTypeOf } from 'vitest';
|
|
643
|
+
|
|
631
644
|
describe('TypeSafetyTests', () => {
|
|
632
645
|
it('should maintain type safety', () => {
|
|
633
646
|
const processor = new DataProcessor<User>();
|
|
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
|
|
|
636
649
|
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
650
|
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
651
|
});
|
|
652
|
+
|
|
653
|
+
it('should infer correct return types', () => {
|
|
654
|
+
const result = getData();
|
|
655
|
+
|
|
656
|
+
// 验证返回值类型
|
|
657
|
+
expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
|
|
658
|
+
expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should validate parameter types', () => {
|
|
662
|
+
function processUser(user: User): void {
|
|
663
|
+
// implementation
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 验证参数类型
|
|
667
|
+
expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
|
|
668
|
+
expectTypeOf(processUser).parameter(0).toHaveProperty('id');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### 泛型类型测试
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
describe('Generic Type Tests', () => {
|
|
677
|
+
it('should work with generic constraints', () => {
|
|
678
|
+
class Storage<T extends { id: number }> {
|
|
679
|
+
store(item: T): T {
|
|
680
|
+
return item;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const storage = new Storage<User>();
|
|
685
|
+
|
|
686
|
+
// 验证泛型类型
|
|
687
|
+
expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
|
|
688
|
+
expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should validate complex generic types', () => {
|
|
692
|
+
type ApiResponse<T> = {
|
|
693
|
+
data: T;
|
|
694
|
+
status: number;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const response: ApiResponse<User[]> = {
|
|
699
|
+
data: [],
|
|
700
|
+
status: 200
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// 验证嵌套泛型类型
|
|
704
|
+
expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
|
|
705
|
+
expectTypeOf(response.data).toEqualTypeOf<User[]>();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### 联合类型与交叉类型测试
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
describe('Union and Intersection Types', () => {
|
|
714
|
+
it('should handle union types correctly', () => {
|
|
715
|
+
type Result = Success | Error;
|
|
716
|
+
type Success = { status: 'success'; data: string };
|
|
717
|
+
type Error = { status: 'error'; message: string };
|
|
718
|
+
|
|
719
|
+
function handleResult(result: Result): void {
|
|
720
|
+
// implementation
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 验证联合类型
|
|
724
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
|
|
725
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle intersection types correctly', () => {
|
|
729
|
+
type Timestamped = { createdAt: Date; updatedAt: Date };
|
|
730
|
+
type UserWithTimestamp = User & Timestamped;
|
|
731
|
+
|
|
732
|
+
const user: UserWithTimestamp = {
|
|
733
|
+
id: 1,
|
|
734
|
+
name: 'John',
|
|
735
|
+
createdAt: new Date(),
|
|
736
|
+
updatedAt: new Date()
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// 验证交叉类型包含所有属性
|
|
740
|
+
expectTypeOf(user).toHaveProperty('id');
|
|
741
|
+
expectTypeOf(user).toHaveProperty('name');
|
|
742
|
+
expectTypeOf(user).toHaveProperty('createdAt');
|
|
743
|
+
expectTypeOf(user).toHaveProperty('updatedAt');
|
|
744
|
+
});
|
|
639
745
|
});
|
|
640
746
|
```
|
|
641
747
|
|
|
748
|
+
#### 类型窄化测试
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
describe('Type Narrowing Tests', () => {
|
|
752
|
+
it('should validate type guards', () => {
|
|
753
|
+
function isString(value: unknown): value is string {
|
|
754
|
+
return typeof value === 'string';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const value: unknown = 'test';
|
|
758
|
+
|
|
759
|
+
if (isString(value)) {
|
|
760
|
+
// 在此作用域内,value 应该被窄化为 string 类型
|
|
761
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should validate discriminated unions', () => {
|
|
766
|
+
type Shape =
|
|
767
|
+
| { kind: 'circle'; radius: number }
|
|
768
|
+
| { kind: 'rectangle'; width: number; height: number };
|
|
769
|
+
|
|
770
|
+
function getArea(shape: Shape): number {
|
|
771
|
+
if (shape.kind === 'circle') {
|
|
772
|
+
// 在此分支,shape 应该被窄化为 circle 类型
|
|
773
|
+
expectTypeOf(shape).toHaveProperty('radius');
|
|
774
|
+
expectTypeOf(shape).not.toHaveProperty('width');
|
|
775
|
+
return Math.PI * shape.radius ** 2;
|
|
776
|
+
} else {
|
|
777
|
+
// 在此分支,shape 应该被窄化为 rectangle 类型
|
|
778
|
+
expectTypeOf(shape).toHaveProperty('width');
|
|
779
|
+
expectTypeOf(shape).toHaveProperty('height');
|
|
780
|
+
return shape.width * shape.height;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### 实用建议
|
|
788
|
+
|
|
789
|
+
1. **结合运行时测试**:类型测试应该与运行时测试相辅相成
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('Combined Runtime and Type Tests', () => {
|
|
793
|
+
it('should validate both runtime behavior and types', () => {
|
|
794
|
+
function add(a: number, b: number): number {
|
|
795
|
+
return a + b;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 类型测试
|
|
799
|
+
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
|
|
800
|
+
expectTypeOf(add).returns.toEqualTypeOf<number>();
|
|
801
|
+
|
|
802
|
+
// 运行时测试
|
|
803
|
+
expect(add(1, 2)).toBe(3);
|
|
804
|
+
expect(add(-1, 1)).toBe(0);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
2. **测试类型推断**:确保 TypeScript 能正确推断类型,避免过度使用 `any`
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
describe('Type Inference Tests', () => {
|
|
813
|
+
it('should infer types correctly', () => {
|
|
814
|
+
const data = { id: 1, name: 'John' };
|
|
815
|
+
|
|
816
|
+
// 验证推断的类型
|
|
817
|
+
expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
|
|
818
|
+
expectTypeOf(data.id).toEqualTypeOf<number>();
|
|
819
|
+
expectTypeOf(data.name).toEqualTypeOf<string>();
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
3. **使用 TypeScript 编译器检查**:在 CI 中运行 `tsc --noEmit` 确保没有类型错误
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# 在 package.json 中添加脚本
|
|
828
|
+
{
|
|
829
|
+
"scripts": {
|
|
830
|
+
"type-check": "tsc --noEmit",
|
|
831
|
+
"test": "pnpm type-check && vitest run"
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
642
836
|
---
|
|
643
837
|
|
|
644
838
|
## 性能测试
|
|
@@ -92,17 +92,14 @@ const eslintConfig = [
|
|
|
92
92
|
'storybook-static/**'
|
|
93
93
|
]
|
|
94
94
|
},
|
|
95
|
+
|
|
95
96
|
{
|
|
96
97
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
97
98
|
languageOptions: {
|
|
98
99
|
parser: tseslint.parser,
|
|
99
100
|
parserOptions: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'./tsconfig.node.json',
|
|
103
|
-
'./tsconfig.test.json',
|
|
104
|
-
'./tsconfig.e2e.json'
|
|
105
|
-
],
|
|
101
|
+
// Note: 'project' is removed here to avoid conflict with projectService in the type-checked config below
|
|
102
|
+
// TypeScript files will get type information from the recommendedTypeChecked config
|
|
106
103
|
tsconfigRootDir: __dirname
|
|
107
104
|
}
|
|
108
105
|
},
|
|
@@ -116,7 +113,7 @@ const eslintConfig = [
|
|
|
116
113
|
rules: {
|
|
117
114
|
'@qlover-eslint/ts-class-method-return': 'error',
|
|
118
115
|
'@qlover-eslint/ts-class-member-accessibility': 'error',
|
|
119
|
-
'@qlover-eslint/ts-class-override': '
|
|
116
|
+
'@qlover-eslint/ts-class-override': 'off',
|
|
120
117
|
'@qlover-eslint/require-root-testid': [
|
|
121
118
|
'error',
|
|
122
119
|
{
|
|
@@ -202,6 +199,81 @@ const eslintConfig = [
|
|
|
202
199
|
'import/no-default-export': 'error'
|
|
203
200
|
}
|
|
204
201
|
},
|
|
202
|
+
|
|
203
|
+
// TypeScript files with type checking for ts-class-override rule
|
|
204
|
+
// The ts-class-override rule requires full type information to accurately detect:
|
|
205
|
+
// - Methods that override parent class methods (via extends)
|
|
206
|
+
// - Methods that implement interface methods (via implements)
|
|
207
|
+
// Without type checking, the rule falls back to AST-based heuristics which are less accurate
|
|
208
|
+
// This separate config block enables type checking only for TypeScript files to provide
|
|
209
|
+
// accurate override detection while maintaining good performance
|
|
210
|
+
...tseslint.configs.recommendedTypeChecked.map((config) => ({
|
|
211
|
+
...config,
|
|
212
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
213
|
+
ignores: [
|
|
214
|
+
'**/dist/**',
|
|
215
|
+
'**/build/**',
|
|
216
|
+
'**/ts-build/**',
|
|
217
|
+
'**/node_modules/**',
|
|
218
|
+
'**/.nx/**',
|
|
219
|
+
'**/.cache/**',
|
|
220
|
+
'**/coverage/**',
|
|
221
|
+
'**/*.d.ts',
|
|
222
|
+
'**/*.config.ts',
|
|
223
|
+
'**/*.test.ts',
|
|
224
|
+
'**/__mocks__/**',
|
|
225
|
+
'**/__tests__/**',
|
|
226
|
+
'**/*.spec.ts',
|
|
227
|
+
...(config.ignores || [])
|
|
228
|
+
],
|
|
229
|
+
languageOptions: {
|
|
230
|
+
...config.languageOptions,
|
|
231
|
+
parserOptions: {
|
|
232
|
+
...config.languageOptions?.parserOptions,
|
|
233
|
+
project: './tsconfig.app.json',
|
|
234
|
+
tsconfigRootDir: __dirname
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
plugins: {
|
|
238
|
+
...config.plugins,
|
|
239
|
+
'@qlover-eslint': qloverEslint
|
|
240
|
+
},
|
|
241
|
+
rules: {
|
|
242
|
+
...config.rules,
|
|
243
|
+
// Enable ts-class-override rule with full type information
|
|
244
|
+
// This rule is disabled in the base config above and only enabled here where
|
|
245
|
+
// type information is available, ensuring accurate detection of override relationships
|
|
246
|
+
'@qlover-eslint/ts-class-override': 'error',
|
|
247
|
+
// Disable other type-checked rules to avoid performance impact
|
|
248
|
+
// We only need type checking for ts-class-override, so we disable other
|
|
249
|
+
// type-aware rules that would slow down linting without providing value
|
|
250
|
+
'@typescript-eslint/ban-ts-comment': 'off',
|
|
251
|
+
'@typescript-eslint/restrict-template-expressions': 'off',
|
|
252
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
253
|
+
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
|
254
|
+
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
|
255
|
+
'@typescript-eslint/no-unsafe-return': 'off',
|
|
256
|
+
'@typescript-eslint/no-empty-object-type': 'off',
|
|
257
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
258
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
259
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
260
|
+
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
|
261
|
+
'@typescript-eslint/no-unsafe-literal-comparison': 'off',
|
|
262
|
+
'@typescript-eslint/no-unsafe-nullish-coalescing': 'off',
|
|
263
|
+
'@typescript-eslint/no-unsafe-optional-chaining': 'off',
|
|
264
|
+
'@typescript-eslint/unbound-method': 'off',
|
|
265
|
+
'@typescript-eslint/await-thenable': 'off',
|
|
266
|
+
'@typescript-eslint/no-floating-promises': 'off',
|
|
267
|
+
'@typescript-eslint/no-misused-promises': 'off',
|
|
268
|
+
'@typescript-eslint/require-await': 'off',
|
|
269
|
+
'@typescript-eslint/no-base-to-string': 'off',
|
|
270
|
+
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
|
271
|
+
'@typescript-eslint/no-duplicate-type-constituents': 'off',
|
|
272
|
+
// Disable @typescript-eslint/no-unused-vars as we use unused-imports/no-unused-vars instead
|
|
273
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
274
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
275
|
+
}
|
|
276
|
+
})),
|
|
205
277
|
{
|
|
206
278
|
// TODO: antd override theme need use import type
|
|
207
279
|
files: ['src/base/types/deprecated-antd.d.ts'],
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"build:staging": "npm run lint && vite build --mode staging",
|
|
37
37
|
"build:prod": "npm run lint && vite build --mode production",
|
|
38
38
|
"build:analyze": "vite build --mode production && start dist/stats.html",
|
|
39
|
-
"lint": "eslint
|
|
40
|
-
"lint:fix": "eslint
|
|
39
|
+
"lint": "eslint",
|
|
40
|
+
"lint:fix": "eslint --ext .ts,.tsx --fix",
|
|
41
41
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
|
42
42
|
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
|
43
43
|
"fix": "npm run lint:fix && npm run format",
|
|
@@ -59,6 +59,9 @@ export interface FeApiTransaction<
|
|
|
59
59
|
export class FeApiBootstarp implements BootstrapExecutorPlugin {
|
|
60
60
|
public readonly pluginName = 'FeApiBootstarp';
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* @override
|
|
64
|
+
*/
|
|
62
65
|
public onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
63
66
|
ioc
|
|
64
67
|
.get<FeApi>(FeApi)
|