Skip to content

Commit d8473ff

Browse files
authored
Merge pull request #10 from hyperweb-io/cga
post create-gen-app from hyperweb-io/inquirerer to hyperweb-io/dev-utils
2 parents ac1f58b + aa61fc8 commit d8473ff

27 files changed

+4385
-5278
lines changed

packages/create-gen-app/README.md

Lines changed: 71 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,133 +16,124 @@
1616
<a href="https://www.npmjs.com/package/create-gen-app"><img height="20" src="https://img.shields.io/github/package-json/v/hyperweb-io/dev-utils?filename=packages%2Fcreate-gen-app%2Fpackage.json"></a>
1717
</p>
1818

19-
A TypeScript library for cloning and customizing template repositories with variable replacement.
19+
A TypeScript-first CLI/library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults.
2020

2121
## Features
2222

23-
- Clone GitHub repositories or any git URL
24-
- Extract template variables from filenames and file contents using `__VARIABLE__` syntax
25-
- Load custom questions from `.questions.json` or `.questions.js` files
26-
- Interactive prompts using inquirerer with CLI argument support
27-
- Stream-based file processing for efficient variable replacement
23+
- Clone any Git repo (or GitHub `org/repo` shorthand) and optionally select a branch + subdirectory
24+
- Extract template variables from filenames and file contents using the safer `____VARIABLE____` convention
25+
- Merge auto-discovered variables with `.questions.{json,js}` (questions win, including `ignore` patterns)
26+
- Interactive prompts powered by `inquirerer`, with CLI flag overrides (`--VAR value`) and non-TTY mode for CI
27+
- Built-in CLI (`create-gen-app` / `cga`) that discovers templates, prompts once, and writes output safely
28+
- License scaffolding: choose from MIT, Apache-2.0, ISC, GPL-3.0, BSD-3-Clause, Unlicense, or MPL-2.0 and generate a populated `LICENSE`
2829

2930
## Installation
3031

3132
```bash
3233
npm install create-gen-app
34+
# or for CLI only
35+
npm install -g create-gen-app
3336
```
3437

35-
## Usage
38+
## CLI Usage
3639

37-
### Basic Usage
40+
```bash
41+
# interactively pick a template from launchql/pgpm-boilerplates
42+
create-gen-app --output ./workspace
43+
44+
# short alias
45+
cga --template module --branch main --output ./module \
46+
--USERFULLNAME "Jane Dev" --USEREMAIL [email protected]
47+
48+
# point to a different repo/branch/path
49+
cga --repo github:my-org/my-templates --branch release \
50+
--path ./templates --template api --output ./api
51+
```
52+
53+
Key flags:
54+
55+
- `--repo`, `--branch`, `--path` – choose the Git repo, branch/tag, and subdirectory that contains templates
56+
- `--template` – folder inside `--path` (auto-prompted if omitted)
57+
- `--output` – destination directory (defaults to `./<template>`); use `--force` to overwrite
58+
- `--no-tty` – disable interactive prompts (ideal for CI)
59+
- `--version`, `--help` – standard metadata
60+
- Any extra `--VAR value` pairs become variable overrides
61+
62+
## Library Usage
3863

3964
```typescript
40-
import { createGen } from 'create-gen-app';
65+
import { createGen } from "create-gen-app";
4166

4267
await createGen({
43-
templateUrl: 'https://github.com/user/template-repo',
44-
outputDir: './my-new-project',
68+
templateUrl: "https://github.com/user/template-repo",
69+
fromBranch: "main",
70+
fromPath: "templates/module",
71+
outputDir: "./my-new-project",
4572
argv: {
46-
PROJECT_NAME: 'my-project',
47-
AUTHOR: 'John Doe'
48-
}
73+
USERFULLNAME: "Jane Dev",
74+
USEREMAIL: "[email protected]",
75+
MODULENAME: "awesome-module",
76+
LICENSE: "MIT",
77+
},
78+
noTty: true,
4979
});
5080
```
5181

5282
### Template Variables
5383

54-
Variables in your template should be wrapped in double underscores:
84+
Variables should be wrapped in four underscores on each side:
5585

56-
**Filename variables:**
5786
```
58-
__PROJECT_NAME__/
59-
__MODULE_NAME__.ts
87+
____PROJECT_NAME____/
88+
src/____MODULE_NAME____.ts
6089
```
6190

