Skip to content

Commit a1de3b8

Browse files
hfurkanbozkurtH. Furkan Bozkurt
andauthored
feat(cli): --help now lists every template with a one-line description (#146)
Co-authored-by: H. Furkan Bozkurt <bozkurh@amazon.com>
1 parent a867fb1 commit a1de3b8

13 files changed

Lines changed: 227 additions & 48 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@aws-blocks/create-blocks-app": patch
3+
---
4+
5+
Fix + polish for `create-blocks-app`:
6+
7+
- Reject `--template amplify` on a fresh directory up front with a
8+
helpful message pointing at `--template default` (for a fresh app)
9+
or the no-arg command (to integrate Blocks into an existing Amplify
10+
Gen 2 project). The `amplify` template is an overlay auto-selected
11+
when the CLI detects `amplify/backend.ts`, not a scaffoldable
12+
starter. Previously the fresh-scaffold path with `--template amplify`
13+
crashed mid-copy on missing template files.
14+
- Correct two template descriptions that misstated the frontend:
15+
`auth-cognito` and `demo` were labeled "Vite + lit-html" but both
16+
ship a Vite + vanilla-DOM frontend. Descriptions now read
17+
"Vite + vanilla-DOM frontend with Cognito passwordless email-OTP
18+
auth end-to-end" and "Fuller example — AuthBasic + KVStore +
19+
DynamoDB priority-sorted todo (Vite, vanilla-DOM)" respectively.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@aws-blocks/create-blocks-app": patch
3+
---
4+
5+
Improve `--help` output for `create-blocks-app`:
6+
7+
- Template discovery is now filesystem-driven — drop a folder under `templates/` with a `package.json` and it auto-registers.
8+
- `--help` now lists every template with a one-line description, sourced from each template's `blocksTemplateDescription` field.
9+
- Added `--yes --template nextjs` to the Examples section.

packages/create-blocks-app/README.md

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@ npx @aws-blocks/create-blocks-app my-app
2020
npx @aws-blocks/create-blocks-app my-app --template react
2121
```
2222

23-
Available templates:
24-
- **default** - Real-time todo app with auth, data, and live sync
25-
- **bare** - Minimal Blocks setup
26-
- **react** - React frontend with Blocks backend
27-
- **backend** - Backend-only (no frontend)
28-
- **nextjs** - Next.js integration
29-
- **auth-cognito** - Cognito authentication example
30-
- **amplify** - Amplify Gen 2 integration
31-
- **demo** - Demo application
23+
For the full list of templates and one-line descriptions, run:
24+
25+
```bash
26+
npx @aws-blocks/create-blocks-app --help
27+
```
28+
29+
Template descriptions are sourced from each template's `package.json` (`blocksTemplateDescription`) so the CLI is always the source of truth.
3230

3331
### Add to Existing Project
3432

@@ -135,33 +133,13 @@ export const blocksStack = await BlocksStack.create(app, stackName, {
135133

136134
## Templates
137135

138-
### Default
139-
Real-time todo application with:
140-
- User authentication (AuthBasic)
141-
- Todo storage (DistributedTable / DynamoDB)
142-
- Optimistic locking
143-
- Real-time sync (Realtime / WebSocket)
144-
145-
### Bare
146-
Minimal setup demonstrating core concepts.
147-
148-
### React
149-
React frontend with Blocks backend.
136+
Available templates: `default`, `bare`, `react`, `backend`, `nextjs`, `auth-cognito`, `amplify`, `demo`.
150137

151-
### Backend
152-
Backend-only template (no frontend bundling).
138+
Every folder under `templates/` is a self-registering template. Each `package.json` in that folder sets a `blocksTemplateDescription` field, which the CLI reads to build the `--help` catalog. To see the current list of templates and their one-line descriptions:
153139

154-
### Next.js
155-
Next.js integration with Blocks backend.
156-
157-
### Auth-Cognito
158-
Cognito authentication example.
159-
160-
### Amplify
161-
Amplify Gen 2 integration template.
162-
163-
### Demo
164-
Demo application showcasing Building Blocks.
140+
```bash
141+
npx @aws-blocks/create-blocks-app --help
142+
```
165143

166144
## Related Packages
167145

packages/create-blocks-app/src/cli.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ describe('create-blocks-app CLI argument parsing', () => {
4545
assert.match(result.stdout, /Usage: create-blocks-app/);
4646
});
4747

48+
it('--help includes template descriptions', () => {
49+
const result = run(['--help']);
50+
assert.strictEqual(result.exitCode, 0);
51+
assert.match(
52+
result.stdout,
53+
/default\s+Vite \+ lit-html frontend with auth/,
54+
'--help should list the default template with its blocksTemplateDescription',
55+
);
56+
});
57+
4858
it('unknown flag exits 1 with error message', () => {
4959
const result = run(['--foo']);
5060
assert.strictEqual(result.exitCode, 1);
@@ -80,6 +90,35 @@ describe('create-blocks-app CLI argument parsing', () => {
8090
assert.doesNotMatch(result.stderr, /ENOENT/);
8191
});
8292

93+
it('a template folder missing package.json is rejected as unknown, not crashed with ENOENT', () => {
94+
// Latent-bug guard: a folder under templates/ with no package.json is shown
95+
// in --help but excluded from validation, so selecting it fails cleanly
96+
// instead of throwing ENOENT on the later template version read.
97+
const brokenDir = join(__dirname, '../templates', '__broken_test_template__');
98+
mkdirSync(brokenDir, { recursive: true });
99+
try {
100+
const result = run(['my-app', '--template', '__broken_test_template__', '--skip-install']);
101+
assert.strictEqual(result.exitCode, 1);
102+
assert.match(result.stderr, /Unknown template "__broken_test_template__"/);
103+
assert.doesNotMatch(result.stderr, /ENOENT/);
104+
} finally {
105+
rmSync(brokenDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
106+
}
107+
});
108+
109+
it('--template amplify on a fresh dir exits 1 with a helpful message', () => {
110+
const tmpDir = mkdtempSync(join(tmpdir(), 'create-blocks-app-amplify-fresh-'));
111+
try {
112+
const result = run([tmpDir, '--template', 'amplify', '--skip-install', '-y']);
113+
assert.strictEqual(result.exitCode, 1);
114+
assert.match(result.stderr, /amplify.*auto-selected/i);
115+
assert.match(result.stderr, /--template default/);
116+
assert.doesNotMatch(result.stderr, /ENOENT/);
117+
} finally {
118+
rmSync(tmpDir, { recursive: true, force: true });
119+
}
120+
});
121+
83122
it('multiple positional args exits 1 with error message', () => {
84123
const result = run(['my-app', 'extra-arg']);
85124
assert.strictEqual(result.exitCode, 1);

packages/create-blocks-app/src/index.ts

Lines changed: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -185,20 +185,123 @@ async function addBlocksWorkspace(targetDir: string, options: {
185185
}
186186
}
187187
188-
const AVAILABLE_TEMPLATES = ['default', 'bare', 'react', 'backend', 'nextjs', 'auth-cognito', 'amplify', 'demo'];
188+
// ─── Template discovery ─────────────────────────────────────────────────────
189+
//
190+
// Templates are the ONLY source of truth for the template list. Each template
191+
// under `templates/<name>/` ships a `package.json` with:
192+
// {
193+
// "blocksTemplate": "<name>",
194+
// "blocksTemplateDescription": "One-line description shown in --help"
195+
// }
196+
//
197+
// Adding a new template = drop a folder with a package.json containing those
198+
// two fields. The CLI's --help, validation, and error messages all pick it up
199+
// automatically — no other code changes required.
200+
//
201+
// An optional `blocksTemplateOverlayOnly: true` marks a template as an overlay
202+
// that's auto-selected when the CLI detects a matching existing project (e.g.
203+
// `amplify`) rather than a scaffoldable fresh-app starter; `createFreshProject`
204+
// rejects it up front. A folder whose package.json can't be read is still shown
205+
// in --help (flagged) but excluded from `--template` validation.
206+
//
207+
// Display order below is preserved from the previous hardcoded list so that
208+
// `--help` reads in the intended progression (default → bare → richer variants
209+
// → framework variants → overlays → demos). New templates default to
210+
// alphabetical after known ones. Update this array to reorder.
211+
212+
type TemplateInfo = {
213+
name: string;
214+
description: string;
215+
valid: boolean;
216+
overlayOnly: boolean;
217+
};
218+
219+
const TEMPLATE_DISPLAY_ORDER = ['default', 'bare', 'react', 'backend', 'nextjs', 'auth-cognito', 'amplify', 'demo'];
220+
221+
let templateCatalogCache: TemplateInfo[] | null = null;
222+
223+
async function loadTemplateCatalog(): Promise<TemplateInfo[]> {
224+
if (templateCatalogCache) return templateCatalogCache;
225+
226+
const templatesDir = join(__dirname, '../templates');
227+
const entries = await readdir(templatesDir, { withFileTypes: true });
228+
const discovered: TemplateInfo[] = [];
229+
230+
for (const entry of entries) {
231+
if (!entry.isDirectory()) continue;
232+
const pkgPath = join(templatesDir, entry.name, 'package.json');
233+
try {
234+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
235+
discovered.push({
236+
name: entry.name,
237+
description: typeof pkg.blocksTemplateDescription === 'string'
238+
? pkg.blocksTemplateDescription
239+
: '(no description — add `blocksTemplateDescription` to this template\'s package.json)',
240+
valid: true,
241+
overlayOnly: pkg.blocksTemplateOverlayOnly === true,
242+
});
243+
} catch {
244+
// Template folder without a readable package.json — surface it so it's
245+
// discoverable, but flag that metadata is missing.
246+
discovered.push({
247+
name: entry.name,
248+
description: '(no package.json — template metadata missing)',
249+
valid: false,
250+
overlayOnly: false,
251+
});
252+
}
253+
}
254+
255+
// Sort by TEMPLATE_DISPLAY_ORDER, then any unknown templates alphabetically.
256+
discovered.sort((a, b) => {
257+
const aIdx = TEMPLATE_DISPLAY_ORDER.indexOf(a.name);
258+
const bIdx = TEMPLATE_DISPLAY_ORDER.indexOf(b.name);
259+
if (aIdx === -1 && bIdx === -1) return a.name.localeCompare(b.name);
260+
if (aIdx === -1) return 1;
261+
if (bIdx === -1) return -1;
262+
return aIdx - bIdx;
263+
});
264+
265+
templateCatalogCache = discovered;
266+
return discovered;
267+
}
189268

190-
function validateTemplateName(templateName: string): void {
191-
if (!AVAILABLE_TEMPLATES.includes(templateName)) {
269+
async function validateTemplateName(templateName: string): Promise<void> {
270+
const catalog = await loadTemplateCatalog();
271+
const names = catalog.filter(t => t.valid).map(t => t.name);
272+
if (!names.includes(templateName)) {
192273
console.error(`Error: Unknown template "${templateName}".`);
193-
console.error(`Available templates: ${AVAILABLE_TEMPLATES.join(', ')}`);
274+
console.error(`Available templates: ${names.join(', ')}`);
194275
process.exit(1);
195276
}
196277
}
197278

198279
// ─── Fresh project creation ──────────────────────────────────────────────────
199280

200281
async function createFreshProject(targetDir: string, templateName: string, skipInstall = false) {
201-
validateTemplateName(templateName);
282+
await validateTemplateName(templateName);
283+
284+
// Overlay templates (e.g. `amplify`, detected via `isAmplifyGen2Project()`)
285+
// are auto-selected when the CLI finds a matching existing project and are
286+
// flagged `blocksTemplateOverlayOnly` in their package.json. They ship only an
287+
// overlay snippet — not a full standalone project (no `src/`, no `index.html`,
288+
// no `gitignore`) — so a fresh scaffold would fail partway through the copy.
289+
// Reject up front. Keying off the catalog flag (not a hardcoded name) means a
290+
// new overlay template needs no change here — just the flag in its package.json.
291+
const overlayEntry = (await loadTemplateCatalog()).find(t => t.name === templateName);
292+
if (overlayEntry?.overlayOnly) {
293+
console.error(`Error: The \`${templateName}\` template is an overlay that's auto-selected`);
294+
console.error('when the CLI detects a compatible existing project. It cannot be');
295+
console.error('scaffolded as a fresh app.');
296+
console.error('');
297+
console.error('To scaffold a fresh Blocks app, choose a different template:');
298+
console.error(' npx @aws-blocks/create-blocks-app my-app --template default');
299+
console.error('');
300+
console.error('To integrate Blocks into an existing project, run this from');
301+
console.error('the project root:');
302+
console.error(' npx @aws-blocks/create-blocks-app');
303+
process.exit(1);
304+
}
202305

