mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
style: format all files with prettier
This commit is contained in:
@@ -20,18 +20,21 @@ Master AngularJS to Angular migration, including hybrid apps, component conversi
|
||||
## Migration Strategies
|
||||
|
||||
### 1. Big Bang (Complete Rewrite)
|
||||
|
||||
- Rewrite entire app in Angular
|
||||
- Parallel development
|
||||
- Switch over at once
|
||||
- **Best for:** Small apps, green field projects
|
||||
|
||||
### 2. Incremental (Hybrid Approach)
|
||||
|
||||
- Run AngularJS and Angular side-by-side
|
||||
- Migrate feature by feature
|
||||
- ngUpgrade for interop
|
||||
- **Best for:** Large apps, continuous delivery
|
||||
|
||||
### 3. Vertical Slice
|
||||
|
||||
- Migrate one feature completely
|
||||
- New features in Angular, maintain old in AngularJS
|
||||
- Gradually replace
|
||||
@@ -41,30 +44,27 @@ Master AngularJS to Angular migration, including hybrid apps, component conversi
|
||||
|
||||
```typescript
|
||||
// main.ts - Bootstrap hybrid app
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { UpgradeModule } from '@angular/upgrade/static';
|
||||
import { AppModule } from './app/app.module';
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { UpgradeModule } from "@angular/upgrade/static";
|
||||
import { AppModule } from "./app/app.module";
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.then(platformRef => {
|
||||
.then((platformRef) => {
|
||||
const upgrade = platformRef.injector.get(UpgradeModule);
|
||||
// Bootstrap AngularJS
|
||||
upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
|
||||
upgrade.bootstrap(document.body, ["myAngularJSApp"], { strictDi: true });
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app.module.ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { UpgradeModule } from '@angular/upgrade/static';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { UpgradeModule } from "@angular/upgrade/static";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
UpgradeModule
|
||||
]
|
||||
imports: [BrowserModule, UpgradeModule],
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(private upgrade: UpgradeModule) {}
|
||||
@@ -78,36 +78,39 @@ export class AppModule {
|
||||
## Component Migration
|
||||
|
||||
### AngularJS Controller → Angular Component
|
||||
|
||||
```javascript
|
||||
// Before: AngularJS controller
|
||||
angular.module('myApp').controller('UserController', function($scope, UserService) {
|
||||
$scope.user = {};
|
||||
angular
|
||||
.module("myApp")
|
||||
.controller("UserController", function ($scope, UserService) {
|
||||
$scope.user = {};
|
||||
|
||||
$scope.loadUser = function(id) {
|
||||
UserService.getUser(id).then(function(user) {
|
||||
$scope.user = user;
|
||||
});
|
||||
};
|
||||
$scope.loadUser = function (id) {
|
||||
UserService.getUser(id).then(function (user) {
|
||||
$scope.user = user;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveUser = function() {
|
||||
UserService.saveUser($scope.user);
|
||||
};
|
||||
});
|
||||
$scope.saveUser = function () {
|
||||
UserService.saveUser($scope.user);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// After: Angular component
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user',
|
||||
selector: "app-user",
|
||||
template: `
|
||||
<div>
|
||||
<h2>{{ user.name }}</h2>
|
||||
<button (click)="saveUser()">Save</button>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class UserComponent implements OnInit {
|
||||
user: any = {};
|
||||
@@ -119,7 +122,7 @@ export class UserComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadUser(id: number) {
|
||||
this.userService.getUser(id).subscribe(user => {
|
||||
this.userService.getUser(id).subscribe((user) => {
|
||||
this.user = user;
|
||||
});
|
||||
}
|
||||
@@ -131,37 +134,38 @@ export class UserComponent implements OnInit {
|
||||
```
|
||||
|
||||
### AngularJS Directive → Angular Component
|
||||
|
||||
```javascript
|
||||
// Before: AngularJS directive
|
||||
angular.module('myApp').directive('userCard', function() {
|
||||
angular.module("myApp").directive("userCard", function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
restrict: "E",
|
||||
scope: {
|
||||
user: '=',
|
||||
onDelete: '&'
|
||||
user: "=",
|
||||
onDelete: "&",
|
||||
},
|
||||
template: `
|
||||
<div class="card">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<button ng-click="onDelete()">Delete</button>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// After: Angular component
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-card',
|
||||
selector: "app-user-card",
|
||||
template: `
|
||||
<div class="card">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<button (click)="delete.emit()">Delete</button>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class UserCardComponent {
|
||||
@Input() user: any;
|
||||
@@ -175,26 +179,26 @@ export class UserCardComponent {
|
||||
|
||||
```javascript
|
||||
// Before: AngularJS service
|
||||
angular.module('myApp').factory('UserService', function($http) {
|
||||
angular.module("myApp").factory("UserService", function ($http) {
|
||||
return {
|
||||
getUser: function(id) {
|
||||
return $http.get('/api/users/' + id);
|
||||
getUser: function (id) {
|
||||
return $http.get("/api/users/" + id);
|
||||
},
|
||||
saveUser: function (user) {
|
||||
return $http.post("/api/users", user);
|
||||
},
|
||||
saveUser: function(user) {
|
||||
return $http.post('/api/users', user);
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// After: Angular service
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class UserService {
|
||||
constructor(private http: HttpClient) {}
|
||||
@@ -204,7 +208,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
saveUser(user: any): Observable<any> {
|
||||
return this.http.post('/api/users', user);
|
||||
return this.http.post("/api/users", user);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -212,30 +216,31 @@ export class UserService {
|
||||
## Dependency Injection Changes
|
||||
|
||||
### Downgrading Angular → AngularJS
|
||||
|
||||
```typescript
|
||||
// Angular service
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class NewService {
|
||||
getData() {
|
||||
return 'data from Angular';
|
||||
return "data from Angular";
|
||||
}
|
||||
}
|
||||
|
||||
// Make available to AngularJS
|
||||
import { downgradeInjectable } from '@angular/upgrade/static';
|
||||
import { downgradeInjectable } from "@angular/upgrade/static";
|
||||
|
||||
angular.module('myApp')
|
||||
.factory('newService', downgradeInjectable(NewService));
|
||||
angular.module("myApp").factory("newService", downgradeInjectable(NewService));
|
||||
|
||||
// Use in AngularJS
|
||||
angular.module('myApp').controller('OldController', function(newService) {
|
||||
angular.module("myApp").controller("OldController", function (newService) {
|
||||
console.log(newService.getData());
|
||||
});
|
||||
```
|
||||
|
||||
### Upgrading AngularJS → Angular
|
||||
|
||||
```typescript
|
||||
// AngularJS service
|
||||
angular.module('myApp').factory('oldService', function() {
|
||||
@@ -274,30 +279,30 @@ export class NewComponent {
|
||||
|
||||
```javascript
|
||||
// Before: AngularJS routing
|
||||
angular.module('myApp').config(function($routeProvider) {
|
||||
angular.module("myApp").config(function ($routeProvider) {
|
||||
$routeProvider
|
||||
.when('/users', {
|
||||
template: '<user-list></user-list>'
|
||||
.when("/users", {
|
||||
template: "<user-list></user-list>",
|
||||
})
|
||||
.when('/users/:id', {
|
||||
template: '<user-detail></user-detail>'
|
||||
.when("/users/:id", {
|
||||
template: "<user-detail></user-detail>",
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// After: Angular routing
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'users', component: UserListComponent },
|
||||
{ path: 'users/:id', component: UserDetailComponent }
|
||||
{ path: "users", component: UserListComponent },
|
||||
{ path: "users/:id", component: UserDetailComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
```
|
||||
@@ -307,8 +312,8 @@ export class AppRoutingModule {}
|
||||
```html
|
||||
<!-- Before: AngularJS -->
|
||||
<form name="userForm" ng-submit="saveUser()">
|
||||
<input type="text" ng-model="user.name" required>
|
||||
<input type="email" ng-model="user.email" required>
|
||||
<input type="text" ng-model="user.name" required />
|
||||
<input type="email" ng-model="user.email" required />
|
||||
<button ng-disabled="userForm.$invalid">Save</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
@@ -20,29 +20,30 @@ Master database schema and data migrations across ORMs (Sequelize, TypeORM, Pris
|
||||
## ORM Migrations
|
||||
|
||||
### Sequelize Migrations
|
||||
|
||||
```javascript
|
||||
// migrations/20231201-create-users.js
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('users', {
|
||||
await queryInterface.createTable("users", {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: Sequelize.DATE,
|
||||
updatedAt: Sequelize.DATE
|
||||
updatedAt: Sequelize.DATE,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('users');
|
||||
}
|
||||
await queryInterface.dropTable("users");
|
||||
},
|
||||
};
|
||||
|
||||
// Run: npx sequelize-cli db:migrate
|
||||
@@ -50,40 +51,41 @@ module.exports = {
|
||||
```
|
||||
|
||||
### TypeORM Migrations
|
||||
|
||||
```typescript
|
||||
// migrations/1701234567-CreateUsers.ts
|
||||
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||
import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||
|
||||
export class CreateUsers1701234567 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'users',
|
||||
name: "users",
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'int',
|
||||
name: "id",
|
||||
type: "int",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: 'increment'
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'varchar',
|
||||
isUnique: true
|
||||
name: "email",
|
||||
type: "varchar",
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'CURRENT_TIMESTAMP'
|
||||
}
|
||||
]
|
||||
})
|
||||
name: "created_at",
|
||||
type: "timestamp",
|
||||
default: "CURRENT_TIMESTAMP",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('users');
|
||||
await queryRunner.dropTable("users");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +94,7 @@ export class CreateUsers1701234567 implements MigrationInterface {
|
||||
```
|
||||
|
||||
### Prisma Migrations
|
||||
|
||||
```prisma
|
||||
// schema.prisma
|
||||
model User {
|
||||
@@ -107,41 +110,41 @@ model User {
|
||||
## Schema Transformations
|
||||
|
||||
### Adding Columns with Defaults
|
||||
|
||||
```javascript
|
||||
// Safe migration: add column with default
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'status', {
|
||||
await queryInterface.addColumn("users", "status", {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: 'active',
|
||||
allowNull: false
|
||||
defaultValue: "active",
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('users', 'status');
|
||||
}
|
||||
await queryInterface.removeColumn("users", "status");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Renaming Columns (Zero Downtime)
|
||||
|
||||
```javascript
|
||||
// Step 1: Add new column
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'full_name', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.addColumn("users", "full_name", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
|
||||
// Copy data from old column
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE users SET full_name = name'
|
||||
);
|
||||
await queryInterface.sequelize.query("UPDATE users SET full_name = name");
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('users', 'full_name');
|
||||
}
|
||||
await queryInterface.removeColumn("users", "full_name");
|
||||
},
|
||||
};
|
||||
|
||||
// Step 2: Update application to use new column
|
||||
@@ -149,26 +152,27 @@ module.exports = {
|
||||
// Step 3: Remove old column
|
||||
module.exports = {
|
||||
up: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('users', 'name');
|
||||
await queryInterface.removeColumn("users", "name");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'name', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.addColumn("users", "name", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Changing Column Types
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// For large tables, use multi-step approach
|
||||
|
||||
// 1. Add new column
|
||||
await queryInterface.addColumn('users', 'age_new', {
|
||||
type: Sequelize.INTEGER
|
||||
await queryInterface.addColumn("users", "age_new", {
|
||||
type: Sequelize.INTEGER,
|
||||
});
|
||||
|
||||
// 2. Copy and transform data
|
||||
@@ -179,34 +183,35 @@ module.exports = {
|
||||
`);
|
||||
|
||||
// 3. Drop old column
|
||||
await queryInterface.removeColumn('users', 'age');
|
||||
await queryInterface.removeColumn("users", "age");
|
||||
|
||||
// 4. Rename new column
|
||||
await queryInterface.renameColumn('users', 'age_new', 'age');
|
||||
await queryInterface.renameColumn("users", "age_new", "age");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'age', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.changeColumn("users", "age", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Data Transformations
|
||||
|
||||
### Complex Data Migration
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Get all records
|
||||
const [users] = await queryInterface.sequelize.query(
|
||||
'SELECT id, address_string FROM users'
|
||||
"SELECT id, address_string FROM users",
|
||||
);
|
||||
|
||||
// Transform each record
|
||||
for (const user of users) {
|
||||
const addressParts = user.address_string.split(',');
|
||||
const addressParts = user.address_string.split(",");
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE users
|
||||
@@ -219,20 +224,20 @@ module.exports = {
|
||||
id: user.id,
|
||||
street: addressParts[0]?.trim(),
|
||||
city: addressParts[1]?.trim(),
|
||||
state: addressParts[2]?.trim()
|
||||
}
|
||||
}
|
||||
state: addressParts[2]?.trim(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Drop old column
|
||||
await queryInterface.removeColumn('users', 'address_string');
|
||||
await queryInterface.removeColumn("users", "address_string");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Reconstruct original column
|
||||
await queryInterface.addColumn('users', 'address_string', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.addColumn("users", "address_string", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
@@ -240,16 +245,17 @@ module.exports = {
|
||||
SET address_string = CONCAT(street, ', ', city, ', ', state)
|
||||
`);
|
||||
|
||||
await queryInterface.removeColumn('users', 'street');
|
||||
await queryInterface.removeColumn('users', 'city');
|
||||
await queryInterface.removeColumn('users', 'state');
|
||||
}
|
||||
await queryInterface.removeColumn("users", "street");
|
||||
await queryInterface.removeColumn("users", "city");
|
||||
await queryInterface.removeColumn("users", "state");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Rollback Strategies
|
||||
|
||||
### Transaction-Based Migrations
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
@@ -257,15 +263,15 @@ module.exports = {
|
||||
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'users',
|
||||
'verified',
|
||||
"users",
|
||||
"verified",
|
||||
{ type: Sequelize.BOOLEAN, defaultValue: false },
|
||||
{ transaction }
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL',
|
||||
{ transaction }
|
||||
"UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL",
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
@@ -276,62 +282,64 @@ module.exports = {
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('users', 'verified');
|
||||
}
|
||||
await queryInterface.removeColumn("users", "verified");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Checkpoint-Based Rollback
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Create backup table
|
||||
await queryInterface.sequelize.query(
|
||||
'CREATE TABLE users_backup AS SELECT * FROM users'
|
||||
"CREATE TABLE users_backup AS SELECT * FROM users",
|
||||
);
|
||||
|
||||
try {
|
||||
// Perform migration
|
||||
await queryInterface.addColumn('users', 'new_field', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.addColumn("users", "new_field", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
|
||||
// Verify migration
|
||||
const [result] = await queryInterface.sequelize.query(
|
||||
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL"
|
||||
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL",
|
||||
);
|
||||
|
||||
if (result[0].count > 0) {
|
||||
throw new Error('Migration verification failed');
|
||||
throw new Error("Migration verification failed");
|
||||
}
|
||||
|
||||
// Drop backup
|
||||
await queryInterface.dropTable('users_backup');
|
||||
await queryInterface.dropTable("users_backup");
|
||||
} catch (error) {
|
||||
// Restore from backup
|
||||
await queryInterface.sequelize.query('DROP TABLE users');
|
||||
await queryInterface.sequelize.query("DROP TABLE users");
|
||||
await queryInterface.sequelize.query(
|
||||
'CREATE TABLE users AS SELECT * FROM users_backup'
|
||||
"CREATE TABLE users AS SELECT * FROM users_backup",
|
||||
);
|
||||
await queryInterface.dropTable('users_backup');
|
||||
await queryInterface.dropTable("users_backup");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Zero-Downtime Migrations
|
||||
|
||||
### Blue-Green Deployment Strategy
|
||||
|
||||
```javascript
|
||||
// Phase 1: Make changes backward compatible
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Add new column (both old and new code can work)
|
||||
await queryInterface.addColumn('users', 'email_new', {
|
||||
type: Sequelize.STRING
|
||||
await queryInterface.addColumn("users", "email_new", {
|
||||
type: Sequelize.STRING,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Phase 2: Deploy code that writes to both columns
|
||||
@@ -344,7 +352,7 @@ module.exports = {
|
||||
SET email_new = email
|
||||
WHERE email_new IS NULL
|
||||
`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Phase 4: Deploy code that reads from new column
|
||||
@@ -352,44 +360,45 @@ module.exports = {
|
||||
// Phase 5: Remove old column
|
||||
module.exports = {
|
||||
up: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('users', 'email');
|
||||
}
|
||||
await queryInterface.removeColumn("users", "email");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Cross-Database Migrations
|
||||
|
||||
### PostgreSQL to MySQL
|
||||
|
||||
```javascript
|
||||
// Handle differences
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const dialectName = queryInterface.sequelize.getDialect();
|
||||
|
||||
if (dialectName === 'mysql') {
|
||||
await queryInterface.createTable('users', {
|
||||
if (dialectName === "mysql") {
|
||||
await queryInterface.createTable("users", {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
data: {
|
||||
type: Sequelize.JSON // MySQL JSON type
|
||||
}
|
||||
type: Sequelize.JSON, // MySQL JSON type
|
||||
},
|
||||
});
|
||||
} else if (dialectName === 'postgres') {
|
||||
await queryInterface.createTable('users', {
|
||||
} else if (dialectName === "postgres") {
|
||||
await queryInterface.createTable("users", {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
data: {
|
||||
type: Sequelize.JSONB // PostgreSQL JSONB type
|
||||
}
|
||||
type: Sequelize.JSONB, // PostgreSQL JSONB type
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ PATCH: Bug fixes, backward compatible
|
||||
## Dependency Analysis
|
||||
|
||||
### Audit Dependencies
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm outdated
|
||||
@@ -50,6 +51,7 @@ npx npm-check-updates -u # Update package.json
|
||||
```
|
||||
|
||||
### Analyze Dependency Tree
|
||||
|
||||
```bash
|
||||
# See why a package is installed
|
||||
npm ls package-name
|
||||
@@ -68,23 +70,23 @@ npx madge --image graph.png src/
|
||||
```javascript
|
||||
// compatibility-matrix.js
|
||||
const compatibilityMatrix = {
|
||||
'react': {
|
||||
'16.x': {
|
||||
'react-dom': '^16.0.0',
|
||||
'react-router-dom': '^5.0.0',
|
||||
'@testing-library/react': '^11.0.0'
|
||||
react: {
|
||||
"16.x": {
|
||||
"react-dom": "^16.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"@testing-library/react": "^11.0.0",
|
||||
},
|
||||
'17.x': {
|
||||
'react-dom': '^17.0.0',
|
||||
'react-router-dom': '^5.0.0 || ^6.0.0',
|
||||
'@testing-library/react': '^12.0.0'
|
||||
"17.x": {
|
||||
"react-dom": "^17.0.0",
|
||||
"react-router-dom": "^5.0.0 || ^6.0.0",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
},
|
||||
'18.x': {
|
||||
'react-dom': '^18.0.0',
|
||||
'react-router-dom': '^6.0.0',
|
||||
'@testing-library/react': '^13.0.0'
|
||||
}
|
||||
}
|
||||
"18.x": {
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.0.0",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function checkCompatibility(packages) {
|
||||
@@ -95,6 +97,7 @@ function checkCompatibility(packages) {
|
||||
## Staged Upgrade Strategy
|
||||
|
||||
### Phase 1: Planning
|
||||
|
||||
```bash
|
||||
# 1. Identify current versions
|
||||
npm list --depth=0
|
||||
@@ -112,6 +115,7 @@ echo "Upgrade order:
|
||||
```
|
||||
|
||||
### Phase 2: Incremental Updates
|
||||
|
||||
```bash
|
||||
# Don't upgrade everything at once!
|
||||
|
||||
@@ -135,17 +139,18 @@ npm install react-router-dom@6
|
||||
```
|
||||
|
||||
### Phase 3: Validation
|
||||
|
||||
```javascript
|
||||
// tests/compatibility.test.js
|
||||
describe('Dependency Compatibility', () => {
|
||||
it('should have compatible React versions', () => {
|
||||
const reactVersion = require('react/package.json').version;
|
||||
const reactDomVersion = require('react-dom/package.json').version;
|
||||
describe("Dependency Compatibility", () => {
|
||||
it("should have compatible React versions", () => {
|
||||
const reactVersion = require("react/package.json").version;
|
||||
const reactDomVersion = require("react-dom/package.json").version;
|
||||
|
||||
expect(reactVersion).toBe(reactDomVersion);
|
||||
});
|
||||
|
||||
it('should not have peer dependency warnings', () => {
|
||||
it("should not have peer dependency warnings", () => {
|
||||
// Run npm ls and check for warnings
|
||||
});
|
||||
});
|
||||
@@ -154,6 +159,7 @@ describe('Dependency Compatibility', () => {
|
||||
## Breaking Change Handling
|
||||
|
||||
### Identifying Breaking Changes
|
||||
|
||||
```bash
|
||||
# Use changelog parsers
|
||||
npx changelog-parser react 16.0.0 17.0.0
|
||||
@@ -163,6 +169,7 @@ curl https://raw.githubusercontent.com/facebook/react/main/CHANGELOG.md
|
||||
```
|
||||
|
||||
### Codemod for Automated Fixes
|
||||
|
||||
```bash
|
||||
# React upgrade codemods
|
||||
npx react-codeshift <transform> <path>
|
||||
@@ -175,25 +182,26 @@ npx react-codeshift \
|
||||
```
|
||||
|
||||
### Custom Migration Script
|
||||
|
||||
```javascript
|
||||
// migration-script.js
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const fs = require("fs");
|
||||
const glob = require("glob");
|
||||
|
||||
glob('src/**/*.tsx', (err, files) => {
|
||||
files.forEach(file => {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
glob("src/**/*.tsx", (err, files) => {
|
||||
files.forEach((file) => {
|
||||
let content = fs.readFileSync(file, "utf8");
|
||||
|
||||
// Replace old API with new API
|
||||
content = content.replace(
|
||||
/componentWillMount/g,
|
||||
'UNSAFE_componentWillMount'
|
||||
"UNSAFE_componentWillMount",
|
||||
);
|
||||
|
||||
// Update imports
|
||||
content = content.replace(
|
||||
/import { Component } from 'react'/g,
|
||||
"import React, { Component } from 'react'"
|
||||
"import React, { Component } from 'react'",
|
||||
);
|
||||
|
||||
fs.writeFileSync(file, content);
|
||||
@@ -204,6 +212,7 @@ glob('src/**/*.tsx', (err, files) => {
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```javascript
|
||||
// Ensure tests pass before and after upgrade
|
||||
npm run test
|
||||
@@ -213,26 +222,28 @@ npm install @testing-library/react@latest
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```javascript
|
||||
// tests/integration/app.test.js
|
||||
describe('App Integration', () => {
|
||||
it('should render without crashing', () => {
|
||||
describe("App Integration", () => {
|
||||
it("should render without crashing", () => {
|
||||
render(<App />);
|
||||
});
|
||||
|
||||
it('should handle navigation', () => {
|
||||
it("should handle navigation", () => {
|
||||
const { getByText } = render(<App />);
|
||||
fireEvent.click(getByText('Navigate'));
|
||||
expect(screen.getByText('New Page')).toBeInTheDocument();
|
||||
fireEvent.click(getByText("Navigate"));
|
||||
expect(screen.getByText("New Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Visual Regression Tests
|
||||
|
||||
```javascript
|
||||
// visual-regression.test.js
|
||||
describe('Visual Regression', () => {
|
||||
it('should match snapshot', () => {
|
||||
describe("Visual Regression", () => {
|
||||
it("should match snapshot", () => {
|
||||
const { container } = render(<App />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
@@ -240,15 +251,16 @@ describe('Visual Regression', () => {
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
```javascript
|
||||
// cypress/e2e/app.cy.js
|
||||
describe('E2E Tests', () => {
|
||||
it('should complete user flow', () => {
|
||||
cy.visit('/');
|
||||
describe("E2E Tests", () => {
|
||||
it("should complete user flow", () => {
|
||||
cy.visit("/");
|
||||
cy.get('[data-testid="login"]').click();
|
||||
cy.get('input[name="email"]').type('user@example.com');
|
||||
cy.get('input[name="email"]').type("user@example.com");
|
||||
cy.get('button[type="submit"]').click();
|
||||
cy.url().should('include', '/dashboard');
|
||||
cy.url().should("include", "/dashboard");
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -256,6 +268,7 @@ describe('E2E Tests', () => {
|
||||
## Automated Dependency Updates
|
||||
|
||||
### Renovate Configuration
|
||||
|
||||
```json
|
||||
// renovate.json
|
||||
{
|
||||
@@ -277,6 +290,7 @@ describe('E2E Tests', () => {
|
||||
```
|
||||
|
||||
### Dependabot Configuration
|
||||
|
||||
```yaml
|
||||
# .github/dependabot.yml
|
||||
version: 2
|
||||
@@ -322,6 +336,7 @@ fi
|
||||
## Common Upgrade Patterns
|
||||
|
||||
### Lock File Management
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install --package-lock-only # Update lock file only
|
||||
@@ -333,6 +348,7 @@ yarn upgrade-interactive # Interactive upgrades
|
||||
```
|
||||
|
||||
### Peer Dependency Resolution
|
||||
|
||||
```bash
|
||||
# npm 7+: strict peer dependencies
|
||||
npm install --legacy-peer-deps # Ignore peer deps
|
||||
@@ -342,6 +358,7 @@ npm install --force
|
||||
```
|
||||
|
||||
### Workspace Upgrades
|
||||
|
||||
```bash
|
||||
# Update all workspace packages
|
||||
npm install --workspaces
|
||||
@@ -375,6 +392,7 @@ npm install package@latest --workspace=packages/app
|
||||
|
||||
```markdown
|
||||
Pre-Upgrade:
|
||||
|
||||
- [ ] Review current dependency versions
|
||||
- [ ] Read changelogs for breaking changes
|
||||
- [ ] Create feature branch
|
||||
@@ -382,6 +400,7 @@ Pre-Upgrade:
|
||||
- [ ] Run full test suite (baseline)
|
||||
|
||||
During Upgrade:
|
||||
|
||||
- [ ] Upgrade one dependency at a time
|
||||
- [ ] Update peer dependencies
|
||||
- [ ] Fix TypeScript errors
|
||||
@@ -390,6 +409,7 @@ During Upgrade:
|
||||
- [ ] Check bundle size impact
|
||||
|
||||
Post-Upgrade:
|
||||
|
||||
- [ ] Full regression testing
|
||||
- [ ] Performance testing
|
||||
- [ ] Update documentation
|
||||
|
||||
@@ -24,12 +24,14 @@ Master React version upgrades, class to hooks migration, concurrent features ado
|
||||
**Breaking Changes by Version:**
|
||||
|
||||
**React 17:**
|
||||
|
||||
- Event delegation changes
|
||||
- No event pooling
|
||||
- Effect cleanup timing
|
||||
- JSX transform (no React import needed)
|
||||
|
||||
**React 18:**
|
||||
|
||||
- Automatic batching
|
||||
- Concurrent rendering
|
||||
- Strict Mode changes (double invocation)
|
||||
@@ -39,6 +41,7 @@ Master React version upgrades, class to hooks migration, concurrent features ado
|
||||
## Class to Hooks Migration
|
||||
|
||||
### State Management
|
||||
|
||||
```javascript
|
||||
// Before: Class component
|
||||
class Counter extends React.Component {
|
||||
@@ -46,13 +49,13 @@ class Counter extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
count: 0,
|
||||
name: ''
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
|
||||
increment = () => {
|
||||
this.setState({ count: this.state.count + 1 });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
@@ -67,7 +70,7 @@ class Counter extends React.Component {
|
||||
// After: Functional component with hooks
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const [name, setName] = useState('');
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const increment = () => {
|
||||
setCount(count + 1);
|
||||
@@ -83,6 +86,7 @@ function Counter() {
|
||||
```
|
||||
|
||||
### Lifecycle Methods to Hooks
|
||||
|
||||
```javascript
|
||||
// Before: Lifecycle methods
|
||||
class DataFetcher extends React.Component {
|
||||
@@ -155,6 +159,7 @@ function DataFetcher({ id }) {
|
||||
```
|
||||
|
||||
### Context and HOCs to Hooks
|
||||
|
||||
```javascript
|
||||
// Before: Context consumer and HOC
|
||||
const ThemeContext = React.createContext();
|
||||
@@ -175,11 +180,7 @@ class ThemedButton extends React.Component {
|
||||
function ThemedButton({ children }) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<button style={{ background: theme }}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
return <button style={{ background: theme }}>{children}</button>;
|
||||
}
|
||||
|
||||
// Before: HOC for data fetching
|
||||
@@ -188,7 +189,7 @@ function withUser(Component) {
|
||||
state = { user: null };
|
||||
|
||||
componentDidMount() {
|
||||
fetchUser().then(user => this.setState({ user }));
|
||||
fetchUser().then((user) => this.setState({ user }));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -218,52 +219,55 @@ function UserProfile() {
|
||||
## React 18 Concurrent Features
|
||||
|
||||
### New Root API
|
||||
|
||||
```javascript
|
||||
// Before: React 17
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
|
||||
// After: React 18
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
const root = createRoot(document.getElementById("root"));
|
||||
root.render(<App />);
|
||||
```
|
||||
|
||||
### Automatic Batching
|
||||
|
||||
```javascript
|
||||
// React 18: All updates are batched
|
||||
function handleClick() {
|
||||
setCount(c => c + 1);
|
||||
setFlag(f => !f);
|
||||
setCount((c) => c + 1);
|
||||
setFlag((f) => !f);
|
||||
// Only one re-render (batched)
|
||||
}
|
||||
|
||||
// Even in async:
|
||||
setTimeout(() => {
|
||||
setCount(c => c + 1);
|
||||
setFlag(f => !f);
|
||||
setCount((c) => c + 1);
|
||||
setFlag((f) => !f);
|
||||
// Still batched in React 18!
|
||||
}, 1000);
|
||||
|
||||
// Opt out if needed
|
||||
import { flushSync } from 'react-dom';
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
flushSync(() => {
|
||||
setCount(c => c + 1);
|
||||
setCount((c) => c + 1);
|
||||
});
|
||||
// Re-render happens here
|
||||
setFlag(f => !f);
|
||||
setFlag((f) => !f);
|
||||
// Another re-render
|
||||
```
|
||||
|
||||
### Transitions
|
||||
|
||||
```javascript
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@@ -288,8 +292,9 @@ function SearchResults() {
|
||||
```
|
||||
|
||||
### Suspense for Data Fetching
|
||||
|
||||
```javascript
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense } from "react";
|
||||
|
||||
// Resource-based data fetching (with React 18)
|
||||
const resource = fetchProfileData();
|
||||
@@ -320,6 +325,7 @@ function ProfileTimeline() {
|
||||
## Codemods for Automation
|
||||
|
||||
### Run React Codemods
|
||||
|
||||
```bash
|
||||
# Install jscodeshift
|
||||
npm install -g jscodeshift
|
||||
@@ -342,22 +348,25 @@ npx codemod react/hooks/convert-class-to-function src/
|
||||
```
|
||||
|
||||
### Custom Codemod Example
|
||||
|
||||
```javascript
|
||||
// custom-codemod.js
|
||||
module.exports = function(file, api) {
|
||||
module.exports = function (file, api) {
|
||||
const j = api.jscodeshift;
|
||||
const root = j(file.source);
|
||||
|
||||
// Find setState calls
|
||||
root.find(j.CallExpression, {
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
property: { name: 'setState' }
|
||||
}
|
||||
}).forEach(path => {
|
||||
// Transform to useState
|
||||
// ... transformation logic
|
||||
});
|
||||
root
|
||||
.find(j.CallExpression, {
|
||||
callee: {
|
||||
type: "MemberExpression",
|
||||
property: { name: "setState" },
|
||||
},
|
||||
})
|
||||
.forEach((path) => {
|
||||
// Transform to useState
|
||||
// ... transformation logic
|
||||
});
|
||||
|
||||
return root.toSource();
|
||||
};
|
||||
@@ -368,38 +377,38 @@ module.exports = function(file, api) {
|
||||
## Performance Optimization
|
||||
|
||||
### useMemo and useCallback
|
||||
|
||||
```javascript
|
||||
function ExpensiveComponent({ items, filter }) {
|
||||
// Memoize expensive calculation
|
||||
const filteredItems = useMemo(() => {
|
||||
return items.filter(item => item.category === filter);
|
||||
return items.filter((item) => item.category === filter);
|
||||
}, [items, filter]);
|
||||
|
||||
// Memoize callback to prevent child re-renders
|
||||
const handleClick = useCallback((id) => {
|
||||
console.log('Clicked:', id);
|
||||
console.log("Clicked:", id);
|
||||
}, []); // No dependencies, never changes
|
||||
|
||||
return (
|
||||
<List items={filteredItems} onClick={handleClick} />
|
||||
);
|
||||
return <List items={filteredItems} onClick={handleClick} />;
|
||||
}
|
||||
|
||||
// Child component with memo
|
||||
const List = React.memo(({ items, onClick }) => {
|
||||
return items.map(item => (
|
||||
return items.map((item) => (
|
||||
<Item key={item.id} item={item} onClick={onClick} />
|
||||
));
|
||||
});
|
||||
```
|
||||
|
||||
### Code Splitting
|
||||
|
||||
```javascript
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
// Lazy load components
|
||||
const Dashboard = lazy(() => import('./Dashboard'));
|
||||
const Settings = lazy(() => import('./Settings'));
|
||||
const Dashboard = lazy(() => import("./Dashboard"));
|
||||
const Settings = lazy(() => import("./Settings"));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -446,12 +455,14 @@ function List<T>({ items, renderItem }: ListProps<T>) {
|
||||
|
||||
```markdown
|
||||
### Pre-Migration
|
||||
|
||||
- [ ] Update dependencies incrementally (not all at once)
|
||||
- [ ] Review breaking changes in release notes
|
||||
- [ ] Set up testing suite
|
||||
- [ ] Create feature branch
|
||||
|
||||
### Class → Hooks Migration
|
||||
|
||||
- [ ] Identify class components to migrate
|
||||
- [ ] Start with leaf components (no children)
|
||||
- [ ] Convert state to useState
|
||||
@@ -461,6 +472,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
|
||||
- [ ] Test thoroughly
|
||||
|
||||
### React 18 Upgrade
|
||||
|
||||
- [ ] Update to React 17 first (if needed)
|
||||
- [ ] Update react and react-dom to 18
|
||||
- [ ] Update @types/react if using TypeScript
|
||||
@@ -470,6 +482,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
|
||||
- [ ] Adopt Suspense/Transitions where beneficial
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Identify performance bottlenecks
|
||||
- [ ] Add React.memo where appropriate
|
||||
- [ ] Use useMemo/useCallback for expensive operations
|
||||
@@ -477,6 +490,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
|
||||
- [ ] Optimize re-renders
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Update test utilities (React Testing Library)
|
||||
- [ ] Test with React 18 features
|
||||
- [ ] Check for warnings in console
|
||||
|
||||
Reference in New Issue
Block a user