62-
**Content variables:**
6391
```typescript
64-
// __MODULE_NAME__.ts
65-
export const projectName = "__PROJECT_NAME__";
66-
export const author = "__AUTHOR__";
92+
// ____MODULE_NAME____.ts
93+
export const projectName = "____PROJECT_NAME____";
94+
export const author = "____USERFULLNAME____";
6795
```
6896

69-
### Custom Questions
97+
### Custom Questions & Ignore Rules
7098

71-
Create a `.questions.json` file in your template repository:
99+
Create a `.questions.json`:
72100

73101
```json
74102
{
103+
"ignore": ["__tests__", "docs/drafts"],
75104
"questions": [
76105
{
77-
"name": "PROJECT_NAME",
106+
"name": "____USERFULLNAME____",
78107
"type": "text",
79-
"message": "What is your project name?",
108+
"message": "Enter author full name",
80109
"required": true
81110
},
82111
{
83-
"name": "AUTHOR",
84-
"type": "text",
85-
"message": "Who is the author?"
112+
"name": "____LICENSE____",
113+
"type": "list",
114+
"message": "Choose a license",
115+
"options": ["MIT", "Apache-2.0", "ISC", "GPL-3.0"]
86116
}
87117
]
88118
}
89119
```
90120

91-
Or use `.questions.js` for dynamic questions:
92-
93-
```javascript
94-
/**
95-
* @typedef {Object} Questions
96-
* @property {Array} questions - Array of question objects
97-
*/
98-
99-
module.exports = {
100-
questions: [
101-
{
102-
name: 'PROJECT_NAME',
103-
type: 'text',
104-
message: 'What is your project name?',
105-
required: true
106-
}
107-
]
108-
};
109-
```
110-
111-
## API
112-
113-
### `createGen(options: CreateGenOptions): Promise<string>`
114-
115-
Main function to create a project from a template.
116-
117-
**Options:**
118-
- `templateUrl` (string): URL or path to the template repository
119-
- `outputDir` (string): Destination directory for the generated project
120-
- `argv` (Record<string, any>): Command-line arguments to pre-populate answers
121-
- `noTty` (boolean): Whether to disable TTY mode for non-interactive usage
122-
123-
### `extractVariables(templateDir: string): Promise<ExtractedVariables>`
124-
125-
Extract all variables from a template directory.
121+
Or `.questions.js` for dynamic logic. Question names can use `____VAR____` or plain `VAR`; they'll be normalized automatically.
126122

127-
### `promptUser(extractedVariables: ExtractedVariables, argv?: Record<string, any>, noTty?: boolean): Promise<Record<string, any>>`
123+
### License Templates
128124

129-
Prompt the user for variable values using inquirerer.
125+
`create-gen-app` ships text templates in `licenses-templates/`. To add another license, drop a `.txt` file matching the desired key (e.g., `BSD-2-CLAUSE.txt`) with placeholders:
130126

131-
### `replaceVariables(templateDir: string, outputDir: string, extractedVariables: ExtractedVariables, answers: Record<string, any>): Promise<void>`
127+
- `{{YEAR}}`, `{{AUTHOR}}`, `{{EMAIL_LINE}}`
132128

133-
Replace variables in all files and filenames.
129+
No code changes are needed; the CLI discovers templates at runtime and will warn if a `.questions` option doesn’t have a matching template.
134130

135-
## Variable Naming Rules
131+
## API Overview
136132

137-
Variables can contain:
138-
- Letters (a-z, A-Z)
139-
- Numbers (0-9)
140-
- Underscores (_)
141-
- Must start with a letter or underscore
133+
- `createGen(options)` – full pipeline (clone → extract → prompt → replace)
134+
- `cloneRepo(url, { branch })` – clone to a temp dir
135+
- `extractVariables(dir)` – parse file/folder names + content for variables, load `.questions`
136+
- `promptUser(extracted, argv, noTty)` – run interactive questions with CLI overrides and alias deduping
137+
- `replaceVariables(templateDir, outputDir, extracted, answers)` – copy files, rename paths, render licenses
142138

