@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 +96 -0
- package/collection.json +17 -0
- package/dist/feature/index.d.ts +3 -0
- package/dist/feature/index.js +47 -0
- package/dist/feature/schema.d.ts +4 -0
- package/dist/feature/schema.js +2 -0
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +226 -0
- package/dist/store/schema.d.ts +9 -0
- package/dist/store/schema.js +2 -0
- package/package.json +42 -0
- package/src/feature/schema.json +20 -0
- package/src/store/schema.json +48 -0
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
|
+
[](https://www.npmjs.com/package/@ngstato/schematics)
|
|
10
|
+
[](https://angular.dev)
|
|
11
|
+
[](#)
|
|
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
|
package/collection.json
ADDED
|
@@ -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,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,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
|
+
}
|
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
|
+
}
|