@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.
Files changed (45) hide show
  1. package/README.md +439 -0
  2. package/generators/example/hello/index.ts +132 -0
  3. package/generators/example/hello/templates/README.md.ejs +20 -0
  4. package/generators/example/hello/templates/index.ts.ejs +9 -0
  5. package/generators/example/webapp/index.ts +509 -0
  6. package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
  7. package/generators/example/webapp/templates/App.tsx.ejs +86 -0
  8. package/generators/example/webapp/templates/README.md.ejs +154 -0
  9. package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
  10. package/generators/example/webapp/templates/app.ts.ejs +132 -0
  11. package/generators/example/webapp/templates/feature.ts.ejs +264 -0
  12. package/generators/example/webapp/templates/index.html.ejs +20 -0
  13. package/generators/example/webapp/templates/main.tsx.ejs +43 -0
  14. package/generators/example/webapp/templates/styles.css.ejs +135 -0
  15. package/generators/init/index.ts +124 -0
  16. package/generators/init/templates/generator.ts.ejs +85 -0
  17. package/generators/init/templates/template-index.ts.ejs +9 -0
  18. package/generators/init/templates/template-test.ts.ejs +8 -0
  19. package/package.json +64 -0
  20. package/src/__tests__/combinators.test.ts +895 -0
  21. package/src/__tests__/dry-run.test.ts +927 -0
  22. package/src/__tests__/effect.test.ts +816 -0
  23. package/src/__tests__/interpreter.test.ts +673 -0
  24. package/src/__tests__/primitives.test.ts +970 -0
  25. package/src/__tests__/task.test.ts +929 -0
  26. package/src/__tests__/template.test.ts +666 -0
  27. package/src/cli-format.ts +165 -0
  28. package/src/cli-types.ts +53 -0
  29. package/src/cli.tsx +1322 -0
  30. package/src/combinators.ts +294 -0
  31. package/src/completion.ts +488 -0
  32. package/src/components/App.tsx +960 -0
  33. package/src/components/ExecutionProgress.tsx +205 -0
  34. package/src/components/FileTreePreview.tsx +97 -0
  35. package/src/components/PromptSequence.tsx +483 -0
  36. package/src/components/Spinner.tsx +36 -0
  37. package/src/components/index.ts +16 -0
  38. package/src/dry-run.ts +434 -0
  39. package/src/effect.ts +224 -0
  40. package/src/index.ts +266 -0
  41. package/src/interpreter.ts +463 -0
  42. package/src/primitives.ts +442 -0
  43. package/src/task.ts +245 -0
  44. package/src/template.ts +537 -0
  45. package/src/types.ts +453 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Web App Generator (Advanced Demo)