143-
Examples of valid variables:
144-
- `__PROJECT_NAME__`
145-
- `__author__`
146-
- `__CamelCase__`
147-
- `__snake_case__`
148-
- `__VERSION_1__`
139+
See `dev/README.md` for the local development helper script (`pnpm dev`).
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
import { runCli } from "../src/cli";
5+
import {
6+
TEST_BRANCH,
7+
TEST_REPO,
8+
TEST_TEMPLATE,
9+
buildAnswers,
10+
cleanupWorkspace,
11+
createTempWorkspace,
12+
} from "../test-utils/integration-helpers";
13+
14+
jest.setTimeout(180_000);
15+
16+
describe("CLI integration (GitHub templates)", () => {
17+
it("generates a project using the real repo", async () => {
18+
const workspace = createTempWorkspace("cli");
19+
const answers = buildAnswers("cli");
20+
21+
const args = [
22+
"--repo",
23+
TEST_REPO,
24+
"--branch",
25+
TEST_BRANCH,
26+
"--path",
27+
".",
28+
"--template",
29+
TEST_TEMPLATE,
30+
"--output",
31+
workspace.outputDir,
32+
"--no-tty",
33+
];
34+
35+
for (const [key, value] of Object.entries(answers)) {
36+
args.push(`--${key}`, value);
37+
}
38+
39+
try {
40+
const result = await runCli(args);
41+
expect(result).toBeDefined();
42+
if (!result) {
43+
return;
44+
}
45+
46+
expect(result.template).toBe(TEST_TEMPLATE);
47+
expect(result.outputDir).toBe(path.resolve(workspace.outputDir));
48+
49+
const pkgPath = path.join(workspace.outputDir, "package.json");
50+
expect(fs.existsSync(pkgPath)).toBe(true);
51+
52+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
53+
expect(pkg.name).toBe(answers.PACKAGE_IDENTIFIER);
54+
expect(pkg.license).toBe(answers.LICENSE);
55+
56+
const licenseContent = fs.readFileSync(
57+
path.join(workspace.outputDir, "LICENSE"),
58+
"utf8"
59+
);
60+
expect(licenseContent).toContain("MIT License");
61+
expect(licenseContent).toContain(answers.USERFULLNAME);
62+
} finally {
63+
cleanupWorkspace(workspace);
64+
}
65+
});
66+
67+
it("prints version and exits when --version is provided", async () => {
68+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
69+
70+
await runCli(["--version"]);
71+
72+
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/create-gen-app v/));
73+
logSpy.mockRestore();
74+
});
75+
});
76+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
import { createGen } from "../src";
5+
import {
6+
TEST_BRANCH,
7+
TEST_REPO,
8+
TEST_TEMPLATE,
9+
buildAnswers,
10+
cleanupWorkspace,
11+
createTempWorkspace,
12+
} from "../test-utils/integration-helpers";
13+
14+
jest.setTimeout(180_000);
15+
16+
describe("createGen integration (GitHub templates)", () => {
17+
it("clones the default repo and generates the module template", async () => {
18+
const workspace = createTempWorkspace("flow");
19+
20+
try {
21+
const answers = buildAnswers("flow");
22+
const result = await createGen({
23+
templateUrl: TEST_REPO,
24+
fromBranch: TEST_BRANCH,
25+
fromPath: TEST_TEMPLATE,
26+
outputDir: workspace.outputDir,
27+
argv: answers,
28+
noTty: true,
29+
});
30+
31+
expect(result).toBe(workspace.outputDir);
32+
33+
const packageJsonPath = path.join(workspace.outputDir, "package.json");
34+
expect(fs.existsSync(packageJsonPath)).toBe(true);
35+
36+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
37+
expect(pkg.name).toBe(answers.PACKAGE_IDENTIFIER);
38+
expect(pkg.license).toBe(answers.LICENSE);
39+
expect(pkg.author).toContain(answers.USERFULLNAME);
40+
41+
const questionsJsonPath = path.join(
42+
workspace.outputDir,
43+
".questions.json"
44+
);
45+
expect(fs.existsSync(questionsJsonPath)).toBe(false);
46+
47+
const licensePath = path.join(workspace.outputDir, "LICENSE");
48+
expect(fs.existsSync(licensePath)).toBe(true);
49+
const licenseContent = fs.readFileSync(licensePath, "utf8");
50+
expect(licenseContent).toContain(answers.USERFULLNAME);
51+
expect(licenseContent).toContain("MIT License");
52+
} finally {
53+
cleanupWorkspace(workspace);
54+
}
55+
});
56+
});
57+

0 commit comments

Comments
 (0)