create-gen-app 0.1.6 → 0.2.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 +35 -31
- package/cache.d.ts +13 -0
- package/cache.js +76 -0
- package/clone.d.ts +6 -0
- package/clone.js +10 -6
- package/esm/cache.js +38 -0
- package/esm/clone.js +14 -11
- package/esm/extract.js +16 -2
- package/esm/index.js +37 -24
- package/esm/replace.js +21 -7
- package/esm/template-cache.js +223 -0
- package/extract.js +16 -2
- package/index.d.ts +8 -6
- package/index.js +27 -14
- package/package.json +4 -10
- package/replace.js +21 -7
- package/template-cache.d.ts +59 -0
- package/template-cache.js +260 -0
- package/types.d.ts +31 -0
- package/cli.d.ts +0 -6
- package/cli.js +0 -239
- package/esm/cli.js +0 -200
package/README.md
CHANGED
|
@@ -16,52 +16,31 @@
|
|
|
16
16
|
<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>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
A TypeScript-first
|
|
19
|
+
A TypeScript-first library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults.
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
23
|
- Clone any Git repo (or GitHub `org/repo` shorthand) and optionally select a branch + subdirectory
|
|
24
24
|
- Extract template variables from filenames and file contents using the safer `____variable____` convention
|
|
25
25
|
- Merge auto-discovered variables with `.questions.{json,js}` (questions win, including `ignore` patterns)
|
|
26
|
-
- Interactive prompts powered by `inquirerer`, with
|
|
27
|
-
- Built-in CLI (`create-gen-app` / `cga`) that discovers templates, prompts once, and writes output safely
|
|
26
|
+
- Interactive prompts powered by `inquirerer`, with flexible override mapping (`argv` support) and non-TTY mode for CI
|
|
28
27
|
- 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`
|
|
28
|
+
- Built-in template caching powered by `appstash`, so repeat runs skip `git clone` (configurable via `cache` options)
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npm install create-gen-app
|
|
34
|
-
# or for CLI only
|
|
35
|
-
npm install -g create-gen-app
|
|
36
34
|
```
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
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 jane@example.com
|
|
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
|
|
36
|
+
> **Note:** The published package is API-only. An internal CLI harness used for integration testing now lives in `packages/create-gen-app-test/`.
|
|
61
37
|
|
|
62
38
|
## Library Usage
|
|
63
39
|
|
|
64
40
|
```typescript
|
|
41
|
+
import * as os from "os";
|
|
42
|
+
import * as path from "path";
|
|
43
|
+
|
|
65
44
|
import { createGen } from "create-gen-app";
|
|
66
45
|
|
|
67
46
|
await createGen({
|
|
@@ -76,9 +55,33 @@ await createGen({
|
|
|
76
55
|
LICENSE: "MIT",
|
|
77
56
|
},
|
|
78
57
|
noTty: true,
|
|
58
|
+
cache: {
|
|
59
|
+
// optional: override tool/baseDir (defaults to pgpm + ~/.pgpm)
|
|
60
|
+
toolName: "pgpm",
|
|
61
|
+
baseDir: path.join(os.tmpdir(), "create-gen-cache"),
|
|
62
|
+
},
|
|
79
63
|
});
|
|
80
64
|
```
|
|
81
65
|
|
|
66
|
+
### Template Caching
|
|
67
|
+
|
|
68
|
+
`create-gen-app` caches repositories under `~/.pgpm/cache/repos/<hash>` by default (using [`appstash`](https://github.com/hyperweb-io/dev-utils/tree/main/packages/appstash)). The first run clones & stores the repo, subsequent runs re-use the cached directory.
|
|
69
|
+
|
|
70
|
+
- Disable caching with `cache: false` or `cache: { enabled: false }`
|
|
71
|
+
- Override the tool name or base directory with `cache: { toolName, baseDir }`
|
|
72
|
+
- For tests/CI, point `baseDir` to a temporary folder so the suite does not touch the developer’s real home directory:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const tempBase = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-cache-"));
|
|
76
|
+
|
|
77
|
+
await createGen({
|
|
78
|
+
...options,
|
|
79
|
+
cache: { baseDir: tempBase, toolName: "pgpm-test-suite" },
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The cache directory never mutates the template, so reusing the same cached repo across many runs is safe.
|
|
84
|
+
|
|
82
85
|
### Template Variables
|
|
83
86
|
|
|
84
87
|
Variables should be wrapped in four underscores on each side:
|
|
@@ -126,14 +129,15 @@ Or `.questions.js` for dynamic logic. Question names can use `____var____` or pl
|
|
|
126
129
|
|
|
127
130
|
- `{{YEAR}}`, `{{AUTHOR}}`, `{{EMAIL_LINE}}`
|
|
128
131
|
|
|
129
|
-
No code changes are needed; the
|
|
132
|
+
No code changes are needed; the generator discovers templates at runtime and will warn if a `.questions` option doesn’t have a matching template.
|
|
130
133
|
|
|
131
134
|
## API Overview
|
|
132
135
|
|
|
133
136
|
- `createGen(options)` – full pipeline (clone → extract → prompt → replace)
|
|
134
137
|
- `cloneRepo(url, { branch })` – clone to a temp dir
|
|
138
|
+
- `normalizeCacheOptions(cache)` / `prepareTemplateDirectory(...)` – inspect or reuse cached template repos
|
|
135
139
|
- `extractVariables(dir)` – parse file/folder names + content for variables, load `.questions`
|
|
136
|
-
- `promptUser(extracted, argv, noTty)` – run interactive questions with
|
|
140
|
+
- `promptUser(extracted, argv, noTty)` – run interactive questions with override alias deduping
|
|
137
141
|
- `replaceVariables(templateDir, outputDir, extracted, answers)` – copy files, rename paths, render licenses
|
|
138
142
|
|
|
139
|
-
See `dev/README.md` for the local development helper script (`pnpm dev`).
|
|
143
|
+
See `packages/create-gen-app-test/dev/README.md` for the local development helper script (`pnpm --filter create-gen-app-test dev`).
|
package/cache.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CacheOptions } from "./types";
|
|
2
|
+
export interface TemplateSource {
|
|
3
|
+
templateDir: string;
|
|
4
|
+
cacheUsed: boolean;
|
|
5
|
+
cleanup: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface PrepareTemplateArgs {
|
|
8
|
+
templateUrl: string;
|
|
9
|
+
branch?: string;
|
|
10
|
+
cache: CacheOptions | false;
|
|
11
|
+
}
|
|
12
|
+
export declare function prepareTemplateDirectory(args: PrepareTemplateArgs): Promise<TemplateSource>;
|
|
13
|
+
export { TemplateCache } from "./template-cache";
|
package/cache.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TemplateCache = void 0;
|
|
37
|
+
exports.prepareTemplateDirectory = prepareTemplateDirectory;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const clone_1 = require("./clone");
|
|
40
|
+
const template_cache_1 = require("./template-cache");
|
|
41
|
+
async function prepareTemplateDirectory(args) {
|
|
42
|
+
const { templateUrl, branch, cache } = args;
|
|
43
|
+
const templateCache = new template_cache_1.TemplateCache(cache);
|
|
44
|
+
if (!templateCache.isEnabled()) {
|
|
45
|
+
const tempDir = await (0, clone_1.cloneRepo)(templateUrl, { branch });
|
|
46
|
+
return {
|
|
47
|
+
templateDir: tempDir,
|
|
48
|
+
cacheUsed: false,
|
|
49
|
+
cleanup: () => cleanupDir(tempDir),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Try to get from cache
|
|
53
|
+
const cachedPath = templateCache.get(templateUrl, branch);
|
|
54
|
+
if (cachedPath) {
|
|
55
|
+
return {
|
|
56
|
+
templateDir: cachedPath,
|
|
57
|
+
cacheUsed: true,
|
|
58
|
+
cleanup: () => { },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Cache miss or expired - clone and cache
|
|
62
|
+
const cachePath = templateCache.set(templateUrl, branch);
|
|
63
|
+
return {
|
|
64
|
+
templateDir: cachePath,
|
|
65
|
+
cacheUsed: false,
|
|
66
|
+
cleanup: () => { },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function cleanupDir(dir) {
|
|
70
|
+
if (fs.existsSync(dir)) {
|
|
71
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Re-export TemplateCache for external use
|
|
75
|
+
var template_cache_2 = require("./template-cache");
|
|
76
|
+
Object.defineProperty(exports, "TemplateCache", { enumerable: true, get: function () { return template_cache_2.TemplateCache; } });
|
package/clone.d.ts
CHANGED
|
@@ -7,3 +7,9 @@ export interface CloneOptions {
|
|
|
7
7
|
* @returns Path to the cloned repository
|
|
8
8
|
*/
|
|
9
9
|
export declare function cloneRepo(url: string, options?: CloneOptions): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a URL to a git-cloneable format
|
|
12
|
+
* @param url - Input URL
|
|
13
|
+
* @returns Normalized git URL
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeGitUrl(url: string): string;
|
package/clone.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.cloneRepo = cloneRepo;
|
|
37
|
+
exports.normalizeGitUrl = normalizeGitUrl;
|
|
37
38
|
const child_process_1 = require("child_process");
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const os = __importStar(require("os"));
|
|
@@ -44,15 +45,16 @@ const path = __importStar(require("path"));
|
|
|
44
45
|
* @returns Path to the cloned repository
|
|
45
46
|
*/
|
|
46
47
|
async function cloneRepo(url, options = {}) {
|
|
47
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
48
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-"));
|
|
48
49
|
const { branch } = options;
|
|
49
50
|
try {
|
|
50
51
|
const gitUrl = normalizeGitUrl(url);
|
|
51
|
-
const branchArgs = branch ? ` --branch ${branch} --single-branch` :
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const branchArgs = branch ? ` --branch ${branch} --single-branch` : "";
|
|
53
|
+
const depthArgs = " --depth 1"; // use shallow clone for speed; remove if future features need full history
|
|
54
|
+
(0, child_process_1.execSync)(`git clone${branchArgs}${depthArgs} ${gitUrl} ${tempDir}`, {
|
|
55
|
+
stdio: "inherit",
|
|
54
56
|
});
|
|
55
|
-
const gitDir = path.join(tempDir,
|
|
57
|
+
const gitDir = path.join(tempDir, ".git");
|
|
56
58
|
if (fs.existsSync(gitDir)) {
|
|
57
59
|
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
58
60
|
}
|
|
@@ -72,7 +74,9 @@ async function cloneRepo(url, options = {}) {
|
|
|
72
74
|
* @returns Normalized git URL
|
|
73
75
|
*/
|
|
74
76
|
function normalizeGitUrl(url) {
|
|
75
|
-
if (url.startsWith(
|
|
77
|
+
if (url.startsWith("git@") ||
|
|
78
|
+
url.startsWith("https://") ||
|
|
79
|
+
url.startsWith("http://")) {
|
|
76
80
|
return url;
|
|
77
81
|
}
|
|
78
82
|
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
package/esm/cache.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { cloneRepo } from "./clone";
|
|
3
|
+
import { TemplateCache } from "./template-cache";
|
|
4
|
+
export async function prepareTemplateDirectory(args) {
|
|
5
|
+
const { templateUrl, branch, cache } = args;
|
|
6
|
+
const templateCache = new TemplateCache(cache);
|
|
7
|
+
if (!templateCache.isEnabled()) {
|
|
8
|
+
const tempDir = await cloneRepo(templateUrl, { branch });
|
|
9
|
+
return {
|
|
10
|
+
templateDir: tempDir,
|
|
11
|
+
cacheUsed: false,
|
|
12
|
+
cleanup: () => cleanupDir(tempDir),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Try to get from cache
|
|
16
|
+
const cachedPath = templateCache.get(templateUrl, branch);
|
|
17
|
+
if (cachedPath) {
|
|
18
|
+
return {
|
|
19
|
+
templateDir: cachedPath,
|
|
20
|
+
cacheUsed: true,
|
|
21
|
+
cleanup: () => { },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Cache miss or expired - clone and cache
|
|
25
|
+
const cachePath = templateCache.set(templateUrl, branch);
|
|
26
|
+
return {
|
|
27
|
+
templateDir: cachePath,
|
|
28
|
+
cacheUsed: false,
|
|
29
|
+
cleanup: () => { },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function cleanupDir(dir) {
|
|
33
|
+
if (fs.existsSync(dir)) {
|
|
34
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Re-export TemplateCache for external use
|
|
38
|
+
export { TemplateCache } from "./template-cache";
|
package/esm/clone.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as os from
|
|
4
|
-
import * as path from
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
5
|
/**
|
|
6
6
|
* Clone a repository to a temporary directory
|
|
7
7
|
* @param url - Repository URL (GitHub or any git URL)
|
|
8
8
|
* @returns Path to the cloned repository
|
|
9
9
|
*/
|
|
10
10
|
export async function cloneRepo(url, options = {}) {
|
|
11
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-"));
|
|
12
12
|
const { branch } = options;
|
|
13
13
|
try {
|
|
14
14
|
const gitUrl = normalizeGitUrl(url);
|
|
15
|
-
const branchArgs = branch ? ` --branch ${branch} --single-branch` :
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const branchArgs = branch ? ` --branch ${branch} --single-branch` : "";
|
|
16
|
+
const depthArgs = " --depth 1"; // use shallow clone for speed; remove if future features need full history
|
|
17
|
+
execSync(`git clone${branchArgs}${depthArgs} ${gitUrl} ${tempDir}`, {
|
|
18
|
+
stdio: "inherit",
|
|
18
19
|
});
|
|
19
|
-
const gitDir = path.join(tempDir,
|
|
20
|
+
const gitDir = path.join(tempDir, ".git");
|
|
20
21
|
if (fs.existsSync(gitDir)) {
|
|
21
22
|
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
22
23
|
}
|
|
@@ -35,8 +36,10 @@ export async function cloneRepo(url, options = {}) {
|
|
|
35
36
|
* @param url - Input URL
|
|
36
37
|
* @returns Normalized git URL
|
|
37
38
|
*/
|
|
38
|
-
function normalizeGitUrl(url) {
|
|
39
|
-
if (url.startsWith(
|
|
39
|
+
export function normalizeGitUrl(url) {
|
|
40
|
+
if (url.startsWith("git@") ||
|
|
41
|
+
url.startsWith("https://") ||
|
|
42
|
+
url.startsWith("http://")) {
|
|
40
43
|
return url;
|
|
41
44
|
}
|
|
42
45
|
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
package/esm/extract.js
CHANGED
|
@@ -120,7 +120,7 @@ async function loadProjectQuestions(templateDir) {
|
|
|
120
120
|
try {
|
|
121
121
|
const content = fs.readFileSync(jsonPath, "utf8");
|
|
122
122
|
const questions = JSON.parse(content);
|
|
123
|
-
return validateQuestions(questions) ? questions : null;
|
|
123
|
+
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
124
124
|
}
|
|
125
125
|
catch (error) {
|
|
126
126
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -132,7 +132,7 @@ async function loadProjectQuestions(templateDir) {
|
|
|
132
132
|
try {
|
|
133
133
|
const module = require(jsPath);
|
|
134
134
|
const questions = module.default || module;
|
|
135
|
-
return validateQuestions(questions) ? questions : null;
|
|
135
|
+
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
136
136
|
}
|
|
137
137
|
catch (error) {
|
|
138
138
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -208,6 +208,20 @@ function normalizePlaceholder(entry) {
|
|
|
208
208
|
}
|
|
209
209
|
return entry || null;
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Normalize questions by ensuring all required fields have default values
|
|
213
|
+
* @param questions - Questions object to normalize
|
|
214
|
+
* @returns Normalized questions object
|
|
215
|
+
*/
|
|
216
|
+
function normalizeQuestions(questions) {
|
|
217
|
+
return {
|
|
218
|
+
...questions,
|
|
219
|
+
questions: questions.questions.map((q) => ({
|
|
220
|
+
...q,
|
|
221
|
+
type: q.type || 'text'
|
|
222
|
+
}))
|
|
223
|
+
};
|
|
224
|
+
}
|
|
211
225
|
/**
|
|
212
226
|
* Validate that the questions object has the correct structure
|
|
213
227
|
* @param obj - Object to validate
|
package/esm/index.js
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import {
|
|
4
|
-
import { extractVariables } from
|
|
5
|
-
import { promptUser } from
|
|
6
|
-
import { replaceVariables } from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { prepareTemplateDirectory } from "./cache";
|
|
4
|
+
import { extractVariables } from "./extract";
|
|
5
|
+
import { promptUser } from "./prompt";
|
|
6
|
+
import { replaceVariables } from "./replace";
|
|
7
|
+
export * from "./cache";
|
|
8
|
+
export * from "./clone";
|
|
9
|
+
export * from "./extract";
|
|
10
|
+
export * from "./prompt";
|
|
11
|
+
export * from "./replace";
|
|
12
|
+
export * from "./types";
|
|
13
|
+
export * from "./template-cache";
|
|
12
14
|
/**
|
|
13
15
|
* Create a new project from a template repository
|
|
14
16
|
* @param options - Options for creating the project
|
|
15
17
|
* @returns Path to the generated project
|
|
16
18
|
*/
|
|
17
19
|
export async function createGen(options) {
|
|
18
|
-
const { templateUrl, outputDir, argv = {}, noTty = false, fromBranch, fromPath } = options;
|
|
19
|
-
console.log(`
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const { templateUrl, outputDir, argv = {}, noTty = false, fromBranch, fromPath, cache, } = options;
|
|
21
|
+
console.log(`Preparing template from ${templateUrl}...`);
|
|
22
|
+
const templateSource = await prepareTemplateDirectory({
|
|
23
|
+
templateUrl,
|
|
24
|
+
branch: fromBranch,
|
|
25
|
+
cache,
|
|
26
|
+
});
|
|
27
|
+
const cacheEnabled = cache !== false && (cache?.enabled !== false);
|
|
28
|
+
if (cacheEnabled) {
|
|
29
|
+
console.log(templateSource.cacheUsed
|
|
30
|
+
? "Using cached repository"
|
|
31
|
+
: "Caching repository for future runs...");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log("Cloning repository without cache...");
|
|
35
|
+
}
|
|
36
|
+
const normalizedPath = fromPath ? path.normalize(fromPath) : ".";
|
|
37
|
+
const templateRoot = normalizedPath && normalizedPath !== "."
|
|
38
|
+
? path.join(templateSource.templateDir, normalizedPath)
|
|
39
|
+
: templateSource.templateDir;
|
|
25
40
|
try {
|
|
26
41
|
if (!fs.existsSync(templateRoot)) {
|
|
27
42
|
throw new Error(`Template path "${fromPath}" does not exist in repository ${templateUrl}.`);
|
|
28
43
|
}
|
|
29
|
-
console.log(
|
|
44
|
+
console.log("Extracting template variables...");
|
|
30
45
|
const extractedVariables = await extractVariables(templateRoot);
|
|
31
46
|
console.log(`Found ${extractedVariables.fileReplacers.length} file replacers`);
|
|
32
47
|
console.log(`Found ${extractedVariables.contentReplacers.length} content replacers`);
|
|
33
48
|
if (extractedVariables.projectQuestions) {
|
|
34
49
|
console.log(`Found ${extractedVariables.projectQuestions.questions.length} project questions`);
|
|
35
50
|
}
|
|
36
|
-
console.log(
|
|
51
|
+
console.log("Prompting for variable values...");
|
|
37
52
|
const answers = await promptUser(extractedVariables, argv, noTty);
|
|
38
53
|
console.log(`Generating project in ${outputDir}...`);
|
|
39
54
|
await replaceVariables(templateRoot, outputDir, extractedVariables, answers);
|
|
40
|
-
console.log(
|
|
55
|
+
console.log("Project created successfully!");
|
|
41
56
|
return outputDir;
|
|
42
57
|
}
|
|
43
58
|
finally {
|
|
44
|
-
|
|
45
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
46
|
-
}
|
|
59
|
+
templateSource.cleanup();
|
|
47
60
|
}
|
|
48
61
|
}
|
package/esm/replace.js
CHANGED
|
@@ -53,7 +53,7 @@ async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, s
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
async function ensureLicenseFile(outputDir, answers) {
|
|
56
|
-
const licenseValue = answers
|
|
56
|
+
const licenseValue = getAnswer(answers, ["LICENSE", "license"]);
|
|
57
57
|
if (typeof licenseValue !== 'string' || licenseValue.trim() === '') {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
@@ -62,12 +62,17 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
62
62
|
console.warn(`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`);
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
const author = answers
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const author = getAnswer(answers, [
|
|
66
|
+
"USERFULLNAME",
|
|
67
|
+
"AUTHOR",
|
|
68
|
+
"AUTHORFULLNAME",
|
|
69
|
+
"USERNAME",
|
|
70
|
+
"fullName",
|
|
71
|
+
"author",
|
|
72
|
+
"authorFullName",
|
|
73
|
+
"userName",
|
|
74
|
+
]) ?? "Unknown Author";
|
|
75
|
+
const email = getAnswer(answers, ["USEREMAIL", "EMAIL", "email", "userEmail"]) ?? "";
|
|
71
76
|
const content = renderLicense(selectedLicense, {
|
|
72
77
|
author: String(author),
|
|
73
78
|
email: String(email || ''),
|
|
@@ -80,6 +85,15 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
80
85
|
fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
|
|
81
86
|
console.log(`[create-gen-app] LICENSE updated with ${selectedLicense} template.`);
|
|
82
87
|
}
|
|
88
|
+
function getAnswer(answers, keys) {
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
const value = answers?.[key];
|
|
91
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
83
97
|
/**
|
|
84
98
|
* Replace variables in a file using streams
|
|
85
99
|
* @param sourcePath - Source file path
|