3
+ *
4
+ * A comprehensive example generator that demonstrates the full power of Summon,
5
+ * including parallel operations, file I/O, conditionals, error handling,
6
+ * context management, and template rendering.
7
+ *
8
+ * Use this as a reference for building complex generators.
9
+ */
10
+
11
+ import * as path from "node:path";
12
+ import {
13
+ appendFile,
14
+ debug,
15
+ exec,
16
+ exists,
17
+ getContext,
18
+ ifElse,
19
+ info,
20
+ mkdir,
21
+ orElse,
22
+ parallel,
23
+ readFile,
24
+ sequence_,
25
+ setContext,
26
+ template,
27
+ traverse_,
28
+ warn,
29
+ when,
30
+ withHelpers,
31
+ writeFile,
32
+ } from "../../../src/index.js";
33
+ import { flatMap, map, pure } from "../../../src/task.js";
34
+ import type { GeneratorDefinition, Task } from "../../../src/types.js";
35
+
36
+ // =============================================================================
37
+ // Types
38
+ // =============================================================================
39
+
40
+ interface WebAppAnswers {
41
+ name: string;
42
+ description: string;
43
+ framework: "react" | "vanilla";
44
+ styling: "css" | "tailwind" | "none";
45
+ features: string[];
46
+ withTests: boolean;
47
+ withDocs: boolean;
48
+ installDeps: boolean;
49
+ }
50
+
51
+ // =============================================================================
52
+ // Helper Tasks
53
+ // =============================================================================
54
+
55
+ /**
56
+ * Check if a directory exists, creating it if not.
57
+ * Demonstrates: exists + ifElse + info logging
58
+ */
59
+ const ensureDir = (dirPath: string): Task<void> =>
60
+ flatMap(exists(dirPath), (doesExist) =>
61
+ ifElse(
62
+ doesExist,
63
+ debug(`Directory ${dirPath} already exists`),
64
+ mkdir(dirPath),
65
+ ),
66
+ );
67
+
68
+ /**
69
+ * Write a JSON file with pretty formatting.
70
+ * Demonstrates: writeFile with transformation
71
+ */
72
+ const writeJson = (filePath: string, data: object): Task<void> =>
73
+ writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`);
74
+
75
+ /**
76
+ * Try to read an existing config, falling back to default.
77
+ * Demonstrates: orElse for error recovery
78
+ */
79
+ const _readConfigOrDefault = <T>(
80
+ configPath: string,
81
+ defaultValue: T,
82
+ ): Task<T> =>
83
+ orElse(
84
+ map(readFile(configPath), (content) => JSON.parse(content) as T),
85
+ pure(defaultValue),
86
+ );
87
+
88
+ // =============================================================================
89
+ // Generator Definition
90
+ // =============================================================================
91
+
92
+ export const generator: GeneratorDefinition<WebAppAnswers> = {
93
+ meta: {
94
+ name: "webapp",
95
+ description:
96
+ "Create a web application with configurable framework, styling, and features",
97
+ version: "0.1.0",
98
+ help: `This advanced generator demonstrates the full capabilities of Summon:
99
+
100
+ EFFECTS DEMONSTRATED:
101
+ - File Operations: readFile, writeFile, copyFile, mkdir, exists
102
+ - Process Execution: exec (npm/bun commands)
103
+ - Logging: info, warn, debug
104
+ - Context: getContext, setContext (for passing data between tasks)
105
+
106
+ COMBINATORS DEMONSTRATED:
107
+ - sequence_: Sequential task composition
108
+ - parallel: Concurrent task execution
109
+ - traverse_: Mapping over arrays with tasks
110
+ - when/ifElse: Conditional task execution
111
+ - orElse: Error recovery with fallbacks
112
+ - flatMap/map: Monadic composition
113
+
114
+ TEMPLATES:
115
+ - EJS templates with withHelpers (camelCase, pascalCase, etc.)
116
+ - Conditional template sections
117
+ - Dynamic content based on answers`,
118
+ examples: [
119
+ // Zero-config: uses all defaults, interactive prompts
120
+ "summon webapp",
121
+ // Minimal: just override the name, rest uses defaults
122
+ "summon webapp --name=my-app",
123
+ // Partial: specify stack choices, defaults for the rest
124
+ "summon webapp --name=my-app --framework=react --styling=tailwind",
125
+ // Features: add specific modules
126
+ "summon webapp --name=my-app --features=router,state,api",
127
+ // Full non-interactive: all options specified, skip prompts
128
+ "summon webapp --name=my-app --framework=react --styling=tailwind --features=router,state --no-withDocs --yes",
129
+ // Preview mode: see what would be generated without writing
130
+ "summon webapp --dry-run",
131
+ ],
132
+ },
133
+
134
+ prompts: [
135
+ {
136
+ name: "name",
137
+ type: "text",
138
+ message: "Project name:",
139
+ default: "my-webapp",
140
+ group: "Project",
141
+ },
142
+ {
143
+ name: "description",
144
+ type: "text",
145
+ message: "Description:",
146
+ default: "A web application",
147
+ group: "Project",
148
+ },
149
+ {
150
+ name: "framework",
151
+ type: "select",
152
+ message: "Framework:",
153
+ choices: [
154
+ { label: "React", value: "react" },
155
+ { label: "Vanilla JS", value: "vanilla" },
156
+ ],
157
+ default: "react",
158
+ group: "Stack",
159
+ },
160
+ {
161
+ name: "styling",
162
+ type: "select",
163
+ message: "Styling solution:",
164
+ choices: [
165
+ { label: "Plain CSS", value: "css" },
166
+ { label: "Tailwind CSS", value: "tailwind" },
167
+ { label: "None", value: "none" },
168
+ ],
169
+ default: "css",
170
+ group: "Stack",
171
+ },
172
+ {
173
+ name: "features",
174
+ type: "multiselect",
175
+ message: "Additional features:",
176
+ choices: [
177
+ { label: "Router", value: "router" },
178
+ { label: "State Management", value: "state" },
179
+ { label: "API Client", value: "api" },
180
+ { label: "Logging", value: "logging" },
181
+ ],
182
+ default: [],
183
+ group: "Stack",
184
+ },
185
+ {
186
+ name: "withTests",
187
+ type: "confirm",
188
+ message: "Include test setup?",
189
+ default: true,
190
+ group: "Extras",
191
+ },
192
+ {
193
+ name: "withDocs",
194
+ type: "confirm",
195
+ message: "Include documentation?",
196
+ default: true,
197
+ group: "Extras",
198
+ },
199
+ {
200
+ name: "installDeps",
201
+ type: "confirm",
202
+ message: "Install dependencies after generation?",
203
+ default: false,
204
+ group: "Extras",
205
+ },
206
+ ],
207
+
208
+ generate: (answers) => {
209
+ const { name, framework, styling, withTests, withDocs, installDeps } =
210
+ answers;
211
+
212
+ // Filter out empty strings from features (handles --features= edge case)
213
+ const features = (answers.features ?? []).filter((f) => f.length > 0);
214
+
215
+ // Enhance answers with case transformation helpers
216
+ const vars = withHelpers({
217
+ ...answers,
218
+ features, // Use cleaned features
219
+ hasRouter: features.includes("router"),
220
+ hasState: features.includes("state"),
221
+ hasApi: features.includes("api"),
222
+ hasLogging: features.includes("logging"),
223
+ });
224
+
225
+ const projectDir = name;
226
+
227
+ // =========================================================================
228
+ // Task 1: Setup directory structure (parallel)
229
+ // Demonstrates: parallel + ensureDir helper
230
+ // =========================================================================
231
+ const createDirectories = sequence_([
232
+ info("Setting up directory structure..."),
233
+ // Store project path in context for later tasks
234
+ setContext("projectDir", projectDir),
235
+ // Create directories in parallel for efficiency
236
+ parallel([
237
+ ensureDir(projectDir),
238
+ ensureDir(path.join(projectDir, "src")),
239
+ ensureDir(path.join(projectDir, "src", "components")),
240
+ ensureDir(path.join(projectDir, "public")),
241
+ when(withTests, ensureDir(path.join(projectDir, "tests"))),
242
+ when(withDocs, ensureDir(path.join(projectDir, "docs"))),
243
+ ]),
244
+ ]);
245
+
246
+ // =========================================================================
247
+ // Task 2: Generate configuration files
248
+ // Demonstrates: writeJson, readConfigOrDefault, parallel
249
+ // =========================================================================
250
+ const generateConfigs = sequence_([
251
+ info("Generating configuration files..."),
252
+ parallel([
253
+ // Package.json with dependencies based on choices
254
+ writeJson(path.join(projectDir, "package.json"), {
255
+ name,
256
+ version: "0.1.0",
257
+ description: answers.description,
258
+ type: "module",
259
+ scripts: {
260
+ dev: framework === "react" ? "vite" : "live-server public",
261
+ build:
262
+ framework === "react" ? "vite build" : "echo 'No build step'",
263
+ ...(withTests ? { test: "vitest" } : {}),
264
+ },
265
+ dependencies: {
266
+ ...(framework === "react"
267
+ ? { react: "^18.2.0", "react-dom": "^18.2.0" }
268
+ : {}),
269
+ ...(styling === "tailwind" ? { tailwindcss: "^3.4.0" } : {}),
270
+ ...(features.includes("router") && framework === "react"
271
+ ? { "react-router-dom": "^6.20.0" }
272
+ : {}),
273
+ ...(features.includes("state") && framework === "react"
274
+ ? { zustand: "^4.4.0" }
275
+ : {}),
276
+ ...(features.includes("api") ? { ky: "^1.1.0" } : {}),
277
+ },
278
+ devDependencies: {
279
+ ...(framework === "react"
280
+ ? { vite: "^5.0.0", "@vitejs/plugin-react": "^4.2.0" }
281
+ : {}),
282
+ ...(withTests ? { vitest: "^1.0.0" } : {}),
283
+ typescript: "^5.3.0",
284
+ ...(styling === "tailwind"
285
+ ? { autoprefixer: "^10.4.0", postcss: "^8.4.0" }
286
+ : {}),
287
+ },
288
+ }),
289
+
290
+ // TypeScript config
291
+ writeJson(path.join(projectDir, "tsconfig.json"), {
292
+ compilerOptions: {
293
+ target: "ES2022",
294
+ module: "ESNext",
295
+ moduleResolution: "bundler",
296
+ strict: true,
297
+ jsx: framework === "react" ? "react-jsx" : undefined,
298
+ esModuleInterop: true,
299
+ skipLibCheck: true,
300
+ outDir: "dist",
301
+ },
302
+ include: ["src/**/*"],
303
+ exclude: ["node_modules"],
304
+ }),
305
+
306
+ // Tailwind config (conditional)
307
+ when(
308
+ styling === "tailwind",
309
+ writeFile(
310
+ path.join(projectDir, "tailwind.config.js"),
311
+ `/** @type {import('tailwindcss').Config} */
312
+ export default {
313
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
314
+ theme: {
315
+ extend: {},
316
+ },
317
+ plugins: [],
318
+ };
319
+ `,
320
+ ),
321
+ ),
322
+ ]),
323
+ ]);
324
+
325
+ // =========================================================================
326
+ // Task 3: Generate source files using templates
327
+ // Demonstrates: template, traverse_, when
328
+ // =========================================================================
329
+ const generateSourceFiles = sequence_([
330
+ info("Generating source files..."),
331
+
332
+ // Main entry point
333
+ template({
334
+ source: path.join(__dirname, "templates", "main.tsx.ejs"),
335
+ dest: path.join(
336
+ projectDir,
337
+ "src",
338
+ `main.${framework === "react" ? "tsx" : "ts"}`,
339
+ ),
340
+ vars,
341
+ }),
342
+
343
+ // App component (React) or main module (Vanilla)
344
+ template({
345
+ source: path.join(
346
+ __dirname,
347
+ "templates",
348
+ framework === "react" ? "App.tsx.ejs" : "app.ts.ejs",
349
+ ),
350
+ dest: path.join(
351
+ projectDir,
352
+ "src",
353
+ framework === "react" ? "App.tsx" : "app.ts",
354
+ ),
355
+ vars,
356
+ }),
357
+
358
+ // Index HTML
359
+ template({
360
+ source: path.join(__dirname, "templates", "index.html.ejs"),
361
+ dest: path.join(projectDir, "index.html"),
362
+ vars,
363
+ }),
364
+
365
+ // Optional: Generate feature modules
366
+ when(
367
+ features.length > 0,
368
+ sequence_([
369
+ info(`Adding ${features.length} feature module(s)...`),
370
+ traverse_(features, (feature) =>
371
+ template({
372
+ source: path.join(__dirname, "templates", "feature.ts.ejs"),
373
+ dest: path.join(projectDir, "src", `${feature}.ts`),
374
+ vars: { ...vars, featureName: feature },
375
+ }),
376
+ ),
377
+ // Create barrel file (index.ts) with exports for each feature
378
+ // Demonstrates: appendFile for creating/updating barrel files
379
+ info("Creating barrel file (src/index.ts)..."),
380
+ writeFile(
381
+ path.join(projectDir, "src", "index.ts"),
382
+ `// Barrel file - auto-generated by summon webapp\n// Re-exports all modules for easy importing\n\n`,
383
+ ),
384
+ traverse_(features, (feature) =>
385
+ appendFile(
386
+ path.join(projectDir, "src", "index.ts"),
387
+ `export * from "./${feature}.js";\n`,
388
+ ),
389
+ ),
390
+ ]),
391
+ ),
392
+
393
+ // Optional: Styles
394
+ when(
395
+ styling !== "none",
396
+ template({
397
+ source: path.join(__dirname, "templates", "styles.css.ejs"),
398
+ dest: path.join(projectDir, "src", "styles.css"),
399
+ vars,
400
+ }),
401
+ ),
402
+ ]);
403
+
404
+ // =========================================================================
405
+ // Task 4: Generate tests (conditional)
406
+ // Demonstrates: when + parallel for optional features
407
+ // =========================================================================
408
+ const generateTests = when(
409
+ withTests,
410
+ sequence_([
411
+ info("Setting up tests..."),
412
+ parallel([
413
+ template({
414
+ source: path.join(__dirname, "templates", "app.test.ts.ejs"),
415
+ dest: path.join(projectDir, "tests", "app.test.ts"),
416
+ vars,
417
+ }),
418
+ writeFile(
419
+ path.join(projectDir, "vitest.config.ts"),
420
+ `import { defineConfig } from 'vitest/config';
421
+
422
+ export default defineConfig({
423
+ test: {
424
+ environment: '${framework === "react" ? "jsdom" : "node"}',
425
+ include: ['tests/**/*.test.ts'],
426
+ },
427
+ });
428
+ `,
429
+ ),
430
+ ]),
431
+ ]),
432
+ );
433
+
434
+ // =========================================================================
435
+ // Task 5: Generate documentation (conditional)
436
+ // Demonstrates: when + traverse_ for multiple docs
437
+ // =========================================================================
438
+ const generateDocs = when(
439
+ withDocs,
440
+ sequence_([
441
+ info("Generating documentation..."),
442
+ traverse_(
443
+ [
444
+ { name: "README.md", template: "README.md.ejs", dest: "" },
445
+ {
446
+ name: "ARCHITECTURE.md",
447
+ template: "ARCHITECTURE.md.ejs",
448
+ dest: "docs",
449
+ },
450
+ ],
451
+ (doc) =>
452
+ template({
453
+ source: path.join(__dirname, "templates", doc.template),
454
+ dest: path.join(projectDir, doc.dest, doc.name),
455
+ vars,
456
+ }),
457
+ ),
458
+ ]),
459
+ );
460
+
461
+ // =========================================================================
462
+ // Task 6: Install dependencies (conditional)
463
+ // Demonstrates: exec + context reading + warn
464
+ // =========================================================================
465
+ const installDependencies = when(
466
+ installDeps,
467
+ sequence_([
468
+ info("Installing dependencies..."),
469
+ warn("This may take a moment..."),
470
+ flatMap(getContext<string>("projectDir"), (dir) =>
471
+ flatMap(exec("bun", ["install"], dir ?? projectDir), (result) =>
472
+ ifElse(
473
+ result.exitCode === 0,
474
+ info("Dependencies installed successfully!"),
475
+ warn(
476
+ `Installation completed with warnings (exit code: ${result.exitCode})`,
477
+ ),
478
+ ),
479
+ ),
480
+ ),
481
+ ]),
482
+ );
483
+
484
+ // =========================================================================
485
+ // Final: Compose all tasks sequentially
486
+ // =========================================================================
487
+ return sequence_([
488
+ info(`Creating ${name} with ${framework} + ${styling}...`),
489
+ debug(`Features: ${features.join(", ") || "none"}`),
490
+
491
+ createDirectories,
492
+ generateConfigs,
493
+ generateSourceFiles,
494
+ generateTests,
495
+ generateDocs,
496
+ installDependencies,
497
+
498
+ info(`
499
+ Done! Your web app is ready.
500
+
501
+ Next steps:
502
+ cd ${name}
503
+ ${installDeps ? "" : "bun install\n "}bun run dev
504
+ `),
505
+ ]);
506
+ },
507
+ };
508
+
509
+ export default generator;
@@ -0,0 +1,180 @@
1
+ # Architecture: <%= pascalCase(name) %>
2
+
3
+ This document describes the architecture and design decisions for <%= name %>.
4
+
5
+ ## Overview
6
+
7
+ <%= description %>
8
+
9
+ ## Technology Choices
10
+
11
+ ### Framework: <%= framework === 'react' ? 'React' : 'Vanilla TypeScript' %>
12
+
13
+ <% if (framework === 'react') { -%>
14
+ React was chosen for its:
15
+ - Component-based architecture
16
+ - Large ecosystem and community
17
+ - Excellent developer tooling
18
+ - Virtual DOM for efficient updates
19
+ <% } else { -%>
20
+ Vanilla TypeScript was chosen for its:
21
+ - Minimal bundle size
22
+ - No framework overhead
23
+ - Direct DOM manipulation for maximum performance
24
+ - Simplicity and maintainability
25
+ <% } -%>
26
+
27
+ ### Styling: <%= styling === 'tailwind' ? 'Tailwind CSS' : styling === 'css' ? 'Plain CSS' : 'None' %>
28
+
29
+ <% if (styling === 'tailwind') { -%>
30
+ Tailwind CSS provides:
31
+ - Utility-first approach for rapid development
32
+ - Consistent design system
33
+ - Built-in responsive design
34
+ - Small production bundle with PurgeCSS
35
+ <% } else if (styling === 'css') { -%>
36
+ Plain CSS was chosen for:
37
+ - No build-time dependencies
38
+ - Full control over styling
39
+ - Browser-native cascade and specificity
40
+ - Easy debugging
41
+ <% } -%>
42
+
43
+ ## Directory Structure
44
+
45
+ ```
46
+ <%= name %>/
47
+ <%= '├── ' %>src/ # Source code
48
+ <%= '│ ├── ' %>main.<%= framework === 'react' ? 'tsx' : 'ts' %> # Application entry point
49
+ <%= '│ ├── ' %><%= framework === 'react' ? 'App.tsx' : 'app.ts' %> # Root component/module
50
+ <% if (styling !== 'none') { -%>
51
+ <%= '│ ├── ' %>styles.css # Global styles
52
+ <% } -%>
53
+ <% if (hasRouter) { -%>
54
+ <%= '│ ├── ' %>router.ts # Routing configuration
55
+ <% } -%>
56
+ <% if (hasState) { -%>
57
+ <%= '│ ├── ' %>state.ts # State management
58
+ <% } -%>
59
+ <% if (hasApi) { -%>
60
+ <%= '│ ├── ' %>api.ts # API client
61
+ <% } -%>
62
+ <% if (hasLogging) { -%>
63
+ <%= '│ ├── ' %>logging.ts # Logging utilities
64
+ <% } -%>
65
+ <%= '│ └── ' %>components/ # Reusable components
66
+ <%= '├── ' %>public/ # Static assets
67
+ <% if (withTests) { -%>
68
+ <%= '├── ' %>tests/ # Test files
69
+ <% } -%>
70
+ <%= '└── ' %>docs/ # Documentation
71
+ ```
72
+
73
+ <% if (features.length > 0) { -%>
74
+ ## Feature Modules
75
+
76
+ <% if (hasRouter) { -%>
77
+ ### Router
78
+
79
+ <% if (framework === 'react') { -%>
80
+ Client-side routing using React Router v6:
81
+ - Declarative route configuration
82
+ - Nested routes support
83
+ - URL parameters and query strings
84
+ - Programmatic navigation
85
+ <% } else { -%>
86
+ Custom client-side router:
87
+ - Hash-based or history-based routing
88
+ - Route parameters extraction
89
+ - Middleware support
90
+ - Navigation guards
91
+ <% } -%>
92
+
93
+ <% } -%>
94
+ <% if (hasState) { -%>
95
+ ### State Management
96
+
97
+ <% if (framework === 'react') { -%>
98
+ Using Zustand for state management:
99
+ - Minimal boilerplate
100
+ - TypeScript-first design
101
+ - No providers required
102
+ - Selective subscriptions for performance
103
+ <% } else { -%>
104
+ Custom pub/sub store:
105
+ - Simple and predictable
106
+ - Type-safe state updates
107
+ - Subscription-based reactivity
108
+ - No external dependencies
109
+ <% } -%>
110
+
111
+ <% } -%>
112
+ <% if (hasApi) { -%>
113
+ ### API Client
114
+
115
+ Using ky for HTTP requests:
116
+ - Fetch-based, modern API
117
+ - Automatic retries
118
+ - Request/response hooks
119
+ - TypeScript generics for responses
120
+
121
+ <% } -%>
122
+ <% if (hasLogging) { -%>
123
+ ### Logging
124
+
125
+ Custom logging utility:
126
+ - Multiple log levels (debug, info, warn, error)
127
+ - Log history for debugging
128
+ - Configurable minimum level
129
+ - Context data support
130
+
131
+ <% } -%>
132
+ <% } -%>
133
+ ## Data Flow
134
+
135
+ ```
136
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
137
+ │ User │────▶│ UI │────▶│ State │
138
+ │ Interaction│ │ Components │ │ Management │
139
+ └─────────────┘ └─────────────┘ └─────────────┘
140
+ │ │
141
+ ▼ ▼
142
+ ┌─────────────┐ ┌─────────────┐
143
+ │ Router │ │ API Layer │
144
+ │ (optional) │ │ (optional) │
145
+ └─────────────┘ └─────────────┘
146
+ ```
147
+
148
+ ## Configuration
149
+
150
+ | Variable | Description | Default |
151
+ |----------|-------------|---------|
152
+ <% if (hasApi) { -%>
153
+ | `VITE_API_URL` | API base URL | `https://api.example.com` |
154
+ <% } -%>
155
+ | `NODE_ENV` | Environment mode | `development` |
156
+
157
+ ## Performance Considerations
158
+
159
+ <% if (framework === 'react') { -%>
160
+ 1. **Component Memoization**: Use `React.memo` for expensive renders
161
+ 2. **State Selectors**: Use selective subscriptions to avoid unnecessary re-renders
162
+ 3. **Code Splitting**: Lazy load routes and heavy components
163
+ 4. **Bundle Analysis**: Regularly analyze bundle size
164
+ <% } else { -%>
165
+ 1. **DOM Updates**: Batch DOM updates for better performance
166
+ 2. **Event Delegation**: Use event delegation for dynamic content
167
+ 3. **Debouncing**: Debounce expensive operations
168
+ 4. **Virtual Scrolling**: For large lists, consider virtualization
169
+ <% } -%>
170
+
171
+ ## Security
172
+
173
+ 1. **Input Validation**: Always validate and sanitize user input
174
+ 2. **XSS Prevention**: Escape HTML output, use CSP headers
175
+ 3. **CORS**: Configure proper CORS settings on the API
176
+ 4. **Authentication**: Store tokens securely, use HttpOnly cookies when possible
177
+
178
+ ---
179
+
180
+ Generated by [Summon](https://github.com/canonical/pragma)