Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add migration schematic for function-based providers #1005

Merged
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ module.exports = {

coverageDirectory: '<rootDir>/coverage/jest',
coverageReporters: ['json', 'text'],
collectCoverageFrom: ['<rootDir>/**/*.ts', '!**/testing/**'],
collectCoverageFrom: [
'<rootDir>/**/*.ts',
'!**/testing/**',
'!**/external-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": {
"const-to-function-manager-providers": {
"version": "1.0.0-beta.35",
"description": "Changes `const`-based metadata manager providers by `function`-based ones for tree shaking and extensibility purposes. See https://ngx-meta.dev/migrations/const-to-function-manager-providers/",
"factory": "./migrations/const-to-function-manager-providers/index#migrate"
}
}
}
Loading