create-gen-app 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/LICENSE +21 -0
- package/README.md +150 -0
- package/clone.d.ts +6 -0
- package/clone.js +71 -0
- package/esm/clone.js +44 -0
- package/esm/extract.js +140 -0
- package/esm/index.js +40 -0
- package/esm/prompt.js +57 -0
- package/esm/replace.js +82 -0
- package/esm/types.js +1 -0
- package/extract.d.ts +7 -0
- package/extract.js +167 -0
- package/index.d.ts +12 -0
- package/index.js +70 -0
- package/package.json +36 -0
- package/prompt.d.ts +16 -0
- package/prompt.js +62 -0
- package/replace.d.ts +9 -0
- package/replace.js +109 -0
- package/types.d.ts +60 -0
- package/types.js +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# create-gen-app
|
|
2
|
+
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="90" src="https://user-images.githubusercontent.com/545047/190171475-b416f99e-2831-4786-9ba3-a7ff4d95b0d3.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
|
|
9
|
+
<a href="https://github.com/pyramation/inquirerer/actions/workflows/run-tests.yml">
|
|
10
|
+
<img height="20" src="https://github.com/pyramation/inquirerer/actions/workflows/run-tests.yml/badge.svg" />
|
|
11
|
+
</a>
|
|
12
|
+
<a href="https://github.com/pyramation/inquirerer/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/inquirerer"><img height="20" src="https://img.shields.io/npm/dt/inquirerer"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/inquirerer"><img height="20" src="https://img.shields.io/github/package-json/v/pyramation/inquirerer?filename=packages%2Finquirerer%2Fpackage.json"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
A TypeScript library for cloning and customizing template repositories with variable replacement.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Clone GitHub repositories or any git URL
|
|
22
|
+
- Extract template variables from filenames and file contents using `__VARIABLE__` syntax
|
|
23
|
+
- Load custom questions from `.questions.json` or `.questions.js` files
|
|
24
|
+
- Interactive prompts using inquirerer with CLI argument support
|
|
25
|
+
- Stream-based file processing for efficient variable replacement
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install create-gen-app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic Usage
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createGen } from 'create-gen-app';
|
|
39
|
+
|
|
40
|
+
await createGen({
|
|
41
|
+
templateUrl: 'https://github.com/user/template-repo',
|
|
42
|
+
outputDir: './my-new-project',
|
|
43
|
+
argv: {
|
|
44
|
+
PROJECT_NAME: 'my-project',
|
|
45
|
+
AUTHOR: 'John Doe'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Template Variables
|
|
51
|
+
|
|
52
|
+
Variables in your template should be wrapped in double underscores:
|
|
53
|
+
|
|
54
|
+
**Filename variables:**
|
|
55
|
+
```
|
|
56
|
+
__PROJECT_NAME__/
|
|
57
|
+
__MODULE_NAME__.ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Content variables:**
|
|
61
|
+
```typescript
|
|
62
|
+
// __MODULE_NAME__.ts
|
|
63
|
+
export const projectName = "__PROJECT_NAME__";
|
|
64
|
+
export const author = "__AUTHOR__";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Custom Questions
|
|
68
|
+
|
|
69
|
+
Create a `.questions.json` file in your template repository:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"questions": [
|
|
74
|
+
{
|
|
75
|
+
"name": "PROJECT_NAME",
|
|
76
|
+
"type": "text",
|
|
77
|
+
"message": "What is your project name?",
|
|
78
|
+
"required": true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "AUTHOR",
|
|
82
|
+
"type": "text",
|
|
83
|
+
"message": "Who is the author?"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or use `.questions.js` for dynamic questions:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {Object} Questions
|
|
94
|
+
* @property {Array} questions - Array of question objects
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
questions: [
|
|
99
|
+
{
|
|
100
|
+
name: 'PROJECT_NAME',
|
|
101
|
+
type: 'text',
|
|
102
|
+
message: 'What is your project name?',
|
|
103
|
+
required: true
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
### `createGen(options: CreateGenOptions): Promise<string>`
|
|
112
|
+
|
|
113
|
+
Main function to create a project from a template.
|
|
114
|
+
|
|
115
|
+
**Options:**
|
|
116
|
+
- `templateUrl` (string): URL or path to the template repository
|
|
117
|
+
- `outputDir` (string): Destination directory for the generated project
|
|
118
|
+
- `argv` (Record<string, any>): Command-line arguments to pre-populate answers
|
|
119
|
+
- `noTty` (boolean): Whether to disable TTY mode for non-interactive usage
|
|
120
|
+
|
|
121
|
+
### `extractVariables(templateDir: string): Promise<ExtractedVariables>`
|
|
122
|
+
|
|
123
|
+
Extract all variables from a template directory.
|
|
124
|
+
|
|
125
|
+
### `promptUser(extractedVariables: ExtractedVariables, argv?: Record<string, any>, noTty?: boolean): Promise<Record<string, any>>`
|
|
126
|
+
|
|
127
|
+
Prompt the user for variable values using inquirerer.
|
|
128
|
+
|
|
129
|
+
### `replaceVariables(templateDir: string, outputDir: string, extractedVariables: ExtractedVariables, answers: Record<string, any>): Promise<void>`
|
|
130
|
+
|
|
131
|
+
Replace variables in all files and filenames.
|
|
132
|
+
|
|
133
|
+
## Variable Naming Rules
|
|
134
|
+
|
|
135
|
+
Variables can contain:
|
|
136
|
+
- Letters (a-z, A-Z)
|
|
137
|
+
- Numbers (0-9)
|
|
138
|
+
- Underscores (_)
|
|
139
|
+
- Must start with a letter or underscore
|
|
140
|
+
|
|
141
|
+
Examples of valid variables:
|
|
142
|
+
- `__PROJECT_NAME__`
|
|
143
|
+
- `__author__`
|
|
144
|
+
- `__CamelCase__`
|
|
145
|
+
- `__snake_case__`
|
|
146
|
+
- `__VERSION_1__`
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
package/clone.d.ts
ADDED
package/clone.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
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 (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.cloneRepo = void 0;
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const fs = __importStar(require("fs"));
|
|
29
|
+
const os = __importStar(require("os"));
|
|
30
|
+
const path = __importStar(require("path"));
|
|
31
|
+
/**
|
|
32
|
+
* Clone a repository to a temporary directory
|
|
33
|
+
* @param url - Repository URL (GitHub or any git URL)
|
|
34
|
+
* @returns Path to the cloned repository
|
|
35
|
+
*/
|
|
36
|
+
async function cloneRepo(url) {
|
|
37
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-gen-'));
|
|
38
|
+
try {
|
|
39
|
+
const gitUrl = normalizeGitUrl(url);
|
|
40
|
+
(0, child_process_1.execSync)(`git clone ${gitUrl} ${tempDir}`, {
|
|
41
|
+
stdio: 'inherit'
|
|
42
|
+
});
|
|
43
|
+
const gitDir = path.join(tempDir, '.git');
|
|
44
|
+
if (fs.existsSync(gitDir)) {
|
|
45
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
return tempDir;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (fs.existsSync(tempDir)) {
|
|
51
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
54
|
+
throw new Error(`Failed to clone repository: ${errorMessage}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.cloneRepo = cloneRepo;
|
|
58
|
+
/**
|
|
59
|
+
* Normalize a URL to a git-cloneable format
|
|
60
|
+
* @param url - Input URL
|
|
61
|
+
* @returns Normalized git URL
|
|
62
|
+
*/
|
|
63
|
+
function normalizeGitUrl(url) {
|
|
64
|
+
if (url.startsWith('git@') || url.startsWith('https://') || url.startsWith('http://')) {
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
|
68
|
+
return `https://github.com/${url}.git`;
|
|
69
|
+
}
|
|
70
|
+
return url;
|
|
71
|
+
}
|
package/esm/clone.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Clone a repository to a temporary directory
|
|
7
|
+
* @param url - Repository URL (GitHub or any git URL)
|
|
8
|
+
* @returns Path to the cloned repository
|
|
9
|
+
*/
|
|
10
|
+
export async function cloneRepo(url) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-gen-'));
|
|
12
|
+
try {
|
|
13
|
+
const gitUrl = normalizeGitUrl(url);
|
|
14
|
+
execSync(`git clone ${gitUrl} ${tempDir}`, {
|
|
15
|
+
stdio: 'inherit'
|
|
16
|
+
});
|
|
17
|
+
const gitDir = path.join(tempDir, '.git');
|
|
18
|
+
if (fs.existsSync(gitDir)) {
|
|
19
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
return tempDir;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (fs.existsSync(tempDir)) {
|
|
25
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
28
|
+
throw new Error(`Failed to clone repository: ${errorMessage}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a URL to a git-cloneable format
|
|
33
|
+
* @param url - Input URL
|
|
34
|
+
* @returns Normalized git URL
|
|
35
|
+
*/
|
|
36
|
+
function normalizeGitUrl(url) {
|
|
37
|
+
if (url.startsWith('git@') || url.startsWith('https://') || url.startsWith('http://')) {
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
|
41
|
+
return `https://github.com/${url}.git`;
|
|
42
|
+
}
|
|
43
|
+
return url;
|
|
44
|
+
}
|
package/esm/extract.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Pattern to match __VARIABLE__ in filenames and content
|
|
5
|
+
*/
|
|
6
|
+
const VARIABLE_PATTERN = /__([A-Za-z_][A-Za-z0-9_]*)__/g;
|
|
7
|
+
/**
|
|
8
|
+
* Extract all variables from a template directory
|
|
9
|
+
* @param templateDir - Path to the template directory
|
|
10
|
+
* @returns Extracted variables including file replacers, content replacers, and project questions
|
|
11
|
+
*/
|
|
12
|
+
export async function extractVariables(templateDir) {
|
|
13
|
+
const fileReplacers = [];
|
|
14
|
+
const contentReplacers = [];
|
|
15
|
+
const fileReplacerVars = new Set();
|
|
16
|
+
const contentReplacerVars = new Set();
|
|
17
|
+
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
18
|
+
await walkDirectory(templateDir, async (filePath) => {
|
|
19
|
+
const relativePath = path.relative(templateDir, filePath);
|
|
20
|
+
if (relativePath === '.questions.json' || relativePath === '.questions.js') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const matches = relativePath.matchAll(VARIABLE_PATTERN);
|
|
24
|
+
for (const match of matches) {
|
|
25
|
+
const varName = match[1];
|
|
26
|
+
if (!fileReplacerVars.has(varName)) {
|
|
27
|
+
fileReplacerVars.add(varName);
|
|
28
|
+
fileReplacers.push({
|
|
29
|
+
variable: varName,
|
|
30
|
+
pattern: new RegExp(`__${varName}__`, 'g')
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const contentVars = await extractFromFileContent(filePath);
|
|
35
|
+
for (const varName of contentVars) {
|
|
36
|
+
if (!contentReplacerVars.has(varName)) {
|
|
37
|
+
contentReplacerVars.add(varName);
|
|
38
|
+
contentReplacers.push({
|
|
39
|
+
variable: varName,
|
|
40
|
+
pattern: new RegExp(`__${varName}__`, 'g')
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
fileReplacers,
|
|
47
|
+
contentReplacers,
|
|
48
|
+
projectQuestions
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract variables from file content using streams
|
|
53
|
+
* @param filePath - Path to the file
|
|
54
|
+
* @returns Set of variable names found in the file
|
|
55
|
+
*/
|
|
56
|
+
async function extractFromFileContent(filePath) {
|
|
57
|
+
const variables = new Set();
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
60
|
+
let buffer = '';
|
|
61
|
+
stream.on('data', (chunk) => {
|
|
62
|
+
buffer += chunk.toString();
|
|
63
|
+
const lines = buffer.split('\n');
|
|
64
|
+
buffer = lines.pop() || ''; // Keep the last incomplete line in buffer
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
67
|
+
for (const match of matches) {
|
|
68
|
+
variables.add(match[1]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
stream.on('end', () => {
|
|
73
|
+
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
74
|
+
for (const match of matches) {
|
|
75
|
+
variables.add(match[1]);
|
|
76
|
+
}
|
|
77
|
+
resolve(variables);
|
|
78
|
+
});
|
|
79
|
+
stream.on('error', () => {
|
|
80
|
+
resolve(variables);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Walk through a directory recursively
|
|
86
|
+
* @param dir - Directory to walk
|
|
87
|
+
* @param callback - Callback function for each file
|
|
88
|
+
*/
|
|
89
|
+
async function walkDirectory(dir, callback) {
|
|
90
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const fullPath = path.join(dir, entry.name);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await walkDirectory(fullPath, callback);
|
|
95
|
+
}
|
|
96
|
+
else if (entry.isFile()) {
|
|
97
|
+
await callback(fullPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Load project questions from .questions.json or .questions.js
|
|
103
|
+
* @param templateDir - Path to the template directory
|
|
104
|
+
* @returns Questions object or null if not found
|
|
105
|
+
*/
|
|
106
|
+
async function loadProjectQuestions(templateDir) {
|
|
107
|
+
const jsonPath = path.join(templateDir, '.questions.json');
|
|
108
|
+
if (fs.existsSync(jsonPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
111
|
+
const questions = JSON.parse(content);
|
|
112
|
+
return validateQuestions(questions) ? questions : null;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
+
console.warn(`Failed to parse .questions.json: ${errorMessage}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const jsPath = path.join(templateDir, '.questions.js');
|
|
120
|
+
if (fs.existsSync(jsPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const module = require(jsPath);
|
|
123
|
+
const questions = module.default || module;
|
|
124
|
+
return validateQuestions(questions) ? questions : null;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
128
|
+
console.warn(`Failed to load .questions.js: ${errorMessage}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate that the questions object has the correct structure
|
|
135
|
+
* @param obj - Object to validate
|
|
136
|
+
* @returns True if valid, false otherwise
|
|
137
|
+
*/
|
|
138
|
+
function validateQuestions(obj) {
|
|
139
|
+
return obj && typeof obj === 'object' && Array.isArray(obj.questions);
|
|
140
|
+
}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { cloneRepo } from './clone';
|
|
3
|
+
import { extractVariables } from './extract';
|
|
4
|
+
import { promptUser } from './prompt';
|
|
5
|
+
import { replaceVariables } from './replace';
|
|
6
|
+
export * from './clone';
|
|
7
|
+
export * from './extract';
|
|
8
|
+
export * from './prompt';
|
|
9
|
+
export * from './replace';
|
|
10
|
+
export * from './types';
|
|
11
|
+
/**
|
|
12
|
+
* Create a new project from a template repository
|
|
13
|
+
* @param options - Options for creating the project
|
|
14
|
+
* @returns Path to the generated project
|
|
15
|
+
*/
|
|
16
|
+
export async function createGen(options) {
|
|
17
|
+
const { templateUrl, outputDir, argv = {}, noTty = false } = options;
|
|
18
|
+
console.log(`Cloning template from ${templateUrl}...`);
|
|
19
|
+
const tempDir = await cloneRepo(templateUrl);
|
|
20
|
+
try {
|
|
21
|
+
console.log('Extracting template variables...');
|
|
22
|
+
const extractedVariables = await extractVariables(tempDir);
|
|
23
|
+
console.log(`Found ${extractedVariables.fileReplacers.length} file replacers`);
|
|
24
|
+
console.log(`Found ${extractedVariables.contentReplacers.length} content replacers`);
|
|
25
|
+
if (extractedVariables.projectQuestions) {
|
|
26
|
+
console.log(`Found ${extractedVariables.projectQuestions.questions.length} project questions`);
|
|
27
|
+
}
|
|
28
|
+
console.log('Prompting for variable values...');
|
|
29
|
+
const answers = await promptUser(extractedVariables, argv, noTty);
|
|
30
|
+
console.log(`Generating project in ${outputDir}...`);
|
|
31
|
+
await replaceVariables(tempDir, outputDir, extractedVariables, answers);
|
|
32
|
+
console.log('Project created successfully!');
|
|
33
|
+
return outputDir;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (fs.existsSync(tempDir)) {
|
|
37
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/esm/prompt.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Inquirerer } from 'inquirerer';
|
|
2
|
+
/**
|
|
3
|
+
* Generate questions from extracted variables
|
|
4
|
+
* @param extractedVariables - Variables extracted from the template
|
|
5
|
+
* @returns Array of questions to prompt the user
|
|
6
|
+
*/
|
|
7
|
+
export function generateQuestions(extractedVariables) {
|
|
8
|
+
const questions = [];
|
|
9
|
+
const askedVariables = new Set();
|
|
10
|
+
if (extractedVariables.projectQuestions) {
|
|
11
|
+
for (const question of extractedVariables.projectQuestions.questions) {
|
|
12
|
+
questions.push(question);
|
|
13
|
+
askedVariables.add(question.name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const replacer of extractedVariables.fileReplacers) {
|
|
17
|
+
if (!askedVariables.has(replacer.variable)) {
|
|
18
|
+
questions.push({
|
|
19
|
+
name: replacer.variable,
|
|
20
|
+
type: 'text',
|
|
21
|
+
message: `Enter value for ${replacer.variable}:`,
|
|
22
|
+
required: true
|
|
23
|
+
});
|
|
24
|
+
askedVariables.add(replacer.variable);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const replacer of extractedVariables.contentReplacers) {
|
|
28
|
+
if (!askedVariables.has(replacer.variable)) {
|
|
29
|
+
questions.push({
|
|
30
|
+
name: replacer.variable,
|
|
31
|
+
type: 'text',
|
|
32
|
+
message: `Enter value for ${replacer.variable}:`,
|
|
33
|
+
required: true
|
|
34
|
+
});
|
|
35
|
+
askedVariables.add(replacer.variable);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return questions;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Prompt the user for variable values
|
|
42
|
+
* @param extractedVariables - Variables extracted from the template
|
|
43
|
+
* @param argv - Command-line arguments to pre-populate answers
|
|
44
|
+
* @param noTty - Whether to disable TTY mode
|
|
45
|
+
* @returns Answers from the user
|
|
46
|
+
*/
|
|
47
|
+
export async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
48
|
+
const questions = generateQuestions(extractedVariables);
|
|
49
|
+
if (questions.length === 0) {
|
|
50
|
+
return argv;
|
|
51
|
+
}
|
|
52
|
+
const prompter = new Inquirerer({
|
|
53
|
+
noTty
|
|
54
|
+
});
|
|
55
|
+
const answers = await prompter.prompt(argv, questions);
|
|
56
|
+
return answers;
|
|
57
|
+
}
|
package/esm/replace.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Transform } from 'stream';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
/**
|
|
6
|
+
* Replace variables in all files in the template directory
|
|
7
|
+
* @param templateDir - Path to the template directory
|
|
8
|
+
* @param outputDir - Path to the output directory
|
|
9
|
+
* @param extractedVariables - Variables extracted from the template
|
|
10
|
+
* @param answers - User answers for variable values
|
|
11
|
+
*/
|
|
12
|
+
export async function replaceVariables(templateDir, outputDir, extractedVariables, answers) {
|
|
13
|
+
if (!fs.existsSync(outputDir)) {
|
|
14
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
await walkAndReplace(templateDir, outputDir, extractedVariables, answers);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Walk through directory and replace variables in files and filenames
|
|
20
|
+
* @param sourceDir - Source directory
|
|
21
|
+
* @param destDir - Destination directory
|
|
22
|
+
* @param extractedVariables - Variables extracted from the template
|
|
23
|
+
* @param answers - User answers for variable values
|
|
24
|
+
* @param sourceRelativePath - Current relative path in source (for recursion)
|
|
25
|
+
* @param destRelativePath - Current relative path in destination (for recursion)
|
|
26
|
+
*/
|
|
27
|
+
async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, sourceRelativePath = '', destRelativePath = '') {
|
|
28
|
+
const currentSource = path.join(sourceDir, sourceRelativePath);
|
|
29
|
+
const entries = fs.readdirSync(currentSource, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const sourceEntryPath = path.join(currentSource, entry.name);
|
|
32
|
+
if (entry.name === '.questions.json' || entry.name === '.questions.js') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let newName = entry.name;
|
|
36
|
+
for (const replacer of extractedVariables.fileReplacers) {
|
|
37
|
+
if (answers[replacer.variable] !== undefined) {
|
|
38
|
+
newName = newName.replace(replacer.pattern, String(answers[replacer.variable]));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const destEntryPath = path.join(destDir, destRelativePath, newName);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
if (!fs.existsSync(destEntryPath)) {
|
|
44
|
+
fs.mkdirSync(destEntryPath, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
await walkAndReplace(sourceDir, destDir, extractedVariables, answers, path.join(sourceRelativePath, entry.name), path.join(destRelativePath, newName));
|
|
47
|
+
}
|
|
48
|
+
else if (entry.isFile()) {
|
|
49
|
+
await replaceInFile(sourceEntryPath, destEntryPath, extractedVariables, answers);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replace variables in a file using streams
|
|
55
|
+
* @param sourcePath - Source file path
|
|
56
|
+
* @param destPath - Destination file path
|
|
57
|
+
* @param extractedVariables - Variables extracted from the template
|
|
58
|
+
* @param answers - User answers for variable values
|
|
59
|
+
*/
|
|
60
|
+
async function replaceInFile(sourcePath, destPath, extractedVariables, answers) {
|
|
61
|
+
const destDir = path.dirname(destPath);
|
|
62
|
+
if (!fs.existsSync(destDir)) {
|
|
63
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
const replaceTransform = new Transform({
|
|
66
|
+
transform(chunk, encoding, callback) {
|
|
67
|
+
let content = chunk.toString();
|
|
68
|
+
for (const replacer of extractedVariables.contentReplacers) {
|
|
69
|
+
if (answers[replacer.variable] !== undefined) {
|
|
70
|
+
content = content.replace(replacer.pattern, String(answers[replacer.variable]));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
callback(null, Buffer.from(content));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
await pipeline(fs.createReadStream(sourcePath), replaceTransform, fs.createWriteStream(destPath));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/extract.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExtractedVariables } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Extract all variables from a template directory
|
|
4
|
+
* @param templateDir - Path to the template directory
|
|
5
|
+
* @returns Extracted variables including file replacers, content replacers, and project questions
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractVariables(templateDir: string): Promise<ExtractedVariables>;
|
package/extract.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
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 (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.extractVariables = void 0;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
/**
|
|
30
|
+
* Pattern to match __VARIABLE__ in filenames and content
|
|
31
|
+
*/
|
|
32
|
+
const VARIABLE_PATTERN = /__([A-Za-z_][A-Za-z0-9_]*)__/g;
|
|
33
|
+
/**
|
|
34
|
+
* Extract all variables from a template directory
|
|
35
|
+
* @param templateDir - Path to the template directory
|
|
36
|
+
* @returns Extracted variables including file replacers, content replacers, and project questions
|
|
37
|
+
*/
|
|
38
|
+
async function extractVariables(templateDir) {
|
|
39
|
+
const fileReplacers = [];
|
|
40
|
+
const contentReplacers = [];
|
|
41
|
+
const fileReplacerVars = new Set();
|
|
42
|
+
const contentReplacerVars = new Set();
|
|
43
|
+
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
44
|
+
await walkDirectory(templateDir, async (filePath) => {
|
|
45
|
+
const relativePath = path.relative(templateDir, filePath);
|
|
46
|
+
if (relativePath === '.questions.json' || relativePath === '.questions.js') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const matches = relativePath.matchAll(VARIABLE_PATTERN);
|
|
50
|
+
for (const match of matches) {
|
|
51
|
+
const varName = match[1];
|
|
52
|
+
if (!fileReplacerVars.has(varName)) {
|
|
53
|
+
fileReplacerVars.add(varName);
|
|
54
|
+
fileReplacers.push({
|
|
55
|
+
variable: varName,
|
|
56
|
+
pattern: new RegExp(`__${varName}__`, 'g')
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const contentVars = await extractFromFileContent(filePath);
|
|
61
|
+
for (const varName of contentVars) {
|
|
62
|
+
if (!contentReplacerVars.has(varName)) {
|
|
63
|
+
contentReplacerVars.add(varName);
|
|
64
|
+
contentReplacers.push({
|
|
65
|
+
variable: varName,
|
|
66
|
+
pattern: new RegExp(`__${varName}__`, 'g')
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
fileReplacers,
|
|
73
|
+
contentReplacers,
|
|
74
|
+
projectQuestions
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
exports.extractVariables = extractVariables;
|
|
78
|
+
/**
|
|
79
|
+
* Extract variables from file content using streams
|
|
80
|
+
* @param filePath - Path to the file
|
|
81
|
+
* @returns Set of variable names found in the file
|
|
82
|
+
*/
|
|
83
|
+
async function extractFromFileContent(filePath) {
|
|
84
|
+
const variables = new Set();
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
87
|
+
let buffer = '';
|
|
88
|
+
stream.on('data', (chunk) => {
|
|
89
|
+
buffer += chunk.toString();
|
|
90
|
+
const lines = buffer.split('\n');
|
|
91
|
+
buffer = lines.pop() || ''; // Keep the last incomplete line in buffer
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
94
|
+
for (const match of matches) {
|
|
95
|
+
variables.add(match[1]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
stream.on('end', () => {
|
|
100
|
+
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
101
|
+
for (const match of matches) {
|
|
102
|
+
variables.add(match[1]);
|
|
103
|
+
}
|
|
104
|
+
resolve(variables);
|
|
105
|
+
});
|
|
106
|
+
stream.on('error', () => {
|
|
107
|
+
resolve(variables);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Walk through a directory recursively
|
|
113
|
+
* @param dir - Directory to walk
|
|
114
|
+
* @param callback - Callback function for each file
|
|
115
|
+
*/
|
|
116
|
+
async function walkDirectory(dir, callback) {
|
|
117
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const fullPath = path.join(dir, entry.name);
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
await walkDirectory(fullPath, callback);
|
|
122
|
+
}
|
|
123
|
+
else if (entry.isFile()) {
|
|
124
|
+
await callback(fullPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Load project questions from .questions.json or .questions.js
|
|
130
|
+
* @param templateDir - Path to the template directory
|
|
131
|
+
* @returns Questions object or null if not found
|
|
132
|
+
*/
|
|
133
|
+
async function loadProjectQuestions(templateDir) {
|
|
134
|
+
const jsonPath = path.join(templateDir, '.questions.json');
|
|
135
|
+
if (fs.existsSync(jsonPath)) {
|
|
136
|
+
try {
|
|
137
|
+
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
138
|
+
const questions = JSON.parse(content);
|
|
139
|
+
return validateQuestions(questions) ? questions : null;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
143
|
+
console.warn(`Failed to parse .questions.json: ${errorMessage}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const jsPath = path.join(templateDir, '.questions.js');
|
|
147
|
+
if (fs.existsSync(jsPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const module = require(jsPath);
|
|
150
|
+
const questions = module.default || module;
|
|
151
|
+
return validateQuestions(questions) ? questions : null;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.warn(`Failed to load .questions.js: ${errorMessage}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Validate that the questions object has the correct structure
|
|
162
|
+
* @param obj - Object to validate
|
|
163
|
+
* @returns True if valid, false otherwise
|
|
164
|
+
*/
|
|
165
|
+
function validateQuestions(obj) {
|
|
166
|
+
return obj && typeof obj === 'object' && Array.isArray(obj.questions);
|
|
167
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CreateGenOptions } from './types';
|
|
2
|
+
export * from './clone';
|
|
3
|
+
export * from './extract';
|
|
4
|
+
export * from './prompt';
|
|
5
|
+
export * from './replace';
|
|
6
|
+
export * from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Create a new project from a template repository
|
|
9
|
+
* @param options - Options for creating the project
|
|
10
|
+
* @returns Path to the generated project
|
|
11
|
+
*/
|
|
12
|
+
export declare function createGen(options: CreateGenOptions): Promise<string>;
|
package/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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 (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createGen = void 0;
|
|
30
|
+
const fs = __importStar(require("fs"));
|
|
31
|
+
const clone_1 = require("./clone");
|
|
32
|
+
const extract_1 = require("./extract");
|
|
33
|
+
const prompt_1 = require("./prompt");
|
|
34
|
+
const replace_1 = require("./replace");
|
|
35
|
+
__exportStar(require("./clone"), exports);
|
|
36
|
+
__exportStar(require("./extract"), exports);
|
|
37
|
+
__exportStar(require("./prompt"), exports);
|
|
38
|
+
__exportStar(require("./replace"), exports);
|
|
39
|
+
__exportStar(require("./types"), exports);
|
|
40
|
+
/**
|
|
41
|
+
* Create a new project from a template repository
|
|
42
|
+
* @param options - Options for creating the project
|
|
43
|
+
* @returns Path to the generated project
|
|
44
|
+
*/
|
|
45
|
+
async function createGen(options) {
|
|
46
|
+
const { templateUrl, outputDir, argv = {}, noTty = false } = options;
|
|
47
|
+
console.log(`Cloning template from ${templateUrl}...`);
|
|
48
|
+
const tempDir = await (0, clone_1.cloneRepo)(templateUrl);
|
|
49
|
+
try {
|
|
50
|
+
console.log('Extracting template variables...');
|
|
51
|
+
const extractedVariables = await (0, extract_1.extractVariables)(tempDir);
|
|
52
|
+
console.log(`Found ${extractedVariables.fileReplacers.length} file replacers`);
|
|
53
|
+
console.log(`Found ${extractedVariables.contentReplacers.length} content replacers`);
|
|
54
|
+
if (extractedVariables.projectQuestions) {
|
|
55
|
+
console.log(`Found ${extractedVariables.projectQuestions.questions.length} project questions`);
|
|
56
|
+
}
|
|
57
|
+
console.log('Prompting for variable values...');
|
|
58
|
+
const answers = await (0, prompt_1.promptUser)(extractedVariables, argv, noTty);
|
|
59
|
+
console.log(`Generating project in ${outputDir}...`);
|
|
60
|
+
await (0, replace_1.replaceVariables)(tempDir, outputDir, extractedVariables, answers);
|
|
61
|
+
console.log('Project created successfully!');
|
|
62
|
+
return outputDir;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (fs.existsSync(tempDir)) {
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.createGen = createGen;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-gen-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
|
+
"description": "Clone and customize template repositories",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "esm/index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"homepage": "https://github.com/pyramation/inquirerer",
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"directory": "dist"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/pyramation/inquirerer"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/pyramation/inquirerer/issues"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"copy": "copyfiles -f ../../LICENSE README.md package.json dist",
|
|
24
|
+
"clean": "del dist/**",
|
|
25
|
+
"prepare": "npm run build",
|
|
26
|
+
"build": "npm run clean; tsc; tsc -p tsconfig.esm.json; npm run copy",
|
|
27
|
+
"dev": "ts-node dev/index",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"test:watch": "jest --watch"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"inquirerer": "^2.1.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [],
|
|
35
|
+
"gitHead": "e1b5c305be94ffa5f7c0664ad2d4e12f66ac046f"
|
|
36
|
+
}
|
package/prompt.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Question } from 'inquirerer';
|
|
2
|
+
import { ExtractedVariables } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Generate questions from extracted variables
|
|
5
|
+
* @param extractedVariables - Variables extracted from the template
|
|
6
|
+
* @returns Array of questions to prompt the user
|
|
7
|
+
*/
|
|
8
|
+
export declare function generateQuestions(extractedVariables: ExtractedVariables): Question[];
|
|
9
|
+
/**
|
|
10
|
+
* Prompt the user for variable values
|
|
11
|
+
* @param extractedVariables - Variables extracted from the template
|
|
12
|
+
* @param argv - Command-line arguments to pre-populate answers
|
|
13
|
+
* @param noTty - Whether to disable TTY mode
|
|
14
|
+
* @returns Answers from the user
|
|
15
|
+
*/
|
|
16
|
+
export declare function promptUser(extractedVariables: ExtractedVariables, argv?: Record<string, any>, noTty?: boolean): Promise<Record<string, any>>;
|
package/prompt.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptUser = exports.generateQuestions = void 0;
|
|
4
|
+
const inquirerer_1 = require("inquirerer");
|
|
5
|
+
/**
|
|
6
|
+
* Generate questions from extracted variables
|
|
7
|
+
* @param extractedVariables - Variables extracted from the template
|
|
8
|
+
* @returns Array of questions to prompt the user
|
|
9
|
+
*/
|
|
10
|
+
function generateQuestions(extractedVariables) {
|
|
11
|
+
const questions = [];
|
|
12
|
+
const askedVariables = new Set();
|
|
13
|
+
if (extractedVariables.projectQuestions) {
|
|
14
|
+
for (const question of extractedVariables.projectQuestions.questions) {
|
|
15
|
+
questions.push(question);
|
|
16
|
+
askedVariables.add(question.name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const replacer of extractedVariables.fileReplacers) {
|
|
20
|
+
if (!askedVariables.has(replacer.variable)) {
|
|
21
|
+
questions.push({
|
|
22
|
+
name: replacer.variable,
|
|
23
|
+
type: 'text',
|
|
24
|
+
message: `Enter value for ${replacer.variable}:`,
|
|
25
|
+
required: true
|
|
26
|
+
});
|
|
27
|
+
askedVariables.add(replacer.variable);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const replacer of extractedVariables.contentReplacers) {
|
|
31
|
+
if (!askedVariables.has(replacer.variable)) {
|
|
32
|
+
questions.push({
|
|
33
|
+
name: replacer.variable,
|
|
34
|
+
type: 'text',
|
|
35
|
+
message: `Enter value for ${replacer.variable}:`,
|
|
36
|
+
required: true
|
|
37
|
+
});
|
|
38
|
+
askedVariables.add(replacer.variable);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return questions;
|
|
42
|
+
}
|
|
43
|
+
exports.generateQuestions = generateQuestions;
|
|
44
|
+
/**
|
|
45
|
+
* Prompt the user for variable values
|
|
46
|
+
* @param extractedVariables - Variables extracted from the template
|
|
47
|
+
* @param argv - Command-line arguments to pre-populate answers
|
|
48
|
+
* @param noTty - Whether to disable TTY mode
|
|
49
|
+
* @returns Answers from the user
|
|
50
|
+
*/
|
|
51
|
+
async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
52
|
+
const questions = generateQuestions(extractedVariables);
|
|
53
|
+
if (questions.length === 0) {
|
|
54
|
+
return argv;
|
|
55
|
+
}
|
|
56
|
+
const prompter = new inquirerer_1.Inquirerer({
|
|
57
|
+
noTty
|
|
58
|
+
});
|
|
59
|
+
const answers = await prompter.prompt(argv, questions);
|
|
60
|
+
return answers;
|
|
61
|
+
}
|
|
62
|
+
exports.promptUser = promptUser;
|
package/replace.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExtractedVariables } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Replace variables in all files in the template directory
|
|
4
|
+
* @param templateDir - Path to the template directory
|
|
5
|
+
* @param outputDir - Path to the output directory
|
|
6
|
+
* @param extractedVariables - Variables extracted from the template
|
|
7
|
+
* @param answers - User answers for variable values
|
|
8
|
+
*/
|
|
9
|
+
export declare function replaceVariables(templateDir: string, outputDir: string, extractedVariables: ExtractedVariables, answers: Record<string, any>): Promise<void>;
|
package/replace.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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 (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.replaceVariables = void 0;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const stream_1 = require("stream");
|
|
30
|
+
const promises_1 = require("stream/promises");
|
|
31
|
+
/**
|
|
32
|
+
* Replace variables in all files in the template directory
|
|
33
|
+
* @param templateDir - Path to the template directory
|
|
34
|
+
* @param outputDir - Path to the output directory
|
|
35
|
+
* @param extractedVariables - Variables extracted from the template
|
|
36
|
+
* @param answers - User answers for variable values
|
|
37
|
+
*/
|
|
38
|
+
async function replaceVariables(templateDir, outputDir, extractedVariables, answers) {
|
|
39
|
+
if (!fs.existsSync(outputDir)) {
|
|
40
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
await walkAndReplace(templateDir, outputDir, extractedVariables, answers);
|
|
43
|
+
}
|
|
44
|
+
exports.replaceVariables = replaceVariables;
|
|
45
|
+
/**
|
|
46
|
+
* Walk through directory and replace variables in files and filenames
|
|
47
|
+
* @param sourceDir - Source directory
|
|
48
|
+
* @param destDir - Destination directory
|
|
49
|
+
* @param extractedVariables - Variables extracted from the template
|
|
50
|
+
* @param answers - User answers for variable values
|
|
51
|
+
* @param sourceRelativePath - Current relative path in source (for recursion)
|
|
52
|
+
* @param destRelativePath - Current relative path in destination (for recursion)
|
|
53
|
+
*/
|
|
54
|
+
async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, sourceRelativePath = '', destRelativePath = '') {
|
|
55
|
+
const currentSource = path.join(sourceDir, sourceRelativePath);
|
|
56
|
+
const entries = fs.readdirSync(currentSource, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const sourceEntryPath = path.join(currentSource, entry.name);
|
|
59
|
+
if (entry.name === '.questions.json' || entry.name === '.questions.js') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let newName = entry.name;
|
|
63
|
+
for (const replacer of extractedVariables.fileReplacers) {
|
|
64
|
+
if (answers[replacer.variable] !== undefined) {
|
|
65
|
+
newName = newName.replace(replacer.pattern, String(answers[replacer.variable]));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const destEntryPath = path.join(destDir, destRelativePath, newName);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
if (!fs.existsSync(destEntryPath)) {
|
|
71
|
+
fs.mkdirSync(destEntryPath, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
await walkAndReplace(sourceDir, destDir, extractedVariables, answers, path.join(sourceRelativePath, entry.name), path.join(destRelativePath, newName));
|
|
74
|
+
}
|
|
75
|
+
else if (entry.isFile()) {
|
|
76
|
+
await replaceInFile(sourceEntryPath, destEntryPath, extractedVariables, answers);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Replace variables in a file using streams
|
|
82
|
+
* @param sourcePath - Source file path
|
|
83
|
+
* @param destPath - Destination file path
|
|
84
|
+
* @param extractedVariables - Variables extracted from the template
|
|
85
|
+
* @param answers - User answers for variable values
|
|
86
|
+
*/
|
|
87
|
+
async function replaceInFile(sourcePath, destPath, extractedVariables, answers) {
|
|
88
|
+
const destDir = path.dirname(destPath);
|
|
89
|
+
if (!fs.existsSync(destDir)) {
|
|
90
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
const replaceTransform = new stream_1.Transform({
|
|
93
|
+
transform(chunk, encoding, callback) {
|
|
94
|
+
let content = chunk.toString();
|
|
95
|
+
for (const replacer of extractedVariables.contentReplacers) {
|
|
96
|
+
if (answers[replacer.variable] !== undefined) {
|
|
97
|
+
content = content.replace(replacer.pattern, String(answers[replacer.variable]));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
callback(null, Buffer.from(content));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
await (0, promises_1.pipeline)(fs.createReadStream(sourcePath), replaceTransform, fs.createWriteStream(destPath));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Question } from 'inquirerer';
|
|
2
|
+
/**
|
|
3
|
+
* Questions configuration that can be loaded from .questions.json or .questions.js
|
|
4
|
+
* @typedef {Object} Questions
|
|
5
|
+
* @property {Question[]} questions - Array of inquirerer questions
|
|
6
|
+
*/
|
|
7
|
+
export interface Questions {
|
|
8
|
+
questions: Question[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Variable extracted from filename patterns like __VARIABLE__
|
|
12
|
+
*/
|
|
13
|
+
export interface FileReplacer {
|
|
14
|
+
variable: string;
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Variable extracted from file content patterns like __VARIABLE__
|
|
19
|
+
*/
|
|
20
|
+
export interface ContentReplacer {
|
|
21
|
+
variable: string;
|
|
22
|
+
pattern: RegExp;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating a new project from a template
|
|
26
|
+
*/
|
|
27
|
+
export interface CreateGenOptions {
|
|
28
|
+
/**
|
|
29
|
+
* URL or path to the template repository
|
|
30
|
+
*/
|
|
31
|
+
templateUrl: string;
|
|
32
|
+
/**
|
|
33
|
+
* Destination directory for the generated project
|
|
34
|
+
*/
|
|
35
|
+
outputDir: string;
|
|
36
|
+
/**
|
|
37
|
+
* Command-line arguments to pre-populate answers
|
|
38
|
+
*/
|
|
39
|
+
argv?: Record<string, any>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to use TTY for interactive prompts
|
|
42
|
+
*/
|
|
43
|
+
noTty?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result of extracting variables from a template
|
|
47
|
+
*/
|
|
48
|
+
export interface ExtractedVariables {
|
|
49
|
+
fileReplacers: FileReplacer[];
|
|
50
|
+
contentReplacers: ContentReplacer[];
|
|
51
|
+
projectQuestions: Questions | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Context for processing a template
|
|
55
|
+
*/
|
|
56
|
+
export interface TemplateContext {
|
|
57
|
+
tempDir: string;
|
|
58
|
+
extractedVariables: ExtractedVariables;
|
|
59
|
+
answers: Record<string, any>;
|
|
60
|
+
}
|
package/types.js
ADDED