Skip to content

Commit

Permalink
feat: add migration schematic for breaking changes
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlj95 committed Oct 29, 2024
1 parent f17560a commit 8421152
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 2 deletions.
7 changes: 6 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ module.exports = {

coverageDirectory: '<rootDir>/coverage/jest',
coverageReporters: ['json', 'text'],
collectCoverageFrom: ['<rootDir>/**/*.ts', '!**/testing/**'],
collectCoverageFrom: [
'<rootDir>/**/*.ts',
'!**/testing/**',
'!**/external-utils/**',
'!**/utils/**',
],
}
19 changes: 19 additions & 0 deletions projects/ngx-meta/schematics/external-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Why is this needed

May schematic utilities are deep imports, hence not part of [Angular's public API surface](https://github.com/angular/angular/blob/main/contributing-docs/public-api-surface.md)

> A deep import is an import deeper than one of the package's entry point. For instance `@angular/core` is a regular import. But `@angular/core/src/utils` is a deep import.
For instance:

- `@schematics/angular/utility`
- `@angular-devkit/core/src/utils`

So the files needed from there are copy / pasted in here to avoid coupling to non-public APIs which can be dangerous (for instance breaking changes)

Indeed, [some `@angular/core` schematic utils mysteriously disappeared in v15.1](https://stackoverflow.com/a/79123753/3263250)

Existing `npm` libraries with exported utils aren't very popular or maintained at the moment of writing this. For instance:

- [`schematics-utilities`](https://www.npmjs.com/package/schematics-utilities). Most popular one. Exports copy/pasted utils from Angular's schematics package and Angular Material package. [Latest release is from 2021 (3+ years ago)](https://github.com/nitayneeman/schematics-utilities/releases/tag/v2.0.3). [Depends on Angular v8 and Typescript v3](https://github.com/nitayneeman/schematics-utilities/blob/v2.0.3/package.json#L38-L41)
- [`@hug/ngx-schematics-utilities`](https://www.npmjs.com/package/@hug/ngx-schematics-utilities). Modern schematics with a builder-like pattern. It's updated: [latest release was last month](https://github.com/DSI-HUG/ngx-schematics-utilities/releases/tag/10.1.4). [Depends with peer dependencies (yay!) on Angular > v17](https://github.com/DSI-HUG/ngx-schematics-utilities/blob/10.1.4/projects/lib/package.json#L53-L58). Not very popular though. So prefer copy/pasting for now.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Partial extraction from
// https://github.com/angular/angular-cli/blob/18.2.10/packages/angular_devkit/core/src/utils/strings.ts
const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g

/**
Returns the lowerCamelCase form of a string.
```javascript
camelize('innerHTML'); // 'innerHTML'
camelize('action_name'); // 'actionName'
camelize('css-class-name'); // 'cssClassName'
camelize('my favorite items'); // 'myFavoriteItems'
camelize('My Favorite Items'); // 'myFavoriteItems'
```
@method camelize
@param {String} str The string to camelize.
@return {String} the camelized string.
*/
export function camelize(str: string): string {
return str
.replace(
STRING_CAMELIZE_REGEXP,
(_match: string, _separator: string, chr: string) => {
return chr ? chr.toUpperCase() : ''
},
)
.replace(/^([A-Z])/, (match: string) => match.toLowerCase())
}

/**
Returns the UpperCamelCase form of a string.
@example
```javascript
'innerHTML'.classify(); // 'InnerHTML'
'action_name'.classify(); // 'ActionName'
'css-class-name'.classify(); // 'CssClassName'
'my favorite items'.classify(); // 'MyFavoriteItems'
'app.component'.classify(); // 'AppComponent'
```
@method classify
@param {String} str the string to classify
@return {String} the classified string
*/
export function classify(str: string): string {
return str
.split('.')
.map((part) => capitalize(camelize(part)))
.join('')
}

/**
Returns the Capitalized form of a string
```javascript
'innerHTML'.capitalize() // 'InnerHTML'
'action_name'.capitalize() // 'Action_name'
'css-class-name'.capitalize() // 'Css-class-name'
'my favorite items'.capitalize() // 'My favorite items'
```
@method capitalize
@param {String} str The string to capitalize.
@return {String} The capitalized string.
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Partial extraction from
// https://github.com/angular/angular-cli/blob/18.2.10/packages/schematics/angular/utility/change.ts
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { UpdateRecorder } from '@angular-devkit/schematics'

export interface Change {
// The file this change should be applied to. Some changes might not apply to
// a file (maybe the config).
readonly path: string | null

// The order this change should be applied. Normally the position inside the file.
// Changes are applied from the bottom of a file to the top.
readonly order: number

// The description of this change. This will be outputted in a dry or verbose run.
readonly description: string
}

/**
* An operation that does nothing.
*/
export class NoopChange implements Change {
description = 'No operation.'
order = Infinity
path = null
}

/**
* Will add text to the source code.
*/
export class InsertChange implements Change {
order: number
description: string

constructor(
public path: string,
public pos: number,
public toAdd: string,
) {
if (pos < 0) {
throw new Error('Negative positions are invalid')
}
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`
this.order = pos
}
}

/**
* Will remove text from the source code.
*/
export class RemoveChange implements Change {
order: number
description: string

constructor(
public path: string,
pos: number,
public toRemove: string,
) {
if (pos < 0) {
throw new Error('Negative positions are invalid')
}
this.description = `Removed ${toRemove} into position ${pos} of ${path}`
this.order = pos
}
}

/**
* Will replace text from the source code.
*/
export class ReplaceChange implements Change {
order: number
description: string

constructor(
public path: string,
pos: number,
public oldText: string,
public newText: string,
) {
if (pos < 0) {
throw new Error('Negative positions are invalid')
}
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`
this.order = pos
}
}

export function applyToUpdateRecorder(
recorder: UpdateRecorder,
changes: Change[],
): void {
for (const change of changes) {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd)
} else if (change instanceof RemoveChange) {
recorder.remove(change.order, change.toRemove.length)
} else if (change instanceof ReplaceChange) {
recorder.remove(change.order, change.oldText.length)
recorder.insertLeft(change.order, change.newText)
} else if (!(change instanceof NoopChange)) {
throw new Error(
'Unknown Change type encountered when updating a recorder.',
)
}
}
}
10 changes: 10 additions & 0 deletions projects/ngx-meta/schematics/migrations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"tree-shakeable-manager-providers": {
"version": "1.0.0-beta.31",
"description": "Updates non tree-shakeable built-in metadata manager providers into tree-shakeable ones",
"factory": "./migrations/tree-shakeable-manager-providers#migrate"
}
}
}
Loading

0 comments on commit 8421152

Please sign in to comment.