@devmed555/angular-clean-architecture-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/bin/index.js +4 -0
- package/generators.json +9 -0
- package/package.json +39 -0
- package/src/generators/clean-feature/files/application/store.ts.template +6 -0
- package/src/generators/clean-feature/files/domain/model.ts.template +4 -0
- package/src/generators/clean-feature/files/infrastructure/service.ts.template +31 -0
- package/src/generators/clean-feature/files/ui/component.ts.template +44 -0
- package/src/generators/clean-feature/generator.d.ts +4 -0
- package/src/generators/clean-feature/generator.js +52 -0
- package/src/generators/clean-feature/generator.js.map +1 -0
- package/src/generators/clean-feature/schema.d.ts +4 -0
- package/src/generators/clean-feature/schema.json +23 -0
- package/src/index.d.ts +0 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# CLI Documentation
|
|
2
|
+
|
|
3
|
+
The CLI is an Nx generator designed to scaffold clean architecture features within the `sandbox` application.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
To generate a new feature, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run generate:feature -- --name=<feature-name>
|
|
11
|
+
|
|
12
|
+
# Examples:
|
|
13
|
+
npm run generate:feature -- --name=products
|
|
14
|
+
npm run generate:feature -- --name=user-profile
|
|
15
|
+
npm run generate:feature -- --name=shopping-cart
|
|
16
|
+
|
|
17
|
+
# Interactive mode with prompts:
|
|
18
|
+
npm run generate:feature:interactive
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Arguments
|
|
22
|
+
|
|
23
|
+
| Argument | Description | Required |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `--name` | The name of the feature to generate (kebab-case recommended). | Yes |
|
|
26
|
+
|
|
27
|
+
### Generated Structure
|
|
28
|
+
|
|
29
|
+
The generator will create the following structure in `apps/sandbox/src/app/features/<name>`:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
<feature-name>/
|
|
33
|
+
├── domain/
|
|
34
|
+
│ └── model.ts # Business entities (PascalCase interfaces)
|
|
35
|
+
├── infrastructure/
|
|
36
|
+
│ └── service.ts # HTTP services with CRUD operations
|
|
37
|
+
├── application/
|
|
38
|
+
│ └── store.ts # NgRx SignalStore for state management
|
|
39
|
+
└── ui/
|
|
40
|
+
└── component.ts # Standalone Angular component with store injection
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Naming Conventions
|
|
44
|
+
|
|
45
|
+
The generator automatically handles naming:
|
|
46
|
+
- **Input**: kebab-case (e.g., `user-profile`)
|
|
47
|
+
- **Classes/Interfaces**: PascalCase (e.g., `UserProfileComponent`, `UserProfile`)
|
|
48
|
+
- **Stores**: PascalCase with "Store" suffix (e.g., `UserProfileStore`)
|
|
49
|
+
- **Services**: PascalCase with "Service" suffix (e.g., `UserProfileService`)
|
|
50
|
+
|
|
51
|
+
### Example Generated Code
|
|
52
|
+
|
|
53
|
+
For `npm run generate:feature -- --name=product`:
|
|
54
|
+
|
|
55
|
+
**Domain Model** (`domain/model.ts`):
|
|
56
|
+
```typescript
|
|
57
|
+
export interface Product {
|
|
58
|
+
id: string;
|
|
59
|
+
createdAt: Date;
|
|
60
|
+
updatedAt: Date;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Infrastructure Service** (`infrastructure/service.ts`):
|
|
65
|
+
```typescript
|
|
66
|
+
@Injectable({ providedIn: 'root' })
|
|
67
|
+
export class ProductService {
|
|
68
|
+
private readonly apiUrl = '/api/products';
|
|
69
|
+
|
|
70
|
+
getAll(): Observable<Product[]> { ... }
|
|
71
|
+
getById(id: string): Observable<Product> { ... }
|
|
72
|
+
create(data: Omit<Product, 'id' | 'createdAt' | 'updatedAt'>): Observable<Product> { ... }
|
|
73
|
+
update(id: string, data: Partial<Product>): Observable<Product> { ... }
|
|
74
|
+
delete(id: string): Observable<void> { ... }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Application Store** (`application/store.ts`):
|
|
79
|
+
```typescript
|
|
80
|
+
export const ProductStore = signalStore(
|
|
81
|
+
{ providedIn: 'root' },
|
|
82
|
+
withState({ loading: false })
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**UI Component** (`ui/component.ts`):
|
|
87
|
+
```typescript
|
|
88
|
+
@Component({
|
|
89
|
+
selector: 'app-product-feature',
|
|
90
|
+
standalone: true,
|
|
91
|
+
imports: [CommonModule],
|
|
92
|
+
template: `
|
|
93
|
+
<div class="product-feature">
|
|
94
|
+
<h1>Product Feature</h1>
|
|
95
|
+
@if (store.loading()) {
|
|
96
|
+
<p>Loading...</p>
|
|
97
|
+
} @else {
|
|
98
|
+
<p>Ready to build your product feature!</p>
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
`,
|
|
102
|
+
})
|
|
103
|
+
export class ProductComponent {
|
|
104
|
+
protected readonly store = inject(ProductStore);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Template Structure
|
|
109
|
+
|
|
110
|
+
Templates are located in `src/generators/clean-feature/files/`:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
files/
|
|
114
|
+
├── application/
|
|
115
|
+
│ └── store.ts.template # SignalStore template
|
|
116
|
+
├── domain/
|
|
117
|
+
│ └── model.ts.template # Interface template
|
|
118
|
+
├── infrastructure/
|
|
119
|
+
│ └── service.ts.template # Service template with CRUD
|
|
120
|
+
└── ui/
|
|
121
|
+
└── component.ts.template # Component template with store
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Templates use EJS syntax:
|
|
125
|
+
- `<%= name %>` - Original kebab-case feature name
|
|
126
|
+
- `<%= pascalName %>` - PascalCase version of the name
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
### Build the CLI
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run cli:build
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Test the CLI
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm run cli:test
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Lint the CLI
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run cli:lint
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Development Workflow
|
|
149
|
+
|
|
150
|
+
1. **Modify templates** in `src/generators/clean-feature/files/`
|
|
151
|
+
2. **Update generator logic** in `generator.ts` if needed
|
|
152
|
+
3. **Build**: `npm run cli:build`
|
|
153
|
+
4. **Test**: Generate a feature with `npm run generate:feature -- --name=test`
|
|
154
|
+
5. **Verify**: Check generated code in `apps/sandbox/src/app/features/test`
|
|
155
|
+
6. **Clean up**: Delete test feature after verification
|
|
156
|
+
|
|
157
|
+
### Adding New Template Variables
|
|
158
|
+
|
|
159
|
+
To add new template variables:
|
|
160
|
+
|
|
161
|
+
1. Update `generator.ts`:
|
|
162
|
+
```typescript
|
|
163
|
+
generateFiles(tree, ..., targetPath, {
|
|
164
|
+
...options,
|
|
165
|
+
name,
|
|
166
|
+
pascalName,
|
|
167
|
+
yourNewVariable: computeValue(name), // Add here
|
|
168
|
+
tmpl: '',
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
2. Use in templates:
|
|
173
|
+
```typescript
|
|
174
|
+
// In any .template file
|
|
175
|
+
export class <%= yourNewVariable %>Something { }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Tips
|
|
179
|
+
|
|
180
|
+
- **Feature naming**: Use kebab-case for multi-word features (e.g., `user-profile`, not `userProfile`)
|
|
181
|
+
- **Testing**: Always test generated code in the sandbox app
|
|
182
|
+
- **Customization**: Modify templates to match your team's conventions
|
|
183
|
+
- **Validation**: Consider adding name validation in `generator.ts` to prevent duplicates
|
package/bin/index.js
ADDED
package/generators.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devmed555/angular-clean-architecture-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI generator for Angular Clean Architecture features using NgRx SignalStore",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"clean-architecture",
|
|
8
|
+
"ngrx",
|
|
9
|
+
"signal-store",
|
|
10
|
+
"cli",
|
|
11
|
+
"generator",
|
|
12
|
+
"schematics"
|
|
13
|
+
],
|
|
14
|
+
"author": "Mohamed Bouattour <mohamedbouattour123@gmail.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/MohamedBouattour/angular-clean-architecture"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"aca": "./bin/index.js"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@nx/devkit": "20.0.4",
|
|
28
|
+
"@nx/js": "20.0.4",
|
|
29
|
+
"@nx/angular": "20.0.4",
|
|
30
|
+
"tslib": "^2.3.0",
|
|
31
|
+
"inquirer": "^9.2.12"
|
|
32
|
+
},
|
|
33
|
+
"type": "commonjs",
|
|
34
|
+
"main": "./src/index.js",
|
|
35
|
+
"typings": "./src/index.d.ts",
|
|
36
|
+
"private": false,
|
|
37
|
+
"generators": "./generators.json",
|
|
38
|
+
"types": "./src/index.d.ts"
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import { <%= pascalName %> } from '../domain/model';
|
|
5
|
+
|
|
6
|
+
@Injectable({ providedIn: 'root' })
|
|
7
|
+
export class <%= pascalName %>Service {
|
|
8
|
+
private readonly apiUrl = '/api/<%= name %>s';
|
|
9
|
+
|
|
10
|
+
constructor(private http: HttpClient) {}
|
|
11
|
+
|
|
12
|
+
getAll(): Observable<<%= pascalName %>[]> {
|
|
13
|
+
return this.http.get<<%= pascalName %>[]>(this.apiUrl);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getById(id: string): Observable<<%= pascalName %>> {
|
|
17
|
+
return this.http.get<<%= pascalName %>>(`${this.apiUrl}/${id}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
create(data: Omit<<%= pascalName %>, 'id'>): Observable<<%= pascalName %>> {
|
|
21
|
+
return this.http.post<<%= pascalName %>>(this.apiUrl, data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
update(id: string, data: Partial<<%= pascalName %>>): Observable<<%= pascalName %>> {
|
|
25
|
+
return this.http.put<<%= pascalName %>>(`${this.apiUrl}/${id}`, data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
delete(id: string): Observable<void> {
|
|
29
|
+
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component, inject } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { <%= pascalName %>Store } from '../application/store';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-<%= name %>-feature',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
template: `
|
|
10
|
+
<div class="<%= name %>-feature">
|
|
11
|
+
<h1><%= pascalName %> Feature</h1>
|
|
12
|
+
|
|
13
|
+
@if (store.loading()) {
|
|
14
|
+
<p>Loading...</p>
|
|
15
|
+
} @else {
|
|
16
|
+
<p>Ready to build your <%= name %> feature!</p>
|
|
17
|
+
|
|
18
|
+
<!-- Example list of <%= name %> items -->
|
|
19
|
+
<ul>
|
|
20
|
+
@for (item of []; track item.id) {
|
|
21
|
+
<li>
|
|
22
|
+
<% attributes.forEach(function(attr) { %>
|
|
23
|
+
<strong><%= attr.name %>:</strong> {{ item.<%= attr.name %> }}<br/>
|
|
24
|
+
<% }); %>
|
|
25
|
+
</li>
|
|
26
|
+
}
|
|
27
|
+
</ul>
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
styles: `
|
|
32
|
+
.<%= name %>-feature {
|
|
33
|
+
padding: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
h1 {
|
|
37
|
+
color: #333;
|
|
38
|
+
margin-bottom: 1rem;
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
})
|
|
42
|
+
export class <%= pascalName %>Component {
|
|
43
|
+
protected readonly store = inject(<%= pascalName %>Store);
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanFeatureGenerator = cleanFeatureGenerator;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
/**
|
|
7
|
+
* Capitalizes the first letter of a string
|
|
8
|
+
*/
|
|
9
|
+
function capitalizeFirst(str) {
|
|
10
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Converts a kebab-case or camelCase string to PascalCase
|
|
14
|
+
*/
|
|
15
|
+
function toPascalCase(str) {
|
|
16
|
+
return str
|
|
17
|
+
.split('-')
|
|
18
|
+
.map(part => capitalizeFirst(part))
|
|
19
|
+
.join('');
|
|
20
|
+
}
|
|
21
|
+
function cleanFeatureGenerator(tree, options) {
|
|
22
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const name = options.name;
|
|
24
|
+
const targetPath = (0, devkit_1.joinPathFragments)('apps/sandbox/src/app/features', name);
|
|
25
|
+
// Format names for templates
|
|
26
|
+
const pascalName = toPascalCase(name);
|
|
27
|
+
// Parse attributes
|
|
28
|
+
let attributes = [];
|
|
29
|
+
if (options.attributes) {
|
|
30
|
+
attributes = options.attributes.split(',').map(attr => {
|
|
31
|
+
const [name, type] = attr.split(':');
|
|
32
|
+
return { name, type: type || 'string' };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// If running in interactive mode (and no attributes provided), we could prompt
|
|
37
|
+
// But since this runs in Nx context, standard inquirer might conflict if not handled carefully
|
|
38
|
+
// For now we'll support the --attributes flag passed from the interactive CLI wrapper
|
|
39
|
+
attributes = [
|
|
40
|
+
{ name: 'createdAt', type: 'Date' },
|
|
41
|
+
{ name: 'updatedAt', type: 'Date' }
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
// Generate files from templates
|
|
45
|
+
(0, devkit_1.generateFiles)(tree, (0, devkit_1.joinPathFragments)(__dirname, 'files'), targetPath, Object.assign(Object.assign({}, options), { name,
|
|
46
|
+
pascalName,
|
|
47
|
+
attributes, tmpl: '' }));
|
|
48
|
+
yield (0, devkit_1.formatFiles)(tree);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
exports.default = cleanFeatureGenerator;
|
|
52
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../../apps/cli/src/generators/clean-feature/generator.ts"],"names":[],"mappings":";;AAyBA,sDA2CC;;AApED,uCAKoB;AAGpB;;GAEG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;SAClC,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,SAAsB,qBAAqB,CACzC,IAAU,EACV,OAAoC;;QAEpC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,MAAM,UAAU,GAAG,IAAA,0BAAiB,EAAC,+BAA+B,EAAE,IAAI,CAAC,CAAC;QAE5E,6BAA6B;QAC7B,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAEtC,mBAAmB;QACnB,IAAI,UAAU,GAAqC,EAAE,CAAC;QAEtD,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;gBACpD,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACrC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC1C,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,+EAA+E;YAC/E,+FAA+F;YAC/F,sFAAsF;YACtF,UAAU,GAAG;gBACX,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE;gBACnC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE;aACpC,CAAC;QACJ,CAAC;QAED,gCAAgC;QAChC,IAAA,sBAAa,EACX,IAAI,EACJ,IAAA,0BAAiB,EAAC,SAAS,EAAE,OAAO,CAAC,EACrC,UAAU,kCAEL,OAAO,KACV,IAAI;YACJ,UAAU;YACV,UAAU,EACV,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA;AAED,kBAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "CleanFeature",
|
|
4
|
+
"title": "",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The name of the feature to generate",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "argv",
|
|
12
|
+
"index": 0
|
|
13
|
+
},
|
|
14
|
+
"x-prompt": "What name would you like to use?"
|
|
15
|
+
},
|
|
16
|
+
"attributes": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Comma-separated list of attributes (e.g., name:string,age:number)",
|
|
19
|
+
"x-prompt": "Enter attributes (format: name:string,age:number) or leave empty:"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["name"]
|
|
23
|
+
}
|
package/src/index.d.ts
ADDED
|
File without changes
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../apps/cli/src/index.ts"],"names":[],"mappings":""}
|