203306
// Read template package.json to get template name
204307
const templateDir = join(__dirname, '../templates', templateName);
@@ -556,7 +659,14 @@ async function integrateWithExistingProject(targetDir: string, templateName = 'd
556659

557660
// ─── Main ────────────────────────────────────────────────────────────────────
558661

559-
function printUsage() {
662+
async function printUsage() {
663+
const catalog = await loadTemplateCatalog();
664+
const names = catalog.map(t => t.name);
665+
const widest = catalog.length ? Math.max(...catalog.map(t => t.name.length)) : 0;
666+
const templateList = catalog
667+
.map(t => ` ${t.name.padEnd(widest)} ${t.description}`)
668+
.join('\n');
669+
560670
console.log(`Usage: create-blocks-app [directory] [options]
561671
562672
Create a new AWS Blocks app or add Blocks to an existing project.
@@ -581,15 +691,20 @@ Options:
581691
starter app; when adding to an existing project it
582692
selects which aws-blocks/ workspace to copy, e.g.
583693
"nextjs" for a Next.js dev server (default: "default")
584-
Available templates: ${AVAILABLE_TEMPLATES.join(', ')}
694+
695+
Available templates: ${names.join(', ')}
696+
697+
${templateList}
698+
585699
--skip-install Skip installing dependencies
586700
-y, --yes Skip confirmation prompts
587701
-h, --help Show this help message
588702
589703
Examples:
590-
npx @aws-blocks/create-blocks-app Add Blocks to current project
591-
npx @aws-blocks/create-blocks-app . Add Blocks to current project
592-
npx @aws-blocks/create-blocks-app my-app Create a standalone Blocks starter app
704+
npx @aws-blocks/create-blocks-app Add Blocks to current project
705+
npx @aws-blocks/create-blocks-app . Add Blocks to current project
706+
npx @aws-blocks/create-blocks-app my-app Create a standalone Blocks starter app
707+
npx @aws-blocks/create-blocks-app . --yes --template nextjs Non-interactive scaffold with the Next.js template
593708
`);
594709
}
595710

@@ -602,7 +717,7 @@ async function create() {
602717

603718
for (let i = 0; i < args.length; i++) {
604719
if (args[i] === '--help' || args[i] === '-h') {
605-
printUsage();
720+
await printUsage();
606721
process.exit(0);
607722
} else if (args[i] === '--template') {
608723
const value = args[i + 1];
@@ -633,7 +748,7 @@ async function create() {
633748
}
634749
}
635750

636-
validateTemplateName(templateName);
751+
await validateTemplateName(templateName);
637752

638753
const templatePkgVersion: string = JSON.parse(
639754
await readFile(join(__dirname, '../templates', templateName, 'package.json'), 'utf-8'),
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "blocks-amplify-overlay",
3+
"version": "0.1.0",
4+
"private": true,
5+
"blocksTemplate": "amplify",
6+
"blocksTemplateDescription": "Overlay for an existing Amplify Gen 2 app (auto-selected when detected)",
7+
"blocksTemplateOverlayOnly": true
8+
}

packages/create-blocks-app/templates/auth-cognito/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"version": "0.1.0",
44
"type": "module",
55
"blocksTemplate": "auth-cognito",
6+
"blocksTemplateDescription": "Vite + vanilla-DOM frontend with Cognito passwordless email-OTP auth end-to-end",
67
"engines": {
78
"node": ">=22.0.0"
89
},
9-
"workspaces": ["aws-blocks"],
10+
"workspaces": [
11+
"aws-blocks"
12+
],
1013
"scripts": {
1114
"typecheck": "tsc --noEmit",
1215
"spec": "blocks-generate-spec",

packages/create-blocks-app/templates/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"type": "module",
55
"blocksTemplate": "backend",
6+
"blocksTemplateDescription": "Backend-only — Blocks API + CDK, no frontend (bring your own client)",
67
"engines": {
78
"node": ">=22.0.0"
89
},

packages/create-blocks-app/templates/bare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"type": "module",
55
"blocksTemplate": "bare",
6+
"blocksTemplateDescription": "Vite + lit-html frontend with a single greet() API (smallest starter)",
67
"engines": {
78
"node": ">=22.0.0"
89
},

packages/create-blocks-app/templates/default/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"type": "module",
55
"blocksTemplate": "default",
6+
"blocksTemplateDescription": "Vite + lit-html frontend with auth, DynamoDB, and realtime (safe default)",
67
"engines": {
78
"node": ">=22.0.0"
89
},

0 commit comments

Comments
 (0)