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 for sfc components #420

Merged
merged 2 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading