@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 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.log("ACA CLI installed successfully!");
4
+ console.log("To use the generator, run: nx g @devmed555/angular-clean-architecture-cli:clean-feature");
@@ -0,0 +1,9 @@
1
+ {
2
+ "generators": {
3
+ "clean-feature": {
4
+ "factory": "./src/generators/clean-feature/generator",
5
+ "schema": "./src/generators/clean-feature/schema.json",
6
+ "description": "clean-feature generator"
7
+ }
8
+ }
9
+ }
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,6 @@
1
+ import { signalStore, withState } from '@ngrx/signals';
2
+
3
+ export const <%= pascalName %>Store = signalStore(
4
+ { providedIn: 'root' },
5
+ withState({ loading: false })
6
+ );
@@ -0,0 +1,4 @@
1
+ export interface <%= pascalName %> {
2
+ id: string;
3
+ <% attributes.forEach(function(attr) { %> <%= attr.name %>: <%= attr.type %>;
4
+ <% }); %>}
@@ -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,4 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { CleanFeatureGeneratorSchema } from './schema';
3
+ export declare function cleanFeatureGenerator(tree: Tree, options: CleanFeatureGeneratorSchema): Promise<void>;
4
+ export default cleanFeatureGenerator;
@@ -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,4 @@
1
+ export interface CleanFeatureGeneratorSchema {
2
+ name: string;
3
+ attributes?: string;
4
+ }
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../apps/cli/src/index.ts"],"names":[],"mappings":""}