@canonical/summon 0.1.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 +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Generated by summon webapp
|
|
2
|
+
// Main App component for <%= name %>
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
<% if (hasState) { -%>
|
|
6
|
+
import { useAppStore } from './state';
|
|
7
|
+
<% } -%>
|
|
8
|
+
<% if (hasApi) { -%>
|
|
9
|
+
import { api } from './api';
|
|
10
|
+
<% } -%>
|
|
11
|
+
<% if (hasLogging) { -%>
|
|
12
|
+
import { logger } from './logging';
|
|
13
|
+
<% } -%>
|
|
14
|
+
|
|
15
|
+
export const App: React.FC = () => {
|
|
16
|
+
<% if (hasState) { -%>
|
|
17
|
+
const { count, increment, decrement } = useAppStore();
|
|
18
|
+
<% } -%>
|
|
19
|
+
<% if (hasLogging) { -%>
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
logger.info('App mounted');
|
|
23
|
+
return () => logger.info('App unmounted');
|
|
24
|
+
}, []);
|
|
25
|
+
<% } -%>
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="<%= styling === 'tailwind' ? 'min-h-screen bg-gray-100 p-8' : 'app-container' %>">
|
|
29
|
+
<header className="<%= styling === 'tailwind' ? 'text-center mb-8' : 'app-header' %>">
|
|
30
|
+
<h1 className="<%= styling === 'tailwind' ? 'text-4xl font-bold text-gray-800' : '' %>">
|
|
31
|
+
Welcome to <%= pascalCase(name) %>
|
|
32
|
+
</h1>
|
|
33
|
+
<p className="<%= styling === 'tailwind' ? 'text-gray-600 mt-2' : '' %>">
|
|
34
|
+
<%= description %>
|
|
35
|
+
</p>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<main className="<%= styling === 'tailwind' ? 'max-w-2xl mx-auto' : 'app-main' %>">
|
|
39
|
+
<% if (hasState) { -%>
|
|
40
|
+
<section className="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6 mb-6' : 'card' %>">
|
|
41
|
+
<h2 className="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Counter Demo</h2>
|
|
42
|
+
<div className="<%= styling === 'tailwind' ? 'flex items-center gap-4' : 'counter' %>">
|
|
43
|
+
<button
|
|
44
|
+
onClick={decrement}
|
|
45
|
+
className="<%= styling === 'tailwind' ? 'px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600' : 'btn btn-danger' %>"
|
|
46
|
+
>
|
|
47
|
+
-
|
|
48
|
+
</button>
|
|
49
|
+
<span className="<%= styling === 'tailwind' ? 'text-2xl font-mono' : 'count' %>">{count}</span>
|
|
50
|
+
<button
|
|
51
|
+
onClick={increment}
|
|
52
|
+
className="<%= styling === 'tailwind' ? 'px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600' : 'btn btn-success' %>"
|
|
53
|
+
>
|
|
54
|
+
+
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
<% } -%>
|
|
59
|
+
|
|
60
|
+
<section className="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6' : 'card' %>">
|
|
61
|
+
<h2 className="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Features</h2>
|
|
62
|
+
<ul className="<%= styling === 'tailwind' ? 'list-disc list-inside space-y-2' : 'feature-list' %>">
|
|
63
|
+
<li>Framework: <%= framework %></li>
|
|
64
|
+
<li>Styling: <%= styling %></li>
|
|
65
|
+
<% if (hasRouter) { -%>
|
|
66
|
+
<li>Routing enabled</li>
|
|
67
|
+
<% } -%>
|
|
68
|
+
<% if (hasState) { -%>
|
|
69
|
+
<li>State management with Zustand</li>
|
|
70
|
+
<% } -%>
|
|
71
|
+
<% if (hasApi) { -%>
|
|
72
|
+
<li>API client configured</li>
|
|
73
|
+
<% } -%>
|
|
74
|
+
<% if (hasLogging) { -%>
|
|
75
|
+
<li>Logging enabled</li>
|
|
76
|
+
<% } -%>
|
|
77
|
+
</ul>
|
|
78
|
+
</section>
|
|
79
|
+
</main>
|
|
80
|
+
|
|
81
|
+
<footer className="<%= styling === 'tailwind' ? 'text-center mt-8 text-gray-500' : 'app-footer' %>">
|
|
82
|
+
<p>Generated by Summon</p>
|
|
83
|
+
</footer>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# <%= pascalCase(name) %>
|
|
2
|
+
|
|
3
|
+
<%= description %>
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bun install
|
|
10
|
+
|
|
11
|
+
# Start development server
|
|
12
|
+
bun run dev
|
|
13
|
+
<% if (withTests) { -%>
|
|
14
|
+
|
|
15
|
+
# Run tests
|
|
16
|
+
bun run test
|
|
17
|
+
<% } -%>
|
|
18
|
+
|
|
19
|
+
# Build for production
|
|
20
|
+
bun run build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Stack
|
|
24
|
+
|
|
25
|
+
- **Framework**: <%= framework === 'react' ? 'React 18' : 'Vanilla TypeScript' %>
|
|
26
|
+
- **Styling**: <%= styling === 'tailwind' ? 'Tailwind CSS' : styling === 'css' ? 'Plain CSS' : 'None' %>
|
|
27
|
+
<% if (features.length > 0) { -%>
|
|
28
|
+
- **Features**:
|
|
29
|
+
<% features.forEach(function(feature) { -%>
|
|
30
|
+
- <%= feature === 'router' ? 'Client-side routing' : feature === 'state' ? 'State management (Zustand)' : feature === 'api' ? 'API client (ky)' : feature === 'logging' ? 'Logging utilities' : feature %>
|
|
31
|
+
<% }); -%>
|
|
32
|
+
<% } -%>
|
|
33
|
+
<% if (withTests) { -%>
|
|
34
|
+
- **Testing**: Vitest
|
|
35
|
+
<% } -%>
|
|
36
|
+
|
|
37
|
+
## Project Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
<%= name %>/
|
|
41
|
+
<%= '├── ' %>src/
|
|
42
|
+
<%= '│ ├── ' %>main.<%= framework === 'react' ? 'tsx' : 'ts' %> # Entry point
|
|
43
|
+
<%= '│ ├── ' %><%= framework === 'react' ? 'App.tsx' : 'app.ts' %> # Main component/module
|
|
44
|
+
<% if (styling !== 'none') { -%>
|
|
45
|
+
<%= '│ ├── ' %>styles.css # Global styles
|
|
46
|
+
<% } -%>
|
|
47
|
+
<% features.forEach(function(feature) { -%>
|
|
48
|
+
<%= '│ ├── ' %><%= feature %>.ts # <%= feature.charAt(0).toUpperCase() + feature.slice(1) %> module
|
|
49
|
+
<% }); -%>
|
|
50
|
+
<%= '│ └── ' %>components/ # Reusable components
|
|
51
|
+
<%= '├── ' %>public/ # Static assets
|
|
52
|
+
<% if (withTests) { -%>
|
|
53
|
+
<%= '├── ' %>tests/ # Test files
|
|
54
|
+
<% } -%>
|
|
55
|
+
<% if (withDocs) { -%>
|
|
56
|
+
<%= '├── ' %>docs/ # Documentation
|
|
57
|
+
<% } -%>
|
|
58
|
+
<%= '├── ' %>package.json
|
|
59
|
+
<%= '└── ' %>tsconfig.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
<% if (framework === 'react') { -%>
|
|
65
|
+
This project uses Vite for fast development with HMR (Hot Module Replacement).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bun run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open http://localhost:5173 to view the app.
|
|
72
|
+
<% } else { -%>
|
|
73
|
+
This project uses a simple development setup with live-server.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun run dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open http://localhost:8080 to view the app.
|
|
80
|
+
<% } -%>
|
|
81
|
+
|
|
82
|
+
<% if (hasState) { -%>
|
|
83
|
+
## State Management
|
|
84
|
+
|
|
85
|
+
<% if (framework === 'react') { -%>
|
|
86
|
+
State is managed using [Zustand](https://github.com/pmndrs/zustand). Access the store in any component:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useAppStore } from './state';
|
|
90
|
+
|
|
91
|
+
function MyComponent() {
|
|
92
|
+
const { count, increment } = useAppStore();
|
|
93
|
+
return <button onClick={increment}>{count}</button>;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
<% } else { -%>
|
|
97
|
+
State is managed using a simple pub/sub store pattern:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { appStore } from './state';
|
|
101
|
+
|
|
102
|
+
// Get current state
|
|
103
|
+
const { count } = appStore.getState();
|
|
104
|
+
|
|
105
|
+
// Update state
|
|
106
|
+
appStore.setState({ count: count + 1 });
|
|
107
|
+
|
|
108
|
+
// Subscribe to changes
|
|
109
|
+
appStore.subscribe((state) => {
|
|
110
|
+
console.log('State changed:', state);
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
<% } -%>
|
|
114
|
+
|
|
115
|
+
<% } -%>
|
|
116
|
+
<% if (hasApi) { -%>
|
|
117
|
+
## API Client
|
|
118
|
+
|
|
119
|
+
The API client is configured in `src/api.ts` using [ky](https://github.com/sindresorhus/ky):
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { get, post } from './api';
|
|
123
|
+
|
|
124
|
+
// GET request
|
|
125
|
+
const users = await get<User[]>('users');
|
|
126
|
+
|
|
127
|
+
// POST request
|
|
128
|
+
const newUser = await post<User>('users', { name: 'John' });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Configure the API base URL via the `VITE_API_URL` environment variable.
|
|
132
|
+
|
|
133
|
+
<% } -%>
|
|
134
|
+
<% if (hasLogging) { -%>
|
|
135
|
+
## Logging
|
|
136
|
+
|
|
137
|
+
Use the logger for debugging and monitoring:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { logger } from './logging';
|
|
141
|
+
|
|
142
|
+
logger.debug('Debug message');
|
|
143
|
+
logger.info('Info message');
|
|
144
|
+
logger.warn('Warning message');
|
|
145
|
+
logger.error('Error message');
|
|
146
|
+
|
|
147
|
+
// View log history
|
|
148
|
+
const history = logger.getHistory();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
<% } -%>
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
Generated by [Summon](https://github.com/canonical/pragma) - A Monadic Task-Centric Code Generator Framework
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Generated by summon webapp
|
|
2
|
+
// Tests for <%= name %>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('<%= pascalCase(name) %>', () => {
|
|
7
|
+
it('should pass a basic test', () => {
|
|
8
|
+
expect(true).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
<% if (hasState && framework === 'react') { -%>
|
|
12
|
+
describe('App Store', () => {
|
|
13
|
+
it('should initialize with count of 0', async () => {
|
|
14
|
+
const { useAppStore } = await import('../src/state');
|
|
15
|
+
const { count } = useAppStore.getState();
|
|
16
|
+
expect(count).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should increment count', async () => {
|
|
20
|
+
const { useAppStore } = await import('../src/state');
|
|
21
|
+
const { increment } = useAppStore.getState();
|
|
22
|
+
increment();
|
|
23
|
+
expect(useAppStore.getState().count).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should decrement count', async () => {
|
|
27
|
+
const { useAppStore } = await import('../src/state');
|
|
28
|
+
const store = useAppStore.getState();
|
|
29
|
+
store.reset();
|
|
30
|
+
store.decrement();
|
|
31
|
+
expect(useAppStore.getState().count).toBe(-1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
<% } -%>
|
|
35
|
+
|
|
36
|
+
<% if (hasApi) { -%>
|
|
37
|
+
describe('API Client', () => {
|
|
38
|
+
it('should be configured with base URL', async () => {
|
|
39
|
+
const { api } = await import('../src/api');
|
|
40
|
+
expect(api).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
<% } -%>
|
|
44
|
+
|
|
45
|
+
<% if (hasLogging) { -%>
|
|
46
|
+
describe('Logger', () => {
|
|
47
|
+
it('should log messages', async () => {
|
|
48
|
+
const { logger } = await import('../src/logging');
|
|
49
|
+
logger.info('Test message');
|
|
50
|
+
const history = logger.getHistory();
|
|
51
|
+
expect(history.length).toBeGreaterThan(0);
|
|
52
|
+
expect(history[history.length - 1].message).toBe('Test message');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should clear history', async () => {
|
|
56
|
+
const { logger } = await import('../src/logging');
|
|
57
|
+
logger.info('Another message');
|
|
58
|
+
logger.clearHistory();
|
|
59
|
+
expect(logger.getHistory().length).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
<% } -%>
|
|
63
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Generated by summon webapp
|
|
2
|
+
// Main application module for <%= name %>
|
|
3
|
+
|
|
4
|
+
<% if (hasLogging) { -%>
|
|
5
|
+
import { logger } from './logging';
|
|
6
|
+
<% } -%>
|
|
7
|
+
<% if (hasApi) { -%>
|
|
8
|
+
import { api } from './api';
|
|
9
|
+
<% } -%>
|
|
10
|
+
|
|
11
|
+
interface AppState {
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state: AppState = {
|
|
16
|
+
count: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the application.
|
|
21
|
+
*/
|
|
22
|
+
export function init(container: HTMLElement): void {
|
|
23
|
+
<% if (hasLogging) { -%>
|
|
24
|
+
logger.info('Initializing <%= name %>...');
|
|
25
|
+
<% } -%>
|
|
26
|
+
|
|
27
|
+
render(container);
|
|
28
|
+
setupEventListeners(container);
|
|
29
|
+
|
|
30
|
+
<% if (hasLogging) { -%>
|
|
31
|
+
logger.info('<%= name %> initialized successfully');
|
|
32
|
+
<% } -%>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Render the application UI.
|
|
37
|
+
*/
|
|
38
|
+
function render(container: HTMLElement): void {
|
|
39
|
+
container.innerHTML = `
|
|
40
|
+
<div class="<%= styling === 'tailwind' ? 'min-h-screen bg-gray-100 p-8' : 'app-container' %>">
|
|
41
|
+
<header class="<%= styling === 'tailwind' ? 'text-center mb-8' : 'app-header' %>">
|
|
42
|
+
<h1 class="<%= styling === 'tailwind' ? 'text-4xl font-bold text-gray-800' : '' %>">
|
|
43
|
+
Welcome to <%= name %>
|
|
44
|
+
</h1>
|
|
45
|
+
<p class="<%= styling === 'tailwind' ? 'text-gray-600 mt-2' : '' %>">
|
|
46
|
+
<%= description %>
|
|
47
|
+
</p>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<main class="<%= styling === 'tailwind' ? 'max-w-2xl mx-auto' : 'app-main' %>">
|
|
51
|
+
<% if (hasState) { -%>
|
|
52
|
+
<section class="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6 mb-6' : 'card' %>">
|
|
53
|
+
<h2 class="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Counter Demo</h2>
|
|
54
|
+
<div class="<%= styling === 'tailwind' ? 'flex items-center gap-4' : 'counter' %>">
|
|
55
|
+
<button
|
|
56
|
+
id="decrement"
|
|
57
|
+
class="<%= styling === 'tailwind' ? 'px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600' : 'btn btn-danger' %>"
|
|
58
|
+
>
|
|
59
|
+
-
|
|
60
|
+
</button>
|
|
61
|
+
<span id="count" class="<%= styling === 'tailwind' ? 'text-2xl font-mono' : 'count' %>">${state.count}</span>
|
|
62
|
+
<button
|
|
63
|
+
id="increment"
|
|
64
|
+
class="<%= styling === 'tailwind' ? 'px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600' : 'btn btn-success' %>"
|
|
65
|
+
>
|
|
66
|
+
+
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
<% } -%>
|
|
71
|
+
|
|
72
|
+
<section class="<%= styling === 'tailwind' ? 'bg-white rounded-lg shadow p-6' : 'card' %>">
|
|
73
|
+
<h2 class="<%= styling === 'tailwind' ? 'text-xl font-semibold mb-4' : '' %>">Features</h2>
|
|
74
|
+
<ul class="<%= styling === 'tailwind' ? 'list-disc list-inside space-y-2' : 'feature-list' %>">
|
|
75
|
+
<li>Framework: <%= framework %></li>
|
|
76
|
+
<li>Styling: <%= styling %></li>
|
|
77
|
+
<% if (hasRouter) { -%>
|
|
78
|
+
<li>Routing enabled</li>
|
|
79
|
+
<% } -%>
|
|
80
|
+
<% if (hasState) { -%>
|
|
81
|
+
<li>State management</li>
|
|
82
|
+
<% } -%>
|
|
83
|
+
<% if (hasApi) { -%>
|
|
84
|
+
<li>API client configured</li>
|
|
85
|
+
<% } -%>
|
|
86
|
+
<% if (hasLogging) { -%>
|
|
87
|
+
<li>Logging enabled</li>
|
|
88
|
+
<% } -%>
|
|
89
|
+
</ul>
|
|
90
|
+
</section>
|
|
91
|
+
</main>
|
|
92
|
+
|
|
93
|
+
<footer class="<%= styling === 'tailwind' ? 'text-center mt-8 text-gray-500' : 'app-footer' %>">
|
|
94
|
+
<p>Generated by Summon</p>
|
|
95
|
+
</footer>
|
|
96
|
+
</div>
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
<% if (hasState) { -%>
|
|
101
|
+
/**
|
|
102
|
+
* Setup event listeners for interactive elements.
|
|
103
|
+
*/
|
|
104
|
+
function setupEventListeners(container: HTMLElement): void {
|
|
105
|
+
const incrementBtn = container.querySelector('#increment');
|
|
106
|
+
const decrementBtn = container.querySelector('#decrement');
|
|
107
|
+
const countDisplay = container.querySelector('#count');
|
|
108
|
+
|
|
109
|
+
incrementBtn?.addEventListener('click', () => {
|
|
110
|
+
state.count++;
|
|
111
|
+
if (countDisplay) countDisplay.textContent = String(state.count);
|
|
112
|
+
<% if (hasLogging) { -%>
|
|
113
|
+
logger.debug(`Count incremented to ${state.count}`);
|
|
114
|
+
<% } -%>
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
decrementBtn?.addEventListener('click', () => {
|
|
118
|
+
state.count--;
|
|
119
|
+
if (countDisplay) countDisplay.textContent = String(state.count);
|
|
120
|
+
<% if (hasLogging) { -%>
|
|
121
|
+
logger.debug(`Count decremented to ${state.count}`);
|
|
122
|
+
<% } -%>
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
<% } else { -%>
|
|
126
|
+
/**
|
|
127
|
+
* Setup event listeners for interactive elements.
|
|
128
|
+
*/
|
|
129
|
+
function setupEventListeners(_container: HTMLElement): void {
|
|
130
|
+
// Add your event listeners here
|
|
131
|
+
}
|
|
132
|
+
<% } -%>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Generated by summon webapp
|
|
2
|
+
// Feature module: <%= featureName %>
|
|
3
|
+
|
|
4
|
+
<% if (featureName === 'router') { -%>
|
|
5
|
+
/**
|
|
6
|
+
* Router configuration for <%= name %>
|
|
7
|
+
*/
|
|
8
|
+
<% if (framework === 'react') { -%>
|
|
9
|
+
import { Routes, Route } from 'react-router-dom';
|
|
10
|
+
|
|
11
|
+
export const AppRoutes = () => (
|
|
12
|
+
<Routes>
|
|
13
|
+
<Route path="/" element={<div>Home</div>} />
|
|
14
|
+
<Route path="/about" element={<div>About</div>} />
|
|
15
|
+
<Route path="*" element={<div>Not Found</div>} />
|
|
16
|
+
</Routes>
|
|
17
|
+
);
|
|
18
|
+
<% } else { -%>
|
|
19
|
+
type RouteHandler = (params: Record<string, string>) => void;
|
|
20
|
+
|
|
21
|
+
interface Route {
|
|
22
|
+
path: string;
|
|
23
|
+
handler: RouteHandler;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const routes: Route[] = [];
|
|
27
|
+
|
|
28
|
+
export function addRoute(path: string, handler: RouteHandler): void {
|
|
29
|
+
routes.push({ path, handler });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function navigate(path: string): void {
|
|
33
|
+
const route = routes.find((r) => matchPath(r.path, path));
|
|
34
|
+
if (route) {
|
|
35
|
+
const params = extractParams(route.path, path);
|
|
36
|
+
route.handler(params);
|
|
37
|
+
history.pushState(null, '', path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchPath(pattern: string, path: string): boolean {
|
|
42
|
+
const regex = new RegExp('^' + pattern.replace(/:\w+/g, '([^/]+)') + '$');
|
|
43
|
+
return regex.test(path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractParams(pattern: string, path: string): Record<string, string> {
|
|
47
|
+
const params: Record<string, string> = {};
|
|
48
|
+
const patternParts = pattern.split('/');
|
|
49
|
+
const pathParts = path.split('/');
|
|
50
|
+
|
|
51
|
+
patternParts.forEach((part, i) => {
|
|
52
|
+
if (part.startsWith(':')) {
|
|
53
|
+
params[part.slice(1)] = pathParts[i];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return params;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Listen for popstate events
|
|
61
|
+
window.addEventListener('popstate', () => {
|
|
62
|
+
navigate(location.pathname);
|
|
63
|
+
});
|
|
64
|
+
<% } -%>
|
|
65
|
+
<% } else if (featureName === 'state') { -%>
|
|
66
|
+
/**
|
|
67
|
+
* State management for <%= name %>
|
|
68
|
+
*/
|
|
69
|
+
<% if (framework === 'react') { -%>
|
|
70
|
+
import { create } from 'zustand';
|
|
71
|
+
|
|
72
|
+
interface AppState {
|
|
73
|
+
count: number;
|
|
74
|
+
increment: () => void;
|
|
75
|
+
decrement: () => void;
|
|
76
|
+
reset: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
80
|
+
count: 0,
|
|
81
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
82
|
+
decrement: () => set((state) => ({ count: state.count - 1 })),
|
|
83
|
+
reset: () => set({ count: 0 }),
|
|
84
|
+
}));
|
|
85
|
+
<% } else { -%>
|
|
86
|
+
type Listener<T> = (state: T) => void;
|
|
87
|
+
|
|
88
|
+
export function createStore<T extends object>(initialState: T) {
|
|
89
|
+
let state = { ...initialState };
|
|
90
|
+
const listeners = new Set<Listener<T>>();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getState: () => state,
|
|
94
|
+
setState: (partial: Partial<T>) => {
|
|
95
|
+
state = { ...state, ...partial };
|
|
96
|
+
listeners.forEach((listener) => listener(state));
|
|
97
|
+
},
|
|
98
|
+
subscribe: (listener: Listener<T>) => {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => listeners.delete(listener);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Application store
|
|
106
|
+
export const appStore = createStore({
|
|
107
|
+
count: 0,
|
|
108
|
+
});
|
|
109
|
+
<% } -%>
|
|
110
|
+
<% } else if (featureName === 'api') { -%>
|
|
111
|
+
/**
|
|
112
|
+
* API client for <%= name %>
|
|
113
|
+
*/
|
|
114
|
+
import ky from 'ky';
|
|
115
|
+
|
|
116
|
+
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com';
|
|
117
|
+
|
|
118
|
+
export const api = ky.create({
|
|
119
|
+
prefixUrl: API_BASE_URL,
|
|
120
|
+
timeout: 30000,
|
|
121
|
+
retry: {
|
|
122
|
+
limit: 2,
|
|
123
|
+
methods: ['get'],
|
|
124
|
+
},
|
|
125
|
+
hooks: {
|
|
126
|
+
beforeRequest: [
|
|
127
|
+
(request) => {
|
|
128
|
+
// Add auth token if available
|
|
129
|
+
const token = localStorage.getItem('auth_token');
|
|
130
|
+
if (token) {
|
|
131
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
afterResponse: [
|
|
136
|
+
async (request, options, response) => {
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
console.error(`API Error: ${response.status} ${response.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
return response;
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Typed API helpers
|
|
147
|
+
export async function get<T>(endpoint: string): Promise<T> {
|
|
148
|
+
return api.get(endpoint).json<T>();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function post<T, D = unknown>(endpoint: string, data: D): Promise<T> {
|
|
152
|
+
return api.post(endpoint, { json: data }).json<T>();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function put<T, D = unknown>(endpoint: string, data: D): Promise<T> {
|
|
156
|
+
return api.put(endpoint, { json: data }).json<T>();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function del<T>(endpoint: string): Promise<T> {
|
|
160
|
+
return api.delete(endpoint).json<T>();
|
|
161
|
+
}
|
|
162
|
+
<% } else if (featureName === 'logging') { -%>
|
|
163
|
+
/**
|
|
164
|
+
* Logging utilities for <%= name %>
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
168
|
+
|
|
169
|
+
interface LogEntry {
|
|
170
|
+
level: LogLevel;
|
|
171
|
+
message: string;
|
|
172
|
+
timestamp: Date;
|
|
173
|
+
context?: Record<string, unknown>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
177
|
+
debug: 0,
|
|
178
|
+
info: 1,
|
|
179
|
+
warn: 2,
|
|
180
|
+
error: 3,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
class Logger {
|
|
184
|
+
private minLevel: LogLevel = 'debug';
|
|
185
|
+
private history: LogEntry[] = [];
|
|
186
|
+
private maxHistory = 1000;
|
|
187
|
+
|
|
188
|
+
setLevel(level: LogLevel): void {
|
|
189
|
+
this.minLevel = level;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
|
|
193
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[this.minLevel]) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const entry: LogEntry = {
|
|
198
|
+
level,
|
|
199
|
+
message,
|
|
200
|
+
timestamp: new Date(),
|
|
201
|
+
context,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this.history.push(entry);
|
|
205
|
+
if (this.history.length > this.maxHistory) {
|
|
206
|
+
this.history.shift();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const styles: Record<LogLevel, string> = {
|
|
210
|
+
debug: 'color: gray',
|
|
211
|
+
info: 'color: blue',
|
|
212
|
+
warn: 'color: orange',
|
|
213
|
+
error: 'color: red',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
console[level === 'debug' ? 'log' : level](
|
|
217
|
+
`%c[${level.toUpperCase()}] ${message}`,
|
|
218
|
+
styles[level],
|
|
219
|
+
context || ''
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
224
|
+
this.log('debug', message, context);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
228
|
+
this.log('info', message, context);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
232
|
+
this.log('warn', message, context);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
error(message: string, context?: Record<string, unknown>): void {
|
|
236
|
+
this.log('error', message, context);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getHistory(): LogEntry[] {
|
|
240
|
+
return [...this.history];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
clearHistory(): void {
|
|
244
|
+
this.history = [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const logger = new Logger();
|
|
249
|
+
<% } else { -%>
|
|
250
|
+
/**
|
|
251
|
+
* Feature module: <%= featureName %>
|
|
252
|
+
*
|
|
253
|
+
* This is a placeholder for the <%= featureName %> feature.
|
|
254
|
+
* Implement your feature logic here.
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
export function init<%= pascalCase(featureName) %>(): void {
|
|
258
|
+
console.log('<%= featureName %> feature initialized');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const <%= camelCase(featureName) %> = {
|
|
262
|
+
init: init<%= pascalCase(featureName) %>,
|
|
263
|
+
};
|
|
264
|
+
<% } -%>
|