@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
package/README.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Summon
|
|
2
|
+
|
|
3
|
+
A code generator framework where generators are pure functions that return data, not side effects.
|
|
4
|
+
|
|
5
|
+
Write a generator once. Run it for real. Preview with `--dry-run`. Test without mocks. Same code, different interpreters.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
generate: (answers) => writeFile(`src/${answers.name}.ts`, code)
|
|
9
|
+
// Returns a Task describing "write this file"
|
|
10
|
+
// Not a file write. Data.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @canonical/summon
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Using Generators
|
|
20
|
+
|
|
21
|
+
Summon discovers generators from installed packages automatically.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# See what's available
|
|
25
|
+
summon
|
|
26
|
+
|
|
27
|
+
# Run a generator interactively
|
|
28
|
+
summon component react
|
|
29
|
+
|
|
30
|
+
# Pass answers as positional argument (when supported)
|
|
31
|
+
summon component react src/components/Button
|
|
32
|
+
|
|
33
|
+
# Or as a flag
|
|
34
|
+
summon component react --component-path=src/components/Button
|
|
35
|
+
|
|
36
|
+
# Preview first (nothing written to disk)
|
|
37
|
+
summon component react src/components/Button --dry-run
|
|
38
|
+
|
|
39
|
+
# Show debug output
|
|
40
|
+
summon component react src/components/Button --verbose
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every prompt becomes a CLI flag. Boolean prompts with `default: true` use the `--no-` prefix to disable. Generators may also define a **positional argument** for their primary input (like a path), allowing cleaner command invocations.
|
|
44
|
+
|
|
45
|
+
### CLI Options
|
|
46
|
+
|
|
47
|
+
| Flag | Description |
|
|
48
|
+
|------|-------------|
|
|
49
|
+
| `-d, --dry-run` | Preview without writing files |
|
|
50
|
+
| `-y, --yes` | Skip confirmation prompts |
|
|
51
|
+
| `-v, --verbose` | Show debug output |
|
|
52
|
+
| `--no-preview` | Skip the file preview |
|
|
53
|
+
| `--no-generated-stamp` | Disable generated file stamp comments |
|
|
54
|
+
|
|
55
|
+
### Shell Autocompletion
|
|
56
|
+
|
|
57
|
+
Summon supports TAB completion for Bash, Zsh, and Fish shells. Completions are **dynamic** - they automatically detect newly installed generator packages without needing to re-run setup.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Automatic setup (recommended)
|
|
61
|
+
summon --setup-completion
|
|
62
|
+
|
|
63
|
+
# Or manual installation for each shell:
|
|
64
|
+
|
|
65
|
+
# Zsh
|
|
66
|
+
echo '. <(summon --completion)' >> ~/.zshrc
|
|
67
|
+
|
|
68
|
+
# Bash (may need: brew install bash-completion on macOS)
|
|
69
|
+
summon --completion >> ~/.summon-completion.sh
|
|
70
|
+
echo 'source ~/.summon-completion.sh' >> ~/.bash_profile
|
|
71
|
+
|
|
72
|
+
# Fish
|
|
73
|
+
echo 'summon --completion-fish | source' >> ~/.config/fish/config.fish
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
After setup, restart your shell or source the config file. Then:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
summon <TAB> # Shows: component, init, ...
|
|
80
|
+
summon component <TAB> # Shows: react, svelte
|
|
81
|
+
summon component react <TAB> # Shows: --component-path, --no-with-styles, ...
|
|
82
|
+
summon component react --comp<TAB> # Completes to: --component-path
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Completion features:
|
|
86
|
+
- **Generator names** - Navigate the command tree with TAB
|
|
87
|
+
- **Generator flags** - All prompts become completable flags
|
|
88
|
+
- **Confirm prompts** - Shows `--no-X` for prompts with `default: true`
|
|
89
|
+
- **Select/multiselect** - Completes with available choices
|
|
90
|
+
- **Path prompts** - Filesystem path completion for prompts containing "path", "dir", "file", etc.
|
|
91
|
+
|
|
92
|
+
To remove autocompletion:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
summon --cleanup-completion
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Positional Arguments
|
|
99
|
+
|
|
100
|
+
Generators can define one prompt as a **positional argument**, allowing users to provide the primary input without a flag:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# With positional argument support
|
|
104
|
+
summon component react src/components/Button
|
|
105
|
+
|
|
106
|
+
# Equivalent to
|
|
107
|
+
summon component react --component-path=src/components/Button
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Positional arguments also get filesystem path completion when using TAB:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
summon component react src/comp<TAB> # Completes to: src/components/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Installing Generator Packages
|
|
117
|
+
|
|
118
|
+
Generator packages follow the naming convention `summon-*` or `@scope/summon-*`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Install a generator package
|
|
122
|
+
bun add @canonical/summon-component
|
|
123
|
+
|
|
124
|
+
# Now available (completions work immediately!)
|
|
125
|
+
summon component react
|
|
126
|
+
summon component svelte
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Creating Generators
|
|
130
|
+
|
|
131
|
+
A generator is a pure function that takes answers and returns a `Task`—a data structure describing what to do.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// src/module/types.ts
|
|
135
|
+
interface ModuleAnswers {
|
|
136
|
+
name: string;
|
|
137
|
+
withTests: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/module/generator.ts
|
|
141
|
+
import type { GeneratorDefinition } from "@canonical/summon";
|
|
142
|
+
import { debug, info, writeFile, mkdir, sequence_, when } from "@canonical/summon";
|
|
143
|
+
import type { ModuleAnswers } from "./types.js";
|
|
144
|
+
|
|
145
|
+
export const generator = {
|
|
146
|
+
meta: {
|
|
147
|
+
name: "module",
|
|
148
|
+
description: "Creates a new module",
|
|
149
|
+
version: "0.1.0",
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
prompts: [
|
|
153
|
+
// positional: true allows `summon module src/utils` instead of `--name=src/utils`
|
|
154
|
+
{ name: "name", type: "text", message: "Module name:", positional: true },
|
|
155
|
+
{ name: "withTests", type: "confirm", message: "Include tests?", default: true },
|
|
156
|
+
],
|
|
157
|
+
|
|
158
|
+
generate: (answers) => sequence_([
|
|
159
|
+
info(`Creating module: ${answers.name}`),
|
|
160
|
+
|
|
161
|
+
debug("Creating module directory"),
|
|
162
|
+
mkdir(`src/${answers.name}`),
|
|
163
|
+
|
|
164
|
+
debug("Creating index file"),
|
|
165
|
+
writeFile(`src/${answers.name}/index.ts`, `export const ${answers.name} = {};\n`),
|
|
166
|
+
|
|
167
|
+
when(answers.withTests, debug("Creating test file")),
|
|
168
|
+
when(answers.withTests,
|
|
169
|
+
writeFile(`src/${answers.name}/index.test.ts`, `test("works", () => {});\n`)
|
|
170
|
+
),
|
|
171
|
+
|
|
172
|
+
info(`Created module at src/${answers.name}`),
|
|
173
|
+
]),
|
|
174
|
+
} as const satisfies GeneratorDefinition<ModuleAnswers>;
|
|
175
|
+
|
|
176
|
+
// src/module/index.ts (barrel)
|
|
177
|
+
export { generator } from "./generator.js";
|
|
178
|
+
export type { ModuleAnswers } from "./types.js";
|
|
179
|
+
|
|
180
|
+
// src/index.ts (package barrel)
|
|
181
|
+
import type { AnyGenerator } from "@canonical/summon";
|
|
182
|
+
import { generator as moduleGenerator } from "./module/index.js";
|
|
183
|
+
|
|
184
|
+
export const generators = {
|
|
185
|
+
"module": moduleGenerator,
|
|
186
|
+
} as const satisfies Record<string, AnyGenerator>;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Package Structure
|
|
190
|
+
|
|
191
|
+
Each generator should be split into three files for maintainability:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
my-summon-package/
|
|
195
|
+
├── package.json
|
|
196
|
+
├── src/
|
|
197
|
+
│ ├── index.ts # Package barrel - exports generators record
|
|
198
|
+
│ ├── module/
|
|
199
|
+
│ │ ├── index.ts # Generator barrel
|
|
200
|
+
│ │ ├── types.ts # Answer types
|
|
201
|
+
│ │ └── generator.ts # Generator definition
|
|
202
|
+
│ └── templates/ # EJS templates (optional)
|
|
203
|
+
└── README.md
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"name": "@myorg/summon-module",
|
|
209
|
+
"main": "src/index.ts",
|
|
210
|
+
"peerDependencies": {
|
|
211
|
+
"@canonical/summon": "workspace:*"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Local Development
|
|
217
|
+
|
|
218
|
+
For developing generators locally, link them to make them globally available:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# From your generator package directory
|
|
222
|
+
cd /path/to/my-summon-package
|
|
223
|
+
bun link # for bun
|
|
224
|
+
npm link # for npm
|
|
225
|
+
|
|
226
|
+
# Now available everywhere
|
|
227
|
+
summon module src/utils
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Project-local packages (in `./node_modules`) take precedence over globally linked ones, so you can override with project-specific versions.
|
|
231
|
+
|
|
232
|
+
### Testing Generators
|
|
233
|
+
|
|
234
|
+
Because generators return data, testing is straightforward—no mocks needed:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { dryRun, getAffectedFiles } from "@canonical/summon";
|
|
238
|
+
import { generators } from "./index";
|
|
239
|
+
|
|
240
|
+
const generator = generators["module"];
|
|
241
|
+
|
|
242
|
+
test("creates expected files", () => {
|
|
243
|
+
const task = generator.generate({ name: "utils", withTests: true });
|
|
244
|
+
const { effects } = dryRun(task);
|
|
245
|
+
|
|
246
|
+
expect(getAffectedFiles(effects)).toEqual([
|
|
247
|
+
"src/utils/index.ts",
|
|
248
|
+
"src/utils/index.test.ts",
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("skips test file when disabled", () => {
|
|
253
|
+
const task = generator.generate({ name: "utils", withTests: false });
|
|
254
|
+
const { effects } = dryRun(task);
|
|
255
|
+
|
|
256
|
+
expect(getAffectedFiles(effects)).not.toContain("src/utils/index.test.ts");
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The dry-run interpreter maintains a virtual filesystem, so conditional logic based on `exists()` works correctly even without touching the disk.
|
|
261
|
+
|
|
262
|
+
## The Monadic Pattern
|
|
263
|
+
|
|
264
|
+
Summon uses a **monad** to compose tasks. If you've used Promises, you already understand the core idea.
|
|
265
|
+
|
|
266
|
+
### What's a Monad?
|
|
267
|
+
|
|
268
|
+
A monad is a design pattern for chaining operations that have some context (like "might fail" or "has effects"). Think of it as a pipeline where each step can:
|
|
269
|
+
|
|
270
|
+
1. **Transform values** (`map`) — Apply a function to the result
|
|
271
|
+
2. **Chain operations** (`flatMap`) — Use the result to create the next step
|
|
272
|
+
3. **Short-circuit on failure** — Errors propagate automatically
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Promise (async context)
|
|
276
|
+
fetchUser(id)
|
|
277
|
+
.then(user => fetchOrders(user.id)) // chain
|
|
278
|
+
.then(orders => orders.length) // transform
|
|
279
|
+
.catch(handleError); // recover
|
|
280
|
+
|
|
281
|
+
// Task (effect context)
|
|
282
|
+
task(readFile("config.json"))
|
|
283
|
+
.map(content => JSON.parse(content)) // transform
|
|
284
|
+
.flatMap(config => writeFile( // chain
|
|
285
|
+
config.outputPath,
|
|
286
|
+
generateCode(config)
|
|
287
|
+
))
|
|
288
|
+
.recover(err => pure(defaultConfig)); // recover
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### The Task Type
|
|
292
|
+
|
|
293
|
+
`Task<A>` represents a computation that:
|
|
294
|
+
- Describes effects (file I/O, shell commands, etc.)
|
|
295
|
+
- Eventually produces a value of type `A`
|
|
296
|
+
- May fail with an error
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// A Task is one of three things:
|
|
300
|
+
type Task<A> =
|
|
301
|
+
| { _tag: "Pure"; value: A } // Done, here's the value
|
|
302
|
+
| { _tag: "Fail"; error: TaskError } // Failed with error
|
|
303
|
+
| { _tag: "Effect"; effect: Effect; cont: ... } // Do this effect, then continue
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Composing Tasks
|
|
307
|
+
|
|
308
|
+
The `TaskBuilder` provides a fluent API for composition:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { task, pure } from "@canonical/summon";
|
|
312
|
+
|
|
313
|
+
// Build a pipeline
|
|
314
|
+
const pipeline = task(readFile("input.txt"))
|
|
315
|
+
.map(content => content.toUpperCase()) // Transform the value
|
|
316
|
+
.flatMap(upper => writeFile("output.txt", upper)) // Chain another effect
|
|
317
|
+
.andThen(info("Done!")) // Sequence (ignore previous value)
|
|
318
|
+
.recover(err => pure(void 0)); // Handle errors
|
|
319
|
+
|
|
320
|
+
// pipeline is still just data — nothing has executed yet
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Key Operations
|
|
324
|
+
|
|
325
|
+
| Operation | Description | Example |
|
|
326
|
+
|-----------|-------------|---------|
|
|
327
|
+
| `pure(value)` | Wrap a value in a Task | `pure(42)` |
|
|
328
|
+
| `map(fn)` | Transform the result | `.map(x => x * 2)` |
|
|
329
|
+
| `flatMap(fn)` | Chain with another Task | `.flatMap(x => writeFile(...))` |
|
|
330
|
+
| `andThen(task)` | Sequence, discard previous | `.andThen(info("next"))` |
|
|
331
|
+
| `recover(fn)` | Handle errors | `.recover(e => pure(default))` |
|
|
332
|
+
| `tap(fn)` | Side effect, keep value | `.tap(x => debug(x))` |
|
|
333
|
+
|
|
334
|
+
### Why Monads for Generators?
|
|
335
|
+
|
|
336
|
+
1. **Composable** — Small tasks combine into complex workflows
|
|
337
|
+
2. **Predictable** — Errors propagate without explicit handling at each step
|
|
338
|
+
3. **Testable** — The pipeline is data; inspect it without running effects
|
|
339
|
+
4. **Declarative** — Describe *what* to do, not *how* to do it
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// Complex workflow, reads like a recipe
|
|
343
|
+
const scaffoldFeature = (name: string) =>
|
|
344
|
+
task(mkdir(`src/features/${name}`))
|
|
345
|
+
.andThen(template({
|
|
346
|
+
source: "templates/feature.ts.ejs",
|
|
347
|
+
dest: `src/features/${name}/index.ts`,
|
|
348
|
+
vars: { name },
|
|
349
|
+
}))
|
|
350
|
+
.andThen(when(config.withTests,
|
|
351
|
+
template({
|
|
352
|
+
source: "templates/test.ts.ejs",
|
|
353
|
+
dest: `src/features/${name}/index.test.ts`,
|
|
354
|
+
vars: { name },
|
|
355
|
+
})
|
|
356
|
+
))
|
|
357
|
+
.andThen(info(`Created feature: ${name}`))
|
|
358
|
+
.unwrap();
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
For a deeper dive into the "effects as data" philosophy, see [Explanation](docs/explanation.md).
|
|
362
|
+
|
|
363
|
+
## Templating Engine
|
|
364
|
+
|
|
365
|
+
Summon uses EJS by default for template rendering, but supports custom templating engines via the `TemplatingEngine` interface.
|
|
366
|
+
|
|
367
|
+
### Default (EJS)
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { template, templateDir } from "@canonical/summon";
|
|
371
|
+
|
|
372
|
+
// Uses EJS by default
|
|
373
|
+
template({
|
|
374
|
+
source: "templates/component.tsx.ejs",
|
|
375
|
+
dest: "src/components/<%= name %>.tsx",
|
|
376
|
+
vars: { name: "Button" },
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Custom Engines
|
|
381
|
+
|
|
382
|
+
Implement `TemplatingEngine` to use Handlebars, Mustache, Nunjucks, or any other engine:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import type { TemplatingEngine } from "@canonical/summon";
|
|
386
|
+
import Handlebars from "handlebars";
|
|
387
|
+
import * as fs from "node:fs/promises";
|
|
388
|
+
|
|
389
|
+
const handlebarsEngine: TemplatingEngine = {
|
|
390
|
+
render(template, vars) {
|
|
391
|
+
return Handlebars.compile(template)(vars);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
async renderAsync(template, vars) {
|
|
395
|
+
return Handlebars.compile(template)(vars);
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
async renderFile(templatePath, vars) {
|
|
399
|
+
const content = await fs.readFile(templatePath, "utf-8");
|
|
400
|
+
return Handlebars.compile(content)(vars);
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Use in templates
|
|
405
|
+
template({
|
|
406
|
+
source: "templates/component.hbs",
|
|
407
|
+
dest: "src/components/{{name}}.tsx",
|
|
408
|
+
vars: { name: "Button" },
|
|
409
|
+
engine: handlebarsEngine,
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Interface
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
interface TemplatingEngine {
|
|
417
|
+
/** Render a template string synchronously */
|
|
418
|
+
render(template: string, vars: Record<string, unknown>): string;
|
|
419
|
+
|
|
420
|
+
/** Render a template string asynchronously */
|
|
421
|
+
renderAsync(template: string, vars: Record<string, unknown>): Promise<string>;
|
|
422
|
+
|
|
423
|
+
/** Render a template file asynchronously */
|
|
424
|
+
renderFile(templatePath: string, vars: Record<string, unknown>): Promise<string>;
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The `engine` option is available on `template()`, `templateDir()`, `renderString()`, `renderStringAsync()`, and `renderFile()`.
|
|
429
|
+
|
|
430
|
+
## Documentation
|
|
431
|
+
|
|
432
|
+
- **[Tutorial](docs/tutorial.md)** — Build your first generator from scratch
|
|
433
|
+
- **[How-To Guides](docs/how-to.md)** — Solve specific problems
|
|
434
|
+
- **[Reference](docs/reference.md)** — Complete API documentation
|
|
435
|
+
- **[Explanation](docs/explanation.md)** — Why effects as data? The ideas behind Summon
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
GPL-3.0
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hello World Generator (Demo)
|
|
3
|
+
*
|
|
4
|
+
* A simple example generator that demonstrates the core features of Summon,
|
|
5
|
+
* including EJS templates and the withHelpers utility.
|
|
6
|
+
*
|
|
7
|
+
* Use this as a reference when building your own generators.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
info,
|
|
13
|
+
mkdir,
|
|
14
|
+
sequence_,
|
|
15
|
+
template,
|
|
16
|
+
when,
|
|
17
|
+
withHelpers,
|
|
18
|
+
} from "../../../src/index.js";
|
|
19
|
+
import type { GeneratorDefinition } from "../../../src/types.js";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
interface HelloAnswers {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
greeting: string;
|
|
29
|
+
withReadme: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Generator Definition
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
export const generator: GeneratorDefinition<HelloAnswers> = {
|
|
37
|
+
meta: {
|
|
38
|
+
name: "hello",
|
|
39
|
+
description: "A demo generator that creates a simple hello world project",
|
|
40
|
+
version: "0.1.0",
|
|
41
|
+
help: `This generator demonstrates the core features of Summon:
|
|
42
|
+
|
|
43
|
+
- EJS templates for dynamic file generation
|
|
44
|
+
- The withHelpers utility for case transformations (camelCase, pascalCase, etc.)
|
|
45
|
+
- Conditional file generation with the 'when' combinator
|
|
46
|
+
- Sequential task composition with 'sequence_' or fluent '.then()' chains
|
|
47
|
+
|
|
48
|
+
Use this generator as a reference when building your own generators.`,
|
|
49
|
+
examples: [
|
|
50
|
+
// Zero-config: interactive prompts with defaults
|
|
51
|
+
"summon hello",
|
|
52
|
+
// Minimal: just the name
|
|
53
|
+
"summon hello --name=my-app",
|
|
54
|
+
// Partial: customize greeting
|
|
55
|
+
"summon hello --name=my-app --greeting=Hey",
|
|
56
|
+
// Skip optional file
|
|
57
|
+
"summon hello --name=my-app --no-withReadme",
|
|
58
|
+
// Preview without writing
|
|
59
|
+
"summon hello --dry-run",
|
|
60
|
+
// Full non-interactive
|
|
61
|
+
"summon hello --name=demo --greeting=Hello --description='A demo' --yes",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
prompts: [
|
|
66
|
+
{
|
|
67
|
+
name: "name",
|
|
68
|
+
type: "text",
|
|
69
|
+
message: "Project name:",
|
|
70
|
+
default: "my-project",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "description",
|
|
74
|
+
type: "text",
|
|
75
|
+
message: "Description (optional):",
|
|
76
|
+
default: "",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "greeting",
|
|
80
|
+
type: "select",
|
|
81
|
+
message: "Choose a greeting:",
|
|
82
|
+
choices: [
|
|
83
|
+
{ label: "Hello", value: "Hello" },
|
|
84
|
+
{ label: "Hi", value: "Hi" },
|
|
85
|
+
{ label: "Hey", value: "Hey" },
|
|
86
|
+
{ label: "Greetings", value: "Greetings" },
|
|
87
|
+
],
|
|
88
|
+
default: "Hello",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "withReadme",
|
|
92
|
+
type: "confirm",
|
|
93
|
+
message: "Include README.md?",
|
|
94
|
+
default: true,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
generate: (answers) => {
|
|
99
|
+
const { name, withReadme } = answers;
|
|
100
|
+
|
|
101
|
+
// withHelpers adds camelCase, kebabCase, pascalCase, etc. to the template vars
|
|
102
|
+
const vars = withHelpers(answers);
|
|
103
|
+
|
|
104
|
+
return sequence_([
|
|
105
|
+
info(`Creating ${name}...`),
|
|
106
|
+
|
|
107
|
+
// Create project directory
|
|
108
|
+
mkdir(name),
|
|
109
|
+
|
|
110
|
+
// Create main file using EJS template
|
|
111
|
+
template({
|
|
112
|
+
source: path.join(__dirname, "templates", "index.ts.ejs"),
|
|
113
|
+
dest: path.join(name, "index.ts"),
|
|
114
|
+
vars,
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
// Conditionally create README using EJS template
|
|
118
|
+
when(
|
|
119
|
+
withReadme,
|
|
120
|
+
template({
|
|
121
|
+
source: path.join(__dirname, "templates", "README.md.ejs"),
|
|
122
|
+
dest: path.join(name, "README.md"),
|
|
123
|
+
vars,
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
info(`Done! Run: cd ${name} && bun run index.ts`),
|
|
128
|
+
]);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default generator;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# <%= name %>
|
|
2
|
+
|
|
3
|
+
<%= greeting %>! This project was generated by `summon hello`.
|
|
4
|
+
|
|
5
|
+
<% if (description) { %>
|
|
6
|
+
## About
|
|
7
|
+
|
|
8
|
+
<%= description %>
|
|
9
|
+
|
|
10
|
+
<% } %>
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun run index.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## About Summon
|
|
18
|
+
|
|
19
|
+
Summon is a monadic task-centric code generator framework.
|
|
20
|
+
Learn more at: https://github.com/canonical/pragma
|