@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,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)
|