Skip to content

Commit

Permalink
feat: add migration for sfc components (#420)
Browse files Browse the repository at this point in the history
* feat: add migration schematics to convert components to Single File Components

* fix(plugin): linting errors
  • Loading branch information
eneajaho authored Jul 15, 2024
1 parent da23dfc commit c2dcf5e
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
- [<strong>inject() Migration</strong> - migrate to inject() based DI](/utilities/migrations/inject-migration/)
- [<strong>viewChild(), contentChild(), viewChildren(), contentChildren() Migration</strong> - migrate to the new template queries()](/utilities/migrations/queries-migration/)
- [<strong>self-closing tag</strong> - migrate to the new self-closing tag](/utilities/migrations/self-closing-tags/)
- [<strong>SFC components</strong> - migrate to the new SFC components](/utilities/migrations/sfc-migration/)
</div>
</Card>

Expand Down
58 changes: 58 additions & 0 deletions docs/src/content/docs/utilities/Migrations/sfc-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Convert to SFC components migration
description: Schematics for converting Angular components to SFC components
entryPoint: convert-to-sfc
badge: stable
contributors: ['enea-jahollari']
---

Angular components can have inline templates or have a separate template file. The inline templates are called SFC (Single File Components) and are a common practice in modern Angular applications.
This schematic helps you convert your Angular components to SFC components.

### How it works?

The moment you run the schematics, it will look for all the components in your project and will convert them to SFC components.

- It will move the template from the `templateUrl` to the `template` property.
- It will move the styles from the `styleUrls` to the `styles` property.
- The maximum lines length for the template is set to 200 lines. If the template has more than 200 lines, it will be skipped.

In order to change the maximum line length, you can pass the `--max-inline-template-lines` param to the schematics. For styles, you can pass the `--max-inline-style-lines` param.

``bash

### Usage

In order to run the schematics for all the project in the app you have to run the following script:

```bash
ng g ngxtension:convert-to-sfc
```

If you want to specify the project name you can pass the `--project` param.

```bash
ng g ngxtension:convert-to-sfc --project=<project-name>
```

If you want to run the schematic for a specific component or directive you can pass the `--path` param.

```bash
ng g ngxtension:convert-to-sfc --path=<path-to-ts-file>
```

If you want to change the maximum line length for the template or styles you can pass the `--max-inline-template-lines` param or `--max-inline-style-lines` param.

```bash
ng g ngxtension:convert-to-sfc --max-inline-template-lines=100 --max-inline-style-lines=100
```

### Usage with Nx

To use the schematics on a Nx monorepo you just swap `ng` with `nx`

Example:

```bash
nx g ngxtension:convert-to-sfc --project=<project-name>
```
1 change: 1 addition & 0 deletions libs/ngxtension/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
{
"files": ["*.html"],
"excludedFiles": ["*inline-template-*.component.html"],
"extends": ["plugin:@nx/angular-template"],
"excludedFiles": ["*inline-template-*.component.html"],
"rules": {}
Expand Down
10 changes: 10 additions & 0 deletions libs/plugin/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"factory": "./src/generators/convert-to-self-closing-tag/generator",
"schema": "./src/generators/convert-to-self-closing-tag/schema.json",
"description": "libs/plugin/src/generators/convert-to-self-closing-tag/ generator"
},
"convert-to-sfc": {
"factory": "./src/generators/convert-to-sfc/generator",
"schema": "./src/generators/convert-to-sfc/schema.json",
"description": "libs/plugin/src/generators/convert-to-sfc/ generator"
}
},
"schematics": {
Expand Down Expand Up @@ -70,6 +75,11 @@
"factory": "./src/generators/convert-to-self-closing-tag/compat",
"schema": "./src/generators/convert-to-self-closing-tag/schema.json",
"description": "libs/plugin/src/generators/convert-to-self-closing-tag/ generator"
},
"convert-to-sfc": {
"factory": "./src/generators/convert-to-sfc/compat",
"schema": "./src/generators/convert-to-sfc/schema.json",
"description": "libs/plugin/src/generators/convert-to-sfc/ generator"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`convertToSFCGenerator should convert properly for templateUrl 1`] = `
"
import { Component, Input } from '@angular/core';
@Component({
template: \`
<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>
\`
})
export class MyCmp {
}
"
`;
exports[`convertToSFCGenerator should convert properly for templateUrl 2`] = `null`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrl 1`] = `
"
import { Component, Input } from '@angular/core';
@Component({
template: \`
<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>
\`,
styles: \`
h1 {
color: red;
}
\`
})
export class MyCmp {
}
"
`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrl 2`] = `null`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrl 3`] = `
"<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>"
`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrls 1`] = `
"
import { Component, Input } from '@angular/core';
@Component({
template: \`
<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>
\`,
styles: \`
h1 {
color: red;
}
\`
})
export class MyCmp {
}
"
`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrls 2`] = `null`;
exports[`convertToSFCGenerator should convert properly for templateUrl and styleUrls 3`] = `
"<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>"
`;
exports[`convertToSFCGenerator should skip components with inline templates 1`] = `
"
import { Component, Input } from '@angular/core';
@Component({
template: \`
<router-outlet></router-outlet>
\`
})
export class MyCmp {
}
"
`;
exports[`convertToSFCGenerator should skip components with inline templates 2`] = `undefined`;
4 changes: 4 additions & 0 deletions libs/plugin/src/generators/convert-to-sfc/compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { convertNxGenerator } from '@nx/devkit';
import convertGenerator from './generator';

export default convertNxGenerator(convertGenerator);
154 changes: 154 additions & 0 deletions libs/plugin/src/generators/convert-to-sfc/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

import convertToSFCGenerator from './generator';
import { ConvertToSFCGeneratorSchema } from './schema';

const template = `<div>Hello</div>
<app-my-cmp1>123</app-my-cmp1>`;

const styles = `h1 {
color: red;
}`;

const filesMap = {
notComponent: `
import { Injectable } from '@angular/core';
@Injectable()
export class MyService {}
`,
componentNoTemplate: `
import { Component } from '@angular/core';
@Component({})
export class MyCmp {}
`,

componentWithTemplateUrl: `
import { Component, Input } from '@angular/core';
@Component({
templateUrl: './my-file.html'
})
export class MyCmp {
}
`,
componentWithTemplateUrlAndStyleUrls: `
import { Component, Input } from '@angular/core';
@Component({
templateUrl: './my-file.html',
styleUrls: ['./my-file.css']
})
export class MyCmp {
}
`,
componentWithTemplateUrlAndStyleUrl: `
import { Component, Input } from '@angular/core';
@Component({
templateUrl: './my-file.html',
styleUrl: './my-file.css'
})
export class MyCmp {
}
`,
componentWithInlineTemplate: `
import { Component, Input } from '@angular/core';
@Component({
template: \`
<router-outlet></router-outlet>
\`
})
export class MyCmp {
}
`,
} as const;

describe('convertToSFCGenerator', () => {
let tree: Tree;
const options: ConvertToSFCGeneratorSchema = {
path: 'libs/my-file.ts',
moveStyles: true,
maxInlineTemplateLines: 10,
maxInlineStyleLines: 10,
};

function setup(file: keyof typeof filesMap) {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write('package.json', `{"dependencies": {"@angular/core": "17.1.0"}}`);
tree.write(`libs/my-file.ts`, filesMap[file]);
tree.write(`libs/my-file.css`, styles);

if (
file === 'componentWithTemplateUrl' ||
file === 'componentWithTemplateUrlAndStyleUrls' ||
file === 'componentWithTemplateUrlAndStyleUrl'
) {
tree.write(`libs/my-file.html`, template);
return () => {
return [
tree.read('libs/my-file.ts', 'utf8'),
filesMap[file],
tree.read('libs/my-file.html', 'utf8'),
template,
tree.read('libs/my-file.css', 'utf8'),
];
};
}

return () => {
return [tree.read('libs/my-file.ts', 'utf8'), filesMap[file]];
};
}

it('should not do anything if not component/directive', async () => {
const readContent = setup('notComponent');
await convertToSFCGenerator(tree, options);
const [updated, original] = readContent();
expect(updated).toEqual(original);
});

it('should not do anything if no template', async () => {
const readContent = setup('componentNoTemplate');
await convertToSFCGenerator(tree, options);
const [updated, original] = readContent();
expect(updated).toEqual(original);
});

it('should convert properly for templateUrl', async () => {
const readContent = setup('componentWithTemplateUrl');
await convertToSFCGenerator(tree, options);
const [updated, , updatedHtml] = readContent();
expect(updated).toMatchSnapshot();
expect(updatedHtml).toMatchSnapshot();
});

it('should convert properly for templateUrl and styleUrls', async () => {
const readContent = setup('componentWithTemplateUrlAndStyleUrls');
await convertToSFCGenerator(tree, options);
const [updated, , updatedHtml, updatedStyles] = readContent();
expect(updated).toMatchSnapshot();
expect(updatedHtml).toMatchSnapshot();
expect(updatedStyles).toMatchSnapshot();
});

it('should convert properly for templateUrl and styleUrl', async () => {
const readContent = setup('componentWithTemplateUrlAndStyleUrl');
await convertToSFCGenerator(tree, options);
const [updated, , updatedHtml, updatedStyles] = readContent();
expect(updated).toMatchSnapshot();
expect(updatedHtml).toMatchSnapshot();
expect(updatedStyles).toMatchSnapshot();
});

it('should skip components with inline templates', async () => {
const readContent = setup('componentWithInlineTemplate');
await convertToSFCGenerator(tree, options);
const [updated, , updatedHtml] = readContent();
expect(updated).toMatchSnapshot();
expect(updatedHtml).toMatchSnapshot();
});
});
Loading

0 comments on commit c2dcf5e

Please sign in to comment.