@ngstato/schematics 0.4.0

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,96 @@
1
+ <div align="center">
2
+
3
+ # @ngstato/schematics
4
+
5
+ ### `ng generate @ngstato/schematics:store users` — done.
6
+
7
+ **Scaffold stores, features, and tests in seconds.**
8
+
9
+ [![npm](https://img.shields.io/badge/npm-v0.3.0-blue)](https://www.npmjs.com/package/@ngstato/schematics)
10
+ [![Angular CLI](https://img.shields.io/badge/Angular_CLI-17%2B-dd0031)](https://angular.dev)
11
+ [![license](https://img.shields.io/badge/license-MIT-lightgrey)](#)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -D @ngstato/schematics
21
+ ```
22
+
23
+ ## Generate a store
24
+
25
+ ```bash
26
+ ng generate @ngstato/schematics:store users
27
+ # or shorthand
28
+ ng g @ngstato/schematics:s users
29
+ ```
30
+
31
+ Creates:
32
+ ```
33
+ src/app/stores/users/
34
+ ├── users.store.ts # Store with CRUD actions, selectors, hooks, DevTools
35
+ └── users.store.spec.ts # Test file with createMockStore
36
+ ```
37
+
38
+ ### Options
39
+
40
+ | Option | Default | Description |
41
+ |--------|---------|-------------|
42
+ | `--name` | (required) | Store name (e.g. `users`, `products`) |
43
+ | `--crud` | `true` | Generate CRUD actions (load, create, update, delete) |
44
+ | `--entity` | `false` | Use `createEntityAdapter` for normalized collections |
45
+ | `--devtools` | `true` | Connect to DevTools with `connectDevTools` |
46
+ | `--spec` | `true` | Generate test file |
47
+ | `--flat` | `false` | Create file directly in path (no subdirectory) |
48
+ | `--path` | `src/app/stores` | Target directory |
49
+
50
+ ### Examples
51
+
52
+ ```bash
53
+ # Basic CRUD store
54
+ ng g @ngstato/schematics:s products
55
+
56
+ # Entity-based store with adapter
57
+ ng g @ngstato/schematics:s orders --entity
58
+
59
+ # Flat file, no tests
60
+ ng g @ngstato/schematics:s settings --flat --no-spec
61
+
62
+ # Custom path
63
+ ng g @ngstato/schematics:s auth --path src/app/core/auth
64
+ ```
65
+
66
+ ## Generate a feature
67
+
68
+ ```bash
69
+ ng generate @ngstato/schematics:feature loading
70
+ # or shorthand
71
+ ng g @ngstato/schematics:f pagination
72
+ ```
73
+
74
+ Creates a reusable store feature:
75
+
76
+ ```ts
77
+ // loading.feature.ts
78
+ export function withLoading(): FeatureConfig {
79
+ return {
80
+ state: { loading: false, error: null },
81
+ actions: { /* ... */ },
82
+ computed: { /* ... */ }
83
+ }
84
+ }
85
+
86
+ // Usage in any store
87
+ const store = createStore({
88
+ items: [],
89
+ ...mergeFeatures(withLoading(), withPagination()),
90
+ actions: { ... }
91
+ })
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
3
+ "schematics": {
4
+ "store": {
5
+ "description": "Generate a new ngStato store",
6
+ "factory": "./dist/store/index#store",
7
+ "schema": "./src/store/schema.json",
8
+ "aliases": ["s"]
9
+ },
10
+ "feature": {
11
+ "description": "Generate a reusable store feature",
12
+ "factory": "./dist/feature/index#feature",
13
+ "schema": "./src/feature/schema.json",
14
+ "aliases": ["f"]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,3 @@
1
+ import { Rule } from '@angular-devkit/schematics';
2
+ import { FeatureSchema } from './schema';
3
+ export declare function feature(options: FeatureSchema): Rule;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.feature = feature;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const core_1 = require("@angular-devkit/core");
6
+ function feature(options) {
7
+ return (_tree, _context) => {
8
+ const name = core_1.strings.dasherize(options.name);
9
+ const fnName = core_1.strings.camelize(`with-${options.name}`);
10
+ const className = core_1.strings.classify(options.name);
11
+ const content = `import type { FeatureConfig } from '@ngstato/core'
12
+
13
+ /**
14
+ * ${fnName}() — Reusable store feature
15
+ *
16
+ * Usage:
17
+ * \`\`\`ts
18
+ * const store = createStore({
19
+ * ...mergeFeatures(${fnName}()),
20
+ * // your state and actions
21
+ * })
22
+ * \`\`\`
23
+ */
24
+ export function ${fnName}(): FeatureConfig {
25
+ return {
26
+ state: {
27
+ // TODO: Add feature state
28
+ },
29
+
30
+ actions: {
31
+ // TODO: Add feature actions
32
+ },
33
+
34
+ computed: {
35
+ // TODO: Add feature computed
36
+ }
37
+ }
38
+ }
39
+ `;
40
+ return (0, schematics_1.chain)([
41
+ (tree) => {
42
+ tree.create(`${options.path}/${name}.feature.ts`, content);
43
+ return tree;
44
+ }
45
+ ]);
46
+ };
47
+ }
@@ -0,0 +1,4 @@
1
+ export interface FeatureSchema {
2
+ name: string;
3
+ path: string;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import { Rule } from '@angular-devkit/schematics';
2
+ import { StoreSchema } from './schema';
3
+ export declare function store(options: StoreSchema): Rule;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.store = store;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const core_1 = require("@angular-devkit/core");
6
+ function store(options) {
7
+ return (_tree, _context) => {
8
+ const name = core_1.strings.dasherize(options.name);
9
+ const className = core_1.strings.classify(options.name);
10
+ const path = options.flat
11
+ ? options.path
12
+ : `${options.path}/${name}`;
13
+ // Generate the store file content
14
+ const storeContent = generateStoreFile(className, name, options);
15
+ const rules = [];
16
+ rules.push((tree) => {
17
+ tree.create(`${path}/${name}.store.ts`, storeContent);
18
+ return tree;
19
+ });
20
+ // Generate spec file
21
+ if (options.spec) {
22
+ const specContent = generateSpecFile(className, name, options);
23
+ rules.push((tree) => {
24
+ tree.create(`${path}/${name}.store.spec.ts`, specContent);
25
+ return tree;
26
+ });
27
+ }
28
+ return (0, schematics_1.chain)(rules);
29
+ };
30
+ }
31
+ function generateStoreFile(className, name, options) {
32
+ const imports = ['createStore', 'http'];
33
+ if (options.devtools)
34
+ imports.push('connectDevTools');
35
+ if (options.entity)
36
+ imports.push('createEntityAdapter', 'withEntities');
37
+ let content = `import { ${imports.join(', ')} } from '@ngstato/core'
38
+ import { StatoStore, injectStore } from '@ngstato/angular'
39
+
40
+ `;
41
+ // Interface
42
+ content += `export interface ${className} {
43
+ id: string
44
+ name: string
45
+ // TODO: Add your entity properties
46
+ }
47
+
48
+ `;
49
+ // Entity adapter
50
+ if (options.entity) {
51
+ content += `const adapter = createEntityAdapter<${className}>()
52
+
53
+ `;
54
+ }
55
+ // Store factory
56
+ content += `function create${className}Store() {
57
+ const store = createStore({
58
+ // ── State ──
59
+ `;
60
+ if (options.entity) {
61
+ content += ` ...withEntities<${className}>(),
62
+ `;
63
+ }
64
+ else {
65
+ content += ` ${name}s: [] as ${className}[],
66
+ `;
67
+ }
68
+ content += ` loading: false,
69
+ error: null as string | null,
70
+
71
+ // ── Selectors ──
72
+ selectors: {
73
+ `;
74
+ if (options.entity) {
75
+ content += ` total: (s) => s.ids.length,
76
+ `;
77
+ }
78
+ else {
79
+ content += ` total: (s) => s.${name}s.length,
80
+ `;
81
+ }
82
+ content += ` },
83
+
84
+ // ── Actions ──
85
+ actions: {
86
+ `;
87
+ if (options.crud) {
88
+ if (options.entity) {
89
+ content += generateEntityCrudActions(className, name);
90
+ }
91
+ else {
92
+ content += generateBasicCrudActions(className, name);
93
+ }
94
+ }
95
+ else {
96
+ content += ` // TODO: Add your actions
97
+ `;
98
+ }
99
+ content += ` },
100
+
101
+ // ── Hooks ──
102
+ hooks: {
103
+ onInit: (store) => store.load${className}s(),
104
+ onError: (err, action) => console.error(\`[${className}Store] \${action}:\`, err.message)
105
+ }
106
+ })
107
+
108
+ `;
109
+ if (options.devtools) {
110
+ content += ` connectDevTools(store, '${className}Store')
111
+ `;
112
+ }
113
+ content += ` return store
114
+ }
115
+
116
+ // ── Angular Injectable ──
117
+ export const ${className}Store = StatoStore(() => create${className}Store())
118
+ `;
119
+ return content;
120
+ }
121
+ function generateBasicCrudActions(className, name) {
122
+ return ` async load${className}s(state) {
123
+ state.loading = true
124
+ state.error = null
125
+ try {
126
+ state.${name}s = await http.get('/${name}s')
127
+ } catch (e) {
128
+ state.error = (e as Error).message
129
+ throw e
130
+ } finally {
131
+ state.loading = false
132
+ }
133
+ },
134
+
135
+ async create${className}(state, payload: Omit<${className}, 'id'>) {
136
+ const created = await http.post<${className}>('/${name}s', payload)
137
+ state.${name}s = [...state.${name}s, created]
138
+ },
139
+
140
+ async update${className}(state, id: string, changes: Partial<${className}>) {
141
+ const updated = await http.patch<${className}>(\`/${name}s/\${id}\`, changes)
142
+ state.${name}s = state.${name}s.map(item =>
143
+ item.id === id ? { ...item, ...updated } : item
144
+ )
145
+ },
146
+
147
+ async delete${className}(state, id: string) {
148
+ await http.delete(\`/${name}s/\${id}\`)
149
+ state.${name}s = state.${name}s.filter(item => item.id !== id)
150
+ },
151
+ `;
152
+ }
153
+ function generateEntityCrudActions(className, name) {
154
+ return ` async load${className}s(state) {
155
+ state.loading = true
156
+ state.error = null
157
+ try {
158
+ const items = await http.get<${className}[]>('/${name}s')
159
+ adapter.setAll(state, items)
160
+ } catch (e) {
161
+ state.error = (e as Error).message
162
+ throw e
163
+ } finally {
164
+ state.loading = false
165
+ }
166
+ },
167
+
168
+ async create${className}(state, payload: Omit<${className}, 'id'>) {
169
+ const created = await http.post<${className}>('/${name}s', payload)
170
+ adapter.addOne(state, created)
171
+ },
172
+
173
+ async update${className}(state, id: string, changes: Partial<${className}>) {
174
+ const updated = await http.patch<${className}>(\`/${name}s/\${id}\`, changes)
175
+ adapter.updateOne(state, { id, changes: updated })
176
+ },
177
+
178
+ async delete${className}(state, id: string) {
179
+ await http.delete(\`/${name}s/\${id}\`)
180
+ adapter.removeOne(state, id)
181
+ },
182
+ `;
183
+ }
184
+ function generateSpecFile(className, name, options) {
185
+ return `import { describe, it, expect, vi } from 'vitest'
186
+ import { createMockStore } from '@ngstato/testing'
187
+
188
+ describe('${className}Store', () => {
189
+ function createTestStore(overrides = {}) {
190
+ return createMockStore({
191
+ ${options.entity ? `ids: [] as string[],
192
+ entities: {} as Record<string, any>,` : `${name}s: [] as any[],`}
193
+ loading: false,
194
+ error: null as string | null,
195
+
196
+ actions: {
197
+ load${className}s: vi.fn(),
198
+ create${className}: vi.fn(),
199
+ update${className}: vi.fn(),
200
+ delete${className}: vi.fn(),
201
+ }
202
+ }, overrides)
203
+ }
204
+
205
+ it('should create with initial state', () => {
206
+ const store = createTestStore()
207
+ expect(store.loading).toBe(false)
208
+ expect(store.error).toBe(null)
209
+ })
210
+
211
+ it('should set loading state', () => {
212
+ const store = createTestStore()
213
+ store.__setState({ loading: true })
214
+ expect(store.loading).toBe(true)
215
+ })
216
+
217
+ it('should set error state', () => {
218
+ const store = createTestStore()
219
+ store.__setState({ error: 'Something went wrong' })
220
+ expect(store.error).toBe('Something went wrong')
221
+ })
222
+
223
+ // TODO: Add your specific tests
224
+ })
225
+ `;
226
+ }
@@ -0,0 +1,9 @@
1
+ export interface StoreSchema {
2
+ name: string;
3
+ path: string;
4
+ flat: boolean;
5
+ crud: boolean;
6
+ entity: boolean;
7
+ devtools: boolean;
8
+ spec: boolean;
9
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ngstato/schematics",
3
+ "version": "0.4.0",
4
+ "private": false,
5
+ "description": "Angular CLI schematics for ngStato — generate stores, features, and entities",
6
+ "schematics": "./collection.json",
7
+ "files": [
8
+ "dist",
9
+ "collection.json",
10
+ "src/store/schema.json",
11
+ "src/feature/schema.json",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json"
16
+ },
17
+ "dependencies": {
18
+ "@angular-devkit/core": "^18.0.0",
19
+ "@angular-devkit/schematics": "^18.0.0"
20
+ },
21
+ "peerDependencies": {
22
+ "@ngstato/core": ">=0.3.0",
23
+ "@ngstato/angular": ">=0.3.0"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/becher/ngStato.git",
29
+ "directory": "packages/schematics"
30
+ },
31
+ "homepage": "https://github.com/becher/ngStato#readme",
32
+ "author": "becher",
33
+ "keywords": [
34
+ "angular",
35
+ "schematics",
36
+ "ngstato",
37
+ "store",
38
+ "generator",
39
+ "cli",
40
+ "ngrx"
41
+ ]
42
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "ngstato-feature-schema",
4
+ "title": "ngStato Feature Schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "description": "The name of the feature (e.g. 'loading', 'pagination')",
10
+ "$default": { "$source": "argv", "index": 0 },
11
+ "x-prompt": "What name would you like for the feature?"
12
+ },
13
+ "path": {
14
+ "type": "string",
15
+ "description": "Path to create the feature in",
16
+ "default": "src/app/store-features"
17
+ }
18
+ },
19
+ "required": ["name"]
20
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "ngstato-store-schema",
4
+ "title": "ngStato Store Schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "description": "The name of the store (e.g. 'users', 'products')",
10
+ "$default": { "$source": "argv", "index": 0 },
11
+ "x-prompt": "What name would you like to use for the store?"
12
+ },
13
+ "path": {
14
+ "type": "string",
15
+ "description": "The path to create the store in",
16
+ "default": "src/app/stores",
17
+ "visible": false
18
+ },
19
+ "flat": {
20
+ "type": "boolean",
21
+ "description": "Create the store file directly in the path without a subdirectory",
22
+ "default": false
23
+ },
24
+ "crud": {
25
+ "type": "boolean",
26
+ "description": "Generate CRUD actions (load, create, update, delete)",
27
+ "default": true,
28
+ "x-prompt": "Include CRUD actions?"
29
+ },
30
+ "entity": {
31
+ "type": "boolean",
32
+ "description": "Use entity adapter for normalized collections",
33
+ "default": false,
34
+ "x-prompt": "Use entity adapter?"
35
+ },
36
+ "devtools": {
37
+ "type": "boolean",
38
+ "description": "Connect to DevTools",
39
+ "default": true
40
+ },
41
+ "spec": {
42
+ "type": "boolean",
43
+ "description": "Generate a spec file for the store",
44
+ "default": true
45
+ }
46
+ },
47
+ "required": ["name"]
48
+ }