pulse-js-framework 1.7.11 → 1.7.13
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 +80 -8
- package/cli/docs.js +712 -0
- package/cli/doctor.js +702 -0
- package/cli/index.js +338 -65
- package/cli/scaffold.js +1037 -0
- package/cli/test.js +455 -0
- package/package.json +19 -2
- package/runtime/a11y.js +824 -1
- package/runtime/context.js +374 -0
- package/runtime/graphql.js +1356 -0
- package/runtime/index.js +6 -0
- package/runtime/logger.js +2 -1
- package/runtime/websocket.js +874 -0
- package/types/context.d.ts +171 -0
- package/types/graphql.d.ts +490 -0
- package/types/index.d.ts +15 -0
- package/types/websocket.d.ts +347 -0
package/cli/scaffold.js
ADDED
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI - Scaffold Command
|
|
3
|
+
* Generate components, pages, stores, and other project files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
+
import { join, dirname, relative, basename } from 'path';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
import { parseArgs } from './utils/file-utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scaffold types and their templates
|
|
13
|
+
*/
|
|
14
|
+
const SCAFFOLD_TYPES = {
|
|
15
|
+
component: {
|
|
16
|
+
description: 'Pulse component (.pulse file)',
|
|
17
|
+
defaultDir: 'src/components',
|
|
18
|
+
extension: '.pulse',
|
|
19
|
+
template: generateComponentTemplate
|
|
20
|
+
},
|
|
21
|
+
page: {
|
|
22
|
+
description: 'Page component with routing',
|
|
23
|
+
defaultDir: 'src/pages',
|
|
24
|
+
extension: '.pulse',
|
|
25
|
+
template: generatePageTemplate
|
|
26
|
+
},
|
|
27
|
+
store: {
|
|
28
|
+
description: 'State store module',
|
|
29
|
+
defaultDir: 'src/stores',
|
|
30
|
+
extension: '.js',
|
|
31
|
+
template: generateStoreTemplate
|
|
32
|
+
},
|
|
33
|
+
hook: {
|
|
34
|
+
description: 'Custom hook/composable',
|
|
35
|
+
defaultDir: 'src/hooks',
|
|
36
|
+
extension: '.js',
|
|
37
|
+
template: generateHookTemplate
|
|
38
|
+
},
|
|
39
|
+
service: {
|
|
40
|
+
description: 'Service/API module',
|
|
41
|
+
defaultDir: 'src/services',
|
|
42
|
+
extension: '.js',
|
|
43
|
+
template: generateServiceTemplate
|
|
44
|
+
},
|
|
45
|
+
test: {
|
|
46
|
+
description: 'Test file',
|
|
47
|
+
defaultDir: 'test',
|
|
48
|
+
extension: '.test.js',
|
|
49
|
+
template: generateTestTemplate
|
|
50
|
+
},
|
|
51
|
+
context: {
|
|
52
|
+
description: 'Context provider module',
|
|
53
|
+
defaultDir: 'src/contexts',
|
|
54
|
+
extension: '.js',
|
|
55
|
+
template: generateContextTemplate
|
|
56
|
+
},
|
|
57
|
+
layout: {
|
|
58
|
+
description: 'Layout component',
|
|
59
|
+
defaultDir: 'src/layouts',
|
|
60
|
+
extension: '.pulse',
|
|
61
|
+
template: generateLayoutTemplate
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert name to different cases
|
|
67
|
+
*/
|
|
68
|
+
function toCase(name, type) {
|
|
69
|
+
// Remove extension if present
|
|
70
|
+
name = name.replace(/\.(pulse|js|ts)$/, '');
|
|
71
|
+
|
|
72
|
+
switch (type) {
|
|
73
|
+
case 'pascal':
|
|
74
|
+
return name
|
|
75
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
76
|
+
.replace(/^./, c => c.toUpperCase());
|
|
77
|
+
case 'camel':
|
|
78
|
+
return name
|
|
79
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
80
|
+
.replace(/^./, c => c.toLowerCase());
|
|
81
|
+
case 'kebab':
|
|
82
|
+
return name
|
|
83
|
+
.replace(/([A-Z])/g, '-$1')
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/^-/, '')
|
|
86
|
+
.replace(/[-_]+/g, '-');
|
|
87
|
+
case 'snake':
|
|
88
|
+
return name
|
|
89
|
+
.replace(/([A-Z])/g, '_$1')
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/^_/, '')
|
|
92
|
+
.replace(/[-_]+/g, '_');
|
|
93
|
+
default:
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate component template
|
|
100
|
+
*/
|
|
101
|
+
function generateComponentTemplate(name, options = {}) {
|
|
102
|
+
const pascalName = toCase(name, 'pascal');
|
|
103
|
+
const kebabName = toCase(name, 'kebab');
|
|
104
|
+
const { withState = true, withStyle = true, withProps = false } = options;
|
|
105
|
+
|
|
106
|
+
let template = `@page ${pascalName}\n`;
|
|
107
|
+
|
|
108
|
+
// Props section if requested
|
|
109
|
+
if (withProps) {
|
|
110
|
+
template += `
|
|
111
|
+
// Props (passed from parent)
|
|
112
|
+
// Usage: <${pascalName} title="Hello" count={5} />
|
|
113
|
+
props {
|
|
114
|
+
title: ""
|
|
115
|
+
count: 0
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// State section
|
|
121
|
+
if (withState) {
|
|
122
|
+
template += `
|
|
123
|
+
state {
|
|
124
|
+
// Add your state variables here
|
|
125
|
+
value: ""
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// View section
|
|
131
|
+
template += `
|
|
132
|
+
view {
|
|
133
|
+
.${kebabName} {
|
|
134
|
+
h2 "${pascalName}"
|
|
135
|
+
p "Your component content here"
|
|
136
|
+
${withProps ? `
|
|
137
|
+
@if(title) {
|
|
138
|
+
p.title "{title}"
|
|
139
|
+
}` : ''}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
// Style section
|
|
145
|
+
if (withStyle) {
|
|
146
|
+
template += `
|
|
147
|
+
style {
|
|
148
|
+
.${kebabName} {
|
|
149
|
+
padding: 1rem
|
|
150
|
+
|
|
151
|
+
h2 {
|
|
152
|
+
margin-bottom: 0.5rem
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return template;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate page template
|
|
164
|
+
*/
|
|
165
|
+
function generatePageTemplate(name, options = {}) {
|
|
166
|
+
const pascalName = toCase(name, 'pascal');
|
|
167
|
+
const kebabName = toCase(name, 'kebab');
|
|
168
|
+
|
|
169
|
+
return `@page ${pascalName}Page
|
|
170
|
+
|
|
171
|
+
// Route parameters are available via router.params
|
|
172
|
+
// import { router } from 'pulse-js-framework/runtime/router'
|
|
173
|
+
|
|
174
|
+
state {
|
|
175
|
+
loading: false
|
|
176
|
+
data: null
|
|
177
|
+
error: null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
actions {
|
|
181
|
+
async loadData() {
|
|
182
|
+
loading = true
|
|
183
|
+
error = null
|
|
184
|
+
try {
|
|
185
|
+
// Load page data here
|
|
186
|
+
// const response = await fetch('/api/${kebabName}')
|
|
187
|
+
// data = await response.json()
|
|
188
|
+
} catch (e) {
|
|
189
|
+
error = e.message
|
|
190
|
+
} finally {
|
|
191
|
+
loading = false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
view {
|
|
197
|
+
.${kebabName}-page {
|
|
198
|
+
header.page-header {
|
|
199
|
+
h1 "${pascalName}"
|
|
200
|
+
// Breadcrumb, actions, etc.
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@if(loading) {
|
|
204
|
+
.loading-state {
|
|
205
|
+
p "Loading..."
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@if(error) {
|
|
210
|
+
.error-state {
|
|
211
|
+
p.error "{error}"
|
|
212
|
+
button @click(loadData()) "Retry"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@if(!loading && !error) {
|
|
217
|
+
main.page-content {
|
|
218
|
+
// Page content here
|
|
219
|
+
p "Welcome to ${pascalName} page"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
style {
|
|
226
|
+
.${kebabName}-page {
|
|
227
|
+
min-height: 100vh
|
|
228
|
+
padding: 2rem
|
|
229
|
+
|
|
230
|
+
.page-header {
|
|
231
|
+
margin-bottom: 2rem
|
|
232
|
+
|
|
233
|
+
h1 {
|
|
234
|
+
font-size: 2rem
|
|
235
|
+
margin: 0
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.page-content {
|
|
240
|
+
max-width: 1200px
|
|
241
|
+
margin: 0 auto
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.loading-state,
|
|
245
|
+
.error-state {
|
|
246
|
+
text-align: center
|
|
247
|
+
padding: 2rem
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.error {
|
|
251
|
+
color: #dc3545
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate store template
|
|
260
|
+
*/
|
|
261
|
+
function generateStoreTemplate(name, options = {}) {
|
|
262
|
+
const pascalName = toCase(name, 'pascal');
|
|
263
|
+
const camelName = toCase(name, 'camel');
|
|
264
|
+
|
|
265
|
+
return `/**
|
|
266
|
+
* ${pascalName} Store
|
|
267
|
+
* State management for ${name}
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
import { createStore, createActions } from 'pulse-js-framework/runtime/store';
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Initial state
|
|
274
|
+
*/
|
|
275
|
+
const initialState = {
|
|
276
|
+
items: [],
|
|
277
|
+
selectedId: null,
|
|
278
|
+
loading: false,
|
|
279
|
+
error: null
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create the store with persistence (optional)
|
|
284
|
+
*/
|
|
285
|
+
export const ${camelName}Store = createStore(initialState, {
|
|
286
|
+
persist: false, // Set to true to persist to localStorage
|
|
287
|
+
name: '${kebabName}'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Store actions
|
|
292
|
+
*/
|
|
293
|
+
export const ${camelName}Actions = createActions(${camelName}Store, {
|
|
294
|
+
/**
|
|
295
|
+
* Set items
|
|
296
|
+
* @param {Object} store - Store instance
|
|
297
|
+
* @param {Array} items - Items to set
|
|
298
|
+
*/
|
|
299
|
+
setItems(store, items) {
|
|
300
|
+
store.items.set(items);
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Add an item
|
|
305
|
+
* @param {Object} store - Store instance
|
|
306
|
+
* @param {Object} item - Item to add
|
|
307
|
+
*/
|
|
308
|
+
addItem(store, item) {
|
|
309
|
+
store.items.update(items => [...items, item]);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Remove an item by ID
|
|
314
|
+
* @param {Object} store - Store instance
|
|
315
|
+
* @param {string|number} id - Item ID to remove
|
|
316
|
+
*/
|
|
317
|
+
removeItem(store, id) {
|
|
318
|
+
store.items.update(items => items.filter(item => item.id !== id));
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Update an item
|
|
323
|
+
* @param {Object} store - Store instance
|
|
324
|
+
* @param {string|number} id - Item ID
|
|
325
|
+
* @param {Object} updates - Fields to update
|
|
326
|
+
*/
|
|
327
|
+
updateItem(store, id, updates) {
|
|
328
|
+
store.items.update(items =>
|
|
329
|
+
items.map(item => item.id === id ? { ...item, ...updates } : item)
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Select an item
|
|
335
|
+
* @param {Object} store - Store instance
|
|
336
|
+
* @param {string|number|null} id - Item ID to select
|
|
337
|
+
*/
|
|
338
|
+
select(store, id) {
|
|
339
|
+
store.selectedId.set(id);
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Set loading state
|
|
344
|
+
* @param {Object} store - Store instance
|
|
345
|
+
* @param {boolean} loading - Loading state
|
|
346
|
+
*/
|
|
347
|
+
setLoading(store, loading) {
|
|
348
|
+
store.loading.set(loading);
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Set error
|
|
353
|
+
* @param {Object} store - Store instance
|
|
354
|
+
* @param {string|null} error - Error message
|
|
355
|
+
*/
|
|
356
|
+
setError(store, error) {
|
|
357
|
+
store.error.set(error);
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Clear all data
|
|
362
|
+
* @param {Object} store - Store instance
|
|
363
|
+
*/
|
|
364
|
+
reset(store) {
|
|
365
|
+
store.items.set([]);
|
|
366
|
+
store.selectedId.set(null);
|
|
367
|
+
store.loading.set(false);
|
|
368
|
+
store.error.set(null);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Selectors (computed values)
|
|
374
|
+
*/
|
|
375
|
+
export const ${camelName}Selectors = {
|
|
376
|
+
/**
|
|
377
|
+
* Get selected item
|
|
378
|
+
*/
|
|
379
|
+
getSelected() {
|
|
380
|
+
const items = ${camelName}Store.items.get();
|
|
381
|
+
const selectedId = ${camelName}Store.selectedId.get();
|
|
382
|
+
return items.find(item => item.id === selectedId) || null;
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get item count
|
|
387
|
+
*/
|
|
388
|
+
getCount() {
|
|
389
|
+
return ${camelName}Store.items.get().length;
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if empty
|
|
394
|
+
*/
|
|
395
|
+
isEmpty() {
|
|
396
|
+
return ${camelName}Store.items.get().length === 0;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Export store parts for convenience
|
|
401
|
+
export const { items, selectedId, loading, error } = ${camelName}Store;
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Generate hook template
|
|
407
|
+
*/
|
|
408
|
+
function generateHookTemplate(name, options = {}) {
|
|
409
|
+
const camelName = toCase(name, 'camel');
|
|
410
|
+
const hookName = camelName.startsWith('use') ? camelName : `use${toCase(name, 'pascal')}`;
|
|
411
|
+
|
|
412
|
+
return `/**
|
|
413
|
+
* ${hookName}
|
|
414
|
+
* Custom hook for ${name}
|
|
415
|
+
*/
|
|
416
|
+
|
|
417
|
+
import { pulse, effect, computed } from 'pulse-js-framework/runtime';
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* ${hookName}
|
|
421
|
+
* @param {Object} options - Hook options
|
|
422
|
+
* @returns {Object} Hook return value
|
|
423
|
+
*/
|
|
424
|
+
export function ${hookName}(options = {}) {
|
|
425
|
+
// State
|
|
426
|
+
const value = pulse(options.initialValue ?? null);
|
|
427
|
+
const loading = pulse(false);
|
|
428
|
+
const error = pulse(null);
|
|
429
|
+
|
|
430
|
+
// Computed values
|
|
431
|
+
const hasValue = computed(() => value.get() !== null);
|
|
432
|
+
const isReady = computed(() => !loading.get() && !error.get());
|
|
433
|
+
|
|
434
|
+
// Actions
|
|
435
|
+
function setValue(newValue) {
|
|
436
|
+
value.set(newValue);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function setLoading(isLoading) {
|
|
440
|
+
loading.set(isLoading);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function setError(err) {
|
|
444
|
+
error.set(err);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function reset() {
|
|
448
|
+
value.set(options.initialValue ?? null);
|
|
449
|
+
loading.set(false);
|
|
450
|
+
error.set(null);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Optional: Setup effects
|
|
454
|
+
// effect(() => {
|
|
455
|
+
// const currentValue = value.get();
|
|
456
|
+
// // React to value changes
|
|
457
|
+
// });
|
|
458
|
+
|
|
459
|
+
// Return hook API
|
|
460
|
+
return {
|
|
461
|
+
// State (read-only pulses)
|
|
462
|
+
value,
|
|
463
|
+
loading,
|
|
464
|
+
error,
|
|
465
|
+
|
|
466
|
+
// Computed
|
|
467
|
+
hasValue,
|
|
468
|
+
isReady,
|
|
469
|
+
|
|
470
|
+
// Actions
|
|
471
|
+
setValue,
|
|
472
|
+
setLoading,
|
|
473
|
+
setError,
|
|
474
|
+
reset
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export default ${hookName};
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Generate service template
|
|
484
|
+
*/
|
|
485
|
+
function generateServiceTemplate(name, options = {}) {
|
|
486
|
+
const pascalName = toCase(name, 'pascal');
|
|
487
|
+
const camelName = toCase(name, 'camel');
|
|
488
|
+
|
|
489
|
+
return `/**
|
|
490
|
+
* ${pascalName} Service
|
|
491
|
+
* API and business logic for ${name}
|
|
492
|
+
*/
|
|
493
|
+
|
|
494
|
+
import { createHttp } from 'pulse-js-framework/runtime/http';
|
|
495
|
+
|
|
496
|
+
// Create HTTP client for this service
|
|
497
|
+
const api = createHttp({
|
|
498
|
+
baseURL: '/api/${toCase(name, 'kebab')}',
|
|
499
|
+
timeout: 10000
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* ${pascalName} Service
|
|
504
|
+
*/
|
|
505
|
+
export const ${camelName}Service = {
|
|
506
|
+
/**
|
|
507
|
+
* Get all items
|
|
508
|
+
* @param {Object} params - Query parameters
|
|
509
|
+
* @returns {Promise<Array>}
|
|
510
|
+
*/
|
|
511
|
+
async getAll(params = {}) {
|
|
512
|
+
const response = await api.get('/', { params });
|
|
513
|
+
return response.data;
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get item by ID
|
|
518
|
+
* @param {string|number} id - Item ID
|
|
519
|
+
* @returns {Promise<Object>}
|
|
520
|
+
*/
|
|
521
|
+
async getById(id) {
|
|
522
|
+
const response = await api.get(\`/\${id}\`);
|
|
523
|
+
return response.data;
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Create new item
|
|
528
|
+
* @param {Object} data - Item data
|
|
529
|
+
* @returns {Promise<Object>}
|
|
530
|
+
*/
|
|
531
|
+
async create(data) {
|
|
532
|
+
const response = await api.post('/', data);
|
|
533
|
+
return response.data;
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Update item
|
|
538
|
+
* @param {string|number} id - Item ID
|
|
539
|
+
* @param {Object} data - Update data
|
|
540
|
+
* @returns {Promise<Object>}
|
|
541
|
+
*/
|
|
542
|
+
async update(id, data) {
|
|
543
|
+
const response = await api.put(\`/\${id}\`, data);
|
|
544
|
+
return response.data;
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Partial update item
|
|
549
|
+
* @param {string|number} id - Item ID
|
|
550
|
+
* @param {Object} data - Partial update data
|
|
551
|
+
* @returns {Promise<Object>}
|
|
552
|
+
*/
|
|
553
|
+
async patch(id, data) {
|
|
554
|
+
const response = await api.patch(\`/\${id}\`, data);
|
|
555
|
+
return response.data;
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Delete item
|
|
560
|
+
* @param {string|number} id - Item ID
|
|
561
|
+
* @returns {Promise<void>}
|
|
562
|
+
*/
|
|
563
|
+
async delete(id) {
|
|
564
|
+
await api.delete(\`/\${id}\`);
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Search items
|
|
569
|
+
* @param {string} query - Search query
|
|
570
|
+
* @param {Object} options - Search options
|
|
571
|
+
* @returns {Promise<Array>}
|
|
572
|
+
*/
|
|
573
|
+
async search(query, options = {}) {
|
|
574
|
+
const response = await api.get('/search', {
|
|
575
|
+
params: { q: query, ...options }
|
|
576
|
+
});
|
|
577
|
+
return response.data;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
export default ${camelName}Service;
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Generate test template
|
|
587
|
+
*/
|
|
588
|
+
function generateTestTemplate(name, options = {}) {
|
|
589
|
+
const pascalName = toCase(name, 'pascal');
|
|
590
|
+
const camelName = toCase(name, 'camel');
|
|
591
|
+
|
|
592
|
+
return `/**
|
|
593
|
+
* ${pascalName} Tests
|
|
594
|
+
*/
|
|
595
|
+
|
|
596
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
597
|
+
import assert from 'node:assert';
|
|
598
|
+
|
|
599
|
+
// Import the module to test
|
|
600
|
+
// import { ${camelName} } from '../src/${name}.js';
|
|
601
|
+
|
|
602
|
+
describe('${pascalName}', () => {
|
|
603
|
+
let instance;
|
|
604
|
+
|
|
605
|
+
beforeEach(() => {
|
|
606
|
+
// Setup before each test
|
|
607
|
+
instance = null;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
afterEach(() => {
|
|
611
|
+
// Cleanup after each test
|
|
612
|
+
instance = null;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe('initialization', () => {
|
|
616
|
+
test('should create instance with default options', () => {
|
|
617
|
+
// const result = ${camelName}();
|
|
618
|
+
// assert.ok(result);
|
|
619
|
+
assert.ok(true, 'placeholder');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('should accept custom options', () => {
|
|
623
|
+
// const result = ${camelName}({ custom: true });
|
|
624
|
+
// assert.strictEqual(result.custom, true);
|
|
625
|
+
assert.ok(true, 'placeholder');
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('core functionality', () => {
|
|
630
|
+
test('should perform main operation', () => {
|
|
631
|
+
// Add your test here
|
|
632
|
+
assert.ok(true, 'placeholder');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('should handle edge cases', () => {
|
|
636
|
+
// Add your test here
|
|
637
|
+
assert.ok(true, 'placeholder');
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe('error handling', () => {
|
|
642
|
+
test('should throw on invalid input', () => {
|
|
643
|
+
// assert.throws(() => ${camelName}(null), { message: /invalid/i });
|
|
644
|
+
assert.ok(true, 'placeholder');
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Generate context template
|
|
653
|
+
*/
|
|
654
|
+
function generateContextTemplate(name, options = {}) {
|
|
655
|
+
const pascalName = toCase(name, 'pascal');
|
|
656
|
+
const camelName = toCase(name, 'camel');
|
|
657
|
+
|
|
658
|
+
return `/**
|
|
659
|
+
* ${pascalName} Context
|
|
660
|
+
* Context provider for ${name}
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
import { createContext, useContext, Provider } from 'pulse-js-framework/runtime/context';
|
|
664
|
+
import { pulse, computed } from 'pulse-js-framework/runtime';
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Default context value
|
|
668
|
+
*/
|
|
669
|
+
const defaultValue = {
|
|
670
|
+
// Add default state here
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Create the context
|
|
675
|
+
*/
|
|
676
|
+
export const ${pascalName}Context = createContext(defaultValue, {
|
|
677
|
+
displayName: '${pascalName}Context'
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* ${pascalName} Provider Component
|
|
682
|
+
* @param {Object} props - Provider props
|
|
683
|
+
* @param {Function} props.children - Child render function
|
|
684
|
+
* @param {Object} props.value - Override context value
|
|
685
|
+
*/
|
|
686
|
+
export function ${pascalName}Provider({ children, value = {} }) {
|
|
687
|
+
// Create reactive state
|
|
688
|
+
const state = pulse({
|
|
689
|
+
...defaultValue,
|
|
690
|
+
...value
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Create context value with state and actions
|
|
694
|
+
const contextValue = {
|
|
695
|
+
state,
|
|
696
|
+
|
|
697
|
+
// Actions
|
|
698
|
+
setValue: (newValue) => {
|
|
699
|
+
state.update(s => ({ ...s, ...newValue }));
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
reset: () => {
|
|
703
|
+
state.set({ ...defaultValue });
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
return Provider(${pascalName}Context, contextValue, children);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Hook to use ${pascalName} context
|
|
712
|
+
* @returns {Object} Context value
|
|
713
|
+
*/
|
|
714
|
+
export function use${pascalName}() {
|
|
715
|
+
const context = useContext(${pascalName}Context);
|
|
716
|
+
|
|
717
|
+
if (!context || context === defaultValue) {
|
|
718
|
+
console.warn('use${pascalName} must be used within a ${pascalName}Provider');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return context;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Higher-order component to inject context
|
|
726
|
+
* @param {Function} Component - Component to wrap
|
|
727
|
+
* @returns {Function} Wrapped component
|
|
728
|
+
*/
|
|
729
|
+
export function with${pascalName}(Component) {
|
|
730
|
+
return function Wrapped${pascalName}(props) {
|
|
731
|
+
const ${camelName} = use${pascalName}();
|
|
732
|
+
return Component({ ...props, ${camelName} });
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export default ${pascalName}Context;
|
|
737
|
+
`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Generate layout template
|
|
742
|
+
*/
|
|
743
|
+
function generateLayoutTemplate(name, options = {}) {
|
|
744
|
+
const pascalName = toCase(name, 'pascal');
|
|
745
|
+
const kebabName = toCase(name, 'kebab');
|
|
746
|
+
|
|
747
|
+
return `@page ${pascalName}Layout
|
|
748
|
+
|
|
749
|
+
// Layout component with slots for header, main content, and footer
|
|
750
|
+
|
|
751
|
+
state {
|
|
752
|
+
sidebarOpen: false
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
actions {
|
|
756
|
+
toggleSidebar() {
|
|
757
|
+
sidebarOpen = !sidebarOpen
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
view {
|
|
762
|
+
.${kebabName}-layout {
|
|
763
|
+
// Header slot
|
|
764
|
+
header.layout-header {
|
|
765
|
+
slot "header" {
|
|
766
|
+
// Default header content
|
|
767
|
+
nav {
|
|
768
|
+
a[href="/"] "Home"
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
button.menu-toggle @click(toggleSidebar()) {
|
|
773
|
+
span.sr-only "Toggle menu"
|
|
774
|
+
span.icon "☰"
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Sidebar (optional)
|
|
779
|
+
aside.layout-sidebar[class.open=sidebarOpen] {
|
|
780
|
+
slot "sidebar" {
|
|
781
|
+
// Default sidebar content
|
|
782
|
+
nav {
|
|
783
|
+
ul {
|
|
784
|
+
li { a[href="/"] "Dashboard" }
|
|
785
|
+
li { a[href="/settings"] "Settings" }
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Main content slot
|
|
792
|
+
main.layout-main {
|
|
793
|
+
slot {
|
|
794
|
+
// Default main content
|
|
795
|
+
p "Content goes here"
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Footer slot
|
|
800
|
+
footer.layout-footer {
|
|
801
|
+
slot "footer" {
|
|
802
|
+
// Default footer content
|
|
803
|
+
p "© 2024"
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
style {
|
|
810
|
+
.${kebabName}-layout {
|
|
811
|
+
display: grid
|
|
812
|
+
grid-template-rows: auto 1fr auto
|
|
813
|
+
grid-template-columns: auto 1fr
|
|
814
|
+
min-height: 100vh
|
|
815
|
+
|
|
816
|
+
.layout-header {
|
|
817
|
+
grid-column: 1 / -1
|
|
818
|
+
display: flex
|
|
819
|
+
justify-content: space-between
|
|
820
|
+
align-items: center
|
|
821
|
+
padding: 1rem 2rem
|
|
822
|
+
background: #f5f5f5
|
|
823
|
+
border-bottom: 1px solid #ddd
|
|
824
|
+
|
|
825
|
+
nav a {
|
|
826
|
+
margin-right: 1rem
|
|
827
|
+
text-decoration: none
|
|
828
|
+
color: #333
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.menu-toggle {
|
|
832
|
+
display: none
|
|
833
|
+
background: none
|
|
834
|
+
border: none
|
|
835
|
+
font-size: 1.5rem
|
|
836
|
+
cursor: pointer
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.layout-sidebar {
|
|
841
|
+
width: 250px
|
|
842
|
+
padding: 1rem
|
|
843
|
+
background: #fafafa
|
|
844
|
+
border-right: 1px solid #eee
|
|
845
|
+
|
|
846
|
+
nav ul {
|
|
847
|
+
list-style: none
|
|
848
|
+
padding: 0
|
|
849
|
+
margin: 0
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
nav li {
|
|
853
|
+
margin-bottom: 0.5rem
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
nav a {
|
|
857
|
+
display: block
|
|
858
|
+
padding: 0.5rem
|
|
859
|
+
text-decoration: none
|
|
860
|
+
color: #333
|
|
861
|
+
border-radius: 4px
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
nav a:hover {
|
|
865
|
+
background: #eee
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.layout-main {
|
|
870
|
+
padding: 2rem
|
|
871
|
+
overflow-y: auto
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.layout-footer {
|
|
875
|
+
grid-column: 1 / -1
|
|
876
|
+
padding: 1rem 2rem
|
|
877
|
+
background: #f5f5f5
|
|
878
|
+
border-top: 1px solid #ddd
|
|
879
|
+
text-align: center
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.sr-only {
|
|
883
|
+
position: absolute
|
|
884
|
+
width: 1px
|
|
885
|
+
height: 1px
|
|
886
|
+
padding: 0
|
|
887
|
+
margin: -1px
|
|
888
|
+
overflow: hidden
|
|
889
|
+
clip: rect(0, 0, 0, 0)
|
|
890
|
+
border: 0
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
@media (max-width: 768px) {
|
|
894
|
+
grid-template-columns: 1fr
|
|
895
|
+
|
|
896
|
+
.layout-header .menu-toggle {
|
|
897
|
+
display: block
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.layout-sidebar {
|
|
901
|
+
position: fixed
|
|
902
|
+
left: -250px
|
|
903
|
+
top: 0
|
|
904
|
+
height: 100vh
|
|
905
|
+
z-index: 100
|
|
906
|
+
transition: left 0.3s ease
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.layout-sidebar.open {
|
|
910
|
+
left: 0
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Main scaffold command handler
|
|
920
|
+
*/
|
|
921
|
+
export async function runScaffold(args) {
|
|
922
|
+
const { options, patterns } = parseArgs(args);
|
|
923
|
+
|
|
924
|
+
// Parse arguments
|
|
925
|
+
const type = patterns[0];
|
|
926
|
+
const name = patterns[1];
|
|
927
|
+
|
|
928
|
+
// Show help if no type provided
|
|
929
|
+
if (!type) {
|
|
930
|
+
showScaffoldHelp();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Check if type is valid
|
|
935
|
+
if (!SCAFFOLD_TYPES[type]) {
|
|
936
|
+
log.error(`Unknown scaffold type: ${type}`);
|
|
937
|
+
log.info('\nAvailable types:');
|
|
938
|
+
for (const [key, info] of Object.entries(SCAFFOLD_TYPES)) {
|
|
939
|
+
log.info(` ${key.padEnd(12)} - ${info.description}`);
|
|
940
|
+
}
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Check if name is provided
|
|
945
|
+
if (!name) {
|
|
946
|
+
log.error(`Please provide a name for the ${type}.`);
|
|
947
|
+
log.info(`\nUsage: pulse scaffold ${type} <name>`);
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const scaffoldType = SCAFFOLD_TYPES[type];
|
|
952
|
+
|
|
953
|
+
// Determine output directory
|
|
954
|
+
const dir = options.dir || options.d || scaffoldType.defaultDir;
|
|
955
|
+
|
|
956
|
+
// Determine file name
|
|
957
|
+
const fileName = toCase(name, type === 'component' || type === 'page' || type === 'layout' ? 'pascal' : 'camel');
|
|
958
|
+
const fullPath = join(process.cwd(), dir, fileName + scaffoldType.extension);
|
|
959
|
+
|
|
960
|
+
// Check if file already exists
|
|
961
|
+
if (existsSync(fullPath) && !options.force && !options.f) {
|
|
962
|
+
log.error(`File already exists: ${relative(process.cwd(), fullPath)}`);
|
|
963
|
+
log.info('Use --force to overwrite.');
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Generate content
|
|
968
|
+
const content = scaffoldType.template(name, {
|
|
969
|
+
withState: options.state !== false,
|
|
970
|
+
withStyle: options.style !== false,
|
|
971
|
+
withProps: options.props || false
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Create directory if needed
|
|
975
|
+
const outputDir = dirname(fullPath);
|
|
976
|
+
if (!existsSync(outputDir)) {
|
|
977
|
+
mkdirSync(outputDir, { recursive: true });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Write file
|
|
981
|
+
writeFileSync(fullPath, content);
|
|
982
|
+
|
|
983
|
+
log.success(`Created ${type}: ${relative(process.cwd(), fullPath)}`);
|
|
984
|
+
|
|
985
|
+
// Show next steps
|
|
986
|
+
if (type === 'component' || type === 'page' || type === 'layout') {
|
|
987
|
+
log.info(`\nImport it in your app:`);
|
|
988
|
+
log.info(` import ${toCase(name, 'pascal')} from './${dir}/${fileName}${scaffoldType.extension}'`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Show scaffold help
|
|
994
|
+
*/
|
|
995
|
+
function showScaffoldHelp() {
|
|
996
|
+
log.info(`
|
|
997
|
+
Pulse Scaffold - Generate project files
|
|
998
|
+
|
|
999
|
+
Usage: pulse scaffold <type> <name> [options]
|
|
1000
|
+
|
|
1001
|
+
Types:
|
|
1002
|
+
`);
|
|
1003
|
+
|
|
1004
|
+
for (const [key, info] of Object.entries(SCAFFOLD_TYPES)) {
|
|
1005
|
+
log.info(` ${key.padEnd(12)} ${info.description}`);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
log.info(`
|
|
1009
|
+
Options:
|
|
1010
|
+
--dir, -d <path> Output directory (default: based on type)
|
|
1011
|
+
--force, -f Overwrite existing files
|
|
1012
|
+
--props Include props section (components)
|
|
1013
|
+
--no-state Skip state section
|
|
1014
|
+
--no-style Skip style section
|
|
1015
|
+
|
|
1016
|
+
Examples:
|
|
1017
|
+
pulse scaffold component Button
|
|
1018
|
+
pulse scaffold component UserCard --props
|
|
1019
|
+
pulse scaffold page Dashboard
|
|
1020
|
+
pulse scaffold store user
|
|
1021
|
+
pulse scaffold hook useAuth
|
|
1022
|
+
pulse scaffold service api
|
|
1023
|
+
pulse scaffold context Theme
|
|
1024
|
+
pulse scaffold layout Admin --dir src/layouts
|
|
1025
|
+
pulse scaffold test Button
|
|
1026
|
+
|
|
1027
|
+
Output Directories:
|
|
1028
|
+
component -> src/components/
|
|
1029
|
+
page -> src/pages/
|
|
1030
|
+
store -> src/stores/
|
|
1031
|
+
hook -> src/hooks/
|
|
1032
|
+
service -> src/services/
|
|
1033
|
+
context -> src/contexts/
|
|
1034
|
+
layout -> src/layouts/
|
|
1035
|
+
test -> test/
|
|
1036
|
+
`);
|
|
1037
|
+
}
|