1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 11:43:51 +00:00
This commit is contained in:
Hinton
2025-07-22 14:12:52 +02:00
parent 299ca23225
commit 5ca1d78975
6 changed files with 869 additions and 1 deletions

1
.gitignore vendored
View File

@@ -54,3 +54,4 @@ apps/**/config/local.json
# Nx
.nx
strict-compliance-report.json

View File

@@ -1,6 +1,6 @@
# Implementation Plan
- [-] 1. Set up infrastructure and tooling for strict mode migration
- [x] 1. Set up infrastructure and tooling for strict mode migration
- Create utility scripts to identify files with @ts-strict-ignore comments
- Implement automated testing for strict mode compliance

View File

@@ -22,6 +22,12 @@
"test:watch": "jest --clearCache && jest --watch",
"test:watch:all": "jest --watchAll",
"test:types": "node ./scripts/test-types.js",
"test:strict": "node ./scripts/test-strict-compliance.js",
"test:strict:all": "node ./scripts/test-strict-compliance.js all",
"test:strict:plugin": "node ./scripts/test-strict-compliance.js plugin",
"strict:find": "node ./scripts/strict-mode-utils.js find",
"strict:progress": "node ./scripts/strict-mode-utils.js progress",
"strict:test": "node ./scripts/strict-mode-utils.js test",
"test:locales": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/test-locales.js",
"lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js",
"docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph",

View File

@@ -0,0 +1,223 @@
# TypeScript Strict Mode Migration Utilities
This directory contains utilities to help with the TypeScript strict mode migration process for the Bitwarden client codebase.
## Overview
The migration involves systematically enabling TypeScript strict mode across all applications and libraries. Currently, the codebase uses `typescript-strict-plugin` to allow gradual migration by excluding files with `@ts-strict-ignore` comments from strict checking.
## Utilities
### 1. Strict Mode Utils (`strict-mode-utils.js`)
A comprehensive utility for managing the strict mode migration process.
#### Commands
```bash
# Find all files with @ts-strict-ignore comments
node scripts/strict-mode-utils.js find
# Generate migration progress report
node scripts/strict-mode-utils.js progress
# Test strict mode compliance for entire codebase
node scripts/strict-mode-utils.js test
# Test strict mode compliance for specific project
node scripts/strict-mode-utils.js test libs/platform
# Check typescript-strict-plugin status
node scripts/strict-mode-utils.js plugin
```
#### NPM Scripts
```bash
# Find files with @ts-strict-ignore comments
npm run strict:find
# Generate progress report
npm run strict:progress
# Test strict mode compliance
npm run strict:test
```
### 2. Strict Compliance Tester (`test-strict-compliance.js`)
Automated testing for strict mode compliance across projects.
#### Commands
```bash
# Test all projects for strict mode compliance
node scripts/test-strict-compliance.js all
# Test projects matching a pattern
node scripts/test-strict-compliance.js pattern libs/platform
node scripts/test-strict-compliance.js pattern apps/
# Test current typescript-strict-plugin functionality
node scripts/test-strict-compliance.js plugin
```
#### NPM Scripts
```bash
# Test all projects
npm run test:strict:all
# Test typescript-strict-plugin
npm run test:strict:plugin
```
## Migration Process
### Current State
- **Total files with @ts-strict-ignore**: ~1,245 files
- **TypeScript strict plugin**: Active (version 2.4.4)
- **Base configuration**: `"strict": false` with plugin providing selective checking
### Migration Categories
The migration follows this order (as defined in the spec):
1. **Core Libraries** (`libs/platform`, `libs/common`)
2. **Domain Libraries** (`libs/auth`, `libs/vault`, etc.)
3. **UI Libraries** (`libs/components`, `libs/angular`)
4. **Applications** (`apps/cli`, `apps/browser`, `apps/desktop`, `apps/web`)
5. **Licensed Features** (`bitwarden_license/`)
### Per-Category Progress
| Category | Files with @ts-strict-ignore |
| ------------------ | ---------------------------- |
| libs/common | 381 files |
| apps/web | 239 files |
| apps/browser | 114 files |
| bitwarden_license | 115 files |
| other | 67 files |
| libs/components | 62 files |
| libs/tools | 54 files |
| libs/angular | 51 files |
| apps/cli | 45 files |
| apps/desktop | 39 files |
| libs/auth | 33 files |
| libs/vault | 28 files |
| libs/admin-console | 17 files |
## Usage Examples
### Finding Files to Migrate
```bash
# Get overview of migration status
npm run strict:progress
# Find all files that need migration
npm run strict:find
```
### Testing Compliance
```bash
# Test if a specific library is ready for strict mode
node scripts/test-strict-compliance.js pattern libs/storage
# Test all projects (warning: this will take time and likely show many failures)
npm run test:strict:all
```
### During Migration
1. **Before starting a library migration**:
```bash
# Check current status
node scripts/test-strict-compliance.js pattern libs/platform
```
2. **After fixing strict mode violations**:
```bash
# Test compliance
node scripts/test-strict-compliance.js pattern libs/platform
# Check progress
npm run strict:progress
```
3. **Final validation**:
```bash
# Test that typescript-strict-plugin still works
npm run test:strict:plugin
# Generate final report
npm run strict:progress
```
## Output Files
### `strict-compliance-report.json`
Generated by the compliance tester, contains:
- Timestamp of test run
- Summary statistics (total, passed, failed, pass rate)
- Detailed results for each project tested
- Error details for failed projects
## Integration with Existing Tools
### Existing Type Checking
The existing `npm run test:types` command runs:
- `npx tsc-strict` (typescript-strict-plugin)
- Type checking for all library tsconfig.json files
### New Strict Mode Testing
The new utilities complement existing tools by:
- Testing native TypeScript strict mode (without plugin)
- Providing detailed progress tracking
- Enabling targeted testing of specific projects
- Generating comprehensive reports
## Migration Workflow
1. **Assessment**: Use `strict:progress` to see current state
2. **Planning**: Use `strict:find` to identify files in target library
3. **Testing**: Use `test-strict-compliance.js pattern <library>` to test current compliance
4. **Implementation**: Fix strict mode violations in the library
5. **Validation**: Re-test with compliance tester
6. **Configuration**: Update library's tsconfig.json to enable strict mode
7. **Final Check**: Ensure all tests pass and plugin still works
## Notes
- The utilities create temporary tsconfig files for testing but clean them up automatically
- Failed tests include error output to help identify what needs to be fixed
- The progress report shows the current state without making any changes
- All utilities are safe to run and don't modify source code
## Troubleshooting
### Common Issues
1. **"Cannot find module" errors**: Ensure you're running from the project root
2. **Permission errors**: Make sure the scripts have execute permissions
3. **TypeScript compilation errors**: These are expected during migration and indicate what needs to be fixed
### Getting Help
Run any utility without arguments to see usage information:
```bash
node scripts/strict-mode-utils.js
node scripts/test-strict-compliance.js
```

294
scripts/strict-mode-utils.js Executable file
View File

@@ -0,0 +1,294 @@
#!/usr/bin/env node
/**
* Utility scripts for TypeScript strict mode migration
*
* This script provides utilities to:
* 1. Identify files with @ts-strict-ignore comments
* 2. Test strict mode compliance
* 3. Track migration progress
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
class StrictModeUtils {
constructor() {
this.rootDir = path.resolve(__dirname, "..");
this.tsStrictIgnorePattern = /@ts-strict-ignore/;
}
/**
* Recursively find all TypeScript files in a directory
*/
findTsFiles(dir, excludeDirs = ["node_modules", "dist", "coverage", ".git", ".angular"]) {
const results = [];
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (!excludeDirs.includes(file)) {
results.push(...this.findTsFiles(filePath, excludeDirs));
}
} else if (file.endsWith(".ts") && !file.endsWith(".d.ts")) {
results.push(filePath);
}
}
} catch (error) {
console.warn(`Warning: Could not read directory ${dir}: ${error.message}`);
}
return results;
}
/**
* Find all files with @ts-strict-ignore comments
*/
findFilesWithStrictIgnore() {
console.log("🔍 Scanning for files with @ts-strict-ignore comments...\n");
const tsFiles = this.findTsFiles(this.rootDir);
const filesWithIgnore = [];
for (const filePath of tsFiles) {
try {
const content = fs.readFileSync(filePath, "utf8");
if (this.tsStrictIgnorePattern.test(content)) {
const relativePath = path.relative(this.rootDir, filePath);
filesWithIgnore.push({
path: relativePath,
fullPath: filePath,
});
}
} catch (error) {
console.warn(`Warning: Could not read file ${filePath}: ${error.message}`);
}
}
return filesWithIgnore;
}
/**
* Generate a report of files with @ts-strict-ignore comments
*/
generateIgnoreReport() {
const filesWithIgnore = this.findFilesWithStrictIgnore();
console.log(`📊 Found ${filesWithIgnore.length} files with @ts-strict-ignore comments:\n`);
// Group by directory for better organization
const byDirectory = {};
filesWithIgnore.forEach((file) => {
const dir = path.dirname(file.path);
if (!byDirectory[dir]) {
byDirectory[dir] = [];
}
byDirectory[dir].push(file.path);
});
// Sort directories and display
const sortedDirs = Object.keys(byDirectory).sort();
for (const dir of sortedDirs) {
console.log(`📁 ${dir}/`);
byDirectory[dir].forEach((file) => {
const fileName = path.basename(file);
console.log(` - ${fileName}`);
});
console.log();
}
return filesWithIgnore;
}
/**
* Test strict mode compliance for a specific project
*/
testStrictCompliance(projectPath = null) {
console.log("🧪 Testing TypeScript strict mode compliance...\n");
try {
let command;
if (projectPath) {
const tsConfigPath = path.join(projectPath, "tsconfig.json");
if (!fs.existsSync(tsConfigPath)) {
throw new Error(`tsconfig.json not found at ${tsConfigPath}`);
}
command = `npx tsc --noEmit --strict --project ${tsConfigPath}`;
console.log(`Testing project: ${projectPath}`);
} else {
command = "npx tsc --noEmit --strict";
console.log("Testing entire codebase with strict mode...");
}
console.log(`Running: ${command}\n`);
const output = execSync(command, {
cwd: this.rootDir,
encoding: "utf8",
stdio: "pipe",
});
console.log("✅ Strict mode compliance test passed!");
if (output.trim()) {
console.log("Output:", output);
}
return { success: true, output };
} catch (error) {
console.log("❌ Strict mode compliance test failed!");
console.log("Error output:");
console.log(error.stdout || error.message);
return { success: false, error: error.stdout || error.message };
}
}
/**
* Check if typescript-strict-plugin is being used
*/
checkStrictPlugin() {
console.log("🔌 Checking typescript-strict-plugin usage...\n");
try {
// Check if plugin is in package.json
const packageJsonPath = path.join(this.rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const hasPlugin =
packageJson.devDependencies && packageJson.devDependencies["typescript-strict-plugin"];
// Check if plugin is configured in tsconfig
const tsConfigPath = path.join(this.rootDir, "tsconfig.base.json");
const tsConfig = JSON.parse(fs.readFileSync(tsConfigPath, "utf8"));
const hasPluginConfig =
tsConfig.compilerOptions &&
tsConfig.compilerOptions.plugins &&
tsConfig.compilerOptions.plugins.some((p) => p.name === "typescript-strict-plugin");
console.log(`Plugin in package.json: ${hasPlugin ? "✅" : "❌"}`);
console.log(`Plugin in tsconfig: ${hasPluginConfig ? "✅" : "❌"}`);
if (hasPlugin) {
console.log(`Plugin version: ${packageJson.devDependencies["typescript-strict-plugin"]}`);
}
return { hasPlugin, hasPluginConfig };
} catch (error) {
console.error("Error checking plugin status:", error.message);
return { hasPlugin: false, hasPluginConfig: false };
}
}
/**
* Generate migration progress report
*/
generateProgressReport() {
console.log("📈 Generating migration progress report...\n");
const filesWithIgnore = this.findFilesWithStrictIgnore();
const pluginStatus = this.checkStrictPlugin();
// Count files by category
const categories = {
"libs/platform": 0,
"libs/common": 0,
"libs/auth": 0,
"libs/vault": 0,
"libs/key-management": 0,
"libs/admin-console": 0,
"libs/billing": 0,
"libs/tools": 0,
"libs/components": 0,
"libs/angular": 0,
"apps/cli": 0,
"apps/browser": 0,
"apps/desktop": 0,
"apps/web": 0,
bitwarden_license: 0,
other: 0,
};
filesWithIgnore.forEach((file) => {
let categorized = false;
for (const category of Object.keys(categories)) {
if (file.path.startsWith(category)) {
categories[category]++;
categorized = true;
break;
}
}
if (!categorized) {
categories.other++;
}
});
console.log("Migration Progress by Category:");
console.log("================================");
for (const [category, count] of Object.entries(categories)) {
if (count > 0) {
console.log(`${category.padEnd(25)} ${count.toString().padStart(3)} files`);
}
}
console.log(`\nTotal files with @ts-strict-ignore: ${filesWithIgnore.length}`);
console.log(`TypeScript strict plugin active: ${pluginStatus.hasPlugin ? "Yes" : "No"}`);
return {
totalFiles: filesWithIgnore.length,
categories,
pluginActive: pluginStatus.hasPlugin,
};
}
}
// CLI interface
if (require.main === module) {
const utils = new StrictModeUtils();
const command = process.argv[2];
switch (command) {
case "find":
utils.generateIgnoreReport();
break;
case "test":
const projectPath = process.argv[3];
utils.testStrictCompliance(projectPath);
break;
case "progress":
utils.generateProgressReport();
break;
case "plugin":
utils.checkStrictPlugin();
break;
default:
console.log("TypeScript Strict Mode Migration Utilities");
console.log("==========================================");
console.log("");
console.log("Usage: node scripts/strict-mode-utils.js <command>");
console.log("");
console.log("Commands:");
console.log(" find - Find all files with @ts-strict-ignore comments");
console.log(" test - Test strict mode compliance (optionally for specific project)");
console.log(" progress - Generate migration progress report");
console.log(" plugin - Check typescript-strict-plugin status");
console.log("");
console.log("Examples:");
console.log(" node scripts/strict-mode-utils.js find");
console.log(" node scripts/strict-mode-utils.js test");
console.log(" node scripts/strict-mode-utils.js test libs/platform");
console.log(" node scripts/strict-mode-utils.js progress");
break;
}
}
module.exports = StrictModeUtils;

344
scripts/test-strict-compliance.js Executable file
View File

@@ -0,0 +1,344 @@
#!/usr/bin/env node
/**
* Automated testing script for TypeScript strict mode compliance
*
* This script tests strict mode compliance across different parts of the codebase
* and provides detailed reporting for the migration process.
*/
const fs = require("fs");
const path = require("path");
const { execSync, spawn } = require("child_process");
const StrictModeUtils = require("./strict-mode-utils");
class StrictComplianceTester {
constructor() {
this.rootDir = path.resolve(__dirname, "..");
this.utils = new StrictModeUtils();
this.results = {
passed: [],
failed: [],
skipped: [],
};
}
/**
* Get all TypeScript configuration files in the project
*/
getTsConfigFiles() {
const tsConfigs = [];
// Find all tsconfig.json files
const findTsConfigs = (
dir,
excludeDirs = ["node_modules", "dist", "coverage", ".git", ".angular"],
) => {
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (!excludeDirs.includes(file)) {
findTsConfigs(filePath, excludeDirs);
}
} else if (file === "tsconfig.json") {
const relativePath = path.relative(this.rootDir, filePath);
// Skip shared directory as mentioned in test-types.js
if (!relativePath.includes("libs/shared/")) {
tsConfigs.push({
path: relativePath,
fullPath: filePath,
directory: path.dirname(relativePath),
});
}
}
}
} catch (error) {
console.warn(`Warning: Could not read directory ${dir}: ${error.message}`);
}
};
findTsConfigs(this.rootDir);
return tsConfigs;
}
/**
* Test strict mode compliance for a specific TypeScript configuration
*/
async testProjectStrictCompliance(tsConfigInfo) {
const { path: configPath, directory } = tsConfigInfo;
console.log(`\n🧪 Testing ${directory}...`);
try {
// Create a temporary strict tsconfig for testing
const originalConfig = JSON.parse(fs.readFileSync(tsConfigInfo.fullPath, "utf8"));
const testConfig = {
...originalConfig,
compilerOptions: {
...originalConfig.compilerOptions,
strict: true,
// Enable all strict flags explicitly
noImplicitAny: true,
strictNullChecks: true,
strictFunctionTypes: true,
strictBindCallApply: true,
strictPropertyInitialization: true,
noImplicitReturns: true,
noImplicitThis: true,
},
};
// Remove typescript-strict-plugin if present
if (testConfig.compilerOptions.plugins) {
testConfig.compilerOptions.plugins = testConfig.compilerOptions.plugins.filter(
(plugin) => plugin.name !== "typescript-strict-plugin",
);
}
const tempConfigPath = path.join(
path.dirname(tsConfigInfo.fullPath),
"tsconfig.strict-test.json",
);
fs.writeFileSync(tempConfigPath, JSON.stringify(testConfig, null, 2));
try {
const command = `npx tsc --noEmit --project ${tempConfigPath}`;
const output = execSync(command, {
cwd: this.rootDir,
encoding: "utf8",
stdio: "pipe",
});
console.log(`${directory} - PASSED`);
this.results.passed.push({
project: directory,
configPath,
output: output.trim(),
});
return { success: true, project: directory, output };
} finally {
// Clean up temporary config file
if (fs.existsSync(tempConfigPath)) {
fs.unlinkSync(tempConfigPath);
}
}
} catch (error) {
console.log(`${directory} - FAILED`);
const errorOutput = error.stdout || error.stderr || error.message;
this.results.failed.push({
project: directory,
configPath,
error: errorOutput,
});
return { success: false, project: directory, error: errorOutput };
}
}
/**
* Test strict mode compliance for all projects
*/
async testAllProjects() {
console.log("🚀 Starting comprehensive strict mode compliance testing...\n");
const tsConfigs = this.getTsConfigFiles();
console.log(`Found ${tsConfigs.length} TypeScript configuration files to test.\n`);
// Test each project
for (const tsConfig of tsConfigs) {
await this.testProjectStrictCompliance(tsConfig);
}
this.generateTestReport();
return this.results;
}
/**
* Test specific projects by pattern
*/
async testProjectsByPattern(pattern) {
console.log(`🎯 Testing projects matching pattern: ${pattern}\n`);
const tsConfigs = this.getTsConfigFiles();
const matchingConfigs = tsConfigs.filter((config) => config.directory.includes(pattern));
if (matchingConfigs.length === 0) {
console.log(`No projects found matching pattern: ${pattern}`);
return this.results;
}
console.log(`Found ${matchingConfigs.length} matching projects:\n`);
matchingConfigs.forEach((config) => console.log(` - ${config.directory}`));
console.log();
for (const tsConfig of matchingConfigs) {
await this.testProjectStrictCompliance(tsConfig);
}
this.generateTestReport();
return this.results;
}
/**
* Generate a detailed test report
*/
generateTestReport() {
console.log("\n📊 STRICT MODE COMPLIANCE TEST REPORT");
console.log("=====================================\n");
const total =
this.results.passed.length + this.results.failed.length + this.results.skipped.length;
const passRate = total > 0 ? ((this.results.passed.length / total) * 100).toFixed(1) : 0;
console.log(`Total Projects Tested: ${total}`);
console.log(`Passed: ${this.results.passed.length} (${passRate}%)`);
console.log(`Failed: ${this.results.failed.length}`);
console.log(`Skipped: ${this.results.skipped.length}\n`);
if (this.results.passed.length > 0) {
console.log("✅ PASSED PROJECTS:");
console.log("-------------------");
this.results.passed.forEach((result) => {
console.log(` ${result.project}`);
});
console.log();
}
if (this.results.failed.length > 0) {
console.log("❌ FAILED PROJECTS:");
console.log("-------------------");
this.results.failed.forEach((result) => {
console.log(` ${result.project}`);
// Show first few lines of error for context
const errorLines = result.error.split("\n").slice(0, 3);
errorLines.forEach((line) => {
if (line.trim()) {
console.log(` ${line.trim()}`);
}
});
console.log();
});
}
// Save detailed report to file
const reportPath = path.join(this.rootDir, "strict-compliance-report.json");
fs.writeFileSync(
reportPath,
JSON.stringify(
{
timestamp: new Date().toISOString(),
summary: {
total,
passed: this.results.passed.length,
failed: this.results.failed.length,
skipped: this.results.skipped.length,
passRate: parseFloat(passRate),
},
results: this.results,
},
null,
2,
),
);
console.log(`📄 Detailed report saved to: ${reportPath}\n`);
if (this.results.failed.length === 0) {
console.log("🎉 All projects are strict mode compliant!");
} else {
console.log(
`⚠️ ${this.results.failed.length} projects need attention before enabling strict mode.`,
);
}
}
/**
* Test current typescript-strict-plugin functionality
*/
testStrictPlugin() {
console.log("🔌 Testing typescript-strict-plugin functionality...\n");
try {
const command = "npx tsc-strict";
console.log(`Running: ${command}`);
const output = execSync(command, {
cwd: this.rootDir,
encoding: "utf8",
stdio: "pipe",
});
console.log("✅ typescript-strict-plugin test passed!");
if (output.trim()) {
console.log("Output:", output);
}
return { success: true, output };
} catch (error) {
console.log("❌ typescript-strict-plugin test failed!");
console.log("Error:", error.stdout || error.message);
return { success: false, error: error.stdout || error.message };
}
}
}
// CLI interface
if (require.main === module) {
const tester = new StrictComplianceTester();
const command = process.argv[2];
const pattern = process.argv[3];
async function main() {
switch (command) {
case "all":
await tester.testAllProjects();
break;
case "pattern":
if (!pattern) {
console.error("Error: Pattern required for pattern command");
console.log("Usage: node scripts/test-strict-compliance.js pattern <pattern>");
process.exit(1);
}
await tester.testProjectsByPattern(pattern);
break;
case "plugin":
tester.testStrictPlugin();
break;
default:
console.log("TypeScript Strict Mode Compliance Tester");
console.log("========================================");
console.log("");
console.log("Usage: node scripts/test-strict-compliance.js <command> [options]");
console.log("");
console.log("Commands:");
console.log(" all - Test all projects for strict mode compliance");
console.log(" pattern - Test projects matching a specific pattern");
console.log(" plugin - Test current typescript-strict-plugin functionality");
console.log("");
console.log("Examples:");
console.log(" node scripts/test-strict-compliance.js all");
console.log(" node scripts/test-strict-compliance.js pattern libs/platform");
console.log(" node scripts/test-strict-compliance.js pattern apps/");
console.log(" node scripts/test-strict-compliance.js plugin");
break;
}
}
main().catch((error) => {
console.error("Error:", error);
process.exit(1);
});
}
module.exports = StrictComplianceTester;